Skip to content

Commit f601e38

Browse files
committed
fix: apl-operator upgrade
1 parent 939e680 commit f601e38

File tree

16 files changed

+1076
-848
lines changed

16 files changed

+1076
-848
lines changed

package-lock.json

Lines changed: 806 additions & 750 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@types/async-retry": "1.4.9",
5555
"@types/debug": "4.1.12",
5656
"@types/express": "5.0.3",
57+
"@types/fs-extra": "^11.0.4",
5758
"@types/ignore-walk": "4.0.3",
5859
"@types/jest": "^30.0.0",
5960
"@types/js-yaml": "4.0.9",

src/cmd/apply-as-apps.ts

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import { mkdirSync, rmdirSync } from 'fs'
2-
import { pathExists } from 'fs-extra'
3-
import { writeFile } from 'fs/promises'
1+
import { mkdirSync, rmdirSync, existsSync } from 'fs'
2+
import { readFile, writeFile } from 'fs/promises'
43
import { cleanupHandler, prepareEnvironment } from 'src/common/cli'
54
import { logLevelString, terminal } from 'src/common/debug'
65
import { hf } from 'src/common/hf'
7-
import { isResourcePresent, k8s, patchContainerResourcesOfSts } from 'src/common/k8s'
6+
import { appRevisionMatches, k8s, patchArgoCdApp, patchContainerResourcesOfSts } from 'src/common/k8s'
87
import { getFilename, loadYaml } from 'src/common/utils'
98
import { getImageTag, objectToYaml } from 'src/common/values'
109
import { appPatches, genericPatch } from 'src/applicationPatches.json'
1110
import { getParsedArgs, HelmArguments, helmOptions, setParsedArgs } from 'src/common/yargs'
1211
import { Argv, CommandModule } from 'yargs'
1312
import { $ } from 'zx'
14-
import { V1ResourceRequirements } from '@kubernetes/client-node'
13+
import { ApiException, V1ResourceRequirements } from '@kubernetes/client-node'
1514
import { env } from '../common/envalid'
1615

