@leverege/build-tools 2.66.8 → 2.67.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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "files.associations": {
3
+ "*.yaml.ovh": "yaml"
4
+ }
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.66.8",
3
+ "version": "2.67.1",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -66,14 +66,14 @@
66
66
  "chalk": "^5.4.1",
67
67
  "command-line-args": "^6.0.1",
68
68
  "command-line-usage": "^7.0.3",
69
- "commander": "^13.1.0",
69
+ "commander": "^14.0.0",
70
70
  "deepmerge": "^4.3.1",
71
71
  "enquirer": "^2.4.1",
72
- "execa": "^9.5.3",
72
+ "execa": "^9.6.0",
73
73
  "glob": "^11.0.2",
74
74
  "handlebars": "^4.7.8",
75
- "ignore": "^7.0.4",
76
- "inquirer": "^12.6.1",
75
+ "ignore": "^7.0.5",
76
+ "inquirer": "^12.6.3",
77
77
  "js-yaml": "^4.1.0",
78
78
  "jsdoc": "^4.0.4",
79
79
  "ms": "^2.1.3",
@@ -83,13 +83,13 @@
83
83
  "package-up": "^5.0.0",
84
84
  "readline-sync": "^1.4.10",
85
85
  "semver": "^7.7.2",
86
- "shell-quote": "^1.8.2",
87
- "simple-git": "^3.27.0",
86
+ "shell-quote": "^1.8.3",
87
+ "simple-git": "^3.28.0",
88
88
  "toml": "^3.0.0",
89
- "zx": "^8.5.4"
89
+ "zx": "^8.5.5"
90
90
  },
91
91
  "devDependencies": {
92
92
  "@leverege/eslint-config-leverege": "^5.0.1",
93
- "npm": "^11.3.0"
93
+ "npm": "^11.4.1"
94
94
  }
95
95
  }
