Skip to content

Commit

Permalink
feat(config): allow environment[].variables to reference top-level vars
Browse files Browse the repository at this point in the history
Also fixes the issue where template strings were resolved in all
environments, instead of just the selected one.

Fixes #1814
  • Loading branch information
edvald committed Jun 26, 2020
1 parent f8854c9 commit ce67728
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 46 deletions.
58 changes: 51 additions & 7 deletions garden-service/src/config/config-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<variable-name>" })
)
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
Expand Down Expand Up @@ -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: "<variable-name>" })
)
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.")
Expand All @@ -372,14 +412,18 @@ export class WorkflowConfigContext extends ProjectConfigContext {
public steps: Map<string, WorkflowStepContext | ErrorContext> | 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()
Expand Down
49 changes: 37 additions & 12 deletions garden-service/src/config/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
`),
})

Expand Down Expand Up @@ -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
*/
Expand All @@ -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: environments.map((e) => omit(e, ["providers"])),
},
schema: projectSchema(),
configType: "project",
path: config.path,
Expand Down Expand Up @@ -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).
Expand All @@ -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) {
export async function pickEnvironment({
config,
envString,
artifactsPath,
username,
}: {
config: ProjectConfig
envString: string
artifactsPath: string
username: string
}) {
const { environments, name: projectName } = config

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}`, {
Expand All @@ -516,12 +532,24 @@ export async function pickEnvironment(config: ProjectConfig, envString: string)
})
}

const projectVarfileVars = await loadVarfile(config.path, config.varfile, defaultVarfilePath)
const projectVariables: DeepPrimitiveMap = <any>merge(config.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 })
)

namespace = getNamespace(environmentConfig, namespace)

const fixedProviders = fixedPlugins.map((name) => ({ name }))
const allProviders = [
...fixedProviders,
...config.providers.filter((p) => !p.environments || p.environments.includes(environment)),
...envProviders,
]

const mergedProviders: { [name: string]: ProviderConfig } = {}
Expand All @@ -535,12 +563,9 @@ 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 = <any>(
merge(merge(config.variables, projectVarfileVars), merge(environmentConfig.variables, envVarfileVars))
)
const variables: DeepPrimitiveMap = <any>merge(projectVariables, merge(environmentConfig.variables, envVarfileVars))

return {
environmentName: environment,
Expand Down
8 changes: 5 additions & 3 deletions garden-service/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,12 @@ export class Garden {
environmentStr = defaultEnvironment
}

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

const buildDir = await BuildDir.factory(projectRoot, gardenDirPath)
const workingCopyId = await getWorkingCopyId(gardenDirPath)
Expand Down
8 changes: 7 additions & 1 deletion garden-service/static/docs/templates/template-strings.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit ce67728

Please sign in to comment.