Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7cc0dea
feat: strip the last applied anotation
j-zimnowoda Jul 20, 2026
22e3f7e
feat: hf to uses server-side apply
j-zimnowoda Jul 20, 2026
f40f120
revert: changes to getArgoCdAppManifest
j-zimnowoda Jul 20, 2026
d7785dd
Merge branch 'main' into gh-3449
svcAPLBot Jul 20, 2026
d0d82bf
Merge branch 'main' into gh-3449
svcAPLBot Jul 21, 2026
68fadbb
Merge branch 'main' into gh-3449
svcAPLBot Jul 21, 2026
706f89a
Merge branch 'main' into gh-3449
svcAPLBot Jul 22, 2026
16f5564
Merge branch 'main' into gh-3449
svcAPLBot Jul 27, 2026
9e5610a
test: remove unused
j-zimnowoda Jul 27, 2026
8563630
feat: remove the last applied annotation on upgrade
j-zimnowoda Jul 27, 2026
ec0ee1d
Merge branch 'main' into gh-3449
svcAPLBot Jul 28, 2026
5cab632
Merge branch 'main' into gh-3449
svcAPLBot Jul 28, 2026
3338bfe
Merge branch 'main' into gh-3449
svcAPLBot Jul 28, 2026
728b90c
Merge branch 'main' into gh-3449
svcAPLBot Jul 28, 2026
d67aed9
Merge branch 'main' into gh-3449
svcAPLBot Jul 28, 2026
30274d3
Merge branch 'main' into gh-3449
svcAPLBot Jul 29, 2026
2941bb5
Merge branch 'main' into gh-3449
svcAPLBot Jul 29, 2026
e92e50f
Merge branch 'main' into gh-3449
svcAPLBot Jul 29, 2026
cdf9b64
Merge branch 'main' into gh-3449
svcAPLBot Jul 29, 2026
1d064e1
Merge branch 'main' into gh-3449
svcAPLBot Jul 30, 2026
a4bf7c0
Merge branch 'main' into gh-3449
svcAPLBot Jul 31, 2026
2584a4e
Merge branch 'main' into gh-3449
svcAPLBot Aug 3, 2026
7591cfd
Merge branch 'main' into gh-3449
svcAPLBot Aug 3, 2026
4d98c1a
Merge branch 'main' into gh-3449
svcAPLBot Aug 4, 2026
4dc38dd
Merge branch 'main' into gh-3449
svcAPLBot Aug 4, 2026
e0f370a
Merge branch 'main' into gh-3449
svcAPLBot Aug 4, 2026
1e5d5b0
Merge branch 'main' into gh-3449
svcAPLBot Aug 4, 2026
3eddfff
Merge branch 'main' into gh-3449
svcAPLBot Aug 5, 2026
d88f977
Merge branch 'main' into gh-3449
svcAPLBot Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions src/cmd/apply-as-apps.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { statSync } from 'fs'
import { glob } from 'glob'
import { ARGOCD_APP_PARAMS } from '../common/constants'
import { env } from '../common/envalid'
import { getNames } from '../common/utils'
import {
addGitOpsApps,
applyArgocdApp,
applyGitOpsApps,
ArgocdAppManifest,
calculateGitOpsAppsSyncState,
checkArgoCdController,
getApplications,
getArgocdCoreAppManifest,
getArgocdGitopsManifest,
checkArgoCdController,
removeGitOpsApps,
} from './apply-as-apps'
import { glob } from 'glob'
import { env } from '../common/envalid'
import { statSync } from 'fs'
import { ARGOCD_APP_PARAMS } from '../common/constants'
import { getNames } from '../common/utils'

