diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 13bf828e70..35a8939c01 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -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 diff --git a/docs/using-garden/tasks.md b/docs/using-garden/tasks.md index f9ded3158d..dbc57d6891 100644 --- a/docs/using-garden/tasks.md +++ b/docs/using-garden/tasks.md @@ -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. diff --git a/docs/using-garden/variables-and-templating.md b/docs/using-garden/variables-and-templating.md index 6ec2865f08..8b1789cf75 100644 --- a/docs/using-garden/variables-and-templating.md +++ b/docs/using-garden/variables-and-templating.md @@ -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` 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` 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`). +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: diff --git a/garden-service/src/cli/cli.ts b/garden-service/src/cli/cli.ts index 0407f989cf..8b3d6a7667 100644 --- a/garden-service/src/cli/cli.ts +++ b/garden-service/src/cli/cli.ts @@ -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" @@ -22,6 +23,7 @@ import { EnvironmentOption, Parameter, StringParameter, + StringsParameter, } from "../commands/base" import { GardenError, PluginError, toGardenError } from "../exceptions" import { Garden, GardenOpts, DummyGarden } from "../garden" @@ -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 =, 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 @@ -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) { @@ -318,6 +328,7 @@ export class GardenCli { log, sessionId, forceRefresh, + variables: parsedCliVars, } let garden: Garden diff --git a/garden-service/src/commands/base.ts b/garden-service/src/commands/base.ts index ec51a49e3b..1d6c954511 100644 --- a/garden-service/src/commands/base.ts +++ b/garden-service/src/commands/base.ts @@ -116,12 +116,13 @@ export interface StringsConstructor extends ParameterConstructor { export class StringsParameter extends Parameter { 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. diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index 2aca27759f..a87e124872 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -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" @@ -113,6 +113,7 @@ export interface GardenOpts { plugins?: RegisterPluginParam[] sessionId?: string noEnterprise?: boolean + variables?: PrimitiveMap } export interface GardenEnterpriseContext { @@ -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() diff --git a/garden-service/test/unit/src/cli/parseCliArgs.ts b/garden-service/test/unit/src/cli/base.ts similarity index 89% rename from garden-service/test/unit/src/cli/parseCliArgs.ts rename to garden-service/test/unit/src/cli/base.ts index 28278d13c8..c460e89a06 100644 --- a/garden-service/test/unit/src/cli/parseCliArgs.ts +++ b/garden-service/test/unit/src/cli/base.ts @@ -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" @@ -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", + ]) + }) +}) diff --git a/garden-service/test/unit/src/cli/cli.ts b/garden-service/test/unit/src/cli/cli.ts index d2d4483047..a16883bdf2 100644 --- a/garden-service/test/unit/src/cli/cli.ts +++ b/garden-service/test/unit/src/cli/cli.ts @@ -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" diff --git a/garden-service/test/unit/src/garden.ts b/garden-service/test/unit/src/garden.ts index 00e346e94f..ad334b4cf1 100644 --- a/garden-service/test/unit/src/garden.ts +++ b/garden-service/test/unit/src/garden.ts @@ -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", () => {