diff --git a/.github/scripts/setup-vars.js b/.github/scripts/setup-vars.js index c78c2f2f63..d538128ca3 100644 --- a/.github/scripts/setup-vars.js +++ b/.github/scripts/setup-vars.js @@ -14,11 +14,12 @@ limitations under the License. */ -export default function setupVars({projectId, core, setup}, runId = null) { +export default function setupVars({projectId, core, setup, serviceAccount, idToken}, runId = null) { // Define automatic variables plus custom variables. const vars = { PROJECT_ID: projectId, RUN_ID: runId || uniqueId(), + SERVICE_ACCOUNT: serviceAccount, ...(setup.env || {}), }; @@ -45,6 +46,16 @@ export default function setupVars({projectId, core, setup}, runId = null) { console.log(` ${key}: ${setup.secrets[key]}`); } + // Set global secret for the Service Account identity token + // Use in place of 'gcloud auth print-identity-token' or auth.getIdTokenClient + // usage: curl -H 'Bearer: $ID_TOKEN' https:// + + idToken = (idToken || '').toString() // debug + core.exportVariable('ID_TOKEN', idToken) + core.setSecret(idToken) + // For logging, show the source of the ID_TOKEN + console.log(` ID_TOKEN: steps.auth.outputs.id_token (from GitHub Action)`); + // Return env and secrets to use for further steps. return { env: env, diff --git a/.github/scripts/setup-vars.test.js b/.github/scripts/setup-vars.test.js index 86524856b4..e1f4b47b90 100644 --- a/.github/scripts/setup-vars.test.js +++ b/.github/scripts/setup-vars.test.js @@ -19,52 +19,54 @@ import setupVars from './setup-vars.js'; import {substituteVars, uniqueId} from './setup-vars.js'; const projectId = 'my-test-project'; +const serviceAccount = "my-sa@my-project.iam.gserviceaccount.com" const core = { exportVariable: (_key, _value) => null, + setSecret: (_key) => null, }; -const autovars = {PROJECT_ID: projectId, RUN_ID: 'run-id'}; +const autovars = {PROJECT_ID: projectId, RUN_ID: 'run-id', SERVICE_ACCOUNT: serviceAccount}; describe('setupVars', () => { describe('env', () => { it('empty', () => { const setup = {}; - const vars = setupVars({projectId, core, setup}, 'run-id'); + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); const expected = autovars; deepStrictEqual(vars.env, expected); }); it('zero vars', () => { const setup = {env: {}}; - const vars = setupVars({projectId, core, setup}, 'run-id'); + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); const expected = autovars; deepStrictEqual(vars.env, expected); }); it('one var', () => { const setup = {env: {A: 'x'}}; - const vars = setupVars({projectId, core, setup}, 'run-id'); + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); const expected = {...autovars, A: 'x'}; deepStrictEqual(vars.env, expected); }); it('three vars', () => { const setup = {env: {A: 'x', B: 'y', C: 'z'}}; - const vars = setupVars({projectId, core, setup}, 'run-id'); + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); const expected = {...autovars, A: 'x', B: 'y', C: 'z'}; deepStrictEqual(vars.env, expected); }); it('should override automatic variables', () => { - const setup = {env: {PROJECT_ID: 'custom-value'}}; - const vars = setupVars({projectId, core, setup}, 'run-id'); - const expected = {PROJECT_ID: 'custom-value', RUN_ID: 'run-id'}; + const setup = {env: {PROJECT_ID: 'custom-value', SERVICE_ACCOUNT: 'baz@foo.com'}}; + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); + const expected = {PROJECT_ID: 'custom-value', RUN_ID: 'run-id', SERVICE_ACCOUNT: 'baz@foo.com'}; deepStrictEqual(vars.env, expected); }); it('should interpolate variables', () => { const setup = {env: {A: 'x', B: 'y', C: '$A/${B}'}}; - const vars = setupVars({projectId, core, setup}, 'run-id'); + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); const expected = {...autovars, A: 'x', B: 'y', C: 'x/y'}; deepStrictEqual(vars.env, expected); }); @@ -74,7 +76,7 @@ describe('setupVars', () => { env: {C: '$x/$y'}, secrets: {A: 'x', B: 'y'}, }; - const vars = setupVars({projectId, core, setup}, 'run-id'); + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); const expected = {...autovars, C: '$x/$y'}; deepStrictEqual(vars.env, expected); }); @@ -83,20 +85,20 @@ describe('setupVars', () => { describe('secrets', () => { it('zero secrets', () => { const setup = {secrets: {}}; - const vars = setupVars({projectId, core, setup}, 'run-id'); + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); deepStrictEqual(vars.secrets, ''); }); it('one secret', () => { const setup = {secrets: {A: 'x'}}; - const vars = setupVars({projectId, core, setup}, 'run-id'); + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); const expected = 'A:x'; deepStrictEqual(vars.secrets, expected); }); it('three secrets', () => { const setup = {secrets: {A: 'x', B: 'y', C: 'z'}}; - const vars = setupVars({projectId, core, setup}, 'run-id'); + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); const expected = 'A:x\nB:y\nC:z'; deepStrictEqual(vars.secrets, expected); }); @@ -106,14 +108,14 @@ describe('setupVars', () => { env: {A: 'x', B: 'y'}, secrets: {C: '$A/$B'}, }; - const vars = setupVars({projectId, core, setup}, 'run-id'); + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); const expected = 'C:$A/$B'; deepStrictEqual(vars.secrets, expected); }); it('should not interpolate secrets', () => { const setup = {secrets: {A: 'x', B: 'y', C: '$A/$B'}}; - const vars = setupVars({projectId, core, setup}, 'run-id'); + const vars = setupVars({projectId, core, setup, serviceAccount}, 'run-id'); const expected = 'A:x\nB:y\nC:$A/$B'; deepStrictEqual(vars.secrets, expected); }); diff --git a/.github/workflows/ci-dev.yaml b/.github/workflows/ci-dev.yaml index 79fda5a6fd..48eec185d2 100644 --- a/.github/workflows/ci-dev.yaml +++ b/.github/workflows/ci-dev.yaml @@ -73,6 +73,7 @@ jobs: path: ${{ fromJson(github.event_name == 'pull_request' && needs.affected.outputs.nodejs-paths || '[]') }} env: GOOGLE_SAMPLES_PROJECT: long-door-651 + GOOGLE_SERVICE_ACCOUNT: kokoro-system-test@long-door-651.iam.gserviceaccount.com CI_SETUP: ${{ toJson(fromJson(needs.affected.outputs.nodejs-setups)[matrix.path])}} steps: - name: CI Setup @@ -84,20 +85,26 @@ jobs: with: node-version: ${{ fromJson(env.CI_SETUP).node-version }} - uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2 + id: auth with: project_id: ${{ env.GOOGLE_SAMPLES_PROJECT }} workload_identity_provider: projects/1046198160504/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider - service_account: kokoro-system-test@long-door-651.iam.gserviceaccount.com + service_account: ${{ env.GOOGLE_SERVICE_ACCOUNT }} access_token_lifetime: 600s # 10 minutes + token_format: 'id_token' + id_token_audience: 'https://action.test/' # service must have this custom audience + id_token_include_email: true - name: Export environment variables uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 id: vars with: script: | - const { default: setupVars } = await import('${{ github.workspace }}/.github/scripts/setup-vars.js') + const { default: setupVars } = await import('${{ github.workspace }}/.github/scripts/setup-vars.js'); const projectId = '${{ env.GOOGLE_SAMPLES_PROJECT }}'; const setup = JSON.parse(process.env.CI_SETUP); - return await setupVars({projectId, core, setup}) + const serviceAccount = '${{ env.GOOGLE_SERVICE_ACCOUNT }}'; + const idToken = '${{ steps.auth.outputs.id_token }}'; + return await setupVars({projectId, core, setup, serviceAccount, idToken}) - uses: google-github-actions/get-secretmanager-secrets@e5bb06c2ca53b244f978d33348d18317a7f263ce # v2 if: ${{ fromJson(steps.vars.outputs.result).secrets }} with: diff --git a/.github/workflows/ci-prod.yaml b/.github/workflows/ci-prod.yaml index 1aea6ef23e..3448fc7c40 100644 --- a/.github/workflows/ci-prod.yaml +++ b/.github/workflows/ci-prod.yaml @@ -85,6 +85,7 @@ jobs: path: ${{ fromJson(github.event_name == 'pull_request' && needs.affected.outputs.nodejs-paths || '[]') }} env: GOOGLE_SAMPLES_PROJECT: long-door-651 + GOOGLE_SERVICE_ACCOUNT: kokoro-system-test@long-door-651.iam.gserviceaccount.com CI_SETUP: ${{ toJson(fromJson(needs.affected.outputs.nodejs-setups)[matrix.path])}} steps: - name: CI Setup @@ -96,20 +97,26 @@ jobs: with: node-version: ${{ fromJson(env.CI_SETUP).node-version }} - uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2 + id: auth with: project_id: ${{ env.GOOGLE_SAMPLES_PROJECT }} workload_identity_provider: projects/1046198160504/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider - service_account: kokoro-system-test@long-door-651.iam.gserviceaccount.com + service_account: ${{ env.GOOGLE_SERVICE_ACCOUNT }} access_token_lifetime: 600s # 10 minutes + token_format: 'id_token' + id_token_audience: 'https://action.test/' # service must have this custom audience + id_token_include_email: true - name: Export environment variables uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 id: vars with: script: | - const { default: setupVars } = await import('${{ github.workspace }}/.github/scripts/setup-vars.js') + const { default: setupVars } = await import('${{ github.workspace }}/.github/scripts/setup-vars.js'); const projectId = '${{ env.GOOGLE_SAMPLES_PROJECT }}'; const setup = JSON.parse(process.env.CI_SETUP); - return await setupVars({projectId, core, setup}) + const serviceAccount = '${{ env.GOOGLE_SERVICE_ACCOUNT }}'; + const idToken = '${{ steps.auth.outputs.id_token }}'; + return await setupVars({projectId, core, setup, serviceAccount, idToken}) - uses: google-github-actions/get-secretmanager-secrets@e5bb06c2ca53b244f978d33348d18317a7f263ce # v2 if: ${{ fromJson(steps.vars.outputs.result).secrets }} with: diff --git a/run/helloworld/ci-setup.json b/run/helloworld/ci-setup.json new file mode 100644 index 0000000000..94233e215c --- /dev/null +++ b/run/helloworld/ci-setup.json @@ -0,0 +1,5 @@ +{ + "env": { + "SERVICE_NAME": "run-helloworld-$RUN_ID" + } +} diff --git a/run/helloworld/package.json b/run/helloworld/package.json index c380d70ab2..62cccf4d37 100644 --- a/run/helloworld/package.json +++ b/run/helloworld/package.json @@ -6,7 +6,9 @@ "main": "index.js", "scripts": { "start": "node index.js", - "test": "c8 mocha -p -j 2 test/index.test.js --exit", + "test": "npm -- run all-test", + "all-test": "npm run unit-test && npm run system-test", + "unit-test": "c8 mocha -p -j 2 test/index.test.js --exit", "system-test": "NAME=Cloud c8 mocha -p -j 2 test/system.test.js --timeout=180000" }, "type": "module", diff --git a/run/helloworld/test/e2e_test_cleanup.yaml b/run/helloworld/test/e2e_test_cleanup.yaml index a82fd25c06..47434239a9 100644 --- a/run/helloworld/test/e2e_test_cleanup.yaml +++ b/run/helloworld/test/e2e_test_cleanup.yaml @@ -17,3 +17,9 @@ substitutions: _VERSION: manual _REGION: us-central1 _PLATFORM: managed + _SERVICE_ACCOUNT: ${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com + +serviceAccount: 'projects/${PROJECT_ID}/serviceAccounts/${_SERVICE_ACCOUNT}' +options: + logging: CLOUD_LOGGING_ONLY + dynamicSubstitutions: true diff --git a/run/helloworld/test/e2e_test_setup.yaml b/run/helloworld/test/e2e_test_setup.yaml index b354bca091..b8405f85f8 100644 --- a/run/helloworld/test/e2e_test_setup.yaml +++ b/run/helloworld/test/e2e_test_setup.yaml @@ -27,8 +27,8 @@ steps: --no-allow-unauthenticated \ --region ${_REGION} \ --platform ${_PLATFORM} \ - --set-env-vars NAME=${_NAME}" - + --set-env-vars NAME=${_NAME} \ + --add-custom-audiences 'https://action.test/'" images: - gcr.io/${PROJECT_ID}/${_SERVICE}:${_VERSION} @@ -39,3 +39,9 @@ substitutions: _REGION: us-central1 _PLATFORM: managed _NAME: Cloud + _SERVICE_ACCOUNT: ${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com + +serviceAccount: 'projects/${PROJECT_ID}/serviceAccounts/${_SERVICE_ACCOUNT}' +options: + logging: CLOUD_LOGGING_ONLY + dynamicSubstitutions: true diff --git a/run/helloworld/test/system.test.js b/run/helloworld/test/system.test.js index 3980f27031..4089f21501 100644 --- a/run/helloworld/test/system.test.js +++ b/run/helloworld/test/system.test.js @@ -15,8 +15,6 @@ import assert from 'assert'; import {execSync} from 'child_process'; import request from 'got'; -import {GoogleAuth} from 'google-auth-library'; -const auth = new GoogleAuth(); const get = (route, base_url) => { if (!ID_TOKEN) { @@ -25,7 +23,7 @@ const get = (route, base_url) => { return request(new URL(route, base_url.trim()), { headers: { - Authorization: `${ID_TOKEN.trim()}`, + Authorization: `Bearer ${ID_TOKEN}`, }, throwHttpErrors: false, }); @@ -44,12 +42,17 @@ describe('End-to-End Tests', () => { `"SERVICE_NAME" env var not found. Defaulting to "${SERVICE_NAME}"` ); } + const {SERVICE_ACCOUNT} = process.env; let {NAME} = process.env; if (!NAME) { NAME = 'Cloud'; console.log(`"NAME" env var not found. Defaulting to "${NAME}"`); } const {SAMPLE_VERSION} = process.env; + ID_TOKEN = process.env; + if (!ID_TOKEN) { + throw Error('"ID_TOKEN" env var not found.'); + } const PLATFORM = 'managed'; const REGION = 'us-central1'; before(async () => { @@ -60,6 +63,7 @@ describe('End-to-End Tests', () => { `--substitutions _SERVICE=${SERVICE_NAME},_PLATFORM=${PLATFORM},_REGION=${REGION}` + `,_NAME=${NAME}`; if (SAMPLE_VERSION) buildCmd += `,_VERSION=${SAMPLE_VERSION}`; + if (SERVICE_ACCOUNT) buildCmd += `,_SERVICE_ACCOUNT=${SERVICE_ACCOUNT}`; console.log('Starting Cloud Build...'); execSync(buildCmd, {timeout: 240000}); // timeout at 4 mins @@ -75,9 +79,8 @@ describe('End-to-End Tests', () => { if (!BASE_URL) throw Error('Cloud Run service URL not found'); // Retrieve ID token for testing - const client = await auth.getIdTokenClient(BASE_URL); - const clientHeaders = await client.getRequestHeaders(); - ID_TOKEN = clientHeaders['Authorization'].trim(); + //const client = await auth.getIdTokenClient(BASE_URL); + //const clientHeaders = await client.getRequestHeaders(); if (!ID_TOKEN) throw Error('Unable to acquire an ID token.'); }); @@ -87,6 +90,7 @@ describe('End-to-End Tests', () => { '--config ./test/e2e_test_cleanup.yaml ' + `--substitutions _SERVICE=${SERVICE_NAME},_PLATFORM=${PLATFORM},_REGION=${REGION}`; if (SAMPLE_VERSION) cleanUpCmd += `,_VERSION=${SAMPLE_VERSION}`; + if (SERVICE_ACCOUNT) cleanUpCmd += `,_SERVICE_ACCOUNT=${SERVICE_ACCOUNT}`; execSync(cleanUpCmd); });