@leverege/build-tools 2.101.1 → 2.102.0

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 (42) hide show
  1. package/package.json +8 -8
  2. package/src/Utils.mjs +5 -0
  3. package/src/bash-funcs +27 -0
  4. package/src/dashboard/DashboardManager.mjs +192 -17
  5. package/src/dashboard/dashboard.mjs +29 -4
  6. package/src/dashboard/json/{standard/noc-rows-12.json → devops/devops-noc.json} +801 -61
  7. package/src/dashboard/json/{standard/pvc-all-usage.json → devops/pvc-usage.json} +2 -2
  8. package/src/dashboard/json/leverege/leverege-noc.json +2364 -0
  9. package/src/dashboard/json/{standard/redis.json → leverege/leverege-rule-engine.json} +695 -646
  10. package/src/dashboard/json/leverege/leverege-services.json +4757 -0
  11. package/src/dashboard/json/recovr/recovr-locators.json +1227 -0
  12. package/src/dashboard/json/recovr/recovr-noc.json +1949 -0
  13. package/src/dashboard/json/recovr/recovr-position-srvr.json +339 -0
  14. package/src/dashboard/json/recovr/recovr-server.json +1127 -0
  15. package/src/dashboard/json/siren/siren-services.json +2066 -0
  16. package/src/dashboard/json/{standard/cloudnative-pg-20417-rev4.json → system/cloudnative-pg.json} +241 -243
  17. package/src/dashboard/json/{standard → system}/elasticsearch8.json +123 -72
  18. package/src/dashboard/json/system/stackdriver.json +1189 -0
  19. package/src/dashboard/json/{standard/traefik-op.json → system/traefik.json} +44 -56
  20. package/src/dashboard/json/{standard → system}/valkey.json +91 -86
  21. package/src/dashboard/json/{standard → tpi}/gpu-autoscaling-dashboard-ui-import.json +1 -1
  22. package/src/dashboard/json/tpi/leverege-noc.json +3624 -0
  23. package/src/helm-charts/cnpg-operator/helmup.bootstrap +19 -21
  24. package/src/helm-charts/eso/helmup.bootstrap +4 -2
  25. package/src/helm-charts/prom-operator/README-CRD-UPGRADES.md +105 -0
  26. package/src/helm-charts/prom-operator/helmup.bootstrap +22 -14
  27. package/src/helm-charts/prom-operator/helmup.plugin +1 -1
  28. package/src/helm-charts/prom-operator/prometheus-stack.yaml.ovh +23 -45
  29. package/src/helm-charts/prom-operator/rules/gke-maintenance-rules.yaml +19 -0
  30. package/src/helm-charts/prom-operator/stackdriver-exporter.yaml.ovh +3 -2
  31. package/src/helm-charts/valkey/valkey-local.yaml +1 -0
  32. package/src/helm-charts/velero/helmup.bootstrap +2 -0
  33. package/src/helmup.sh +31 -4
  34. package/src/init-my-chart/Chartwright.mjs +2 -2
  35. package/src/init-my-chart/leaf-chart-templates/templates/extraobjects.yaml +1 -0
  36. package/src/init-my-chart/leaf-chart-templates/templates/ingressroute.yaml +1 -0
  37. package/src/overwhelm.mjs +5 -5
  38. package/src/service-man/config/secrets/npmrc.json +14 -0
  39. package/src/service-man/config/secrets/tsdb-dense-password.json +13 -0
  40. package/src/dashboard/json/standard/stackdriver.json +0 -624
  41. package/src/docker-to-registry-py.mjs +0 -287
  42. /package/src/dashboard/json/{standard → devops}/k8s-api-latency.json +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.101.1",
3
+ "version": "2.102.0",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -75,8 +75,8 @@
75
75
  },
76
76
  "author": "Leverege Devs",
77
77
  "engines": {
78
- "node": "^22.11 || ^24.11",
79
- "npm": ">=11.6"
78
+ "node": "^22.11 || >=24",
79
+ "npm": ">=10.2 || >=11.6"
80
80
  },
81
81
  "license": "SEE LICENSE IN LICENSE.md",
