Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 10 additions & 52 deletions src/cloudFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -53,6 +48,7 @@ export type CloudFunctionOptions = {
sourceDir?: string;
envVars?: string;
envVarsFile?: string;
envVarsDelimiter?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
envVarsDelimiter?: string;
envVarsDelimiter:string = ',';

entryPoint?: string;
runtime: string;
availableMemoryMb?: number;
Expand Down Expand Up @@ -128,28 +124,32 @@ 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 = this.parseEnvVarsFile(opts.envVarsFile);
envVars = parseEnvVarsFile(opts.envVarsFile);
}

if (opts?.envVars) {
envVars = {
...envVars,
...this.parseKVPairs(opts.envVars),
...parseKVPairs(opts.envVars, envVarsDelimiter),
};
}

request.environmentVariables = envVars;
}

if (opts?.labels) {
request.labels = this.parseKVPairs(opts.labels);
request.labels = parseKVPairs(opts.labels, envVarsDelimiter);
}

this.request = request;
Expand All @@ -168,46 +168,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 {
const valuePairs = values.split(',');
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('='));
const value = pair.substring(pair.indexOf('=') + 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;
}
}
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async function run(): Promise<void> {
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');
Expand Down Expand Up @@ -58,6 +59,7 @@ async function run(): Promise<void> {
entryPoint,
envVars,
envVarsFile,
envVarsDelimiter,
timeout,
maxInstances: +maxInstances,
eventTriggerType,
Expand Down
48 changes: 48 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -143,3 +148,46 @@ export async function uploadSource(
core.info(`zip file ${zipPath} uploaded successfully`);
return uploadUrl;
}

/**
* Parses a string of the format `KEY1=VALUE1<delimiter>KEY2=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, delimiter: string): KVPair {
const valuePairs = values.split(delimiter).filter((x) => x !== '');
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('='));
const value = pair.substring(pair.indexOf('=') + 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;
}
55 changes: 18 additions & 37 deletions tests/cloudFunction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,24 @@ describe('CloudFunction', function () {
expect(cf.request.environmentVariables?.KEY3).equal('VALUE3');
});

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(cf.request.environmentVariables?.KEY1).equals('VALUE1');
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 () {
Expand All @@ -115,43 +133,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';
Expand Down
121 changes: 120 additions & 1 deletion tests/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -56,6 +56,125 @@ describe('Zip', function () {
});
});

describe('Parse KV pairs', function () {
describe('Positive parsing tests', () => {
const positiveParsingTests = [
{
name: 'parse single unquoted envVar',
input: 'KEY1=VALUE1',
delimiter: ',',
output: { KEY1: 'VALUE1' },
},
{
name: 'parse single quoted envVar',
input: '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"',
delimiter: ',',
output: { KEY1: '"VALUE1"', KEY2: '"VALUE2"' },
},
{
name: 'parse mix of quoted and unquoted envVars',
input: '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' },
},
{
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,
);
});
});
});
describe('Negative parsing tests', () => {
const negativeParsingTests = [
{
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""',
},
];
negativeParsingTests.forEach((test) => {
it(test.name, () => {
expect(function () {
parseKVPairs(test.input, test.delimiter);
}).to.throw(test.error);
});
});
});
});

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
Expand Down