@leverege/build-tools 2.64.1 โ 2.65.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.
- package/package.json +7 -4
- package/src/Utils.mjs +45 -7
- package/src/awesome-fetch.mjs +44 -0
- package/src/clone-cnpg-from-snapshot.mjs +449 -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/overwhelm.mjs +2 -7
- package/.vscode/settings.json +0 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leverege/build-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.65.1",
|
|
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,43 @@ export const shellCmd = async ( cmdstr, opts = {} ) => {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
export const mkdirSafe = ( dir ) => {
|
|
94
|
+
try {
|
|
95
|
+
fs.mkdirSync( dir, { recursive : true } ) // eslint-disable-line security/detect-non-literal-fs-filename
|
|
96
|
+
} catch ( error ) {
|
|
97
|
+
condir( { error }, '<==fs.mkdir failed?' )
|
|
98
|
+
warning( { error }, `Could not create output dir: ${dir}` )
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const getDirectories = ( { path : srcPath = process.cwd(), respectGitignore = false } = {} ) => {
|
|
103
|
+
let ig = null
|
|
104
|
+
|
|
105
|
+
if ( respectGitignore ) {
|
|
106
|
+
const gitignorePath = path.join( srcPath, '.gitignore' )
|
|
107
|
+
|
|
108
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
109
|
+
if ( fs.existsSync( gitignorePath ) ) {
|
|
110
|
+
const gitignoreContent = fs.readFileSync( gitignorePath, 'utf8' )
|
|
111
|
+
ig = ignore().add( gitignoreContent )
|
|
112
|
+
}
|
|
113
|
+
/* eslint-enable security/detect-non-literal-fs-filename */
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return fs.readdirSync( srcPath, { withFileTypes : true } ) // eslint-disable-line security/detect-non-literal-fs-filename
|
|
117
|
+
.filter( ( dirent ) => {
|
|
118
|
+
if ( !dirent.isDirectory() ) return false
|
|
119
|
+
return !( ig?.ignores( dirent.name ) )
|
|
120
|
+
} )
|
|
121
|
+
.map( dirent => dirent.name )
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const xgetDirectories = ( srcPath = process.cwd() ) => {
|
|
125
|
+
return fs.readdirSync( srcPath, { withFileTypes : true } ) // eslint-disable-line security/detect-non-literal-fs-filename
|
|
126
|
+
.filter( dirent => dirent.isDirectory() )
|
|
127
|
+
.map( dirent => dirent.name )
|
|
128
|
+
}
|
|
129
|
+
|
|
92
130
|
// returns YYYMMDD-hhmm
|
|
93
131
|
export const getDateTimestamp = () => {
|
|
94
132
|
const fullDTS = new Date().toISOString().replace( /[-T:]/g, '' ).slice( 0, 14 )
|
|
@@ -158,9 +196,9 @@ export const gitRepoIsDirty = async () => {
|
|
|
158
196
|
|
|
159
197
|
export const parseJsonFile = async ( jsonFile ) => {
|
|
160
198
|
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
161
|
-
if ( existsSync( jsonFile ) ) {
|
|
199
|
+
if ( fs.existsSync( jsonFile ) ) {
|
|
162
200
|
try {
|
|
163
|
-
const json = JSON.parse( readFileSync( jsonFile, 'utf8' ) )
|
|
201
|
+
const json = JSON.parse( fs.readFileSync( jsonFile, 'utf8' ) )
|
|
164
202
|
return json
|
|
165
203
|
} catch ( error ) {
|
|
166
204
|
throw new Error( `${error} in ${jsonFile}` )
|
|
@@ -369,13 +407,13 @@ https://console.cloud.google.com/artifacts/browse/leverege-registry?project=leve
|
|
|
369
407
|
|
|
370
408
|
export const parseHelmChart = async ( helmroot = './helm' ) => {
|
|
371
409
|
/* 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' ) )
|
|
410
|
+
if ( !fs.existsSync( helmroot ) ) { return undefined }
|
|
411
|
+
const chartFiles = fs.readdirSync( helmroot, { encoding : 'utf8', recursive : true } )
|
|
412
|
+
const chartYaml = YAML.load( fs.readFileSync( `${helmroot}/Chart.yaml`, 'utf8' ) )
|
|
375
413
|
const chartName = chartYaml.name
|
|
376
414
|
const chartVersion = chartYaml.version
|
|
377
415
|
const chartPackage = `${chartName}-${chartVersion}.tgz`
|
|
378
|
-
const valuesYaml = YAML.load( readFileSync( `${helmroot}/values.yaml`, 'utf8' ) )
|
|
416
|
+
const valuesYaml = YAML.load( fs.readFileSync( `${helmroot}/values.yaml`, 'utf8' ) )
|
|
379
417
|
debug( valuesYaml, '<== helm/values.yaml' )
|
|
380
418
|
if ( !valuesYaml.image ) {
|
|
381
419
|
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,449 @@
|
|
|
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( '--artifacts-only', 'Only generate the volume snapshot YAML, script and readme' )
|
|
29
|
+
.option( '--dry-run', 'Show commands but don\'t run them' )
|
|
30
|
+
.option( '--execute', 'Perform real actions (required to run gcloud commands)' )
|
|
31
|
+
.option( '--cleanup', 'Remove temporary GCP resources after snapshot creation' )
|
|
32
|
+
.option( '--cleanup-only', 'Only do removal of temporary GCP resources and exit' )
|
|
33
|
+
.parse()
|
|
34
|
+
|
|
35
|
+
const options = program.opts()
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
execute,
|
|
39
|
+
cleanup,
|
|
40
|
+
cleanupOnly,
|
|
41
|
+
destProject,
|
|
42
|
+
destRegion,
|
|
43
|
+
dryRun,
|
|
44
|
+
snapshot,
|
|
45
|
+
sourceProject,
|
|
46
|
+
} = options
|
|
47
|
+
|
|
48
|
+
const k8sSnapshotName = `${snapshot}-final`
|
|
49
|
+
const tempDisk = `temp-from-${snapshot}`
|
|
50
|
+
const tempImage = `img-from-${snapshot}`
|
|
51
|
+
const intermediateSnapshot = `source-snapshot-${sourceProject}-${snapshot}`
|
|
52
|
+
const destSnapshot = `snapshot-${sourceProject}-${snapshot}`
|
|
53
|
+
const tempDiskFromImage = `temp-from-img-${snapshot}`
|
|
54
|
+
const destZone = `${destRegion}-a` // infer from region - implies "-a" zone always available
|
|
55
|
+
|
|
56
|
+
// directory for receiving the YAML necessary for defining the snapshot class,
|
|
57
|
+
// content and volume
|
|
58
|
+
const emitYamlDir = `cnpg-clone-${snapshot}`
|
|
59
|
+
|
|
60
|
+
let sourceZone // set and used in main(), also needed by cleanupResouces
|
|
61
|
+
|
|
62
|
+
const getPrettyTimeNow = () => {
|
|
63
|
+
const now = new Date()
|
|
64
|
+
const [ month, day, year ] = now.toLocaleDateString( 'en-US' ).split( '/' )
|
|
65
|
+
const time = now.toLocaleTimeString( 'en-US', { hour12 : true } )
|
|
66
|
+
return `${year}-${month.padStart( 2, '0' )}-${day.padStart( 2, '0' )} ${time}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const getElapsedTime = ( startMs ) => {
|
|
70
|
+
const deltaSec = Math.floor( ( Date.now() - startMs ) / 1000 )
|
|
71
|
+
const minutes = Math.floor( deltaSec / 60 ).toString().padStart( 2, '0' )
|
|
72
|
+
const seconds = ( deltaSec % 60 ).toString().padStart( 2, '0' )
|
|
73
|
+
return `${minutes}:${seconds}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// equivalent to Unix touch command to create an empty file
|
|
77
|
+
const touchFile = async ( filename ) => {
|
|
78
|
+
if ( filename ) {
|
|
79
|
+
const handle = await fs.open( filename, 'w' ) // eslint-disable-line security/detect-non-literal-fs-filename
|
|
80
|
+
await handle.close()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// generates an executable script to use for defining the volume on k8s
|
|
85
|
+
const writeApplyScript = async ( dir ) => {
|
|
86
|
+
const applyScript = `#!/ust/bin/env bash
|
|
87
|
+
#
|
|
88
|
+
# Apply VolumeSnapshot resources prior to deploying the CNPG cluster
|
|
89
|
+
#
|
|
90
|
+
kubectl apply -f ${dir}/volumesnapshotclass.yaml
|
|
91
|
+
kubectl apply -f ${dir}/volumesnapshotcontent.yaml
|
|
92
|
+
kubectl apply -f ${dir}/volumesnapshot.yaml
|
|
93
|
+
`
|
|
94
|
+
|
|
95
|
+
const scriptPath = path.join( dir, 'apply-volumesnapshots.sh' )
|
|
96
|
+
await fs.writeFile( scriptPath, applyScript, { mode : 0o755 } ) // eslint-disable-line security/detect-non-literal-fs-filename
|
|
97
|
+
log( chalk.green( ' [โ] Helper script written:' ), scriptPath )
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const writeReadme = async ( dir ) => {
|
|
101
|
+
const readmePath = path.join( dir, 'readme.txt' )
|
|
102
|
+
const content = `This directory contains the Kubernetes manifests needed to bootstrap a new CNPG cluster
|
|
103
|
+
using a cloned volume snapshot from another GCP project.
|
|
104
|
+
|
|
105
|
+
Files:
|
|
106
|
+
- volumesnapshotclass.yaml (optional, use if the class doesn't already exist)
|
|
107
|
+
- volumesnapshotcontent.yaml (refers to the GCE snapshot copied into this project)
|
|
108
|
+
- volumesnapshot.yaml (used by CNPG to bootstrap from the volume snapshot)
|
|
109
|
+
- apply-volumesnapshots.sh (helper script to apply these manifests in correct order)
|
|
110
|
+
- .nohelm (marker file for overwhelm to skip values.yaml generation)
|
|
111
|
+
|
|
112
|
+
To deploy the volume snapshots to your Kubernetes cluster:
|
|
113
|
+
|
|
114
|
+
$ ./apply-volumesnapshots.sh
|
|
115
|
+
|
|
116
|
+
Once applied, reference the following VolumeSnapshot in your CNPG cluster bootstrap config:
|
|
117
|
+
|
|
118
|
+
spec.bootstrap.recovery.volumeSnapshot.volumeSnapshotName: ${k8sSnapshotName}
|
|
119
|
+
|
|
120
|
+
Then deploy your CNPG cluster normally. Validate the cluster startup:
|
|
121
|
+
|
|
122
|
+
$ kubectl get pods -n cnpg-operands
|
|
123
|
+
|
|
124
|
+
Enjoy your cloned cluster โจ
|
|
125
|
+
`
|
|
126
|
+
|
|
127
|
+
await fs.writeFile( readmePath, content ) // eslint-disable-line security/detect-non-literal-fs-filename
|
|
128
|
+
log( chalk.green( ' [โ] README written:' ), readmePath )
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const writeYaml = async ( filename, data ) => {
|
|
132
|
+
const fullPath = path.join( emitYamlDir, filename )
|
|
133
|
+
const yaml = YAML.dump( data )
|
|
134
|
+
await fs.writeFile( fullPath, yaml ) // eslint-disable-line security/detect-non-literal-fs-filename
|
|
135
|
+
log( chalk.green( ' [โ] YAML written:' ), fullPath )
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const getVolumeSnapshotContentName = async ( snapshotName ) => {
|
|
139
|
+
const json = await shellCmd(
|
|
140
|
+
'kubectl get volumesnapshotcontents.snapshot.storage.k8s.io -o json'
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const data = JSON.parse( json )
|
|
144
|
+
const match = data.items.find(
|
|
145
|
+
item => item?.spec?.volumeSnapshotRef?.name === snapshotName
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return match?.metadata?.name || ''
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const getVolumeHandle = async ( vscName ) => {
|
|
152
|
+
const cmd = `kubectl get volumesnapshotcontent ${vscName} -o jsonpath='{.spec.source.volumeHandle}'`
|
|
153
|
+
const handle = ( await shellCmd( cmd ) ).replace( /'/g, '' )
|
|
154
|
+
debug( { handle }, '<==getVolumeHandle' )
|
|
155
|
+
return handle
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// this function is responsible for emitting all of the artifacts for the creation
|
|
159
|
+
// of the k8s volume that will then be used to bootstrap the cnpg cluster
|
|
160
|
+
const emitCloningArtifacts = async () => {
|
|
161
|
+
try {
|
|
162
|
+
const snapshotYamlRaw = await shellCmd(
|
|
163
|
+
`kubectl get volumesnapshot ${snapshot} -n cnpg-operands -o json`
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
const snapshotData = JSON.parse( snapshotYamlRaw )
|
|
167
|
+
const annotations = snapshotData?.metadata?.annotations || {}
|
|
168
|
+
const labels = snapshotData?.metadata?.labels || {}
|
|
169
|
+
|
|
170
|
+
// Only keep CNPG-related annotations
|
|
171
|
+
const filteredAnnotations = Object.fromEntries(
|
|
172
|
+
Object.entries( annotations ).filter( ( [ key ] ) => key.startsWith( 'cnpg.io/' ) || key.startsWith( 'snapshotOf' )
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
// Only keep CNPG-related labels
|
|
177
|
+
const filteredLabels = Object.fromEntries(
|
|
178
|
+
Object.entries( labels ).filter( ( [ key ] ) => key.startsWith( 'cnpg.io/' ) || key.startsWith( 'snapshotOf' )
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const snapshotClass = {
|
|
183
|
+
apiVersion : 'snapshot.storage.k8s.io/v1',
|
|
184
|
+
kind : 'VolumeSnapshotClass',
|
|
185
|
+
metadata : { name : 'cnpg-preprovisioned' },
|
|
186
|
+
driver : 'pd.csi.storage.gke.io',
|
|
187
|
+
deletionPolicy : 'Retain'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const snapshotContent = {
|
|
191
|
+
apiVersion : 'snapshot.storage.k8s.io/v1',
|
|
192
|
+
kind : 'VolumeSnapshotContent',
|
|
193
|
+
metadata : { name : k8sSnapshotName },
|
|
194
|
+
spec : {
|
|
195
|
+
deletionPolicy : 'Retain',
|
|
196
|
+
driver : 'pd.csi.storage.gke.io',
|
|
197
|
+
source : {
|
|
198
|
+
snapshotHandle : `projects/${destProject}/global/snapshots/${destSnapshot}`,
|
|
199
|
+
},
|
|
200
|
+
volumeSnapshotClassName : 'cnpg-preprovisioned',
|
|
201
|
+
volumeSnapshotRef : {
|
|
202
|
+
name : k8sSnapshotName,
|
|
203
|
+
namespace : 'cnpg-operands',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const snapshotYaml = {
|
|
209
|
+
apiVersion : 'snapshot.storage.k8s.io/v1',
|
|
210
|
+
kind : 'VolumeSnapshot',
|
|
211
|
+
metadata : {
|
|
212
|
+
name : k8sSnapshotName,
|
|
213
|
+
namespace : 'cnpg-operands',
|
|
214
|
+
annotations : filteredAnnotations,
|
|
215
|
+
labels : filteredLabels,
|
|
216
|
+
},
|
|
217
|
+
spec : {
|
|
218
|
+
volumeSnapshotClassName : 'cnpg-preprovisioned',
|
|
219
|
+
source : {
|
|
220
|
+
volumeSnapshotContentName : k8sSnapshotName,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
mkdirSafe( emitYamlDir )
|
|
226
|
+
|
|
227
|
+
await writeYaml( 'volumesnapshotclass.yaml', snapshotClass )
|
|
228
|
+
await writeYaml( 'volumesnapshotcontent.yaml', snapshotContent )
|
|
229
|
+
await writeYaml( 'volumesnapshot.yaml', snapshotYaml )
|
|
230
|
+
await touchFile( path.join( emitYamlDir, '.nohelm' ) )
|
|
231
|
+
await writeApplyScript( emitYamlDir )
|
|
232
|
+
await writeReadme( emitYamlDir )
|
|
233
|
+
|
|
234
|
+
} catch ( err ) {
|
|
235
|
+
errorExit( `Unable to generate VolumeSnapshot YAML: ${err.message}` )
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// runs a CLI command with a silly little spinner in an attempt to convince
|
|
240
|
+
// the user its worth waiting around for while also being somewhere tolerant
|
|
241
|
+
// of certain faults which are probably safe to ignore
|
|
242
|
+
const runCommand = async ( commandParts, opts = {} ) => {
|
|
243
|
+
const cmdIndent = ' '
|
|
244
|
+
const optIndent = ' '
|
|
245
|
+
const rendered = commandParts
|
|
246
|
+
.map( ( part, idx ) => {
|
|
247
|
+
const isLast = idx === commandParts.length - 1
|
|
248
|
+
return `${idx === 0 ? cmdIndent : optIndent}${part}${isLast ? '' : ' \\'}`
|
|
249
|
+
} )
|
|
250
|
+
.join( '\n' )
|
|
251
|
+
|
|
252
|
+
log( ` \n ${chalk.yellow( ` ๐ Launching @ ${getPrettyTimeNow()}` )}\n${chalk.cyan( rendered )}` )
|
|
253
|
+
|
|
254
|
+
if ( dryRun ) { return }
|
|
255
|
+
|
|
256
|
+
const fullCommand = commandParts.join( ' ' )
|
|
257
|
+
const spinner = ora( {
|
|
258
|
+
text : chalk.green( ' Executing' ),
|
|
259
|
+
prefixText : ' ',
|
|
260
|
+
spinner : 'dots13',
|
|
261
|
+
} ).start()
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const startTimeMs = Date.now()
|
|
265
|
+
await shellCmd( fullCommand, opts )
|
|
266
|
+
spinner.succeed( chalk.green( ` Completed @ ${getPrettyTimeNow()} (${getElapsedTime( startTimeMs )})` ) )
|
|
267
|
+
} catch ( error ) {
|
|
268
|
+
const nonFatal = [
|
|
269
|
+
'already exists',
|
|
270
|
+
'is already being used',
|
|
271
|
+
'Cannot reuse name',
|
|
272
|
+
'already exists',
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
const msg = error.stderr || ''
|
|
276
|
+
|
|
277
|
+
if ( nonFatal.some( warnError => msg.includes( warnError ) ) ) {
|
|
278
|
+
log( { error }, '<==Full Error' )
|
|
279
|
+
warning( 'non fatal error ignored' )
|
|
280
|
+
spinner.succeed( chalk.yellow( ' Warned' ) )
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
spinner.fail( chalk.red( ' Failed' ) )
|
|
285
|
+
if ( cleanupOnly ) { return }
|
|
286
|
+
|
|
287
|
+
throw error
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const main = async () => {
|
|
292
|
+
if ( !execute && !dryRun ) {
|
|
293
|
+
errorExit( 'Must specify either --dry-run or --execute' )
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if ( dryRun ) {
|
|
297
|
+
warning( 'Dry run enabled โ no gcloud commands will run.' )
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// STEP 1: Resolve volume handle from k8s VolumeSnapshotContent
|
|
301
|
+
log( `\n[STEP 1] ๐ฆ Snapshot clone process starting: ${snapshot}` )
|
|
302
|
+
const vscName = await getVolumeSnapshotContentName( snapshot )
|
|
303
|
+
if ( !vscName ) errorExit( `Unable to locate VolumeSnapshotContent for ${snapshot}` )
|
|
304
|
+
|
|
305
|
+
const volumeHandle = await getVolumeHandle( vscName )
|
|
306
|
+
if ( !volumeHandle ) errorExit( `Unable to locate volumeHandle for ${vscName}` )
|
|
307
|
+
|
|
308
|
+
// expect volumeHandle to be in this format:
|
|
309
|
+
// projects/<project>/zones/<zone>/disks/<diskName>
|
|
310
|
+
const volumeParts = volumeHandle.split( '/' )
|
|
311
|
+
const diskName = volumeParts[5]
|
|
312
|
+
sourceZone = volumeParts[3] // sourceZone also needed in cleanupResources
|
|
313
|
+
|
|
314
|
+
log( `\n [โ] Resolved disk: ${chalk.cyan( diskName )}` )
|
|
315
|
+
log( ` from handle: ${chalk.cyan( volumeHandle )}` )
|
|
316
|
+
log( ` in zone: ${chalk.cyan( sourceZone )}\n` )
|
|
317
|
+
|
|
318
|
+
// if all we want are the artifacts
|
|
319
|
+
if ( options.artifactsOnly ) {
|
|
320
|
+
await emitCloningArtifacts()
|
|
321
|
+
process.exit( 0 )
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
log( `\n[STEP 2] ๐ธ Creating GCE snapshot from source disk [${diskName}]` )
|
|
325
|
+
await runCommand( [
|
|
326
|
+
`gcloud compute disks snapshot ${diskName}`,
|
|
327
|
+
`--snapshot-names=${intermediateSnapshot}`,
|
|
328
|
+
`--zone=${sourceZone}`,
|
|
329
|
+
`--project=${sourceProject}`,
|
|
330
|
+
`--storage-location=${destRegion}`,
|
|
331
|
+
] )
|
|
332
|
+
|
|
333
|
+
log( '\n[STEP 3] ๐ฝ Creating temp disk from snapshot...' )
|
|
334
|
+
await runCommand( [
|
|
335
|
+
`gcloud compute disks create ${tempDisk}`,
|
|
336
|
+
`--source-snapshot=${intermediateSnapshot}`,
|
|
337
|
+
`--zone=${sourceZone}`,
|
|
338
|
+
`--project=${sourceProject}`,
|
|
339
|
+
] )
|
|
340
|
+
|
|
341
|
+
log( '\n[STEP 4] ๐ผ๏ธ Creating image from temp disk...' )
|
|
342
|
+
await runCommand( [
|
|
343
|
+
`gcloud compute images create ${tempImage}`,
|
|
344
|
+
`--source-disk=${tempDisk}`,
|
|
345
|
+
`--source-disk-zone=${sourceZone}`,
|
|
346
|
+
`--storage-location=${destRegion}`,
|
|
347
|
+
`--project=${sourceProject}`,
|
|
348
|
+
] )
|
|
349
|
+
|
|
350
|
+
log( '\n[STEP 5] ๐ Granting image access to destination project...' )
|
|
351
|
+
await runCommand( [
|
|
352
|
+
`gcloud compute images add-iam-policy-binding ${tempImage}`,
|
|
353
|
+
`--member=serviceAccount:${destProject}@appspot.gserviceaccount.com`,
|
|
354
|
+
'--role=roles/compute.imageUser',
|
|
355
|
+
`--project=${sourceProject}`,
|
|
356
|
+
] )
|
|
357
|
+
|
|
358
|
+
log( '\n[STEP 6] ๐ Importing image into destination project...' )
|
|
359
|
+
await runCommand( [
|
|
360
|
+
`gcloud compute images create ${tempImage}`,
|
|
361
|
+
`--source-image=${tempImage}`,
|
|
362
|
+
`--source-image-project=${sourceProject}`,
|
|
363
|
+
`--storage-location=${destRegion}`,
|
|
364
|
+
`--project=${destProject}`,
|
|
365
|
+
] )
|
|
366
|
+
|
|
367
|
+
log( '\n[STEP 7] ๐ง Creating temp disk from image in destination project...' )
|
|
368
|
+
await runCommand( [
|
|
369
|
+
`gcloud compute disks create ${tempDiskFromImage}`,
|
|
370
|
+
`--image=${tempImage}`,
|
|
371
|
+
`--image-project=${destProject}`,
|
|
372
|
+
`--zone=${destZone}`,
|
|
373
|
+
`--project=${destProject}`,
|
|
374
|
+
] )
|
|
375
|
+
|
|
376
|
+
log( '\n[STEP 8] ๐ง Creating snapshot in destination project...' )
|
|
377
|
+
await runCommand( [
|
|
378
|
+
`gcloud compute snapshots create ${destSnapshot}`,
|
|
379
|
+
`--source-disk=${tempDiskFromImage}`,
|
|
380
|
+
`--source-disk-zone=${destZone}`,
|
|
381
|
+
`--storage-location=${destRegion}`,
|
|
382
|
+
`--project=${destProject}`,
|
|
383
|
+
] )
|
|
384
|
+
|
|
385
|
+
await emitCloningArtifacts()
|
|
386
|
+
|
|
387
|
+
log( `
|
|
388
|
+
โ
Done. Snapshot: ${chalk.cyan( destSnapshot )}
|
|
389
|
+
Recovery YAMLs written to: ${chalk.cyan( emitYamlDir )}
|
|
390
|
+
Run '${chalk.yellow( './apply-recovery-yaml.sh' )}' to prep the cluster for recovery.
|
|
391
|
+
` )
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const cleanupResources = async () => {
|
|
395
|
+
log( '\n[STEP 9] ๐งน Cleaning up temporary resources (unwinding the stack)...' )
|
|
396
|
+
|
|
397
|
+
const commands = [
|
|
398
|
+
[ // STEP 7: Remove temp disk from image in destination project
|
|
399
|
+
`gcloud compute disks delete ${tempDiskFromImage}`,
|
|
400
|
+
`--zone=${destZone}`,
|
|
401
|
+
`--project=${destProject}`,
|
|
402
|
+
'--quiet'
|
|
403
|
+
],
|
|
404
|
+
|
|
405
|
+
[ // STEP 6: Remove copied image from destination project
|
|
406
|
+
`gcloud compute images delete ${tempImage}`,
|
|
407
|
+
`--project=${destProject}`,
|
|
408
|
+
'--quiet'
|
|
409
|
+
],
|
|
410
|
+
|
|
411
|
+
[ // STEP 4: Remove image from source project
|
|
412
|
+
`gcloud compute images delete ${tempImage}`,
|
|
413
|
+
`--project=${sourceProject}`,
|
|
414
|
+
'--quiet'
|
|
415
|
+
],
|
|
416
|
+
|
|
417
|
+
[ // STEP 3: Remove temp disk from source project
|
|
418
|
+
`gcloud compute disks delete ${tempDisk}`,
|
|
419
|
+
`--zone=${sourceZone}`,
|
|
420
|
+
`--project=${sourceProject}`,
|
|
421
|
+
'--quiet'
|
|
422
|
+
],
|
|
423
|
+
|
|
424
|
+
[ // STEP 2: Remove source snapshot
|
|
425
|
+
`gcloud compute snapshots delete ${intermediateSnapshot}`,
|
|
426
|
+
`--project=${sourceProject}`,
|
|
427
|
+
'--quiet'
|
|
428
|
+
],
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
await Promise.all(
|
|
432
|
+
commands.map( cmdParts => runCommand( cmdParts ).catch( ( err ) => {
|
|
433
|
+
warning( `Non-fatal error during cleanup: ${err.message}` )
|
|
434
|
+
} ) )
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
log( chalk.green( ' [โ] Parallel complete. Final GCE snapshot retained for CNPG bootstrapping.' ) )
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if ( cleanupOnly ) {
|
|
441
|
+
await cleanupResources()
|
|
442
|
+
process.exit( 1 )
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
await main().catch( err => errorExit( err.message || err ) )
|
|
446
|
+
|
|
447
|
+
if ( cleanup ) {
|
|
448
|
+
await cleanupResources()
|
|
449
|
+
}
|
|
@@ -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()
|