@leverege/build-tools 2.38.1 → 2.38.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,397 @@
1
+ #!/usr/bin/env node
2
+
3
+ // XXX console.log( `TOP=>[${process.cwd()}] PWD=[${process.env.PWD}]` )
4
+ /* eslint-disable no-console */
5
+ /* eslint-disable security/detect-non-literal-fs-filename */
6
+
7
+ import path from 'node:path'
8
+ import { execSync as exec } from 'node:child_process'
9
+ import fs from 'node:fs'
10
+
11
+ import chalk from 'chalk'
12
+ import cliArgs from 'command-line-args'
13
+ // const cliHelp = require( 'command-line-usage' )
14
+ import deepmerge from 'deepmerge'
15
+ import glob from 'glob'
16
+ import parse from 'parse-gitignore'
17
+ import ask from 'readline-sync'
18
+ import YAML from 'js-yaml'
19
+
20
+ const optionsDefinitions = [
21
+ /* eslint-disable no-multi-spaces */
22
+ { name : 'context', alias : 'x', type : String, default : '' },
23
+ { name : 'section', alias : 's', type : String, default : 'default' },
24
+ { name : 'key', alias : 'k', type : String },
25
+ { name : 'export', alias : 'e', type : Boolean, default : false },
26
+ { name : 'continue', alias : 'c', type : Boolean, default : false },
27
+ { name : 'checkctx', alias : 't', type : Boolean, default : false }, // checks overwhelm context
28
+ { name : 'fetchctx', alias : 'f', type : Boolean, default : false }, // fetch and set k8s context
29
+ { name : 'genvals', alias : 'g', type : Boolean, default : false }, // only generates values.yaml
30
+ { name : 'matchk8s', alias : 'm', type : Boolean, default : false }, // checks k8s context
31
+ { name : 'verbose', alias : 'v', type : Boolean, default : false },
32
+ /* eslint-enable no-multi-spaces */
33
+ ]
34
+ const args = cliArgs( optionsDefinitions )
35
+
36
+ // See if we are in a monorepo, which changes some of the rules.
37
+ const gitTop = exec(
38
+ 'git rev-parse --show-toplevel', { stdio : [ undefined ] }
39
+ ).toString().trim()
40
+ let monoTop
41
+ if ( fs.existsSync( `${gitTop}/packages` ) ) {
42
+ monoTop = path.basename( path.resolve() ) // process.env.PWD
43
+ console.log( `MONO=[${monoTop}]` )
44
+ }
45
+
46
+ // The kubernetes context is equal to the git branch name.
47
+ const gitBranch = exec(
48
+ 'git rev-parse --abbrev-ref HEAD', { stdio : [ undefined ] }
49
+ ).toString().trim()
50
+
51
+ const context = args.context || monoTop || gitBranch
52
+
53
+ if ( context === 'master' ) {
54
+ console.log( '***ERROR: master context is forbidden; change to a different branch\n\n' )
55
+ process.exit( 1 )
56
+ }
57
+
58
+ if ( !fs.existsSync( 'overwhelm.yaml' ) ) {
59
+ console.log( chalk.red.bold( `
60
+
61
+ ***ERROR overwhelm => missing the overwhelm.yaml file - this command expects
62
+ to execute from within a platform-k8s branch
63
+ ` ) )
64
+ process.exit( 1 )
65
+ }
66
+
67
+ // Load the default values and then overlay with the project specific yaml
68
+ // values. This supports the notion of this type of approach in the
69
+ // overwhelm yaml settings:
70
+ // default:
71
+ // GCE_MACHINE : "n1-standard-2"
72
+ //
73
+ // my-project:
74
+ // GCE_MACHINE : "n1-highcpu-8"
75
+ //
76
+ // fashion, but it is advised not to set a default region with this
77
+ // approach in order to keep us cognizant of exactly where a cluster is
78
+ // being formed.
79
+ //
80
+ const yaml = YAML.load( fs.readFileSync( 'overwhelm.yaml', 'utf-8' ) )
81
+ const over = yaml.default
82
+ if ( process.env.OVERWHELM_DEBUG ) {
83
+ console.log( { over, yaml }, '<==Contents of the over and yaml structs' )
84
+ }
85
+
86
+ let cfgs = yaml[context]
87
+
88
+ // If we just need to check context return 0 if cfgs exists, else error.
89
+ if ( args.checkctx ) { process.exit( !cfgs ) }
90
+
91
+ if ( !cfgs ) {
92
+ console.log( `***ERROR: Unknown context => [${context}]\n` )
93
+ console.log( ' Known k8s contexts are:' )
94
+ Object.keys( yaml ).forEach( ( key ) => {
95
+ if ( key !== 'default' ) { console.log( ` ${key}` ) }
96
+ } )
97
+ console.log( `Add a section into overwhelm.yaml for ${context}\n\n` )
98
+ process.exit( 1 )
99
+ }
100
+
101
+ // Check to see if we are deriving from another configuration.
102
+ if ( cfgs.PROJECT_BASE ) {
103
+ const baseName = cfgs.PROJECT_BASE
104
+ const tempCfgs = cfgs
105
+ cfgs = yaml[baseName]
106
+ Object.keys( tempCfgs ).forEach( ( key ) => { cfgs[key] = tempCfgs[key] } )
107
+ console.log( 'PROJECT_BASE still under evaluation' )
108
+ console.log( '\n cfgs=', cfgs )
109
+ process.exit( 1 ) // XXX HERE
110
+ }
111
+
112
+ Object.keys( cfgs ).forEach( ( key ) => { over[key] = cfgs[key] || over[key] } )
113
+
114
+ // Expand any variables that reference other variable settings. A perfect
115
+ // example of where this is useful is in the setting of a PROJECT_NAME and
116
+ // a PROJECT_ID. Most of the time these values are identical, which is why
117
+ // this setting exists in the default section:
118
+ // default:
119
+ // PROJECT_ID : ${PROJECT_NAME}
120
+ //
121
+ // However, in the case of projects like Siren Marine, this is the more
122
+ // likely setup due to legacy naming schemes:
123
+ // sire-marine:
124
+ // PROJECT_NAME : siren-marine
125
+ // PROJECT_ID : prod-rt-data
126
+ //
127
+ const varErrors = []
128
+
129
+ Object.keys( over ).forEach( ( key ) => {
130
+ if ( !over[key] ) return // no value to work with - skip it
131
+
132
+ // Look for something in the form of "thing-${VAR1}-${VAR2}-${VARn}-stuff"
133
+ const matches = over[key].match( /\$\{\w+\}/g )
134
+ if ( !matches ) return
135
+
136
+ matches.forEach( ( replaceThis ) => {
137
+ // The matched variable names will be in the form of ${VARNAME} so drop
138
+ // the ${} characters before using the VARNAME to map into the
139
+ // overwhelm map containing the desired replacement value.
140
+ const withThat = over[replaceThis.replace( /\W+/g, '' )]
141
+ if ( !withThat ) {
142
+ varErrors.push( `${key} : ${over[key]}` )
143
+ return
144
+ }
145
+ // Store the replaced string back in the overwhelm value map.
146
+ over[key] = over[key].replace( replaceThis, withThat )
147
+ } )
148
+ } )
149
+
150
+ if ( varErrors.length > 0 ) {
151
+ console.log( '***ERROR: unkown variable reference(s) in overwhelm.yaml:\n' )
152
+ varErrors.forEach( ( error ) => {
153
+ console.log( ` ${error}` )
154
+ } )
155
+ console.log( '' )
156
+ process.exit( 1 )
157
+ }
158
+
159
+ // Grab a list of the existing yaml templates.
160
+ const yams = glob.sync( 'values-*.yaml' )
161
+
162
+ // A function to apply the replacements to all of the matching yaml which
163
+ // will error out if it runs into an unknown OVH<tag>.
164
+ const doReplacements = ( yamls, replacements ) => {
165
+ let replYaml = yamls
166
+
167
+ // wrap special chars with quotes if needed, like "#---incident-alerts---"
168
+ const quoteSpecials = ( raw ) => {
169
+ return /[#$?*]/g.test( raw ) ? `"${raw}"` : raw
170
+ }
171
+
172
+ Object.keys( replacements ).forEach( ( repl ) => {
173
+ const replaceThis = new RegExp( `OVH:<${repl}>`, 'g' )
174
+ const withThat = quoteSpecials( `${replacements[repl]}` )
175
+ replYaml = replYaml.replace( replaceThis, withThat )
176
+ } )
177
+
178
+ // Verify all replaceables have been handled, or error out.
179
+ const missed = replYaml.match( /OVH:<(.*)>/g )
180
+
181
+ if ( missed ) {
182
+ console.log( '\n***ERROR: missing replacements in overwhelm.yaml for these tags:\n' )
183
+ missed.forEach( ( miss ) => {
184
+ console.log( ` TAG: ${miss.match( /OVH:<(.*)>/ )[1]}` )
185
+ } )
186
+ console.log( '' )
187
+ process.exit( 1 )
188
+ }
189
+
190
+ return replYaml
191
+ }
192
+
193
+ // Collect all of the yaml from the yams...
194
+ const buildYaml = ( yamls, replacements ) => {
195
+ const always = [ 'values-default.yaml' ]
196
+ const values = [ ...always, ...yamls ]
197
+ const loaded = []
198
+ let allYamls = ''
199
+
200
+ values.forEach( ( yaml ) => {
201
+ if ( !fs.existsSync( yaml ) ) {
202
+ console.log( `\n***ERROR: yaml file missing => ${yaml}\n` )
203
+ process.exit( 1 )
204
+ }
205
+
206
+ if ( !loaded.includes( yaml ) ) {
207
+ allYamls += `\n# AHOY => ${yaml}\n\n`
208
+ allYamls += fs.readFileSync( yaml, { encoding : 'utf-8' } )
209
+ loaded.push( yaml )
210
+ }
211
+ } )
212
+
213
+ // Now apply all of the replacements from the overwhelm file.
214
+ return doReplacements( allYamls, replacements )
215
+ }
216
+
217
+ // Call buildYaml to replace the OVH strings in the yaml files.
218
+ const values = buildYaml( yams, over )
219
+
220
+ // Check to see if someone is requesting a value from the yaml.
221
+ if ( args.key ) {
222
+ const section = args.section || 'default'
223
+ const keys = yaml[section]
224
+ if ( args.verbose ) {
225
+ console.log( 'replacements' )
226
+ console.log( keys )
227
+ }
228
+ if ( args.export ) {
229
+ console.log( `export ${args.key}=${keys[args.key]}` )
230
+ } else {
231
+ console.log( keys[args.key] )
232
+ }
233
+ if ( !args.continue ) process.exit()
234
+ }
235
+
236
+ // Find all of the potential target dirs. Utilize .gitignore to ignore files
237
+ const gitignore = parse( fs.readFileSync( '.gitignore' ) )
238
+ const opt = {
239
+ ignore : gitignore.patterns,
240
+ }
241
+
242
+ const ahoy = glob.sync( '*/', opt )
243
+
244
+ // .noahoy = skips generation of values.yaml but allows helmup to run
245
+ // .nohelm = overwhelm and helmup will skip the deployment entirely
246
+ ahoy.forEach( ( dir ) => {
247
+ if ( fs.existsSync( `${dir}/.noahoy` ) ) {
248
+ !args.fetchctx && console.log( chalk.yellow( `***WARNING: .noahoy detected, not regenerating ${dir}values.yaml` ) )
249
+ } else if ( !fs.existsSync( `${dir}/.nohelm` ) ) {
250
+ args.verbose && console.log( `Generating values in ${dir}` ) // eslint-disable-line no-unused-expressions
251
+
252
+ let valuesOut = [] // An accumulator for all of the output values.
253
+
254
+ // Add support for localized values for things like keel (or whatever)
255
+ const valuesLocal = `${dir}values-local.yaml`
256
+
257
+ if ( fs.existsSync( valuesLocal ) ) {
258
+ args.verbose && console.log( ` Local values in ${valuesLocal}` ) // eslint-disable-line no-unused-expressions
259
+ valuesOut += '\n# AHOY => values-local.yaml\n\n'
260
+ valuesOut += fs.readFileSync( valuesLocal, { encoding : 'utf-8' } )
261
+ }
262
+ valuesOut += `\n${values}`
263
+
264
+ // Augment the default service section of the YAML with settings that
265
+ // usually come from the top level overwhelm.yaml file.
266
+ //
267
+ const valuesAll = YAML.load( valuesOut ) || {}
268
+ const valuesSvc = {
269
+ service : {
270
+ project_id : 'OVH:<PROJECT_ID>',
271
+ project_name : 'OVH:<PROJECT_NAME>',
272
+ host : 'OVH:<HOST>',
273
+ ingress : 'OVH:<INGRESS>',
274
+ }
275
+ }
276
+ const valuesMerged = YAML.dump( deepmerge( valuesSvc, valuesAll ), { quotingType : '"' } )
277
+
278
+ valuesOut = doReplacements( valuesMerged, over )
279
+
280
+ // DEPRECATED once siren and manheim are platform 4.0
281
+ // If we are going to override then we need to convert it to YAML and
282
+ // do a deepmerge to combine the values.
283
+ const valuesOverride = `${dir}/values-override.yaml`
284
+
285
+ if ( fs.existsSync( valuesOverride ) ) {
286
+ console.log( `===>Applying Overrides=> ${valuesOverride}` )
287
+ const overrides = YAML.load( fs.readFileSync( valuesOverride, { encofind : 'utf-8' } ) )
288
+ const allValues = YAML.load( valuesOut )
289
+ valuesOut = YAML.dump( deepmerge( allValues, overrides ), { quotingType : '"' } )
290
+ }
291
+ // DEPRECATED
292
+
293
+ fs.writeFileSync( `${dir}/values.yaml`, valuesOut )
294
+ }
295
+ } )
296
+
297
+ // we can also apply replaceables to any top level .ovh files to support top level config maps
298
+ glob.sync( '**/*.ovh' ).forEach( ( cfgmap ) => {
299
+ const target = cfgmap.replace( /.ovh$/, '' )
300
+ let valuesOut = fs.readFileSync( cfgmap, { encoding : 'utf-8' } )
301
+ valuesOut = doReplacements( valuesOut, over )
302
+ fs.writeFileSync( target, valuesOut )
303
+ } )
304
+
305
+ if ( args.genvals ) { process.exit( 0 ) }
306
+
307
+ // Set the GCP context for safety!
308
+ const clusterName = over.CLUSTER_NAME
309
+ if ( over.PROJECT_NAME === 'minikube' ) {
310
+ process.exit( 0 )
311
+ }
312
+ const projectId = over.PROJECT_ID
313
+ const zone = over.GCE_REGION + ( over.GCE_ZONE ? `-${over.GCE_ZONE}` : '' )
314
+
315
+ if ( !clusterName ) {
316
+ console.log( chalk.yellow(
317
+ `
318
+
319
+ The overwhelm.yaml file in this branch is missing a CLUSTER_NAME setting,
320
+ which was added to support cluster names that do not match the project
321
+ name. Since the mismatch is rare, you can probably get by with simply
322
+ adding this line following PROJECT_NAME in the default section of
323
+ overwhelm.yaml:
324
+
325
+ ${chalk.bold.green( 'CLUSTER_NAME : \'${PROJECT_NAME}\'' )}
326
+
327
+ However, if the CLUSTER_NAME is not the same as the PROJECT_NAME then go
328
+ ahead and add the CLUSTER_NAME setting to the ${chalk.bold.green( projectId )} section of the
329
+ overwhelm.yaml file.
330
+
331
+ ` ) )
332
+ process.exit( 1 )
333
+ }
334
+
335
+ let kubeout = '_no_current_context_'
336
+ try {
337
+ kubeout = exec( 'kubectl config current-context', { stdio : [ undefined ] } )
338
+ } catch ( err ) {
339
+ // err && console.log( `Caught Error =>[${err}]` )
340
+ }
341
+
342
+ const currentCtx = kubeout.toString().trim()
343
+ const desiredCtx = `gke_${projectId}_${zone}_${clusterName}`
344
+ const cmd = `gcloud container clusters get-credentials ${clusterName} --zone ${zone} --project ${projectId}`
345
+
346
+ const matchedCtx = ( currentCtx === desiredCtx )
347
+
348
+ if ( args.matchk8s ) {
349
+ process.exit( matchedCtx ? 0 : 1 )
350
+ }
351
+
352
+ // just fetch the context and exit if -f is specified
353
+ if ( args.fetchctx ) {
354
+ if ( !matchedCtx ) {
355
+ try {
356
+ exec( `${cmd}`, { stdio : [ undefined ] } )
357
+ } catch ( err ) {
358
+ process.exit( 61 ) // ECONNREFUSED
359
+ }
360
+ }
361
+ process.exit( 0 )
362
+ }
363
+
364
+ if ( matchedCtx ) {
365
+ console.log( `
366
+
367
+ Current context matches => ${chalk.bold.green( currentCtx )}
368
+
369
+ ` )
370
+ } else {
371
+ console.log( `
372
+ You are about to change your Kubernetes context to:
373
+ CLUSTER_NAME => ${chalk.bold.green( clusterName )}
374
+ PROJECT_ID => ${chalk.bold.green( projectId )}
375
+ REGION => ${chalk.bold.green( zone )}
376
+
377
+ ` )
378
+ }
379
+ const operation = matchedCtx ? 'refetch the credentials' : 'switch your context'
380
+ const ans = ask.question( `Enter "${chalk.bold.green( 'yes' )}" if you wish to ${operation} => ` )
381
+ if ( ans !== 'yes' ) {
382
+ if ( matchedCtx ) {
383
+ console.log( chalk.bold.yellow( '\n\nSkipping a refetch of your Kubernetes context and credentials\n\n' ) )
384
+ process.exit( 0 )
385
+ }
386
+
387
+ console.log( chalk.bold.red( '\n\nYour Kubernetes context and credentials do NOT match - this could be bad!\n\n' ) )
388
+ process.exit( 1 )
389
+ }
390
+
391
+ console.log( `\n\nExecuting: ${chalk.bold.yellow( cmd )}\n\n` )
392
+ exec( `${cmd}`, ( error, out ) => {
393
+ if ( error ) {
394
+ console.log( `\n\n***ERROR ${error}\n\n` )
395
+ process.exit( 1 )
396
+ }
397
+ } )
@@ -1,150 +0,0 @@
1
- version: 2.1
2
- jobs:
3
- install:
4
- docker:
5
- - image: $CIRCLE_NODE_IMG
6
- steps:
7
- - checkout
8
- - restore_cache:
9
- keys:
10
- - v1-npm-deps-{{ checksum "package-lock.json" }}
11
- - v1-npm-deps-
12
-
13
- - run:
14
- name: Setup @leverege Token
15
- command: echo \${NPMRC} > ~/.npmrc
16
-
17
- - run: npm install
18
-
19
- - save_cache:
20
- key: v1-npm-deps-{{ checksum "package-lock.json" }}
21
- paths:
22
- - ./node_modules
23
-
24
- - persist_to_workspace:
25
- root: /home/circleci/project
26
- paths:
27
- - ./node_modules
28
-
29
- security-check:
30
- docker:
31
- - image: $CIRCLE_NODE_IMG
32
- steps:
33
- - checkout
34
- - attach_workspace:
35
- at: ~/project
36
- - run:
37
- name: Check for vulnerabilities
38
- command: npm audit --production --audit-level=moderate
39
-
40
- license-check:
41
- docker:
42
- - image: $CIRCLE_NODE_IMG
43
- steps:
44
- - checkout
45
- - attach_workspace:
46
- at: ~/project
47
- - run:
48
- name: Check for LICENSE and README files
49
- command: npm run pkglint
50
-
51
- - run:
52
- name: Check for license violations (e.g. GPL)
53
- command: npx license-checker --production --summary --failOn GPL
54
-
55
- lint:
56
- docker:
57
- - image: $CIRCLE_NODE_IMG
58
- steps:
59
- - checkout
60
- - attach_workspace:
61
- at: ~/project
62
- - run:
63
- name: Lint and save results
64
- command: npm run lint-ci
65
-
66
- - store_test_results:
67
- path: reports
68
-
69
- docs:
70
- docker:
71
- - image: $CIRCLE_NODE_IMG
72
- steps:
73
- - checkout
74
- - attach_workspace:
75
- at: ~/project
76
- - run:
77
- name: Generate docs
78
- command: npx jsdoc -r src -R README.md -d docs
79
- - store_artifacts:
80
- path: docs
81
- prefix: docs
82
-
83
- coverage:
84
- # coverage runs tests, generates coverage, and saves docs
85
- environment:
86
- TZ: 'America/New_York'
87
- docker:
88
- - image: $CIRCLE_NODE_IMG
89
- # IF YOU NEED ANY OF THESE, UNCOMMENT TO USE
90
- # - image: redis
91
- # - image: circleci/mysql:5.7
92
- # environment:
93
- # MYSQL_USER: root
94
- # MYSQL_ROOT_PASSWORD: root
95
- # MYSQL_ALLOW_EMPTY_PASSWORD: true
96
- # MYSQL_HOST: 127.0.0.1
97
- # - image: nsqio/nsq
98
- # command: /nsqlookupd -broadcast-address localhost:4160 -tcp-address 0.0.0.0:4160 -http-address 0.0.0.0:4161
99
-
100
- # - image: nsqio/nsq
101
- # command: >
102
- # /nsqd
103
- # -broadcast-address localhost:4150
104
- # -tcp-address 0.0.0.0:4150
105
- # -http-address 0.0.0.0:4151
106
- # -lookupd-tcp-address localhost:4160
107
-
108
- steps:
109
- - checkout
110
- - attach_workspace:
111
- at: ~/project
112
-
113
- # UNCOMMENT IF YOU NEED DECRYPTING SECRETS
114
- # - run:
115
- # name: Load secrets
116
- # command: npm run decrypt
117
-
118
- - run:
119
- name: Run Tests and Generate Code Coverage
120
- command: npm run coverage
121
-
122
- - run:
123
- name: Check if coverage meets at least 70%
124
- command: npx c8 check-coverage --functions 70 --lines 70 --statements 70
125
-
126
- - store_artifacts:
127
- path: coverage
128
- prefix: coverage
129
-
130
- workflows:
131
- version: 2.1
132
- test-build-deploy:
133
- jobs:
134
- - install:
135
- context: npm
136
- - security-check:
137
- requires:
138
- - install
139
- - license-check:
140
- requires:
141
- - install
142
- - lint:
143
- requires:
144
- - install
145
- - coverage:
146
- requires:
147
- - install
148
- - docs:
149
- requires:
150
- - install