@leverege/build-tools 2.64.0 โ†’ 2.65.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.64.0",
3
+ "version": "2.65.0",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -20,6 +20,7 @@
20
20
  "chart-to-museum": "src/chart-to-museum.mjs",
21
21
  "chart-to-registry": "src/chart-to-registry.mjs",
22
22
  "circleate": "src/circleate.sh",
23
+ "clone-cnpg-from-snapshot": "src/clone-cnpg-from-snapshot.mjs",
23
24
  "decrypt-secrets": "src/decrypt-secrets.sh",
24
25
  "dirty-git": "src/dirty-git.sh",
25
26
  "dockreate": "src/dockreate.sh",
@@ -64,23 +65,25 @@
64
65
  "chalk": "^5.4.1",
65
66
  "command-line-args": "^6.0.1",
66
67
  "command-line-usage": "^7.0.3",
68
+ "commander": "^13.1.0",
67
69
  "deepmerge": "^4.3.1",
68
70
  "enquirer": "^2.4.1",
69
71
  "execa": "^9.5.2",
70
72
  "glob": "^11.0.1",
71
73
  "handlebars": "^4.7.8",
72
- "inquirer": "^12.4.3",
74
+ "ignore": "^7.0.3",
75
+ "inquirer": "^12.5.2",
73
76
  "js-yaml": "^4.1.0",
74
77
  "jsdoc": "^4.0.4",
75
78
  "ms": "^2.1.3",
76
79
  "npm-registry-fetch": "^18.0.2",
80
+ "ora": "^8.2.0",
77
81
  "package-up": "^5.0.0",
78
- "parse-gitignore": "^2.0.0",
79
82
  "readline-sync": "^1.4.10",
80
83
  "semver": "^7.7.1",
81
84
  "simple-git": "^3.27.0",
82
85
  "toml": "^3.0.0",
83
- "zx": "^8.4.0"
86
+ "zx": "^8.5.0"
84
87
  },
85
88
  "devDependencies": {
86
89
  "@leverege/eslint-config-leverege": "^5.0.1",
package/src/Utils.mjs CHANGED
@@ -1,9 +1,10 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
1
+ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import url from 'node:url'
4
4
 
5
5
  import chalk from 'chalk'
6
6
  import { $ } from 'execa'
7
+ import ignore from 'ignore'
7
8
  import inquirer from 'inquirer'
8
9
  import { glob } from 'glob'
9
10
  import { packageUp } from 'package-up'
@@ -89,6 +90,42 @@ export const shellCmd = async ( cmdstr, opts = {} ) => {
89
90
  }
90
91
  }
91
92
 
93
+ export const mkdirSafe = async ( dir ) => {
94
+ try {
95
+ await fs.mkdir( dir, { recursive : true } ) // eslint-disable-line security/detect-non-literal-fs-filename
96
+ } catch ( e ) {
97
+ warning( `Could not create output dir: ${dir}` )
98
+ }
99
+ }
100
+
101
+ export const getDirectories = ( { path : srcPath = process.cwd(), respectGitignore = false } = {} ) => {
102
+ let ig = null
103
+
104
+ if ( respectGitignore ) {
105
+ const gitignorePath = path.join( srcPath, '.gitignore' )
106
+
107
+ /* eslint-disable security/detect-non-literal-fs-filename */
108
+ if ( fs.existsSync( gitignorePath ) ) {
109
+ const gitignoreContent = fs.readFileSync( gitignorePath, 'utf8' )
110
+ ig = ignore().add( gitignoreContent )
111
+ }
112
+ /* eslint-enable security/detect-non-literal-fs-filename */
113
+ }
114
+
115
+ return fs.readdirSync( srcPath, { withFileTypes : true } ) // eslint-disable-line security/detect-non-literal-fs-filename
116
+ .filter( ( dirent ) => {
117
+ if ( !dirent.isDirectory() ) return false
118
+ return !( ig?.ignores( dirent.name ) )
119
+ } )
120
+ .map( dirent => dirent.name )
121
+ }
122
+
123
+ export const xgetDirectories = ( srcPath = process.cwd() ) => {
124
+ return fs.readdirSync( srcPath, { withFileTypes : true } ) // eslint-disable-line security/detect-non-literal-fs-filename
125
+ .filter( dirent => dirent.isDirectory() )
126
+ .map( dirent => dirent.name )
127
+ }
128
+
92
129
  // returns YYYMMDD-hhmm
