@leverege/build-tools 2.113.6 → 2.114.0-DEVOP-578.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.
Files changed (41) hide show
  1. package/README.md +0 -15
  2. package/package.json +15 -10
  3. package/src/bash-funcs +1 -1
  4. package/src/build-tools.mjs +3 -2
  5. package/src/chart-compass.yaml +6 -1
  6. package/src/helm-charts/api-server/values-local.yaml +1 -1
  7. package/src/helm-charts/cnpg-db-psql-stack/cluster.yaml.ovh +1 -1
  8. package/src/helm-charts/cnpg-db-tsdb-basic/cluster.yaml.ovh +1 -1
  9. package/src/helm-charts/cnpg-db-tsdb-dense/cluster.yaml.ovh +1 -1
  10. package/src/helm-charts/llm-prompt-server/values-local.yaml +5 -0
  11. package/src/helm-charts/prom-operator/helmup.plugin +3 -3
  12. package/src/helm-charts/prom-operator/prometheus-stack.yaml.ovh +1 -1
  13. package/src/helm-charts/triton-inference-server/values-local.yaml +28 -0
  14. package/src/helm-charts/vms-server/values-local.yaml +45 -0
  15. package/src/helmup.sh +5 -4
  16. package/src/k8s/commands/completion.mjs +100 -0
  17. package/src/k8s/commands/cryo.mjs +164 -0
  18. package/src/k8s/commands/exe.mjs +57 -0
  19. package/src/k8s/commands/fwd.mjs +35 -0
  20. package/src/k8s/commands/log.mjs +76 -0
  21. package/src/k8s/commands/roll.mjs +37 -0
  22. package/src/k8s/commands/scale.mjs +43 -0
  23. package/src/k8s/k8s.mjs +26 -0
  24. package/src/k8s/lib/gate.mjs +9 -0
  25. package/src/k8s/lib/kubectl.mjs +26 -0
  26. package/src/k8s/lib/select.mjs +18 -0
  27. package/src/overwhelm.mjs +1 -1
  28. package/src/repo-panopticon.mjs +16 -12
  29. package/src/service-man/config/Config.mjs +1 -1
  30. package/src/service-man/config/iam-roles/leveregeVertexEndpointUser.json +10 -0
  31. package/src/service-man/config/service-accounts/llm-prompt-server.json +6 -0
  32. package/src/service-man/config/service-accounts/triton-inference-server.json +5 -0
  33. package/src/service-man/config/service-accounts/vms-server.json +8 -0
  34. package/src/service-man/gcloud-k8s/ServiceAccountManager.mjs +9 -5
  35. package/src/artifacts/ArtifactPuller.mjs +0 -131
  36. package/src/artifacts/ArtifactPusher.mjs +0 -110
  37. package/src/artifacts/Structs.mjs +0 -98
  38. package/src/artifacts/Utils.mjs +0 -284
  39. package/src/artifacts/pull-artifact.mjs +0 -81
  40. package/src/artifacts/push-artifact.mjs +0 -80
  41. package/src/change-up.sh +0 -27
package/README.md CHANGED
@@ -199,21 +199,6 @@ klog <app-name> <ns> # specify namespace
199
199
 
200
200
  ---
201
201
 
202
- ### Artifact Management
203
-
204
- **`pull-artifact`** — Pulls generic artifacts from GCP Artifact Registry based on an
205
- `artifacts.yaml` spec file.
206
- ```bash
207
- pull-artifact [-f <spec-file>] [-n <name>] [-v <version>]
208
- ```
209
-
210
- **`push-artifact`** — Pushes generic artifacts to GCP Artifact Registry.
211
- ```bash
212
- push-artifact [-f <spec-file>] [-n <name>] [-v <version>]
213
- ```
214
-
215
- ---
216
-
217
202
  ### Package & Release Management
218
203
 
219
204
  **`hook-and-release`** — Manages git pre-commit hooks and version consistency for npm
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.113.6",
3
+ "version": "2.114.0-DEVOP-578.2",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "lint": "eslint",
15
- "lint-fix": "eslint --fix",
15
+ "lint:fix": "eslint --fix",
16
16
  "prepack": "npm exec prepack",