82
82
  "dependencies": {
@@ -94,29 +94,29 @@
94
94
  "googleapis": "^171.4.0",
95
95
  "handlebars": "^4.7.9",
96
96
  "ignore": "^7.0.5",
97
- "inquirer": "^13.4.1",
97
+ "inquirer": "^13.4.3",
98
98
  "js-yaml": "^4.1.1",
99
99
  "jsdoc": "^4.0.5",
100
100
  "ms": "^2.1.3",
101
101
  "npm-registry-fetch": "^19.1.1",
102
- "ora": "^9.3.0",
102
+ "ora": "^9.4.0",
103
103
  "p-limit": "^7.3.0",
104
104
  "package-up": "^5.0.0",
105
105
  "readline-sync": "^1.4.10",
106
- "semver": "^7.7.4",
106
+ "semver": "^7.8.0",
107
107
  "shell-quote": "^1.8.3",
108
108
  "simple-git": "^3.36.0",
109
109
  "sloc": "^0.3.2",
110
110
  "superstruct": "^2.0.2",
111
111
  "tmp": "^0.2.5",
112
112
  "toml": "^3.0.0",
113
- "yaml": "^2.8.3",
113
+ "yaml": "^2.9.0",
114
114
  "zx": "^8.8.5"
115
115
  },
116
116
  "devDependencies": {
117
117
  "@leverege/eslint-config-leverege": "^5.1.2",
118
118
  "chai": "^6.2.2",
119
119
  "mocha": "^11.7.5",
120
- "npm": "^11.12.1"
120
+ "npm": "^11.14.1"
121
121
  }
122
122
  }
package/src/Utils.mjs CHANGED
@@ -560,6 +560,11 @@ export const parseHelmChart = async ( helmroot = './helm' ) => {
560
560
  } )
561
561
 
562
562
  depsOutOfSync = mismatches.length > 0
563
+
564
+ // One final check - if the helm/charts dir is missing then we also need to update dependencies
565
+ if ( hasDependencies && !chartFiles.includes( 'charts' ) ) {
566
+ depsOutOfSync = true
567
+ }
563
568
  }
564
569
  /* eslint-enable security/detect-non-literal-fs-filename */
565
570
 
package/src/bash-funcs CHANGED
@@ -229,6 +229,33 @@ function waitForIAM() {
229
229
  done
230
230
  }
231
231
 
