Skip to content

Commit 1001702

Browse files
authored
Add default environment variable and secret (#3994)
* Add service account as a default envvar * add access token as secert value * test availability of new envvars * process.env lol * envvars gonna envvar * update ci-dev.yaml * add logging for secret * revert audit-storage changes * add example usage of ACCESS_TOKEN, SERVICE_ACCOUNT * revert testing sample * make use of access token in a test * doc: update usage * use service account * reference correct variable * update order, add service_name * pull envvar * debug: try using id_token * fix missing renames * lint * set the value of the token secret, not the key/string * lint * update setupvars tests * debug: ¯\_(ツ)_/¯ * error handling * what if we just * debugging * debugging * add custom audience, set by the auth action, updated by e2e test * debug * lol * debugging * try separating setup, using a known unused domain * spelling * try matching service account * trim * ...oh * maybe * debug: clientapi * attempt adding audience validation * debug: getting closer * debug: i gotta know * debugging: oh * debug: ah * debug: literal string * debug: maybe * debug: maybe this is the issue? * debug: or maybe this? * debug: global args? * debug: remove testing code, move all vars local, no more globals
1 parent fe903de commit 1001702

File tree

9 files changed

+93
-51
lines changed

9 files changed

+93
-51
lines changed

.github/scripts/setup-vars.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
limitations under the License.
1515
*/
1616

17-
export default function setupVars({projectId, core, setup}, runId = null) {
17+
export default function setupVars({projectId, core, setup, serviceAccount, idToken}, runId = null) {
1818
// Define automatic variables plus custom variables.
1919
const vars = {
2020
PROJECT_ID: projectId,
2121
RUN_ID: runId || uniqueId(),
22+
SERVICE_ACCOUNT: serviceAccount,
2223
...(setup.env || {}),
2324
};
2425

@@ -45,6 +46,14 @@ export default function setupVars({projectId, core, setup}, runId = null) {
4546
console.log(` ${key}: ${setup.secrets[key]}`);
4647
}
4748

49+
// Set global secret for the Service Account identity token
50+
// Use in place of 'gcloud auth print-identity-token' or auth.getIdTokenClient
51+
// usage: curl -H 'Bearer: $ID_TOKEN' https://
52+
core.exportVariable('ID_TOKEN', idToken)
53+
core.setSecret(idToken)
54+
// For logging, show the source of the ID_TOKEN
55+
console.log(` ID_TOKEN: steps.auth.outputs.id_token (from GitHub Action)`);
56+
4857
// Return env and secrets to use for further steps.
4958
return {
5059
env: env,

.github/scripts/setup-vars.test.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,52 +19,54 @@ import setupVars from './setup-vars.js';
1919
import {substituteVars, uniqueId} from './setup-vars.js';
2020

2121
const projectId = 'my-test-project';
22+
const serviceAccount = "[email protected]"
2223
const core = {
2324
exportVariable: (_key, _value) => null,
25+
setSecret: (_key) => null,
2426
};
2527

26-
const autovars = {PROJECT_ID: projectId, RUN_ID: 'run-id'};
28+
const autovars = {PROJECT_ID: projectId, RUN_ID: 'run-id', SERVICE_ACCOUNT: serviceAccount};
2729

2830
describe('setupVars', () => {
2931
describe('env', () => {
3032
it('empty', () => {
3133
const setup = {};
32-
const vars = setupVars({projectId, core, setup}, 'run-id');
34+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
3335
const expected = autovars;
3436
deepStrictEqual(vars.env, expected);
3537
});
3638

3739
it('zero vars', () => {
3840
const setup = {env: {}};
39-
const vars = setupVars({projectId, core, setup}, 'run-id');
41+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
4042
const expected = autovars;
4143
deepStrictEqual(vars.env, expected);
4244
});
4345

4446
it('one var', () => {
4547
const setup = {env: {A: 'x'}};
46-
const vars = setupVars({projectId, core, setup}, 'run-id');
48+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
4749
const expected = {...autovars, A: 'x'};
4850
deepStrictEqual(vars.env, expected);
4951
});
5052

5153
it('three vars', () => {
5254
const setup = {env: {A: 'x', B: 'y', C: 'z'}};
53-
const vars = setupVars({projectId, core, setup}, 'run-id');
55+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
5456
const expected = {...autovars, A: 'x', B: 'y', C: 'z'};
5557
deepStrictEqual(vars.env, expected);
5658
});
5759

5860
it('should override automatic variables', () => {
59-
const setup = {env: {PROJECT_ID: 'custom-value'}};
60-
const vars = setupVars({projectId, core, setup}, 'run-id');
61-
const expected = {PROJECT_ID: 'custom-value', RUN_ID: 'run-id'};
61+
const setup = {env: {PROJECT_ID: 'custom-value', SERVICE_ACCOUNT: '[email protected]'}};
62+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
63+
const expected = {PROJECT_ID: 'custom-value', RUN_ID: 'run-id', SERVICE_ACCOUNT: '[email protected]'};
6264
deepStrictEqual(vars.env, expected);
6365
});
6466

6567
it('should interpolate variables', () => {
6668
const setup = {env: {A: 'x', B: 'y', C: '$A/${B}'}};
67-
const vars = setupVars({projectId, core, setup}, 'run-id');
69+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
6870
const expected = {...autovars, A: 'x', B: 'y', C: 'x/y'};
6971
deepStrictEqual(vars.env, expected);
7072
});
@@ -74,7 +76,7 @@ describe('setupVars', () => {
7476
env: {C: '$x/$y'},
7577
secrets: {A: 'x', B: 'y'},
7678
};
77-
const vars = setupVars({projectId, core, setup}, 'run-id');
79+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
7880
const expected = {...autovars, C: '$x/$y'};
7981
deepStrictEqual(vars.env, expected);
8082
});
@@ -83,20 +85,20 @@ describe('setupVars', () => {
8385
describe('secrets', () => {
8486
it('zero secrets', () => {
8587
const setup = {secrets: {}};
86-
const vars = setupVars({projectId, core, setup}, 'run-id');
88+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
8789
deepStrictEqual(vars.secrets, '');
8890
});
8991

9092
it('one secret', () => {
9193
const setup = {secrets: {A: 'x'}};
92-
const vars = setupVars({projectId, core, setup}, 'run-id');
94+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
9395
const expected = 'A:x';
9496
deepStrictEqual(vars.secrets, expected);
9597
});
9698

9799
it('three secrets', () => {
98100
const setup = {secrets: {A: 'x', B: 'y', C: 'z'}};
99-
const vars = setupVars({projectId, core, setup}, 'run-id');
101+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
100102
const expected = 'A:x\nB:y\nC:z';
101103
deepStrictEqual(vars.secrets, expected);
102104
});
@@ -106,14 +108,14 @@ describe('setupVars', () => {
106108
env: {A: 'x', B: 'y'},
107109
secrets: {C: '$A/$B'},
108110
};
109-
const vars = setupVars({projectId, core, setup}, 'run-id');
111+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
110112
const expected = 'C:$A/$B';
111113
deepStrictEqual(vars.secrets, expected);
112114
});
113115

114116
it('should not interpolate secrets', () => {
115117
const setup = {secrets: {A: 'x', B: 'y', C: '$A/$B'}};
116-
const vars = setupVars({projectId, core, setup}, 'run-id');
118+
const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id');
117119
const expected = 'A:x\nB:y\nC:$A/$B';
118120
deepStrictEqual(vars.secrets, expected);
119121
});

.github/workflows/ci-dev.yaml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ jobs:
7373
path: ${{ fromJson(github.event_name == 'pull_request' && needs.affected.outputs.nodejs-paths || '[]') }}
7474
env:
7575
GOOGLE_SAMPLES_PROJECT: long-door-651
76+
GOOGLE_SERVICE_ACCOUNT: [email protected]
7677
CI_SETUP: ${{ toJson(fromJson(needs.affected.outputs.nodejs-setups)[matrix.path])}}
7778
steps:
7879
- name: CI Setup
@@ -84,20 +85,26 @@ jobs:
8485
with:
8586
node-version: ${{ fromJson(env.CI_SETUP).node-version }}
8687
- uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2
88+
id: auth
8789
with:
8890
project_id: ${{ env.GOOGLE_SAMPLES_PROJECT }}
8991
workload_identity_provider: projects/1046198160504/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
90-
service_account: [email protected]
92+
service_account: ${{ env.GOOGLE_SERVICE_ACCOUNT }}
9193
access_token_lifetime: 600s # 10 minutes
94+
token_format: 'id_token'
95+
id_token_audience: 'https://action.test/' # service must have this custom audience
96+
id_token_include_email: true
9297
- name: Export environment variables
9398
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
9499
id: vars
95100
with:
96101
script: |
97-
const { default: setupVars } = await import('${{ github.workspace }}/.github/scripts/setup-vars.js')
102+
const { default: setupVars } = await import('${{ github.workspace }}/.github/scripts/setup-vars.js');
98103
const projectId = '${{ env.GOOGLE_SAMPLES_PROJECT }}';
99104
const setup = JSON.parse(process.env.CI_SETUP);
100-
return await setupVars({projectId, core, setup})
105+
const serviceAccount = '${{ env.GOOGLE_SERVICE_ACCOUNT }}';
106+
const idToken = '${{ steps.auth.outputs.id_token }}';
107+
return await setupVars({projectId, core, setup, serviceAccount, idToken})
101108
- uses: google-github-actions/get-secretmanager-secrets@e5bb06c2ca53b244f978d33348d18317a7f263ce # v2
102109
if: ${{ fromJson(steps.vars.outputs.result).secrets }}
103110
with:

.github/workflows/ci-prod.yaml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ jobs:
8585
path: ${{ fromJson(github.event_name == 'pull_request' && needs.affected.outputs.nodejs-paths || '[]') }}
8686
env:
8787
GOOGLE_SAMPLES_PROJECT: long-door-651
88+
GOOGLE_SERVICE_ACCOUNT: [email protected]
8889
CI_SETUP: ${{ toJson(fromJson(needs.affected.outputs.nodejs-setups)[matrix.path])}}
8990
steps:
9091
- name: CI Setup
@@ -96,20 +97,26 @@ jobs:
9697
with:
9798
node-version: ${{ fromJson(env.CI_SETUP).node-version }}
9899
- uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2
100+
id: auth
99101
with:
100102
project_id: ${{ env.GOOGLE_SAMPLES_PROJECT }}
101103
workload_identity_provider: projects/1046198160504/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
102-
service_account: [email protected]
104+
service_account: ${{ env.GOOGLE_SERVICE_ACCOUNT }}
103105
access_token_lifetime: 600s # 10 minutes
106+
token_format: 'id_token'
107+
id_token_audience: 'https://action.test/' # service must have this custom audience
108+
id_token_include_email: true
104109
- name: Export environment variables
105110
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
106111
id: vars
107112
with:
108113
script: |
109-
const { default: setupVars } = await import('${{ github.workspace }}/.github/scripts/setup-vars.js')
114+
const { default: setupVars } = await import('${{ github.workspace }}/.github/scripts/setup-vars.js');
110115
const projectId = '${{ env.GOOGLE_SAMPLES_PROJECT }}';
111116
const setup = JSON.parse(process.env.CI_SETUP);
112-
return await setupVars({projectId, core, setup})
117+
const serviceAccount = '${{ env.GOOGLE_SERVICE_ACCOUNT }}';
118+
const idToken = '${{ steps.auth.outputs.id_token }}';
119+
return await setupVars({projectId, core, setup, serviceAccount, idToken})
113120
- uses: google-github-actions/get-secretmanager-secrets@e5bb06c2ca53b244f978d33348d18317a7f263ce # v2
114121
if: ${{ fromJson(steps.vars.outputs.result).secrets }}
115122
with:

run/helloworld/ci-setup.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"env": {
3+
"SERVICE_NAME": "run-helloworld-$RUN_ID"
4+
}
5+
}

run/helloworld/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
"main": "index.js",
77
"scripts": {
88
"start": "node index.js",
9-
"test": "c8 mocha -p -j 2 test/index.test.js --exit",
9+
"test": "npm -- run all-test",
10+
"all-test": "npm run unit-test && npm run system-test",
11+
"unit-test": "c8 mocha -p -j 2 test/index.test.js --exit",
1012
"system-test": "NAME=Cloud c8 mocha -p -j 2 test/system.test.js --timeout=180000"
1113
},
1214
"type": "module",

run/helloworld/test/e2e_test_cleanup.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ substitutions:
1717
_VERSION: manual
1818
_REGION: us-central1
1919
_PLATFORM: managed
20+
_SERVICE_ACCOUNT: ${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com
21+
22+
serviceAccount: 'projects/${PROJECT_ID}/serviceAccounts/${_SERVICE_ACCOUNT}'
23+
options:
24+
logging: CLOUD_LOGGING_ONLY
25+
dynamicSubstitutions: true

run/helloworld/test/e2e_test_setup.yaml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ steps:
2727
--no-allow-unauthenticated \
2828
--region ${_REGION} \
2929
--platform ${_PLATFORM} \
30-
--set-env-vars NAME=${_NAME}"
31-
32-
30+
--set-env-vars NAME=${_NAME} \
31+
--add-custom-audiences 'https://action.test/'"
32+
# Audience matches ID Token audience
3333
images:
3434
- gcr.io/${PROJECT_ID}/${_SERVICE}:${_VERSION}
3535

@@ -39,3 +39,9 @@ substitutions:
3939
_REGION: us-central1
4040
_PLATFORM: managed
4141
_NAME: Cloud
42+
_SERVICE_ACCOUNT: ${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com
43+
44+
serviceAccount: 'projects/${PROJECT_ID}/serviceAccounts/${_SERVICE_ACCOUNT}'
45+
options:
46+
logging: CLOUD_LOGGING_ONLY
47+
dynamicSubstitutions: true

run/helloworld/test/system.test.js

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,16 @@
1515
import assert from 'assert';
1616
import {execSync} from 'child_process';
1717
import request from 'got';
18-
import {GoogleAuth} from 'google-auth-library';
19-
const auth = new GoogleAuth();
20-
21-
const get = (route, base_url) => {
22-
if (!ID_TOKEN) {
23-
throw Error('"ID_TOKEN" environment variable is required.');
24-
}
2518

19+
const get = (route, base_url, id_token) => {
2620
return request(new URL(route, base_url.trim()), {
2721
headers: {
28-
Authorization: `${ID_TOKEN.trim()}`,
22+
Authorization: `Bearer ${id_token}`,
2923
},
3024
throwHttpErrors: false,
3125
});
3226
};
3327

34-
let BASE_URL, ID_TOKEN;
3528
describe('End-to-End Tests', () => {
3629
const {GOOGLE_CLOUD_PROJECT} = process.env;
3730
if (!GOOGLE_CLOUD_PROJECT) {
@@ -44,12 +37,21 @@ describe('End-to-End Tests', () => {
4437
`"SERVICE_NAME" env var not found. Defaulting to "${SERVICE_NAME}"`
4538
);
4639
}
40+
const {SERVICE_ACCOUNT} = process.env;
4741
let {NAME} = process.env;
4842
if (!NAME) {
4943
NAME = 'Cloud';
5044
console.log(`"NAME" env var not found. Defaulting to "${NAME}"`);
5145
}
5246
const {SAMPLE_VERSION} = process.env;
47+
48+
// ID Token is made available via the test runner.
49+
// Otherwise, use auth.getIdTokenClient(BASE_URL);
50+
const {ID_TOKEN} = process.env;
51+
if (!ID_TOKEN) {
52+
throw Error('"ID_TOKEN" env var not found.');
53+
}
54+
5355
const PLATFORM = 'managed';
5456
const REGION = 'us-central1';
5557
before(async () => {
@@ -60,25 +62,11 @@ describe('End-to-End Tests', () => {
6062
`--substitutions _SERVICE=${SERVICE_NAME},_PLATFORM=${PLATFORM},_REGION=${REGION}` +
6163
`,_NAME=${NAME}`;
6264
if (SAMPLE_VERSION) buildCmd += `,_VERSION=${SAMPLE_VERSION}`;
65+
if (SERVICE_ACCOUNT) buildCmd += `,_SERVICE_ACCOUNT=${SERVICE_ACCOUNT}`;
6366

6467
console.log('Starting Cloud Build...');
6568
execSync(buildCmd, {timeout: 240000}); // timeout at 4 mins
6669
console.log('Cloud Build completed.');
67-
68-
// Retrieve URL of Cloud Run service
69-
const url = execSync(
70-
`gcloud run services describe ${SERVICE_NAME} --project=${GOOGLE_CLOUD_PROJECT} ` +
71-
`--platform=${PLATFORM} --region=${REGION} --format='value(status.url)'`
72-
);
73-
74-
BASE_URL = url.toString('utf-8').trim();
75-
if (!BASE_URL) throw Error('Cloud Run service URL not found');
76-
77-
// Retrieve ID token for testing
78-
const client = await auth.getIdTokenClient(BASE_URL);
79-
const clientHeaders = await client.getRequestHeaders();
80-
ID_TOKEN = clientHeaders['Authorization'].trim();
81-
if (!ID_TOKEN) throw Error('Unable to acquire an ID token.');
8270
});
8371

8472
after(() => {
@@ -87,12 +75,22 @@ describe('End-to-End Tests', () => {
8775
'--config ./test/e2e_test_cleanup.yaml ' +
8876
`--substitutions _SERVICE=${SERVICE_NAME},_PLATFORM=${PLATFORM},_REGION=${REGION}`;
8977
if (SAMPLE_VERSION) cleanUpCmd += `,_VERSION=${SAMPLE_VERSION}`;
78+
if (SERVICE_ACCOUNT) cleanUpCmd += `,_SERVICE_ACCOUNT=${SERVICE_ACCOUNT}`;
9079

9180
execSync(cleanUpCmd);
9281
});
9382

9483
it('Service uses the NAME override', async () => {
95-
const response = await get('/', BASE_URL);
84+
// Retrieve URL of Cloud Run service
85+
const url = execSync(
86+
`gcloud run services describe ${SERVICE_NAME} --project=${GOOGLE_CLOUD_PROJECT} ` +
87+
`--platform=${PLATFORM} --region=${REGION} --format='value(status.url)'`
88+
);
89+
90+
const BASE_URL = url.toString('utf-8').trim();
91+
if (!BASE_URL) throw Error('Cloud Run service URL not found');
92+
93+
const response = await get('/', BASE_URL, ID_TOKEN);
9694
assert.strictEqual(
9795
response.statusCode,
9896
200,

0 commit comments

Comments
 (0)