jest.mock('glob')
jest.mock('fs', () => ({
Expand Down Expand Up @@ -687,3 +688,34 @@ describe('checkArgoCdController', () => {
expect(mockRestartStatefulSet).not.toHaveBeenCalled()
})
})

describe('getArgocdCoreAppManifest', () => {
const release = {
name: 'kyverno',
namespace: 'kyverno',
enabled: true,
installed: true,
labels: '',
chart: '../charts/kyverno',
version: '1.0.0',
}

beforeEach(() => {
jest.clearAllMocks()
;(env as any).APPS_REPO_URL = 'https://charts.example.com'
;(env as any).APPS_REVISION = undefined
})

it('should include ServerSideApply=true in syncOptions', () => {
const manifest = getArgocdCoreAppManifest(release, {}, '1.0.0')

expect(manifest.spec.syncPolicy.syncOptions).toContain('ServerSideApply=true')
})

it('should preserve ServerSideApply=true when app has a patch with only ignoreDifferences', () => {
const istioBase = { ...release, name: 'istio-base', namespace: 'istio-system' }
const manifest = getArgocdCoreAppManifest(istioBase, {}, '1.0.0')

expect(manifest.spec.syncPolicy.syncOptions).toContain('ServerSideApply=true')
})
})
3 changes: 2 additions & 1 deletion src/cmd/apply-as-apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,14 @@ const getArgoCdAppManifest = (name: string, appLabel: string, spec: Record<strin
}
}

