@leverege/build-tools 2.94.0 → 2.96.0-quinn.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.vscode/launch.json +14 -0
  2. package/package.json +5 -2
  3. package/src/Docker.mjs +3 -2
  4. package/src/Utils.mjs +80 -6
  5. package/src/artifacts/Utils.mjs +1 -1
  6. package/src/bash-funcs +1 -1
  7. package/src/firebaseDeploy.mjs +8 -2
  8. package/src/firebaseServe.mjs +8 -2
  9. package/src/git-tar.sh +1 -1
  10. package/src/helm-charts/emailer/values-local.yaml +1 -8
  11. package/src/helm-charts/prom-operator/helmup.plugin +1 -1
  12. package/src/helm-charts/traefik/helmup.plugin +1 -1
  13. package/src/helm-charts/velero/README.md +135 -0
  14. package/src/helm-charts/velero/helmup.bootstrap +1 -0
  15. package/src/helm-charts/velero/helmup.plugin +29 -2
  16. package/src/helm-charts/velero/values-local.yaml +1 -1
  17. package/src/helmup.sh +49 -41
  18. package/src/service-man/ServiceMan.mjs +75 -0
  19. package/src/service-man/config/CommandLine.mjs +199 -0
  20. package/src/service-man/config/Config.mjs +203 -0
  21. package/src/service-man/config/VersionCheck.mjs +79 -0
  22. package/src/service-man/config/iam-roles/leveregeBigQueryReader.json +10 -0
  23. package/src/service-man/config/iam-roles/leveregeBigQueryWriter.json +13 -0
  24. package/src/service-man/config/iam-roles/leveregeCloudFunctionsManager.json +12 -0
  25. package/src/service-man/config/iam-roles/leveregeFirebaseHosting.json +9 -0
  26. package/src/service-man/config/iam-roles/leveregeFirebaseMessaging.json +13 -0
  27. package/src/service-man/config/iam-roles/leveregeFirebaseUpdates.json +10 -0
  28. package/src/service-man/config/iam-roles/leveregeLogsViewer.json +8 -0
  29. package/src/service-man/config/iam-roles/leveregePubSub.json +13 -0
  30. package/src/service-man/config/iam-roles/leveregeSecretsViewer.json +9 -0
  31. package/src/service-man/config/iam-roles/leveregeStorageObjectAdmin.json +21 -0
  32. package/src/service-man/config/iam-roles/leveregeStorageObjectReader.json +13 -0
  33. package/src/service-man/config/iam-roles/leveregeSvcAcctActor.json +8 -0
  34. package/src/service-man/config/iam-roles/leveregeSvcAcctTokenCreator.json +8 -0
  35. package/src/service-man/config/index.mjs +7 -0
  36. package/src/service-man/config/secrets/authz-jwt.json +6 -0
  37. package/src/service-man/config/secrets/authz-postgres.json +7 -0
  38. package/src/service-man/config/secrets/db-timescale-dense.json +8 -0
  39. package/src/service-man/config/secrets/jwt-options.json +7 -0
  40. package/src/service-man/config/secrets/mail-gun-api-key.json +6 -0
  41. package/src/service-man/config/secrets/npmrc.json +6 -0
  42. package/src/service-man/config/secrets/oidc-client-secret.json +6 -0
  43. package/src/service-man/config/secrets/postgresql-password.json +8 -0
  44. package/src/service-man/config/secrets/session-secret.json +6 -0
  45. package/src/service-man/config/secrets/twilio.json +8 -0
  46. package/src/service-man/config/service-accounts/api-server.json +12 -0
  47. package/src/service-man/config/service-accounts/authz-server.json +5 -0
  48. package/src/service-man/config/service-accounts/db-curator.json +8 -0
  49. package/src/service-man/config/service-accounts/emailer.json +7 -0
  50. package/src/service-man/config/service-accounts/fota-server.json +6 -0
  51. package/src/service-man/config/service-accounts/geotile-server.json +8 -0
  52. package/src/service-man/config/service-accounts/imagine.json +6 -0
  53. package/src/service-man/config/service-accounts/message-processor.json +6 -0
  54. package/src/service-man/config/service-accounts/messenger.json +7 -0
  55. package/src/service-man/config/service-accounts/pubsub-pulse.json +6 -0
  56. package/src/service-man/config/service-accounts/push-notifier.json +8 -0
  57. package/src/service-man/config/service-accounts/reason.json +5 -0
  58. package/src/service-man/config/service-accounts/resource-server.json +9 -0
  59. package/src/service-man/config/service-accounts/rule-engine.json +7 -0
  60. package/src/service-man/config/service-accounts/scheduler.json +6 -0
  61. package/src/service-man/config/service-accounts/transponder.json +15 -0
  62. package/src/service-man/gcloud-k8s/RolesManager.mjs +41 -0
  63. package/src/service-man/gcloud-k8s/SecretsManager.mjs +182 -0
  64. package/src/service-man/gcloud-k8s/ServiceAccountManager.mjs +246 -0
  65. package/src/service-man/gcloud-k8s/VersionManager.mjs +114 -0
  66. package/src/service-man/gcloud-k8s/index.mjs +11 -0
  67. package/src/service-man/service-man.mjs +18 -0
  68. package/src/service-man/utils/index.mjs +83 -0
