-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Detect listener pod drift by comparing the pod, not a hash #4635
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| package actionsgithubcom | ||
|
|
||
| import ( | ||
| corev1 "k8s.io/api/core/v1" | ||
| apiequality "k8s.io/apimachinery/pkg/api/equality" | ||
| ) | ||
|
|
||
| // listenerPodSpecRequiresRecreation reports whether the live listener pod must be | ||
| // deleted and rebuilt to match the desired spec. | ||
| // | ||
| // The config secret is checked first. The pod mounts it as a volume and the | ||
| // listener parses it once at startup, so a change to the scale set URL, the TLS | ||
| // certificate, the metrics configuration or the scaler tuning only reaches the | ||
| // listener after a restart. None of that is visible in the pod spec, which | ||
| // references the secret by name and is byte-identical before and after, so the | ||
| // desired pod carries the secret's resource version as an annotation and drift | ||
| // is detected by comparing it. | ||
| // | ||
| // A pod that predates this annotation counts as drift and is recreated once, on | ||
| // the first reconcile after the controller is upgraded. Ignoring it instead is | ||
| // tempting, to avoid that rollout, but it loses updates: the reconcile that | ||
| // declines to recreate the pod goes on to patch the desired annotations onto it, | ||
| // so a config change that landed while the old controller was running is | ||
| // recorded as already applied and the listener keeps serving the configuration | ||
| // it parsed at startup, with nothing to ever correct it. The rollout is cheap by | ||
| // comparison - deleting a listener does not disturb the runners it started, and | ||
| // an upgrade normally changes the listener image and so recreates the pod | ||
| // anyway. | ||
| // | ||
| // The resource version also moves when only the secret's labels or annotations | ||
| // change, which does not affect the listener. That is accepted rather than | ||
| // worked around by hashing the secret data: those fields come from the | ||
| // AutoscalingListener spec, and a change to that spec already makes the | ||
| // AutoscalingRunnerSet controller replace the listener wholesale. | ||
| // | ||
| // DeepDerivative, not DeepEqual: the live pod carries a large number of fields | ||
| // the desired pod never sets, written by the API server and by admission | ||
| // (nodeName, dnsPolicy, schedulerName, securityContext, enableServiceLinks, | ||
| // the default tolerations, the kube-api-access-* projected volume and its mount, | ||
| // terminationMessagePath, imagePullPolicy, secret defaultMode, ...). DeepEqual | ||
| // would therefore report drift on every reconcile of every healthy listener and | ||
| // spin in a delete/create loop. It cannot be made to work by pre-populating the | ||
| // defaults either, since nodeName is scheduler-assigned and the access-token | ||
| // volume has a generated name. See TestListenerPodSpecRequiresRecreation. | ||
| // | ||
| // The cost of DeepDerivative is that it ignores empty values on the desired side, | ||
| // so a field being *removed* is invisible to it. For everything sourced from the | ||
| // user-facing template that is harmless: the AutoscalingRunnerSet controller | ||
| // compares the whole AutoscalingListener spec with cmp.Equal and deletes the | ||
| // listener outright, which takes the pod with it. Container ports are the | ||
| // exception, because they come from the --listener-metrics-addr controller flag | ||
| // rather than from any resource, so disabling metrics would otherwise leave the | ||
| // port on the pod forever. Comparing port length is enough: additions and value | ||
| // changes are already caught by DeepDerivative, only removal is blind. The | ||
| // contents are deliberately not compared, because the API server defaults | ||
| // protocol to TCP and that would reintroduce the delete/create loop. | ||
| func listenerPodSpecRequiresRecreation(current, desired *corev1.Pod) bool { | ||
| if current == nil || desired == nil { | ||
| return current != desired | ||
| } | ||
|
|
||
| if listenerConfigChanged(current, desired) { | ||
| return true | ||
| } | ||
|
|
||
| if listenerContainerPortsRemoved(current, desired) { | ||
| return true | ||
| } | ||
|
|
||
| return !apiequality.Semantic.DeepDerivative(desired.Spec, current.Spec) | ||
| } | ||
|
|
||
| func listenerConfigChanged(current, desired *corev1.Pod) bool { | ||
| desiredVersion := desired.Annotations[AnnotationKeyListenerConfigResourceVersion] | ||
| if desiredVersion == "" { | ||
| // Nothing to compare against, so there is nothing this check can say. | ||
| // The desired pod is always built from a secret read back from the API | ||
| // server, so this only happens in tests. | ||
| return false | ||
| } | ||
|
|
||
| return current.Annotations[AnnotationKeyListenerConfigResourceVersion] != desiredVersion | ||
| } | ||
|
|
||
| func listenerContainerPortsRemoved(current, desired *corev1.Pod) bool { | ||
| for i := range desired.Spec.Containers { | ||
| desiredContainer := &desired.Spec.Containers[i] | ||
| currentContainer := findContainerByName(current.Spec.Containers, desiredContainer.Name) | ||
| if currentContainer == nil { | ||
| // A container the live pod does not have at all is drift that | ||
| // DeepDerivative already reports; nothing to decide here. | ||
| continue | ||
| } | ||
| if len(desiredContainer.Ports) < len(currentContainer.Ports) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func findContainerByName(containers []corev1.Container, name string) *corev1.Container { | ||
| for i := range containers { | ||
| if containers[i].Name == name { | ||
| return &containers[i] | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package actionsgithubcom | ||
|
|
||
| import ( | ||
| "testing" | ||
| ) | ||
|
|
||
| // BenchmarkListenerPodSpecRequiresRecreation measures the listener pod drift | ||
| // check, which also runs on every AutoscalingListener reconcile. | ||
| func BenchmarkListenerPodSpecRequiresRecreation(b *testing.B) { | ||
| desired := desiredListenerPod() | ||
| live := livePodFromDesired(desired) | ||
| b.ReportAllocs() | ||
| b.ResetTimer() | ||
| for range b.N { | ||
| if listenerPodSpecRequiresRecreation(live, desired) { | ||
| b.Fatal("expected no recreation") | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.