Skip to content

Commit

Permalink
Add update_strategy for envvars and secrets
Browse files Browse the repository at this point in the history
The default behavior remains to merge (--update), but setting the update_strategy to "overwrite" will set all values.
  • Loading branch information
sethvargo committed May 16, 2024
1 parent 08b62e2 commit 4263196
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 52 deletions.
73 changes: 35 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,61 +92,46 @@ jobs:
specifying 'v1' for a service named 'helloworld', would lead to a revision
named 'helloworld-v1'. The default value is no suffix.

- `env_vars`: (Optional) List of key=value pairs to set as environment
variables. All existing environment variables will be retained. If both
`env_vars` and `env_vars_file` are specified, the keys in `env_vars` will take
precendence over the keys in `env_vars_files`.
- `env_vars`, `env_vars_file`, and `env_vars_update_strategy`: (Optional)
These values define environment variables and their update strategy.

```yaml
with:
env_vars: |
FOO=bar
ZIP=zap
```

Entries are separated by commas (`,`) and newline characters. Keys and
values are separated by `=`. To use `,`, `=`, or newline characters, escape
them with a backslash:
`env_vars` is specified as comma-separated or newline-separated key-value
pairs, with special characters escaped using a backslash.

```yaml
with:
env_vars: |
NAME=person
EMAILS=foo@bar.com\,zip@zap.com
```

- `env_vars_file`: (Optional) Path to a file on disk, relative to the
workspace, that defines environment variables. The file can be
newline-separated KEY=VALUE pairs, JSON, or YAML format. If both `env_vars`
and `env_vars_file` are specified, the keys in env_vars will take
precendence over the keys in env_vars_files.
`env_vars_file` is the path to a file on disk relative to the workspace that
defines newline-separated KEY=VALUE pairs, JSON, or YAML.

```text
FOO=bar
ZIP=zap
NAME=person
EMAILS=foo@bar.com\,zip@zap.com
```

or

```json
{
"FOO": "bar",
"ZIP": "zap"
}
```
If both `env_vars` and `env_vars_file` are specified, they are merged and
the values from `env_vars` will take precedence on conflict.

or
`env_vars_update_strategy` controls how the environment variables are set on
the Cloud Run service. If `env_vars_update_strategy` is set to "merge", then
the environment variables are _merged_ with any upstream values. If set to
"overwrite", then all environment variables on the Cloud Run service will be
replaced with exactly the values given by the GitHub Action (making it
authoritative). The default value is "merge".

```yaml
FOO: 'bar'
ZIP: 'zap'
with:
env_vars_update_strategy: 'overwrite'
```

When specified as KEY=VALUE pairs, the same escaping rules apply as
described in `env_vars`. You do not have to escape YAML or JSON.

- `secrets`: (Optional) List of key=value pairs to use as secrets. These can
either be injected as environment variables or mounted as volumes. All
existing environment secrets and volume mounts will be retained.
- `secrets`, `secrets_update_strategy`: (Optional) List of key=value pairs to
use as secrets. These can either be injected as environment variables or
mounted as volumes. All existing environment secrets and volume mounts will
be retained.