package/src/Docker.mjs CHANGED
@@ -177,7 +177,6 @@ const buildContainerImage = async ( {
177
177
  artifactProject, artifactRegistry, containerName,
178
178
  imageVersion, isNpmWorkspace, options
179
179
  } ) => {
180
- const useCloudBuildFile = true // { useCloudBuildFile } = options || {}
181
180
  const buildWorkspace = './build/workspace' // passed to Cloud Build
182
181
  const imageName = `${artifactRegistry}/images/${containerName}`
183
182
 
@@ -213,35 +212,37 @@ const buildContainerImage = async ( {
213
212
 
214
213
  setPreviousBuild( imageVersion ) // update docker/.previous file
215
214
 
216
- const { fullName : builderName } = await getGcpAccount()
217
- const gcloudBuild = `gcloud builds submit --project ${artifactProject}`
215
+ const { fullName : imageBuilder } = await getGcpAccount()
216
+ const imageInfoTag = `zzinfo_${defaultSettings.date}_${defaultSettings.nodeimage}_${imageBuilder}`
218
217
  const gcloudProjectNumber = await shellCmd( `gcloud projects describe ${artifactProject} --format="value(projectNumber)"` )
219
- const gcloudBuildSA = `--service-account=projects/${artifactProject}/serviceAccounts/${gcloudProjectNumber}-compute@developer.gserviceaccount.com`
220
- const gcloudLogs = `--gcs-log-dir ${formCloudBuildBucketName( artifactProject )}/log`
221
- const imageTag = `${defaultSettings.date}-${defaultSettings.nodeimage}-${builderName}`
222
218
 
223
- // create the cloud build config from the handlebars template
224
- const cloudBuildFile = './docker/cloudbuild.yaml'
219
+ // build the cloudbuild.yaml from the handlebars template
220
+ const cloudBuildFile = './build/cloudbuild.yaml'
225
221
  const cbst = handlebars.compile( cloudBuildSteps, { noEscape : true } )
226
- const cbs = cbst( { artifactProject, artifactRegistry, imageName, imageVersion, imageTag } )
222
+ const cbs = cbst( { artifactProject, artifactRegistry, imageName, imageVersion, imageInfoTag, } )
227
223
  fs.writeFileSync( cloudBuildFile, cbs )
228
224
 
229
- const gcloudBuildConfig = `--config=${cloudBuildFile}`
230
-
225
+ // build up the cloud build command and options
226
+ const cloudBuildCmd = `gcloud builds submit --project ${artifactProject}`
227
+ const cloudBuildLogs = `--gcs-log-dir ${formCloudBuildBucketName( artifactProject )}/log`
228
+ const cloudBuildCfg = `--config=${cloudBuildFile}`
229
+ const cloudBuildSA = `--service-account=${gcloudProjectNumber}-compute@cloudbuild.gserviceaccount.com`
230
+
231
231
  log( chalk.green.bold( `
232
- *** Submitting Build as ${builderName} ***
233
- ${gcloudBuild} \\
234
- ${gcloudLogs} \\
235
- ${gcloudBuildConfig} \\
236
- ${gcloudBuildSA}
232
+ *** Submitting Build as ${imageBuilder} ***
233
+ ${cloudBuildCmd} \\
234
+ ${cloudBuildLogs} \\
235
+ ${cloudBuildCfg} \\
236
+ ${cloudBuildSA}
237
237
  ` ) )
238
238
 
239
239
  if ( process.env.DRY_RUN === '1' ) {
240
+ log( `Would have run => ${cloudBuildCmd} ${cloudBuildLogs} ${cloudBuildCfg} ./build` )
240
241
  log( '\n***Exiting from DRY_RUN\n' )
241
242
  process.exit( 0 )
242
243
  }
243
244
 
244
- await shellCmd( `${gcloudBuild} ${gcloudLogs} ${gcloudBuildConfig} ${gcloudBuildSA} ./build`, { stdio : 'inherit' } )
245
+ await shellCmd( `${cloudBuildCmd} ${cloudBuildLogs} ${cloudBuildCfg} ./build`, { stdio : 'inherit' } )
245
246
  }
246
247
 
247
248
  const repoCleanup = async () => {
package/src/DockerPy.mjs CHANGED
@@ -43,10 +43,8 @@ const bashrcPyTemplate = fs.readFileSync( path.join( DIRNAME, './templates/bashr
43
43
  bashrcTemplate = `${bashrcTemplate}\n${bashrcPyTemplate}`
44
44
 
45
45
  const defaultSettings = {
46
- apkadds : '',
47
46
  date : getDateTimestamp(),
48
- nodeimage : 'jod-alpine',
49
- imageBase : 'node:jod-alpine',
47
+ imageBase : 'python:3.12.8-slim-bookworm',
50
48
  npmlogging : process.env.VERBOSE_NPM_LOGGING ? '--ddd' : '--silent',
51
49
  npmVersion : 'npm@11',
52
50
  pluginfile : '# NO PLUGIN',
@@ -55,6 +53,7 @@ const defaultSettings = {
55
53
  runuser : 'python',
56
54
  }
57
55
 
56
+ // the previous build version is simply informational for the user
58
57
  const previousFile = './docker/.previous'
59
58
 
60
59
  function getPreviousBuild() {
@@ -65,16 +64,26 @@ const setPreviousBuild = ( version ) => {
65
64
  fs.writeFileSync( previousFile, version )
66
65
  }
67
66
 
67
+ // the base image string to be in the form of an image name and a version
68
+ // for example:
69
+ //
70
+ // python:3.12.8-slim-bookworm becomes python-3.12.8
71
+ // nvcr.io/nvidia/cuda:12.3.1-base-ubuntu22.04 becomes cuda-12.3.1
72
+ //
73
+ // which will then be used to tag the image for information purposes only
74
+ const extractBaseImageTag = ( imageStr ) => {
75
+ const match = imageStr.match( /(\w+:\d+(?:\.\d+)*)(?=-)/ ) // eslint-disable-line security/detect-unsafe-regex
76
+ return match ? match[1].replace( ':', '-' ) : null
77
+ }
78
+
68
79
  function generateDockerfile( options ) {
69
80
 
70
81
  const settings = { ...defaultSettings, ...options }
71
82
 
72
- if ( !settings.imageBase ) {
73
- warning( 'using the default image python:3.12.8-slim-bookworm' )
74
- settings.image = 'python:3.12.8-slim-bookworm'
75
- } else {
76
- settings.image = settings.imageBase
77
- }
83
+ settings.image = settings.imageBase
84
+ // TODO: this is a janky way to pass the image tag back out
85
+ defaultSettings.buildTag = extractBaseImageTag( settings.image )
86
+
78
87
  // Ensure the presence of the docker dir with plugin and bashrc - this code
79
88
  // could probably be tightened up a little.
80
89
  try {
@@ -155,18 +164,13 @@ async function buildContainerImage( {
155
164
  artifactProject, artifactRegistry, containerName,
156
165
  imageVersion, isNpmWorkspace, options
157
166
  } ) {
158
-
159
- const useCloudBuildFile = true
160
167
  const buildWorkspace = './build/workspace' // passed to Cloud Build
161
168
  const imageName = `${artifactRegistry}/images/${containerName}`
162
169
 
170
+ // clean up any residual build cruft (should be clean though)
163
171
  if ( fs.existsSync( buildWorkspace ) ) {
164
172
  fs.renameSync( buildWorkspace, `${buildWorkspace}-junk` )
165
- try {
166
- fs.rmSync( `${buildWorkspace}-junk`, { recursive : true } )
167
- } catch ( err ) {
168
- log( `***Error: from Docker.buildContainerImage\n\n${chalk.bold.yellow( err )}` )
169
- }
173
+ fs.rm( `${buildWorkspace}-junk`, { recursive : true }, ( err ) => {} ) // fire and forget
170
174
  }
171
175
 
172
176
  // TODO: This is janky - I'll clean it up later
@@ -176,48 +180,45 @@ async function buildContainerImage( {
176
180
  fs.mkdirSync( './build' )
177
181
  fs.renameSync( './workspace', buildWorkspace )
178
182
  fs.renameSync( './docker/Dockerfile', './build/Dockerfile' )
183
+ fs.renameSync( './docker/bashrc', './build/bashrc' )
179
184
  // fs.cpSync( `${process.env.HOME}/.npmrc`, './build/.npmrc' )
180
- fs.cpSync( './docker/bashrc', './build/bashrc' )
181
185
  // fs.cpSync( './package.json', `${buildWorkspace}/package.json` )
182
186
  // fs.cpSync( './package-lock.json', `${buildWorkspace}/package-lock.json` )
183
187
 
184
188
  setPreviousBuild( imageVersion ) // update docker/.previous file
185
189
 
186
- const { fullName : builderName } = await getGcpAccount()
187
- const gcloudBuild = `gcloud builds submit --project ${artifactProject}`
190
+ const { fullName : imageBuilder } = await getGcpAccount()
191
+ const imageArch = defaultSettings.buildTag
192
+ const imageInfoTag = `zzinfo_${imageArch}_${imageBuilder}_${defaultSettings.date}`
188
193
  const gcloudProjectNumber = await shellCmd( `gcloud projects describe ${artifactProject} --format="value(projectNumber)"` )
194
+
195
+ // build the cloudbuild.yaml from the handlebars template
196
+ const cloudBuildFile = './build/cloudbuild.yaml'
197
+ const cbst = handlebars.compile( cloudBuildSteps, { noEscape : true } )
198
+ const cbs = cbst( { artifactProject, artifactRegistry, imageName, imageVersion, imageInfoTag, } )
199
+ fs.writeFileSync( cloudBuildFile, cbs )
200
+
201
+ // build up the cloud build command and options
202
+ const cloudBuildCmd = `gcloud builds submit --project ${artifactProject}`
203
+ const cloudBuildLogs = `--gcs-log-dir ${formCloudBuildBucketName( artifactProject )}/log`
204
+ const cloudBuildCfg = `--config=${cloudBuildFile}`
189
205
  const cloudBuildSA = `--service-account=${gcloudProjectNumber}-compute@cloudbuild.gserviceaccount.com`
190
- const gcloudLogs = `--gcs-log-dir ${formCloudBuildBucketName( artifactProject )}/log`
191
- let gcloudTags = `--tag ${imageName}:${imageVersion}`
192
-
193
- if ( useCloudBuildFile ) {
194
- const cloudBuildFile = './docker/cloudbuild.yaml'
195
- gcloudTags = `--config=${cloudBuildFile}`
196
- const cbst = handlebars.compile( cloudBuildSteps, { noEscape : true } )
197
- const cbs = cbst( { artifactProject, artifactRegistry, imageName, imageVersion } )
198
-
199
- fs.writeFileSync( cloudBuildFile, cbs )
200
- }
201
206
 
202
207
  log( chalk.green.bold( `
203
- *** Submitting Build as ${builderName} ***
204
- ${gcloudBuild} \\
205
- ${gcloudLogs} \\
206
- ${gcloudTags} \\
208
+ *** Submitting Build as ${imageBuilder} ***
209
+ ${cloudBuildCmd} \\
210
+ ${cloudBuildLogs} \\
211
+ ${cloudBuildCfg} \\
207
212
  ${cloudBuildSA}
208
213
  ` ) )
209
214
 
210
215
  if ( process.env.DRY_RUN === '1' ) {
216
+ log( `Would have run => ${cloudBuildCmd} ${cloudBuildLogs} ${cloudBuildCfg} ./build` )
211
217
  log( '\n***Exiting from DRY_RUN\n' )
212
218
  process.exit( 0 )
213
219
  }
214
220
 
215
- await shellCmd( `${gcloudBuild} ${gcloudLogs} ${gcloudTags} ./build`, { stdio : 'inherit' } )
216
-
217
- const imageTag = `${defaultSettings.date}-${defaultSettings.nodeimage}-${builderName}`
218
- await shellCmd(
219
- `gcloud container images add-tag --quiet ${imageName}:${imageVersion} ${imageName}:${imageTag} ${imageName}:latest`
220
- )
221
+ await shellCmd( `${cloudBuildCmd} ${cloudBuildLogs} ${cloudBuildCfg} ./build`, { stdio : 'inherit' } )
221
222
  }
222
223
 
223
224
  async function repoCleanup() {
package/src/Utils.mjs CHANGED
@@ -9,6 +9,7 @@ import inquirer from 'inquirer'
9
9
  import { glob } from 'glob'
10
10
  import { packageUp } from 'package-up'
11
11
  import { parse } from 'shell-quote'
12
+ import toml from 'toml'
12
13
  import YAML from 'js-yaml'
13
14
 
14
15
  // __dirname goes away with ESM so we take this approach instead and
@@ -127,12 +128,6 @@ export const getDirectories = ( { path : srcPath = process.cwd(), respectGitigno
127
128
  .map( dirent => dirent.name )
128
129
  }
129
130
 
130
- export const xgetDirectories = ( srcPath = process.cwd() ) => {
131
- return fs.readdirSync( srcPath, { withFileTypes : true } ) // eslint-disable-line security/detect-non-literal-fs-filename
132
- .filter( dirent => dirent.isDirectory() )
133
- .map( dirent => dirent.name )
134
- }
135
-
136
131
  // returns YYYMMDD-hhmm
137
132
  export const getDateTimestamp = () => {
138
133
  const fullDTS = new Date().toISOString().replace( /[-T:]/g, '' ).slice( 0, 14 )
@@ -170,7 +165,8 @@ export const getGcpSecret = async ( gcpProject, secretName, opts ) => {
170
165
  }
171
166
  }
172
167
 
173
- const runGitCommand = async ( cmd, debugLabel ) => {
168
+ // just a wrapper to run command with try/catch
169
+ const tryCommand = async ( cmd, debugLabel ) => {
174
170
  try {
175
171
  const result = await shellCmd( cmd )
176
172
  debug( { result }, `<==${debugLabel}` )
@@ -181,16 +177,16 @@ const runGitCommand = async ( cmd, debugLabel ) => {
181
177
  }
182
178
  }
183
179
 
184
- export const getGitBranch = async () => runGitCommand( 'git rev-parse --abbrev-ref HEAD', 'getGitCurrentBranch' )
185
- export const getGitDiff = async opts => runGitCommand( `git diff ${opts}`, 'getGitDiff' )
186
- export const getGitHooksPath = async () => runGitCommand( 'git config --get core.hooksPath', 'getGitHooksPath' )
187
- export const setGitHooksPath = async hookPath => runGitCommand( `git config core.hooksPath ${hookPath}`, 'setGitHooksPath' )
188
- export const getGitRootDirectory = async () => runGitCommand( 'git rev-parse --show-toplevel', 'getGitRootDirectory' )
180
+ export const getGitBranch = async () => tryCommand( 'git rev-parse --abbrev-ref HEAD', 'getGitCurrentBranch' )
181
+ export const getGitDiff = async opts => tryCommand( `git diff ${opts}`, 'getGitDiff' )
182
+ export const getGitHooksPath = async () => tryCommand( 'git config --get core.hooksPath', 'getGitHooksPath' )
183
+ export const setGitHooksPath = async hookPath => tryCommand( `git config core.hooksPath ${hookPath}`, 'setGitHooksPath' )
184
+ export const getGitRootDirectory = async () => tryCommand( 'git rev-parse --show-toplevel', 'getGitRootDirectory' )
189
185
  export const getGitBranchAndUpstream = async () => {
190
186
  const branch = await getGitBranch() ?? 'detached'
191
187
  let upstream = 'no-upstream'
192
188
  try {
193
- upstream = await runGitCommand( `git rev-parse --abbrev-ref ${branch}@{u}`, 'getGitUpstream' ) ?? 'no-upstream'
189
+ upstream = await tryCommand( `git rev-parse --abbrev-ref ${branch}@{u}`, 'getGitUpstream' ) ?? 'no-upstream'
194
190
  } catch ( error ) {}
195
191
  return { branch, upstream }
196
192
  }
@@ -201,6 +197,8 @@ export const gitRepoIsDirty = async () => {
201
197
  }
202
198
 
203
199
  export const parseJsonFile = async ( jsonFile ) => {
200
+ // TODO: rework this file exists logic / throw
201
+ if ( !fs.existsSync( jsonFile ) ) { return undefined } // eslint-disable-line security/detect-non-literal-fs-filename
204
202
  /* eslint-disable security/detect-non-literal-fs-filename */
205
203
  if ( fs.existsSync( jsonFile ) ) {
206
204
  try {
@@ -222,7 +220,9 @@ const deprecated = ( opts ) => {
222
220
  process.exit( 1 )
223
221
  }
224
222
 
225
- export const parsePackageJson = async ( packageFileName ) => {
223
+ export const parsePackageJson = async ( packageFileName = './package.json' ) => {
224
+ if ( !fs.existsSync( packageFileName ) ) { return undefined } // eslint-disable-line security/detect-non-literal-fs-filename
225
+
226
226
  const packageJson = await parseJsonFile( packageFileName )
227
227
 
228
228
  // deprecation checks are here to ease the pain of upgrading older repos
@@ -411,6 +411,13 @@ https://console.cloud.google.com/artifacts/browse/leverege-registry?project=leve
411
411
  return { artifactRegistry, ...packageJson }
412
412
  }
413
413
 
414
+ export const parsePyProjectToml = async ( packageFileName = './pyproject.toml' ) => {
415
+ if ( !fs.existsSync( packageFileName ) ) { return undefined } // eslint-disable-line security/detect-non-literal-fs-filename
416
+ const tomlStr = fs.readFileSync( packageFileName ) // eslint-disable-line security/detect-non-literal-fs-filename
417
+ const pyproject = toml.parse( tomlStr )
418
+ return pyproject
419
+ }
420
+
414
421
  export const parseHelmChart = async ( helmroot = './helm' ) => {
415
422
  /* eslint-disable security/detect-non-literal-fs-filename */
416
423
  if ( !fs.existsSync( helmroot ) ) { return undefined }
@@ -453,12 +460,8 @@ export const getRepositoryDescr = async () => {
453
460
  const subPkgs = await glob( `${gitRoot}/packages/**/package.json`, { ignore : '/**/node_modules/**' } )
454
461
  const helmChart = await parseHelmChart()
455
462
  const isMonoRepo = subPkgs?.length > 0
456
- let rootPackageJson
457
- try {
458
- rootPackageJson = await parseJsonFile( `${gitRoot}/package.json` )
459
- } catch ( error ) {
460
- rootPackageJson = { isInvalid : true, error : 'invalid or non-existent root package.json file' }
461
- }
463
+ const rootPackageJson = await parseJsonFile( `${gitRoot}/package.json` )
464
+ const rootPyProjectToml = await parsePyProjectToml( `${gitRoot}/pyproject.toml` )
462
465
  const isNpmWorkspace = rootPackageJson?.workspaces?.length > 0
463
466
 
464
467
  return {
@@ -473,6 +476,7 @@ export const getRepositoryDescr = async () => {
473
476
  isMonoRepo,
474
477
  isNpmWorkspace,
475
478
  rootPackageJson,
479
+ rootPyProjectToml,
476
480
  subPkgs,
477
481
  }
478
482
  }
@@ -484,6 +488,7 @@ export const analyzeRepository = async ( giveGuidance ) => {
484
488
  helmChart,
485
489
  isMonoRepo,
486
490
  rootPackageJson,
491
+ rootPyProjectToml,
487
492
  isNpmWorkspace,
488
493
  } = await getRepositoryDescr()
489
494
 
@@ -527,15 +532,12 @@ export const analyzeRepository = async ( giveGuidance ) => {
527
532
  await shellCmd( 'npm run build' )
528
533
  }
529
534
 
530
- const buildToolsVersion = closestPackageJson.devDependencies['@leverege/build-tools'] || closestPackageJson.dependencies['@leverege/build-tools']
531
- if ( !buildToolsVersion ) {
532
- errorExit( 'Error: missing devDependency for @leverege/build-tools in package.json' )
533
- }
535
+ const buildToolsVersion = closestPackageJson?.devDependencies['@leverege/build-tools'] || closestPackageJson?.dependencies['@leverege/build-tools']
534
536
 
535
- const containerName = closestPackageJson.name.replace( /^@[^/]+\//, '' )
536
- const artifactProject = closestPackageJson.artifactRegistry.project
537
- const artifactRegistry = closestPackageJson.leverege.registry
538
- const packageVersion = `v${closestPackageJson.version}`
537
+ const containerName = closestPackageJson?.name.replace( /^@[^/]+\//, '' ) || rootPyProjectToml?.project.name
538
+ const artifactProject = closestPackageJson?.artifactRegistry.project || 'unused'
539
+ const artifactRegistry = closestPackageJson?.leverege.registry || rootPyProjectToml?.registry
540
+ const packageVersion = `v${closestPackageJson?.version || rootPyProjectToml?.project.version}`
539
541
 
540
542
  const currentDir = process.cwd()
541
543
  const closestPkg = await packageUp()
@@ -567,6 +569,7 @@ export const analyzeRepository = async ( giveGuidance ) => {
567
569
  isMonoRepo,
568
570
  isNpmWorkspace,
569
571
  rootPackageJson,
572
+ rootPyProjectToml,
570
573
  closestPackageJson,
571
574
  packageVersion,
572
575
  subPkgs,
@@ -89,7 +89,7 @@ const pyproject = toml.parse( tomlStr )
89
89
  const repoDescr = {
90
90
  artifactProject : pyproject.leverege.project || 'leverege-registry',
91
91
  containerName : pyproject.project.name,
92
- packageVersion : pyproject.project.version,
92
+ packageVersion : `v${pyproject.project.version}`,
93
93
  artifactRegistry : pyproject.leverege.registry || 'us-docker.pkg.dev/leverege-registry/stack',
94
94
  registryFolder : `${pyproject.leverege.registryFolder}/images/${pyproject.project.name}`,
95
95
  imageBase : pyproject.leverege.imageBase,
@@ -219,13 +219,9 @@ log( `
219
219
 
220
220
  ${chalk.green.bold( 'Docker Information:' )}
221
221
  NodeImage: ${chalk.green.bold( dockerInfo.imageBase || `node:${dockerInfo.nodeimage}` )}
222
- Docker Env: ${chalk.green.bold( dockerInfo.buildEnv || 'alpine' )}
223
- AddedPkgs: ${chalk.green.bold( dockerInfo.apkadds )}
224
222
  Run User: ${chalk.green.bold( dockerInfo.runuser )}
225
223
  Previous: ${chalk.yellow.bold( dockerInfo.previousBuild )}
226
224
  DateStamp: ${chalk.green.bold( dockerInfo.date )}
227
- NPM Logs: ${chalk.green.bold( dockerInfo.npmlogging )}
228
- NPM Version: ${chalk.green.bold( dockerInfo.npmVersion )}
229
225
  ` )
230
226
 
231
227
  if ( imageVersion !== packageVersion ) {
@@ -2,26 +2,16 @@
2
2
  #
3
3
  # See => https://cloudnative-pg.io/documentation/current/installation_upgrade/
4
4
  #
5
- OPVER="1.25.1"
5
+ OPVER="1.26.0" # 05/23/2025
6
6
 
7
7
  createNamespaceIfNeeded cnpg-system
8
8
 
9
9
  kubectl apply --server-side -f \
10
10
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-${OPVER%.*}/releases/cnpg-${OPVER}.yaml $K8S_WHAT
11
11
 
12
- # Legacy terraformed clusters (like sandbox) may need a special firewall rule
13
- # added to the network layer on k8s. It should look like this:
12
+ # NOTE: installing release candidates may reference something that looks like:
14
13
  #
15
- # Name : cnpg-operator
16
- # Ports : 8000,9443 <= kubectl cnpg status and webhooks
17
- # Filters: 172.16.0.0/28 <= k8s control plane
14
+ # OPVER="1.26.0-rc1" # 03/28/2025
18
15
  #
19
- # Old approach used the helm chart.
20
- #
21
- #addHelmRepo cnpg https://cloudnative-pg.github.io/charts
22
- #
23
- #helm upgrade --install cnpg cnpg/cloudnative-pg \
24
- # --namespace cnpg-system --create-namespace \
25
- # --set webhook.port="10250" $HELM_WHAT
26
- #
27
- #removeHelmRepo cnpg
16
+ # kubectl apply --server-side -f \
17
+ # https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/releases/cnpg-${OPVER}.yaml $K8S_WHAT
@@ -6,7 +6,7 @@ addHelmRepo prometheus-community https://prometheus-community.github.io/helm-cha
6
6
  NS="--namespace prometheus"
7
7
 
8
8
  showInstalling "The Prometheus Operator (kube-prometheus-stack)"
9
- [ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="72"
9
+ [ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="73"
10
10
 
11
11
  # https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack
12
12
  helm upgrade $NS --install prometheus-stack prometheus-community/kube-prometheus-stack \
@@ -34,4 +34,8 @@ helm upgrade $NS --install prometheus-adapter prometheus-community/prometheus-ad
34
34
  --values prom-operator/prometheus-adapter.yaml \
35
35
  --version $PROMETHEUS_ADAPTER_CHART_VERSION $HELM_WHAT
36
36
 
37
+ # finally, apply the PrometheusRule to generate the GPU duty cycle metric
38
+ showInstalling "The Prometheus Rule (custom GPU metrics rule)"
39
+ kubectl apply -n prometheus -f prom-operator/stackdriver-exporter-gpu-rules.yaml
40
+
37
41
  removeHelmRepo prometheus-community
@@ -39,6 +39,11 @@ prometheus:
39
39
  ruleSelectorNilUsesHelmValues: false
40
40
  serviceMonitorSelectorNilUsesHelmValues: false
41
41
  probeSelectorNilUsesHelmValues: false
42
+ maximumStartupDurationSeconds: 60
43
+ resources:
44
+ requests:
45
+ cpu: 500m
46
+ memory: 2Gi
42
47
  storageSpec:
43
48
  volumeClaimTemplate:
44
49
  spec:
@@ -84,7 +84,7 @@ spec:
84
84
  #
85
85
  - alert: PostgresqlDown
86
86
  expr: cnpg_collector_up == 0
87
- for: 0m
87
+ for: 1m
88
88
  labels:
89
89
  severity: critical
90
90
  annotations:
@@ -10,7 +10,7 @@ spec:
10
10
  rules:
11
11
  - alert: ElasticsearchHeapUsageTooHigh
12
12
  expr: (elasticsearch_jvm_memory_used_bytes{area="heap"} / elasticsearch_jvm_memory_max_bytes{area="heap"}) * 100 > 90
13
- for: 2m
13
+ for: 5m # awesome was => 2m
14
14
  labels:
15
15
  severity: critical
16
16
  annotations:
@@ -21,7 +21,7 @@ spec:
21
21
  LABELS = {{ $labels }}
22
22
  - alert: ElasticsearchHeapUsageWarning
23
23
  expr: (elasticsearch_jvm_memory_used_bytes{area="heap"} / elasticsearch_jvm_memory_max_bytes{area="heap"}) * 100 > 80
24
- for: 2m
24
+ for: 5m # awesome was => 2m
25
25
  labels:
26
26
  severity: warning
27
27
  annotations:
@@ -43,7 +43,7 @@ spec:
43
43
  LABELS = {{ $labels }}
44
44
  - alert: ElasticsearchDiskSpaceLow
45
45
  expr: elasticsearch_filesystem_data_available_bytes / elasticsearch_filesystem_data_size_bytes * 100 < 20
46
- for: 2m
46
+ for: 5m # awesome was => 2m
47
47
  labels:
48
48
  severity: warning
49
49
  annotations:
@@ -54,7 +54,7 @@ spec:
54
54
  LABELS = {{ $labels }}
55
55
  - alert: ElasticsearchClusterRed
56
56
  expr: elasticsearch_cluster_health_status{color="red"} == 1
57
- for: 0m
57
+ for: 5m # awesome was => 0m
58
58
  labels:
59
59
  severity: critical
60
60
  annotations:
@@ -65,7 +65,7 @@ spec:
65
65
  LABELS = {{ $labels }}
66
66
  - alert: ElasticsearchClusterYellow
67
67
  expr: elasticsearch_cluster_health_status{color="yellow"} == 1
68
- for: 0m
68
+ for: 5m # awesome was => 0m
69
69
  labels:
70
70
  severity: warning
71
71
  annotations:
@@ -174,7 +174,7 @@ spec:
174
174
  VALUE = {{ $value }}
175
175
  LABELS = {{ $labels }}
176
176
  - alert: ElasticsearchHighIndexingLatency
177
- expr: elasticsearch_indices_indexing_index_time_seconds_total / elasticsearch_indices_indexing_index_total > 0.002 # awesome was 0.0005
177
+ expr: elasticsearch_indices_indexing_index_time_seconds_total / elasticsearch_indices_indexing_index_total > 0.005 # awesome was 0.0005
178
178
  for: 10m
179
179
  labels:
180
180
  severity: warning
@@ -21,7 +21,7 @@ spec:
21
21
  LABELS = {{ $labels }}
22
22
  - alert: KubernetesNodeMemoryPressure
23
23
  expr: kube_node_status_condition{condition="MemoryPressure",status="true"} == 1
24
- for: 2m
24
+ for: 5m # awesome was => 2m
25
25
  labels:
26
26
  severity: critical
27
27
  annotations:
@@ -32,7 +32,7 @@ spec:
32
32
  LABELS = {{ $labels }}
33
33
  - alert: KubernetesNodeDiskPressure
34
34
  expr: kube_node_status_condition{condition="DiskPressure",status="true",node!~".*preemptible.*"} == 1 # awesome mod => added preemptible check
35
- for: 2m
35
+ for: 5m # awesome was => 2m
36
36
  labels:
37
37
  severity: critical
38
38
  annotations:
@@ -43,7 +43,7 @@ spec:
43
43
  LABELS = {{ $labels }}
44
44
  - alert: KubernetesNodeNetworkUnavailable
45
45
  expr: kube_node_status_condition{condition="NetworkUnavailable",status="true"} == 1
46
- for: 2m
46
+ for: 5m # awesome was => 2m
47
47
  labels:
48
48
  severity: critical
49
49
  annotations:
@@ -54,7 +54,7 @@ spec:
54
54
  LABELS = {{ $labels }}
55
55
  - alert: KubernetesNodeOutOfPodCapacity
56
56
  expr: sum by (node) ((kube_pod_status_phase{phase="Running"} == 1) + on(uid, instance) group_left(node) (0 * kube_pod_info{pod_template_hash=""})) / sum by (node) (kube_node_status_allocatable{resource="pods"}) * 100 > 90
57
- for: 2m
57
+ for: 5m # awesome was => 2m
58
58
  labels:
59
59
  severity: warning
60
60
  annotations:
@@ -1,6 +1,6 @@
1
1
  #
2
2
  # these custom rules exist to essentially boil down the GPU information provided
3
- # by the stackdriver exporter into a form that is a little more humand readable
3
+ # by the stackdriver exporter into a form that is a little more human readable
4
4
  # in an attempt to keep the grafana / alert manager rules a little less "verbose"
5
5
  #
6
6
  # In a nutshell, we take metrics in the form of:
@@ -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="35"
20
+ [ -z "$TRAEFIK_CHART_VERSION" ] && TRAEFIK_CHART_VERSION="36"
21
21
  helm upgrade --install traefik traefik/traefik \
22
22
  --namespace $TRAEFIK_NAMESPACE --create-namespace \
23
23
  --values traefik/values.yaml \
@@ -4,7 +4,7 @@ showInstalling "Velero Backup System"
4
4
 
5
5
  addHelmRepo vmware-tanzu https://vmware-tanzu.github.io/helm-charts
6
6
 
7
- [ -z "$VELERO_CHART_VERSION" ] && VELERO_CHART_VERSION="9"
7
+ [ -z "$VELERO_CHART_VERSION" ] && VELERO_CHART_VERSION="10"
8
8
  #
9
9
  # Build the bucket, region and SA email variables and use --set to mod the
10
10
  # chart values as opposed to using an OVH:<label> approach. This eliminates
@@ -71,7 +71,7 @@ resources:
71
71
  cpu: 250m
72
72
  memory: 128Mi
73
73
  limits:
74
- cpu: 500m
74
+ cpu: 750m
75
75
  memory: 256Mi
76
76
 
77
77
  # Annotations to add to the Velero deployment's. Optional.
package/src/helmup.sh CHANGED
@@ -12,8 +12,8 @@ REGISTRY_DOMAIN="us-docker.pkg.dev"
12
12
  # open container initiative (oci) => https://opencontainers.org/
13
13
  ARTIFACT_REGISTRY="oci://$REGISTRY_DOMAIN/leverege-registry"
14
14
  # login to the registry in the background
15
- gcloud auth print-access-token | helm registry login -u oauth2accesstoken \
16
- --password-stdin https://$REGISTRY_DOMAIN &> $DEVNULL &
15
+ gcloud auth print-access-token | helm registry login \
16
+ -u oauth2accesstoken --password-stdin $REGISTRY_DOMAIN &> $DEVNULL
17
17
 
18
18
  # make sure we have a current npmrc file
19
19
  refresh-npm-token
@@ -175,21 +175,21 @@ function createNamespaceIfNeeded() {
175
175
  function makeUniformBucketWithCors() {
176
176
  local BUCKET_NAME="gs://${GCP_PROJECT_ID}-${1}"
177
177
  printf "Creating/Updating bucket: `color g $BUCKET_NAME`\n"
178
- gsutil ls -p $GCP_PROJECT_ID -b $BUCKET_NAME &> /dev/null
178
+ gsutil ls -p $GCP_PROJECT_ID -b $BUCKET_NAME &> $DEVNULL
179
179
  if [ "$?" -ne 0 ];
180
180
  then
181
- gsutil mb -l $GCE_REGION -p $GCP_PROJECT_ID $BUCKET_NAME &> /dev/null
182
- [ "$?" -ne 0 ] && errorExit "failed to create the bucket: `color y $BUCKET_NAME`"
181
+ gsutil mb -l $GCE_REGION -p $GCP_PROJECT_ID $BUCKET_NAME &> $DEVNULL
182
+ exitOnError $? "failed to create the bucket: `color y $BUCKET_NAME`"
183
183
  fi
184
184
  gcloud storage buckets update $BUCKET_NAME --uniform-bucket-level-access
185
185
  CORS_JSON_FILE="$(build-tools --reporoot)/src/helmup-cors-for-buckets.json"
186
186
  printf " Applying CORS policy\n"
187
- gsutil cors set $CORS_JSON_FILE $BUCKET_NAME &> /dev/null
187
+ gsutil cors set $CORS_JSON_FILE $BUCKET_NAME &> $DEVNULL
188
188
 
189
189
  if [ "$2" == "PUBLIC" ];
190
190
  then
191
191
  printf " Making the bucket `color r PUBLIC`\n"
192
- gsutil iam ch allUsers:objectViewer $BUCKET_NAME &> /dev/null
192
+ gsutil iam ch allUsers:objectViewer $BUCKET_NAME &> $DEVNULL
193
193
  fi
194
194
  }
195
195
 
@@ -479,7 +479,15 @@ spec:
479
479
  - "/bin/sh"
480
480
  args:
481
481
  - "-c"
482
- - "echo 'Hunting Zombies' ; kubectl get pods --all-namespaces --field-selector=status.phase=Failed -o json | kubectl delete -f - 2> /dev/null"
482
+ - |
483
+ echo 'Hunting Zombies' ;
484
+ namespaces="default cnpg-operands elastic prometheus traefik velero"
485
+ for ns in \$namespaces; do
486
+ pods=\$(kubectl get pods -n "\$ns" --field-selector=status.phase=Failed -o name 2>/dev/null)
487
+ if [ -n "\$pods" ]; then
488
+ echo "\$pods" | xargs -r kubectl delete -n "\$ns"
489
+ fi
490
+ done
483
491
  status: {}
484
492
  END_OF_ZOMBIE
485
493
  }
@@ -793,7 +801,7 @@ function installVeleroEnvironment() {
793
801
  gcloud config set project $GCP_PROJECT_ID
794
802
 
795
803
  ## The major chart version will determine the bucket suffix
796
- [ -z "$VELERO_CHART_VERSION" ] && VELERO_CHART_VERSION="9"
804
+ [ -z "$VELERO_CHART_VERSION" ] && VELERO_CHART_VERSION="10"
797
805
 
798
806
  ## Create a bucket
799
807
  BUCKET="$GCP_PROJECT_ID-velero-$VELERO_CHART_VERSION"
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander'
4
+ import { shellCmd, log, warning, errorExit } from './Utils.mjs'
5
+
6
+ const program = new Command()
7
+
8
+ program
9
+ .requiredOption( '--project <projectId>', 'GCP project ID of the GSA' )
10
+ .requiredOption( '--bucket <bucketName>', 'GCS bucket name (no gs:// prefix)' )
11
+ .requiredOption( '--gsa <gsaName>', 'Service account name, e.g. my-service' )
12
+ .requiredOption( '--role <roleId>', 'Custom role ID', 'leveregeStorageObjectReader' )
13
+ .option( '--org', 'Treat as GCP organizational level role' )
14
+ .option( '--add', 'Add IAM binding (default)' )
15
+ .option( '--remove', 'Remove IAM binding' )
16
+ .option( '--dry-run', 'Show command without executing' )
17
+
18
+ program.parse( process.argv )
19
+
20
+ const opts = program.opts()
21
+
22
+ // Validate mutually exclusive add/remove
23
+ if ( opts.add && opts.remove ) {
24
+ errorExit( 'You cannot specify both --add and --remove.' )
25
+ }
26
+
27
+ const action = opts.remove ? 'remove' : 'add'
28
+
29
+ const {
30
+ project,
31
+ org,
32
+ bucket,
33
+ gsa,
34
+ role,
35
+ dryRun
36
+ } = opts
37
+
38
+ const orgId = await shellCmd( 'gcloud organizations list --format="value(ID)"' )
39
+ const gsaEmail = `${gsa}@${project}.iam.gserviceaccount.com`
40
+ const fullRole = org ? `organizations/${orgId}/roles/${role}` : `roles/${role}`
41
+ const bucketUri = `gs://${bucket}`
42
+
43
+ const iamMember = `serviceAccount:${gsaEmail}:${fullRole}`
44
+
45
+ const command = action === 'add' ?
46
+ [ 'gsutil', 'iam', 'ch', iamMember, bucketUri ] :
47
+ [ 'gsutil', 'iam', 'ch', '-d', iamMember, bucketUri ]
48
+
49
+ log( `🔧 ${action === 'add' ? 'Granting' : 'Removing'} ${fullRole} for ${gsaEmail} on ${bucketUri}...` )
50
+
51
+ if ( dryRun ) {
52
+ warning( '💡 DRY RUN mode — no commands executed.' )
53
+ log( `🔗 Command:\n$ ${command.join( ' ' )}` )
54
+ process.exit( 0 )
55
+ }
56
+
57
+ try {
58
+ const result = await shellCmd( command )
59
+ log( `✅ IAM ${action} successful:\n${result}` )
60
+ } catch ( err ) {
61
+ errorExit( `❌ Failed to ${action} IAM binding: ${err.message}` )
62
+ }
63
+
64
+ // === Post-change audit ===
65
+ log( '🔍 Verifying IAM policy for this GSA on the bucket...\n' )
66
+
67
+ try {
68
+ const policyRaw = await shellCmd( [ 'gsutil', 'iam', 'get', bucketUri ] )
69
+ const policy = JSON.parse( policyRaw )
70
+
71
+ const matchingRoles = policy.bindings.filter(
72
+ b => b.members.includes( `serviceAccount:${gsaEmail}` )
73
+ )
74
+
75
+ if ( matchingRoles.length === 0 ) {
76
+ log( `â„šī¸ No IAM bindings found for ${gsaEmail} on ${bucketUri}.` )
77
+ } else {
78
+ for ( const binding of matchingRoles ) {
79
+ log( `🔐 ${gsaEmail} has role: ${binding.role}` )
80
+ }
81
+ }
82
+ } catch ( err ) {
83
+ warning( `âš ī¸ Failed to retrieve or parse IAM policy: ${err.message}` )
84
+ }
@@ -20,23 +20,23 @@ steps:
20
20
  args:
21
21
  - 'tag'
22
22
  - '{{imageName}}:{{imageVersion}}'
23
- - '{{imageName}}:{{imageTag}}'
23
+ - '{{imageName}}:latest'
24
24
 
25
25
  - name: 'gcr.io/cloud-builders/docker'
26
26
  args:
27
- - 'tag'
28
- - '{{imageName}}:{{imageVersion}}'
27
+ - 'push'
29
28
  - '{{imageName}}:latest'
30
29
 
31
30
  - name: 'gcr.io/cloud-builders/docker'
32
31
  args:
33
- - 'push'
34
- - '{{imageName}}:{{imageTag}}'
32
+ - 'tag'
33
+ - '{{imageName}}:{{imageVersion}}'
34
+ - '{{imageName}}:{{imageInfoTag}}'
35
35
 
36
36
  - name: 'gcr.io/cloud-builders/docker'
37
37
  args:
38
38
  - 'push'
39
- - '{{imageName}}:latest'
39
+ - '{{imageName}}:{{imageInfoTag}}'
40
40
 
41
41
  images:
42
42
  - '{{imageName}}:{{imageVersion}}'
@@ -1,60 +1,64 @@
1
1
  # Version {{regvers}} @ {{date}}
2
2
 
3
- # The FROM directive sets the Base Image for subsequent instructions
4
3
  FROM {{image}}
5
4
 
6
5
  ARG BUILD_ENV="{{buildEnv}}"
7
6
 
8
7
  RUN useradd -ms /bin/bash {{runuser}} && \
9
- mkdir -p /usr/src/app && \
10
- chown {{runuser}} /usr/src/app && \
8
+ mkdir -p /usr/src/app /home/{{runuser}} && \
9
+ chown -R {{runuser}} /usr/src/app /home/{{runuser}} && \
11
10
  apt-get update && apt-get install -y \
12
- wget git libgl1 libglib2.0-0 \
13
- && rm -rf /var/lib/apt/lists/*
11
+ wget git libgl1 libglib2.0-0 && \
12
+ rm -rf /var/lib/apt/lists/*
14
13
 
15
- # gcc \
16
- # git \
17
- # && rm -rf /var/lib/apt/lists/*
18
-
19
- # Copy uv binary from the official distroless Docker image.
14
+ # Copy uv binary from distroless image
20
15
  COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
21
16
 
22
- WORKDIR /usr/src/app
23
-
24
- # Install app dependencies
25
- COPY --chown={{runuser}}:{{runuser}} ./workspace/ /usr/src/app/
26
- ENV GRPC_VERBOSITY ERROR
27
-
28
- # Setup pip.conf for Google Artifact Registry.
29
- RUN mkdir -p ~/.config/pip && \
30
- echo "[global]" >> ~/.config/pip/pip.conf && \
31
- echo "break-system-packages = true" >> ~/.config/pip/pip.conf
32
-
17
+ # Copy bashrc early
18
+ COPY --chown={{runuser}}:{{runuser}} ./bashrc /home/{{runuser}}/.bashrc
33
19
 
34
- # Set up the virtual environment.
20
+ WORKDIR /usr/src/app
35
21
  USER {{runuser}}
36
- COPY --chown={{runuser}}:{{runuser}} ./bashrc /home/{{runuser}}/.bashrc
37
22
 
38
- RUN uv venv
23
+ ENV GRPC_VERBOSITY=ERROR
39
24
  ENV VIRTUAL_ENV=/usr/src/app/.venv
40
25
  ENV PATH="$VIRTUAL_ENV/bin:$PATH:/home/{{runuser}}/.local/bin"
41
26
 
42
- # Setup keyring for Google Artifact Registry.
43
- RUN uv tool install keyring --with keyrings.google-artifactregistry-auth
44
-
45
- #RUN echo "extra-index-url = https://oauth2accesstoken@us-python.pkg.dev/leverege-registry/leverege-python-packages/simple" >> ~/.config/pip/pip.conf
46
- #ENV UV_EXTRA_INDEX_URL "https://oauth2accesstoken@us-python.pkg.dev/leverege-registry/leverege-python-packages/simple"
27
+ # Setup pip.conf for GCP Artifact Registry in case pip is needed for building
28
+ RUN mkdir -p /home/{{runuser}}/.config/pip && \
29
+ echo "[global]" >> /home/{{runuser}}/.config/pip/pip.conf && \
30
+ echo "break-system-packages = true" >> /home/{{runuser}}/.config/pip/pip.conf
47
31
 
32
+ # Set up venv and keyring tools before app copy
33
+ RUN uv venv && \
34
+ uv tool install keyring --with keyrings.google-artifactregistry-auth
48
35
 
36
+ # Pre-plugin block: models or pre-install commands
49
37
  {{preInstallPluginfile}}
50
38
 
51
- RUN uv sync --keyring-provider subprocess --extra-index-url https://oauth2accesstoken@us-python.pkg.dev/leverege-registry/leverege-python-packages/simple
39
+ # Copy only dependency definitions first
40
+ COPY --chown=python:python ./workspace/pyproject.toml \
41
+ ./workspace/uv.lock \
42
+ ./workspace/.python-version \
43
+ ./workspace/README.md \
44
+ ./workspace/LICENSE.md \
45
+ /usr/src/app/
52
46
 
53
- # Pull in the Dockerfile.plugin file here
47
+ # Sync dependencies
48
+ RUN uv sync --keyring-provider subprocess \
49
+ --extra-index-url https://oauth2accesstoken@us-python.pkg.dev/leverege-registry/leverege-python-packages/simple \
50
+ && rm -rf /home/{{runuser}}/.cache
51
+
52
+ # Copy application source AFTER venv + tool setup
53
+ COPY --chown={{runuser}}:{{runuser}} ./workspace/ /usr/src/app/
54
+
55
+ # Plugin block for additional pip packages
54
56
  {{pluginfile}}
55
57
 
56
- #USER {{runuser}}
57
- #COPY ./bashrc /home/{{runuser}}/.bashrc
58
+ # Ensure src/ layout is importable from anywhere in the container
59
+ ENV PYTHONPATH="/usr/src/app/src"
60
+
61
+ # Show final venv state (optional)
58
62
  RUN ls /usr/src/app/.venv/bin
63
+
59
64
  CMD [ "/bin/bash", "-c", "source /home/{{runuser}}/.bashrc && python scripts/server.py" ]
60
- #CMD [ "/bin/bash" ]