17
17
  "prepare": "[ $SKIP_PREPARE ] && exit 0 || ./src/hook-and-release.mjs",
18
18
  "test": "mocha -t 60000 --exit 'test/**/!(*e2e*).mjs'",
@@ -21,7 +21,6 @@
21
21
  "bin": {
22
22
  "build-cnpg-image": "src/build-cnpg-image.mjs",
23
23
  "build-tools": "src/build-tools.mjs",
24
- "change-up": "src/change-up.sh",
25
24
  "chart-compass": "src/chart-compass.mjs",
26
25
  "chart-to-registry": "src/chart-to-registry.mjs",
27
26
  "circle-orb-it": "src/circle-orb-it/circle-orb-it.mjs",
@@ -53,15 +52,14 @@
53
52
  "k8scale": "src/k8scale.sh",
54
53
  "k8x": "src/k8x.sh",
55
54
  "klog": "src/klog.sh",
55
+ "k8s": "src/k8s/k8s.mjs",
56
56
  "npm-link": "src/npm-link.sh",
57
57
  "npm-unlink": "src/npm-unlink.sh",
58
58
  "overwhelm": "src/overwhelm.mjs",
59
59
  "pkglint": "src/pkglint.sh",
60
60
  "prepack": "src/prepack.mjs",
61
61
  "prune-git": "src/prune-git.sh",
62
- "pull-artifact": "src/artifacts/pull-artifact.mjs",
63
62
  "pull-git": "src/pull-git.sh",
64
- "push-artifact": "src/artifacts/push-artifact.mjs",
65
63
  "push-my-chart": "src/push-my-chart.mjs",
66
64
  "refresh-npm-token": "src/refresh-npm-token.mjs",
67
65
  "refresh-py-idx": "src/refresh-py-idx.sh",
@@ -70,6 +68,7 @@
70
68
  "service-man": "src/service-man/service-man.mjs",
71
69
  "snapshot-cnpg": "src/snapshot-cnpg.mjs",
72
70
  "sync-chart-version": "src/sync-chart-version.mjs",
71
+ "throwup": "src/helmup.sh",
73
72
  "upload-model": "src/upload-model.sh",
74
73
  "yarn-build": "src/yarn-build.mjs"
75
74
  },
@@ -83,16 +82,22 @@
83
82
  "npm": "11.12.0"
84
83
  },
85
84
  "license": "SEE LICENSE IN LICENSE.md",
85
+ "overrides": {
86
+ "diff": "^8.0.3",
87
+ "mocha": {
88
+ "serialize-javascript": "^7.0.3"
89
+ }
90
+ },
86
91
  "dependencies": {
87
92
  "@kubernetes/client-node": "^1.4.0",
88
93
  "@leverege/jsdoc-template": "^1.0.1",
89
- "chalk": "^5.6.2",
94
+ "chalk": "^6.0.0",
90
95
  "command-line-args": "^6.0.2",
91
96
  "command-line-usage": "^7.0.4",
92
97
  "commander": "^15.0.0",
93
98
  "deepmerge": "^4.3.1",
94
99
  "enquirer": "^2.4.1",
95
- "execa": "^9.6.1",
100
+ "execa": "^10.0.1",
96
101
  "find-yarn-workspace-root": "^2.0.0",
97
102
  "glob": "^13.0.6",
98
103
  "googleapis": "^173.0.0",
@@ -113,7 +118,7 @@
113
118
  "sloc": "^0.3.2",
114
119
  "superstruct": "^2.0.2",
115
120
  "tmp": "^0.2.7",
116
- "toml": "^4.1.1",
121
+ "toml": "^5.0.0",
117
122
  "yaml": "^2.9.0",
118
123
  "zx": "^8.8.5"
119
124
  },
@@ -121,8 +126,8 @@
121
126
  "@leverege/eslint-config-leverege": "^9",
122
127
  "chai": "^6.2.2",
123
128
  "eslint": "^9",
