@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.
- package/.vscode/launch.json +14 -0
- package/package.json +5 -2
- package/src/Docker.mjs +3 -2
- package/src/Utils.mjs +53 -6
- package/src/artifacts/Utils.mjs +1 -1
- package/src/bash-funcs +1 -1
- package/src/git-tar.sh +1 -1
- package/src/helm-charts/emailer/values-local.yaml +1 -8
- package/src/helm-charts/prom-operator/helmup.plugin +1 -1
- package/src/helm-charts/traefik/helmup.plugin +1 -1
- package/src/helm-charts/velero/README.md +135 -0
- package/src/helm-charts/velero/helmup.bootstrap +1 -0
- package/src/helm-charts/velero/helmup.plugin +29 -2
- package/src/helm-charts/velero/values-local.yaml +1 -1
- package/src/helmup.sh +49 -41
- package/src/service-man/ServiceMan.mjs +75 -0
- package/src/service-man/config/CommandLine.mjs +199 -0
- package/src/service-man/config/Config.mjs +203 -0
- package/src/service-man/config/VersionCheck.mjs +79 -0
- package/src/service-man/config/iam-roles/leveregeBigQueryReader.json +10 -0
- package/src/service-man/config/iam-roles/leveregeBigQueryWriter.json +13 -0
- package/src/service-man/config/iam-roles/leveregeCloudFunctionsManager.json +12 -0
- package/src/service-man/config/iam-roles/leveregeFirebaseHosting.json +9 -0
- package/src/service-man/config/iam-roles/leveregeFirebaseMessaging.json +13 -0
- package/src/service-man/config/iam-roles/leveregeFirebaseUpdates.json +10 -0
- package/src/service-man/config/iam-roles/leveregeLogsViewer.json +8 -0
- package/src/service-man/config/iam-roles/leveregePubSub.json +13 -0
- package/src/service-man/config/iam-roles/leveregeSecretsViewer.json +9 -0
- package/src/service-man/config/iam-roles/leveregeStorageObjectAdmin.json +21 -0
- package/src/service-man/config/iam-roles/leveregeStorageObjectReader.json +13 -0
- package/src/service-man/config/iam-roles/leveregeSvcAcctActor.json +8 -0
- package/src/service-man/config/iam-roles/leveregeSvcAcctTokenCreator.json +8 -0
- package/src/service-man/config/index.mjs +7 -0
- package/src/service-man/config/secrets/authz-jwt.json +6 -0
- package/src/service-man/config/secrets/authz-postgres.json +7 -0
- package/src/service-man/config/secrets/db-timescale-dense.json +8 -0
- package/src/service-man/config/secrets/jwt-options.json +7 -0
- package/src/service-man/config/secrets/mail-gun-api-key.json +6 -0
- package/src/service-man/config/secrets/npmrc.json +6 -0
- package/src/service-man/config/secrets/oidc-client-secret.json +6 -0
- package/src/service-man/config/secrets/postgresql-password.json +8 -0
- package/src/service-man/config/secrets/session-secret.json +6 -0
- package/src/service-man/config/secrets/twilio.json +8 -0
- package/src/service-man/config/service-accounts/api-server.json +12 -0
- package/src/service-man/config/service-accounts/authz-server.json +5 -0
- package/src/service-man/config/service-accounts/db-curator.json +8 -0
- package/src/service-man/config/service-accounts/emailer.json +7 -0
- package/src/service-man/config/service-accounts/fota-server.json +6 -0
- package/src/service-man/config/service-accounts/geotile-server.json +8 -0
- package/src/service-man/config/service-accounts/imagine.json +6 -0
- package/src/service-man/config/service-accounts/message-processor.json +6 -0
- package/src/service-man/config/service-accounts/messenger.json +7 -0
- package/src/service-man/config/service-accounts/pubsub-pulse.json +6 -0
- package/src/service-man/config/service-accounts/push-notifier.json +8 -0
- package/src/service-man/config/service-accounts/reason.json +5 -0
- package/src/service-man/config/service-accounts/resource-server.json +9 -0
- package/src/service-man/config/service-accounts/rule-engine.json +7 -0
- package/src/service-man/config/service-accounts/scheduler.json +6 -0
- package/src/service-man/config/service-accounts/transponder.json +15 -0
- package/src/service-man/gcloud-k8s/RolesManager.mjs +41 -0
- package/src/service-man/gcloud-k8s/SecretsManager.mjs +182 -0
- package/src/service-man/gcloud-k8s/ServiceAccountManager.mjs +246 -0
- package/src/service-man/gcloud-k8s/VersionManager.mjs +114 -0
- package/src/service-man/gcloud-k8s/index.mjs +11 -0
- package/src/service-man/service-man.mjs +18 -0
- package/src/service-man/utils/index.mjs +83 -0
- package/leverege-build-tools-2.21.13.tgz +0 -0
- package/lib/server/build-tools.js +0 -138
- package/lib/server/firebaseDeploy.js +0 -195
- package/lib/server/firebaseServe.js +0 -116
- package/lib/server/getfbcfg.js +0 -25
- package/lib/server/getjson.js +0 -70
- package/lib/server/overwhelm.js +0 -447
- package/lib/server/push-my-chart.js +0 -18
- package/lib/server/refresh-npm-token.js +0 -135
- package/lib/server/tag-release.js +0 -681
- package/lib/server/unleash.js +0 -50
- package/lib/web/build-tools.js +0 -138
- package/lib/web/firebaseDeploy.js +0 -195
- package/lib/web/firebaseServe.js +0 -116
- package/lib/web/getfbcfg.js +0 -25
- package/lib/web/getjson.js +0 -70
- package/lib/web/overwhelm.js +0 -447
- package/lib/web/push-my-chart.js +0 -18
- package/lib/web/refresh-npm-token.js +0 -135
- package/lib/web/tag-release.js +0 -681
- package/lib/web/unleash.js +0 -50
|
@@ -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
|
+
}
|