Skip to content

Commit

Permalink
feat(cli): add --var flag for setting individual variable values
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald authored and eysi09 committed Aug 3, 2020
1 parent 7215f94 commit 5ec3fd5
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 8 deletions.
1 change: 1 addition & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The following option flags can be used with any of the CLI commands:
| `--emoji` | | boolean | Enable emoji in output (defaults to true if the environment supports it).
| `--yes` | `-y` | boolean | Automatically approve any yes/no prompts during execution.
| `--force-refresh` | | boolean | Force refresh of any caches, e.g. cached provider statuses.
| `--var` | | array:string | Set a specific variable value, using the format <key>=<value>, e.g. `--var some-key=custom-value`. This will override any value set in your project configuration. You can specify multiple variables by separating with a comma, e.g. `--var key-a=foo,key-b="value with quotes"`.

### garden build

Expand Down
21 changes: 21 additions & 0 deletions docs/using-garden/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,27 @@ tasks:

Tasks are also implicitly disabled when the parent module is disabled.

### Running tasks with arguments from the CLI

For tasks that are often run ad-hoc from the CLI, you can use variables and the `--var` CLI flag to pass in values to the task.
Here for example, we have a simple container task that can receive an argument via a variable:

```yaml
kind: Module
type: container
...
tasks:
- name: my-task
command: ["echo", "${var.my-task-arg || ''}"]
...
```

You can run this task and override the argument variable like this:

```sh
garden run task my-task --var my-task-arg="hello!"
```

### Kubernetes Provider

The Kubernetes providers execute each task in its own Pod inside the project namespace. The Pod is removed once the task has finished running.
Expand Down
18 changes: 16 additions & 2 deletions docs/using-garden/variables-and-templating.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,28 @@ services:

### Variable files (varfiles)

You can also provide variables using "variable files" or _varfiles_. These work mostly like "dotenv" files or envfiles. However, they don't implicitly affect the environment of the Garden process and the configured services, but rather are added on top of the `variables` you define in your project `garden.yml`.
You can also provide variables using "variable files" or _varfiles_. These work mostly like "dotenv" files or envfiles. However, they don't implicitly affect the environment of the Garden process and the configured services, but rather are added on top of the `variables` you define in your project configuration.