```yaml
with:
Expand All @@ -161,6 +146,18 @@ jobs:
The same rules apply for escaping entries as from `env_vars`, but Cloud Run
is more restrictive with allowed keys and names for secrets.

`secrets_update_strategy` controls how the secrets are set on the Cloud Run
service. If `secrets_update_strategy` is set to "merge", then the secrets
are _merged_ with any upstream values. If set to "overwrite", then all
secrets on the Cloud Run service will be replaced with exactly the values
given by the GitHub Action (making it authoritative). The default value is
"merge".

```yaml
with:
secrets_update_strategy: 'overwrite'
```

- `labels`: (Optional) List of key=value pairs to set as labels on the Cloud
Run service. Existing labels will be overwritten.

Expand Down
20 changes: 20 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ inputs:
described in `env_vars`. You do not have to escape YAML or JSON.
required: false

env_vars_update_strategy:
description: |-
(Optional) Controls how the environment variables are set on the Cloud Run
service. If set to "merge", then the environment variables are merged with
any upstream values. If set to "overwrite", then all environment variables
on the Cloud Run service will be replaced with exactly the values given by
the GitHub Action (making it authoritative).
required: true
default: 'merge'

secrets:
description: |-
(Optional) List of key=value pairs to use as secrets. These can either be
Expand All @@ -129,6 +139,16 @@ inputs:
Run is more restrictive with allowed keys and names for secrets.
required: false

secrets_update_strategy:
description: |-
(Optional) Controls how the secrets are set on the Cloud Run service. If
set to "merge", then the secrets are merged with any upstream values. If
set to "overwrite", then all secrets on the Cloud Run service will be
replaced with exactly the values given by the GitHub Action (making it
authoritative).
required: true
default: 'merge'

labels:
description: |-
(Optional) List of key=value pairs to set as labels on the Cloud
Expand Down
56 changes: 42 additions & 14 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ export async function run(): Promise<void> {
const gcloudComponent = presence(getInput('gcloud_component')); // Cloud SDK component version
const envVars = getInput('env_vars'); // String of env vars KEY=VALUE,...
const envVarsFile = getInput('env_vars_file'); // File that is a string of env vars KEY=VALUE,...
const envVarsUpdateStrategy = getInput('env_vars_update_strategy') || 'merge';
const secrets = parseKVString(getInput('secrets')); // String of secrets KEY=VALUE,...
const secretsUpdateStrategy = getInput('secrets_update_strategy') || 'merge';
const region = parseCSV(getInput('region') || 'us-central1');
const source = getInput('source'); // Source directory
const suffix = getInput('suffix');
Expand Down Expand Up @@ -176,13 +178,8 @@ export async function run(): Promise<void> {
}

// Set optional flags from inputs
const compiledEnvVars = parseKVStringAndFile(envVars, envVarsFile);
if (compiledEnvVars && Object.keys(compiledEnvVars).length > 0) {
cmd.push('--update-env-vars', joinKVStringForGCloud(compiledEnvVars));
}
if (secrets && Object.keys(secrets).length > 0) {
cmd.push('--set-secrets', joinKVStringForGCloud(secrets));
}
setEnvVarsFlags(cmd, envVars, envVarsFile, envVarsUpdateStrategy);
setSecretsFlags(cmd, secrets, secretsUpdateStrategy);

// Compile the labels
const defLabels = skipDefaultLabels ? {} : defaultLabels();
Expand All @@ -200,13 +197,9 @@ export async function run(): Promise<void> {
}

// Set optional flags from inputs
const compiledEnvVars = parseKVStringAndFile(envVars, envVarsFile);
if (compiledEnvVars && Object.keys(compiledEnvVars).length > 0) {
cmd.push('--update-env-vars', joinKVStringForGCloud(compiledEnvVars));
}
if (secrets && Object.keys(secrets).length > 0) {
cmd.push('--update-secrets', joinKVStringForGCloud(secrets));
}
setEnvVarsFlags(cmd, envVars, envVarsFile, envVarsUpdateStrategy);
setSecretsFlags(cmd, secrets, secretsUpdateStrategy);

if (tag) {
cmd.push('--tag', tag);
}
Expand Down Expand Up @@ -333,6 +326,41 @@ async function computeGcloudVersion(str: string): Promise<string> {
return str;
}

function setEnvVarsFlags(cmd: string[], envVars: string, envVarsFile: string, strategy: string) {
const compiledEnvVars = parseKVStringAndFile(envVars, envVarsFile);
if (compiledEnvVars && Object.keys(compiledEnvVars).length > 0) {
let flag = '';
if (strategy === 'overwrite') {
flag = '--set-env-vars';
} else if (strategy === 'merge') {
flag = '--update-env-vars';
} else {
throw new Error(
`Invalid "env_vars_update_strategy" value "${strategy}", valid values ` +
`are "overwrite" and "merge".`,
);
}
cmd.push(flag, joinKVStringForGCloud(compiledEnvVars));
}
}

function setSecretsFlags(cmd: string[], secrets: KVPair, strategy: string) {
if (secrets && Object.keys(secrets).length > 0) {
let flag = '';
if (strategy === 'overwrite') {
flag = '--set-secrets';
} else if (strategy === 'merge') {
flag = '--update-secrets';
} else {
throw new Error(
`Invalid "secrets_update_strategy" value "${strategy}", valid values ` +
`are "overwrite" and "merge".`,
);
}
cmd.push(flag, joinKVStringForGCloud(secrets));
}
}

/**
* execute the main function when this module is required directly.
*/
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,60 @@ test('#run', { concurrency: true }, async (suite) => {
assert.deepStrictEqual(args, 'beta');
});

await suite.test('merges envvars', async (t) => {
const mocks = defaultMocks(t.mock, {
service: 'my-test-service',
env_vars: 'FOO=BAR',
});

await run();

const args = mocks.getExecOutput.mock.calls?.at(0).arguments?.at(1);
const envVars = splitKV(args.at(args.indexOf('--update-env-vars') + 1));
assert.deepStrictEqual(envVars, { FOO: 'BAR' });
});

await suite.test('overwrites envvars', async (t) => {
const mocks = defaultMocks(t.mock, {
service: 'my-test-service',
env_vars: 'FOO=BAR',
env_vars_update_strategy: 'overwrite',
});

await run();

const args = mocks.getExecOutput.mock.calls?.at(0).arguments?.at(1);
const envVars = splitKV(args.at(args.indexOf('--set-env-vars') + 1));
assert.deepStrictEqual(envVars, { FOO: 'BAR' });
});

await suite.test('merges secrets', async (t) => {
const mocks = defaultMocks(t.mock, {
service: 'my-test-service',
secrets: 'FOO=bar:latest',
});

await run();

const args = mocks.getExecOutput.mock.calls?.at(0).arguments?.at(1);
const envVars = splitKV(args.at(args.indexOf('--update-secrets') + 1));
assert.deepStrictEqual(envVars, { FOO: 'bar:latest' });
});

await suite.test('overwrites secrets', async (t) => {
const mocks = defaultMocks(t.mock, {
service: 'my-test-service',
secrets: 'FOO=bar:latest',
secrets_update_strategy: 'overwrite',
});

await run();

const args = mocks.getExecOutput.mock.calls?.at(0).arguments?.at(1);
const envVars = splitKV(args.at(args.indexOf('--set-secrets') + 1));
assert.deepStrictEqual(envVars, { FOO: 'bar:latest' });
});

await suite.test('sets labels', async (t) => {
const mocks = defaultMocks(t.mock, {
service: 'my-test-service',
Expand Down

0 comments on commit 4263196

Please sign in to comment.