@@ -17,6 +17,20 @@
17
17
  "env": {
18
18
  },
19
19
  "outputCapture": "std"
20
+ },
21
+ {
22
+ "name": "service-man (launch)",
23
+ "type": "node",
24
+ "request": "launch",
25
+ "program": "${workspaceFolder}/src/service-man/service-man.mjs",
26
+ "args": ["--dump-configs","dbg-cfgs"],
27
+ "cwd": "/Users/john/Work/leverege/k8s/tst-builder",
28
+ "runtimeExecutable": "node",
29
+ "console": "integratedTerminal",
30
+ "skipFiles": ["<node_internals>/**"],
31
+ "env": {
32
+ "NODE_ENV": "development"
33
+ }
20
34
  }
21
35
  ]
22
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.94.0",
3
+ "version": "2.96.0-quinn.1",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -40,6 +40,7 @@
40
40
  "getjson": "src/getjson.mjs",
41
41
  "git-tar": "src/git-tar.sh",
42
42
  "helm-audit": "src/helm-audit.mjs",
43
+ "helmboot": "src/helmup.sh",
43
44
  "helmcycle": "src/helmup.sh",
44
45
  "helmdn": "src/helmdn.sh",
45
46
  "helminit": "src/helmup.sh",
@@ -64,6 +65,7 @@
64
65
  "refresh-npm-token": "src/refresh-npm-token.mjs",
65
66
  "refresh-py-idx": "src/refresh-py-idx.sh",
66
67
  "send-to-slack": "src/send-to-slack.mjs",
68
+ "service-man": "src/service-man/service-man.mjs",
67
69
  "snapshot-cnpg": "src/snapshot-cnpg.mjs",
68
70
  "tag-release": "src/tag-release.mjs",
69
71
  "unleash": "src/unleash.mjs",
@@ -99,12 +101,13 @@
99
101
  "simple-git": "^3.30.0",
100
102
  "sloc": "^0.3.2",
101
103
  "superstruct": "^2.0.2",
104
+ "tmp": "^0.2.5",
102
105
  "toml": "^3.0.0",
103
106
  "zx": "^8.8.5"
104
107
  },
105
108
  "devDependencies": {
106
109
  "@leverege/eslint-config-leverege": "^5.1.1",
107
- "chai": "^6.2.1",
110
+ "chai": "^6.2.2",
108
111
  "mocha": "^11.7.5",
109
112
  "npm": "^11.7.0"
110
113
  }
package/src/Docker.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable security/detect-non-literal-fs-filename */
1
2
  import fs from 'node:fs'
2
3
  import path from 'node:path'
3
4
  import url from 'node:url'
@@ -23,7 +24,7 @@ const DIRNAME = path.dirname( url.fileURLToPath( import.meta.url ) )
23
24
  //
24
25
  // See the latest docker images here => https://hub.docker.com/_/node
25
26
  //
26
- const dockerfileNodeVersion = process.env.DOCKER_BUILD_NODE_VERSION || '22.21.1-alpine3.23' // TODO: 1/1/26 => '24.12.0-alpine3.23'
27
+ const dockerfileNodeVersion = process.env.DOCKER_BUILD_NODE_VERSION || '24.12.0-alpine3.23'
27
28
  const dockerfileNpmVersion = process.env.DOCKER_BUILD_NPM_VERSION || '11.6.0'
28
29
 
29
30
  // The contents of these variables were initially located in files that live in
