-
Notifications
You must be signed in to change notification settings - Fork 277
fix: restore node-level DNS after unexpected LocalDNS exit #9360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8abba5b
43379ba
8273e13
a0ee237
b6cba8f
a3c0874
6eb258a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,8 @@ | ||||||||||||||||||||
| package scenario | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import ( | ||||||||||||||||||||
| "context" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| aksnodeconfigv1 "github.com/Azure/agentbaker/aks-node-controller/pkg/gen/aksnodeconfig/v1" | ||||||||||||||||||||
| "github.com/Azure/agentbaker/e2e/config" | ||||||||||||||||||||
| "github.com/Azure/agentbaker/pkg/agent/datamodel" | ||||||||||||||||||||
|
|
@@ -23,6 +25,7 @@ func init() { | |||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| for _, tt := range tests { | ||||||||||||||||||||
| tt := tt | ||||||||||||||||||||
| cluster := ClusterKubenet | ||||||||||||||||||||
| if tt.name == "Ubuntu2604Minimal" { | ||||||||||||||||||||
| cluster = ClusterLatestKubernetesVersionKubenet | ||||||||||||||||||||
|
|
@@ -42,7 +45,191 @@ func init() { | |||||||||||||||||||
| config.LocalDnsProfile.EnableLocalDns = true | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| VMConfigMutator: tt.vmConfigMutator, | ||||||||||||||||||||
| Validator: func(ctx context.Context, s *Scenario) error { | ||||||||||||||||||||
| // Validate the full LocalDNS service lifecycle (including the | ||||||||||||||||||||
| // unexpected-exit DNS teardown this PR fixes) on the target | ||||||||||||||||||||
| // distros. The hosts-plugin functionality itself is covered by | ||||||||||||||||||||
| // the scenario's default provisioning validation. | ||||||||||||||||||||
| if tt.name == "Ubuntu2204" || tt.name == "Ubuntu2404" || tt.name == "AzureLinuxV3" { | ||||||||||||||||||||
| return validateLocalDNSLifecycle(ctx, s) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return nil | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| func validateLocalDNSLifecycle(ctx context.Context, s *Scenario) error { | ||||||||||||||||||||
| _, err := execScriptOnVMForScenarioValidateExitCode(ctx, s, ` | ||||||||||||||||||||
| set -eu | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # This validation requires the ExecStopPost hook baked into the branch VHD. | ||||||||||||||||||||
| # Standalone E2E may run against an older published VHD, where this behavior | ||||||||||||||||||||
| # is unavailable and should be skipped rather than reported as a false failure. | ||||||||||||||||||||
| if ! sudo systemctl show localdns.service -p ExecStopPost --value | grep -q 'localdns.sh cleanup'; then | ||||||||||||||||||||
| echo "SKIP: VHD predates the ExecStopPost cleanup hook" | ||||||||||||||||||||
| exit 0 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
|
|
||||||||||||||||||||
| NORESTART=/run/systemd/system/localdns.service.d/99-e2e-no-restart.conf | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Install cleanup before any service mutation so set -e cannot leave the node | ||||||||||||||||||||
| # with the temporary Restart=no override or a failed LocalDNS unit. | ||||||||||||||||||||
| restore_localdns_test_state() { | ||||||||||||||||||||
| test_status=$? | ||||||||||||||||||||
| trap - EXIT | ||||||||||||||||||||
| set +e | ||||||||||||||||||||
| cleanup_status=0 | ||||||||||||||||||||
| if [ -f "$NORESTART" ]; then | ||||||||||||||||||||
| sudo rm -f "$NORESTART" || { echo "ERROR: failed to remove $NORESTART"; cleanup_status=1; } | ||||||||||||||||||||
| sudo systemctl daemon-reload || { echo "ERROR: systemd daemon-reload failed during test cleanup"; cleanup_status=1; } | ||||||||||||||||||||
| sudo systemctl reset-failed localdns.service || { echo "ERROR: reset-failed localdns.service failed during test cleanup"; cleanup_status=1; } | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| if ! sudo systemctl is-active --quiet localdns.service; then | ||||||||||||||||||||
| sudo systemctl start localdns.service || { echo "ERROR: failed to restart localdns.service during test cleanup"; cleanup_status=1; } | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| if ! sudo systemctl is-active --quiet localdns.service; then | ||||||||||||||||||||
| echo "ERROR: localdns.service is not active after test cleanup" | ||||||||||||||||||||
| cleanup_status=1 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| if [ "$test_status" -eq 0 ] && [ "$cleanup_status" -ne 0 ]; then | ||||||||||||||||||||
| test_status=$cleanup_status | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| exit "$test_status" | ||||||||||||||||||||
| } | ||||||||||||||||||||
| trap restore_localdns_test_state EXIT | ||||||||||||||||||||
|
|
||||||||||||||||||||
| sudo systemctl is-active --quiet localdns.service | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Normal systemd stop must complete cleanup and return success. | ||||||||||||||||||||
| sudo systemctl restart localdns.service | ||||||||||||||||||||
| sudo systemctl is-active --quiet localdns.service | ||||||||||||||||||||
| sudo systemctl stop localdns.service | ||||||||||||||||||||
| test "$(sudo systemctl show localdns.service -p ActiveState --value)" = inactive | ||||||||||||||||||||
| sudo systemctl start localdns.service | ||||||||||||||||||||
| sudo systemctl is-active --quiet localdns.service | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Repeatedly kill the supervisor and wait for Restart=on-failure recovery. | ||||||||||||||||||||
|
saewoni marked this conversation as resolved.
|
||||||||||||||||||||
| # This loop validates ordinary service recovery; the terminal dead-service | ||||||||||||||||||||
| # regression for ExecStopPost is covered by the block below. | ||||||||||||||||||||
| # Require a genuinely new MainPID after each kill: immediately after kill -9, | ||||||||||||||||||||
| # systemd may still report the killed invocation as active/running until it | ||||||||||||||||||||
| # processes SIGCHLD, so checking active/running alone can observe the old | ||||||||||||||||||||
| # process and falsely declare recovery. Save the killed PID and require the | ||||||||||||||||||||
| # new MainPID to be nonzero and different from it. | ||||||||||||||||||||
| test_start=$(date +%s) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| for i in 1 2 3; do | ||||||||||||||||||||
|
saewoni marked this conversation as resolved.
|
||||||||||||||||||||
| killed=$(sudo systemctl show -p MainPID --value localdns.service) | ||||||||||||||||||||
| test "$killed" -gt 0 | ||||||||||||||||||||
| sudo kill -9 "$killed" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| recovered=false | ||||||||||||||||||||
| for attempt in 1 2 3 4 5 6 7 8 9 10 11 12; do | ||||||||||||||||||||
| state=$(sudo systemctl show localdns.service -p ActiveState -p SubState --value) | ||||||||||||||||||||
| main=$(sudo systemctl show -p MainPID --value localdns.service) | ||||||||||||||||||||
| if [ "$state" = $'active\nrunning' ] && [ "$main" -gt 0 ] && [ "$main" != "$killed" ]; then | ||||||||||||||||||||
| recovered=true | ||||||||||||||||||||
| break | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| sleep 1 | ||||||||||||||||||||
| done | ||||||||||||||||||||
| test "$recovered" = true | ||||||||||||||||||||
| done | ||||||||||||||||||||
|
|
||||||||||||||||||||
| state=$(sudo systemctl show localdns.service -p ActiveState -p SubState -p Result -p ControlGroup) | ||||||||||||||||||||
| printf '%s\n' "$state" | ||||||||||||||||||||
| printf '%s\n' "$state" | grep -q '^ActiveState=active$' | ||||||||||||||||||||
| printf '%s\n' "$state" | grep -q '^SubState=running$' | ||||||||||||||||||||
| printf '%s\n' "$state" | grep -q '^Result=success$' | ||||||||||||||||||||
| # The cgroup teardown warning is diagnostic only: fixing it is out of scope for | ||||||||||||||||||||
| # this PR (which is about restoring node DNS after an unexpected exit), so we | ||||||||||||||||||||
| # surface it but do not fail on it. | ||||||||||||||||||||
| if sudo journalctl -u localdns.service --since "@$test_start" --no-pager | grep -q 'Failed to kill control group'; then | ||||||||||||||||||||
| echo "WARNING: LocalDNS cgroup teardown warning observed" | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| if sudo journalctl -u localdns.service --since "@$test_start" --no-pager | grep -q 'Start request repeated too quickly'; then | ||||||||||||||||||||
| echo "LocalDNS reached systemd StartLimit" | ||||||||||||||||||||
| exit 1 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| dig +short +time=5 +tries=1 mcr.microsoft.com @169.254.10.10 | grep -q . | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Terminal dead-service case: this is the incident scenario the PR fixes. | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. blocker. that's exactly what's happening right now: build 180533805 (the required check on this PR) failed all three lifecycle scenarios, while the branch-VHD run passed. merge this and the required check goes red on every PR in the repo until a main VHD carrying gate the block on the unit actually having the hook:
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 5fc8605. The lifecycle validator now checks for the baked ExecStopPost hook before asserting the new behavior and skips with a diagnostic on published VHDs that predate the hook. The branch-VHD E2E remains the authoritative validation for the new lifecycle behavior.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the gate works, but it sits at the top of the validator ( move the check down to guard only the terminal block at if sudo systemctl show localdns.service -p ExecStopPost --value | grep -q 'localdns.sh cleanup'; then
# terminal dead-service case ... (current :158-229)
else
echo "SKIP: VHD predates the ExecStopPost cleanup hook"
fithe EXIT trap at |
||||||||||||||||||||
| # When localdns ends up dead (systemd exhausts restart attempts), ExecStopPost | ||||||||||||||||||||
| # must still revert node DNS so the node does not keep pointing at the dead | ||||||||||||||||||||
| # localdns listener (169.254.10.10). We reach the dead state deterministically | ||||||||||||||||||||
| # by disabling auto-restart with a transient drop-in, then killing the | ||||||||||||||||||||
| # supervisor -- tripping StartLimit via rapid kills is timing dependent and | ||||||||||||||||||||
| # flaky. ExecStopPost runs on the SIGKILL path regardless of Restart=. | ||||||||||||||||||||
| sudo mkdir -p "$(dirname "$NORESTART")" | ||||||||||||||||||||
| printf '[Service]\nRestart=no\n' | sudo tee "$NORESTART" >/dev/null | ||||||||||||||||||||
| sudo systemctl daemon-reload | ||||||||||||||||||||
|
|
||||||||||||||||||||
| dead_main=$(sudo systemctl show -p MainPID --value localdns.service) | ||||||||||||||||||||
| test "$dead_main" -gt 0 | ||||||||||||||||||||
| sudo kill -9 "$dead_main" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Wait for the service to reach a terminal ActiveState (failed or inactive). | ||||||||||||||||||||
| # A non-running SubState is not sufficient: SubState passes through transitional | ||||||||||||||||||||
| # values such as stop-post while ExecStopPost is still running the cleanup under | ||||||||||||||||||||
| # test, so asserting on drop-in removal then could race the cleanup. ActiveState | ||||||||||||||||||||
| # only becomes failed/inactive after ExecStopPost has completed. | ||||||||||||||||||||
| dead=false | ||||||||||||||||||||
| for attempt in 1 2 3 4 5 6 7 8 9 10 11 12; do | ||||||||||||||||||||
| active_state=$(sudo systemctl show localdns.service -p ActiveState --value) | ||||||||||||||||||||
| if [ "$active_state" = failed ] || [ "$active_state" = inactive ]; then | ||||||||||||||||||||
| dead=true | ||||||||||||||||||||
| break | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| sleep 1 | ||||||||||||||||||||
| done | ||||||||||||||||||||
| test "$dead" = true | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # The localdns network drop-in must have been removed by ExecStopPost. This is | ||||||||||||||||||||
| # the authoritative signal that DNS was reverted: the drop-in is what points the | ||||||||||||||||||||
| # link's DNS at the localdns listener. | ||||||||||||||||||||
| if ls /run/systemd/network/*.d/70-localdns.conf >/dev/null 2>&1; then | ||||||||||||||||||||
| echo "FAIL: 70-localdns.conf still present after localdns died" | ||||||||||||||||||||
| exit 1 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # The live link DNS must no longer include the localdns node listener. This is | ||||||||||||||||||||
| # eventually consistent: networkctl reload propagates to systemd-resolved | ||||||||||||||||||||
| # asynchronously, so poll (like wait_for_localdns_removed_from_resolv_conf does) | ||||||||||||||||||||
| # until the listener IP is gone rather than checking once. Prefer resolvectl | ||||||||||||||||||||
| # (the per-link view the drop-in configures); fall back to the resolved stub. | ||||||||||||||||||||
| # Only accept a successful, non-empty resolver snapshot: an errored or empty | ||||||||||||||||||||
| # read must not be treated as "restored", or a failed read would mask the very | ||||||||||||||||||||
| # regression under test. Retry those instead. | ||||||||||||||||||||
| dns_reverted=false | ||||||||||||||||||||
| for attempt in 1 2 3 4 5 6 7 8 9 10 11 12; do | ||||||||||||||||||||
| if command -v resolvectl >/dev/null 2>&1; then | ||||||||||||||||||||
| current_dns=$(resolvectl status 2>/dev/null) || current_dns="" | ||||||||||||||||||||
| else | ||||||||||||||||||||
| current_dns=$(cat /run/systemd/resolve/resolv.conf 2>/dev/null) || current_dns="" | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| # Require a non-empty snapshot before trusting the absence check. | ||||||||||||||||||||
| if [ -n "$current_dns" ] && ! printf '%s' "$current_dns" | grep -q '169\.254\.10\.10'; then | ||||||||||||||||||||
| dns_reverted=true | ||||||||||||||||||||
| break | ||||||||||||||||||||
| fi | ||||||||||||||||||||
| sleep 1 | ||||||||||||||||||||
| done | ||||||||||||||||||||
| if [ "$dns_reverted" != true ]; then | ||||||||||||||||||||
| echo "FAIL: link DNS still points at 169.254.10.10 (or resolver state unreadable) after localdns died" | ||||||||||||||||||||
| exit 1 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
|
saewoni marked this conversation as resolved.
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| # Removing the LocalDNS address is not sufficient: verify the node has a | ||||||||||||||||||||
| # working resolver after cleanup. | ||||||||||||||||||||
| if ! getent hosts mcr.microsoft.com >/dev/null 2>&1; then | ||||||||||||||||||||
| echo "FAIL: node cannot resolve DNS after localdns died" | ||||||||||||||||||||
| exit 1 | ||||||||||||||||||||
| fi | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # The EXIT trap removes the temporary override and restores LocalDNS even if | ||||||||||||||||||||
| # an assertion above exits the validation early. | ||||||||||||||||||||
| `, 0, "LocalDNS lifecycle validation failed") | ||||||||||||||||||||
| return err | ||||||||||||||||||||
| } | ||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.