This can be very useful when you need to provide secrets and other contextual values to your stack. You could add your varfiles to your `.gitignore` file to keep them out of your repository, or use e.g. [git-crypt](https://github.com/AGWA/git-crypt), [BlackBox](https://github.com/StackExchange/blackbox) or [git-secret](https://git-secret.io/) to securely store the files in your Git repo.

By default, Garden will look for a `garden.env` file in your project root for project-wide variables, and a `garden.<env-name>.env` file for environment-specific variables. You can override the filename for each as well. The format of the files is the one supported by [dotenv](https://github.com/motdotla/dotenv#rules).

You can also set variables on the command line, with `--var` flags. Note that while this is handy for ad-hoc invocations, we don't generally recommend relying on this for normal operations, since you lose a bit of visibility within your configuration. But here's one practical example:

```sh
# Override two specific variables value and run a task
garden run task my-task --var my-task-arg=foo,some-numeric-var=123
```

Multiple variables are separated with a comma, and each part is parsed using [dotenv](https://github.com/motdotla/dotenv#rules) syntax.

The order of precedence across the varfiles and project config fields is as follows (from highest to lowest):
_`garden.<env-name>.env` file_ > _`environment[].variables` field_ > _`garden.env` file_ > _`variables` field

1. Individual variables set with `--var` flags.
2. The environment-specific varfile (defaults to `garden.<env-name>.env`).
3. The environment-specific variables set in `environment[].variables`.
4. Configured project-wide varfile (defaults to `garden.env`).
5. The project-wide `variables` field.

Here's an example, where we have some project variables defined in our project config, and environment-specific values—including secret data—in varfiles:

Expand Down
13 changes: 12 additions & 1 deletion garden-service/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import sywac from "sywac"
import dotenv = require("dotenv")
import chalk from "chalk"
import sywac from "sywac"
import { intersection, merge, sortBy } from "lodash"
import { resolve, join } from "path"
import { coreCommands } from "../commands/commands"
Expand All @@ -22,6 +23,7 @@ import {
EnvironmentOption,
Parameter,
StringParameter,
StringsParameter,
} from "../commands/base"
import { GardenError, PluginError, toGardenError } from "../exceptions"
import { Garden, GardenOpts, DummyGarden } from "../garden"
Expand Down Expand Up @@ -158,6 +160,10 @@ export const GLOBAL_OPTIONS = {
help: "Force refresh of any caches, e.g. cached provider statuses.",
defaultValue: false,
}),
"var": new StringsParameter({
help:
'Set a specific variable value, using the format <key>=<value>, e.g. `--var some-key=custom-value`. This will override any value set in your project configuration. You can specify multiple variables by separating with a comma, e.g. `--var key-a=foo,key-b="value with quotes"`.',
}),
}

export type GlobalOptions = typeof GLOBAL_OPTIONS
Expand Down Expand Up @@ -285,8 +291,12 @@ export class GardenCli {
silent,
output,
"force-refresh": forceRefresh,
"var": cliVars,
} = parsedOpts

// Parse command line --var input
const parsedCliVars = cliVars ? dotenv.parse(cliVars.join("\n")) : {}

let loggerType = loggerTypeOpt || command.getLoggerType({ opts: parsedOpts, args: parsedArgs })

if (silent || output) {
Expand Down Expand Up @@ -318,6 +328,7 @@ export class GardenCli {
log,
sessionId,
forceRefresh,
variables: parsedCliVars,
}

let garden: Garden
Expand Down
5 changes: 3 additions & 2 deletions garden-service/src/commands/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,13 @@ export interface StringsConstructor extends ParameterConstructor<string[]> {
export class StringsParameter extends Parameter<string[] | undefined> {
type = "array:string"
schema = joi.array().items(joi.string())
delimiter: string
delimiter: string | RegExp

constructor(args: StringsConstructor) {
super(args)

this.delimiter = args.delimiter || ","
// The default delimiter splits on commas, ignoring commas between double quotes
this.delimiter = args.delimiter || /,(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/
}

// Sywac returns [undefined] if input is empty so we coerce that into undefined.
Expand Down
8 changes: 6 additions & 2 deletions garden-service/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { TaskGraph, GraphResults, ProcessTasksOpts } from "./task-graph"
import { getLogger } from "./logger/logger"
import { PluginActionHandlers, GardenPlugin } from "./types/plugin/plugin"
import { loadConfigResources, findProjectConfig, prepareModuleResource } from "./config/base"
import { DeepPrimitiveMap, StringMap } from "./config/common"
import { DeepPrimitiveMap, StringMap, PrimitiveMap } from "./config/common"
import { validateSchema } from "./config/validation"
import { BaseTask } from "./tasks/base"
import { LocalConfigStore, ConfigStore, GlobalConfigStore } from "./config-store"
Expand Down Expand Up @@ -113,6 +113,7 @@ export interface GardenOpts {
plugins?: RegisterPluginParam[]
sessionId?: string
noEnterprise?: boolean
variables?: PrimitiveMap
}

export interface GardenEnterpriseContext {
Expand Down Expand Up @@ -302,13 +303,16 @@ export class Garden {
environmentStr = defaultEnvironment
}

const { environmentName, namespace, providers, variables, production } = await pickEnvironment({
let { environmentName, namespace, providers, variables, production } = await pickEnvironment({
projectConfig: config,
envString: environmentStr,
artifactsPath,
username: _username,
})

// Allow overriding variables
variables = { ...variables, ...(opts.variables || {}) }

const buildDir = await BuildDir.factory(projectRoot, gardenDirPath)
const workingCopyId = await getWorkingCopyId(gardenDirPath)
const log = opts.log || getLogger().placeholder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { expect } from "chai"
import { TestGarden, makeTestGardenA, withDefaultGlobalOpts } from "../../../helpers"
import { deployOpts, deployArgs, DeployCommand } from "../../../../src/commands/deploy"
import { parseCliArgs } from "../../../../src/commands/base"
import { parseCliArgs, StringsParameter } from "../../../../src/commands/base"
import { LogEntry } from "../../../../src/logger/log-entry"
import { DeleteServiceCommand, deleteServiceArgs } from "../../../../src/commands/delete"
import { GetOutputsCommand } from "../../../../src/commands/get/get-outputs"
Expand Down Expand Up @@ -145,3 +145,19 @@ describe("parseCliArgs", () => {
})
})
})

describe("StringsParameter", () => {
it("should by default split on a comma", () => {
const param = new StringsParameter({ help: "" })
expect(param.parseString("service-a,service-b")).to.eql(["service-a", "service-b"])
})

it("should not split on commas within double-quoted strings", () => {
const param = new StringsParameter({ help: "" })
expect(param.parseString('key-a="comma,in,value",key-b=foo,key-c=bar')).to.eql([
'key-a="comma,in,value"',
"key-b=foo",
"key-c=bar",
])
})
})
19 changes: 19 additions & 0 deletions garden-service/test/unit/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ describe("cli", () => {
expect(result).to.eql({ args: { _: ["-v", "--flag", "arg"] } })
})

it("should correctly parse --var flag", async () => {
class TestCommand extends Command {
name = "test-command-var"
help = "halp!"
noProject = true

async action({ garden }) {
return { result: { variables: garden.variables } }
}
}

const command = new TestCommand()
const cli = new GardenCli()
cli.addCommand(command, cli["program"])

const { result } = await cli.parse(["test-command-var", "--var", 'key-a=value-a,key-b="value with quotes"'])
expect(result).to.eql({ variables: { "key-a": "value-a", "key-b": "value with quotes" } })
})

it(`should configure a dummy environment when command has noProject=true and --env is specified`, async () => {
class TestCommand2 extends Command {
name = "test-command-2"
Expand Down
22 changes: 22 additions & 0 deletions garden-service/test/unit/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,28 @@ describe("Garden", () => {
)
)
})

it("should optionally override project variables", async () => {
const config: ProjectConfig = {
apiVersion: DEFAULT_API_VERSION,
kind: "Project",
name: "test",
path: pathFoo,
defaultEnvironment: "default",
dotIgnoreFiles: [],
environments: [{ name: "default", defaultNamespace: "foo", variables: {} }],
providers: [{ name: "foo" }],
variables: { foo: "default", bar: "something" },
}

const garden = await TestGarden.factory(pathFoo, {
config,
environmentName: "default",
variables: { foo: "override" },
})

expect(garden.variables).to.eql({ foo: "override", bar: "something" })
})
})

describe("getPlugins", () => {
Expand Down

0 comments on commit 5ec3fd5

Please sign in to comment.