93
130
  export const getDateTimestamp = () => {
94
131
  const fullDTS = new Date().toISOString().replace( /[-T:]/g, '' ).slice( 0, 14 )
@@ -158,9 +195,9 @@ export const gitRepoIsDirty = async () => {
158
195
 
159
196
  export const parseJsonFile = async ( jsonFile ) => {
160
197
  /* eslint-disable security/detect-non-literal-fs-filename */
161
- if ( existsSync( jsonFile ) ) {
198
+ if ( fs.existsSync( jsonFile ) ) {
162
199
  try {
163
- const json = JSON.parse( readFileSync( jsonFile, 'utf8' ) )
200
+ const json = JSON.parse( fs.readFileSync( jsonFile, 'utf8' ) )
164
201
  return json
165
202
  } catch ( error ) {
166
203
  throw new Error( `${error} in ${jsonFile}` )
@@ -369,13 +406,13 @@ https://console.cloud.google.com/artifacts/browse/leverege-registry?project=leve
369
406
 
370
407
  export const parseHelmChart = async ( helmroot = './helm' ) => {
371
408
  /* eslint-disable security/detect-non-literal-fs-filename */
372
- if ( !existsSync( helmroot ) ) { return undefined }
373
- const chartFiles = readdirSync( helmroot, { encoding : 'utf8', recursive : true } )
374
- const chartYaml = YAML.load( readFileSync( `${helmroot}/Chart.yaml`, 'utf8' ) )
409
+ if ( !fs.existsSync( helmroot ) ) { return undefined }
410
+ const chartFiles = fs.readdirSync( helmroot, { encoding : 'utf8', recursive : true } )
411
+ const chartYaml = YAML.load( fs.readFileSync( `${helmroot}/Chart.yaml`, 'utf8' ) )
375
412
  const chartName = chartYaml.name
376
413
  const chartVersion = chartYaml.version
377
414
  const chartPackage = `${chartName}-${chartVersion}.tgz`
378
- const valuesYaml = YAML.load( readFileSync( `${helmroot}/values.yaml`, 'utf8' ) )
415
+ const valuesYaml = YAML.load( fs.readFileSync( `${helmroot}/values.yaml`, 'utf8' ) )
379
416
  debug( valuesYaml, '<== helm/values.yaml' )
380
417
  if ( !valuesYaml.image ) {
381
418
  errorExit( 'Error: legacy helm chart detected - must upgrade to latest ignition template to proceed' )
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import yaml from 'js-yaml'
5
+
6
+ const SOURCE_URL = 'https://raw.githubusercontent.com/samber/awesome-prometheus-alerts/master/dist/rules/kubernetes/kubestate-exporter.yml'
7
+ const OUTPUT_FILE = 'converted-alerts.yaml'
8
+
9
+ async function fetchAndConvert() {
10
+ /* eslint-disable no-console */
11
+ try {
12
+ console.log( `Fetching YAML from ${SOURCE_URL}...` )
13
+ const response = await fetch( SOURCE_URL )
14
+ if ( !response.ok ) throw new Error( `Failed to fetch: ${response.statusText}` )
15
+
16
+ const rawYaml = await response.text()
17
+ const parsedYaml = yaml.load( rawYaml )
18
+ if ( !parsedYaml.groups ) throw new Error( 'Invalid YAML structure, missing "groups"' )
19
+
20
+ // Reformat into PrometheusRule CRD format
21
+ const prometheusRule = {
22
+ apiVersion : 'monitoring.coreos.com/v1',
23
+ kind : 'PrometheusRule',
24
+ metadata : {
25
+ name : 'node-alerts', // Adjust based on the source
26
+ },
27
+ spec : {
28
+ groups : parsedYaml.groups.map( group => ( {
29
+ name : `${group.name}-rules`,
30
+ rules : group.rules,
31
+ } ) ),
32
+ },
33
+ }
34
+
35
+ // Convert back to YAML with proper formatting
36
+ const formattedYaml = yaml.dump( prometheusRule, { noRefs : true, indent : 0 } )
37
+ fs.writeFileSync( OUTPUT_FILE, formattedYaml, 'utf8' )
38
+ console.log( `Converted YAML written to ${OUTPUT_FILE}` )
39
+ } catch ( error ) {
40
+ console.error( 'Error processing YAML:', error.message )
41
+ }
42
+ }
43
+
44
+ fetchAndConvert()
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env node
2
+
3
+ // clone-cnpg-from-snapshot.mjs
4
+
5
+ import fs from 'node:fs/promises'
6
+ import path from 'node:path'
7
+ import ora from 'ora'
8
+
9
+ import { program } from 'commander'
10
+ import chalk from 'chalk'
11
+ import YAML from 'js-yaml'
12
+ import {
13
+ // condir,
14
+ debug,
15
+ // err,
16
+ errorExit,
17
+ log,
18
+ mkdirSafe,
19
+ shellCmd,
20
+ warning,
21
+ } from './Utils.mjs'
22
+
23
+ program
24
+ .requiredOption( '--snapshot <name>', 'CNPG VolumeSnapshot name' )
25
+ .requiredOption( '--source-project <project>', 'Source GCP project ID' )
26
+ .requiredOption( '--dest-project <project>', 'Destination GCP project ID' )
27
+ .requiredOption( '--dest-region <region>', 'Destination region, e.g. us-east4' )
28
+ .option( '--only-k8s-yaml', 'Only generate the volume snapshot YAML files' )
29
+ .option( '--emit-yaml-dir <dir>', 'Directory to emit VolumeSnapshot YAMLs', './out' )
30
+ .option( '--dry-run', 'Show commands but don\'t run them' )
31
+ .option( '--execute', 'Perform real actions (required to run gcloud commands)' )
32
+ .option( '--cleanup', 'Remove temporary GCP resources after snapshot creation' )
33
+ .option( '--cleanup-only', 'Only do removal of temporary GCP resources and exit' )
34
+ .parse()
35
+
36
+ const options = program.opts()
37
+
38
+ const {
39
+ execute,
40
+ cleanup,
41
+ cleanupOnly,
42
+ destProject,
43
+ destRegion,
44
+ dryRun,
45
+ emitYamlDir,
46
+ snapshot,
47
+ sourceProject,
48
+ } = options
49
+
50
+ const k8sSnapshotName = `${snapshot}-final`
51
+ const tempDisk = `temp-from-${snapshot}`
52
+ const tempImage = `img-from-${snapshot}`
53
+ const intermediateSnapshot = `source-snapshot-${sourceProject}-${snapshot}`
54
+ const destSnapshot = `snapshot-${sourceProject}-${snapshot}`
55
+ const tempDiskFromImage = `temp-from-img-${snapshot}`
56
+ const destZone = `${destRegion}-a` // infer from region - implies "-a" zone always available
57
+
58
+ let sourceZone // set and used in main(), also needed by cleanupResouces
59
+
60
+ const getPrettyTimeNow = () => {
61
+ const now = new Date()
62
+ const [ month, day, year ] = now.toLocaleDateString( 'en-US' ).split( '/' )
63
+ const time = now.toLocaleTimeString( 'en-US', { hour12 : true } )
64
+ return `${year}-${month.padStart( 2, '0' )}-${day.padStart( 2, '0' )} ${time}`
65
+ }
66
+
67
+ const getElapsedTime = ( startMs ) => {
68
+ const deltaSec = Math.floor( ( Date.now() - startMs ) / 1000 )
69
+ const minutes = Math.floor( deltaSec / 60 ).toString().padStart( 2, '0' )
70
+ const seconds = ( deltaSec % 60 ).toString().padStart( 2, '0' )
71
+ return `${minutes}:${seconds}`
72
+ }
73
+
74
+ const writeYaml = async ( filename, data ) => {
75
+ const fullPath = path.join( emitYamlDir, filename )
76
+ const yaml = YAML.dump( data )
77
+ await fs.writeFile( fullPath, yaml ) // eslint-disable-line security/detect-non-literal-fs-filename
78
+ log( chalk.green( ' [โœ“] YAML written:' ), fullPath )
79
+ }
80
+
81
+ const getVolumeSnapshotContentName = async ( snapshotName ) => {
82
+ const json = await shellCmd(
83
+ 'kubectl get volumesnapshotcontents.snapshot.storage.k8s.io -o json'
84
+ )
85
+
86
+ const data = JSON.parse( json )
87
+ const match = data.items.find(
88
+ item => item?.spec?.volumeSnapshotRef?.name === snapshotName
89
+ )
90
+
91
+ return match?.metadata?.name || ''
92
+ }
93
+
94
+ const getVolumeHandle = async ( vscName ) => {
95
+ const cmd = `kubectl get volumesnapshotcontent ${vscName} -o jsonpath='{.spec.source.volumeHandle}'`
96
+ const handle = ( await shellCmd( cmd ) ).replace( /'/g, '' )
97
+ debug( { handle }, '<==getVolumeHandle' )
98
+ return handle
99
+ }
100
+
101
+ const generateK8sYaml = async () => {
102
+ try {
103
+ const snapshotYamlRaw = await shellCmd(
104
+ `kubectl get volumesnapshot ${snapshot} -n cnpg-operands -o json`
105
+ )
106
+
107
+ const snapshotData = JSON.parse( snapshotYamlRaw )
108
+ const annotations = snapshotData?.metadata?.annotations || {}
109
+ const labels = snapshotData?.metadata?.labels || {}
110
+
111
+ // Only keep CNPG-related annotations
112
+ const filteredAnnotations = Object.fromEntries(
113
+ Object.entries( annotations ).filter( ( [ key ] ) => key.startsWith( 'cnpg.io/' ) || key.startsWith( 'snapshotOf' )
114
+ )
115
+ )
116
+
117
+ // Only keep CNPG-related labels
118
+ const filteredLabels = Object.fromEntries(
119
+ Object.entries( labels ).filter( ( [ key ] ) => key.startsWith( 'cnpg.io/' ) || key.startsWith( 'snapshotOf' )
120
+ )
121
+ )
122
+
123
+ const snapshotContent = {
124
+ apiVersion : 'snapshot.storage.k8s.io/v1',
125
+ kind : 'VolumeSnapshotContent',
126
+ metadata : { name : k8sSnapshotName },
127
+ spec : {
128
+ deletionPolicy : 'Retain',
129
+ driver : 'pd.csi.storage.gke.io',
130
+ source : {
131
+ snapshotHandle : `projects/${destProject}/global/snapshots/${destSnapshot}`,
132
+ },
133
+ volumeSnapshotClassName : 'cnpg-preprovisioned',
134
+ volumeSnapshotRef : {
135
+ name : k8sSnapshotName,
136
+ namespace : 'cnpg-operands',
137
+ },
138
+ },
139
+ }
140
+
141
+ const snapshotYaml = {
142
+ apiVersion : 'snapshot.storage.k8s.io/v1',
143
+ kind : 'VolumeSnapshot',
144
+ metadata : {
145
+ name : k8sSnapshotName,
146
+ namespace : 'cnpg-operands',
147
+ annotations : filteredAnnotations,
148
+ labels : filteredLabels,
149
+ },
150
+ spec : {
151
+ volumeSnapshotClassName : 'cnpg-preprovisioned',
152
+ source : {
153
+ volumeSnapshotContentName : k8sSnapshotName,
154
+ },
155
+ },
156
+ }
157
+
158
+ await mkdirSafe( emitYamlDir )
159
+ await writeYaml( 'volumesnapshotcontent.yaml', snapshotContent )
160
+ await writeYaml( 'volumesnapshot.yaml', snapshotYaml )
161
+ } catch ( err ) {
162
+ errorExit( `Unable to generate VolumeSnapshot YAML: ${err.message}` )
163
+ }
164
+
165
+ if ( options.onlyK8sYaml ) {
166
+ process.exit( 0 )
167
+ }
168
+ }
169
+
170
+ // runs a CLI command with a silly little spinner in an attempt to convince
171
+ // the user its worth waiting around for while also being somewhere tolerant
172
+ // of certain faults which are probably safe to ignore
173
+ const runCommand = async ( commandParts, opts = {} ) => {
174
+ const cmdIndent = ' '
175
+ const optIndent = ' '
176
+ const rendered = commandParts
177
+ .map( ( part, idx ) => {
178
+ const isLast = idx === commandParts.length - 1
179
+ return `${idx === 0 ? cmdIndent : optIndent}${part}${isLast ? '' : ' \\'}`
180
+ } )
181
+ .join( '\n' )
182
+
183
+ log( ` \n ${chalk.yellow( ` ๐Ÿš€ Launching @ ${getPrettyTimeNow()}` )}\n${chalk.cyan( rendered )}` )
184
+
185
+ if ( dryRun ) { return }
186
+
187
+ const fullCommand = commandParts.join( ' ' )
188
+ const spinner = ora( {
189
+ text : chalk.green( ' Executing' ),
190
+ prefixText : ' ',
191
+ spinner : 'dots13',
192
+ } ).start()
193
+
194
+ try {
195
+ const startTimeMs = Date.now()
196
+ await shellCmd( fullCommand, opts )
197
+ spinner.succeed( chalk.green( ` Completed @ ${getPrettyTimeNow()} (${getElapsedTime( startTimeMs )})` ) )
198
+ } catch ( error ) {
199
+ const nonFatal = [
200
+ 'already exists',
201
+ 'is already being used',
202
+ 'Cannot reuse name',
203
+ 'already exists',
204
+ ]
205
+
206
+ const msg = error.stderr || ''
207
+
208
+ if ( nonFatal.some( warnError => msg.includes( warnError ) ) ) {
209
+ log( { error }, '<==Full Error' )
210
+ warning( 'non fatal error ignored' )
211
+ spinner.succeed( chalk.yellow( ' Warned' ) )
212
+ return
213
+ }
214
+
215
+ spinner.fail( chalk.red( ' Failed' ) )
216
+ if ( cleanupOnly ) { return }
217
+
218
+ throw error
219
+ }
220
+ }
221
+
222
+ const main = async () => {
223
+ if ( !execute && !dryRun ) {
224
+ errorExit( 'Must specify either --dry-run or --execute' )
225
+ }
226
+
227
+ if ( dryRun ) {
228
+ warning( 'Dry run enabled โ€” no gcloud commands will run.' )
229
+ }
230
+
231
+ // STEP 1: Resolve volume handle from k8s VolumeSnapshotContent
232
+ log( `\n[STEP 1] ๐Ÿ“ฆ Snapshot clone process starting: ${snapshot}` )
233
+ const vscName = await getVolumeSnapshotContentName( snapshot )
234
+ if ( !vscName ) errorExit( `Unable to locate VolumeSnapshotContent for ${snapshot}` )
235
+
236
+ const volumeHandle = await getVolumeHandle( vscName )
237
+ if ( !volumeHandle ) errorExit( `Unable to locate volumeHandle for ${vscName}` )
238
+
239
+ // expect volumeHandle to be in this format:
240
+ // projects/<project>/zones/<zone>/disks/<diskName>
241
+ const volumeParts = volumeHandle.split( '/' )
242
+ const diskName = volumeParts[5]
243
+ sourceZone = volumeParts[3] // sourceZone also needed in cleanupResources
244
+
245
+ log( `\n [โœ“] Resolved disk: ${chalk.cyan( diskName )}` )
246
+ log( ` from handle: ${chalk.cyan( volumeHandle )}` )
247
+ log( ` in zone: ${chalk.cyan( sourceZone )}\n` )
248
+
249
+ await generateK8sYaml()
250
+
251
+ log( `\n[STEP 2] ๐Ÿ“ธ Creating GCE snapshot from source disk [${diskName}]` )
252
+ await runCommand( [
253
+ `gcloud compute disks snapshot ${diskName}`,
254
+ `--snapshot-names=${intermediateSnapshot}`,
255
+ `--zone=${sourceZone}`,
256
+ `--project=${sourceProject}`,
257
+ `--storage-location=${destRegion}`,
258
+ ] )
259
+
260
+ log( '\n[STEP 3] ๐Ÿ’ฝ Creating temp disk from snapshot...' )
261
+ await runCommand( [
262
+ `gcloud compute disks create ${tempDisk}`,
263
+ `--source-snapshot=${intermediateSnapshot}`,
264
+ `--zone=${sourceZone}`,
265
+ `--project=${sourceProject}`,
266
+ ] )
267
+
268
+ log( '\n[STEP 4] ๐Ÿ–ผ๏ธ Creating image from temp disk...' )
269
+ await runCommand( [
270
+ `gcloud compute images create ${tempImage}`,
271
+ `--source-disk=${tempDisk}`,
272
+ `--source-disk-zone=${sourceZone}`,
273
+ `--storage-location=${destRegion}`,
274
+ `--project=${sourceProject}`,
275
+ ] )
276
+
277
+ log( '\n[STEP 5] ๐Ÿ” Granting image access to destination project...' )
278
+ await runCommand( [
279
+ `gcloud compute images add-iam-policy-binding ${tempImage}`,
280
+ `--member=serviceAccount:${destProject}@appspot.gserviceaccount.com`,
281
+ '--role=roles/compute.imageUser',
282
+ `--project=${sourceProject}`,
283
+ ] )
284
+
285
+ log( '\n[STEP 6] ๐Ÿšš Importing image into destination project...' )
286
+ await runCommand( [
287
+ `gcloud compute images create ${tempImage}`,
288
+ `--source-image=${tempImage}`,
289
+ `--source-image-project=${sourceProject}`,
290
+ `--storage-location=${destRegion}`,
291
+ `--project=${destProject}`,
292
+ ] )
293
+
294
+ log( '\n[STEP 7] ๐ŸงŠ Creating temp disk from image in destination project...' )
295
+ await runCommand( [
296
+ `gcloud compute disks create ${tempDiskFromImage}`,
297
+ `--image=${tempImage}`,
298
+ `--image-project=${destProject}`,
299
+ `--zone=${destZone}`,
300
+ `--project=${destProject}`,
301
+ ] )
302
+
303
+ log( '\n[STEP 8] ๐ŸงŠ Creating snapshot in destination project...' )
304
+ await runCommand( [
305
+ `gcloud compute snapshots create ${destSnapshot}`,
306
+ `--source-disk=${tempDiskFromImage}`,
307
+ `--source-disk-zone=${destZone}`,
308
+ `--storage-location=${destRegion}`,
309
+ `--project=${destProject}`,
310
+ ] )
311
+
312
+ log( `\nโœ… Done. Snapshot: ${destSnapshot} and recovery YAMLs ready.` )
313
+ }
314
+
315
+ const cleanupResources = async () => {
316
+ log( '\n[STEP 9] ๐Ÿงน Cleaning up temporary resources (unwinding the stack)...' )
317
+
318
+ // STEP 7: Remove temp disk from image in destination project
319
+ await runCommand( [
320
+ `gcloud compute disks delete ${tempDiskFromImage}`,
321
+ `--zone=${destZone}`,
322
+ `--project=${destProject}`,
323
+ '--quiet'
324
+ ] )
325
+
326
+ // STEP 6: Remove copied image from destination project
327
+ await runCommand( [
328
+ `gcloud compute images delete ${tempImage}`,
329
+ `--project=${destProject}`,
330
+ '--quiet'
331
+ ] )
332
+
333
+ // STEP 4: Remove image from source project
334
+ await runCommand( [
335
+ `gcloud compute images delete ${tempImage}`,
336
+ `--project=${sourceProject}`,
337
+ '--quiet'
338
+ ] )
339
+
340
+ // STEP 3: Remove temp disk from source project
341
+ await runCommand( [
342
+ `gcloud compute disks delete ${tempDisk}`,
343
+ `--zone=${sourceZone}`,
344
+ `--project=${sourceProject}`,
345
+ '--quiet'
346
+ ] )
347
+
348
+ // STEP 2: Remove source snapshot
349
+ await runCommand( [
350
+ `gcloud compute snapshots delete ${intermediateSnapshot}`,
351
+ `--project=${sourceProject}`,
352
+ '--quiet'
353
+ ] )
354
+
355
+ log( chalk.green( ' [โœ“] Cleanup complete. Final GCE snapshot retained for CNPG bootstrapping.' ) )
356
+ }
357
+
358
+ if ( cleanupOnly ) {
359
+ await cleanupResources()
360
+ process.exit( 1 )
361
+ }
362
+
363
+ await main().catch( err => errorExit( err.message || err ) )
364
+
365
+ if ( cleanup ) {
366
+ await cleanupResources()
367
+ }
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import yaml from 'js-yaml'
6
+ import { Command } from 'commander'
7
+ import chalk from 'chalk'
8
+
9
+ import { log, err } from './Utils.mjs'
10
+
11
+ const program = new Command()
12
+
13
+ program
14
+ .name( 'fetch-alerts' )
15
+ .description( 'Fetch a Prometheus alert YAML file and convert it to a PrometheusRule CRD.' )
16
+ .argument( '<sourceFile>', 'YAML filename to fetch from awesome-prometheus' )
17
+ .option( '-o, --output <outputFile>', 'Output file name', 'converted-alerts.yaml' )
18
+ .option( '-n, --name <customName>', 'Override metadata.name in the PrometheusRule' )
19
+ .option( '--dry-run', 'Print output to console instead of writing to file' )
20
+ .parse()
21
+
22
+ const [ sourceFile ] = program.args
23
+ const { output : outputFile, dryRun, name : customName } = program.opts()
24
+
25
+ const BASE_URL = 'https://raw.githubusercontent.com/samber/awesome-prometheus-alerts/master/dist/rules'
26
+ const SOURCE_URL = `${BASE_URL}/${sourceFile}`
27
+
28
+ const defaultName = path.basename( sourceFile ).replace( /\.(yaml|yml)$/, '' )
29
+ const metadataName = customName || defaultName
30
+
31
+ async function fetchAndConvert() {
32
+ try {
33
+ log( chalk.blueBright( `๐Ÿ” Fetching YAML from ${SOURCE_URL}...` ) )
34
+ const response = await fetch( SOURCE_URL )
35
+ if ( !response.ok ) throw new Error( `Failed to fetch: ${response.statusText}` )
36
+
37
+ const rawYaml = await response.text()
38
+ const parsedYaml = yaml.load( rawYaml )
39
+ if ( !parsedYaml.groups ) throw new Error( 'Invalid YAML structure, missing "groups"' )
40
+
41
+ const prometheusRule = {
42
+ apiVersion : 'monitoring.coreos.com/v1',
43
+ kind : 'PrometheusRule',
44
+ metadata : {
45
+ name : metadataName,
46
+ namespace : 'prometheus',
47
+ },
48
+ spec : {
49
+ groups : parsedYaml.groups.map( group => ( {
50
+ name : `${group.name}-rules`,
51
+ rules : group.rules,
52
+ } ) ),
53
+ },
54
+ }
55
+
56
+ const formattedYaml = yaml.dump( prometheusRule, {
57
+ noRefs : true,
58
+ indent : 2,
59
+ lineWidth : -1,
60
+ } )
61
+
62
+ if ( dryRun ) {
63
+ log( chalk.green( `\n๐ŸŸข Dry Run Output for: ${chalk.yellow( metadataName )}\n` ) )
64
+ log( formattedYaml )
65
+ } else {
66
+ fs.writeFileSync( outputFile, formattedYaml, 'utf8' ) // eslint-disable-line security/detect-non-literal-fs-filename
67
+ log( chalk.green( `โœ… Converted YAML written to ${chalk.cyan( outputFile )}` ) )
68
+ }
69
+ } catch ( error ) {
70
+ err( chalk.red( 'โŒ Error:' ), error.message )
71
+ }
72
+ }
73
+
74
+ fetchAndConvert()
@@ -2,12 +2,13 @@
2
2
  #
3
3
  # See => https://cloudnative-pg.io/documentation/current/installation_upgrade/
4
4
  #
5
- OPVER="1.25.1"
5
+ # OPVER="1.25.1"
6
+ OPVER="1.26.0-rc1" # 03/28/2025
6
7
 
7
8
  createNamespaceIfNeeded cnpg-system
8
9
 
9
10
  kubectl apply --server-side -f \
10
- https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-${OPVER%.*}/releases/cnpg-${OPVER}.yaml $K8S_WHAT
11
+ https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/releases/cnpg-${OPVER}.yaml $K8S_WHAT
11
12
 
12
13
  # Legacy terraformed clusters (like sandbox) may need a special firewall rule
13
14
  # added to the network layer on k8s. It should look like this: