@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 +7 -4
- package/src/Utils.mjs +44 -7
- package/src/awesome-fetch.mjs +44 -0
- package/src/clone-cnpg-from-snapshot.mjs +367 -0
- package/src/fetch-awesome-alerts.mjs +74 -0
- package/src/helm-charts/cnpg-operator/helmup.plugin +3 -2
- package/src/helm-charts/prom-operator/rules/awesome-k8s.yaml +326 -0
- package/src/helm-charts/prom-operator/rules/new-awesome.yaml +393 -0
- package/src/helm-charts/velero/velero-local.yaml +1 -1
- package/src/k8cryo.sh +25 -0
- package/src/overwhelm.mjs +2 -7
- package/src/helm-charts/cnpg-db-psql-stack/.cluster.yaml.ovh.swo +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leverege/build-tools",
|
|
3
|
-
"version": "2.
|
|
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
|
-
"
|
|
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.
|
|
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
|
|
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/
|
|
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:
|