Skip to content

controller: trigger extproc pod rollout on injected-config drift - #2427

Open
Aias00 wants to merge 9 commits into
envoyproxy:mainfrom
Aias00:fix/extproc-config-drift-rollout-2395
Open

controller: trigger extproc pod rollout on injected-config drift#2427
Aias00 wants to merge 9 commits into
envoyproxy:mainfrom
Aias00:fix/extproc-config-drift-rollout-2395

Conversation

@Aias00

@Aias00 Aias00 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Description

The mutating webhook injects the extproc container config only at pod creation time, but the previous drift check only compared the container image, -logLevel, and -mcpAddr presence. Restarting the controller with changed extproc settings, or editing GatewayConfig.spec.extProc.kubernetes, could leave existing pods running stale injected config until they were recreated for some unrelated reason.

This PR computes a stable desired hash from the extproc injection inputs and writes it to the Deployment/DaemonSet pod template annotation aigateway.envoyproxy.io/extproc-config-hash. Kubernetes then owns the rollout whenever injected extproc config changes.

Upgrade behavior: workloads created before this annotation existed will not have the desired hash on their pod template. On the next Gateway reconcile, Deployments/DaemonSets with effective routes get the template hash annotation and Kubernetes performs one rollout to converge pods onto the current injected extproc config. After that first convergence rollout, reconciles are no-op unless the desired hash changes again.

The existing direct pod UUID annotation path remains for filter-config Secret refresh because it speeds projected-volume updates without restarting pods. Workload pod template patching is limited to cases where pod template state must change: sidecar add/remove, inconsistent pod state self-heal, or extproc config hash drift.

To avoid rollout loops from Kubernetes API shape/defaulting changes, the hash serializes an explicit digest of the inputs that affect extproc injection instead of hashing a full corev1.Container. The hash covers controller flags, GatewayConfig extproc settings, image pull secrets, and MCP input. It intentionally excludes secret-presence-driven -configPath / -configBundlePath args and legacy/bundle volume mounts.

Related Issues/PRs (if applicable)

Fixes #2395

Special notes for reviewers (if applicable)

  • The shared builder in internal/controller/extproc_builder.go is the single source of truth for the injected container and the desired template hash.
  • GatewayConfig.spec.extProc.kubernetes.image / imageRepository are used when checking whether an existing extproc sidecar is current.
  • The reconciler uses the cached route-list path; the no-cache fallback remains only on the admission webhook path.
  • Tests cover deterministic hashing, drift-relevant inputs, GatewayConfig-resolved image checks, and Deployment/DaemonSet template hash rollout behavior.

@Aias00
Aias00 requested a review from a team as a code owner July 27, 2026 02:44
Copilot AI review requested due to automatic review settings July 27, 2026 02:44
@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Jul 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov-commenter

codecov-commenter commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.69118% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.29%. Comparing base (25afc17) to head (7fcb556).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/controller/gateway.go 90.90% 2 Missing and 2 partials ⚠️
internal/controller/gateway_mutator.go 89.28% 0 Missing and 3 partials ⚠️
internal/controller/extproc_builder.go 99.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2427      +/-   ##
==========================================
+ Coverage   85.05%   85.29%   +0.24%     
==========================================
  Files         159      160       +1     
  Lines       22645    22704      +59     
==========================================
+ Hits        19261    19366     +105     
+ Misses       2217     2180      -37     
+ Partials     1167     1158       -9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Aias00
Aias00 force-pushed the fix/extproc-config-drift-rollout-2395 branch from 2c15b86 to 7211191 Compare July 27, 2026 03:07
@nacx

nacx commented Jul 30, 2026

Copy link
Copy Markdown
Member

@kanurag94 can you take a look?