124
- "mocha": "^11.7.6",
125
- "npm": "^11.17.0"
129
+ "mocha": "^11.8.0",
130
+ "npm": "^12.0.2"
126
131
  },
127
132
  "allowScripts": {
128
133
  "unrs-resolver@1.12.2": true
package/src/bash-funcs CHANGED
@@ -298,7 +298,7 @@ function ifPluggedIn() {
298
298
  local PLUGIN=
299
299
  HELMUP="helm upgrade --install $SERVICE $CHARTLOC -f $SERVICE/values.yaml $CHARTVER $HELM_WHAT"
300
300
  case "$INVOKED_AS" in
301
- "helmup"|"helmwhat"|"helmcycle"|"helminit" )
301
+ "helmup"|"helmwhat"|"helmcycle"|"helminit"|"throwup" )
302
302
  PLUGIN="$1/helmup.plugin"
303
303
  ;;
304
304
  "helmdn" )
@@ -9,7 +9,7 @@ import chalk from 'chalk'
9
9
  import cliArgs from 'command-line-args'
10
10
  import cliHelp from 'command-line-usage'
11
11
  import npmRegistryFetch from 'npm-registry-fetch'
12
- import { lt as semverLt } from 'semver'
12
+ import { lt as semverLt, prerelease } from 'semver'
13
13
 
14
14
  // because __dirname goes away with ESM
