Skip to content
23 changes: 15 additions & 8 deletions src/cmd/apply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe('Apply command', () => {
}

// Set up default mock return values
mockDeps.getDeploymentState.mockResolvedValue({ status: 'deployed' })
mockDeps.getDeploymentState.mockResolvedValue({ status: 'deployed', deployingVersion: '1.0.0' })
mockDeps.getImageTagFromValues.mockResolvedValue('v1.0.0')
mockDeps.getPackageVersion.mockReturnValue('1.0.0')
mockDeps.applyAsApps.mockResolvedValue(true)
Expand Down Expand Up @@ -150,23 +150,30 @@ describe('Apply command', () => {

await applyAll()

// Verify pre-upgrade steps
expect(mockDeps.runtimeUpgrade).toHaveBeenCalledWith({ when: 'pre' })

// Verify deployment state management
expect(mockDeps.getDeploymentState).toHaveBeenCalled()
expect(mockDeps.getImageTagFromValues).toHaveBeenCalled()
expect(mockDeps.setDeploymentState).toHaveBeenCalledWith({
status: 'deploying',
deployingTag: 'v1.0.0',
deployingVersion: '1.0.0',
})

expect(mockDeps.getDeploymentState).toHaveBeenCalled()
// Verify pre-upgrade steps
expect(mockDeps.runtimeUpgrade).toHaveBeenCalledWith({
when: 'pre',
deploymentState: { status: 'deployed', deployingVersion: '1.0.0' },
})

// Verify deployment state management
expect(mockDeps.getImageTagFromValues).toHaveBeenCalled()

// Verify core apply process
expect(mockDeps.applyAsApps).toHaveBeenCalled()

// Verify post-upgrade steps
expect(mockDeps.runtimeUpgrade).toHaveBeenCalledWith({ when: 'post' })
expect(mockDeps.runtimeUpgrade).toHaveBeenCalledWith({
when: 'post',
deploymentState: { status: 'deployed', deployingVersion: '1.0.0' },
})

// Verify GitOps apps setup
expect(mockDeps.applyGitOpsApps).toHaveBeenCalled()
Expand Down
13 changes: 7 additions & 6 deletions src/cmd/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,17 @@ const setup = (): void => {

export const applyAll = async () => {
const d = terminal(`cmd:${cmdName}:applyAll`)
const prevState = await getDeploymentState()
const argv: HelmArguments = getParsedArgs()

await runtimeUpgrade({ when: 'pre' })

d.info('Start apply all')
d.info(`Deployment state: ${JSON.stringify(prevState)}`)
const tag = await getImageTagFromValues()
const deployingVersion = getPackageVersion()
await setDeploymentState({ status: 'deploying', deployingTag: tag, deployingVersion })
const deploymentState = await getDeploymentState()

await runtimeUpgrade({ when: 'pre', deploymentState })

d.info('Start apply all')
d.info(`Deployment state: ${JSON.stringify(deploymentState)}`)

// We still need to deploy all teams because some settings depend on platform apps.
const teamsApplyCompleted = await applyTeams()
Expand All @@ -52,7 +53,7 @@ export const applyAll = async () => {
const appsApplyCompleted = await applyAsApps(params)

if (appsApplyCompleted) {
await runtimeUpgrade({ when: 'post' })
await runtimeUpgrade({ when: 'post', deploymentState })
} else {
d.info('Apps apply step not completed, skipping upgrade checks')
}
Expand Down
37 changes: 9 additions & 28 deletions src/common/runtime-upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ jest.mock('./runtime-upgrades/runtime-upgrades', () => ({
},
}))

const mockGetDeploymentState = getDeploymentState as jest.MockedFunction<typeof getDeploymentState>
const mockWaitForArgoCDAppSync = waitForArgoCDAppSync as jest.MockedFunction<typeof waitForArgoCDAppSync>
const mockWaitForArgoCDAppHealthy = waitForArgoCDAppHealthy as jest.MockedFunction<typeof waitForArgoCDAppHealthy>
const mockGetApplications = getApplications as jest.MockedFunction<typeof getApplications>
Expand Down Expand Up @@ -47,18 +46,14 @@ describe('runtimeUpgrade', () => {

describe('first installation scenarios', () => {
it('should skip runtime upgrade for first installation (empty deployment state)', async () => {
mockGetDeploymentState.mockResolvedValue({})

await runtimeUpgrade({ when: 'pre' })
await runtimeUpgrade({ when: 'pre', deploymentState: {} })

expect(mockDebugger.info).toHaveBeenCalledWith(
'Skipping the runtime upgrade procedure because this is initial installation',
)
})

it('should skip runtime upgrade for null deployment state', async () => {
mockGetDeploymentState.mockResolvedValue(null as any)

await runtimeUpgrade({ when: 'pre' })

expect(mockDebugger.info).toHaveBeenCalledWith(
Expand All @@ -69,19 +64,15 @@ describe('runtimeUpgrade', () => {

describe('version handling and filtering', () => {
it('should skip when no applicable upgrades found', async () => {
mockGetDeploymentState.mockResolvedValue({ version: '2.0.0', deployingVersion: '2.0.1' })

await runtimeUpgrade({ when: 'pre' })
await runtimeUpgrade({ when: 'pre', deploymentState: { version: '2.0.0', deployingVersion: '2.0.1' } })

expect(mockDebugger.info).toHaveBeenCalledWith('The current version of the App Platform: 2.0.0')
expect(mockDebugger.info).toHaveBeenCalledWith('Deploying essential manifests')
expect(mockDebugger.info).toHaveBeenCalledWith('No runtime upgrade operations detected, skipping')
})

it('should use current version when deployment state has no version', async () => {
mockGetDeploymentState.mockResolvedValue({ status: 'deployed', deployingVersion: '1.0.1' })

await runtimeUpgrade({ when: 'pre' })
await runtimeUpgrade({ when: 'pre', deploymentState: { status: 'deployed', deployingVersion: '1.0.1' } })

expect(mockDebugger.info).toHaveBeenCalledWith(
'Skipping the runtime upgrade procedure because this is initial installation',
Expand All @@ -103,9 +94,7 @@ describe('runtimeUpgrade', () => {
})

it('should execute global pre operations', async () => {
mockGetDeploymentState.mockResolvedValue({ version: '1.0.0', deployingVersion: '1.0.1' })

await runtimeUpgrade({ when: 'pre' })
await runtimeUpgrade({ when: 'pre', deploymentState: { version: '1.0.0', deployingVersion: '1.0.1' } })

expect(mockPreOperation).toHaveBeenCalledWith({
debug: mockDebugger,
Expand All @@ -114,9 +103,7 @@ describe('runtimeUpgrade', () => {
})

it('should execute global post operations', async () => {
mockGetDeploymentState.mockResolvedValue({ version: '1.0.0', deployingVersion: '1.0.1' })

await runtimeUpgrade({ when: 'post' })
await runtimeUpgrade({ when: 'post', deploymentState: { version: '1.0.0', deployingVersion: '1.0.1' } })

expect(mockPostOperation).toHaveBeenCalledWith({
debug: mockDebugger,
Expand All @@ -131,9 +118,7 @@ describe('runtimeUpgrade', () => {
pre: mockPreOperation,
})

mockGetDeploymentState.mockResolvedValue({ version: '1.0.0' })

await runtimeUpgrade({ when: 'post' })
await runtimeUpgrade({ when: 'post', deploymentState: { version: '1.0.0' } })

expect(mockPreOperation).not.toHaveBeenCalled()
})
Expand Down Expand Up @@ -161,9 +146,8 @@ describe('runtimeUpgrade', () => {
})

it('should execute application-specific operations with ArgoCD waits', async () => {
mockGetDeploymentState.mockResolvedValue({ version: '1.0.0', deployingVersion: '1.0.1' })
mockGetApplications.mockResolvedValue(['istio-operator'])
await runtimeUpgrade({ when: 'post' })
await runtimeUpgrade({ when: 'post', deploymentState: { version: '1.0.0', deployingVersion: '1.0.1' } })

expect(mockWaitForArgoCDAppSync).toHaveBeenCalledWith('istio-operator', mockCustomApi, mockDebugger)
expect(mockWaitForArgoCDAppHealthy).toHaveBeenCalledWith('istio-operator', mockCustomApi, mockDebugger)
Expand All @@ -176,10 +160,9 @@ describe('runtimeUpgrade', () => {
})

it('should not execute application operations for wrong phase', async () => {
mockGetDeploymentState.mockResolvedValue({ version: '1.0.0', deployingVersion: '1.0.1' })
mockGetApplications.mockResolvedValue(['argocd'])

await runtimeUpgrade({ when: 'pre' })
await runtimeUpgrade({ when: 'pre', deploymentState: { version: '1.0.0', deployingVersion: '1.0.1' } })

expect(mockWaitForArgoCDAppSync).toHaveBeenCalledWith('argocd', mockCustomApi, mockDebugger)
expect(mockWaitForArgoCDAppHealthy).toHaveBeenCalledWith('argocd', mockCustomApi, mockDebugger)
Expand Down Expand Up @@ -207,9 +190,7 @@ describe('runtimeUpgrade', () => {
})

it('should execute both global and application operations in correct order', async () => {
mockGetDeploymentState.mockResolvedValue({ version: '1.0.0', deployingVersion: '1.0.1' })

await runtimeUpgrade({ when: 'pre' })
await runtimeUpgrade({ when: 'pre', deploymentState: { version: '1.0.0', deployingVersion: '1.0.1' } })

expect(mockGlobalPre).toHaveBeenCalledWith({
debug: mockDebugger,
Expand Down
15 changes: 9 additions & 6 deletions src/common/runtime-upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import semver from 'semver'
import { getApplications } from 'src/cmd/apply-as-apps'
import { terminal } from './debug'
import { deployEssential } from './hf'
import { getDeploymentState, k8s, waitForArgoCDAppHealthy, waitForArgoCDAppSync } from './k8s'
import { DeploymentState, k8s, waitForArgoCDAppHealthy, waitForArgoCDAppSync } from './k8s'
import { RuntimeUpgradeContext, RuntimeUpgrades, runtimeUpgrades } from './runtime-upgrades/runtime-upgrades'

interface RuntimeUpgradeArgs {
when: string
deploymentState?: DeploymentState
}

export async function runtimeUpgrade({ when }: RuntimeUpgradeArgs): Promise<void> {
export async function runtimeUpgrade({ when, deploymentState }: RuntimeUpgradeArgs): Promise<void> {
const d = terminal('cmd:upgrade:runtimeUpgrade')
const deploymentState = await getDeploymentState()

if (!deploymentState?.version || !deploymentState?.deployingVersion) {
d.info('Skipping the runtime upgrade procedure because this is initial installation')
Expand Down Expand Up @@ -69,9 +69,12 @@ export async function runtimeUpgrade({ when }: RuntimeUpgradeArgs): Promise<void
d.info(
`Runtime upgrade operations detected for version ${upgrade.version}, application: ${applicationName}`,
)
// Wait for the ArgoCD app to be synced and healthy before running the operation
await waitForArgoCDAppSync(applicationName, k8s.custom(), d)
await waitForArgoCDAppHealthy(applicationName, k8s.custom(), d)
// In dev-mode, the app is likely not syncing in the cluster
if (process.env.NODE_ENV !== 'development' || applicationName !== 'apl-operator-apl-operator') {
// Wait for the ArgoCD app to be synced and healthy before running the operation
await waitForArgoCDAppSync(applicationName, k8s.custom(), d)
await waitForArgoCDAppHealthy(applicationName, k8s.custom(), d)
}
//execute the application-specific operation
await applicationOperation(context)
}
Expand Down
Loading