@leverege/build-tools 2.97.1 → 2.98.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.
@@ -18,6 +18,26 @@
18
18
  },
19
19
  "outputCapture": "std"
20
20
  },
21
+ {
22
+ "name": "ingestor in monorepo",
23
+ "type": "node",
24
+ "request": "launch",
25
+ "program": "${workspaceFolder}/src/init-my-chart/init-my-chart.mjs",
26
+ "args": [],
27
+ "cwd": "/Users/john/Work/leverege/cicd/@kud/recovr-services/packages/recovr-ingestor",
28
+ "runtimeExecutable": "node",
29
+ "console": "integratedTerminal"
30
+ },
31
+ {
32
+ "name": "init-my-chart emailer legacy",
33
+ "type": "node",
34
+ "request": "launch",
35
+ "program": "${workspaceFolder}/src/init-my-chart/init-my-chart.mjs",
36
+ "args": [],
37
+ "cwd": "/Users/john/Work/leverege/cicd/@plt/emailer",
38
+ "runtimeExecutable": "node",
39
+ "console": "integratedTerminal"
40
+ },
21
41
  {
22
42
  "name": "init-my-chart pubsub-pulse",
23
43
  "type": "node",
@@ -38,6 +58,16 @@
38
58
  "runtimeExecutable": "node",
39
59
  "console": "integratedTerminal"
40
60
  },
61
+ {
62
+ "name": "init-my-chart in a lib",
63
+ "type": "node",
64
+ "request": "launch",
65
+ "program": "${workspaceFolder}/src/init-my-chart/init-my-chart.mjs",
66
+ "args": ["--dry-run"],
67
+ "cwd": "/Users/john/Work/leverege/cicd/@lev/secrets",
68
+ "runtimeExecutable": "node",
69
+ "console": "integratedTerminal"
70
+ },
41
71
  {
42
72
  "name": "service-man (launch)",
43
73
  "type": "node",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.97.1",
3
+ "version": "2.98.0",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -83,7 +83,7 @@
83
83
  "@leverege/jsdoc-template": "^1.0.1",
84
84
  "chalk": "^5.6.2",
85
85
  "command-line-args": "^6.0.1",
86
- "command-line-usage": "^7.0.3",
86
+ "command-line-usage": "^7.0.4",
87
87
  "commander": "^14.0.3",
88
88
  "deepmerge": "^4.3.1",
89
89
  "enquirer": "^2.4.1",
@@ -104,17 +104,18 @@
104
104
  "readline-sync": "^1.4.10",
105
105
  "semver": "^7.7.4",
106
106
  "shell-quote": "^1.8.3",
107
- "simple-git": "^3.32.2",
107
+ "simple-git": "^3.32.3",
108
108
  "sloc": "^0.3.2",
109
109
  "superstruct": "^2.0.2",
110
110
  "tmp": "^0.2.5",
111
111
  "toml": "^3.0.0",
112
+ "yaml": "^2.8.2",
112
113
  "zx": "^8.8.5"
113
114
  },
114
115
  "devDependencies": {
115
116
  "@leverege/eslint-config-leverege": "^5.1.2",
116
117
  "chai": "^6.2.2",
117
118
  "mocha": "^11.7.5",
118
- "npm": "^11.10.1"
119
+ "npm": "^11.11.0"
119
120
  }
120
121
  }
