Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
4b20a18
fix: remove secrets for pending helm releases
Nov 10, 2025
81d2b48
fix: await
Nov 10, 2025
e0150ba
fix: pending helm secrets
Nov 10, 2025
3647fdf
fix: move check to apply
Nov 10, 2025
0b4860a
fix: move checkoperationsinprogress fixed tests
Nov 10, 2025
a148ab1
fix: add check in install as well
Nov 10, 2025
ba96834
fix: more logging
Nov 11, 2025
4c013f3
Merge branch 'main' into APL-1150
svcAPLBot Nov 11, 2025
8856d1d
fix: add more statuses to check for
Nov 11, 2025
5d964d0
Merge branch 'APL-1150' of github.com:linode/apl-core into APL-1150
Nov 11, 2025
b09cb50
Merge branch 'main' into APL-1150
svcAPLBot Nov 11, 2025
4ed6252
Merge branch 'main' into APL-1150
svcAPLBot Nov 11, 2025
dd787cf
fix: exception for otomi
Nov 11, 2025
e53fcd6
Merge branch 'APL-1150' of github.com:linode/apl-core into APL-1150
Nov 11, 2025
59e8386
fix: check labelname
Nov 12, 2025
31fd8c1
Merge branch 'main' into APL-1150
svcAPLBot Nov 12, 2025
fe72dd7
Merge branch 'main' into APL-1150
svcAPLBot Nov 12, 2025
2e03564
feat: added tests
Nov 12, 2025
4146f07
Merge branch 'APL-1150' of github.com:linode/apl-core into APL-1150
Nov 12, 2025
0f6c5c9
Merge branch 'main' into APL-1150
svcAPLBot Nov 12, 2025
0ad97c6
Merge branch 'main' into APL-1150
svcAPLBot Nov 12, 2025
d2393f7
Merge branch 'main' into APL-1150
svcAPLBot Nov 12, 2025
72734a0
Merge branch 'main' into APL-1150
svcAPLBot Nov 12, 2025
e46493b
fix: remove logs
Nov 12, 2025
a8e571e
Merge branch 'APL-1150' of github.com:linode/apl-core into APL-1150
Nov 12, 2025
3aee8d8
Merge branch 'main' into APL-1150
svcAPLBot Nov 13, 2025
ac2ee81
Merge branch 'main' into APL-1150
svcAPLBot Nov 13, 2025
5819130
Merge branch 'main' into APL-1150
svcAPLBot Nov 13, 2025
2bdf936
Merge branch 'main' into APL-1150
svcAPLBot Nov 13, 2025
0581879
Merge branch 'main' into APL-1150
svcAPLBot Nov 13, 2025
8b55b30
fix: argocd controller label conflict on reapply
Nov 13, 2025
fb3238c
fix: change args order
Nov 13, 2025
e50f785
fix: change backofflimit for testing purposes
Nov 13, 2025
4ba96e6
Merge branch 'main' into APL-1150
svcAPLBot Nov 14, 2025
dcb4a12
Merge branch 'main' into APL-1150
svcAPLBot Nov 14, 2025
9212043
Merge branch 'main' into APL-1150
svcAPLBot Nov 14, 2025
5da8813
Merge branch 'main' into APL-1150
svcAPLBot Nov 14, 2025
b701e08
fix: turn back backoff limit
Nov 14, 2025
0de2410
Merge branch 'APL-1150' of github.com:linode/apl-core into APL-1150
Nov 14, 2025
8a4556f
Merge branch 'main' into APL-1150
svcAPLBot Nov 14, 2025
dcb2f59
Merge branch 'main' into APL-1150
svcAPLBot Nov 14, 2025
addb21a
Merge branch 'main' into APL-1150
svcAPLBot Nov 14, 2025
19fa29d
Merge branch 'main' into APL-1150
svcAPLBot Nov 17, 2025
8558500
Merge branch 'main' into APL-1150
svcAPLBot Nov 18, 2025
96e1b78
Merge branch 'main' into APL-1150
svcAPLBot Nov 18, 2025
3ff8a4e
Merge branch 'main' into APL-1150
svcAPLBot Nov 18, 2025
4a9d2dd
Merge branch 'main' into APL-1150
svcAPLBot Nov 18, 2025
bce12c6
Merge branch 'main' into APL-1150
svcAPLBot Nov 18, 2025
f67e6f6
Merge branch 'main' into APL-1150
svcAPLBot Nov 18, 2025
1b65cba
Merge branch 'main' into APL-1150
svcAPLBot Nov 20, 2025
d3efa35
Merge branch 'main' into APL-1150
svcAPLBot Nov 20, 2025
bc84dd1
Merge branch 'main' into APL-1150
svcAPLBot Nov 20, 2025
64e2d9b
feat: merge with main and solve conflicts
Nov 21, 2025
9d9407f
fix: checkoperationsinprogress test
Nov 21, 2025
a969a0a
Merge branch 'main' into APL-1150
svcAPLBot Nov 21, 2025
05360e8
fix: remove unnecessary test code
Nov 21, 2025
36ad049
Merge branch 'APL-1150' of github.com:linode/apl-core into APL-1150
Nov 21, 2025
ddcd861
Merge branch 'main' into APL-1150
svcAPLBot Nov 21, 2025
8283bb4
Merge branch 'main' into APL-1150
svcAPLBot Nov 21, 2025
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
3 changes: 3 additions & 0 deletions src/cmd/apply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jest.mock('fs', () => ({
}))