232
+ # waitForSAIAM SVC_ACCT PROJECT ROLE MEMBER [TIMEOUT_SECONDS]
233
+ # Like waitForIAM but checks the SA's own IAM policy (for bindings like workloadIdentityUser
234
+ # that are set via `gcloud iam service-accounts add-iam-policy-binding`, not project-level).
235
+ function waitForSAIAM() {
236
+ local svcAcct="$1" project="$2" role="$3" member="$4" timeout="${5:-120}"
237
+ local svcAcctEmail="${svcAcct}@${project}.iam.gserviceaccount.com"
238
+ local start=$(date +%s) delay=2
239
+
240
+ echo "⏳ waiting for SA IAM binding ${role} → ${member} on ${svcAcctEmail} (timeout ${timeout}s)..." >&2
241
+ while true; do
242
+ local found
243
+ found="$(gcloud iam service-accounts get-iam-policy "${svcAcctEmail}" \
244
+ --flatten="bindings[].members" \
245
+ --filter="bindings.role=${role} AND bindings.members=${member}" \
246
+ --format="value(bindings.role)" 2> "$DEVNULL")"
247
+ if [[ -n "$found" ]]; then
248
+ echo "✅ SA IAM binding active: ${role} → ${member}" >&2
249
+ return 0
250
+ fi
251
+ if (( $(date +%s) - start >= timeout )); then
252
+ echo "❌ timed out waiting for SA IAM binding ${role} → ${member}" >&2
253
+ return 1
254
+ fi
255
+ sleep "$delay"; (( delay = delay < 20 ? delay * 2 : 20 ))
256
+ done
257
+ }
258
+
232
259
  function removeHelmRepo(){
233
260
  [ ! -z "$SKIP_RM_HELM_REPO" ] && return
234
261
  helm repo remove $1 &> $DEVNULL
@@ -9,16 +9,25 @@ import { debug, errorExit, log } from '../Utils.mjs'
9
9
  // const FIELD_MANAGER = 'dashboard'
10
10
  const GRAFANA_LABEL = 'grafana_dashboard'
11
11
  const GRAFANA_FOLDER = 'grafana_folder'
12
+ const GRAFANA_LOCAL = 'grafana_dashboard_local'
12
13
 
13
14
  /* eslint-disable security/detect-non-literal-fs-filename */
14
15
  export default class DashboardManager {
15
16
  #api
16
17
  #namespace
17
18
  #dryRun
19
+ #prune
20
+ #folders
21
+ #localDir
22
+ #pruneLocal
18
23
 
19
- constructor( { namespace = 'prometheus', dryRun = false } = {} ) {
24
+ constructor( { namespace = 'prometheus', dryRun = false, prune = false, folders = [], localDir = null, pruneLocal = false } = {} ) {
20
25
  this.#namespace = namespace
21
26
  this.#dryRun = dryRun
27
+ this.#prune = prune
28
+ this.#folders = folders
29
+ this.#localDir = localDir
30
+ this.#pruneLocal = pruneLocal
22
31
  }
23
32
 
24
33
  #getApi() {
@@ -31,21 +40,61 @@ export default class DashboardManager {
31
40
  }
32
41
 
33
42
  async apply( dir ) {
34
- const folders = fs.readdirSync( dir, { withFileTypes : true } )
35
- .filter( e => e.isDirectory() )
43
+ if ( !fs.existsSync( dir ) ) {
44
+ errorExit( `Dashboard directory not found: ${dir}` )
45
+ }
36
46
 
37
- if ( folders.length === 0 ) {
38
- log( chalk.yellow( `No dashboard subfolders found in ${dir}` ) )
47
+ const entries = fs.readdirSync( dir, { withFileTypes : true } )
48
+ const folders = entries.filter( e => e.isDirectory() )
49
+ const jsonFiles = entries.filter( e => e.isFile() && e.name.endsWith( '.json' ) )
50
+
51
+ if ( folders.length === 0 && jsonFiles.length === 0 ) {
52
+ log( chalk.yellow( `No dashboards or subfolders found in ${dir}` ) )
39
53
  return
40
54
  }
41
55
 
42
- for ( const folder of folders ) {
43
- const files = fs.readdirSync( path.join( dir, folder.name ) )
44
- .filter( f => f.endsWith( '.json' ) )
56
+ const applied = new Set()
57
+
58
+ if ( folders.length === 0 ) {
59
+ // leaf directory — apply JSON files using basename as the Grafana folder name
60
+ const folderName = path.basename( dir )
61
+ for ( const file of jsonFiles ) {
62
+ const name = await this.#applyDashboard( folderName, path.join( dir, file.name ) )
63
+ if ( name ) applied.add( name )
64
+ }
65
+ if ( this.#prune ) await this.#pruneOrphans( applied, folderName )
66
+ } else {
67
+ // filter to configured folders if DASHBOARDS was set in overwhelm.yaml
68
+ const activeFolders = this.#folders.length > 0 ?
69
+ folders.filter( f => this.#folders.includes( f.name ) ) :
70
+ folders
71
+
72
+ if ( this.#folders.length > 0 && activeFolders.length < this.#folders.length ) {
73
+ const missing = this.#folders.filter( f => !folders.find( d => d.name === f ) )
74
+ missing.forEach( f => log( chalk.yellow( ` [warn] requested folder not found: ${f}` ) ) )
75
+ }
76
+
77
+ for ( const folder of activeFolders ) {
78
+ const files = fs.readdirSync( path.join( dir, folder.name ) )
79
+ .filter( f => f.endsWith( '.json' ) )
80
+
81
+ for ( const file of files ) {
82
+ const name = await this.#applyDashboard( folder.name, path.join( dir, folder.name, file ) )
83
+ if ( name ) applied.add( name )
84
+ }
85
+ }
86
+
87
+ if ( this.#prune ) await this.#pruneOrphans( applied )
88
+ }
45
89
 
46
- for ( const file of files ) {
47
- await this.#applyDashboard( folder.name, path.join( dir, folder.name, file ) )
90
+ // Apply local dashboards if --local-dir was specified
91
+ if ( this.#localDir ) {
92
+ if ( !fs.existsSync( this.#localDir ) ) {
93
+ errorExit( `Local dashboard directory not found: ${this.#localDir}` )
48
94
  }
95
+ log( chalk.bold( `Applying local dashboards from: ${this.#localDir}` ) )
96
+ const localApplied = await this.#applyDir( this.#localDir, true )
97
+ if ( this.#pruneLocal ) await this.#pruneLocalOrphans( localApplied )
49
98
  }
50
99
  }
51
100
 
@@ -64,7 +113,11 @@ export default class DashboardManager {
64
113
  return
65
114
  }
66
115
 
67
- for ( const folder of folders ) {
116
+ const activeFolders = this.#folders.length > 0 ?
117
+ folders.filter( f => this.#folders.includes( f.name ) ) :
118
+ folders
119
+
120
+ for ( const folder of activeFolders ) {
68
121
  const files = fs.readdirSync( path.join( dir, folder.name ) )
69
122
  .filter( f => f.endsWith( '.json' ) )
70
123
 
@@ -75,20 +128,74 @@ export default class DashboardManager {
75
128
  files.forEach( f => log( ` ${chalk.cyan( path.basename( f, '.json' ) )}` ) )
76
129
  }
77
130
  }
131
+
132
+ if ( this.#localDir && fs.existsSync( this.#localDir ) ) {
133
+ log( chalk.bold( `\nLocal dashboards in: ${this.#localDir}` ) )
134
+ const localFolders = fs.readdirSync( this.#localDir, { withFileTypes : true } )
135
+ .filter( e => e.isDirectory() )
136
+
137
+ for ( const folder of localFolders ) {
138
+ const files = fs.readdirSync( path.join( this.#localDir, folder.name ) )
139
+ .filter( f => f.endsWith( '.json' ) )
140
+
141
+ log( chalk.bold( `\n[${folder.name}] (local)` ) )
142
+ if ( files.length === 0 ) {
143
+ log( chalk.dim( ' (empty)' ) )
144
+ } else {
145
+ files.forEach( f => log( ` ${chalk.cyan( path.basename( f, '.json' ) )}` ) )
146
+ }
147
+ }
148
+ }
78
149
  }
79
150
 
80
- async #applyDashboard( folder, filePath ) {
151
+ // Scans a directory for JSON files (leaf mode) or subfolders and applies all dashboards.
152
+ // Returns the Set of ConfigMap names applied.
153
+ async #applyDir( dir, local = false ) {
154
+ const entries = fs.readdirSync( dir, { withFileTypes : true } )
155
+ const subDirs = entries.filter( e => e.isDirectory() )
156
+ const jsonFiles = entries.filter( e => e.isFile() && e.name.endsWith( '.json' ) )
157
+ const applied = new Set()
158
+
159
+ if ( subDirs.length === 0 ) {
160
+ const folderName = path.basename( dir )
161
+ for ( const file of jsonFiles ) {
162
+ const name = await this.#applyDashboard( folderName, path.join( dir, file.name ), local )
163
+ if ( name ) applied.add( name )
164
+ }
165
+ } else {
166
+ for ( const folder of subDirs ) {
167
+ const files = fs.readdirSync( path.join( dir, folder.name ) )
168
+ .filter( f => f.endsWith( '.json' ) )
169
+
170
+ for ( const file of files ) {
171
+ const name = await this.#applyDashboard( folder.name, path.join( dir, folder.name, file ), local )
172
+ if ( name ) applied.add( name )
173
+ }
174
+ }
175
+ }
176
+
177
+ return applied
178
+ }
179
+
180
+ async #applyDashboard( folder, filePath, local = false ) {
81
181
  const filename = path.basename( filePath )
82
- const name = `dashboard-${path.basename( filePath, '.json' )}`
182
+ const slug = path.basename( filePath, '.json' )
183
+ .toLowerCase()
184
+ .replace( /[^a-z0-9.-]+/g, '-' )
185
+ .replace( /^-+|-+$/g, '' )
186
+ const name = `dashboard-${local ? 'local-' : ''}${slug}`
83
187
  const json = fs.readFileSync( filePath, 'utf8' )
84
188
 
189
+ const labels = { [GRAFANA_LABEL] : '1' }
190
+ if ( local ) labels[GRAFANA_LOCAL] = '1'
191
+
85
192
  const configMap = {
86
193
  apiVersion : 'v1',
87
194
  kind : 'ConfigMap',
88
195
  metadata : {
89
196
  name,
90
197
  namespace : this.#namespace,
91
- labels : { [GRAFANA_LABEL] : '1' },
198
+ labels,
92
199
  annotations : { [GRAFANA_FOLDER] : folder },
93
200
  },
94
201
  data : { [filename] : json },
@@ -97,8 +204,8 @@ export default class DashboardManager {
97
204
  debug( configMap, `dashboard: ${name}` )
98
205
 
99
206
  if ( this.#dryRun ) {
100
- log( chalk.yellow( ` [dry-run] ${filename} → [${folder}]` ) )
101
- return
207
+ log( chalk.yellow( ` [dry-run] ${filename} → [${folder}]${local ? ' (local)' : ''}` ) )
208
+ return name
102
209
  }
103
210
 
104
211
  const api = this.#getApi()
@@ -117,10 +224,78 @@ export default class DashboardManager {
117
224
  } else {
118
225
  await api.createNamespacedConfigMap( { namespace : this.#namespace, body : configMap } )
119
226
  }
120
- log( chalk.green( ` ✓ ${filename} → [${folder}]` ) )
227
+ log( chalk.green( ` ✓ ${filename} → [${folder}]${local ? ' (local)' : ''}` ) )
121
228
  } catch ( e ) {
122
229
  errorExit( `Failed to apply ${name}: ${e.message}` )
123
230
  }
231
+ return name
232
+ }
233
+
234
+ async #pruneOrphans( applied, folderFilter = null ) {
235
+ const api = this.#getApi()
236
+ let configMaps
237
+ try {
238
+ const res = await api.listNamespacedConfigMap( {
239
+ namespace : this.#namespace,
240
+ labelSelector : `${GRAFANA_LABEL}=1`,
241
+ } )
242
+ configMaps = res.items
243
+ } catch ( e ) {
244
+ log( chalk.yellow( ` [prune] could not list ConfigMaps: ${e.message}` ) )
245
+ return
246
+ }
247
+
248
+ const orphans = configMaps.filter( ( cm ) => {
249
+ if ( applied.has( cm.metadata.name ) ) return false
250
+ if ( cm.metadata.labels?.[GRAFANA_LOCAL] === '1' ) return false // leave local CMs to pruneLocal
251
+ if ( folderFilter && cm.metadata.annotations?.[GRAFANA_FOLDER] !== folderFilter ) return false
252
+ return true
253
+ } )
254
+
255
+ for ( const cm of orphans ) {
256
+ const name = cm.metadata.name
257
+ if ( this.#dryRun ) {
258
+ log( chalk.yellow( ` [dry-run] prune ${name}` ) )
259
+ continue
260
+ }
261
+ try {
262
+ await api.deleteNamespacedConfigMap( { name, namespace : this.#namespace } )
263
+ log( chalk.red( ` ✗ pruned ${name}` ) )
264
+ } catch ( e ) {
265
+ log( chalk.yellow( ` [prune] failed to delete ${name}: ${e.message}` ) )
266
+ }
267
+ }
268
+ }
269
+
270
+ async #pruneLocalOrphans( applied ) {
271
+ const api = this.#getApi()
272
+ let configMaps
273
+ try {
274
+ const res = await api.listNamespacedConfigMap( {
275
+ namespace : this.#namespace,
276
+ labelSelector : `${GRAFANA_LOCAL}=1`,
277
+ } )
278
+ configMaps = res.items
279
+ } catch ( e ) {
280
+ log( chalk.yellow( ` [prune-local] could not list ConfigMaps: ${e.message}` ) )
281
+ return
282
+ }
283
+
284
+ const orphans = configMaps.filter( cm => !applied.has( cm.metadata.name ) )
285
+
286
+ for ( const cm of orphans ) {
287
+ const name = cm.metadata.name
288
+ if ( this.#dryRun ) {
289
+ log( chalk.yellow( ` [dry-run] prune-local ${name}` ) )
290
+ continue
291
+ }
292
+ try {
293
+ await api.deleteNamespacedConfigMap( { name, namespace : this.#namespace } )
294
+ log( chalk.red( ` ✗ pruned (local) ${name}` ) )
295
+ } catch ( e ) {
296
+ log( chalk.yellow( ` [prune-local] failed to delete ${name}: ${e.message}` ) )
297
+ }
298
+ }
124
299
  }
125
300
 
126
301
  }
@@ -5,7 +5,7 @@ import path from 'node:path'
5
5
  import { program } from 'commander'
6
6
  import chalk from 'chalk'
7
7
 
8
- import { enableDebug, errorExit, log } from '../Utils.mjs'
8
+ import { enableDebug, errorExit, log, shellCmd } from '../Utils.mjs'
9
9
 
10
10
  import DashboardManager from './DashboardManager.mjs'
11
11
 
@@ -17,16 +17,41 @@ program
17
17
  .argument( '[dir]', 'Directory of dashboard subfolders', defaultJsonDir )
18
18
  .option( '--list', 'List available dashboards without applying' )
19
19
  .option( '--dry-run', 'Show what would be applied without making changes' )
20
+ .option( '--prune', 'Delete ConfigMaps for dashboards no longer on disk' )
21
+ .option( '--local-dir <path>', 'Additional local directory of dashboards to apply alongside official ones' )
22
+ .option( '--prune-local', 'Delete ConfigMaps for local dashboards no longer on disk' )
20
23
  .option( '--debug', 'Enable debug logging' )
21
24
  .option( '-n, --namespace <ns>', 'Target Kubernetes namespace', 'prometheus' )
22
25
  .parse()
23
26
 
24
- const { list, dryRun, debug, namespace } = program.opts()
25
- const [ dir = defaultJsonDir ] = program.args
27
+ const { list, dryRun, prune, localDir, pruneLocal, debug, namespace } = program.opts()
28
+ const [ rawDir ] = program.args
29
+ const dir = rawDir ?
30
+ path.isAbsolute( rawDir ) ? rawDir : path.join( defaultJsonDir, rawDir ) :
31
+ defaultJsonDir
26
32
 
27
33
  if ( debug ) enableDebug()
28
34
 
29
- const mgr = new DashboardManager( { namespace, dryRun } )
35
+ // Read DASHBOARDS from overwhelm.yaml for the current context (exits early, no interactive prompts).
36
+ // If the key is absent or overwhelm.yaml is not present, all folders are loaded.
37
+ let folders = []
38
+ try {
39
+ const raw = ( await shellCmd( 'overwhelm -k DASHBOARDS' ) ).trim()
40
+ if ( raw ) {
41
+ folders = raw.split( ',' ).map( f => f.trim() ).filter( Boolean )
42
+ log( chalk.dim( `Dashboard folders from overwhelm: ${folders.join( ', ' )}` ) )
43
+ }
44
+ } catch {
45
+ // DASHBOARDS not configured — load all folders
46
+ }
47
+
48
+ try {
49
+ await shellCmd( 'overwhelm', { stdio : 'inherit' } )
50
+ } catch ( err ) {
51
+ process.exit( err.exitCode )
52
+ }
53
+
54
+ const mgr = new DashboardManager( { namespace, dryRun, prune, folders, localDir, pruneLocal } )
30
55
 
31
56
  if ( list ) {
32
57
  await mgr.list( dir ).catch( e => errorExit( e ) )