1716
const cmdName = getFilename(__filename)
@@ -92,27 +91,24 @@ const getArgocdAppManifest = (release: HelmRelease, values: Record<string, any>,
9291
const setFinalizers = async (name: string) => {
9392
d.info(`Setting finalizers for ${name}`)
9493
const resPatch =
95-
await $`kubectl -n argocd patch application ${name} -p '{"metadata": {"finalizers": ["resources-finalizer.argocd.argoproj.io"]}}' --type merge`
94+
await $`kubectl -n argocd patch applications.argoproj.io ${name} -p '{"metadata": {"finalizers": ["resources-finalizer.argocd.argoproj.io"]}}' --type merge`
9695
if (resPatch.exitCode !== 0) {
9796
throw new Error(`Failed to set finalizers for ${name}: ${resPatch.stderr}`)
9897
}
9998
}
10099

101100
const getFinalizers = async (name: string): Promise<string[]> => {
102-
const res = await $`kubectl -n argocd get application ${name} -o jsonpath='{.metadata.finalizers}'`
101+
const res = await $`kubectl -n argocd get applications.argoproj.io ${name} -o jsonpath='{.metadata.finalizers}'`
103102
return res.stdout ? JSON.parse(res.stdout) : []
104103
}
105104

106-
const removeApplication = async (release: HelmRelease): Promise<void> => {
107-
const name = getAppName(release)
108-
if (!(await isResourcePresent('application', name, 'argocd'))) return
109-
105+
const removeApplication = async (name: string): Promise<void> => {
110106
try {
111107
const finalizers = await getFinalizers(name)
112108
if (!finalizers.includes('resources-finalizer.argocd.argoproj.io')) {
113109
await setFinalizers(name)
114110
}
115-
const resDelete = await $`kubectl -n argocd delete application ${name}`
111+
const resDelete = await $`kubectl -n argocd delete applications.argoproj.io ${name}`
116112
d.info(resDelete.stdout.toString().trim())
117113
} catch (e) {
118114
d.error(`Failed to delete application ${name}: ${e.message}`)
@@ -149,25 +145,62 @@ async function patchArgocdResources(release: HelmRelease, values: Record<string,
149145
}
150146
}
151147

148+
const getApplications = async (): Promise<string[]> => {
149+
const res = await $`kubectl get application.argoproj.io -n argocd -oname`
150+
return res.stdout.split('\n')
151+
}
152+
152153
const writeApplicationManifest = async (release: HelmRelease, otomiVersion: string): Promise<void> => {
153154
const appName = `${release.namespace}-${release.name}`
154155
const applicationPath = `${appsDir}/${appName}.yaml`
155156
const valuesPath = `${valuesDir}/${appName}.yaml`
156157
let values = {}
157158

158-
if (await pathExists(valuesPath)) values = (await loadYaml(valuesPath)) || {}
159+
if (existsSync(valuesPath)) values = (await loadYaml(valuesPath)) || {}
159160
const manifest = getArgocdAppManifest(release, values, otomiVersion)
160161
await writeFile(applicationPath, objectToYaml(manifest))
161162

162163
await patchArgocdResources(release, values)
163164
}
164165

165-
export const applyAsApps = async (argv: HelmArguments): Promise<void> => {
166+
const getAplOperatorValues = async (): Promise<string> => {
167+
await hf({
168+
labelOpts: ['name=apl-operator'],
169+
logLevel: logLevelString(),
170+
args: ['write-values', `--output-file-template=${valuesDir}/{{.Release.Namespace}}-{{.Release.Name}}.yaml`],
171+
})
172+
return await readFile(`${valuesDir}/apl-operator-apl-operator.yaml`, 'utf-8')
173+
}
174+
175+
export const applyAsApps = async (argv: HelmArguments): Promise<boolean> => {
166176
const helmfileSource = argv.file?.toString() || 'helmfile.d/'
167177
d.info(`Parsing helm releases defined in ${helmfileSource}`)
168178
setup()
169179
const otomiVersion = await getImageTag()
170-
180+
try {
181+
const expectedRevision = env.APPS_REVISION || otomiVersion
182+
d.info('Checking running revision of apl-operator...')
183+
const operatorRevisionMatches = await appRevisionMatches(
184+
'apl-operator-apl-operator',
185+
env.APPS_REVISION || otomiVersion,
186+
k8s.custom(),
187+
)
188+
if (operatorRevisionMatches) {
189+
d.info(`Expected revision ${expectedRevision} found for apl-operator.`)
190+
} else {
191+
const values = await getAplOperatorValues()
192+
d.info(`Updating apl-operator application to revision ${expectedRevision}.`)
193+
await patchArgoCdApp('apl-operator-apl-operator', expectedRevision, values, k8s.custom())
194+
d.info('Skipping further updates until apl-operator has restarted.')
195+
return false
196+
}
197+
} catch (error) {
198+
if (error instanceof ApiException && error.code === 404) {
199+
d.info('apl-operator application not found, continuing')
200+
} else {
201+
throw error
202+
}
203+
}
171204
const res = await hf({
172205
fileOpts: argv.file,
173206
labelOpts: argv.label,
@@ -186,6 +219,7 @@ export const applyAsApps = async (argv: HelmArguments): Promise<void> => {
186219
const errors: Array<any> = []
187220
// Generate JSON object with all helmfile releases defined in helmfile.d
188221
const releases: [] = JSON.parse(res.stdout.toString())
222+
const currentApplications = await getApplications()
189223
await Promise.allSettled(
190224
releases.map(async (release: HelmRelease) => {
191225
try {
@@ -197,7 +231,11 @@ export const applyAsApps = async (argv: HelmArguments): Promise<void> => {
197231

198232
if (release.installed) await writeApplicationManifest(release, otomiVersion)
199233
else {
200-
await removeApplication(release)
234+
const appName = getAppName(release)
235+
const resourceName = `application.argoproj.io/${appName}`
236+
if (currentApplications.includes(resourceName)) {
237+
await removeApplication(appName)
238+
}
201239
}
202240
} catch (e) {
203241
errors.push(e)
@@ -218,6 +256,7 @@ export const applyAsApps = async (argv: HelmArguments): Promise<void> => {
218256
errors.map((e) => d.error(e))
219257
d.error(`Not all applications has been deployed successfully`)
220258
}
259+
return true
221260
}
222261

223262
export const module: CommandModule = {

src/cmd/apply.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ const applyAll = async () => {
8686
await $`kubectl apply -f charts/tekton-triggers/crds --server-side`
8787
d.info('Deploying essential manifests')
8888
await $`kubectl apply -f ${templateFile}`
89+
let applyCompleted = false
8990
if (initialInstall) {
9091
d.info('Deploying charts containing label stage=prep')
9192
await hf(
@@ -116,13 +117,15 @@ const applyAll = async () => {
116117
// We still need to deploy all teams because some settings depend on platform apps.
117118
// Note that team-ns-admin contains ingress for platform apps.
118119
const params = cloneDeep(argv)
119-
//TODO here happens the real installation of the apps
120-
await applyAsApps(params)
120+
applyCompleted = await applyAsApps(params)
121121
}
122-
123122
if (!initialInstall) {
124-
await upgrade({ when: 'post' })
125-
await runtimeUpgrade({ when: 'post' })
123+
if (applyCompleted) {
124+
await upgrade({ when: 'post' })
125+
await runtimeUpgrade({ when: 'post' })
126+
} else {
127+
d.info('Apply step not completed, skipping upgrade checks')
128+
}
126129
}
127130
if (!(env.isDev && env.DISABLE_SYNC)) {
128131
await commit(initialInstall)

src/cmd/bootstrap.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { randomUUID } from 'crypto'
2-
import { copy, pathExists } from 'fs-extra'
3-
import { copyFile, mkdir, readFile, writeFile } from 'fs/promises'
2+
import { existsSync } from 'fs'
3+
import { copyFile, cp, mkdir, readFile, writeFile } from 'fs/promises'
44
import { generate as generatePassword } from 'generate-password'
55
import { cloneDeep, get, isEmpty, merge, set } from 'lodash'
66
import { pki } from 'node-forge'
@@ -39,7 +39,7 @@ export const bootstrapSops = async (
3939
encrypt,
4040
gucci,
4141
loadYaml,
42-
pathExists,
42+
pathExists: existsSync,
4343
getKmsSettings,
4444
readFile,
4545
terminal,
@@ -78,7 +78,7 @@ export const bootstrapSops = async (
7878
}
7979
}
8080

81-
const exists = await deps.pathExists(targetPath)
81+
const exists = deps.pathExists(targetPath)
8282
d.log(`Creating sops file for provider ${provider}`)
8383
const output = (await deps.gucci(templatePath, obj, true)) as string
8484
await deps.writeFile(targetPath, output)
@@ -230,7 +230,7 @@ export const getUsers = (originalInput: any, deps = { generatePassword, addIniti
230230
}
231231

232232
export const copyBasicFiles = async (
233-
deps = { copy, copyFile, copySchema, mkdir, pathExists, terminal },
233+
deps = { copy: cp, copyFile, copySchema, mkdir, pathExists: existsSync, terminal },
234234
): Promise<void> => {
235235
const d = deps.terminal(`cmd:${cmdName}:copyBasicFiles`)
236236
const { ENV_DIR } = env
@@ -242,15 +242,15 @@ export const copyBasicFiles = async (
242242
])
243243
d.info('Copied bin files')
244244
await deps.mkdir(`${ENV_DIR}/.vscode`, { recursive: true })
245-
await deps.copy(`${rootDir}/.values/.vscode`, `${ENV_DIR}/.vscode`)
245+
await deps.copy(`${rootDir}/.values/.vscode`, `${ENV_DIR}/.vscode`, { recursive: true })
246246
d.info('Copied vscode folder')
247247

248248
await deps.copySchema()
249249

250250
// only copy sample files if a real one is not found
251251
await Promise.allSettled(
252252
['.secrets.sample']
253-
.filter(async (val) => !(await deps.pathExists(`${ENV_DIR}/${val.replace(/\.sample$/g, '')}`)))
253+
.filter((val) => !deps.pathExists(`${ENV_DIR}/${val.replace(/\.sample$/g, '')}`))
254254
.map(async (val) => deps.copyFile(`${rootDir}/.values/${val}`, `${ENV_DIR}/${val}`)),
255255
)
256256

@@ -276,7 +276,7 @@ export const processValues = async (
276276
getStoredClusterSecrets,
277277
getKmsValues,
278278
writeValues,
279-
pathExists,
279+
pathExists: existsSync,
280280
hfValues,
281281
validateValues,
282282
generateSecrets,
@@ -301,7 +301,7 @@ export const processValues = async (
301301
d.log(`Loading repo values from ${ENV_DIR}`)
302302
// we can only read values from ENV_DIR if we can determine cluster.providers
303303
storedSecrets = {}
304-
if (await deps.pathExists(`${ENV_DIR}/env/settings/cluster.yaml`)) {
304+
if (deps.pathExists(`${ENV_DIR}/env/settings/cluster.yaml`)) {
305305
await deps.decrypt()
306306
originalInput = (await deps.hfValues({ defaultValues: true })) || {}
307307
}
@@ -429,7 +429,7 @@ export const createCustomCA = (deps = { terminal, pki, writeValues }): Record<st
429429

430430
export const bootstrap = async (
431431
deps = {
432-
pathExists,
432+
pathExists: existsSync,
433433
getDeploymentState,
434434
getImageTag,
435435
getCurrentVersion,
@@ -458,7 +458,7 @@ export const bootstrap = async (
458458
if (prevVersion && prevTag && version === prevVersion && tag === prevTag) return
459459
}
460460
const { ENV_DIR } = env
461-
const hasOtomi = await deps.pathExists(`${ENV_DIR}/bin/otomi`)
461+
const hasOtomi = deps.pathExists(`${ENV_DIR}/bin/otomi`)
462462

463463
const otomiImage = `linode/apl-core:${tag}`
464464
d.log(`Installing artifacts from ${otomiImage}`)

src/cmd/migrate.ts

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { ApiException } from '@kubernetes/client-node'
22
import { randomUUID } from 'crypto'
33
import { diff } from 'deep-diff'
4-
import { copy, createFileSync, move, pathExists, renameSync, rm } from 'fs-extra'
5-
import { mkdir, readFile, writeFile } from 'fs/promises'
4+
import { existsSync, renameSync, rmSync, writeFileSync } from 'fs'
5+
import { cp, mkdir, readFile, rename as fsRename, writeFile } from 'fs/promises'
66
import { glob, globSync } from 'glob'
77
import { cloneDeep, each, get, isObject, isUndefined, mapKeys, mapValues, omit, pick, pull, set, unset } from 'lodash'
88
import { basename, dirname, join } from 'path'
@@ -20,6 +20,7 @@ import { parse } from 'yaml'
2020
import { Argv } from 'yargs'
2121
import { $, cd } from 'zx'
2222
import { k8s } from '../common/k8s'
23+
import { ARGOCD_APP_PARAMS } from '../common/constants'
2324

2425
const cmdName = getFilename(__filename)
2526

@@ -62,27 +63,27 @@ export type Changes = Array<Change>
6263
export const deleteFile = async (
6364
relativeFilePath: string,
6465
dryRun = false,
65-
deps = { pathExists, renameSync, terminal, copy, rm },
66+
deps = { existsSync, renameSync, terminal, rmSync },
6667
): Promise<void> => {
6768
const d = deps.terminal(`cmd:${cmdName}:rename`)
6869
const path = `${env.ENV_DIR}/${relativeFilePath}`
69-
if (!(await deps.pathExists(path))) {
70+
if (!deps.existsSync(path)) {
7071
d.warn(`File does not exist: "${path}". Already removed?`)
7172
return
7273
}
7374
if (!dryRun) {
74-
await deps.rm(path)
75+
deps.rmSync(path)
7576
}
7677
}
7778

7879
export const rename = async (
7980
oldName: string,
8081
newName: string,
8182
dryRun = false,
82-
deps = { pathExists, renameSync, terminal, move, copy, rm },
83+
deps = { pathExists: existsSync, renameSync, terminal, move: fsRename, cp, rmSync },
8384
): Promise<void> => {
8485
const d = deps.terminal(`cmd:${cmdName}:rename`)
85-
if (!(await deps.pathExists(`${env.ENV_DIR}/${oldName}`))) {
86+
if (!deps.pathExists(`${env.ENV_DIR}/${oldName}`)) {
8687
d.warn(`File does not exist: "${env.ENV_DIR}/${oldName}". Already renamed?`)
8788
return
8889
}
@@ -92,7 +93,7 @@ export const rename = async (
9293
if (oldName.includes('.yaml') && !oldName.includes('secrets.')) {
9394
const lastSlashPosOld = oldName.lastIndexOf('/') + 1
9495
const tmpOld = `${oldName.substring(0, lastSlashPosOld)}secrets.${oldName.substring(lastSlashPosOld)}`
95-
if (await deps.pathExists(`${env.ENV_DIR}/${secretsCompanionOld}`)) {
96+
if (deps.pathExists(`${env.ENV_DIR}/${secretsCompanionOld}`)) {
9697
secretsCompanionOld = tmpOld
9798
const lastSlashPosNew = oldName.lastIndexOf('/') + 1
9899
secretsCompanionNew = `${newName.substring(0, lastSlashPosNew)}secrets.${newName.substring(lastSlashPosNew)}`
@@ -105,16 +106,16 @@ export const rename = async (
105106
if (secretsCompanionOld) {
106107
// we also rename the secret companion
107108
await deps.move(`${env.ENV_DIR}/${secretsCompanionOld}`, `${env.ENV_DIR}/${secretsCompanionNew}`)
108-
if (await deps.pathExists(`${env.ENV_DIR}/${secretsCompanionOld}.dec`))
109+
if (deps.pathExists(`${env.ENV_DIR}/${secretsCompanionOld}.dec`))
109110
// and remove the old decrypted file
110-
await deps.rm(`${env.ENV_DIR}/${secretsCompanionOld}.dec`)
111+
deps.rmSync(`${env.ENV_DIR}/${secretsCompanionOld}.dec`)
111112
}
112113
} catch (e) {
113114
if (e.message === 'dest already exists.') {
114115
// we were given a folder that already exists, which is allowed,
115116
// so we defer to copying the contents and remove the source
116-
await deps.copy(`${env.ENV_DIR}/${oldName}`, `${env.ENV_DIR}/${newName}`, { preserveTimestamps: true })
117-
await deps.rm(`${env.ENV_DIR}/${oldName}`, { recursive: true, force: true })
117+
await deps.cp(`${env.ENV_DIR}/${oldName}`, `${env.ENV_DIR}/${newName}`, { preserveTimestamps: true })
118+
deps.rmSync(`${env.ENV_DIR}/${oldName}`, { recursive: true, force: true })
118119
}
119120
}
120121
}
@@ -274,7 +275,7 @@ const networkPoliciesMigration = async (values: Record<string, any>): Promise<vo
274275
servicePermissions.filter((s: any) => s !== 'networkPolicy'),
275276
)
276277

277-
createFileSync(`${env.ENV_DIR}/env/teams/netpols.${teamName}.yaml`)
278+
writeFileSync(`${env.ENV_DIR}/env/teams/netpols.${teamName}.yaml`, '')
278279
let services = get(values, `teamConfig.${teamName}.services`)
279280
if (!services || services.length === 0) return
280281
const valuesToWrite = {
@@ -466,10 +467,7 @@ async function appExists(name: string): Promise<boolean> {
466467
return await checkExists(
467468
async () =>
468469
await k8s.custom().getNamespacedCustomObject({
469-
group: 'argoproj.io',
470-
version: 'v1alpha1',
471-
namespace: 'argocd',
472-
plural: 'applications',
470+
...ARGOCD_APP_PARAMS,
473471
name,
474472
}),
475473
)
@@ -650,7 +648,7 @@ export const applyChanges = async (
650648
for (const c of changes) {
651649
c.renamings?.forEach((entry) => each(entry, async (newName, oldName) => deps.rename(oldName, newName, dryRun)))
652650
// same for any new file additions
653-
c.fileAdditions?.forEach((path) => createFileSync(`${env.ENV_DIR}/${path}`))
651+
c.fileAdditions?.forEach((path) => writeFileSync(`${env.ENV_DIR}/${path}`, ''))
654652
}
655653
// only then can we get the values and do mutations on them
656654
const prevValues = (await deps.hfValues({ filesOnly: true })) as Record<string, any>
@@ -879,7 +877,7 @@ export const migrate = async (): Promise<boolean> => {
879877
const d = terminal(`cmd:${cmdName}:migrate`)
880878
const argv: Arguments = getParsedArgs()
881879
d.log('Migrating values')
882-
if (await pathExists(`${env.ENV_DIR}/env/settings.yaml`)) {
880+
if (existsSync(`${env.ENV_DIR}/env/settings.yaml`)) {
883881
d.log('Detected the old values file structure')
884882
await migrateLegacyValues(env.ENV_DIR)
885883
}

0 commit comments

Comments
 (0)