@leverege/build-tools 2.94.0 → 2.95.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.
Files changed (87) hide show
  1. package/.vscode/launch.json +14 -0
  2. package/package.json +5 -2
  3. package/src/Docker.mjs +3 -2
  4. package/src/Utils.mjs +53 -6
  5. package/src/artifacts/Utils.mjs +1 -1
  6. package/src/bash-funcs +1 -1
  7. package/src/git-tar.sh +1 -1
  8. package/src/helm-charts/emailer/values-local.yaml +1 -8
  9. package/src/helm-charts/prom-operator/helmup.plugin +1 -1
  10. package/src/helm-charts/traefik/helmup.plugin +1 -1
  11. package/src/helm-charts/velero/README.md +135 -0
  12. package/src/helm-charts/velero/helmup.bootstrap +1 -0
  13. package/src/helm-charts/velero/helmup.plugin +29 -2
  14. package/src/helm-charts/velero/values-local.yaml +1 -1
  15. package/src/helmup.sh +49 -41
  16. package/src/service-man/ServiceMan.mjs +75 -0
  17. package/src/service-man/config/CommandLine.mjs +199 -0
  18. package/src/service-man/config/Config.mjs +203 -0
  19. package/src/service-man/config/VersionCheck.mjs +79 -0
  20. package/src/service-man/config/iam-roles/leveregeBigQueryReader.json +10 -0
  21. package/src/service-man/config/iam-roles/leveregeBigQueryWriter.json +13 -0
  22. package/src/service-man/config/iam-roles/leveregeCloudFunctionsManager.json +12 -0
  23. package/src/service-man/config/iam-roles/leveregeFirebaseHosting.json +9 -0
  24. package/src/service-man/config/iam-roles/leveregeFirebaseMessaging.json +13 -0
  25. package/src/service-man/config/iam-roles/leveregeFirebaseUpdates.json +10 -0
  26. package/src/service-man/config/iam-roles/leveregeLogsViewer.json +8 -0
  27. package/src/service-man/config/iam-roles/leveregePubSub.json +13 -0
  28. package/src/service-man/config/iam-roles/leveregeSecretsViewer.json +9 -0
  29. package/src/service-man/config/iam-roles/leveregeStorageObjectAdmin.json +21 -0
  30. package/src/service-man/config/iam-roles/leveregeStorageObjectReader.json +13 -0
  31. package/src/service-man/config/iam-roles/leveregeSvcAcctActor.json +8 -0
  32. package/src/service-man/config/iam-roles/leveregeSvcAcctTokenCreator.json +8 -0
  33. package/src/service-man/config/index.mjs +7 -0
  34. package/src/service-man/config/secrets/authz-jwt.json +6 -0
  35. package/src/service-man/config/secrets/authz-postgres.json +7 -0
  36. package/src/service-man/config/secrets/db-timescale-dense.json +8 -0
  37. package/src/service-man/config/secrets/jwt-options.json +7 -0
  38. package/src/service-man/config/secrets/mail-gun-api-key.json +6 -0
  39. package/src/service-man/config/secrets/npmrc.json +6 -0
  40. package/src/service-man/config/secrets/oidc-client-secret.json +6 -0
  41. package/src/service-man/config/secrets/postgresql-password.json +8 -0
  42. package/src/service-man/config/secrets/session-secret.json +6 -0
  43. package/src/service-man/config/secrets/twilio.json +8 -0
  44. package/src/service-man/config/service-accounts/api-server.json +12 -0
  45. package/src/service-man/config/service-accounts/authz-server.json +5 -0
  46. package/src/service-man/config/service-accounts/db-curator.json +8 -0
  47. package/src/service-man/config/service-accounts/emailer.json +7 -0
  48. package/src/service-man/config/service-accounts/fota-server.json +6 -0
  49. package/src/service-man/config/service-accounts/geotile-server.json +8 -0
  50. package/src/service-man/config/service-accounts/imagine.json +6 -0
  51. package/src/service-man/config/service-accounts/message-processor.json +6 -0
  52. package/src/service-man/config/service-accounts/messenger.json +7 -0
  53. package/src/service-man/config/service-accounts/pubsub-pulse.json +6 -0
  54. package/src/service-man/config/service-accounts/push-notifier.json +8 -0
  55. package/src/service-man/config/service-accounts/reason.json +5 -0
  56. package/src/service-man/config/service-accounts/resource-server.json +9 -0
  57. package/src/service-man/config/service-accounts/rule-engine.json +7 -0
  58. package/src/service-man/config/service-accounts/scheduler.json +6 -0
  59. package/src/service-man/config/service-accounts/transponder.json +15 -0
  60. package/src/service-man/gcloud-k8s/RolesManager.mjs +41 -0
  61. package/src/service-man/gcloud-k8s/SecretsManager.mjs +182 -0
  62. package/src/service-man/gcloud-k8s/ServiceAccountManager.mjs +246 -0
  63. package/src/service-man/gcloud-k8s/VersionManager.mjs +114 -0
  64. package/src/service-man/gcloud-k8s/index.mjs +11 -0
  65. package/src/service-man/service-man.mjs +18 -0
  66. package/src/service-man/utils/index.mjs +83 -0
  67. package/leverege-build-tools-2.21.13.tgz +0 -0
  68. package/lib/server/build-tools.js +0 -138
  69. package/lib/server/firebaseDeploy.js +0 -195
  70. package/lib/server/firebaseServe.js +0 -116
  71. package/lib/server/getfbcfg.js +0 -25
  72. package/lib/server/getjson.js +0 -70
  73. package/lib/server/overwhelm.js +0 -447
  74. package/lib/server/push-my-chart.js +0 -18
  75. package/lib/server/refresh-npm-token.js +0 -135
  76. package/lib/server/tag-release.js +0 -681
  77. package/lib/server/unleash.js +0 -50
  78. package/lib/web/build-tools.js +0 -138
  79. package/lib/web/firebaseDeploy.js +0 -195
  80. package/lib/web/firebaseServe.js +0 -116
  81. package/lib/web/getfbcfg.js +0 -25
  82. package/lib/web/getjson.js +0 -70
  83. package/lib/web/overwhelm.js +0 -447
  84. package/lib/web/push-my-chart.js +0 -18
  85. package/lib/web/refresh-npm-token.js +0 -135
  86. package/lib/web/tag-release.js +0 -681
  87. package/lib/web/unleash.js +0 -50