@@ -83,7 +84,7 @@ const generateDockerfile = async ( options ) => {
83
84
  const gitReference = await shellCmd( 'git rev-parse --short HEAD' )
84
85
  const settings = { gitReference, ...defaultSettings, ...options }
85
86
  const dockerTemplate = options.isNodeJsProject ? 'dockerfileTemplateNodeJs' : 'dockerfileTemplatePython'
86
- const dockerfileTemplate = fs.readFileSync( path.join( DIRNAME, `./templates/${dockerTemplate}.hbs` ), 'utf8' ) // eslint-disable-line security/detect-non-literal-fs-filename
87
+ const dockerfileTemplate = fs.readFileSync( path.join( DIRNAME, `./templates/${dockerTemplate}.hbs` ), 'utf8' )
87
88
 
88
89
  // defaults to the current "official" version of node unless it gets
89
90
  // overridden in the leverege.nodeimg package.json stanza
package/src/Utils.mjs CHANGED
@@ -39,11 +39,42 @@ export const warning = ( warningText ) => {
39
39
  console.error( `\n${chalk.yellow.bold( fullWarning )}` )
40
40
  }
41
41
 
42
- export const errorExit = ( error, opts = {} ) => {
43
- const { errorCode = 1 } = opts
44
- console.error( `\n${chalk.red.bold( error )}\n` )
45
- process.exit( errorCode )
42
+ export const errorExit = ( arg, opts = {} ) => {
43
+ const exitCode =
44
+ typeof opts.exitCode === 'number' ? opts.exitCode :
45
+ ( arg && typeof arg.exitCode === 'number' ) ? arg.exitCode : 1
46
+
47
+ // lets figure out what to print
48
+ let message
49
+ let details
50
+
51
+ if ( arg instanceof Error ) {
52
+ message = arg.message || 'Error'
53
+ details = arg.stack
54
+ } else if ( typeof arg === 'string' ) {
55
+ message = arg
56
+ } else if ( arg && typeof arg === 'object' ) {
57
+ message = 'Error'
58
+ details = JSON.stringify( arg, null, 2 )
59
+ } else if ( arg != null ) {
60
+ message = String( arg )
61
+ }
62
+
63
+ console.error( `\n${chalk.red.bold( message )}\n` )
64
+ if ( details && details !== message ) {
65
+ console.error( `${details}\n` )
66
+ }
67
+
68
+ process.exit( exitCode )
46
69
  }
70
+
71
+ /**
72
+ * Clears the screen and optionally homes the cursor.
73
+ */
74
+ export const clear = ( home = true ) => {
75
+ log( `${home ? '\x1B[0f' : ''}\x1B[2J` )
76
+ }
77
+
47
78
  /* eslint-enable no-console */
48
79
 
49
80
  export const isHex = ( hex ) => {
@@ -54,6 +85,22 @@ export const isString = ( s ) => {
54
85
  return typeof s === 'string' || s instanceof String
55
86
  }
56
87
 
88
+ /**
89
+ * * Sets first char to Upper
90
+ * */
91
+ export const capitalize = ( norm ) => {
92
+ return norm.charAt( 0 ).toUpperCase() + norm.slice( 1 )
93
+ }
94
+
95
+ /**
96
+ * A simple mS sleep delay.
97
+ */
98
+ export const sleep = ( ms = 0 ) => {
99
+ return new Promise( ( resolve ) => {
100
+ setTimeout( resolve, ms )
101
+ } )
102
+ }
103
+
57
104
  /**
58
105
  * General "yes" to proceed function.
59
106
  */
@@ -226,7 +273,7 @@ export const gitRepoIsDirty = async () => {
226
273
 
227
274
  export const parseYamlFile = async ( filePath ) => {
228
275
  try {
229
- const fileContent = await fs.promises.readFile( filePath, 'utf8' )
276
+ const fileContent = await fs.promises.readFile( filePath, 'utf8' ) /* eslint-disable-line security/detect-non-literal-fs-filename */
230
277
  return YAML.load( fileContent )
231
278
  } catch ( error ) {
232
279
  throw new Error( `Failed to parse YAML file ${filePath}: ${error.message}` )
@@ -538,7 +585,7 @@ export const getRepositoryDescr = async () => {
538
585
  const helmChart = await parseHelmChart()
539
586
  const rootPackageJson = await parseJsonFile( `${gitRoot}/package.json` )
540
587
  const rootPyProjectToml = await parsePyProjectToml( `${gitRoot}/pyproject.toml` )
541
- const isYarnProject = fs.existsSync( path.join( gitRoot, 'yarn.lock' ) ) || fs.existsSync( path.join( gitRoot, '.yarn' ) )
588
+ const isYarnProject = getIsYarnProject( gitRoot )
542
589
 
543
590
  return {
544
591
  currentDir : process.cwd(),
@@ -683,3 +730,30 @@ export const analyzeRepository = async ( giveGuidance ) => {
683
730
  isYarnProject
684
731
  }
685
732
  }
733
+
734
+ export function isDir( path ) {
735
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
736
+ const exists = fs.existsSync( path )
737
+
738
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
739
+ return exists && fs.statSync( path ).isDirectory()
740
+ }
741
+
742
+ export function getSecretsDir() {
743
+ let secretsDir
744
+
745
+ if ( process.env.SECRETS_DIR ) {
746
+ secretsDir = process.env.SECRETS_DIR
747
+ } else if ( isDir( path.join( process.cwd(), 'env' ) ) ) {
748
+ secretsDir = 'env'
749
+ } else {
750
+ secretsDir = 'secrets'
751
+ }
752
+
753
+ return secretsDir
754
+ }
755
+
756
+ export function getIsYarnProject( gitRoot ) {
757
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
758
+ return fs.existsSync( path.join( gitRoot, 'yarn.lock' ) ) || isDir( path.join( gitRoot, '.yarn' ) )
759
+ }
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import crypto from 'crypto'
3
+ import crypto from 'node:crypto'
4
4
 
5
5
  import { validate } from 'superstruct'
6
6
 
package/src/bash-funcs CHANGED
@@ -322,4 +322,4 @@ function commandChecker() {
322
322
  done
323
323
  }
324
324
 
325
- commandChecker git helm kubectl service-man
325
+ commandChecker git helm jq kubectl service-man
@@ -4,6 +4,8 @@
4
4
  import pkg from 'enquirer'
5
5
  import { $, fs, argv, path, chalk } from 'zx'
6
6
 
7
+ import { getGitRootDirectory, getIsYarnProject, getSecretsDir } from './Utils.mjs'
8
+
7
9
  const { prompt } = pkg
8
10
 
9
11
  // This will preserve coloration from spawned child processes
@@ -34,8 +36,9 @@ const INTERACTIVE_MODE = !TARGET
34
36
 
35
37
  let exited = false
36
38
 
37
- const SECRETS_DIR = 'secrets'
39
+ const SECRETS_DIR = await getSecretsDir()
38
40
  const EXIT_EVENTS = [ 'SIGINT', 'exit', 'uncaughException', 'unhandledRejection' ]
41
+ const isYarnProject = getIsYarnProject( await getGitRootDirectory() )
39
42
 
40
43
  const [
41
44
  firebaserc,
@@ -211,7 +214,10 @@ try {
211
214
  const deployType = CHANNEL ? `hosting:channel:deploy ${CHANNEL.name} ${CHANNEL.expiration ? `--expires ${CHANNEL.expiration}` : ''}`.split( ' ' ) : 'deploy'
212
215
  const hostingPrefix = CHANNEL ? '' : 'hosting:'
213
216
 
214
- await $( { ac } )`npm run clean && DOTENV_CONFIG_PATH=${path.join( process.cwd(), '.env.temp' )} DEPLOYMENT_TARGET=${TARGET.siteId} npm run build && firebase use ${TARGET.projectId} && firebase ${deployType} --only ${hostingPrefix}${TARGET.siteId}`
217
+ const cleanCommand = isYarnProject ? 'yarn clean' : 'npm run clean'
218
+ const buildCommand = isYarnProject ? 'yarn build' : 'npm run build'
219
+
220
+ await $( { ac } )`${cleanCommand} && DOTENV_CONFIG_PATH=${path.join( process.cwd(), '.env.temp' )} DEPLOYMENT_TARGET=${TARGET.siteId} ${buildCommand} && firebase use ${TARGET.projectId} && firebase ${deployType} --only ${hostingPrefix}${TARGET.siteId}`
215
221
  } catch ( err ) {
216
222
  console.error( err )
217
223
  }
@@ -4,6 +4,8 @@
4
4
  import pkg from 'enquirer'
5
5
  import { $, fs, argv, path, chalk } from 'zx'
6
6
 
7
+ import { getGitRootDirectory, getIsYarnProject, getSecretsDir } from './Utils.mjs'
8
+
7
9
  const { prompt } = pkg
8
10
 
9
11
  // This will preserve coloration from spawned child processes
@@ -13,9 +15,12 @@ process.env.FORCE_COLOR = '1'
13
15
  let TARGET = argv._[0]
14
16
  let exited = false
15
17
 
16
- const SECRETS_DIR = 'secrets'
17
18
  const EXIT_EVENTS = [ 'SIGINT', 'exit', 'uncaughtException', 'unhandledRejection' ]
18
19
 
20
+ const SECRETS_DIR = await getSecretsDir()
21
+
22
+ const isYarnProject = getIsYarnProject( await getGitRootDirectory() )
23
+
19
24
  const [
20
25
  firebaserc,
21
26
  firebaseJson,
@@ -142,7 +147,8 @@ EXIT_EVENTS
142
147
  await fs.writeFile( path.join( process.cwd(), '.env.temp' ), envFileContent )
143
148
 
144
149
  try {
145
- await $( { ac } )`DOTENV_CONFIG_PATH=${path.join( process.cwd(), '.env.temp' )} DEPLOYMENT_TARGET=${TARGET.siteId} npm run serve --colors`
150
+ const serveCommand = isYarnProject ? 'yarn serve' : 'npm run serve'
151
+ await $( { ac } )`DOTENV_CONFIG_PATH=${path.join( process.cwd(), '.env.temp' )} DEPLOYMENT_TARGET=${TARGET.siteId} ${serveCommand} --colors`
146
152
  } catch ( err ) {
147
153
  console.error( err )
148
154
  }
package/src/git-tar.sh CHANGED
@@ -3,7 +3,7 @@
3
3
  # Load the standard helper functions
4
4
  . `build-tools --bashfun`
5
5
 
6
- # If it isn't already set use the default ~/work/.gitar
6
+ # If it isn't already set use the default ~/.gitar
7
7
  #
8
8
  [ -z $GITAR ] && GITAR="${HOME}/.gitar"
9
9
 
@@ -1,12 +1,5 @@
1
1
  image:
2
- tag: # v3.7.0
2
+ tag: # v5.1.0
3
3
 
4
4
  config:
5
5
  LOG_CONFIG: '{"type":"pino","level":"warn"}'
6
-
7
- API_UI_URL: "${PROJECT_ID}.web.app"
8
- MAIL_GUN_DOMAIN: "${MAIL_GUN_DOMAIN}"
9
- MAIL_GUN_DEFAULT_SENDER: "no-reply@${MAIL_GUN_DOMAIN}"
10
-
11
- serviceMonitor:
12
- enabled: true
@@ -8,7 +8,7 @@ showInstalling "The Prometheus Operator (kube-prometheus-stack)"
8
8
 
9
9
  # https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack
10
10
  OCI_CHART="oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack"
11
- [ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="80.4.1"
11
+ [ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="80.11.0"
12
12
  helm upgrade $NS --install prometheus-stack $OCI_CHART \
13
13
  --values prom-operator/prometheus-stack.yaml \
14
14
  --version $PROMETHEUS_STACK_CHART_VERSION $HELM_WHAT
@@ -17,7 +17,7 @@ TRAEFIK_NAMESPACE="traefik"
17
17
 
18
18
  addHelmRepo traefik https://helm.traefik.io/traefik
19
19
 
20
- [ -z "$TRAEFIK_CHART_VERSION" ] && TRAEFIK_CHART_VERSION="37"
20
+ [ -z "$TRAEFIK_CHART_VERSION" ] && TRAEFIK_CHART_VERSION="38"
21
21
  helm upgrade --install traefik traefik/traefik \
22
22
  --namespace $TRAEFIK_NAMESPACE --create-namespace \
23
23
  --values traefik/values.yaml \
@@ -0,0 +1,135 @@
1
+ # Velero Deployment Notes
2
+
3
+ ## Why this document exists
4
+
5
+ This document explains **why we had to change the Velero deployment process** and how we now safely manage **CRDs and Helm upgrades** without relying on Bitnami images or brittle Helm hook jobs.
6
+
7
+ These changes were driven by:
8
+ - Removal / breakage of Bitnami `kubectl` images referenced by the `vmware-tanzu/velero` Helm chart
9
+ - Broadcom’s changes to Bitnami image access and licensing
10
+ - The need for **deterministic, version-safe CRD management** on new and existing GKE clusters
11
+
12
+ ---
13
+
14
+ ## Background: What broke
15
+
16
+ The Velero Helm chart (`vmware-tanzu/velero` >= 11.x) includes a Helm **hook job** that attempts to install or upgrade CRDs using an initContainer based on:
17
+
18
+ ```
19
+ docker.io/bitnamilegacy/kubectl:<k8s-version>
20
+ ```
21
+
22
+ On Kubernetes 1.34+ clusters, this image **no longer exists**, causing installs to fail with errors such as:
23
+
24
+ ```
25
+ ErrImagePull: bitnamilegacy/kubectl:1.34 not found
26
+ ```
27
+
28
+ Attempts to substitute other kubectl images failed due to hard assumptions in the hook job (e.g. `/tmp/sh`, shell paths, filesystem layout).
29
+
30
+ Because this hook runs **before Helm considers the release successful**, it blocks both fresh installs and upgrades.
31
+
32
+ ---
33
+
34
+ ## Why we disabled Helm-based CRD upgrades
35
+
36
+ Helm does not natively manage CRD upgrades well:
37
+ - CRDs in `crds/` directories are only installed once
38
+ - Schema upgrades require hooks or out-of-band logic
39
+ - Hook jobs are fragile and opaque when they fail
40
+
41
+ The Velero chart’s CRD hook job is effectively doing:
42
+
43
+ ```
44
+ velero install --crds-only | kubectl apply
45
+ ```
46
+
47
+ …but wrapped in a container that is now broken due to image dependencies.
48
+
49
+ **Conclusion:** We disable the Helm CRD hook entirely and manage CRDs explicitly.
50
+
51
+ ---
52
+
53
+ ## Our current approach (high-level)
54
+
55
+ 1. **CRDs are managed explicitly**, outside Helm
56
+ 2. **CRDs are generated from the exact Velero version being deployed**
57
+ 3. Helm is used only for deploying/upgrading the Velero controller and configuration
58
+ 4. Helm CRD hooks are disabled (`upgradeCRDs=false`)
59
+
60
+ This makes installs:
61
+ - Reproducible
62
+ - Debuggable
63
+ - Independent of third-party image publishing decisions
64
+
65
+ ---
66
+
67
+ ## Version alignment is critical
68
+
69
+ The command:
70
+
71
+ ```bash
72
+ velero install --crds-only --dry-run -o yaml
73
+ ```
74
+
75
+ **uses the local `velero` CLI version** to generate CRDs.
76
+
77
+ If the CLI version does not match the Velero server version deployed by Helm, you can:
78
+ - Apply incompatible CRD schemas
79
+ - Introduce validation failures on existing custom resources
80
+
81
+ ### Our safeguard
82
+
83
+ `helmup.plugin` now:
84
+ - Reads the chart `appVersion` directly from Helm
85
+ - Verifies the local `velero` CLI version matches exactly
86
+ - Fails fast if versions are mismatched
87
+
88
+ This guarantees CRDs and controllers stay in lockstep.
89
+
90
+ ---
91
+
92
+ ## When CRDs are applied
93
+
94
+ CRDs are **not applied on every deploy**.
95
+
96
+ They are applied only when:
97
+ - Bootstrapping a new cluster
98
+ - Upgrading the Velero chart to a version with a new `appVersion`
99
+
100
+ This is controlled explicitly via:
101
+
102
+ ```bash
103
+ VELERO_APPLY_CRDS=1 helmup velero
104
+ ```
105
+
106
+ Normal configuration-only updates do **not** touch CRDs.
107
+
108
+ ---
109
+
110
+ ## Summary of changes
111
+
112
+ | Area | Old behavior | New behavior |
113
+ |-----|-------------|-------------|
114
+ | CRD management | Helm hook job | Explicit, pinned CRD apply |
115
+ | kubectl dependency | Bitnami legacy image | None |
116
+ | Version safety | Implicit | Enforced |
117
+ | Failure mode | Late, opaque | Early, explicit |
118
+ | New cluster installs | Unreliable | Deterministic |
119
+
120
+ ---
121
+
122
+ ## Key takeaways
123
+
124
+ - **CRDs are part of the API, not application config** — they must be versioned and applied intentionally
125
+ - Helm hooks are not a reliable mechanism for critical cluster-scoped resources
126
+ - Pinning tool versions eliminates an entire class of upgrade failures
127
+ - This approach aligns with GitOps and production-safe Kubernetes practices
128
+
129
+ ---
130
+
131
+ ## References
132
+
133
+ - Velero documentation: https://velero.io/docs/
134
+ - Velero Helm charts: https://github.com/vmware-tanzu/helm-charts
135
+ - Kubernetes CRD management: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/
@@ -4,6 +4,7 @@
4
4
  function installVeleroEnvironment() {
5
5
  # Follows instructions => https://github.com/vmware-tanzu/velero-plugin-for-gcp
6
6
  showInstalling "Velero Backup Management"
7
+
7
8
  createNamespaceIfNeeded velero
8
9
 
9
10
  # Do we need to => gcloud auth application-default
@@ -2,10 +2,36 @@
2
2
  #
3
3
  showInstalling "Velero Backup System"
4
4
 
5
+ # We use a specific version for the velero chart because we need to control
6
+ # the version of the locally installed velero client application.
7
+ #
8
+ VELERO_CHART_VERSION="11.3.1"
9
+ EXPECTED_CLIENT_VERSION="v1.17.1"
10
+
5
11
  addHelmRepo vmware-tanzu https://vmware-tanzu.github.io/helm-charts
6
12
 
7
- [ -z "$VELERO_CHART_VERSION" ] && VELERO_CHART_VERSION="11"
13
+ commandChecker velero # verify the velero client is installed
14
+
15
+ local client_version="$(velero version --client-only 2>/dev/null | awk -F': ' '/Version:/ {print $2; exit}')"
16
+
17
+ if [[ "$client_version" != "$EXPECTED_CLIENT_VERSION" ]]; then
18
+ cat<<WRONG_VELERO_APP_VERSION
19
+
20
+ ERROR: velero CLI version mismatch.
21
+ Expected: `color g "${EXPECTED_CLIENT_VERSION}"`
22
+ Found: `color r "${client_version}"`
23
+
24
+ Fix => `color y "install velero ${EXPECTED_CLIENT_VERSION}"`
25
+ WRONG_VELERO_APP_VERSION
26
+ exit 1
27
+ fi
28
+
29
+ # If the CRDs need to be updated set VELERO_APPLY_CRDS!
8
30
  #
31
+ if [[ "${VELERO_APPLY_CRDS:-0}" == "1" ]]; then
32
+ velero install --crds-only --dry-run -o yaml | kubectl apply -f - --validate=false
33
+ fi
34
+
9
35
  # Build the bucket, region and SA email variables and use --set to mod the
10
36
  # chart values as opposed to using an ${label} approach. This eliminates
11
37
  # the need for an overwhelm generated values.yaml file which also contains
@@ -13,7 +39,7 @@ addHelmRepo vmware-tanzu https://vmware-tanzu.github.io/helm-charts
13
39
  #
14
40
  # Note: GCP_PROJECT_ID is passed down from helmup's environment.
15
41
  #
16
- GCE_BUCKET="$GCP_PROJECT_ID-velero-$VELERO_CHART_VERSION"
42
+ GCE_BUCKET="$GCP_PROJECT_ID-velero-${VELERO_CHART_VERSION%%.*}"
17
43
  GCE_REGION=`overwhelm -k GCE_REGION`
18
44
  GCE_SA_EMAIL="velero@${GCP_PROJECT_ID}.iam.gserviceaccount.com"
19
45
 
@@ -27,6 +53,7 @@ helm upgrade \
27
53
  --namespace velero --create-namespace \
28
54
  --values velero/values-local.yaml \
29
55
  --version $VELERO_CHART_VERSION $HELM_WHAT \
56
+ --set upgradeCRDs=false \
30
57
  --set configuration.backupStorageLocation[0].bucket="$GCE_BUCKET" \
31
58
  --set configuration.backupStorageLocation[0].config.serviceAccount="$GCE_SA_EMAIL" \
32
59
  --set configuration.volumeSnapshotLocation[0].config.snapshotLocation="$GCE_REGION"
@@ -137,7 +137,7 @@ nodeSelector: {}
137
137
  # See => https://github.com/vmware-tanzu/velero-plugin-for-gcp
138
138
  initContainers:
139
139
  - name: velero-plugin-for-gcp
140
- image: velero/velero-plugin-for-gcp:v1.10.0
140
+ image: velero/velero-plugin-for-gcp:v1.13.1
141
141
  volumeMounts:
142
142
  - mountPath: /target
143
143
  name: plugins