From 5f944596fc539e29953be5ab562f00eb2346278d Mon Sep 17 00:00:00 2001 From: Karthik Balakrishnan Date: Mon, 26 Jul 2021 17:52:31 +0000 Subject: [PATCH 1/6] fix: support env vars with quotes --- src/cloudFunction.ts | 21 +++++++++++++++++++-- tests/cloudFunction.test.ts | 11 +++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/cloudFunction.ts b/src/cloudFunction.ts index b2dc569..e68f9a2 100644 --- a/src/cloudFunction.ts +++ b/src/cloudFunction.ts @@ -176,7 +176,21 @@ export class CloudFunction { * @returns map of type {KEY1:VALUE1} */ protected parseKVPairs(values: string): KVPair { - const valuePairs = values.split(','); + /** + * Regex to split on ',' while ignoring commas in double quotes + * /, // Match a `,` + * (?= // Positive lookahead after the `,` + * (?: // Not capturing group since we don't actually want to extract the values + * [^\"]* // Any number of non `"` characters + * \" // Match a `"` + * [^\"] // Any number of non `"` characters + * *\" // Match a `"` + * )* // Capture as many times as needed + * [^\"] // End with any number of non `"` characters + * *$) // Ensure we are at the end of the line + * /g // Match all + */ + const valuePairs = values.split(/,(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/g); const kvPairs: KVPair = {}; valuePairs.forEach((pair) => { if (!pair.includes('=')) { @@ -186,7 +200,10 @@ export class CloudFunction { } // Split on the first delimiter only const name = pair.substring(0, pair.indexOf('=')); - const value = pair.substring(pair.indexOf('=') + 1); + let value = pair.substring(pair.indexOf('=') + 1); + if (value.match(/\".*\"/)) { // If our value includes quotes (Ex. '"foo"'), we should ignore the outer quotes + value = value.slice(1, -1); + } kvPairs[name] = value; }); return kvPairs; diff --git a/tests/cloudFunction.test.ts b/tests/cloudFunction.test.ts index e0c6ed3..354e583 100644 --- a/tests/cloudFunction.test.ts +++ b/tests/cloudFunction.test.ts @@ -97,6 +97,17 @@ describe('CloudFunction', function () { expect(cf.request.environmentVariables?.KEY3).equal('VALUE3'); }); + it('creates a http function with quoted envVars', function () { + const envVars = 'KEY1="VALUE1.1,VALUE1.2",KEY2=VALUE2,KEY3=VALUE3'; + const cf = new CloudFunction({ name, runtime, parent, envVars }); + expect(cf.request.name).equal(`${parent}/functions/${name}`); + expect(cf.request.runtime).equal(runtime); + expect(cf.request.httpsTrigger).not.to.be.null; + expect(cf.request.environmentVariables?.KEY1).equal('VALUE1.1,VALUE1.2'); + expect(cf.request.environmentVariables?.KEY2).equal('VALUE2'); + expect(cf.request.environmentVariables?.KEY3).equal('VALUE3'); + }); + it('throws an error with bad envVars', function () { const envVars = 'KEY1,VALUE1'; expect(function () { From 7c1608f70c1b9e936bb06dc55f9e0d8993412ccb Mon Sep 17 00:00:00 2001 From: Karthik Balakrishnan Date: Tue, 27 Jul 2021 16:34:54 +0000 Subject: [PATCH 2/6] test: add json example to test --- tests/cloudFunction.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/cloudFunction.test.ts b/tests/cloudFunction.test.ts index 354e583..8b038e7 100644 --- a/tests/cloudFunction.test.ts +++ b/tests/cloudFunction.test.ts @@ -97,13 +97,14 @@ describe('CloudFunction', function () { expect(cf.request.environmentVariables?.KEY3).equal('VALUE3'); }); - it('creates a http function with quoted envVars', function () { - const envVars = 'KEY1="VALUE1.1,VALUE1.2",KEY2=VALUE2,KEY3=VALUE3'; + it('creates a http function with some quoted and some unquoted envVars', function () { + const obj = {foo: "bar", baz: "foo"}; + const envVars = `KEY1="${JSON.stringify(obj)}",KEY2=VALUE2,KEY3=VALUE3`; const cf = new CloudFunction({ name, runtime, parent, envVars }); expect(cf.request.name).equal(`${parent}/functions/${name}`); expect(cf.request.runtime).equal(runtime); expect(cf.request.httpsTrigger).not.to.be.null; - expect(cf.request.environmentVariables?.KEY1).equal('VALUE1.1,VALUE1.2'); + expect(JSON.parse(cf.request.environmentVariables?.KEY1 || "{}")).deep.equals(obj); expect(cf.request.environmentVariables?.KEY2).equal('VALUE2'); expect(cf.request.environmentVariables?.KEY3).equal('VALUE3'); }); From 42553735d6643a5bc34973ca6ae978af771fafa9 Mon Sep 17 00:00:00 2001 From: Karthik Balakrishnan Date: Sat, 31 Jul 2021 02:06:24 +0000 Subject: [PATCH 3/6] test: adds test cases for parse envVars - moves envVar parsing function to util - reorganizes unit test cases --- src/cloudFunction.ts | 73 ++------------------------------- src/util.ts | 65 +++++++++++++++++++++++++++++ tests/cloudFunction.test.ts | 43 ++------------------ tests/util.test.ts | 81 ++++++++++++++++++++++++++++++++++++- 4 files changed, 153 insertions(+), 109 deletions(-) diff --git a/src/cloudFunction.ts b/src/cloudFunction.ts index e68f9a2..ae22aae 100644 --- a/src/cloudFunction.ts +++ b/src/cloudFunction.ts @@ -15,13 +15,8 @@ */ import { cloudfunctions_v1 } from 'googleapis'; -import fs from 'fs'; -import YAML from 'yaml'; import { env } from 'process'; - -export type KVPair = { - [key: string]: string; -}; +import { parseEnvVarsFile, parseKVPairs } from './util'; /** * Available options to create the cloudFunction. @@ -135,21 +130,20 @@ export class CloudFunction { let envVars; if (opts?.envVarsFile) { - envVars = this.parseEnvVarsFile(opts.envVarsFile); + envVars = parseEnvVarsFile(opts.envVarsFile); } if (opts?.envVars) { envVars = { ...envVars, - ...this.parseKVPairs(opts.envVars), + ...parseKVPairs(opts.envVars), }; } - request.environmentVariables = envVars; } if (opts?.labels) { - request.labels = this.parseKVPairs(opts.labels); + request.labels = parseKVPairs(opts.labels); } this.request = request; @@ -168,63 +162,4 @@ export class CloudFunction { setSourceUrl(sourceUrl: string): void { this.request.sourceUploadUrl = sourceUrl; } - - /** - * Parses a string of the format `KEY1=VALUE1,KEY2=VALUE2`. - * - * @param values String with key/value pairs to parse. - * @returns map of type {KEY1:VALUE1} - */ - protected parseKVPairs(values: string): KVPair { - /** - * Regex to split on ',' while ignoring commas in double quotes - * /, // Match a `,` - * (?= // Positive lookahead after the `,` - * (?: // Not capturing group since we don't actually want to extract the values - * [^\"]* // Any number of non `"` characters - * \" // Match a `"` - * [^\"] // Any number of non `"` characters - * *\" // Match a `"` - * )* // Capture as many times as needed - * [^\"] // End with any number of non `"` characters - * *$) // Ensure we are at the end of the line - * /g // Match all - */ - const valuePairs = values.split(/,(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/g); - const kvPairs: KVPair = {}; - valuePairs.forEach((pair) => { - if (!pair.includes('=')) { - throw new TypeError( - `The expected data format should be "KEY1=VALUE1", got "${pair}" while parsing "${values}"`, - ); - } - // Split on the first delimiter only - const name = pair.substring(0, pair.indexOf('=')); - let value = pair.substring(pair.indexOf('=') + 1); - if (value.match(/\".*\"/)) { // If our value includes quotes (Ex. '"foo"'), we should ignore the outer quotes - value = value.slice(1, -1); - } - kvPairs[name] = value; - }); - return kvPairs; - } - - /** - * Read and parse an env var file. - * - * @param envVarsFile env var file path. - * @returns map of type {KEY1:VALUE1} - */ - protected parseEnvVarsFile(envVarFilePath: string): KVPair { - const content = fs.readFileSync(envVarFilePath, 'utf-8'); - const yamlContent = YAML.parse(content) as KVPair; - for (const [key, val] of Object.entries(yamlContent)) { - if (typeof key !== 'string' || typeof val !== 'string') { - throw new Error( - `env_vars_file yaml must contain only key/value pair of strings. Error parsing key ${key} of type ${typeof key} with value ${val} of type ${typeof val}`, - ); - } - } - return yamlContent; - } } diff --git a/src/util.ts b/src/util.ts index 3d42746..07c17c7 100644 --- a/src/util.ts +++ b/src/util.ts @@ -20,6 +20,11 @@ import { Gaxios } from 'gaxios'; import * as Archiver from 'archiver'; import * as path from 'path'; import ignore from 'ignore'; +import YAML from 'yaml'; + +export type KVPair = { + [key: string]: string; +}; /** * Zip a directory. @@ -143,3 +148,63 @@ export async function uploadSource( core.info(`zip file ${zipPath} uploaded successfully`); return uploadUrl; } + +/** + * Parses a string of the format `KEY1=VALUE1,KEY2=VALUE2`. + * + * @param values String with key/value pairs to parse. + * @returns map of type {KEY1:VALUE1} + */ +export function parseKVPairs(values: string): KVPair { + /** + * Regex to split on ',' while ignoring commas in double quotes + * /, // Match a `,` + * (?= // Positive lookahead after the `,` + * (?: // Not capturing group since we don't actually want to extract the values + * [^\"]* // Any number of non `"` characters + * \" // Match a `"` + * [^\"] // Any number of non `"` characters + * *\" // Match a `"` + * )* // Capture as many times as needed + * [^\"] // End with any number of non `"` characters + * *$) // Ensure we are at the end of the line + * /g // Match all + */ + const valuePairs = values.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/g); + const kvPairs: KVPair = {}; + valuePairs.forEach((pair) => { + if (!pair.includes('=')) { + throw new TypeError( + `The expected data format should be "KEY1=VALUE1", got "${pair}" while parsing "${values}"`, + ); + } + // Split on the first delimiter only + const name = pair.substring(0, pair.indexOf('=')); + let value = pair.substring(pair.indexOf('=') + 1); + if (value.match(/".*"/)) { + // If our value includes quotes (Ex. '"foo"'), we should ignore the outer quotes + value = value.slice(1, -1); + } + kvPairs[name] = value; + }); + return kvPairs; +} + +/** + * Read and parse an env var file. + * + * @param envVarsFile env var file path. + * @returns map of type {KEY1:VALUE1} + */ +export function parseEnvVarsFile(envVarFilePath: string): KVPair { + const content = fs.readFileSync(envVarFilePath, 'utf-8'); + const yamlContent = YAML.parse(content) as KVPair; + for (const [key, val] of Object.entries(yamlContent)) { + if (typeof key !== 'string' || typeof val !== 'string') { + throw new Error( + `env_vars_file yaml must contain only key/value pair of strings. Error parsing key ${key} of type ${typeof key} with value ${val} of type ${typeof val}`, + ); + } + } + return yamlContent; +} diff --git a/tests/cloudFunction.test.ts b/tests/cloudFunction.test.ts index 8b038e7..397e5a4 100644 --- a/tests/cloudFunction.test.ts +++ b/tests/cloudFunction.test.ts @@ -98,13 +98,15 @@ describe('CloudFunction', function () { }); it('creates a http function with some quoted and some unquoted envVars', function () { - const obj = {foo: "bar", baz: "foo"}; + const obj = { foo: 'bar', baz: 'foo' }; const envVars = `KEY1="${JSON.stringify(obj)}",KEY2=VALUE2,KEY3=VALUE3`; const cf = new CloudFunction({ name, runtime, parent, envVars }); expect(cf.request.name).equal(`${parent}/functions/${name}`); expect(cf.request.runtime).equal(runtime); expect(cf.request.httpsTrigger).not.to.be.null; - expect(JSON.parse(cf.request.environmentVariables?.KEY1 || "{}")).deep.equals(obj); + expect( + JSON.parse(cf.request.environmentVariables?.KEY1 || '{}'), + ).deep.equals(obj); expect(cf.request.environmentVariables?.KEY2).equal('VALUE2'); expect(cf.request.environmentVariables?.KEY3).equal('VALUE3'); }); @@ -127,43 +129,6 @@ describe('CloudFunction', function () { ); }); - it('creates a http function with two envVars containing equals character', function () { - const envVars = 'KEY1=VALUE=1,KEY2=VALUE=2'; - const cf = new CloudFunction({ name, runtime, parent, envVars }); - expect(cf.request.name).equal(`${parent}/functions/${name}`); - expect(cf.request.runtime).equal(runtime); - expect(cf.request.httpsTrigger).not.to.be.null; - expect(cf.request.environmentVariables?.KEY1).equal('VALUE=1'); - expect(cf.request.environmentVariables?.KEY2).equal('VALUE=2'); - }); - - it('creates a http function with envVarsFile', function () { - const envVarsFile = 'tests/env-var-files/test.good.yaml'; - const cf = new CloudFunction({ name, runtime, parent, envVarsFile }); - expect(cf.request.name).equal(`${parent}/functions/${name}`); - expect(cf.request.environmentVariables?.KEY1).equal('VALUE1'); - expect(cf.request.environmentVariables?.KEY2).equal('VALUE2'); - expect(cf.request.environmentVariables?.JSONKEY).equal('{"bar":"baz"}'); - }); - - it('throws an error with bad envVarsFile', function () { - const envVarsFile = 'tests/env-var-files/test.bad.yaml'; - expect(function () { - new CloudFunction({ name, runtime, parent, envVarsFile }); - }).to.throw( - 'env_vars_file yaml must contain only key/value pair of strings. Error parsing key KEY2 of type string with value VALUE2,VALUE3 of type object', - ); - }); - - it('throws an error with nonexistent envVarsFile', function () { - const envVarsFile = 'tests/env-var-files/test.nonexistent.yaml'; - expect(function () { - new CloudFunction({ name, runtime, parent, envVarsFile }); - }).to.throw( - "ENOENT: no such file or directory, open 'tests/env-var-files/test.nonexistent.yaml", - ); - }); - it('Merge envVars and envVarsFile if both specified', function () { const envVarsFile = 'tests/env-var-files/test.good.yaml'; const envVars = 'KEY3=VALUE3,KEY4=VALUE4'; diff --git a/tests/util.test.ts b/tests/util.test.ts index b86c403..6f3e1e8 100644 --- a/tests/util.test.ts +++ b/tests/util.test.ts @@ -3,7 +3,7 @@ import os from 'os'; import * as fs from 'fs'; import 'mocha'; import * as path from 'path'; -import { zipDir } from '../src/util'; +import { zipDir, parseKVPairs, parseEnvVarsFile } from '../src/util'; import StreamZip from 'node-stream-zip'; const testDirNoIgnore = 'tests/test-node-func'; @@ -56,6 +56,85 @@ describe('Zip', function () { }); }); +describe('Parse KV pairs', function () { + it('parse single unquoted envVar', async function () { + const envVar = 'KEY1=VALUE1'; + const pairs = parseKVPairs(envVar); + expect(pairs).deep.equal({ KEY1: 'VALUE1' }); + }); + it('parse single quoted envVar', async function () { + const envVar = 'KEY1="VALUE1"'; + const pairs = parseKVPairs(envVar); + expect(pairs).deep.equal({ KEY1: 'VALUE1' }); + }); + it('parse multiple unquoted envVars', async function () { + const envVars = 'KEY1=VALUE1,KEY2=VALUE2'; + const pairs = parseKVPairs(envVars); + expect(pairs).deep.equal({ KEY1: 'VALUE1', KEY2: 'VALUE2' }); + }); + it('parse multiple quoted envVars', async function () { + const envVars = 'KEY1="VALUE1",KEY2="VALUE2"'; + const pairs = parseKVPairs(envVars); + expect(pairs).deep.equal({ KEY1: 'VALUE1', KEY2: 'VALUE2' }); + }); + it('parse mix of quoted and unquoted envVars', async function () { + const envVars = 'KEY1=VALUE1,KEY2="VALUE2"'; + const pairs = parseKVPairs(envVars); + expect(pairs).deep.equal({ KEY1: 'VALUE1', KEY2: 'VALUE2' }); + }); + it('parse envVars with multiple = characters', async function () { + const envVars = 'KEY1=VALUE=1,KEY2=VALUE=2'; + const pairs = parseKVPairs(envVars); + expect(pairs).deep.equal({ KEY1: 'VALUE=1', KEY2: 'VALUE=2' }); + }); + it('throws an error if envVars is malformed', async function () { + const envVars = 'KEY1,VALUE1'; + expect(function () { + parseKVPairs(envVars); + }).to.throw( + 'The expected data format should be "KEY1=VALUE1", got "KEY1" while parsing "KEY1,VALUE1"', + ); + }); + it('throws an error if envVars are not quoted correctly', async function () { + const envVars = 'KEY1="VALUE1.1,VALUE1.2,KEY2="VALUE2"'; + expect(function () { + parseKVPairs(envVars); + }).to.throw( + `The expected data format should be "KEY1=VALUE1", got "VALUE1.2" while parsing "KEY1="VALUE1.1,VALUE1.2,KEY2="VALUE2""`, + ); + }); +}); + +describe('Parse envVars file', function () { + it('creates a http function with envVarsFile', function () { + const envVarsFile = 'tests/env-var-files/test.good.yaml'; + const pairs = parseEnvVarsFile(envVarsFile); + expect(pairs).to.deep.equal({ + KEY1: 'VALUE1', + KEY2: 'VALUE2', + JSONKEY: '{"bar":"baz"}', + }); + }); + + it('throws an error with bad envVarsFile', function () { + const envVarsFile = 'tests/env-var-files/test.bad.yaml'; + expect(function () { + parseEnvVarsFile(envVarsFile); + }).to.throw( + 'env_vars_file yaml must contain only key/value pair of strings. Error parsing key KEY2 of type string with value VALUE2,VALUE3 of type object', + ); + }); + + it('throws an error with nonexistent envVarsFile', function () { + const envVarsFile = 'tests/env-var-files/test.nonexistent.yaml'; + expect(function () { + parseEnvVarsFile(envVarsFile); + }).to.throw( + "ENOENT: no such file or directory, open 'tests/env-var-files/test.nonexistent.yaml", + ); + }); +}); + /** * * @param zipFile path to zipfile From 33baaaa84b516f6eaa5e819028331d3d02db9d60 Mon Sep 17 00:00:00 2001 From: Karthik Balakrishnan Date: Fri, 6 Aug 2021 02:21:49 +0000 Subject: [PATCH 4/6] test: refactor into table --- tests/util.test.ts | 105 ++++++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 44 deletions(-) diff --git a/tests/util.test.ts b/tests/util.test.ts index 6f3e1e8..4f489fe 100644 --- a/tests/util.test.ts +++ b/tests/util.test.ts @@ -57,51 +57,68 @@ describe('Zip', function () { }); describe('Parse KV pairs', function () { - it('parse single unquoted envVar', async function () { - const envVar = 'KEY1=VALUE1'; - const pairs = parseKVPairs(envVar); - expect(pairs).deep.equal({ KEY1: 'VALUE1' }); - }); - it('parse single quoted envVar', async function () { - const envVar = 'KEY1="VALUE1"'; - const pairs = parseKVPairs(envVar); - expect(pairs).deep.equal({ KEY1: 'VALUE1' }); - }); - it('parse multiple unquoted envVars', async function () { - const envVars = 'KEY1=VALUE1,KEY2=VALUE2'; - const pairs = parseKVPairs(envVars); - expect(pairs).deep.equal({ KEY1: 'VALUE1', KEY2: 'VALUE2' }); - }); - it('parse multiple quoted envVars', async function () { - const envVars = 'KEY1="VALUE1",KEY2="VALUE2"'; - const pairs = parseKVPairs(envVars); - expect(pairs).deep.equal({ KEY1: 'VALUE1', KEY2: 'VALUE2' }); - }); - it('parse mix of quoted and unquoted envVars', async function () { - const envVars = 'KEY1=VALUE1,KEY2="VALUE2"'; - const pairs = parseKVPairs(envVars); - expect(pairs).deep.equal({ KEY1: 'VALUE1', KEY2: 'VALUE2' }); - }); - it('parse envVars with multiple = characters', async function () { - const envVars = 'KEY1=VALUE=1,KEY2=VALUE=2'; - const pairs = parseKVPairs(envVars); - expect(pairs).deep.equal({ KEY1: 'VALUE=1', KEY2: 'VALUE=2' }); - }); - it('throws an error if envVars is malformed', async function () { - const envVars = 'KEY1,VALUE1'; - expect(function () { - parseKVPairs(envVars); - }).to.throw( - 'The expected data format should be "KEY1=VALUE1", got "KEY1" while parsing "KEY1,VALUE1"', - ); + describe('Positive parsing tests', () => { + const positiveParsingTests = [ + { + name: 'parse single unquoted envVar', + input: 'KEY1=VALUE1', + output: { KEY1: 'VALUE1' }, + }, + { + name: 'parse single quoted envVar', + input: 'KEY1="VALUE1"', + output: { KEY1: 'VALUE1' }, + }, + { + name: 'parse multiple unquoted envVars', + input: 'KEY1=VALUE1,KEY2=VALUE2', + output: { KEY1: 'VALUE1', KEY2: 'VALUE2' }, + }, + { + name: 'parse multiple quoted envVars', + input: 'KEY1="VALUE1",KEY2="VALUE2"', + output: { KEY1: 'VALUE1', KEY2: 'VALUE2' }, + }, + { + name: 'parse mix of quoted and unquoted envVars', + input: 'KEY1=VALUE1,KEY2="VALUE2"', + output: { KEY1: 'VALUE1', KEY2: 'VALUE2' }, + }, + { + name: 'parse envVars with multiple = characters', + input: 'KEY1=VALUE=1,KEY2=VALUE=2', + output: { KEY1: 'VALUE=1', KEY2: 'VALUE=2' }, + }, + ]; + + positiveParsingTests.forEach((test) => { + it(test.name, () => { + expect(parseKVPairs(test.input)).to.deep.equal(test.output); + }); + }); }); - it('throws an error if envVars are not quoted correctly', async function () { - const envVars = 'KEY1="VALUE1.1,VALUE1.2,KEY2="VALUE2"'; - expect(function () { - parseKVPairs(envVars); - }).to.throw( - `The expected data format should be "KEY1=VALUE1", got "VALUE1.2" while parsing "KEY1="VALUE1.1,VALUE1.2,KEY2="VALUE2""`, - ); + describe('Negative parsing tests', () => { + const negativeParsingTests = [ + { + name: 'throws an error if envVars is malformed', + input: 'KEY1,VALUE1', + error: + 'The expected data format should be "KEY1=VALUE1", got "KEY1" while parsing "KEY1,VALUE1"', + }, + { + name: 'throws an error if envVars are not quoted correctly', + input: 'KEY1="VALUE1.1,VALUE1.2,KEY2="VALUE2"', + error: + 'The expected data format should be "KEY1=VALUE1", got "VALUE1.2" while parsing "KEY1="VALUE1.1,VALUE1.2,KEY2="VALUE2""', + }, + ]; + negativeParsingTests.forEach((test) => { + it(test.name, () => { + expect(function () { + parseKVPairs(test.input); + }).to.throw(test.error); + }); + }); }); }); From b22f13a970582675a0d1c77d7f1fe4e4243f1a60 Mon Sep 17 00:00:00 2001 From: Karthik Balakrishnan Date: Mon, 9 Aug 2021 16:04:04 +0000 Subject: [PATCH 5/6] add new opts `env_vars_delimiter` to split envVars --- src/cloudFunction.ts | 10 ++++++++-- src/main.ts | 2 ++ src/util.ts | 25 ++++--------------------- tests/util.test.ts | 24 +++++++++++++++++++----- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/cloudFunction.ts b/src/cloudFunction.ts index ae22aae..d0c37d0 100644 --- a/src/cloudFunction.ts +++ b/src/cloudFunction.ts @@ -48,6 +48,7 @@ export type CloudFunctionOptions = { sourceDir?: string; envVars?: string; envVarsFile?: string; + envVarsDelimiter?: string; entryPoint?: string; runtime: string; availableMemoryMb?: number; @@ -123,11 +124,16 @@ export class CloudFunction { ? opts.availableMemoryMb : null; + let envVarsDelimiter = ','; // Default delimiter is `,` + // Check if `envVars` or `envVarsFile` are set. // If two var keys are the same between `envVars` and `envVarsFile` // `envVars` will override the one on `envVarsFile` if (opts?.envVars || opts?.envVarsFile) { let envVars; + if (opts?.envVarsDelimiter) { + envVarsDelimiter = opts.envVarsDelimiter; + } if (opts?.envVarsFile) { envVars = parseEnvVarsFile(opts.envVarsFile); @@ -136,14 +142,14 @@ export class CloudFunction { if (opts?.envVars) { envVars = { ...envVars, - ...parseKVPairs(opts.envVars), + ...parseKVPairs(opts.envVars, envVarsDelimiter), }; } request.environmentVariables = envVars; } if (opts?.labels) { - request.labels = parseKVPairs(opts.labels); + request.labels = parseKVPairs(opts.labels, envVarsDelimiter); } this.request = request; diff --git a/src/main.ts b/src/main.ts index baa6e21..03fee16 100644 --- a/src/main.ts +++ b/src/main.ts @@ -29,6 +29,7 @@ async function run(): Promise { const region = core.getInput('region') || 'us-central1'; const envVars = core.getInput('env_vars'); const envVarsFile = core.getInput('env_vars_file'); + const envVarsDelimiter = core.getInput('env_vars_delimiter'); const entryPoint = core.getInput('entry_point'); const sourceDir = core.getInput('source_dir'); const vpcConnector = core.getInput('vpc_connector'); @@ -58,6 +59,7 @@ async function run(): Promise { entryPoint, envVars, envVarsFile, + envVarsDelimiter, timeout, maxInstances: +maxInstances, eventTriggerType, diff --git a/src/util.ts b/src/util.ts index 07c17c7..f88eeeb 100644 --- a/src/util.ts +++ b/src/util.ts @@ -150,27 +150,14 @@ export async function uploadSource( } /** - * Parses a string of the format `KEY1=VALUE1,KEY2=VALUE2`. + * Parses a string of the format `KEY1=VALUE1KEY2=VALUE2`. * * @param values String with key/value pairs to parse. + * @param delimiter String on which to split the values. * @returns map of type {KEY1:VALUE1} */ -export function parseKVPairs(values: string): KVPair { - /** - * Regex to split on ',' while ignoring commas in double quotes - * /, // Match a `,` - * (?= // Positive lookahead after the `,` - * (?: // Not capturing group since we don't actually want to extract the values - * [^\"]* // Any number of non `"` characters - * \" // Match a `"` - * [^\"] // Any number of non `"` characters - * *\" // Match a `"` - * )* // Capture as many times as needed - * [^\"] // End with any number of non `"` characters - * *$) // Ensure we are at the end of the line - * /g // Match all - */ - const valuePairs = values.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/g); +export function parseKVPairs(values: string, delimiter: string): KVPair { + const valuePairs = values.split(delimiter).filter(x => x !== ""); const kvPairs: KVPair = {}; valuePairs.forEach((pair) => { if (!pair.includes('=')) { @@ -181,10 +168,6 @@ export function parseKVPairs(values: string): KVPair { // Split on the first delimiter only const name = pair.substring(0, pair.indexOf('=')); let value = pair.substring(pair.indexOf('=') + 1); - if (value.match(/".*"/)) { - // If our value includes quotes (Ex. '"foo"'), we should ignore the outer quotes - value = value.slice(1, -1); - } kvPairs[name] = value; }); return kvPairs; diff --git a/tests/util.test.ts b/tests/util.test.ts index 4f489fe..b8d1ae0 100644 --- a/tests/util.test.ts +++ b/tests/util.test.ts @@ -62,38 +62,50 @@ describe('Parse KV pairs', function () { { name: 'parse single unquoted envVar', input: 'KEY1=VALUE1', + delimiter: ',', output: { KEY1: 'VALUE1' }, }, { name: 'parse single quoted envVar', input: 'KEY1="VALUE1"', - output: { KEY1: 'VALUE1' }, + delimiter: ',', + output: { KEY1: '"VALUE1"' }, }, { name: 'parse multiple unquoted envVars', input: 'KEY1=VALUE1,KEY2=VALUE2', + delimiter: ',', output: { KEY1: 'VALUE1', KEY2: 'VALUE2' }, }, { name: 'parse multiple quoted envVars', input: 'KEY1="VALUE1",KEY2="VALUE2"', - output: { KEY1: 'VALUE1', KEY2: 'VALUE2' }, + delimiter: ',', + output: { KEY1: '"VALUE1"', KEY2: '"VALUE2"' }, }, { name: 'parse mix of quoted and unquoted envVars', input: 'KEY1=VALUE1,KEY2="VALUE2"', - output: { KEY1: 'VALUE1', KEY2: 'VALUE2' }, + delimiter: ',', + output: { KEY1: 'VALUE1', KEY2: '"VALUE2"' }, }, { name: 'parse envVars with multiple = characters', input: 'KEY1=VALUE=1,KEY2=VALUE=2', + delimiter: ',', output: { KEY1: 'VALUE=1', KEY2: 'VALUE=2' }, }, + { + name: 'parse envVars that are quoted JSON', + input: 'KEY1={"foo":"v1,v2","bar":"v3"}|KEY2=FOO', + delimiter: '|', + output: { KEY1: '{"foo":"v1,v2","bar":"v3"}', KEY2: 'FOO' }, + }, ]; positiveParsingTests.forEach((test) => { it(test.name, () => { - expect(parseKVPairs(test.input)).to.deep.equal(test.output); + expect(parseKVPairs(test.input, test.delimiter)).to.deep.equal(test.output); }); }); }); @@ -102,12 +114,14 @@ describe('Parse KV pairs', function () { { name: 'throws an error if envVars is malformed', input: 'KEY1,VALUE1', + delimiter: ',', error: 'The expected data format should be "KEY1=VALUE1", got "KEY1" while parsing "KEY1,VALUE1"', }, { name: 'throws an error if envVars are not quoted correctly', input: 'KEY1="VALUE1.1,VALUE1.2,KEY2="VALUE2"', + delimiter: ',', error: 'The expected data format should be "KEY1=VALUE1", got "VALUE1.2" while parsing "KEY1="VALUE1.1,VALUE1.2,KEY2="VALUE2""', }, @@ -115,7 +129,7 @@ describe('Parse KV pairs', function () { negativeParsingTests.forEach((test) => { it(test.name, () => { expect(function () { - parseKVPairs(test.input); + parseKVPairs(test.input, test.delimiter); }).to.throw(test.error); }); }); From e80292481a48985c66d6cab594d5538e272f2592 Mon Sep 17 00:00:00 2001 From: Karthik Balakrishnan Date: Mon, 9 Aug 2021 16:10:30 +0000 Subject: [PATCH 6/6] fix linting --- src/util.ts | 4 ++-- tests/cloudFunction.test.ts | 18 +++++++++++------- tests/util.test.ts | 11 ++++++++++- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/util.ts b/src/util.ts index f88eeeb..c748975 100644 --- a/src/util.ts +++ b/src/util.ts @@ -157,7 +157,7 @@ export async function uploadSource( * @returns map of type {KEY1:VALUE1} */ export function parseKVPairs(values: string, delimiter: string): KVPair { - const valuePairs = values.split(delimiter).filter(x => x !== ""); + const valuePairs = values.split(delimiter).filter((x) => x !== ''); const kvPairs: KVPair = {}; valuePairs.forEach((pair) => { if (!pair.includes('=')) { @@ -167,7 +167,7 @@ export function parseKVPairs(values: string, delimiter: string): KVPair { } // Split on the first delimiter only const name = pair.substring(0, pair.indexOf('=')); - let value = pair.substring(pair.indexOf('=') + 1); + const value = pair.substring(pair.indexOf('=') + 1); kvPairs[name] = value; }); return kvPairs; diff --git a/tests/cloudFunction.test.ts b/tests/cloudFunction.test.ts index 397e5a4..f8dfdbb 100644 --- a/tests/cloudFunction.test.ts +++ b/tests/cloudFunction.test.ts @@ -97,16 +97,20 @@ describe('CloudFunction', function () { expect(cf.request.environmentVariables?.KEY3).equal('VALUE3'); }); - it('creates a http function with some quoted and some unquoted envVars', function () { - const obj = { foo: 'bar', baz: 'foo' }; - const envVars = `KEY1="${JSON.stringify(obj)}",KEY2=VALUE2,KEY3=VALUE3`; - const cf = new CloudFunction({ name, runtime, parent, envVars }); + it('creates a http function with custom delimiter', function () { + const envVars = `KEY1=VALUE1|KEY2=VALUE2|KEY3=VALUE3`; + const envVarsDelimiter = '|'; + const cf = new CloudFunction({ + name, + runtime, + parent, + envVars, + envVarsDelimiter, + }); expect(cf.request.name).equal(`${parent}/functions/${name}`); expect(cf.request.runtime).equal(runtime); expect(cf.request.httpsTrigger).not.to.be.null; - expect( - JSON.parse(cf.request.environmentVariables?.KEY1 || '{}'), - ).deep.equals(obj); + expect(cf.request.environmentVariables?.KEY1).equals('VALUE1'); expect(cf.request.environmentVariables?.KEY2).equal('VALUE2'); expect(cf.request.environmentVariables?.KEY3).equal('VALUE3'); }); diff --git a/tests/util.test.ts b/tests/util.test.ts index b8d1ae0..052abd8 100644 --- a/tests/util.test.ts +++ b/tests/util.test.ts @@ -101,11 +101,20 @@ describe('Parse KV pairs', function () { delimiter: '|', output: { KEY1: '{"foo":"v1,v2","bar":"v3"}', KEY2: 'FOO' }, }, + { + name: 'parse envVars that are delimited by newline', + input: `KEY1={"foo":"v1,v2","bar":"v3"} +KEY2=FOO`, + delimiter: '\n', + output: { KEY1: '{"foo":"v1,v2","bar":"v3"}', KEY2: 'FOO' }, + }, ]; positiveParsingTests.forEach((test) => { it(test.name, () => { - expect(parseKVPairs(test.input, test.delimiter)).to.deep.equal(test.output); + expect(parseKVPairs(test.input, test.delimiter)).to.deep.equal( + test.output, + ); }); }); });