package/src/Utils.mjs CHANGED
@@ -530,7 +530,8 @@ export const parseHelmChart = async ( helmroot = './helm' ) => {
530
530
  const chartLibrary = chartYaml?.type === 'library'
531
531
  const chartVersion = chartYaml.version
532
532
  const chartPackage = `${chartName}-${chartVersion}.tgz`
533
- const valuesYaml = YAML.load( fs.readFileSync( `${helmroot}/values.yaml`, 'utf8' ) )
533
+ const valuesText = fs.readFileSync( `${helmroot}/values.yaml`, 'utf8' )
534
+ const valuesYaml = YAML.load( valuesText )
534
535
  debug( valuesYaml, '<== helm/values.yaml' )
535
536
  if ( !valuesYaml.image ) {
536
537
  errorExit( 'Error: legacy helm chart detected - must upgrade to latest ignition template to proceed' )
@@ -572,6 +573,7 @@ export const parseHelmChart = async ( helmroot = './helm' ) => {
572
573
  files : chartFiles,
573
574
  Chart : chartYaml,
574
575
  Values : valuesYaml,
576
+ valuesText, // in case we need to preserve formatting or comments for pushing to registry
575
577
  yaml : { // DEPRECATED
576
578
  'Chart.yaml' : chartYaml,
577
579
  'values.yaml' : valuesYaml,
@@ -8,7 +8,7 @@ showInstalling "The Prometheus Operator (kube-prometheus-stack)"
8
8
 
9
9
  # https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack
10
10
  OCI_CHART="oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack"
11
- [ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="82.1.0"
11
+ [ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="82.9.0"
12
12
  helm upgrade $NS --install prometheus-stack $OCI_CHART \
13
13
  --values prom-operator/prometheus-stack.yaml \
14
14
  --version $PROMETHEUS_STACK_CHART_VERSION $HELM_WHAT
@@ -1,10 +1,10 @@
1
- name: {{containerName}}
2
- version: {{chartVersion}}
3
- appVersion: {{chartAppVersion}}
1
+ name: {{hbsChartName}}
2
+ version: {{hbsChartVersion}}
3
+ appVersion: {{hbsChartAppVersion}}
4
4
  description: A leaf chart that depends on the Leverege base chart
5
5
  apiVersion: v2
6
6
  icon: https://raw.githubusercontent.com/kubernetes/kubernetes/master/logo/logo.svg
7
7
  dependencies:
8
8
  - name: leverege
9
- version: "^1.0.0"
9
+ version: "^1.0.1"
10
10
  repository: "oci://us-docker.pkg.dev/leverege-registry/base-charts"
@@ -1,69 +1,152 @@
1
1
  // Chartwright.mjs
2
+ /* eslint-disable security/detect-non-literal-fs-filename */
3
+ import fs from 'node:fs'
4
+
5
+ import chalk from 'chalk'
6
+ import Handlebars from 'handlebars'
7
+ import YAML from 'yaml'
8
+
2
9
  import * as Utils from '../Utils.mjs'
3
10
 
4
- import {
5
- BUILD_TOOLS_ROOT,
6
- } from '../Utils.mjs'
7
-
8
- const DEFAULT_LIVENESS = {
9
- httpGet : {
10
- path : '/livenessProbe',
11
- port : 'http'
12
- },
13
- initialDelaySeconds : 30,
14
- periodSeconds : 60
15
- }
16
- const DEFAULT_READINESS = {
17
- httpGet : {
18
- path : '/healthz',
19
- port : 'http'
20
- },
21
- initialDelaySeconds : 30,
22
- periodSeconds : 10
11
+ // handlebar variables available in the templates:
12
+ const hbsContext = {
13
+ hbsChartName : '',
14
+ hbsChartVersion : '1.0.0',
15
+ hbsChartAppVersion : 'v1.0.0',
16
+ hbsArtifactRegistry : 'us-docker.pkg.dev/leverege-registry/FOOLDER/images',
17
+ hbsPartOf : '',
18
+ hbsServicePreemptible : true,
19
+ hbsRedisEnabled : true,
20
+ hbsAutoScalingEnabled : true
21
+ }
22
+
23
+ const PRESERVE_SECTIONS = new Set( [
24
+ 'config',
25
+ 'resources',
26
+ 'autoscaling',
27
+ 'staticscaling',
28
+ 'pdb',
29
+ 'probes',
30
+ 'redis',
31
+ ] )
32
+
33
+ // Helper functions
34
+ const warnDeprecatedSections = ( doc, sections ) => {
35
+ let deprecationWarningShown = false
36
+ for ( const key of sections ) {
37
+ if ( !doc.has( key ) ) continue
38
+ Utils.log( `${chalk.yellow.bold( '⚠️ WARNING:' )} Deprecated values.yaml section detected => ${chalk.red.bold( key )}` )
39
+ deprecationWarningShown = true
40
+ }
41
+
42
+ if ( deprecationWarningShown ) {
43
+ Utils.log( chalk.yellow(
44
+ '\nPlease remove any deprecated sections and migrate to the new configuration options as needed.'
45
+ ) )
46
+ }
23
47
  }
24
48
 
25
49
  export class Chartwright {
26
- constructor( { cwd = process.cwd(), debug = false } = {} ) {
27
- this.cwd = cwd
28
- this.chartTemplate = `${BUILD_TOOLS_ROOT}/init-my-chart/leaf-chart-templates`
29
- this.debug = debug
50
+ constructor( { Container, debug = false } = {} ) {
51
+ this.Container = Container
52
+ this.helmTemplates = `${Utils.BUILD_TOOLS_ROOT}/init-my-chart/leaf-chart-templates`
53
+ this.debug = debug
54
+ this.templates = {}
55
+ }
30
56
 
31
- this.pkg = null
32
- this.existingChart = null
57
+ // This function takes a Handlebars template string and a context object,
58
+ // renders the template with the context, and parses the result as YAML,
59
+ // returning the resulting object.
60
+ async renderHbsTemplate( hbsTemplateFileName, context ) {
61
+ // TODO: better error handling here - if the template file doesn't exist,
62
+ // or if the rendered template isn't valid YAML, we should handle those
63
+ // errors gracefully and provide useful error messages.
64
+ const hbsTemplate = fs.readFileSync( hbsTemplateFileName, 'utf8' )
65
+ const compiled = Handlebars.compile( hbsTemplate )
66
+ const rendered = compiled( context )
67
+ return rendered
33
68
  }
34
69
 
35
- async init() {
36
- this.pkg = await Utils.parsePackageJson( { cwd : this.cwd } )
37
- this.existingChart = await Utils.parseHelmChart( )
38
- return this
70
+ // Load the handlebars templates for Chart.yaml and values.yaml into memory.
71
+ async loadTemplates() {
72
+ this.templates.Chart = await this.renderHbsTemplate( `${Utils.BUILD_TOOLS_ROOT}/init-my-chart/Chart.yaml.hbs`, hbsContext )
73
+ this.templates.Values = await this.renderHbsTemplate( `${Utils.BUILD_TOOLS_ROOT}/init-my-chart/values.yaml.hbs`, hbsContext )
39
74
  }
40
75
 
41
- analyze() {
42
- return {
43
- serviceName : this.pkg?.name,
44
- hasExistingChart : !!this.existingChart,
45
- templateLoaded : !!this.templateChart
76
+ // Adds missing top-level keys from base into existing WITHOUT overwriting.
77
+ // IMPORTANT: skips PRESERVE_SECTIONS here; those are handled separately.
78
+ mergeTopLevelAddMissing( baseDoc, existingDoc ) {
79
+ const baseRoot = baseDoc.contents
80
+ const existingRoot = existingDoc.contents
81
+
82
+ if ( !YAML.isMap( baseRoot ) || !YAML.isMap( existingRoot ) ) return
83
+
84
+ for ( const pair of baseRoot.items ) {
85
+ const key = pair.key?.value ?? pair.key
86
+ if ( PRESERVE_SECTIONS.has( key ) ) continue
87
+
88
+ if ( !existingRoot.has( key ) ) {
89
+ // Push the whole Pair node to preserve comments attached to it
90
+ existingRoot.items.push( pair )
91
+ }
46
92
  }
47
93
  }
48
94
 
49
- bootstrap() {
50
- // TODO: create new chart from template
51
- }
95
+ // Preserve sections: if existing has it, keep it untouched.
96
+ // If existing is missing it and base has it, copy it in (with comments).
97
+ applyPreserveSections( baseDoc, existingDoc ) {
98
+ for ( const key of PRESERVE_SECTIONS ) {
99
+ const existingNode = existingDoc.get( key, true )
100
+ if ( existingNode ) continue // keep existing exactly
52
101
 
53
- morph() {
54
- // TODO: transform existing chart to new base structure
102
+ const baseNode = baseDoc.get( key, true )
103
+ if ( baseNode ) {
104
+ existingDoc.set( key, baseNode ) // brings comments from base node
105
+ }
106
+ }
55
107
  }
56
108
 
57
- async scribe() {
58
- // TODO: write resulting chart to disk
109
+ // Bootstraps a helm chart layout by copying the template structure and rendering the
110
+ // main files with the provided context. Optionally can suppress writing values.yaml
111
+ // if the intention is to preserve an existing one.
112
+ async bootstrap( { Container, suppressValues = false } ) {
113
+ // load the templates and render them with the context to get the initial
114
+ // Chart.yaml and values.yaml content
115
+ await this.loadTemplates()
116
+
117
+ // replicate the chart structure from the templates directory
118
+ fs.mkdirSync( 'helm' )
119
+ fs.cpSync( `${Utils.BUILD_TOOLS_ROOT}/init-my-chart/leaf-chart-templates`, 'helm', { recursive : true } )
120
+ fs.writeFileSync( 'helm/Chart.yaml', this.templates.Chart )
121
+ if ( !suppressValues ) {
122
+ fs.writeFileSync( 'helm/values.yaml', this.templates.Values )
123
+ }
59
124
  }
60
125
 
61
- dump() {
62
- if ( !this.debug ) return
63
- console.dir( {
64
- pkg : this.pkg,
65
- existingChart : this.existingChart,
66
- templateChart : this.templateChart
67
- }, { depth : 4 } )
126
+ // Scribes the new chart by bootstrapping the structure and then merging the existing
127
+ // values.yaml with the new template, preserving existing values and comments where possible.
128
+ // This allows us to update the chart structure and add any new configuration options without
129
+ // overwriting user customizations in values.yaml.
130
+ async scribe( { Container, Chart, Values, ValuesText, Registry } ) {
131
+ hbsContext.hbsChartName = Container
132
+ hbsContext.hbsChartVersion = Chart.version
133
+ hbsContext.hbsChartAppVersion = Chart.appVersion
134
+ hbsContext.hbsArtifactRegistry = Registry
135
+ hbsContext.hbsPartOf = Values.partOf
136
+ await this.bootstrap( { Container, suppressValues : true } )
137
+
138
+ // Now we need to smart merge the values.yaml template while preserving existing sections
139
+ const baseValuesDoc = YAML.parseDocument( this.templates.Values )
140
+ const existingValuesDoc = YAML.parseDocument( ValuesText )
141
+ // prune obsolete sections and write the merged values.yaml back to disk, preserving formatting and comments
142
+ const OBSOLETE_SECTIONS = [ 'ingressRoute', 'loadbalancer', 'udploadbalancer' ]
143
+ warnDeprecatedSections( existingValuesDoc, OBSOLETE_SECTIONS )
144
+
145
+ this.mergeTopLevelAddMissing( baseValuesDoc, existingValuesDoc )
146
+ this.applyPreserveSections( baseValuesDoc, existingValuesDoc )
147
+
148
+ // Final write of the merged values.yaml, preserving formatting and comments as much as possible
149
+ fs.writeFileSync( 'helm/values.yaml', existingValuesDoc.toString( { lineWidth : 0, doubleQuotedAsJSON : true } ) )
68
150
  }
69
- }
151
+
152
+ }
@@ -2,16 +2,13 @@
2
2
  /* eslint-disable security/detect-non-literal-fs-filename */
3
3
 
4
4
  import fs from 'node:fs'
5
- import path from 'node:path'
6
5
 
7
6
  import { program } from 'commander'
8
7
  import chalk from 'chalk'
9
- import handlebars from 'handlebars'
10
8
 
11
9
  import {
12
10
  analyzeRepository,
13
11
  condir,
14
- debug,
15
12
  enableDebug,
16
13
  errorExit,
17
14
  log,
@@ -27,54 +24,52 @@ program
27
24
  .parse()
28
25
 
29
26
  const options = program.opts()
30
- const chartwright = await( new Chartwright( { debug : options.debug } ) ).init()
27
+ if ( options.debug ) {
28
+ enableDebug() // enables debugging in the Utils functions
29
+ }
31
30
 
32
31
  const repo = await analyzeRepository()
33
-
34
32
  if ( !repo.isGitRepo ) {
35
33
  errorExit( '***ERROR: This command must be run inside a git repository - run "git init" to proceed' )
36
34
  }
35
+ const Container = repo.containerName
36
+ const chartwright = new Chartwright( { Container, debug : options.debug } )
37
37
 
38
- if ( options.debug ) {
39
- enableDebug()
40
- }
41
-
38
+ // If there isn't an existing helm chart then we just bootstrap the new chart structure
39
+ // from the current helm template and we're done. This will most likely be the case after
40
+ // a new service has been bootstrapped with ignition.
42
41
  if ( !repo.helmChart ) {
43
- log( '*** Bootstrap with the Default Helm Template' ) // TODO: finish this
44
- process.exit( 1 )
42
+ await chartwright.bootstrap( { Container } )
43
+ process.exit( 0 )
45
44
  }
46
45
 
47
- const { Chart, Values, Registry, } = repo.helmChart
48
- const gitRoot = repo.gitRoot
49
-
50
- if ( fs.existsSync( `${gitRoot}/helm-old` ) ) {
46
+ // Safety First: if there is already an older helm chart, we don't want to overwrite it - we
47
+ // want the user to review / intervene before proceeding.
48
+ const packageRoot = repo.currentDir
49
+ if ( fs.existsSync( `${packageRoot}/helm-old` ) ) {
51
50
  errorExit( '***ERROR: An old helm chart structure already exists - cannot proceed' )
52
51
  }
53
52
 
54
53
  let movedOldHelm = false
55
- if ( fs.existsSync( `${gitRoot}/helm` ) && !options.dryRun ) {
56
- fs.renameSync( `${gitRoot}/helm`, `${gitRoot}/helm-old` )
54
+ if ( fs.existsSync( `${packageRoot}/helm` ) && !options.dryRun ) {
55
+ fs.renameSync( `${packageRoot}/helm`, `${packageRoot}/helm-old` )
57
56
  movedOldHelm = true
58
57
  }
59
58
 
60
- debug( { initMyChartRoot, movedOldHelm, repo }, '<==InitMyChart Root Variables' )
59
+ const {
60
+ Chart,
61
+ Values,
62
+ valuesText : ValuesText,
63
+ imageRegistry : Registry,
64
+ } = repo.helmChart
61
65
 
62
66
  if ( options.dryRun ) {
63
- condir( { Chart, Values, Registry, }, chalk.green( '=== Analyzed Repository Helm Chart Info ===' ) )
64
- condir( { chartOutput, valuesOutput }, chalk.green( '=== Generated Helm Chart Outputs ===' ) )
65
- chartwright.dump()
66
- log( { chartOutput }, chalk.green( '=== Chart.yaml ===' ) )
67
- log( { valuesOutput }, chalk.green( '=== values.yaml ===' ) )
67
+ condir( { repo, Container, Chart, Values, ValuesText, Registry }, chalk.green( '<== Dry Run Mode - No Changes Made' ) )
68
68
  process.exit( 0 )
69
69
  }
70
70
 
71
- // TODO: I needed to reset this effort because I didn't like the direction it was
72
- // going in - I want to focus on the transformation of the existing chart to
73
- // the new structure, and less on the templating aspect. The templating can be
74
- // a secondary step that we add after we have the transformation working. For
75
- // now, I will focus on the analysis and transformation of the existing chart,
76
- // and then we can add the templating aspect later.
71
+ await chartwright.scribe( { Container, Chart, Values, ValuesText, Registry } )
77
72
 
78
73
  if ( movedOldHelm ) {
79
- log( chalk.yellow( 'An existing helm chart was detected and moved to helm-old. Please review and migrate any customizations as needed.' ) )
74
+ log( chalk.yellow( '\nAn existing helm chart was detected and moved to helm-old. Please review and migrate any customizations as needed.' ) )
80
75
  }