jest.mock('src/common/k8s', () => ({
checkOperationsInProgress: jest.fn(),
getDeploymentState: jest.fn(),
setDeploymentState: jest.fn(),
restartOtomiApiDeployment: jest.fn(),
Expand Down Expand Up @@ -93,6 +94,7 @@ describe('Apply command', () => {
applyAsApps: require('./apply-as-apps').applyAsApps,
commit: require('./commit').commit,
runtimeUpgrade: require('src/common/runtime-upgrade').runtimeUpgrade,
checkOperationsInProgress: require('src/common/k8s').checkOperationsInProgress,
cd: require('zx').cd,
getParsedArgs: require('src/common/yargs').getParsedArgs,
}
Expand All @@ -104,6 +106,7 @@ describe('Apply command', () => {
mockDeps.applyAsApps.mockResolvedValue(true)
mockDeps.commit.mockResolvedValue(undefined)
mockDeps.runtimeUpgrade.mockResolvedValue(undefined)
mockDeps.checkOperationsInProgress.mockResolvedValue(undefined)
mockDeps.getParsedArgs.mockReturnValue({})
})

Expand Down
3 changes: 2 additions & 1 deletion src/cmd/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash'
import { cleanupHandler, prepareEnvironment } from 'src/common/cli'
import { terminal } from 'src/common/debug'
import { env } from 'src/common/envalid'
import { getDeploymentState, setDeploymentState } from 'src/common/k8s'
import { checkOperationsInProgress, getDeploymentState, setDeploymentState } from 'src/common/k8s'
import { getFilename, rootDir } from 'src/common/utils'
import { getImageTagFromValues, getPackageVersion } from 'src/common/values'
import { getParsedArgs, HelmArguments, helmOptions, setParsedArgs } from 'src/common/yargs'
Expand Down Expand Up @@ -91,6 +91,7 @@ export const apply = async (): Promise<void> => {
d.error('Failed to collect traces:', traceError)
}
d.info(`Retrying in ${retryOptions.maxTimeout} ms`)
await checkOperationsInProgress()
throw e
}
}, retryOptions)
Expand Down
6 changes: 3 additions & 3 deletions src/common/hf.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { existsSync, mkdirSync, writeFileSync } from 'fs'
import { readFile } from 'fs/promises'
import { glob } from 'glob'
import { has, set } from 'lodash'
import { resolve } from 'path'
import { parse } from 'yaml'
import { $, ProcessPromise } from 'zx'
import { logLevels, terminal } from './debug'
import { env } from './envalid'
import { applyServerSide } from './k8s'
import { getFileMaps, setValuesFile } from './repo'
import { asArray, extract, flattenObject, getValuesSchema, isCore, rootDir } from './utils'
import { getParsedArgs, HelmArguments } from './yargs'
import { ProcessOutputTrimmed, Streams } from './zx-enhance'
import { resolve } from 'path'
import { existsSync, mkdirSync, writeFileSync } from 'fs'
import { applyServerSide } from './k8s'

const replaceHFPaths = (output: string, envDir = env.ENV_DIR): string => output.replaceAll('../env', envDir)
export const HF_DEFAULT_SYNC_ARGS = [
Expand Down
46 changes: 42 additions & 4 deletions src/common/k8s.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
V1StatefulSet,
V1Status,
} from '@kubernetes/client-node'
import retry from 'async-retry'
import { ARGOCD_APP_PARAMS } from './constants'
import { terminal } from './debug'
import { env } from './envalid'
import * as k8s from './k8s'
import {
appRevisionMatches,
Expand All @@ -22,10 +26,6 @@ import {
patchContainerResourcesOfSts,
patchStatefulSetResources,
} from './k8s'
import { terminal } from './debug'
import retry from 'async-retry'
import { env } from './envalid'
import { ARGOCD_APP_PARAMS } from './constants'

class MockApiException<T> extends ApiException<T> {
code: number
Expand Down Expand Up @@ -613,6 +613,44 @@ describe('patchArgoCdApp', () => {
})
})

describe('helm operations in progress check', () => {
it('should get pending helm releases', async () => {
const mockGetK8sHelmReleases = jest.spyOn(k8s, 'getK8sHelmReleases').mockResolvedValue({
'release-1:ns-1': { name: 'release-1', namespace: 'ns-1', revision: 2, status: 'pending-upgrade' },
'release-2:ns-2': { name: 'release-2', namespace: 'ns-2', revision: 1, status: 'deployed' },
})

const pendingReleases = await k8s.getPendingHelmReleases()
expect(mockGetK8sHelmReleases).toHaveBeenCalled()
expect(pendingReleases).toEqual([{ name: 'release-1', namespace: 'ns-1', revision: 2, status: 'pending-upgrade' }])
})

it('should delete secrets for helm releases', async () => {
const mockDeleteSecretForHelmRelease = jest.spyOn(k8s, 'deleteSecretForHelmRelease').mockResolvedValue()
await k8s.deleteSecretForHelmRelease('release-1', 'ns-1')
expect(mockDeleteSecretForHelmRelease).toHaveBeenCalledWith('release-1', 'ns-1')
})

it('should delete secrets for pending releases', async () => {
const mockDeleteSecretForHelmRelease = jest.spyOn(k8s, 'deleteSecretForHelmRelease').mockResolvedValue()
const mockGetPendingHelmReleases = jest.spyOn(k8s, 'getPendingHelmReleases').mockResolvedValue([
{
name: 'release-1',
namespace: 'ns-1',
revision: 2,
status: 'pending-upgrade',
app_version: '',
},
{ name: 'release-2', namespace: 'ns-2', revision: 1, status: 'pending-install', app_version: '' },
])

await k8s.checkOperationsInProgress()
expect(mockGetPendingHelmReleases).toHaveBeenCalled()
expect(mockDeleteSecretForHelmRelease).toHaveBeenNthCalledWith(1, 'release-1', 'ns-1')
expect(mockDeleteSecretForHelmRelease).toHaveBeenNthCalledWith(3, 'release-2', 'ns-2')
})
})

describe('restartOtomiApiDeployment', () => {
let mockAppApi: jest.Mocked<AppsV1Api>

Expand Down
96 changes: 96 additions & 0 deletions src/common/k8s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ export const getK8sSecret = async (name: string, namespace: string): Promise<Rec
}
}

export const deleteSecretForHelmRelease = async (releaseName: string, namespace: string) => {
const d = terminal('common:k8s:deleteSecretForHelmRelease')
d.info(`Deleting secret for Helm release ${releaseName} in namespace ${namespace}`)
try {
await coreClient.deleteNamespacedSecret({ name: `sh.helm.release.v1.${releaseName}.v1`, namespace })
d.debug(`Deleted secret for Helm release ${releaseName} in namespace ${namespace}`)
} catch (error) {
if (error?.response?.statusCode !== 404) {
throw error
}
}
}

export interface DeploymentState {
status?: 'deploying' | 'deployed'
tag?: string
Expand All @@ -164,6 +177,46 @@ export const getDeploymentState = async (): Promise<DeploymentState> => {
const result = await $`kubectl get cm -n otomi ${DEPLOYMENT_STATUS_CONFIGMAP} -o jsonpath='{.data}'`.nothrow().quiet()
return JSON.parse(result.stdout || '{}')
}
interface HelmRelease {
name: string
namespace: string
status: string
app_version: string
revision: number
first_deployed?: string
last_deployed?: string
chart?: string
}

export const checkOperationsInProgress = async (): Promise<void> => {
const d = terminal(`common:k8s:checkOperationsInProgress`)
const pendingHelmReleases = await getPendingHelmReleases()
if (pendingHelmReleases.length > 0) {
d.info(`Pending Helm operations detected for releases: ${pendingHelmReleases.join(', ')}. removing secrets...`)
for (const release of pendingHelmReleases) {
await deleteSecretForHelmRelease(release.name, release.namespace)
}
}
}

export const getPendingHelmReleases = async (): Promise<HelmRelease[]> => {
const d = terminal('common:k8s:getPendingHelmReleases')
d.info('Checking for pending Helm operations')
const releases = await getK8sHelmReleases()
const pendingReleases: HelmRelease[] = []
Object.keys(releases).forEach((key) => {
const release = releases[key]
if (release.labelName === 'apl' || release.labelName === 'otomi') return
if (
release.status === 'pending-upgrade' ||
release.status === 'pending-install' ||
release.status === 'pending-rollback'
) {
pendingReleases.push(release)
}
})
return pendingReleases
}

/**
* Result of command execution
Expand Down Expand Up @@ -241,6 +294,49 @@ export const getHelmReleases = async (): Promise<Record<string, any>> => {
return status
}

export const getK8sHelmReleases = async (): Promise<Record<string, any>> => {
const coreApi = k8s.core()

try {
const secretsResponse = await coreApi.listSecretForAllNamespaces({
labelSelector: 'owner=helm,status',
})

const releases: Record<string, any> = {}

for (const secret of secretsResponse.items) {
if (!secret.metadata?.name || !secret.metadata?.namespace || !secret.metadata?.labels) continue

const match = secret.metadata.name.match(/^sh\.helm\.release\.v1\.(.+)\.v(\d+)$/)
if (!match) continue

const [, releaseName, revision] = match
const releaseKey = `${secret.metadata.namespace}/${releaseName}`

const release = {
name: releaseName,
labelName: secret.metadata.labels.name,
namespace: secret.metadata.namespace,
revision: parseInt(revision),
status: secret.metadata.labels.status,
app_version: secret.metadata.labels.version || secret.metadata.labels.app_version,
chart: secret.metadata.labels.chart,
first_deployed: secret.metadata?.creationTimestamp,
last_deployed: secret.metadata.labels.modifiedAt,
}

// Keep only the latest revision for each release
if (!releases[releaseKey] || releases[releaseKey].revision < release.revision) {
releases[releaseKey] = release
}
}

return releases
} catch (error) {
throw new Error(`Failed to get Helm releases from Kubernetes: ${error.message}`)
}
}

export const setDeploymentState = async (state: Record<string, any>): Promise<void> => {
if (env.isDev && env.DISABLE_SYNC) return
const d = terminal('common:k8s:setDeploymentState')
Expand Down
7 changes: 4 additions & 3 deletions src/operator/installer.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Installer } from './installer'
import { AplOperations } from './apl-operations'
import * as k8s from '../common/k8s'
import { hfValues } from '../common/hf'
import * as k8s from '../common/k8s'
import { AplOperations } from './apl-operations'
import { Installer } from './installer'

jest.mock('../common/debug', () => ({
terminal: jest.fn().mockImplementation(() => ({
Expand All @@ -13,6 +13,7 @@ jest.mock('../common/debug', () => ({
}))

jest.mock('../common/k8s', () => ({
checkOperationsInProgress: jest.fn(),
getK8sConfigMap: jest.fn(),
getK8sSecret: jest.fn(),
createUpdateConfigMap: jest.fn(),
Expand Down
12 changes: 10 additions & 2 deletions src/operator/installer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import * as process from 'node:process'
import { terminal } from '../common/debug'
import { createUpdateConfigMap, createUpdateGenericSecret, getK8sConfigMap, getK8sSecret, k8s } from '../common/k8s'
import { hfValues } from '../common/hf'
import {
checkOperationsInProgress,
createUpdateConfigMap,
createUpdateGenericSecret,
getK8sConfigMap,
getK8sSecret,
k8s,
} from '../common/k8s'
import { AplOperations } from './apl-operations'
import { getErrorMessage } from './utils'
import * as process from 'node:process'

export interface GitCredentials {
username: string
Expand Down Expand Up @@ -48,6 +55,7 @@ export class Installer {
this.d.error(`Installation attempt ${attemptNumber} failed:`, errorMessage)
await this.updateInstallationStatus('failed', attemptNumber, errorMessage)
this.d.warn(`Installation attempt ${attemptNumber} failed, retrying in 1 second...`, getErrorMessage(error))
await checkOperationsInProgress()

// Wait 1 second before retrying
await new Promise((resolve) => setTimeout(resolve, 1000))
Expand Down
Loading