15
15
  const getFilename = () => {
@@ -124,7 +124,8 @@ const checkForLatest = async ( pkg = '@leverege/build-tools' ) => {
124
124
  const list = await npmRegistryFetch.json(
125
125
  pkg, { '//registry.npmjs.org/:_authToken' : getToken() }
126
126
  )
127
- return list['dist-tags'].latest
127
+ const latest = list['dist-tags'].latest
128
+ return prerelease( latest ) ? undefined : latest
128
129
  } catch ( err ) {
129
130
  console.log( err )
130
131
  }
@@ -22,6 +22,11 @@
22
22
  - transponder-tsdb
23
23
 
24
24
  leverege:
25
+ llm-prompt-server: {}
26
+ overdose: {}
25
27
  pubsub-pulse: {}
26
28
  pusher: {}
27
- overdose: {}
29
+ vms-server: {}
30
+
31
+ system:
32
+ triton-inference-server: {}
@@ -15,7 +15,7 @@ config:
15
15
  MODEL_SQL_HOST: "cnpg-db-psql-stack-pool-rw.cnpg-operands"
16
16
  PG_MODELS_HOST: "cnpg-db-psql-stack-pool-rw.cnpg-operands"
17
17
  # timescale
18
- PG_HOST: "cnpg-db-tsdb-basic-pool-r.cnpg-operands"
18
+ PG_HOST: "cnpg-db-tsdb-basic-r.cnpg-operands"
19
19
  # imagine db is legacy - newer setups use timescale as the db name
20
20
  PG_DATABASE: "timescale"
21
21
  PG_DENSE_HISTORY_DATABASE: "timescale"
@@ -55,7 +55,7 @@ spec:
55
55
  size: 16Gi
56
56
 
57
57
  resources:
58
- # limit:
58
+ # limits:
59
59
  # cpu: 1000m
60
60
  # memory: 8Gi
61
61
  requests:
@@ -55,7 +55,7 @@ spec:
55
55
  size: 16Gi
56
56
 
57
57
  resources:
58
- # limit:
58
+ # limits:
59
59
  # cpu: 1000m
60
60
  # memory: 8Gi
61
61
  requests:
@@ -54,7 +54,7 @@ spec:
54
54
  size: 16Gi
55
55
 
56
56
  resources:
57
- # limit:
57
+ # limits:
58
58
  # cpu: 1000m
59
59
  # memory: 8Gi
60
60
  requests:
@@ -0,0 +1,5 @@
1
+ image:
2
+ tag:
3
+
4
+ config:
5
+ LOG_CONFIG: '{"type":"pino","level":"warn"}'
@@ -8,14 +8,14 @@ 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="86.2.3"
11
+ [ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="87.21.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
15
15
 
16
16
  # https://artifacthub.io/packages/helm/prometheus-community/prometheus-elasticsearch-exporter
17
17
  showInstalling "The Elasticsearch Exporter (prom-operator ES metrics exports)"
18
- [ -z "$ELASTICSEARCH_EXPORTER_CHART_VERSION" ] && ELASTICSEARCH_EXPORTER_CHART_VERSION="7.2.1"
18
+ [ -z "$ELASTICSEARCH_EXPORTER_CHART_VERSION" ] && ELASTICSEARCH_EXPORTER_CHART_VERSION="7.4.0"
19
19
  OCI_CHART="oci://ghcr.io/prometheus-community/charts/prometheus-elasticsearch-exporter"
20
20
  helm upgrade $NS --install elasticsearch8-exporter $OCI_CHART \
21
21
  --values prom-operator/elasticsearch-exporter.yaml \
@@ -23,7 +23,7 @@ helm upgrade $NS --install elasticsearch8-exporter $OCI_CHART \
23
23
 
24
24
  # https://artifacthub.io/packages/helm/prometheus-community/prometheus-stackdriver-exporter
25
25
  showInstalling "The Stackdriver Exporter (prom-operator GKE metrics exports)"
26
- [ -z "$STACKDRIVER_EXPORTER_CHART_VERSION" ] && STACKDRIVER_EXPORTER_CHART_VERSION="4.12.2"
26
+ [ -z "$STACKDRIVER_EXPORTER_CHART_VERSION" ] && STACKDRIVER_EXPORTER_CHART_VERSION="4.13.0"
27
27
  OCI_CHART="oci://ghcr.io/prometheus-community/charts/prometheus-stackdriver-exporter"
28
28
  helm upgrade $NS --install stackdriver-exporter $OCI_CHART \
29
29
  --values prom-operator/stackdriver-exporter.yaml \
@@ -7,7 +7,7 @@ crds:
7
7
  enabled: true
8
8
  upgradeJob:
9
9
  enabled: true
10
- forceConflicts: false
10
+ forceConflicts: true
11
11
 
12
12
  kubeControllerManager:
13
13
  enabled: false
@@ -0,0 +1,28 @@
1
+ image:
2
+ tag:
3
+
4
+ modelInit:
5
+ sources:
6
+ triton: "gs://${PROJECT_ID}-triton-model-repository/models"
7
+ targetDir: "/models"
8
+
9
+ # container args may be tuned as necessary - see the repo README for more info
10
+ #
11
+ # https://bitbucket.org/leverege/triton-inference-server/src/development/
12
+
13
+ # example of how to add objects
14
+ #extraObjects:
15
+ # - apiVersion: traefik.io/v1alpha1
16
+ # kind: IngressRoute
17
+ # metadata:
18
+ # name: ${PROJECT_NAME}-extra-api-${HOST}-com
19
+ # namespace: default
20
+ # spec:
21
+ # entryPoints:
22
+ # - websecure
23
+ # routes:
24
+ # - match: Host(`${PROJECT_NAME}-test-api.${HOST}.com`)
25
+ # kind: Rule
26
+ # services:
27
+ # - name: triton-inference-server
28
+ # port: 8001
@@ -0,0 +1,45 @@
1
+ image:
2
+ tag:
3
+
4
+ config:
5
+ LOG_LEVEL: "WARNING"
6
+ GCP_PROJECT_ID: "${PROJECT_ID}" # needed for python secrets manager?
7
+ GCP_SECRET_MANAGER_NAMESPACE: "VMS_SERVER"
8
+ IMAGINE_API_HOST: "http://api-server:8181"
9
+ IMAGINE_API_PROJECT_ID: "set-in-values-local"
10
+ IMAGINE_API_SYSTEM_ID: "set-in-values-local"
11
+ IMAGINE_API_KEY: "set-in-values-local"
12
+ IMAGINE_API_AUDIENCE: "${PROJECT_ID}-aud"
13
+ IMAGINE_SERVICE_SYNC_ENABLED: "true"
14
+ IMAGINE_CAMERA_BLUEPRINT_ID: "set-in-values-local"
15
+ VMS_SERVER_HOST: "http://vms-server:8080"
16
+ VIDEO_PLAYBACK_SERVICE_VERSION: "20"
17
+ VIDEO_MANAGEMENT_SERVICE_VERSION: "2"
18
+ USE_SHARED_METRICS: "true"
19
+ VIDEO_SEGMENTS_UPLOAD_TIMEOUT: "60"
20
+ PLAYLIST_OPEN_ENDED_CACHE_ENABLED: "true"
21
+ SEGMENT_RECONCILER_ENABLED: "true"
22
+ SEGMENT_RECONCILER_INTERVAL_SECONDS: "300"
23
+ SEGMENT_RECONCILER_MIN_AGE_SECONDS: "300"
24
+ SEGMENT_RECONCILER_LOOKBACK_HOURS: "24"
25
+
26
+ # run on the preemptible node pool (base chart adds the target-env=preemptible
27
+ # nodeAffinity, toleration and safe-to-evict annotation)
28
+ service:
29
+ preemptible: true
30
+
31
+ serviceMonitor:
32
+ enabled: true
33
+ interval: "60s"
34
+ namespace: "default"
35
+
36
+ resources:
37
+ requests:
38
+ cpu: 500m
39
+ memory: 512Mi
40
+
41
+ autoscaling:
42
+ enabled: true
43
+ minReplicas: 2
44
+ maxReplicas: 16
45
+ averageCPU: 75
package/src/helmup.sh CHANGED
@@ -52,9 +52,6 @@ for arg in "$@"; do
52
52
  --mutate)
53
53
  MUTATE_DEPLOYMENT=1
54
54
  ;;
55
- --conflicts)
56
- export FORCE_CONFLICTS=1
57
- ;;
58
55
  --help|-h)
