fix(migrate): strip oversized last-applied-configuration annotations - #3469
Open
aweingarten wants to merge 5 commits into
Open
fix(migrate): strip oversized last-applied-configuration annotations#3469aweingarten wants to merge 5 commits into
aweingarten wants to merge 5 commits into
Conversation
Client-side apply writes the entire object into the
`kubectl.kubernetes.io/last-applied-configuration` annotation. For an
object carrying a large embedded schema — kyverno's policy CRDs, the
ESO/cnpg/Gateway-API CRDs, grafana's dashboard ConfigMaps — that runs
150-250KB and tips past the apiserver's 262144-byte annotations cap,
after which *every* subsequent patch to the object fails:
CustomResourceDefinition.apiextensions.k8s.io "clusterpolicies.kyverno.io"
is invalid: metadata.annotations: Too long: may not be more than 262144 bytes
The owning Applications never reach Synced and platform-bootstrap
stalls. This reproduces reliably on reused/long-lived clusters and not
on fresh ones, which is the tell: core Applications already sync with
`ServerSideApply=true`, so nothing rewrites the annotation today — but
an object that picked one up under an *earlier* client-side apply keeps
it forever, and keeps spending the annotation budget on it.
Add a migration that sweeps CRDs and ConfigMaps and removes the
annotation wherever it exceeds 100KB. Under server-side apply the
annotation is inert, so removing it costs nothing and unwedges clusters
carrying one from before.
The sweep paginates (100 objects per page) rather than listing
cluster-wide in one call, and a failed patch on one object is logged
and skipped rather than failing the migration.
Refs linode#3449, linode#3421
Bumps specVersion to 72.
aweingarten
requested review from
Ani1357,
CasLubbers,
ferruhcihan,
j-zimnowoda and
merll
as code owners
July 28, 2026 14:15
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new migration step to unblock Argo CD sync/bootstraps on long-lived clusters where oversized kubectl.kubernetes.io/last-applied-configuration annotations (left over from historical client-side apply) push objects over the apiserver annotation-size limit.
Changes:
- Introduces migration specVersion 72 that sweeps CRDs + ConfigMaps and removes
last-applied-configurationwhen it exceeds a threshold. - Adds unit tests covering threshold behavior, pagination, failure isolation, and JSON-pointer escaping.
- Bumps
specVersionreferences and wires the migration into the versioned migration list.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| values-changes.yaml | Registers migration function for specVersion 72. |
| tests/fixtures/env/settings/versions.yaml | Updates fixture specVersion to 72 for migration tests. |
| src/cmd/migrate.ts | Implements the new sweep + patch logic and registers it as a custom migration function. |
| src/cmd/migrate.test.ts | Adds unit tests for the new migration and helper escaping. |
| helmfile.d/snippets/defaults.yaml | Updates default specVersion to 72. |
Comment on lines
+853
to
+854
| export const isOversized = (object: AnnotatedObject): boolean => | ||
| (object.metadata?.annotations?.[LAST_APPLIED_ANNOTATION]?.length ?? 0) > LAST_APPLIED_SIZE_THRESHOLD |
Comment on lines
+906
to
+907
| cont = page.metadata?._continue || undefined | ||
| } while (cont) |
Comment on lines
+901
to
+904
| // A 404/422 here means someone else already removed it — never fail the migration over it. | ||
| if (error instanceof ApiException && (error.code === 404 || error.code === 422)) continue | ||
| d.error(`Could not strip ${LAST_APPLIED_ANNOTATION} from ${kind} ${name}: ${error}`) | ||
| } |
Comment on lines
+639
to
+642
| : { | ||
| items: [{ metadata: { name: 'first.example.io', annotations: oversized } }], | ||
| metadata: { _continue: 'page-2' }, | ||
| }, |
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/cmd/migrate.ts:855
isOversizedcomparesannotationValue.length(UTF-16 code units) against a threshold described in bytes, but the Kubernetes annotations limit is byte-based. This can under-count non-ASCII content (e.g., CRD schemas/descriptions with Unicode), potentially skipping objects that are actually near/over the apiserver byte cap.
export const isOversized = (object: AnnotatedObject): boolean =>
(object.metadata?.annotations?.[LAST_APPLIED_ANNOTATION]?.length ?? 0) > LAST_APPLIED_SIZE_THRESHOLD
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/cmd/migrate.ts:854
isOversizedcompares the annotation value using.length, but the Kubernetes annotation limit (and your threshold comment) are in bytes. For non-ASCII content,.length(UTF-16 code units) can significantly undercount bytes, causing genuinely oversizedlast-applied-configurationvalues to be skipped and leaving the cluster wedged.
export const isOversized = (object: AnnotatedObject): boolean =>
(object.metadata?.annotations?.[LAST_APPLIED_ANNOTATION]?.length ?? 0) > LAST_APPLIED_SIZE_THRESHOLD
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
📌 Summary
Fixes #3449 (and item 1 of #3421).
Client-side apply writes the entire object into
kubectl.kubernetes.io/last-applied-configuration. For an object with a large embedded schema — kyverno's policy CRDs, the ESO/cnpg/Gateway-API CRDs, grafana's dashboard ConfigMaps — that annotation runs 150–250KB and tips past the apiserver's 262144-byte cap, after which every subsequent patch to the object fails:The owning Applications never reach Synced and
platform-bootstrapstalls.Half of this is already fixed, which is why it only bites reused clusters.
ARGOCD_APP_DEFAULT_SYNC_POLICYhas carriedServerSideApply=truesince v6.0.0, so nothing writes the annotation today. But an object that picked one up under an earlier client-side apply keeps it forever — and keeps spending the annotation budget on it. Fresh clusters are fine; long-lived ones stay wedged no matter how many times they resync.So this adds the cleanup rather than another sync-option change: a migration (specVersion 72) that sweeps CRDs and ConfigMaps and removes the annotation wherever it exceeds 100KB. Under server-side apply the annotation is inert, so removing it costs nothing and unwedges the object immediately.
🔍 Reviewer Notes
deps,dryRun/local/DISABLE_SYNCguard — same aspreservePvcStorageClassInRawValues.specVersionto 72 invalues-changes.yaml,helmfile.d/snippets/defaults.yamland the test fixture;ci/src/check-schema-version.mjspasses.5 new unit tests (threshold behaviour, pagination, per-object failure isolation, JSON-pointer escaping).
jest(24 tests inmigrate.test.ts),eslint,tsc --noEmitall clean locally; the helmfile-backed gates needhelmfile/gucci, which I don't have locally.🧹 Checklist