const getArgocdCoreAppManifest = (
export const getArgocdCoreAppManifest = (
release: HelmRelease,
values: Record<string, any>,
otomiVersion: string,
): ArgocdAppManifest => {
Comment on lines +116 to 120
const name = getAppName(release)
const patch = (appPatches[name] || genericPatch) as Record<string, any>

return getArgoCdAppManifest(name, ARGOCD_APP_DEFAULT_LABEL, {
syncPolicy: ARGOCD_APP_DEFAULT_SYNC_POLICY,
Comment on lines 121 to 125
project: 'default',
Expand Down
4 changes: 2 additions & 2 deletions src/common/hf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const HF_DEFAULT_SYNC_ARGS = [
'--concurrency=1',
'--sync-args',
// These two need to be in same string as is passed as single argument to --sync-args
'--disable-openapi-validation --qps=20',
'--disable-openapi-validation --qps=20 --serverSide=true',

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.

That is true. The server-side parameter needs was introduced in helm v4

]

export const HF_DEFAULT_SYNC_ON_INITIAL_INSTALL_ARGS = [
Expand All @@ -28,7 +28,7 @@ export const HF_DEFAULT_SYNC_ON_INITIAL_INSTALL_ARGS = [
'--concurrency=1',
'--sync-args',
// These two need to be in same string as is passed as single argument to --sync-args
'--disable-openapi-validation --qps=20',
'--disable-openapi-validation --qps=20 --serverSide=true',
]

type HFParams = {
Expand Down
7 changes: 7 additions & 0 deletions src/common/runtime-upgrades/runtime-upgrades.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { OtomiDebugger } from '../debug'
import { k8s } from '../k8s'
import { detectAndRestartOutdatedIstioSidecars } from './restart-istio-sidecars'
import { stripOversizedLastAppliedAnnotations } from './v6.2.0'

export interface RuntimeUpgradeContext {
debug: OtomiDebugger
Expand Down Expand Up @@ -35,4 +36,10 @@ export const runtimeUpgrades: RuntimeUpgrades = [
},
},
},
{
version: '6.2.0',
pre: async ({ debug }) => {
await stripOversizedLastAppliedAnnotations().catch((e) => debug.warn('Failed to strip oversized annotations:', e))
},
},
]
82 changes: 82 additions & 0 deletions src/common/runtime-upgrades/v6.2.0.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { stripOversizedLastAppliedAnnotations } from './v6.2.0'

jest.mock('../debug', () => ({
...jest.requireActual('../debug'),
terminal: jest.fn(() => ({
info: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
stream: { log: process.stdout, error: process.stderr },
})),
}))

describe('stripOversizedLastAppliedAnnotations', () => {
const annotation = 'kubectl.kubernetes.io/last-applied-configuration'
const oversizedValue = 'x'.repeat(262145)

const mockListCRDs = jest.fn()
const mockPatchCRD = jest.fn()
const mockListConfigMaps = jest.fn()
const mockPatchConfigMap = jest.fn()

const mockDeps = {
getCrdApi: () => ({ listCustomResourceDefinition: mockListCRDs, patchCustomResourceDefinition: mockPatchCRD }),
getCoreApi: () => ({
listConfigMapForAllNamespaces: mockListConfigMaps,
patchNamespacedConfigMap: mockPatchConfigMap,
}),
}

beforeEach(() => {
jest.clearAllMocks()
mockListCRDs.mockResolvedValue({ items: [] })
mockListConfigMaps.mockResolvedValue({ items: [] })
mockPatchCRD.mockResolvedValue({})
mockPatchConfigMap.mockResolvedValue({})
})

it('removes last-applied-configuration from a CRD whose annotation exceeds the limit', async () => {
mockListCRDs.mockResolvedValue({
items: [{ metadata: { name: 'clusterpolicies.kyverno.io', annotations: { [annotation]: oversizedValue } } }],
})

await stripOversizedLastAppliedAnnotations(mockDeps as any)

expect(mockPatchCRD).toHaveBeenCalledWith(
expect.objectContaining({ name: 'clusterpolicies.kyverno.io' }),
expect.anything(),
)
})

it('removes last-applied-configuration from a ConfigMap whose annotation exceeds the limit', async () => {
mockListConfigMaps.mockResolvedValue({
items: [
{
metadata: {
name: 'grafana-dashboards-k8s-admin',
namespace: 'grafana',
annotations: { [annotation]: oversizedValue },
},
},
],
})

await stripOversizedLastAppliedAnnotations(mockDeps as any)

expect(mockPatchConfigMap).toHaveBeenCalledWith(
expect.objectContaining({ name: 'grafana-dashboards-k8s-admin', namespace: 'grafana' }),
expect.anything(),
)
})

it('does not patch a ConfigMap without the annotation', async () => {
mockListConfigMaps.mockResolvedValue({
items: [{ metadata: { name: 'some-config', namespace: 'default', annotations: {} } }],
})

await stripOversizedLastAppliedAnnotations(mockDeps as any)

expect(mockPatchConfigMap).not.toHaveBeenCalled()
})
})
49 changes: 49 additions & 0 deletions src/common/runtime-upgrades/v6.2.0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ApiextensionsV1Api, CoreV1Api, PatchStrategy, setHeaderOptions } from '@kubernetes/client-node'
import { terminal } from '../debug'
import { k8s } from '../k8s'

const LAST_APPLIED_ANNOTATION = 'kubectl.kubernetes.io/last-applied-configuration'
// JSON Patch requires '/' in key names to be escaped as '~1'
const LAST_APPLIED_PATCH_PATH = '/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration'

export const stripOversizedLastAppliedAnnotations = async (
deps = {
getCrdApi: (): ApiextensionsV1Api => k8s.kc().makeApiClient(ApiextensionsV1Api),
getCoreApi: (): CoreV1Api => k8s.core(),
},
): Promise<void> => {
const log = terminal('common:runtime-upgrades:v6.2.0:stripOversized')
const patchHeaders = setHeaderOptions('Content-Type', PatchStrategy.JsonPatch)
const removePatch = [{ op: 'remove', path: LAST_APPLIED_PATCH_PATH }]

const crdApi = deps.getCrdApi()
const { items: crds } = await crdApi.listCustomResourceDefinition()
await Promise.allSettled(
crds
.filter((crd) => {
const value = crd.metadata?.annotations?.[LAST_APPLIED_ANNOTATION]
return value !== undefined
})
.map(async (crd) => {
const name = crd.metadata!.name!
log.info(`Stripping oversized last-applied-configuration from CRD ${name}`)
await crdApi.patchCustomResourceDefinition({ name, body: removePatch }, patchHeaders)
}),
)
Comment on lines +19 to +32

const coreApi = deps.getCoreApi()
const { items: configMaps } = await coreApi.listConfigMapForAllNamespaces()
await Promise.allSettled(
configMaps
.filter((cm) => {
const value = cm.metadata?.annotations?.[LAST_APPLIED_ANNOTATION]
return value !== undefined
})
.map(async (cm) => {
const name = cm.metadata!.name!
const namespace = cm.metadata!.namespace!
log.info(`Stripping oversized last-applied-configuration from ConfigMap ${namespace}/${name}`)
await coreApi.patchNamespacedConfigMap({ name, namespace, body: removePatch }, patchHeaders)
}),
)
Comment on lines +34 to +48
}
Loading