59
56
  giveHelp && exit 0
60
57
  ;;
@@ -100,6 +97,8 @@ gcloud auth application-default print-access-token | helm registry login \
100
97
  refresh-npm-token
101
98
  exitOnError $? "refresh-npm-token failed - the rc files were not refreshed"
102
99
 
100
+ HELM_EMOJI=$( [ "$INVOKED_AS" == "throwup" ] && echo "🤮" || echo "⎈" )
101
+
103
102
  # removed geotile-server from standard platform 3/20/22 - low usage
104
103
  PLATFORM=(
105
104
  authz-server
@@ -107,6 +106,7 @@ PLATFORM=(
107
106
  db-curator
108
107
  emailer
109
108
  imagine
109
+ llm-prompt-server
110
110
  message-processor
111
111
  messenger
112
112
  resource-server
@@ -129,6 +129,7 @@ AUXILIARY=(
129
129
  reason
130
130
  transponder-dh
131
131
  vin-decoder-server
132
+ vms-server
132
133
  )
133
134
 
134
135
  # There's a chicken / egg situation that arises with new clusters in that
@@ -629,7 +630,7 @@ HELMUP_BANNER
629
630
  function doHelmup() {
630
631
  local culprit="$(gcloud config get account)"
631
632
  local deployT="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
632
- local doing="Helming => `color g \"$*\"`"
633
+ local doing="Helming => $HELM_EMOJI `color g \"$*\"` $HELM_EMOJI"
633
634
  local from=
634
635
  [ ! -z "$FROM_VERSIONS" ] && from="`color y \"<= from Versions.json\"`"
635
636
  printf "$doing $from\n"
@@ -0,0 +1,100 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { errorExit } from '../../Utils.mjs'
4
+
5
+ const SHELLS = [ 'bash', 'zsh', 'fish' ]
6
+
7
+ const BASH = `\
8
+ _k8s_completions() {
9
+ local cur prev words
10
+ COMPREPLY=()
11
+ cur="\${COMP_WORDS[COMP_CWORD]}"
12
+ words=("\${COMP_WORDS[@]}")
13
+
14
+ case "\${words[1]}" in
15
+ fwd)
16
+ COMPREPLY=($(compgen -W "elastic redis valkey prom alert grafana" -- "$cur"))
17
+ return ;;
18
+ exe)
19
+ COMPREPLY=($(compgen -W "cnpg es8 grafana grafold prom-operator psql redis valkey" -- "$cur"))
20
+ return ;;
21
+ scale)
22
+ [ "\${#words[@]}" -eq 3 ] && COMPREPLY=($(compgen -W "up dn" -- "$cur"))
23
+ return ;;
24
+ log)
25
+ COMPREPLY=($(compgen -W "--namespace --container --tail --tee --no-pretty" -- "$cur"))
26
+ return ;;
27
+ completion)
28
+ COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
29
+ return ;;
30
+ *)
31
+ COMPREPLY=($(compgen -W "fwd exe scale roll log completion" -- "$cur"))
32
+ ;;
33
+ esac
34
+ }
35
+ complete -F _k8s_completions k8s
36
+ `
37
+
38
+ const ZSH = `\
39
+ #compdef k8s
40
+
41
+ _k8s() {
42
+ local state
43
+ _arguments '1: :->cmd' '*: :->args'
44
+
45
+ case $state in
46
+ cmd)
47
+ local -a commands=(
48
+ 'fwd:Port-forward to a cluster endpoint'
49
+ 'exe:Exec into a pod interactively'
50
+ 'scale:Scale a deployment up or down'
51
+ 'roll:Rolling restart of deployments'
52
+ 'log:Stream logs with pino-pretty'
53
+ 'completion:Emit shell completion script'
54
+ )
55
+ _describe 'command' commands ;;
56
+ args)
57
+ case $words[2] in
58
+ fwd) _values target elastic redis valkey prom alert grafana ;;
59
+ exe) _values target cnpg es8 grafana grafold prom-operator psql redis valkey ;;
60
+ scale) [ $CURRENT -eq 3 ] && _values direction up dn ;;
61
+ completion) _values shell bash zsh fish ;;
62
+ esac ;;
63
+ esac
64
+ }
65
+
66
+ _k8s
67
+ `
68
+
69
+ const FISH = `\
70
+ set -l cmds fwd exe scale roll log completion
71
+ complete -c k8s -f
72
+ complete -c k8s -n "not __fish_seen_subcommand_from $cmds" -a fwd -d 'Port-forward to a cluster endpoint'
73
+ complete -c k8s -n "not __fish_seen_subcommand_from $cmds" -a exe -d 'Exec into a pod interactively'
74
+ complete -c k8s -n "not __fish_seen_subcommand_from $cmds" -a scale -d 'Scale a deployment up or down'
75
+ complete -c k8s -n "not __fish_seen_subcommand_from $cmds" -a roll -d 'Rolling restart of deployments'
76
+ complete -c k8s -n "not __fish_seen_subcommand_from $cmds" -a log -d 'Stream logs with pino-pretty'
77
+ complete -c k8s -n "not __fish_seen_subcommand_from $cmds" -a completion -d 'Emit shell completion script'
78
+
79
+ complete -c k8s -n "__fish_seen_subcommand_from fwd" -a "elastic redis valkey prom alert grafana"
80
+ complete -c k8s -n "__fish_seen_subcommand_from exe" -a "cnpg es8 grafana grafold prom-operator psql redis valkey"
81
+ complete -c k8s -n "__fish_seen_subcommand_from scale; and not __fish_seen_subcommand_from up dn" -a "up dn"
82
+ complete -c k8s -n "__fish_seen_subcommand_from completion" -a "bash zsh fish"
83
+ `
84
+
85
+ export const completionCommand = new Command( 'completion' )
86
+ .description( 'Emit a shell completion script' )
87
+ .argument( '<shell>', `shell: ${SHELLS.join( ', ' )}` )
88
+ .addHelpText( 'after', `
89
+ Examples:
90
+ eval "$(k8s completion bash)" # add to ~/.bashrc
91
+ eval "$(k8s completion zsh)" # add to ~/.zshrc
92
+ k8s completion fish | source # fish interactive session
93
+ ` )
94
+ .action( ( shell ) => {
95
+ const scripts = { bash: BASH, zsh: ZSH, fish: FISH }
96
+ if ( !scripts[shell] ) {
97
+ errorExit( `Unknown shell: ${shell}\nValid: ${SHELLS.join( ', ' )}` )
98
+ }
99
+ process.stdout.write( scripts[shell] )
100
+ } )
@@ -0,0 +1,164 @@
1
+ import { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { $ } from 'zx'
4
+
5
+ import { log, errorExit, sleep, warning, proceed, parseJsonFile } from '../../Utils.mjs'
6
+ import { overwhelmGate } from '../lib/gate.mjs'
7
+
8
+ $.verbose = false
9
+
10
+ // ── Config ────────────────────────────────────────────────────────────────
11
+
12
+ const loadConfig = async ( configPath ) => {
13
+ const config = await parseJsonFile( configPath )
14
+ if ( !config ) errorExit( `Config file not found: ${configPath}` )
15
+
16
+ const { cluster, deploymentPhases, nodePools } = config
17
+ if ( !cluster?.project || !cluster?.name || !cluster?.region ) {
18
+ errorExit( 'Config must include cluster.project, cluster.name, and cluster.region' )
19
+ }
20
+ if ( !deploymentPhases || !nodePools ) {
21
+ errorExit( 'Config must include deploymentPhases and nodePools' )
22
+ }
23
+ return config
24
+ }
25
+
26
+ // ── Display ───────────────────────────────────────────────────────────────
27
+
28
+ const banner = ( text ) => {
29
+ const bar = '─'.repeat( Math.max( 0, 56 - text.length ) )
30
+ log( chalk.cyan.bold( `\n── ${text} ${bar}` ) )
31
+ }
32
+
33
+ // ── kubectl ───────────────────────────────────────────────────────────────
34
+
35
+ const scaleDeployments = async ( names, replicas, namespace ) =>
36
+ $`kubectl scale deployment ${names} --replicas=${replicas} -n ${namespace}`
37
+
38
+ const scaleStatefulSets = async ( names, replicas, namespace ) =>
39
+ $`kubectl scale statefulset ${names} --replicas=${replicas} -n ${namespace}`
40
+
41
+ const waitForPodsGone = async ( namespace, timeoutSecs = 300 ) => {
42
+ log( `\nWaiting for pods to terminate (up to ${timeoutSecs}s)...` )
43
+ try {
44
+ await $`kubectl wait pod --all -n ${namespace} --for=delete --timeout=${timeoutSecs}s`
45
+ } catch {
46
+ warning( `Some pods still terminating after ${timeoutSecs}s — proceeding.` )
47
+ }
48
+ }
49
+
50
+ const waitForNodesReady = async ( timeoutSecs = 600 ) => {
51
+ log( `\nWaiting for nodes to become Ready (up to ${timeoutSecs}s)...` )
52
+ await $`kubectl wait node --all --for=condition=Ready --timeout=${timeoutSecs}s`
53
+ }
54
+
55
+ // ── gcloud ────────────────────────────────────────────────────────────────
56
+
57
+ const updateAutoscalerMin = async ( pool, cluster, min ) => {
58
+ log( ` ${pool.name}: autoscaler min → ${min}` )
59
+ const { name, project, region } = cluster
60
+ await $`gcloud container node-pools update ${pool.name} --cluster=${name} --project=${project} --region=${region} --enable-autoscaling --min-nodes=${min} --max-nodes=${pool.autoscaler.max} --quiet`
61
+ }
62
+
63
+ const resizePool = async ( pool, cluster, size ) => {
64
+ log( ` ${pool.name} → ${size} node(s)` )
65
+ const { name, project, region } = cluster
66
+ await $`gcloud container clusters resize ${name} --node-pool=${pool.name} --num-nodes=${size} --project=${project} --region=${region} --quiet`
67
+ }
68
+
69
+ // ── Hibernate ─────────────────────────────────────────────────────────────
70
+
71
+ const runHibernate = async ( configPath ) => {
72
+ const config = await loadConfig( configPath )
73
+ const {
74
+ cluster,
75
+ namespace = 'default',
76
+ phasePauseMs = 10_000,
77
+ deploymentPhases,
78
+ statefulsetPhases = [],
79
+ nodePools,
80
+ } = config
81
+
82
+ await overwhelmGate()
83
+ await proceed( `Hibernate ${chalk.yellow.bold( cluster.name )}? All workloads scale to 0 and node pools drain.` )
84
+
85
+ for ( const group of deploymentPhases ) {
86
+ banner( `Deployments: ${group.name}` )
87
+ await scaleDeployments( group.members, 0, namespace )
88
+ await sleep( group.pauseMs ?? phasePauseMs )
89
+ }
90
+
91
+ if ( statefulsetPhases.length ) {
92
+ for ( const group of statefulsetPhases ) {
93
+ banner( `StatefulSets: ${group.name}` )
94
+ await scaleStatefulSets( group.members, 0, namespace )
95
+ await sleep( group.pauseMs ?? phasePauseMs )
96
+ }
97
+ } else {
98
+ // no ordered phases defined yet — scale all StatefulSets together
99
+ banner( 'StatefulSets' )
100
+ await $`kubectl scale statefulset --all -n ${namespace} --replicas=0`
101
+ }
102
+
103
+ await waitForPodsGone( namespace )
104
+
105
+ const poolsWithAutoscaler = nodePools.filter( p => p.autoscaler )
106
+ if ( poolsWithAutoscaler.length ) {
107
+ banner( 'Clearing autoscaler minimums' )
108
+ for ( const pool of poolsWithAutoscaler ) {
109
+ await updateAutoscalerMin( pool, cluster, 0 )
110
+ }
111
+ }
112
+
113
+ banner( 'Draining node pools' )
114
+ for ( const pool of nodePools ) {
115
+ await resizePool( pool, cluster, 0 )
116
+ }
117
+
118
+ log( chalk.green( '\nCluster hibernated. Control plane (~$0.10/hr) continues running.' ) )
119
+ log( `To wake: ${chalk.cyan( `k8s wake ${configPath}` )}` )
120
+ }
121
+
122
+ // ── Wake ──────────────────────────────────────────────────────────────────
123
+
124
+ const runWake = async ( configPath ) => {
125
+ const config = await loadConfig( configPath )
126
+ const { cluster, nodePools } = config
127
+
128
+ await overwhelmGate()
129
+
130
+ // Restore autoscaler minimums before resizing so the autoscaler
131
+ // doesn't immediately scale back down the nodes we're bringing up.
132
+ const poolsWithAutoscaler = nodePools.filter( p => p.autoscaler )
133
+ if ( poolsWithAutoscaler.length ) {
134
+ banner( 'Restoring autoscaler minimums' )
135
+ for ( const pool of poolsWithAutoscaler ) {
136
+ await updateAutoscalerMin( pool, cluster, pool.autoscaler.min )
137
+ }
138
+ }
139
+
140
+ banner( 'Restoring node pools' )
141
+ for ( const pool of nodePools ) {
142
+ if ( pool.restoreSize > 0 ) {
143
+ await resizePool( pool, cluster, pool.restoreSize )
144
+ } else {
145
+ log( ` ${pool.name} → skipped (autoscaler brings up on demand)` )
146
+ }
147
+ }
148
+
149
+ await waitForNodesReady()
150
+
151
+ log( chalk.green( '\nNodes ready. Run helmup to restore all services.' ) )
152
+ }
153
+
154
+ // ── Commands ──────────────────────────────────────────────────────────────
155
+
156
+ export const hibernateCommand = new Command( 'hibernate' )
157
+ .description( 'Scale all workloads to zero and drain all node pools' )
158
+ .argument( '[config]', 'path to cluster-config.json', './cluster-config.json' )
159
+ .action( async ( config ) => runHibernate( config ).catch( errorExit ) )
160
+
161
+ export const wakeCommand = new Command( 'wake' )
162
+ .description( 'Restore node pools from hibernation (then run helmup)' )
163
+ .argument( '[config]', 'path to cluster-config.json', './cluster-config.json' )
164
+ .action( async ( config ) => runWake( config ).catch( errorExit ) )