@@ -0,0 +1,7 @@
1
+ {
2
+ "type": "random",
3
+ "content": {
4
+ "JWT_SECRET": 64,
5
+ "JWT_SERVICE_SECRET": 64
6
+ }
7
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "managed",
3
+ "content": {
4
+ "key": "leverege-registry:MAIL_GUN_API_KEY"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "managed",
3
+ "content": {
4
+ "key": "leverege-registry:DEPRECATED_NPMRC"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "random",
3
+ "content": {
4
+ "key": 64
5
+ }
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "type": "random",
3
+ "immutable": true,
4
+ "content": {
5
+ "postgresql-password": 48,
6
+ "postgresql-replication-password": 48
7
+ }
8
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "random",
3
+ "content": {
4
+ "key": 64
5
+ }
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "type": "managed",
3
+ "content": {
4
+ "twilio-auth-token": "leverege-registry:TWILIO_AUTH_TOKEN",
5
+ "twilio-phone": "leverege-registry:TWILIO_PHONE",
6
+ "twilio-sid": "leverege-registry:TWILIO_SID"
7
+ }
8
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeBigQueryReader",
4
+ "leveregeCloudFunctionsManager",
5
+ "leveregeFirebaseHosting",
6
+ "leveregeFirebaseUpdates",
7
+ "leveregeLogsViewer",
8
+ "leveregePubSub",
9
+ "leveregeSvcAcctActor",
10
+ "leveregeSvcAcctTokenCreator"
11
+ ]
12
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeSecretsViewer"
4
+ ]
5
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeBigQueryReader",
4
+ "leveregeBigQueryWriter",
5
+ "leveregeFirebaseUpdates",
6
+ "leveregeSecretsViewer"
7
+ ]
8
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeFirebaseUpdates",
4
+ "leveregePubSub",
5
+ "leveregeSecretsViewer"
6
+ ]
7
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "roles" : [
3
+ "leveregePubSub",
4
+ "leveregeSecretsViewer"
5
+ ]
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeFirebaseUpdates",
4
+ "leveregeLogsViewer",
5
+ "leveregePubSub",
6
+ "leveregeStorageObjectAdmin"
7
+ ]
8
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeFirebaseUpdates",
4
+ "leveregePubSub"
5
+ ]
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeFirebaseUpdates",
4
+ "leveregePubSub"
5
+ ]
6
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeFirebaseUpdates",
4
+ "leveregePubSub",
5
+ "leveregeSecretsViewer"
6
+ ]
7
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "roles" : [
3
+ "leveregePubSub",
4
+ "leveregeSecretsViewer"
5
+ ]
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeFirebaseMessaging",
4
+ "leveregeFirebaseUpdates",
5
+ "leveregePubSub",
6
+ "leveregeSecretsViewer"
7
+ ]
8
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "roles" : [
3
+ "leveregePubSub"
4
+ ]
5
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeBigQueryWriter",
4
+ "leveregePubSub",
5
+ "leveregeSecretsViewer",
6
+ "leveregeStorageObjectAdmin",
7
+ "leveregeSvcAcctTokenCreator"
8
+ ]
9
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeFirebaseUpdates",
4
+ "leveregePubSub",
5
+ "leveregeSecretsViewer"
6
+ ]
7
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "roles" : [
3
+ "leveregePubSub",
4
+ "leveregeSecretsViewer"
5
+ ]
6
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "roles" : [
3
+ "leveregeBigQueryReader",
4
+ "leveregeBigQueryWriter",
5
+ "leveregeFirebaseUpdates",
6
+ "leveregePubSub",
7
+ "leveregeSecretsViewer"
8
+ ],
9
+ "members" : [
10
+ "transponder-bq",
11
+ "transponder-dh",
12
+ "transponder-tsdb",
13
+ "transponder-rt"
14
+ ]
15
+ }
@@ -0,0 +1,41 @@
1
+ import chalk from 'chalk'
2
+
3
+ import { capitalize, log, shellCmd } from '../../Utils.mjs'
4
+
5
+ export default class RolesManager {
6
+ constructor( svcConfig ) {
7
+ this.config = svcConfig
8
+ }
9
+
10
+ // IAM_VERB is create | update
11
+ // verify => gcloud --project $PROJ iam roles describe $ROLE &>/dev/null
12
+ // apply => gcloud --project $PROJ iam roles $IAM_VERB $ROLE --file $JSON
13
+ async start() {
14
+ const svcCfg = this.config
15
+ const k8sProject = svcCfg.k8sProject
16
+ const roleNames = svcCfg.getRoleNames()
17
+
18
+ log( chalk.green.bold( '\nIAM Roles Management\n' ) )
19
+ for ( let i = 0; i < roleNames.length; i++ ) {
20
+ const roleName = roleNames[i]
21
+ const roleFile = `${svcCfg.getRolesDir()}/${roleName}.json`
22
+ let iamVerb = 'describe' // if it describes - it exists
23
+ try {
24
+ /* eslint-disable-next-line no-await-in-loop */
25
+ await shellCmd( `gcloud iam roles ${iamVerb} ${roleName} --project ${k8sProject}` )
26
+ iamVerb = 'update' // no err then the role exists - do an update
27
+ } catch ( err ) {
28
+ iamVerb = 'create' // failed call so role does not exist - create it
29
+ }
30
+
31
+ log( ` ${capitalize( iamVerb )} IAM Role => ${chalk.cyan( roleName )}` )
32
+ try {
33
+ /* eslint-disable-next-line no-await-in-loop */
34
+ await shellCmd( `gcloud iam roles ${iamVerb} ${roleName} --project ${k8sProject} --file ${roleFile} --quiet` )
35
+ } catch ( err ) {
36
+ log( chalk.red( `***ERROR: ${err.shortMessage}\n` ), chalk.yellow( `\n${err.stderr}` ) )
37
+ process.exit( err.exitCode )
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,182 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+
4
+ import chalk from 'chalk'
5
+ import { $ } from 'execa' // TODO: I think $ works for pipes - shellCmd bombs on pipes - fix this
6
+ import inquirer from 'inquirer'
7
+ import tmp from 'tmp'
8
+
9
+ import { debug, errorExit, getGcpSecret, log, shellCmd } from '../../Utils.mjs'
10
+
11
+ export default class SecretsManager {
12
+ constructor( svcConfig ) {
13
+ this.config = svcConfig
14
+ }
15
+
16
+ async generatePassword( type, descr ) {
17
+ if ( type === 'random' ) {
18
+ return crypto.randomBytes( descr ).toString( 'hex' ).slice( 0, descr )
19
+ }
20
+
21
+ if ( type !== 'managed' ) {
22
+ throw new Error( `Unknown secret type = [${type}]` )
23
+ }
24
+
25
+ const projectSecretPair = descr.split( ':' )
26
+ if ( projectSecretPair.length !== 2 ) {
27
+ throw new Error( `Expecting [project:secretName] got [${descr}]` )
28
+ }
29
+
30
+ const shellOut = await shellCmd(
31
+ `gcloud secrets versions access latest --project ${projectSecretPair[0]} --secret ${projectSecretPair[1]}`
32
+ )
33
+ debug( { shellOut }, '<==SecretsManager.generatePassword (fetched managed)' )
34
+ return shellOut
35
+ }
36
+
37
+ async generateLiterals( secretDef ) {
38
+ const { namespace = 'default', type, content } = secretDef
39
+ const secretKeys = Object.keys( content )
40
+
41
+ const literals = []
42
+ literals.push( `--namespace=${namespace}` )
43
+
44
+ for ( let k = 0; k < secretKeys.length; k++ ) {
45
+ const sec = secretKeys[k]
46
+ const passwd = await this.generatePassword( type, content[sec] ) // eslint-disable-line no-await-in-loop
47
+ literals.push( `--from-literal=${sec}=${passwd}` )
48
+ }
49
+
50
+ return literals
51
+ }
52
+
53
+ // kubectl create secret generic ${secretName}
54
+ // --from-literal=${key}="${random generated password of ${size}}
55
+ // ...
56
+ //
57
+ // based on a JSON file named jwt-options.json containing
58
+ //
59
+ // {
60
+ // "namespace": "ns-name",
61
+ // "type": "random",
62
+ // "content": {
63
+ // "JWT_SECRET": 64,
64
+ // "JWT_AUDIENCE": 24,
65
+ // "JWT_SERVICE_SECRET": 64
66
+ // }
67
+ // }
68
+ //
69
+ // results in a secret named jwt-options with 2-64 character secrets and a
70
+ // 24 character audience.
71
+ //
72
+ async start() {
73
+ const secrets = this.config.secrets
74
+ const secretsNames = this.config.getSecretsNames()
75
+
76
+ log( chalk.green.bold( '\n\nSecrets Management\n' ) )
77
+
78
+ let overwrite = this.config.overwrite // gets set with --new-k8s
79
+ if ( !overwrite ) {
80
+ const overwriteMessage = `It is ok to ${chalk.red.bold( 'overwrite existing' )} Secrets?`
81
+ const existing = await inquirer.prompt( [
82
+ {
83
+ type : 'confirm',
84
+ name : 'overwrite',
85
+ message : overwriteMessage,
86
+ default : false,
87
+ }
88
+ ] )
89
+ overwrite = existing.overwrite
90
+ }
91
+
92
+ for ( let i = 0; i < secretsNames.length; i++ ) {
93
+ const secretName = secretsNames[i]
94
+ const secret = secrets[secretName]
95
+ const namespace = secrets[secretName].namespace || 'default'
96
+
97
+ try {
98
+ /* eslint-disable-next-line no-await-in-loop */
99
+ await shellCmd( `kubectl describe secret --namespace ${namespace} ${secretName}` )
100
+ if ( !overwrite || secret.immutable ) {
101
+ const reason = secret.immutable ? chalk.red.bold( 'immutable' ) : 'existing'
102
+ log( ` Skip ${reason} secret => ${chalk.yellow( secretName )}` )
103
+ continue
104
+ }
105
+
106
+ log( ' Recreating existing Secret =>', chalk.yellow( secretName ) )
107
+ await shellCmd( // eslint-disable-line no-await-in-loop
108
+ `kubectl delete secret --namespace ${namespace} ${secretName}`, { stderr : 'inherit' }
109
+ )
110
+ } catch ( err ) {
111
+ log( ` Creating new secret => ${chalk.green( secretName )}` )
112
+ }
113
+
114
+ try {
115
+ const literals = await this.generateLiterals( secret ) // eslint-disable-line no-await-in-loop
116
+
117
+ if ( secret.destType === 'gcloudSecret' ) {
118
+ // TODO: the $ bugs me because it's easy to miss, but shellCmd doesn't handle the pipe | properly - fix it
119
+ await $`printf "${literals}" | gcloud secrets create ${secretName}` // eslint-disable-line no-await-in-loop
120
+ } else {
121
+ await shellCmd( // eslint-disable-line no-await-in-loop
122
+ `kubectl create secret --namespace ${namespace} generic ${secretName} ${literals.join( ' ' )}`
123
+ )
124
+ }
125
+ } catch ( err ) {
126
+ log( chalk.red( `\nSecret creation failed for ${secretName}\n` ) )
127
+ }
128
+ }
129
+ }
130
+
131
+ async synchronize() {
132
+ const { syncSecrets } = this.config.CLI.args
133
+ const k8sProject = this.config.k8sProject
134
+
135
+ if ( !syncSecrets.match( /\S+=\S+/ ) ) {
136
+ errorExit( ` Invalid --sync-secrets syntax [${syncSecrets}]` )
137
+ }
138
+
139
+ // validate something=something in the syncSecrets value
140
+ const [ mgrSecretName, k8sSecretName ] = syncSecrets.split( '=' )
141
+
142
+ debug( {
143
+ config : this.config,
144
+ syncSecrets,
145
+ mgrSecretName,
146
+ k8sSecretName
147
+ }, chalk.green.bold( '\n\nSecrets Synchronizer\n' ) )
148
+
149
+ let k8sSecret
150
+ try {
151
+ const b64String = await shellCmd( `kubectl get secret ${k8sSecretName} -o jsonpath="{.data.password}"` )
152
+ k8sSecret = Buffer.from( b64String, 'base64' ).toString()
153
+ } catch ( ex ) {
154
+ errorExit( ` K8S secret fetch => ${ex.stderr}` )
155
+ }
156
+
157
+ let mgrSecret
158
+ try {
159
+ mgrSecret = await getGcpSecret( this.config.k8sProject, mgrSecretName )
160
+ } catch ( ex ) {
161
+ debug( { mgrSecretName }, ex )
162
+ }
163
+
164
+ if ( !mgrSecret ) {
165
+ const tmpfile = tmp.fileSync()
166
+ try {
167
+ log( chalk.green.bold( `Creating ${mgrSecretName} from k8s secret ${k8sSecretName}` ) )
168
+ await fs.writeFileSync( tmpfile.name, k8sSecret, { encoding : 'utf-8' } ) // eslint-disable-line security/detect-non-literal-fs-filename
169
+
170
+ const createCmd =
171
+ `gcloud secrets create ${mgrSecretName} --project=${k8sProject} --labels=env-type=string --data-file=${tmpfile.name}`
172
+ await shellCmd( createCmd )
173
+ } catch ( ex ) {
174
+ errorExit( { ex }, ' Secret creation failed' )
175
+ } finally {
176
+ tmpfile.removeCallback()
177
+ }
178
+ } else if ( mgrSecret !== k8sSecret ) {
179
+ errorExit( ` Secrets ${mgrSecretName} and ${k8sSecretName} exist, but do not match` )
180
+ }
181
+ }
182
+ }
@@ -0,0 +1,246 @@
1
+ import chalk from 'chalk'
2
+ import { execa } from 'execa'
3
+ import inquirer from 'inquirer'
4
+
5
+ import { log, shellCmd, sleep } from '../../Utils.mjs'
6
+
7
+ // The sleep delays in this code gives the IAM operations a chance to
8
+ // complete before moving on to the next IAM command. This is not a perfect
9
+ // solution but will hopefully avoid any concurrency exceptions since the
10
+ // IAM commands take some time to complete, even after an await.
11
+ //
12
+ const BACKOFF_DELAYS = [ 100, 100, 200, 250, 500, 750, 1000, 2000, 4000 ]
13
+ const MAX_RETRIES = BACKOFF_DELAYS.length
14
+
15
+ const delayWithBackoff = async ( retryCount = 0 ) => {
16
+ if ( retryCount >= MAX_RETRIES ) {
17
+ throw new Error( '***ERROR: exceeded max retries - fix the code!' )
18
+ }
19
+ const delayWithJitter = BACKOFF_DELAYS[retryCount] + ( Math.floor( Math.random() * 100 ) - 50 ) // =/- 50ms jitter
20
+ await sleep( delayWithJitter )
21
+ }
22
+
23
+ /**
24
+ * gcloud iam service-accounts create ${serviceAcct}
25
+ * --project ${project}
26
+ * --display-name "SvcMan SA => ${serviceAcct}"
27
+ * --description "SvcMan SA"
28
+ *
29
+ * gcloud projects add-iam-policy-binding ${projectId}
30
+ * --project ${projectId}
31
+ * --member "serviceAccount:${serviceAcct}@${serviceAcct}.iam.gserviceaccount.com"
32
+ * --role "projects/${projectId}/roles/${roleName}"
33
+ *
34
+ * gcloud iam service-accounts add-iam-policy-binding \
35
+ * --project ${projectId}
36
+ * --role roles/iam.workloadIdentityUser \
37
+ * --member "serviceAccount:${projectId}.svc.id.goog[default/${serviceAcct}]" \
38
+ * ${serviceAcct}@${projectId}.iam.gserviceaccount.com
39
+ */
40
+ export default class ServiceAccountManager {
41
+ constructor( svcConfig ) {
42
+ this.config = svcConfig
43
+ }
44
+
45
+ buildWorkloadMembers( saName ) {
46
+ const members = this.config.serviceAccounts[saName].members
47
+ return !members ? [ saName ] : members
48
+ }
49
+
50
+ getServiceAccountEmail( serviceName ) {
51
+ return `${serviceName}@${this.config.k8sProject}.iam.gserviceaccount.com`
52
+ }
53
+
54
+ // throws if the SA does not exist else returns true
55
+ async serviceAccountExists( serviceName ) {
56
+ const saEmail = this.getServiceAccountEmail( serviceName )
57
+ await shellCmd(
58
+ `gcloud iam service-accounts describe ${saEmail} --project ${this.config.k8sProject}`
59
+ )
60
+ }
61
+
62
+ // throws if the SA fails role binding else returns true - note that this
63
+ // is purely a gcloud operation, the connection to the k8s SA will occur
64
+ // when the service is deployed
65
+ async bindIamRole( serviceName, role, k8sProject ) {
66
+ const fullRoleName = `projects/${k8sProject}/roles/${role}`
67
+ const saEmail = this.getServiceAccountEmail( serviceName )
68
+
69
+ log( ' Binding IAM Role => ', chalk.cyan( role ) )
70
+
71
+ const command = `gcloud projects add-iam-policy-binding ${k8sProject} --project ${k8sProject}`
72
+ const options = `--member serviceAccount:${saEmail} --condition None --role ${fullRoleName}`
73
+
74
+ for ( let retryCount = 0; retryCount < MAX_RETRIES; retryCount++ ) {
75
+ try {
76
+ await shellCmd( `${command} ${options}` ) // eslint-disable-line no-await-in-loop
77
+ return true
78
+ } catch ( err ) {
79
+ log( ' IAM propagating => ', chalk.yellow( role ) )
80
+ }
81
+ await delayWithBackoff( retryCount ) // eslint-disable-line no-await-in-loop
82
+ }
83
+
84
+ throw new Error( ` Binding IAM role ${role} failed for SA ${serviceName}` )
85
+ }
86
+
87
+ // throws if any of the bindings fail else returns true
88
+ async bindIamRoles( serviceName, roles, k8sProject ) {
89
+ if ( !Array.isArray( roles ) ) {
90
+ throw new Error( 'bindIamRoles expects an Array of roles' )
91
+ }
92
+
93
+ for ( let r = 0; r < roles.length; r++ ) {
94
+ await this.bindIamRole( serviceName, roles[r], k8sProject ) // eslint-disable-line no-await-in-loop
95
+ }
96
+ }
97
+
98
+ // throws if the SA fails to bind else returns true
99
+ async bindWorkloadIdentity( opts ) {
100
+ const { serviceName, member, k8sProject, namespace = 'default' } = opts
101
+ const saEmail = this.getServiceAccountEmail( serviceName )
102
+
103
+ log( ` Binding Workload Identity => [${chalk.cyan( namespace )} / ${chalk.cyan( member )}]` )
104
+
105
+ const command = `gcloud iam service-accounts add-iam-policy-binding --project ${k8sProject}`
106
+ const options = `--member serviceAccount:${k8sProject}.svc.id.goog[${namespace}/${member}] --role roles/iam.workloadIdentityUser ${saEmail}`
107
+
108
+ for ( let retryCount = 0; retryCount < MAX_RETRIES; retryCount++ ) {
109
+ try {
110
+ await shellCmd( `${command} ${options}` ) // eslint-disable-line no-await-in-loop
111
+ return true
112
+ } catch ( err ) {
113
+ await delayWithBackoff( retryCount ) // eslint-disable-line no-await-in-loop
114
+ }
115
+ }
116
+
117
+ throw new Error( `Failed to bind workloadIdentity to ${serviceName}` )
118
+ }
119
+
120
+ // throws if the SA fails to create else returns true
121
+ async createServiceAccount( serviceName, k8sProject ) {
122
+ log( ' Creating Service Account => ', chalk.cyan( serviceName ) )
123
+ const command = `gcloud iam service-accounts create ${serviceName} --project ${k8sProject}`
124
+ const options = `--display-name \'SvcMan SA => ${serviceName}\' --description 'SvcMan SA'`
125
+ await shellCmd( `${command} ${options}` )
126
+ await this.waitForServiceAccount( serviceName, k8sProject )
127
+ }
128
+
129
+ // lingers for a period of timed retries waiting for the SA to stabilize
130
+ async waitForServiceAccount( serviceName, k8sProject ) {
131
+ const saEmail = this.getServiceAccountEmail( serviceName )
132
+
133
+ for ( let retryCount = 0; retryCount < MAX_RETRIES; retryCount++ ) {
134
+ try {
135
+ // eslint-disable-next-line no-await-in-loop
136
+ const shellOut = await shellCmd( `gcloud iam service-accounts describe ${saEmail} --project ${k8sProject}` )
137
+ if ( shellOut.match( `email: ${serviceName}` ) ) {
138
+ return true
139
+ }
140
+ } catch ( err ) {
141
+ log( ' SA propagating => ', chalk.yellow( serviceName ) )
142
+ await delayWithBackoff( retryCount ) // eslint-disable-line no-await-in-loop
143
+ }
144
+ }
145
+
146
+ throw new Error( `Service account ${serviceName} did not become available in time` )
147
+ }
148
+
149
+ // throws if the SA fails to delete else returns true
150
+ async deleteServiceAccount( serviceName, k8sProject ) {
151
+ const saEmail = this.getServiceAccountEmail( serviceName )
152
+ await shellCmd( `gcloud iam service-accounts delete ${saEmail} --project ${k8sProject} --quiet` )
153
+ }
154
+
155
+ // If the desired SA does not exist then inquire which roles it needs, build
156
+ // the SA, and bind workload identity to it.
157
+ async createBoundServiceAccount( serviceName = '' ) {
158
+ const svcCfg = this.config
159
+ const k8sProject = svcCfg.k8sProject
160
+
161
+ if ( !serviceName || serviceName.length <= 0 ) {
162
+ log( chalk.red.bold` ***Error: missing service account name` )
163
+ process.exit( 1 )
164
+ }
165
+
166
+ try {
167
+ await this.serviceAccountExists( serviceName )
168
+ log( `The gcloud SA ${serviceName} already exists - skipping recreation` )
169
+ process.exit( 0 )
170
+ } catch ( err ) {
171
+ if ( err.exitCode !== 1 ) {
172
+ log( chalk.red.bold( `\n ***${err.stderr}` ) )
173
+ process.exit( err.exitCode )
174
+ }
175
+ }
176
+
177
+ log( chalk.green( `\n service-man creating new Service Account => ${chalk.yellow.bold( serviceName )}\n` ) )
178
+ try {
179
+ await this.createServiceAccount( serviceName, k8sProject )
180
+ } catch ( err ) {
181
+ log( { err }, chalk.red.bold( `\n ***${err.stderr}` ) )
182
+ process.exit( err.exitCode )
183
+ }
184
+
185
+ const saRoles = await svcCfg.askForRoles( serviceName )
186
+ await this.bindIamRoles( serviceName, saRoles, k8sProject )
187
+
188
+ const namespace = svcCfg.CLI?.args?.namespace || svcCfg.serviceAccounts[serviceName]?.namespace || 'default'
189
+ await this.bindWorkloadIdentity( { serviceName, member : [ serviceName ], k8sProject, namespace } )
190
+ }
191
+
192
+ async start() {
193
+ const svcCfg = this.config
194
+ const k8sProject = svcCfg.k8sProject
195
+ const saNames = svcCfg.getServiceAccountNames()
196
+
197
+ log( chalk.green.bold( '\nService Account Management\n' ) )
198
+
199
+ let overwrite = svcCfg.overwrite // gets set with --new-k8s
200
+ if ( !overwrite ) {
201
+ const overwriteMessage = `It is ok to ${chalk.red.bold( 'overwrite existing' )} Service Accounts?`
202
+ const existing = await inquirer.prompt( [
203
+ {
204
+ type : 'confirm',
205
+ name : 'overwrite',
206
+ message : overwriteMessage,
207
+ default : false,
208
+ }
209
+ ] )
210
+ overwrite = existing.overwrite
211
+ }
212
+
213
+ for ( let i = 0; i < saNames.length; i++ ) {
214
+ const saName = saNames[i]
215
+ try {
216
+ await this.serviceAccountExists( saName ) // eslint-disable-line no-await-in-loop
217
+ if ( overwrite ) {
218
+ log( '\nRecreating existing SvcAct =>', chalk.yellow( saName ) )
219
+ await this.deleteServiceAccount( saName, k8sProject ) // eslint-disable-line no-await-in-loop
220
+ } else {
221
+ log( '\nSkip existing SvcAct =>', chalk.yellow( saName ) )
222
+ continue
223
+ }
224
+ } catch ( err ) {
225
+ log( '\nCreating new SvcAcct =>', chalk.green( saName ) )
226
+ }
227
+
228
+ await this.createServiceAccount( saName, k8sProject ) // eslint-disable-line no-await-in-loop
229
+
230
+ const saRoles = svcCfg.serviceAccounts[saName].roles
231
+ await this.bindIamRoles( saName, saRoles, k8sProject ) // eslint-disable-line no-await-in-loop
232
+
233
+ // lastly - bind the service to role workloadIdentity - TODO Promisfy
234
+ const members = this.buildWorkloadMembers( saName )
235
+ for ( let m = 0; m < members.length; m++ ) {
236
+ // eslint-disable-next-line no-await-in-loop
237
+ await this.bindWorkloadIdentity( {
238
+ serviceName : saName,
239
+ member : members[m],
240
+ k8sProject,
241
+ namespace : svcCfg.serviceAccounts[saName].namespace,
242
+ } )
243
+ }
244
+ }
245
+ }
246
+ }