From b7e8d4052d382b2fc7922c4431b410cc5ef2dfd9 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Thu, 23 Dec 2021 10:54:13 -0800 Subject: [PATCH 01/13] Add secrets to runtime option. --- src/cloud-functions.ts | 3 ++- src/function-configuration.ts | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 688c4eb78..f81f02635 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -499,7 +499,8 @@ export function optionsToTrigger(options: DeploymentOptions) { 'ingressSettings', 'vpcConnectorEgressSettings', 'vpcConnector', - 'labels' + 'labels', + 'secrets' ); convertIfPresent( trigger, diff --git a/src/function-configuration.ts b/src/function-configuration.ts index 5aa6177fd..aced78766 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -165,6 +165,11 @@ export interface RuntimeOptions { * Allow requests with invalid App Check tokens on callable functions. */ allowInvalidAppCheckToken?: boolean; + + /* + * Bind secrets in Secret Manager as environment variables. + */ + secrets?: string[]; } export interface DeploymentOptions extends RuntimeOptions { From bb3978026862f181d7c01ca7f1de0f06d4d0df0d Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Thu, 30 Dec 2021 11:23:52 -0800 Subject: [PATCH 02/13] Update documentation. --- src/function-configuration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/function-configuration.ts b/src/function-configuration.ts index aced78766..73cc64eac 100644 --- a/src/function-configuration.ts +++ b/src/function-configuration.ts @@ -167,7 +167,7 @@ export interface RuntimeOptions { allowInvalidAppCheckToken?: boolean; /* - * Bind secrets in Secret Manager as environment variables. + * Secrets to bind to a function instance. */ secrets?: string[]; } From c09cf1f87ccdcb265d87f6dfc8a240c9b9cae9b5 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Tue, 4 Jan 2022 23:21:15 -0800 Subject: [PATCH 03/13] Validate secret configs. --- spec/v1/function-builder.spec.ts | 50 +++++++++++++++++++++++++++++++- src/cloud-functions.ts | 1 + src/function-builder.ts | 27 +++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/spec/v1/function-builder.spec.ts b/spec/v1/function-builder.spec.ts index 13d4daa06..96ce96615 100644 --- a/spec/v1/function-builder.spec.ts +++ b/spec/v1/function-builder.spec.ts @@ -472,11 +472,59 @@ describe('FunctionBuilder', () => { ).to.throw(); }); - it('', () => { + it('should throw an error if private identifier is in the invoker array', () => { expect(() => functions.runWith({ invoker: ['service-account1', 'private', 'service-account2'], }) ).to.throw(); }); + + it('should allow valid secret config expressed using short form', () => { + const secrets = ['API_KEY', 'API_KEY@3', 'API_KEY@latest']; + const fn = functions + .runWith({ secrets }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.secrets).to.deep.equal(secrets); + }); + + it('should allow valid secret config expressed with full resource name', () => { + const secrets = [ + 'projects/my-project/secrets/API_KEY', + 'projects/my-project/secrets/API_KEY/versions/3', + 'projects/my-project/secrets/API_KEY/versions/latest', + ]; + const fn = functions + .runWith({ secrets }) + .auth.user() + .onCreate((user) => user); + + expect(fn.__trigger.secrets).to.deep.equal(secrets); + }); + + it('should throw error given invalid secret config', () => { + expect(() => + functions.runWith({ + secrets: ['ABC!@#$'], + }) + ).to.throw(); + }); + + it('should throw error given invalid secret version on short form', () => { + expect(() => + functions.runWith({ + secrets: ['ABC@live'], + }) + ).to.throw(); + }); + + it('should throw error given invalid secret version on long form', () => { + expect(() => + functions.runWith({ + secrets: ['projects/my-project/secrets/API_KEY/versions/live'], + }) + ).to.throw(); + }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index f81f02635..3e68e5c49 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -280,6 +280,7 @@ export interface TriggerAnnotated { vpcConnectorEgressSettings?: string; serviceAccountEmail?: string; ingressSettings?: string; + secrets?: string[]; }; } diff --git a/src/function-builder.ts b/src/function-builder.ts index ef6f1ad16..0ab0b126f 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -229,6 +229,33 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { } } + if (runtimeOptions.secrets !== undefined) { + // Secrets can be configured in few ways: + // 1. Shortform notation (with or without version) + // e.g. MY_API_KEY[@VERSION] + // 2. Full resource name (with or without version) + // e.g projects/my-project/secrets/MY_API_KEY[/versions/3] + const SECRET_SHORT_REGEX = /^[A-Za-z\d\-_]+(?:@[latest|\d]+)?$/ + const SECRET_FULL_REGEX = new RegExp( + "^" + + "projects\\/" + + "((?:[0-9]+)|(?:[A-Za-z]+[A-Za-z\\d-]*[A-Za-z\\d]?))\\/" + + "secrets\\/" + + "([A-Za-z\\d\\-_]+)" + + "(?:\\/versions\\/" + "(latest|[0-9]+))?" + + "$" + ); + const invalidSecrets = runtimeOptions.secrets.filter(s => !SECRET_SHORT_REGEX.test(s) && !SECRET_FULL_REGEX.test(s)) + if (invalidSecrets.length > 0) { + throw new Error( + `Invalid secrets: ${invalidSecrets.join(',')}. ` + + "Secret must be configured using the short form (e.g. API_KEY@1) " + + "or with full resource name (e.g. projects/my-project/secrets/API_KEY)." + ); + } + } + + return true; } From b1f6fd7676ea4f7f39d40764e493590fd7cfc1a2 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 5 Jan 2022 10:33:27 -0800 Subject: [PATCH 04/13] Refactor a bit. --- src/function-builder.ts | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/function-builder.ts b/src/function-builder.ts index 0ab0b126f..386203065 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -44,6 +44,22 @@ import * as remoteConfig from './providers/remoteConfig'; import * as storage from './providers/storage'; import * as testLab from './providers/testLab'; +// Secrets can be configured in 2 ways: +// 1. Shortform notation (with or without version) +// e.g. MY_API_KEY[@VERSION] +const SECRET_SHORT_REGEX = /^[A-Za-z\d\-_]+(?:@[latest|\d]+)?$/ +// 2. Full resource name (with or without version) +// e.g projects/my-project/secrets/MY_API_KEY[/versions/3] +const SECRET_FULL_REGEX = new RegExp( + "^" + + "projects\\/" + + "((?:[0-9]+)|(?:[A-Za-z]+[A-Za-z\\d-]*[A-Za-z\\d]?))\\/" + + "secrets\\/" + + "([A-Za-z\\d\\-_]+)" + + "(?:\\/versions\\/" + "(latest|[0-9]+))?" + + "$" +); + /** * Assert that the runtime options passed in are valid. * @param runtimeOptions object containing memory and timeout information. @@ -230,21 +246,6 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { } if (runtimeOptions.secrets !== undefined) { - // Secrets can be configured in few ways: - // 1. Shortform notation (with or without version) - // e.g. MY_API_KEY[@VERSION] - // 2. Full resource name (with or without version) - // e.g projects/my-project/secrets/MY_API_KEY[/versions/3] - const SECRET_SHORT_REGEX = /^[A-Za-z\d\-_]+(?:@[latest|\d]+)?$/ - const SECRET_FULL_REGEX = new RegExp( - "^" + - "projects\\/" + - "((?:[0-9]+)|(?:[A-Za-z]+[A-Za-z\\d-]*[A-Za-z\\d]?))\\/" + - "secrets\\/" + - "([A-Za-z\\d\\-_]+)" + - "(?:\\/versions\\/" + "(latest|[0-9]+))?" + - "$" - ); const invalidSecrets = runtimeOptions.secrets.filter(s => !SECRET_SHORT_REGEX.test(s) && !SECRET_FULL_REGEX.test(s)) if (invalidSecrets.length > 0) { throw new Error( From 6c51e2c85df95c289d271049600860617e60ae1b Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 5 Jan 2022 10:47:49 -0800 Subject: [PATCH 05/13] Run prettier. --- src/function-builder.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/function-builder.ts b/src/function-builder.ts index 386203065..87c3a904e 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -47,17 +47,18 @@ import * as testLab from './providers/testLab'; // Secrets can be configured in 2 ways: // 1. Shortform notation (with or without version) // e.g. MY_API_KEY[@VERSION] -const SECRET_SHORT_REGEX = /^[A-Za-z\d\-_]+(?:@[latest|\d]+)?$/ +const SECRET_SHORT_REGEX = /^[A-Za-z\d\-_]+(?:@[latest|\d]+)?$/; // 2. Full resource name (with or without version) // e.g projects/my-project/secrets/MY_API_KEY[/versions/3] const SECRET_FULL_REGEX = new RegExp( - "^" + - "projects\\/" + - "((?:[0-9]+)|(?:[A-Za-z]+[A-Za-z\\d-]*[A-Za-z\\d]?))\\/" + - "secrets\\/" + - "([A-Za-z\\d\\-_]+)" + - "(?:\\/versions\\/" + "(latest|[0-9]+))?" + - "$" + '^' + + 'projects\\/' + + '((?:[0-9]+)|(?:[A-Za-z]+[A-Za-z\\d-]*[A-Za-z\\d]?))\\/' + + 'secrets\\/' + + '([A-Za-z\\d\\-_]+)' + + '(?:\\/versions\\/' + + '(latest|[0-9]+))?' + + '$' ); /** @@ -246,17 +247,18 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { } if (runtimeOptions.secrets !== undefined) { - const invalidSecrets = runtimeOptions.secrets.filter(s => !SECRET_SHORT_REGEX.test(s) && !SECRET_FULL_REGEX.test(s)) + const invalidSecrets = runtimeOptions.secrets.filter( + (s) => !SECRET_SHORT_REGEX.test(s) && !SECRET_FULL_REGEX.test(s) + ); if (invalidSecrets.length > 0) { throw new Error( `Invalid secrets: ${invalidSecrets.join(',')}. ` + - "Secret must be configured using the short form (e.g. API_KEY@1) " + - "or with full resource name (e.g. projects/my-project/secrets/API_KEY)." + 'Secret must be configured using the short form (e.g. API_KEY@1) ' + + 'or with full resource name (e.g. projects/my-project/secrets/API_KEY).' ); } } - return true; } From 75a449291b3da91287686e9133ddc4808c890eb2 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 12 Jan 2022 17:15:35 -0800 Subject: [PATCH 06/13] Cut scope and only support shortform, non-versioned secrets. --- spec/v1/function-builder.spec.ts | 28 +++++++--------------------- src/function-builder.ts | 21 +-------------------- 2 files changed, 8 insertions(+), 41 deletions(-) diff --git a/spec/v1/function-builder.spec.ts b/spec/v1/function-builder.spec.ts index 96ce96615..8e01cd87a 100644 --- a/spec/v1/function-builder.spec.ts +++ b/spec/v1/function-builder.spec.ts @@ -481,7 +481,7 @@ describe('FunctionBuilder', () => { }); it('should allow valid secret config expressed using short form', () => { - const secrets = ['API_KEY', 'API_KEY@3', 'API_KEY@latest']; + const secrets = ['API_KEY']; const fn = functions .runWith({ secrets }) .auth.user() @@ -490,40 +490,26 @@ describe('FunctionBuilder', () => { expect(fn.__trigger.secrets).to.deep.equal(secrets); }); - it('should allow valid secret config expressed with full resource name', () => { - const secrets = [ - 'projects/my-project/secrets/API_KEY', - 'projects/my-project/secrets/API_KEY/versions/3', - 'projects/my-project/secrets/API_KEY/versions/latest', - ]; - const fn = functions - .runWith({ secrets }) - .auth.user() - .onCreate((user) => user); - - expect(fn.__trigger.secrets).to.deep.equal(secrets); - }); - - it('should throw error given invalid secret config', () => { + it('should throw error given secrets expressed with full resource name', () => { expect(() => functions.runWith({ - secrets: ['ABC!@#$'], + secrets: ['projects/my-project/secrets/API_KEY'], }) ).to.throw(); }); - it('should throw error given invalid secret version on short form', () => { + it('should throw error given invalid secret config', () => { expect(() => functions.runWith({ - secrets: ['ABC@live'], + secrets: ['ABC/efg'], }) ).to.throw(); }); - it('should throw error given invalid secret version on long form', () => { + it('should throw error given invalid secret with versions', () => { expect(() => functions.runWith({ - secrets: ['projects/my-project/secrets/API_KEY/versions/live'], + secrets: ['ABC@3'], }) ).to.throw(); }); diff --git a/src/function-builder.ts b/src/function-builder.ts index 87c3a904e..9d796fb45 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -44,23 +44,6 @@ import * as remoteConfig from './providers/remoteConfig'; import * as storage from './providers/storage'; import * as testLab from './providers/testLab'; -// Secrets can be configured in 2 ways: -// 1. Shortform notation (with or without version) -// e.g. MY_API_KEY[@VERSION] -const SECRET_SHORT_REGEX = /^[A-Za-z\d\-_]+(?:@[latest|\d]+)?$/; -// 2. Full resource name (with or without version) -// e.g projects/my-project/secrets/MY_API_KEY[/versions/3] -const SECRET_FULL_REGEX = new RegExp( - '^' + - 'projects\\/' + - '((?:[0-9]+)|(?:[A-Za-z]+[A-Za-z\\d-]*[A-Za-z\\d]?))\\/' + - 'secrets\\/' + - '([A-Za-z\\d\\-_]+)' + - '(?:\\/versions\\/' + - '(latest|[0-9]+))?' + - '$' -); - /** * Assert that the runtime options passed in are valid. * @param runtimeOptions object containing memory and timeout information. @@ -247,9 +230,7 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { } if (runtimeOptions.secrets !== undefined) { - const invalidSecrets = runtimeOptions.secrets.filter( - (s) => !SECRET_SHORT_REGEX.test(s) && !SECRET_FULL_REGEX.test(s) - ); + const invalidSecrets = runtimeOptions.secrets.filter((s) => !/^[A-Za-z\d\-_]+$/.test(s)); if (invalidSecrets.length > 0) { throw new Error( `Invalid secrets: ${invalidSecrets.join(',')}. ` + From 355da29319ebe1a38d3425d30d80011aba28f952 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 12 Jan 2022 17:17:07 -0800 Subject: [PATCH 07/13] Prettier. --- src/function-builder.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/function-builder.ts b/src/function-builder.ts index 9d796fb45..18380a7ee 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -230,7 +230,9 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { } if (runtimeOptions.secrets !== undefined) { - const invalidSecrets = runtimeOptions.secrets.filter((s) => !/^[A-Za-z\d\-_]+$/.test(s)); + const invalidSecrets = runtimeOptions.secrets.filter( + (s) => !/^[A-Za-z\d\-_]+$/.test(s) + ); if (invalidSecrets.length > 0) { throw new Error( `Invalid secrets: ${invalidSecrets.join(',')}. ` + From f5e8ab0f0be3cc438b0974205998fd02610c4d8e Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 19 Jan 2022 15:14:06 -0800 Subject: [PATCH 08/13] Add changelog entry. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d6230214..6c6fd80fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Parallelizes network calls that occur when validating authorization for onCall handlers. - Adds new regions to V2 API +- Add new runtime option for setting secrets. From 2312a377d7c6323616726d88fecb101ed682854d Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Thu, 20 Jan 2022 21:30:01 -0800 Subject: [PATCH 09/13] Update error message to reflect changed api surface. --- src/function-builder.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/function-builder.ts b/src/function-builder.ts index 18380a7ee..cf52bfb56 100644 --- a/src/function-builder.ts +++ b/src/function-builder.ts @@ -236,8 +236,7 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean { if (invalidSecrets.length > 0) { throw new Error( `Invalid secrets: ${invalidSecrets.join(',')}. ` + - 'Secret must be configured using the short form (e.g. API_KEY@1) ' + - 'or with full resource name (e.g. projects/my-project/secrets/API_KEY).' + 'Secret must be configured using the resource id (e.g. API_KEY)' ); } } From c7ffa02f31255e2d7c0a9c7f47a93262d9e1696c Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 2 Feb 2022 15:56:39 -0800 Subject: [PATCH 10/13] Annotate endpoint with secrets. --- spec/v1/cloud-functions.spec.ts | 2 ++ src/cloud-functions.ts | 7 +++++++ src/common/manifest.ts | 1 + 3 files changed, 10 insertions(+) diff --git a/spec/v1/cloud-functions.spec.ts b/spec/v1/cloud-functions.spec.ts index c894bfa23..6e6f3dd5b 100644 --- a/spec/v1/cloud-functions.spec.ts +++ b/spec/v1/cloud-functions.spec.ts @@ -107,6 +107,7 @@ describe('makeCloudFunction', () => { regions: ['us-central1'], memory: '128MB', serviceAccount: 'foo@google.com', + secrets: ['MY_SECRET'], }, }); @@ -123,6 +124,7 @@ describe('makeCloudFunction', () => { }, retry: false, }, + secretEnvironmentVariables: [{ secret: 'MY_SECRET' }], labels: {}, }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index c631c3750..faae299e0 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -622,6 +622,13 @@ export function optionsToEndpoint( 'serviceAccount', (sa) => sa ); + convertIfPresent( + endpoint, + options, + 'secretEnvironmentVariables', + 'secrets', + (secrets) => secrets.map((secret) => ({ secret })) + ); if (options?.vpcConnector) { endpoint.vpc = { connector: options.vpcConnector }; convertIfPresent( diff --git a/src/common/manifest.ts b/src/common/manifest.ts index 379e61a4f..2fae25c63 100644 --- a/src/common/manifest.ts +++ b/src/common/manifest.ts @@ -40,6 +40,7 @@ export interface ManifestEndpoint { labels?: Record; ingressSettings?: string; environmentVariables?: Record; + secretEnvironmentVariables?: { key?: string; secret: string; }[]; httpsTrigger?: { invoker?: string[]; From bf17b87252220ec4446e8d70a777a580950069a8 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 2 Feb 2022 16:03:57 -0800 Subject: [PATCH 11/13] Fix formatting. --- src/common/manifest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/manifest.ts b/src/common/manifest.ts index 2fae25c63..c7727e385 100644 --- a/src/common/manifest.ts +++ b/src/common/manifest.ts @@ -40,7 +40,7 @@ export interface ManifestEndpoint { labels?: Record; ingressSettings?: string; environmentVariables?: Record; - secretEnvironmentVariables?: { key?: string; secret: string; }[]; + secretEnvironmentVariables?: { key?: string; secret: string }[]; httpsTrigger?: { invoker?: string[]; From 1ad418df76a94e500f836a3464bebaa3690b76c1 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 7 Feb 2022 15:47:50 -0800 Subject: [PATCH 12/13] Apply update to manifest definition to correct file. --- src/runtime/manifest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/runtime/manifest.ts b/src/runtime/manifest.ts index 656a0c5d1..cc1c8e64f 100644 --- a/src/runtime/manifest.ts +++ b/src/runtime/manifest.ts @@ -39,6 +39,7 @@ export interface ManifestEndpoint { labels?: Record; ingressSettings?: string; environmentVariables?: Record; + secretEnvironmentVariables?: { key?: string; secret: string }[]; httpsTrigger?: { invoker?: string[]; From e8e08b769671dfbe2a37463361439bbc6a32d80a Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 9 Feb 2022 11:34:41 -0800 Subject: [PATCH 13/13] Set both secret key and secret. --- spec/v1/cloud-functions.spec.ts | 2 +- src/cloud-functions.ts | 2 +- src/runtime/manifest.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/v1/cloud-functions.spec.ts b/spec/v1/cloud-functions.spec.ts index 6e6f3dd5b..5dd0a8941 100644 --- a/spec/v1/cloud-functions.spec.ts +++ b/spec/v1/cloud-functions.spec.ts @@ -124,7 +124,7 @@ describe('makeCloudFunction', () => { }, retry: false, }, - secretEnvironmentVariables: [{ secret: 'MY_SECRET' }], + secretEnvironmentVariables: [{ secret: 'MY_SECRET', key: 'MY_SECRET' }], labels: {}, }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 460136f24..ffe94e031 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -627,7 +627,7 @@ export function optionsToEndpoint( options, 'secretEnvironmentVariables', 'secrets', - (secrets) => secrets.map((secret) => ({ secret })) + (secrets) => secrets.map((secret) => ({ secret, key: secret })) ); if (options?.vpcConnector) { endpoint.vpc = { connector: options.vpcConnector }; diff --git a/src/runtime/manifest.ts b/src/runtime/manifest.ts index cc1c8e64f..aaaf87761 100644 --- a/src/runtime/manifest.ts +++ b/src/runtime/manifest.ts @@ -39,7 +39,7 @@ export interface ManifestEndpoint { labels?: Record; ingressSettings?: string; environmentVariables?: Record; - secretEnvironmentVariables?: { key?: string; secret: string }[]; + secretEnvironmentVariables?: { key: string; secret?: string }[]; httpsTrigger?: { invoker?: string[];