diff --git a/docs/README.md b/docs/README.md index 47d08f8baa..91dcd8a143 100644 --- a/docs/README.md +++ b/docs/README.md @@ -45,6 +45,7 @@ * [`conftest-container`](./reference/providers/conftest-container.md) * [`conftest-kubernetes`](./reference/providers/conftest-kubernetes.md) * [`conftest`](./reference/providers/conftest.md) + * [`exec`](./reference/providers/exec.md) * [`hadolint`](./reference/providers/hadolint.md) * [`kubernetes`](./reference/providers/kubernetes.md) * [`local-kubernetes`](./reference/providers/local-kubernetes.md) diff --git a/docs/reference/providers/README.md b/docs/reference/providers/README.md index 05568c3812..0e5e0a96d0 100644 --- a/docs/reference/providers/README.md +++ b/docs/reference/providers/README.md @@ -9,6 +9,7 @@ title: Providers * [`conftest-container`](./conftest-container.md) * [`kubernetes`](./kubernetes.md) * [`conftest-kubernetes`](./conftest-kubernetes.md) +* [`exec`](./exec.md) * [`hadolint`](./hadolint.md) * [`local-kubernetes`](./local-kubernetes.md) * [`maven-container`](./maven-container.md) diff --git a/docs/reference/providers/exec.md b/docs/reference/providers/exec.md new file mode 100644 index 0000000000..428d1d0abf --- /dev/null +++ b/docs/reference/providers/exec.md @@ -0,0 +1,94 @@ +--- +title: "`exec` Provider" +tocTitle: "`exec`" +--- + +# `exec` Provider + +## Description + +A simple provider that allows running arbitary scripts when initializing providers, and provides the exec +module type. + +_Note: This provider is always loaded when running Garden. You only need to explicitly declare it in your provider +configuration if you want to configure a script for it to run._ + +Below is the full schema reference for the provider configuration. For an introduction to configuring a Garden project with providers, please look at our [configuration guide](../../guides/configuration-files.md). + +The reference is divided into two sections. The [first section](#complete-yaml-schema) contains the complete YAML schema, and the [second section](#configuration-keys) describes each schema key. + +## Complete YAML Schema + +The values in the schema below are the default values. + +```yaml +providers: + - # The name of the provider plugin to use. + name: + + # If specified, this provider will only be used in the listed environments. Note that an empty array effectively + # disables the provider. To use a provider in all environments, omit this field. + environments: + + # An optional script to run in the project root when initializing providers. This is handy for running an + # arbitrary + # script when initializing. For example, another provider might declare a dependency on this provider, to ensure + # this script runs before resolving that provider. + initScript: +``` +## Configuration Keys + +### `providers[]` + +| Type | Default | Required | +| --------------- | ------- | -------- | +| `array[object]` | `[]` | No | + +### `providers[].name` + +[providers](#providers) > name + +The name of the provider plugin to use. + +| Type | Required | +| -------- | -------- | +| `string` | Yes | + +Example: + +```yaml +providers: + - name: "local-kubernetes" +``` + +### `providers[].environments[]` + +[providers](#providers) > environments + +If specified, this provider will only be used in the listed environments. Note that an empty array effectively disables the provider. To use a provider in all environments, omit this field. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | No | + +Example: + +```yaml +providers: + - environments: + - dev + - stage +``` + +### `providers[].initScript` + +[providers](#providers) > initScript + +An optional script to run in the project root when initializing providers. This is handy for running an arbitrary +script when initializing. For example, another provider might declare a dependency on this provider, to ensure +this script runs before resolving that provider. + +| Type | Required | +| -------- | -------- | +| `string` | No | + diff --git a/garden-service/src/commands/run/workflow.ts b/garden-service/src/commands/run/workflow.ts index f8a5ff360e..a2e5e15b5d 100644 --- a/garden-service/src/commands/run/workflow.ts +++ b/garden-service/src/commands/run/workflow.ts @@ -21,8 +21,9 @@ import { ConfigurationError, FilesystemError } from "../../exceptions" import { posix, join } from "path" import { ensureDir, writeFile } from "fs-extra" import Bluebird from "bluebird" -import { splitStream, getDurationMsec } from "../../util/util" -import execa, { ExecaError } from "execa" +import { getDurationMsec } from "../../util/util" +import { runScript } from "../../util/util" +import { ExecaError } from "execa" import { LogLevel } from "../../logger/log-node" const runWorkflowArgs = { @@ -358,35 +359,8 @@ class WorkflowScriptError extends GardenBaseError { } export async function runStepScript({ garden, log, step }: RunStepParams): Promise> { - // Run the script, capturing any errors - const proc = execa("bash", ["-s"], { - all: true, - cwd: garden.projectRoot, - // The script is piped to stdin - input: step.script, - // Set a very large max buffer (we only hold one of these at a time, and want to avoid overflow errors) - buffer: true, - maxBuffer: 100 * 1024 * 1024, - }) - - // Stream output to `log`, splitting by line - const stdout = splitStream() - const stderr = splitStream() - - stdout.on("error", () => {}) - stdout.on("data", (line: Buffer) => { - log.info(line.toString()) - }) - stderr.on("error", () => {}) - stderr.on("data", (line: Buffer) => { - log.info(line.toString()) - }) - - proc.stdout!.pipe(stdout) - proc.stderr!.pipe(stderr) - try { - await proc + await runScript(log, garden.projectRoot, step.script!) return { result: {} } } catch (_err) { const error = _err as ExecaError diff --git a/garden-service/src/docs/generate.ts b/garden-service/src/docs/generate.ts index eb60e43d7f..38ed3bbcd9 100644 --- a/garden-service/src/docs/generate.ts +++ b/garden-service/src/docs/generate.ts @@ -58,6 +58,7 @@ export async function writeConfigReferenceDocs(docsRoot: string) { { name: "conftest" }, { name: "conftest-container" }, { name: "conftest-kubernetes" }, + { name: "exec" }, { name: "hadolint" }, { name: "kubernetes" }, { name: "local-kubernetes" }, @@ -77,7 +78,7 @@ export async function writeConfigReferenceDocs(docsRoot: string) { const name = plugin.name // Currently nothing to document for these - if (name === "container" || name === "exec") { + if (name === "container") { continue } diff --git a/garden-service/src/plugins/exec.ts b/garden-service/src/plugins/exec.ts index 8f571a85a4..e8bf45f524 100644 --- a/garden-service/src/plugins/exec.ts +++ b/garden-service/src/plugins/exec.ts @@ -26,9 +26,11 @@ import { BuildModuleParams, BuildResult } from "../types/plugin/module/build" import { TestModuleParams } from "../types/plugin/module/testModule" import { TestResult } from "../types/plugin/module/getTestResult" import { RunTaskParams, RunTaskResult } from "../types/plugin/task/runTask" -import { exec } from "../util/util" -import { ConfigurationError } from "../exceptions" +import { exec, runScript } from "../util/util" +import { ConfigurationError, RuntimeError } from "../exceptions" import { LogEntry } from "../logger/log-entry" +import { providerConfigBaseSchema } from "../config/provider" +import { ExecaError } from "execa" const execPathDoc = dedent` By default, the command is run inside the Garden build directory (under .garden/build/). @@ -321,6 +323,20 @@ export async function runExecTask(params: RunTaskParams): Promise {}) + stdout.on("data", (line: Buffer) => { + log.info(line.toString()) + }) + stderr.on("error", () => {}) + stderr.on("data", (line: Buffer) => { + log.info(line.toString()) + }) + + proc.stdout!.pipe(stdout) + proc.stderr!.pipe(stderr) + + await proc +} diff --git a/garden-service/test/unit/src/actions.ts b/garden-service/test/unit/src/actions.ts index 2a93121bca..3437628974 100644 --- a/garden-service/test/unit/src/actions.ts +++ b/garden-service/test/unit/src/actions.ts @@ -803,7 +803,7 @@ describe("ActionRouter", () => { it("should return all handlers for a type", async () => { const handlers = await actions["getActionHandlers"]("prepareEnvironment") - expect(Object.keys(handlers)).to.eql(["test-plugin", "test-plugin-b"]) + expect(Object.keys(handlers)).to.eql(["exec", "test-plugin", "test-plugin-b"]) }) }) diff --git a/garden-service/test/unit/src/plugins/exec.ts b/garden-service/test/unit/src/plugins/exec.ts index 054ffddde6..88f81a7d64 100644 --- a/garden-service/test/unit/src/plugins/exec.ts +++ b/garden-service/test/unit/src/plugins/exec.ts @@ -10,7 +10,7 @@ import { expect } from "chai" import { join, resolve } from "path" import { Garden } from "../../../../src/garden" import { gardenPlugin, configureExecModule } from "../../../../src/plugins/exec" -import { GARDEN_BUILD_VERSION_FILENAME } from "../../../../src/constants" +import { GARDEN_BUILD_VERSION_FILENAME, DEFAULT_API_VERSION } from "../../../../src/constants" import { LogEntry } from "../../../../src/logger/log-entry" import { keyBy } from "lodash" import { getDataDir, makeTestModule, expectError } from "../../../helpers" @@ -22,6 +22,8 @@ import { ConfigGraph } from "../../../../src/config-graph" import { pathExists, emptyDir } from "fs-extra" import { TestTask } from "../../../../src/tasks/test" import { findByName } from "../../../../src/util/util" +import { defaultNamespace } from "../../../../src/config/project" +import { readFile } from "fs-extra" describe("exec plugin", () => { const projectRoot = resolve(dataDir, "test-project-exec") @@ -38,6 +40,48 @@ describe("exec plugin", () => { await garden.clearBuilds() }) + it("should run a script on init in the project root, if configured", async () => { + const _garden = await Garden.factory(garden.projectRoot, { + plugins: [], + config: { + apiVersion: DEFAULT_API_VERSION, + kind: "Project", + name: "test", + path: garden.projectRoot, + defaultEnvironment: "default", + dotIgnoreFiles: [], + environments: [{ name: "default", defaultNamespace, variables: {} }], + providers: [{ name: "exec", initScript: "echo hello! > .garden/test.txt" }], + variables: {}, + }, + }) + + await _garden.resolveProviders(_garden.log) + + const f = await readFile(join(garden.projectRoot, ".garden", "test.txt")) + + expect(f.toString().trim()).to.equal("hello!") + }) + + it("should throw if a script configured and exits with a non-zero code", async () => { + const _garden = await Garden.factory(garden.projectRoot, { + plugins: [], + config: { + apiVersion: DEFAULT_API_VERSION, + kind: "Project", + name: "test", + path: garden.projectRoot, + defaultEnvironment: "default", + dotIgnoreFiles: [], + environments: [{ name: "default", defaultNamespace, variables: {} }], + providers: [{ name: "exec", initScript: "echo oh no!; exit 1" }], + variables: {}, + }, + }) + + await expectError(() => _garden.resolveProviders(_garden.log), "plugin") + }) + it("should correctly parse exec modules", async () => { const modules = keyBy(graph.getModules(), "name") const { "module-a": moduleA, "module-b": moduleB, "module-c": moduleC, "module-local": moduleLocal } = modules