Comment thread internal/controller/gateway.go Outdated
for i := range podSpec.InitContainers {
// If there's an extproc sidecar container with the current target image, we don't need to roll out the deployment.
if podSpec.InitContainers[i].Name == extProcContainerName && podSpec.InitContainers[i].Image == c.extProcImage {
if podSpec.InitContainers[i].Name == extProcContainerName && podSpec.InitContainers[i].Image == c.image {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this PR already does resolveExtProcImage, we can fix this and instead of using the global extproc image, use the one from gatewayConfig GatewayConfig.spec.extProc.kubernetes.image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6284456. The Gateway reconciler now resolves the desired extproc image from the same extProcContainerInput used for hashing, so GatewayConfig.spec.extProc.kubernetes.image / imageRepository are honored when checking whether the existing sidecar is current.

Added regression coverage for the GatewayConfig-resolved image path.

require.Equal(t, h1, h2, "hash must be stable across recomputes")
})
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test will be more useful if it really mutates a pod against a fake client and validates it. Currently it just is a UT to check hash calculation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f3e96d2. Added GatewayController coverage that creates pods and deployments in the fake Kubernetes client, runs the rollout path, and validates the deployment template annotation is patched when the extproc config hash drifts.

The smaller hash unit test remains to pin deterministic hash behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find the change. Can you please point me?
We need something to mutate the pod and calculate hash

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this directly to the mutator fake-client test. TestGatewayMutator_mutatePod_UsesNoCacheReader now calls mutatePod and verifies the pod's extproc-config-hash annotation equals
g.extProcContainerHash(extProcContainerInput{}).

Comment thread internal/controller/gateway.go Outdated
Comment on lines 105 to 107

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it differs from gateway_mutator.go. This only uses the cached index while webhook uses the fallback. There can be a small window where cache is not updated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17673ee. The Gateway reconciler now gets mgr.GetAPIReader() and uses the same cache-index + no-cache fallback route lookup as the mutating webhook for both AIGatewayRoute and MCPRoute. Added controller tests for the cache-empty/API-reader-has-route window.

Verified with:

  • go test ./internal/controller -run 'TestGatewayController_listAIGatewayRoutesForGateway_NoCacheReaderFallback|TestGatewayController_listMCPRoutesForGateway_NoCacheReaderFallback|TestGatewayMutator_listAIGatewayRoutesForGateway_NoCacheReaderFallback|TestGatewayMutator_listMCPRoutesForGateway_NoCacheReaderFallback|TestGatewayController_annotateGatewayPods_ConfigHashDrift'
  • go test ./internal/controller

Comment thread internal/controller/extproc_builder.go Outdated
// signal can never diverge from the injected config.
func (b *extProcBuilder) extProcContainerHash(input extProcContainerInput) string {
digest := extProcConfigDigest{
Container: b.buildExtProcContainer(input),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when k8s api changes for some reason? This rolls out all the pods. Maybe we could hash the actual inputs. WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6284456. The drift hash now serializes an explicit extProcConfigDigest of the inputs that affect extproc injection instead of serializing the full corev1.Container object. That keeps Kubernetes Container API shape/defaulting changes from changing the rollout signal, while controller flags, GatewayConfig extproc settings, image pull secrets, and MCP input still move the hash.

Verified with go test ./internal/controller.

@kanurag94 kanurag94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is close, but we need to ensure this doesn't break anything. The comments are too verbose right now. If possible, can we make them just what and why.

Alternative to the approach in this PR, wdyt about just patching pod annotations through deployment so kubernetes takes care of rollout itself?

@Aias00

Aias00 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review summary in 6284456: comments are trimmed to what/why, the hash no longer serializes the full Kubernetes Container API object, and GatewayConfig-resolved extproc images are used when checking existing sidecars.

On rollout behavior: I kept direct pod annotation patching for ordinary filter-config Secret updates because it speeds projected-volume refresh without restarting pods. Deployment/DaemonSet template patching is still limited to cases where the pod template must change: missing sidecar, sidecar removal, inconsistent pod state, or extproc config drift.

Verified with go test ./internal/controller.

Comment thread internal/controller/gateway.go Outdated
Comment on lines +1063 to +1071
// checkPodHasSideCar checks the extproc container against the controller-global
// image. It is kept for tests and legacy callers without GatewayConfig input.
func (c *GatewayController) checkPodHasSideCar(pod *corev1.Pod, needMCP bool, desiredHash string) (hasSideCar, needsRollout bool) {
return c.checkPodHasSideCarWithImage(pod, needMCP, c.image, desiredHash)
}

// checkPodHasSideCarWithImage returns whether the extproc container is current,
// and whether a stamped config hash mismatch requires a rollout.
func (c *GatewayController) checkPodHasSideCarWithImage(pod *corev1.Pod, needMCP bool, desiredImage, desiredHash string) (hasSideCar, needsRollout bool) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary duplicated I think. Is there a reason for it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the duplicate wrapper. checkPodHasSideCar now takes the desired image directly

Comment on lines 1153 to +1178
@@ -1164,38 +1169,54 @@
uuid string,
hasEffectiveRoute bool,
needMCP bool,
desiredHash string,
) (ctrl.Result, error) {
return c.annotateGatewayPodsWithDesiredImage(ctx, pods, deployments, daemonSets, uuid, hasEffectiveRoute, needMCP, c.image, desiredHash)
}

func (c *GatewayController) annotateGatewayPodsWithDesiredImage(ctx context.Context,
pods []corev1.Pod,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this is duplicated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the duplicate annotate wrapper.

Comment on lines 53 to 63
func NewGatewayController(
client client.Client, kube kubernetes.Interface, logger logr.Logger, envoyGatewayNamespace string,
extProcImage string, extProcLogLevel string, standAlone bool, uuidFn func() string, extProcAsSideCar bool,
standAlone bool, uuidFn func() string, options *Options, extProcAsSideCar bool,
) *GatewayController {
return NewGatewayControllerWithAPIReader(client, nil, kube, logger, envoyGatewayNamespace, standAlone, uuidFn, options, extProcAsSideCar)
}

func NewGatewayControllerWithAPIReader(
client client.Client, noCacheReader client.Reader, kube kubernetes.Interface, logger logr.Logger, envoyGatewayNamespace string,
standAlone bool, uuidFn func() string, options *Options, extProcAsSideCar bool,
) *GatewayController {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems unnecessary, and just passes nil

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed NewGatewayControllerWithAPIReader and the noCacheReader field from GatewayController. The reconciler now uses the cached client path only.

Comment on lines +173 to 175
if err := noCacheReader.List(ctx, &all); err != nil {
return routes, fmt.Errorf("failed to list routes: %w", err)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to not have noCache reader here. Reconcile can be hit multiple times and it may make listing without cache too expensive. But the current callers are fine.
Eventual consistency should be okay here so we can avoid it as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the reconciler route-list helpers to pass nil for the no-cache reader. The no-cache fallback remains only on the admission webhook/mutator path.

Comment thread internal/controller/gateway.go Outdated
if hasSideCar {
if stamped := pod.Annotations[extProcConfigHashAnnotationKey]; stamped != "" && stamped != desiredHash {
needsRollout = true
c.logger.Info("extproc config hash mismatch, triggering rollout",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add stamped and desiredHash here?
If these two ever disagree permanently, this rollout repeats forever with a fresh UUID each time, and this log line is the only signal.
A counter metric would be even better

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added both fields to the hash-mismatch log line: stamped and desiredHash.

require.False(t, hasSideCar, "logLevel mismatch must clear hasSideCar")
})

t.Run("sidecar mode: uses GatewayConfig resolved image", func(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This passes desiredImage in by hand, so it tests checkPodHasSideCarWithImage but not the wiring that produces the image. We should test c.extProcImage(input) here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the GatewayConfig image test to build an extProcContainerInput and use c.extProcImage(input) as the desired image, instead of passing a hand-written image value.

Comment thread internal/controller/gateway.go Outdated

return hasSideCar
if hasSideCar {
if stamped := pod.Annotations[extProcConfigHashAnnotationKey]; stamped != "" && stamped != desiredHash {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stamped != "" check is the decision that a pod with no annotation is never rolled by the hash.
It means an operator who upgrades and then changes a flag sees nothing happen until something else recreates the pods.
Could we add the comment that was previously there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored the comment explaining that a missing hash means a pre-upgrade pod and should not trigger a rollout only because the annotation is absent.

Comment on lines -1216 to -1220
// We annotate the deployments and daemonsets under three scenarios:
// 1. If there's an effective route but no sidecar container, we need to add the sidecar container.
// 2. If there's no effective route but has sidecar container,
// we need to roll out the deployment to trigger the mutation webhook to remove the sidecar container.
// 3. If pods are inconsistent even when rollout isn't in progress, force rollout to self-heal.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep this explanation please?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

@kanurag94

Copy link
Copy Markdown
Member

Addressed the review summary in 6284456: comments are trimmed to what/why, the hash no longer serializes the full Kubernetes Container API object, and GatewayConfig-resolved extproc images are used when checking existing sidecars.

On rollout behavior: I kept direct pod annotation patching for ordinary filter-config Secret updates because it speeds projected-volume refresh without restarting pods. Deployment/DaemonSet template patching is still limited to cases where the pod template must change: missing sidecar, sidecar removal, inconsistent pod state, or extproc config drift.

Verified with go test ./internal/controller.

Thanks. I agree on keeping the pod-annotation fast path for filter-config Secret refresh.
My question was about the hash specifically. The reconciler already computes desiredHash. Could it write that to Deployment.spec.template.metadata.annotations and let Kubernetes handle the rollout, instead of the webhook stamping it on pods and the reconciler comparing?

I am just curious to know if that was thought of and rejected as an approach. If so, why?

@Aias00

Aias00 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review summary in 6284456: comments are trimmed to what/why, the hash no longer serializes the full Kubernetes Container API object, and GatewayConfig-resolved extproc images are used when checking existing sidecars.
On rollout behavior: I kept direct pod annotation patching for ordinary filter-config Secret updates because it speeds projected-volume refresh without restarting pods. Deployment/DaemonSet template patching is still limited to cases where the pod template must change: missing sidecar, sidecar removal, inconsistent pod state, or extproc config drift.
Verified with go test ./internal/controller.

Thanks. I agree on keeping the pod-annotation fast path for filter-config Secret refresh. My question was about the hash specifically. The reconciler already computes desiredHash. Could it write that to Deployment.spec.template.metadata.annotations and let Kubernetes handle the rollout, instead of the webhook stamping it on pods and the reconciler comparing?

I am just curious to know if that was thought of and rejected as an approach. If so, why?

Updated this to follow the pod template annotation approach.

The webhook no longer stamps the config hash on individual pods. The reconciler now writes the desired extproc config hash to the Deployment/DaemonSet pod template annotation, so Kubernetes handles the rollout
when the injected config changes.

The existing UUID annotation is still used only for sidecar add/remove and inconsistent pod self-heal cases.

require.Equal(t, tt.expected, imageTagOrDigest(tt.image))
})
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add test for asserting number of fields?

require.Equal(t, 19, reflect.TypeOf(extProcBuilder{}).NumField(),...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the field-count check and expanded the drift coverage for the builder fields.

Comment thread internal/controller/gateway_mutator.go Outdated
Comment on lines +150 to +156
func (c *GatewayController) listAIGatewayRoutesForGateway(ctx context.Context, gatewayName, gatewayNamespace string) (aigv1b1.AIGatewayRouteList, error) {
return listAIGatewayRoutesForGateway(ctx, c.client, nil, gatewayName, gatewayNamespace)
}

func (c *GatewayController) listMCPRoutesForGateway(ctx context.Context, gatewayName, gatewayNamespace string) (aigv1b1.MCPRouteList, error) {
return listMCPRoutesForGateway(ctx, c.client, nil, gatewayName, gatewayNamespace)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think reconciler will never need a non cached reader as it waits for warmup. We can drop these methods and call them inline.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

result, err := c.annotateGatewayPods(ctx, pods, deployments, daemonSets, uid, hasEffectiveRoutes, len(mcpRoutes.Items) > 0)
input := extProcContainerInput{gatewayConfig: gwConfig, needMCP: len(mcpRoutes.Items) > 0}
result, err := c.annotateGatewayPods(ctx, pods, deployments, daemonSets, uid,
hasEffectiveRoutes, input.needMCP, c.extProcImage(input), c.extProcContainerHash(input))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add one test which goes through Reconcile with a Gateway whose GatewayConfig overrides the image, with pods already running the overridden image, and assert no workload patch happens?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

for i := range daemonSets {
daemonSet := &daemonSets[i]
// Hash changes are written to the pod template so Kubernetes owns the rollout.
needsHashRollout := hasEffectiveRoute && desiredHash != "" && daemonSet.Spec.Template.Annotations[extProcConfigHashAnnotationKey] != desiredHash

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find a test for desiredHash not being nil for daemonSet. WDYT about a test for this as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a DaemonSet test for non-empty desiredHash.

Comment on lines +172 to +176
// TestExtProcContainerHash_ExcludesConfigRoutingArgs ensures the secret-
// presence-driven -configPath / -configBundlePath args are NOT part of the hash
// (they are added by the webhook after buildExtProcContainer returns). The base
// container's Args must contain neither flag, so secret-existence transitions
// do not spuriously trigger rollouts.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test only does the second part of this comment and not computes hash

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

} {
t.Run(tc.name, func(t *testing.T) {
h1 := b.extProcContainerHash(tc.input)
h2 := b.extProcContainerHash(tc.input)

@kanurag94 kanurag94 Aug 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: You can do something like

for range 20 {
  require.Equal(t, h1, b.extProcContainerHash(tc.input))
}

to ensure determinism

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@kanurag94 kanurag94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good shape now. Can you please add the upgrade rollout behavior in PR description? We can also think about it a little more if the first rollout should do restarts or not.

Aias00 and others added 9 commits August 2, 2026 01:11
The mutating webhook injects the extproc container config (env vars,
imagePullSecrets, endpointPrefixes, enableRedaction, maxRecvMsgSize,
header-attribute flags, and GatewayConfig resources/securityContext/
volumeMounts) only at pod-creation time, but the drift check that
decides whether to roll existing pods — checkPodHasSideCar — only
compares the container image, -logLevel, and -mcpAddr presence. So
restarting the controller with a changed --extProcExtraEnvVars (or
editing GatewayConfig.spec.extProc.kubernetes.env) has no effect on
running pods; new values apply only to pods recreated for unrelated
reasons, leaving the fleet in a mixed state.

Fix this by having the webhook stamp a hash of the injected extproc
config as a pod annotation, and having the reconciler recompute the
desired hash with the same builder and trigger the existing rollout
path on mismatch.

To rule out an infinite rollout loop (the failure mode called out in
the issue), both sides share literally one build function
(extProcBuilder.buildExtProcContainer) and hash its output. The hash
uses stdlib encoding/json rather than sonic, because sonic does not
guarantee stable field order and a non-deterministic hash would cause
exactly the rollout loop we are guarding against.

Scope: the hash covers config derived from controller flags +
GatewayConfig + needMCP. It intentionally excludes the secret-presence-
driven -configPath / -configBundlePath args and the legacy/bundle
volumeMounts, so secret-existence transitions do not drive rollouts
(the UUID-annotation fast path handles filter-config content). A pod
created before this annotation existed has no hash; "missing" is
treated as "no drift signal" (not a rollout trigger) so upgrading the
controller does not roll every managed proxy once.

Fixes envoyproxy#2395

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: liuhy <liuhongyu@apache.org>
Signed-off-by: liuhy <liuhongyu@apache.org>
Signed-off-by: liuhy <liuhongyu@apache.org>
Signed-off-by: liuhy <liuhongyu@apache.org>
Signed-off-by: liuhy <liuhongyu@apache.org>
Signed-off-by: liuhy <liuhongyu@apache.org>
Signed-off-by: liuhy <liuhongyu@apache.org>
Signed-off-by: liuhy <liuhongyu@apache.org>
Signed-off-by: liuhy <liuhongyu@apache.org>
@Aias00
Aias00 force-pushed the fix/extproc-config-drift-rollout-2395 branch from eb22cde to 7fcb556 Compare August 2, 2026 08:13
@Aias00

Aias00 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Updated the PR description with the upgrade behavior: workloads without the template hash get one convergence rollout on the next Gateway reconcile, then future reconciles are no-op unless the desired hash changes again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Changing extProcExtraEnvVars (or GatewayConfig env) does not trigger rollout of managed Envoy pods

5 participants