From bd8727185a6f3ce5f826830a3dd61bdf77b5b1e9 Mon Sep 17 00:00:00 2001 From: Jon Edvald Date: Fri, 26 Jun 2020 16:40:42 +0200 Subject: [PATCH] feat(config): allow environment[].variables to reference top-level vars (#1910) * feat(config): allow environment[].variables to reference top-level vars Also fixes the issue where template strings were resolved in all environments, instead of just the selected one. Fixes #1814 --- docs/reference/config.md | 5 +- docs/reference/template-strings.md | 338 ++++++++++++------ garden-service/src/config/common.ts | 2 +- garden-service/src/config/config-context.ts | 58 ++- garden-service/src/config/project.ts | 71 +++- garden-service/src/docs/template-strings.ts | 14 +- garden-service/src/garden.ts | 10 +- .../docs/templates/template-strings.hbs | 8 +- .../test/unit/src/config/project.ts | 210 +++++++++-- 9 files changed, 543 insertions(+), 173 deletions(-) diff --git a/docs/reference/config.md b/docs/reference/config.md index 22fbfb21bc..dddb2455d5 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -65,7 +65,8 @@ environments: varfile: # A key/value map of variables that modules can reference when using this environment. These take precedence over - # variables defined in the top-level `variables` field. + # variables defined in the top-level `variables` field, but may also reference the top-level variables in template + # strings. variables: {} # A list of providers that should be used for this project, and their configuration. Please refer to individual @@ -330,7 +331,7 @@ environments: [environments](#environments) > variables -A key/value map of variables that modules can reference when using this environment. These take precedence over variables defined in the top-level `variables` field. +A key/value map of variables that modules can reference when using this environment. These take precedence over variables defined in the top-level `variables` field, but may also reference the top-level variables in template strings. | Type | Default | Required | | -------- | ------- | -------- | diff --git a/docs/reference/template-strings.md b/docs/reference/template-strings.md index ae2a237b87..f4957621f6 100644 --- a/docs/reference/template-strings.md +++ b/docs/reference/template-strings.md @@ -11,7 +11,7 @@ Note that there are four sections below, since different configuration sections ## Project configuration context -The following keys are available in any template strings within project definitions in `garden.yml` config files, except the `name` field (which cannot be templated). See the [Provider](#provider-configuration-context) section below for additional keys available when configuring `providers`: +The following keys are available in any template strings within project definitions in `garden.yml` config files, except the `name` field (which cannot be templated). See the [Environment](#environment-configuration-context) and [Provider](#provider-configuration-context) sections below for additional keys available when configuring `environments` and `providers`, respectively. ### `${local.*}` @@ -103,11 +103,9 @@ my-variable: ${project.name} ``` -## Provider configuration context - -The following keys are available in template strings under the `providers` key (or `environments[].providers) in project configs. +## Environment configuration context -Providers can also reference outputs defined by other providers, via the `${providers..outputs}` key. For details on which outputs are available for a given provider, please refer to the [reference](https://docs.garden.io/reference/providers) docs for the provider in question, and look for the _Outputs_ section. +The following keys are available in template strings under the `environments` key in project configs. Additional keys are available for the `environments[].providers` field, see the [Provider](#provider-configuration-context) section below for those. ### `${local.*}` @@ -198,17 +196,55 @@ Example: my-variable: ${project.name} ``` -### `${environment.*}` +### `${variables.*}` + +A map of all variables defined in the project configuration. + +| Type | Default | +| -------- | ------- | +| `object` | `{}` | + +### `${variables.}` + +| Type | +| ------------------------------------------------ | +| `number | string | boolean | link | array[link]` | + +### `${var.*}` + +Alias for the variables field. + +| Type | Default | +| -------- | ------- | +| `object` | `{}` | + +### `${var.}` + +Number, string or boolean + +| Type | +| --------------------------- | +| `number | string | boolean` | + + +## Provider configuration context + +The following keys are available in template strings under the `providers` key (or `environments[].providers) in project configs. + +Providers can also reference outputs defined by other providers, via the `${providers..outputs}` key. For details on which outputs are available for a given provider, please refer to the [reference](https://docs.garden.io/reference/providers) docs for the provider in question, and look for the _Outputs_ section. -Information about the environment that Garden is running against. + +### `${local.*}` + +Context variables that are specific to the currently running environment/machine. | Type | | -------- | | `object` | -### `${environment.name}` +### `${local.artifactsPath}` -The name of the environment Garden is running against, excluding the namespace. +The absolute path to the directory where exported artifacts from test and task runs are stored. | Type | | -------- | @@ -217,12 +253,28 @@ The name of the environment Garden is running against, excluding the namespace. Example: ```yaml -my-variable: ${environment.name} +my-variable: ${local.artifactsPath} ``` -### `${environment.fullName}` +### `${local.env.*}` -The full name of the environment Garden is running against, including the namespace. +A map of all local environment variables (see https://nodejs.org/api/process.html#process_process_env). + +| Type | +| -------- | +| `object` | + +### `${local.env.}` + +The environment variable value. + +| Type | +| -------- | +| `string` | + +### `${local.platform}` + +A string indicating the platform that the framework is running on (see https://nodejs.org/api/process.html#process_process_platform) | Type | | -------- | @@ -231,12 +283,12 @@ The full name of the environment Garden is running against, including the namesp Example: ```yaml -my-variable: ${environment.fullName} +my-variable: ${local.platform} ``` -### `${environment.namespace}` +### `${local.username}` -The currently active namespace (if any). +The current username (as resolved by https://github.com/sindresorhus/username) | Type | | -------- | @@ -245,12 +297,34 @@ The currently active namespace (if any). Example: ```yaml -my-variable: ${environment.namespace} +my-variable: ${local.username} +``` + +### `${project.*}` + +Information about the Garden project. + +| Type | +| -------- | +| `object` | + +### `${project.name}` + +The name of the Garden project. + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${project.name} ``` ### `${variables.*}` -A map of all variables defined in the project configuration. +A map of all variables defined in the project configuration, including environment-specific variables. | Type | Default | | -------- | ------- | @@ -278,6 +352,56 @@ Number, string or boolean | --------------------------- | | `number | string | boolean` | +### `${environment.*}` + +Information about the environment that Garden is running against. + +| Type | +| -------- | +| `object` | + +### `${environment.name}` + +The name of the environment Garden is running against, excluding the namespace. + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${environment.name} +``` + +### `${environment.fullName}` + +The full name of the environment Garden is running against, including the namespace. + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${environment.fullName} +``` + +### `${environment.namespace}` + +The currently active namespace (if any). + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${environment.namespace} +``` + ### `${providers.*}` Retrieve information about providers that are defined in the project. @@ -417,6 +541,36 @@ Example: my-variable: ${project.name} ``` +### `${variables.*}` + +A map of all variables defined in the project configuration, including environment-specific variables. + +| Type | Default | +| -------- | ------- | +| `object` | `{}` | + +### `${variables.}` + +| Type | +| ------------------------------------------------ | +| `number | string | boolean | link | array[link]` | + +### `${var.*}` + +Alias for the variables field. + +| Type | Default | +| -------- | ------- | +| `object` | `{}` | + +### `${var.}` + +Number, string or boolean + +| Type | +| --------------------------- | +| `number | string | boolean` | + ### `${environment.*}` Information about the environment that Garden is running against. @@ -467,36 +621,6 @@ Example: my-variable: ${environment.namespace} ``` -### `${variables.*}` - -A map of all variables defined in the project configuration. - -| Type | Default | -| -------- | ------- | -| `object` | `{}` | - -### `${variables.}` - -| Type | -| ------------------------------------------------ | -| `number | string | boolean | link | array[link]` | - -### `${var.*}` - -Alias for the variables field. - -| Type | Default | -| -------- | ------- | -| `object` | `{}` | - -### `${var.}` - -Number, string or boolean - -| Type | -| --------------------------- | -| `number | string | boolean` | - ### `${providers.*}` Retrieve information about providers that are defined in the project. @@ -758,6 +882,36 @@ Example: my-variable: ${project.name} ``` +### `${variables.*}` + +A map of all variables defined in the project configuration, including environment-specific variables. + +| Type | Default | +| -------- | ------- | +| `object` | `{}` | + +### `${variables.}` + +| Type | +| ------------------------------------------------ | +| `number | string | boolean | link | array[link]` | + +### `${var.*}` + +Alias for the variables field. + +| Type | Default | +| -------- | ------- | +| `object` | `{}` | + +### `${var.}` + +Number, string or boolean + +| Type | +| --------------------------- | +| `number | string | boolean` | + ### `${environment.*}` Information about the environment that Garden is running against. @@ -808,36 +962,6 @@ Example: my-variable: ${environment.namespace} ``` -### `${variables.*}` - -A map of all variables defined in the project configuration. - -| Type | Default | -| -------- | ------- | -| `object` | `{}` | - -### `${variables.}` - -| Type | -| ------------------------------------------------ | -| `number | string | boolean | link | array[link]` | - -### `${var.*}` - -Alias for the variables field. - -| Type | Default | -| -------- | ------- | -| `object` | `{}` | - -### `${var.}` - -Number, string or boolean - -| Type | -| --------------------------- | -| `number | string | boolean` | - ### `${providers.*}` Retrieve information about providers that are defined in the project. @@ -1096,6 +1220,36 @@ Example: my-variable: ${project.name} ``` +### `${variables.*}` + +A map of all variables defined in the project configuration, including environment-specific variables. + +| Type | Default | +| -------- | ------- | +| `object` | `{}` | + +### `${variables.}` + +| Type | +| ------------------------------------------------ | +| `number | string | boolean | link | array[link]` | + +### `${var.*}` + +Alias for the variables field. + +| Type | Default | +| -------- | ------- | +| `object` | `{}` | + +### `${var.}` + +Number, string or boolean + +| Type | +| --------------------------- | +| `number | string | boolean` | + ### `${environment.*}` Information about the environment that Garden is running against. @@ -1146,36 +1300,6 @@ Example: my-variable: ${environment.namespace} ``` -### `${variables.*}` - -A map of all variables defined in the project configuration. - -| Type | Default | -| -------- | ------- | -| `object` | `{}` | - -### `${variables.}` - -| Type | -| ------------------------------------------------ | -| `number | string | boolean | link | array[link]` | - -### `${var.*}` - -Alias for the variables field. - -| Type | Default | -| -------- | ------- | -| `object` | `{}` | - -### `${var.}` - -Number, string or boolean - -| Type | -| --------------------------- | -| `number | string | boolean` | - ### `${steps.*}` Reference previous steps in a workflow. Only available in the `steps[].command` and `steps[].script` fields. diff --git a/garden-service/src/config/common.ts b/garden-service/src/config/common.ts index 97de4b2fe6..e5673646ea 100644 --- a/garden-service/src/config/common.ts +++ b/garden-service/src/config/common.ts @@ -362,7 +362,7 @@ export const identifierRegex = /^(?![0-9]+$)(?!.*-$)(?!-)[a-z0-9-]{1,63}$/ export const userIdentifierRegex = /^(?!garden)(?=.{1,63}$)[a-z][a-z0-9]*(-[a-z0-9]+)*$/ export const envVarRegex = /^(?!garden)[a-z_][a-z0-9_\.]*$/i export const gitUrlRegex = /(?:git|ssh|https?|git@[-\w.]+):(\/\/)?(.*?)(\/?|\#[-\d\w._\/]+?)$/ -export const variableNameRegex = /[a-zA-Z][a-zA-Z0-9_\-]+/i +export const variableNameRegex = /[a-zA-Z][a-zA-Z0-9_\-]*/i export const joiIdentifierDescription = "valid RFC1035/RFC1123 (DNS) label (may contain lowercase letters, numbers and dashes, must start with a letter, " + diff --git a/garden-service/src/config/config-context.ts b/garden-service/src/config/config-context.ts index dbb0b227cc..1c344b5613 100644 --- a/garden-service/src/config/config-context.ts +++ b/garden-service/src/config/config-context.ts @@ -307,6 +307,46 @@ export class ProjectConfigContext extends ConfigContext { } } +/** + * This context is available for template strings for all `environments[]` fields (except name) + */ +export class EnvironmentConfigContext extends ProjectConfigContext { + @schema( + LocalContext.getSchema().description( + "Context variables that are specific to the currently running environment/machine." + ) + ) + public local: LocalContext + + @schema(ProjectContext.getSchema().description("Information about the Garden project.")) + public project: ProjectContext + + @schema( + joiVariables() + .description("A map of all variables defined in the project configuration.") + .meta({ keyPlaceholder: "" }) + ) + public variables: DeepPrimitiveMap + + @schema(joiIdentifierMap(joiPrimitive()).description("Alias for the variables field.")) + public var: DeepPrimitiveMap + + constructor({ + projectName, + artifactsPath, + username, + variables, + }: { + projectName: string + artifactsPath: string + username?: string + variables: DeepPrimitiveMap + }) { + super({ projectName, artifactsPath, username }) + this.variables = this.var = variables + } +} + class EnvironmentContext extends ConfigContext { @schema( joi @@ -342,22 +382,22 @@ class EnvironmentContext extends ConfigContext { } } -export class WorkflowConfigContext extends ProjectConfigContext { +export class WorkflowConfigContext extends EnvironmentConfigContext { @schema( EnvironmentContext.getSchema().description("Information about the environment that Garden is running against.") ) public environment: EnvironmentContext + // Overriding to update the description. Same schema as base. @schema( joiVariables() - .description("A map of all variables defined in the project configuration.") + .description( + "A map of all variables defined in the project configuration, including environment-specific variables." + ) .meta({ keyPlaceholder: "" }) ) public variables: DeepPrimitiveMap - @schema(joiIdentifierMap(joiPrimitive()).description("Alias for the variables field.")) - public var: DeepPrimitiveMap - @schema( joiStringMap(joi.string().description("The secret's value.")) .description("A map of all secrets for this project in the current environment.") @@ -372,14 +412,18 @@ export class WorkflowConfigContext extends ProjectConfigContext { public steps: Map | PassthroughContext constructor(garden: Garden) { - super({ projectName: garden.projectName, artifactsPath: garden.artifactsPath, username: garden.username }) + super({ + projectName: garden.projectName, + artifactsPath: garden.artifactsPath, + username: garden.username, + variables: garden.variables, + }) const fullEnvName = garden.namespace ? `${garden.namespace}.${garden.environmentName}` : garden.environmentName this.environment = new EnvironmentContext(this, garden.environmentName, fullEnvName, garden.namespace) this.project = new ProjectContext(this, garden.projectName) - this.var = this.variables = garden.variables this.secrets = garden.secrets this.steps = new PassthroughContext() diff --git a/garden-service/src/config/project.ts b/garden-service/src/config/project.ts index 40fa183f33..410c240983 100644 --- a/garden-service/src/config/project.ts +++ b/garden-service/src/config/project.ts @@ -24,7 +24,7 @@ import { } from "./common" import { validateWithPath } from "./validation" import { resolveTemplateStrings } from "../template-string" -import { ProjectConfigContext } from "./config-context" +import { ProjectConfigContext, EnvironmentConfigContext } from "./config-context" import { findByName, getNames } from "../util/util" import { ConfigurationError, ParameterError, ValidationError } from "../exceptions" import { PrimitiveMap } from "./common" @@ -117,7 +117,8 @@ export const environmentSchema = () => .example("custom.env"), variables: joiVariables().description(deline` A key/value map of variables that modules can reference when using this environment. These take precedence - over variables defined in the top-level \`variables\` field. + over variables defined in the top-level \`variables\` field, but may also reference the top-level variables in + template strings. `), }) @@ -405,7 +406,7 @@ export const projectSchema = () => /** * Resolves and validates the given raw project configuration, and returns it in a canonical form. * - * Note: Does _not_ resolve template strings on providers (this needs to happen later in the process). + * Note: Does _not_ resolve template strings on environments and providers (this needs to happen later in the process). * * @param config raw project configuration */ @@ -420,14 +421,19 @@ export function resolveProjectConfig(config: ProjectConfig, artifactsPath: strin sources: config.sources, varfile: config.varfile, variables: config.variables, - environments: environments.map((e) => omit(e, ["providers"])), + environments: [], }, new ProjectConfigContext({ projectName: name, artifactsPath, username }) ) // Validate after resolving global fields config = validateWithPath({ - config: { ...config, ...globalConfig, name }, + config: { + ...config, + ...globalConfig, + name, + environments: [], + }, schema: projectSchema(), configType: "project", path: config.path, @@ -449,13 +455,13 @@ export function resolveProjectConfig(config: ProjectConfig, artifactsPath: strin environment.providers = [] } - const variables = config.variables + // This will be validated separately, after resolving templates + config.environments = environments.map((e) => omit(e, ["providers"])) config = { ...config, environments: config.environments || [], providers, - variables, } // TODO: get rid of the default environment config @@ -480,7 +486,7 @@ export function resolveProjectConfig(config: ProjectConfig, artifactsPath: strin /** * Given an environment name, pulls the relevant environment-specific configuration from the specified project - * config, and merges values appropriately. + * config, and merges values appropriately. Also resolves template strings in the picked environment. * * For project variables, we apply the variables specified to the selected environment on the global variables * specified on the top-level `variables` key using a JSON Merge Patch (https://tools.ietf.org/html/rfc7396). @@ -500,12 +506,22 @@ export function resolveProjectConfig(config: ProjectConfig, artifactsPath: strin * @param config a resolved project config (as returned by `resolveProjectConfig()`) * @param envString the name of the environment to use */ -export async function pickEnvironment(config: ProjectConfig, envString: string) { - const { environments, name: projectName } = config +export async function pickEnvironment({ + projectConfig, + envString, + artifactsPath, + username, +}: { + projectConfig: ProjectConfig + envString: string + artifactsPath: string + username: string +}) { + const { environments, name: projectName } = projectConfig let { environment, namespace } = parseEnvironment(envString) - const environmentConfig = findByName(environments, environment) + let environmentConfig = findByName(environments, environment) if (!environmentConfig) { throw new ParameterError(`Project ${projectName} does not specify environment ${environment}`, { @@ -516,12 +532,32 @@ export async function pickEnvironment(config: ProjectConfig, envString: string) }) } + const projectVarfileVars = await loadVarfile(projectConfig.path, projectConfig.varfile, defaultVarfilePath) + const projectVariables: DeepPrimitiveMap = merge(projectConfig.variables, projectVarfileVars) + + const envProviders = environmentConfig.providers || [] + + // Resolve template strings in the environment config, except providers + environmentConfig = resolveTemplateStrings( + { ...environmentConfig, providers: [] }, + new EnvironmentConfigContext({ projectName, artifactsPath, username, variables: projectVariables }) + ) + + environmentConfig = validateWithPath({ + config: environmentConfig, + schema: environmentSchema(), + configType: `environment ${environment}`, + path: projectConfig.path, + projectRoot: projectConfig.path, + }) + namespace = getNamespace(environmentConfig, namespace) const fixedProviders = fixedPlugins.map((name) => ({ name })) const allProviders = [ ...fixedProviders, - ...config.providers.filter((p) => !p.environments || p.environments.includes(environment)), + ...projectConfig.providers.filter((p) => !p.environments || p.environments.includes(environment)), + ...envProviders, ] const mergedProviders: { [name: string]: ProviderConfig } = {} @@ -535,13 +571,14 @@ export async function pickEnvironment(config: ProjectConfig, envString: string) } } - const projectVarfileVars = await loadVarfile(config.path, config.varfile, defaultVarfilePath) - const envVarfileVars = await loadVarfile(config.path, environmentConfig.varfile, defaultEnvVarfilePath(environment)) - - const variables: DeepPrimitiveMap = ( - merge(merge(config.variables, projectVarfileVars), merge(environmentConfig.variables, envVarfileVars)) + const envVarfileVars = await loadVarfile( + projectConfig.path, + environmentConfig.varfile, + defaultEnvVarfilePath(environment) ) + const variables: DeepPrimitiveMap = merge(projectVariables, merge(environmentConfig.variables, envVarfileVars)) + return { environmentName: environment, namespace, diff --git a/garden-service/src/docs/template-strings.ts b/garden-service/src/docs/template-strings.ts index 880e9f498c..037f923f73 100644 --- a/garden-service/src/docs/template-strings.ts +++ b/garden-service/src/docs/template-strings.ts @@ -14,6 +14,7 @@ import { ProviderConfigContext, OutputConfigContext, WorkflowStepConfigContext, + EnvironmentConfigContext, } from "../config/config-context" import { readFileSync, writeFileSync } from "fs" import handlebars from "handlebars" @@ -31,6 +32,10 @@ export function writeTemplateStringReferenceDocs(docsRoot: string) { schema: ProviderConfigContext.getSchema().required(), }) + const environmentContext = renderTemplateStringReference({ + schema: EnvironmentConfigContext.getSchema().required(), + }) + const moduleContext = renderTemplateStringReference({ schema: ModuleConfigContext.getSchema().required(), }) @@ -45,7 +50,14 @@ export function writeTemplateStringReferenceDocs(docsRoot: string) { const templatePath = resolve(TEMPLATES_DIR, "template-strings.hbs") const template = handlebars.compile(readFileSync(templatePath).toString()) - const markdown = template({ projectContext, providerContext, moduleContext, outputContext, workflowContext }) + const markdown = template({ + projectContext, + environmentContext, + providerContext, + moduleContext, + outputContext, + workflowContext, + }) writeFileSync(outputPath, markdown) } diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index 438c3382cd..79bfc37fb8 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -302,10 +302,12 @@ export class Garden { environmentStr = defaultEnvironment } - const { environmentName, namespace, providers, variables, production } = await pickEnvironment( - config, - environmentStr - ) + const { environmentName, namespace, providers, variables, production } = await pickEnvironment({ + projectConfig: config, + envString: environmentStr, + artifactsPath, + username: _username, + }) const buildDir = await BuildDir.factory(projectRoot, gardenDirPath) const workingCopyId = await getWorkingCopyId(gardenDirPath) diff --git a/garden-service/static/docs/templates/template-strings.hbs b/garden-service/static/docs/templates/template-strings.hbs index 757e576cbe..ee58594301 100644 --- a/garden-service/static/docs/templates/template-strings.hbs +++ b/garden-service/static/docs/templates/template-strings.hbs @@ -11,10 +11,16 @@ Note that there are four sections below, since different configuration sections ## Project configuration context -The following keys are available in any template strings within project definitions in `garden.yml` config files, except the `name` field (which cannot be templated). See the [Provider](#provider-configuration-context) section below for additional keys available when configuring `providers`: +The following keys are available in any template strings within project definitions in `garden.yml` config files, except the `name` field (which cannot be templated). See the [Environment](#environment-configuration-context) and [Provider](#provider-configuration-context) sections below for additional keys available when configuring `environments` and `providers`, respectively. {{{projectContext}}} +## Environment configuration context + +The following keys are available in template strings under the `environments` key in project configs. Additional keys are available for the `environments[].providers` field, see the [Provider](#provider-configuration-context) section below for those. + +{{{environmentContext}}} + ## Provider configuration context The following keys are available in template strings under the `providers` key (or `environments[].providers) in project configs. diff --git a/garden-service/test/unit/src/config/project.ts b/garden-service/test/unit/src/config/project.ts index 0871308c69..18f78e216c 100644 --- a/garden-service/test/unit/src/config/project.ts +++ b/garden-service/test/unit/src/config/project.ts @@ -24,8 +24,9 @@ import { expectError } from "../../../helpers" import { defaultDotIgnoreFiles } from "../../../../src/util/fs" import { realpath, writeFile } from "fs-extra" import { dedent } from "../../../../src/util/string" -import { resolve } from "path" +import { resolve, join } from "path" import stripAnsi from "strip-ansi" +import { keyBy } from "lodash" describe("resolveProjectConfig", () => { it("should pass through a canonical project config", async () => { @@ -48,8 +49,6 @@ describe("resolveProjectConfig", () => { { name: "default", defaultNamespace, - production: false, - providers: [], variables: {}, }, ], @@ -80,7 +79,7 @@ describe("resolveProjectConfig", () => { }) }) - it("should resolve template strings on fields other than provider configs", async () => { + it("should resolve template strings on fields other than environment and provider configs", async () => { const repositoryUrl = "git://github.com/foo/bar.git#boo" const config: ProjectConfig = { @@ -119,10 +118,8 @@ describe("resolveProjectConfig", () => { { name: "default", defaultNamespace, - production: false, - providers: [], variables: { - envVar: "foo", + envVar: "${local.env.TEST_ENV_VAR}", }, }, ], @@ -182,8 +179,6 @@ describe("resolveProjectConfig", () => { { name: "default", defaultNamespace, - production: false, - providers: [], variables: { envVar: "foo", }, @@ -209,6 +204,32 @@ describe("resolveProjectConfig", () => { delete process.env.TEST_ENV_VAR_B }) + it("should pass through templated fields on environment configs", async () => { + const config: ProjectConfig = { + apiVersion: DEFAULT_API_VERSION, + kind: "Project", + name: "my-project", + path: "/tmp/foo", + defaultEnvironment: "default", + dotIgnoreFiles: defaultDotIgnoreFiles, + environments: [ + { + name: "default", + defaultNamespace, + variables: { + envVar: "${var.foo}", + }, + }, + ], + providers: [], + variables: {}, + } + + const result = resolveProjectConfig(config, "/tmp", "some-user") + + expect(result.environments[0].variables).to.eql(config.environments[0].variables) + }) + it("should set defaultEnvironment to first environment if not configured", async () => { const config: ProjectConfig = { apiVersion: DEFAULT_API_VERSION, @@ -255,7 +276,7 @@ describe("resolveProjectConfig", () => { }) }) - it("should include providers in correct precedency order from all possible config keys", async () => { + it("should include providers in correct precedence order from all possible config keys", async () => { const config: ProjectConfig = { apiVersion: DEFAULT_API_VERSION, kind: "Project", @@ -294,8 +315,6 @@ describe("resolveProjectConfig", () => { { name: "default", defaultNamespace, - production: false, - providers: [], variables: { envVar: "foo", }, @@ -358,8 +377,6 @@ describe("resolveProjectConfig", () => { { name: "default", defaultNamespace, - providers: [], - production: false, variables: { envVar: "bar", }, @@ -387,10 +404,13 @@ describe("resolveProjectConfig", () => { describe("pickEnvironment", () => { let tmpDir: tmp.DirectoryResult let tmpPath: string + let artifactsPath: string + const username = "test" beforeEach(async () => { tmpDir = await tmp.dir({ unsafeCleanup: true }) tmpPath = await realpath(tmpDir.path) + artifactsPath = join(tmpPath, ".garden", "artifacts") }) afterEach(async () => { @@ -410,7 +430,10 @@ describe("pickEnvironment", () => { variables: {}, } - await expectError(() => pickEnvironment(config, "foo"), "parameter") + await expectError( + () => pickEnvironment({ projectConfig: config, envString: "foo", artifactsPath, username }), + "parameter" + ) }) it("should include fixed providers in output", async () => { @@ -426,7 +449,7 @@ describe("pickEnvironment", () => { variables: {}, } - expect(await pickEnvironment(config, "default")).to.eql({ + expect(await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username })).to.eql({ environmentName: "default", namespace: "default", providers: [{ name: "exec" }, { name: "container" }], @@ -443,7 +466,14 @@ describe("pickEnvironment", () => { path: "/tmp/foo", defaultEnvironment: "default", dotIgnoreFiles: defaultDotIgnoreFiles, - environments: [{ name: "default", defaultNamespace, variables: {} }], + environments: [ + { + name: "default", + defaultNamespace, + variables: {}, + providers: [{ name: "my-provider", b: "d" }, { name: "env-provider" }], + }, + ], providers: [ { name: "container", newKey: "foo" }, { name: "my-provider", a: "a" }, @@ -453,10 +483,15 @@ describe("pickEnvironment", () => { variables: {}, } - expect(await pickEnvironment(config, "default")).to.eql({ + expect(await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username })).to.eql({ environmentName: "default", namespace: "default", - providers: [{ name: "exec" }, { name: "container", newKey: "foo" }, { name: "my-provider", a: "c", b: "b" }], + providers: [ + { name: "exec" }, + { name: "container", newKey: "foo" }, + { name: "my-provider", a: "c", b: "d" }, + { name: "env-provider" }, + ], production: false, variables: {}, }) @@ -480,7 +515,7 @@ describe("pickEnvironment", () => { variables: {}, } - expect(await pickEnvironment(config, "default")).to.eql({ + expect(await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username })).to.eql({ environmentName: "default", namespace: "default", providers: [{ name: "exec" }, { name: "container", newKey: "foo" }, { name: "my-provider", b: "b" }], @@ -524,7 +559,7 @@ describe("pickEnvironment", () => { }, } - const result = await pickEnvironment(config, "default") + const result = await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }) expect(result.variables).to.eql({ a: "project value A", @@ -570,7 +605,7 @@ describe("pickEnvironment", () => { variables: {}, } - const result = await pickEnvironment(config, "default") + const result = await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }) expect(result.variables).to.eql({ a: "a", @@ -610,7 +645,7 @@ describe("pickEnvironment", () => { }, } - const result = await pickEnvironment(config, "default") + const result = await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }) expect(result.variables).to.eql({ a: "a", @@ -651,7 +686,7 @@ describe("pickEnvironment", () => { variables: {}, } - const result = await pickEnvironment(config, "default") + const result = await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }) expect(result.variables).to.eql({ a: "a", @@ -692,7 +727,7 @@ describe("pickEnvironment", () => { }, } - const result = await pickEnvironment(config, "default") + const result = await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }) expect(result.variables).to.eql({ a: "a", @@ -701,6 +736,85 @@ describe("pickEnvironment", () => { }) }) + it("should resolve template strings in the picked environment", async () => { + const config: ProjectConfig = { + apiVersion: DEFAULT_API_VERSION, + kind: "Project", + name: "my-project", + path: "/tmp/foo", + defaultEnvironment: "default", + dotIgnoreFiles: defaultDotIgnoreFiles, + environments: [{ name: "default", defaultNamespace, variables: { foo: "${local.username}" } }], + providers: [], + variables: {}, + } + + const result = await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }) + + expect(result.variables).to.eql({ + foo: username, + }) + }) + + it("should ignore template strings in other environments", async () => { + const config: ProjectConfig = { + apiVersion: DEFAULT_API_VERSION, + kind: "Project", + name: "my-project", + path: "/tmp/foo", + defaultEnvironment: "default", + dotIgnoreFiles: defaultDotIgnoreFiles, + environments: [ + { name: "default", defaultNamespace, variables: {} }, + { name: "other", defaultNamespace, variables: { foo: "${var.missing}" } }, + ], + providers: [], + variables: {}, + } + + await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }) + }) + + it("should pass through template strings in the providers field on environments", async () => { + const config: ProjectConfig = { + apiVersion: DEFAULT_API_VERSION, + kind: "Project", + name: "my-project", + path: "/tmp/foo", + defaultEnvironment: "default", + dotIgnoreFiles: defaultDotIgnoreFiles, + environments: [ + { name: "default", defaultNamespace, variables: {}, providers: [{ name: "my-provider", a: "${var.missing}" }] }, + ], + providers: [], + variables: {}, + } + + const result = await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }) + + expect(keyBy(result.providers, "name")["my-provider"].a).to.equal("${var.missing}") + }) + + it("should allow referencing top-level variables", async () => { + const config: ProjectConfig = { + apiVersion: DEFAULT_API_VERSION, + kind: "Project", + name: "my-project", + path: "/tmp/foo", + defaultEnvironment: "default", + dotIgnoreFiles: defaultDotIgnoreFiles, + environments: [{ name: "default", defaultNamespace, variables: { foo: "${var.foo}" } }], + providers: [], + variables: { foo: "value" }, + } + + const result = await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }) + + expect(result.variables).to.eql({ + foo: "value", + }) + }) + it("should correctly merge all variable sources in precedence order (variables fields and varfiles)", async () => { // Precedence 1/4 (highest) await writeFile( @@ -746,7 +860,7 @@ describe("pickEnvironment", () => { }, } - const result = await pickEnvironment(config, "default") + const result = await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }) expect(result.variables).to.eql({ a: "a", @@ -757,6 +871,36 @@ describe("pickEnvironment", () => { }) }) + it("should validate the picked environment", async () => { + const config: ProjectConfig = { + apiVersion: DEFAULT_API_VERSION, + kind: "Project", + name: "my-project", + path: tmpPath, + defaultEnvironment: "default", + dotIgnoreFiles: defaultDotIgnoreFiles, + environments: [ + { + name: "default", + defaultNamespace: "${var.foo}", + variables: {}, + }, + ], + providers: [], + variables: { + foo: 123, + }, + } + + await expectError( + () => pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }), + (err) => + expect(stripAnsi(err.message)).to.equal( + "Error validating environment default (/garden.yml): key .defaultNamespace must be a string" + ) + ) + }) + it("should throw if project varfile is set to non-default and it doesn't exist", async () => { const config: ProjectConfig = { apiVersion: DEFAULT_API_VERSION, @@ -778,7 +922,7 @@ describe("pickEnvironment", () => { } await expectError( - () => pickEnvironment(config, "default"), + () => pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }), (err) => expect(err.message).to.equal("Could not find varfile at path 'foo.env'") ) }) @@ -804,7 +948,7 @@ describe("pickEnvironment", () => { } await expectError( - () => pickEnvironment(config, "default"), + () => pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }), (err) => expect(err.message).to.equal("Could not find varfile at path 'foo.env'") ) }) @@ -822,7 +966,7 @@ describe("pickEnvironment", () => { variables: {}, } - expect(await pickEnvironment(config, "foo.default")).to.eql({ + expect(await pickEnvironment({ projectConfig: config, envString: "foo.default", artifactsPath, username })).to.eql({ environmentName: "default", namespace: "foo", providers: [{ name: "exec" }, { name: "container" }], @@ -844,7 +988,7 @@ describe("pickEnvironment", () => { variables: {}, } - expect(await pickEnvironment(config, "foo.default")).to.eql({ + expect(await pickEnvironment({ projectConfig: config, envString: "foo.default", artifactsPath, username })).to.eql({ environmentName: "default", namespace: "foo", providers: [{ name: "exec" }, { name: "container" }], @@ -866,7 +1010,7 @@ describe("pickEnvironment", () => { variables: {}, } - expect(await pickEnvironment(config, "default")).to.eql({ + expect(await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username })).to.eql({ environmentName: "default", namespace: "default", providers: [{ name: "exec" }, { name: "container" }], @@ -889,7 +1033,7 @@ describe("pickEnvironment", () => { } await expectError( - () => pickEnvironment(config, "$.%"), + () => pickEnvironment({ projectConfig: config, envString: "$.%", artifactsPath, username }), (err) => expect(err.message).to.equal( "Invalid environment specified ($.%): must be a valid environment name or ." @@ -911,7 +1055,7 @@ describe("pickEnvironment", () => { } await expectError( - () => pickEnvironment(config, "default"), + () => pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username }), (err) => expect(stripAnsi(err.message)).to.equal( "Environment default has defaultNamespace set to null, and no explicit namespace was specified. Please either set a defaultNamespace or explicitly set a namespace at runtime (e.g. --env=some-namespace.default)."