From 7b0c6f2c6797acaa7001f1e811b6383cfec6643b Mon Sep 17 00:00:00 2001 From: Thorarinn Sigurdsson Date: Tue, 23 Jun 2020 14:31:02 +0200 Subject: [PATCH] feat(workflows): add namespacing support Added an optional `namespace` field to trigger specs. We apply the same validation and default namespace rules for trigger namespaces as we do for environment specs. --- docs/reference/commands.md | 4 + docs/reference/config.md | 14 +++ garden-service/src/config/project.ts | 50 +++++--- garden-service/src/config/workflow.ts | 23 +++- garden-service/src/garden.ts | 21 ++-- .../test/unit/src/config/workflow.ts | 110 +++++++++++++++++- 6 files changed, 193 insertions(+), 29 deletions(-) diff --git a/docs/reference/commands.md b/docs/reference/commands.md index bd200fb349..269203cbce 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -1186,6 +1186,10 @@ workflowConfigs: - # The environment name (from your project configuration) to use for the workflow when matched by this trigger. environment: + # The namespace to use for the workflow when matched by this trigger. Follows the namespacing setting used for + # this trigger's environment, as defined in your project's environment configs. + namespace: + # A list of GitHub events that should trigger this workflow. events: diff --git a/docs/reference/config.md b/docs/reference/config.md index 08ac8c1320..426961138f 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -1013,6 +1013,10 @@ triggers: - # The environment name (from your project configuration) to use for the workflow when matched by this trigger. environment: + # The namespace to use for the workflow when matched by this trigger. Follows the namespacing setting used for + # this trigger's environment, as defined in your project's environment configs. + namespace: + # A list of GitHub events that should trigger this workflow. events: @@ -1262,6 +1266,16 @@ The environment name (from your project configuration) to use for the workflow w | -------- | -------- | | `string` | Yes | +### `triggers[].namespace` + +[triggers](#triggers) > namespace + +The namespace to use for the workflow when matched by this trigger. Follows the namespacing setting used for this trigger's environment, as defined in your project's environment configs. + +| Type | Required | +| -------- | -------- | +| `string` | No | + ### `triggers[].events[]` [triggers](#triggers) > events diff --git a/garden-service/src/config/project.ts b/garden-service/src/config/project.ts index 9b1bd84020..e8809ee5ea 100644 --- a/garden-service/src/config/project.ts +++ b/garden-service/src/config/project.ts @@ -533,25 +533,7 @@ export async function pickEnvironment(config: ProjectConfig, envString: string) }) } - if (namespace && environmentConfig.namespacing === "disabled") { - throw new ParameterError( - `Environment ${environment} does not allow namespacing, but namespace '${namespace}' was specified.`, - { environmentConfig, namespace } - ) - } - - if (!namespace && environmentConfig.defaultNamespace) { - namespace = environmentConfig.defaultNamespace - } - - if (!namespace && environmentConfig.namespacing === "required") { - throw new ParameterError( - `Environment ${environment} requires a namespace, but none was specified and no defaultNamespace is configured.`, - { - environmentConfig, - } - ) - } + namespace = getNamespace(environmentConfig, namespace) const fixedProviders = fixedPlugins.map((name) => ({ name })) const allProviders = [ @@ -586,6 +568,36 @@ export async function pickEnvironment(config: ProjectConfig, envString: string) } } +/** + * Validates that the value passed for `namespace` conforms with the namespacing setting in `environmentConfig`, + * and returns `namespace` (or a default namespace, if appropriate). + */ +export function getNamespace(environmentConfig: EnvironmentConfig, namespace: string | undefined): string | undefined { + const envName = environmentConfig.name + + if (namespace && environmentConfig.namespacing === "disabled") { + throw new ParameterError( + `Environment ${envName} does not allow namespacing, but namespace '${namespace}' was specified.`, + { environmentConfig, namespace } + ) + } + + if (!namespace && environmentConfig.defaultNamespace) { + namespace = environmentConfig.defaultNamespace + } + + if (!namespace && environmentConfig.namespacing === "required") { + throw new ParameterError( + `Environment ${envName} requires a namespace, but none was specified and no defaultNamespace is configured.`, + { + environmentConfig, + } + ) + } + + return namespace +} + export function parseEnvironment(env: string): ParsedEnvironment { const result = joi.environment().validate(env, { errors: { label: false } }) diff --git a/garden-service/src/config/workflow.ts b/garden-service/src/config/workflow.ts index b506e6247a..b1ca531d10 100644 --- a/garden-service/src/config/workflow.ts +++ b/garden-service/src/config/workflow.ts @@ -18,6 +18,7 @@ import { validateWithPath } from "./validation" import { ConfigurationError } from "../exceptions" import { coreCommands } from "../commands/commands" import { Parameters } from "../commands/base" +import { EnvironmentConfig, getNamespace } from "./project" export interface WorkflowConfig { apiVersion: string @@ -204,6 +205,7 @@ export const triggerEvents = [ export interface TriggerSpec { environment: string + namespace?: string events?: string[] branches?: string[] tags?: string[] @@ -216,6 +218,10 @@ export const triggerSchema = () => environment: joi.string().required().description(deline` The environment name (from your project configuration) to use for the workflow when matched by this trigger. `), + namespace: joi.string().description(deline` + The namespace to use for the workflow when matched by this trigger. Follows the namespacing setting used for + this trigger's environment, as defined in your project's environment configs. + `), events: joi .array() .items(joi.string().valid(...triggerEvents)) @@ -263,7 +269,8 @@ export function resolveWorkflowConfig(garden: Garden, config: WorkflowConfig) { }) validateSteps(resolvedConfig) - validateTriggers(resolvedConfig, garden.allEnvironmentNames) + validateTriggers(resolvedConfig, garden.environmentConfigs) + populateNamespaceForTriggers(resolvedConfig, garden.environmentConfigs) return resolvedConfig } @@ -319,8 +326,9 @@ function validateSteps(config: WorkflowConfig) { /** * Throws if one or more triggers uses an environment that isn't defined in the project's config. */ -function validateTriggers(config: WorkflowConfig, environmentNames: string[]) { +function validateTriggers(config: WorkflowConfig, environmentConfigs: EnvironmentConfig[]) { const invalidTriggers: TriggerSpec[] = [] + const environmentNames = environmentConfigs.map((c) => c.name) for (const trigger of config.triggers || []) { if (!environmentNames.includes(trigger.environment)) { invalidTriggers.push(trigger) @@ -346,3 +354,14 @@ function validateTriggers(config: WorkflowConfig, environmentNames: string[]) { throw new ConfigurationError(msg, { invalidTriggers }) } } + +export function populateNamespaceForTriggers(config: WorkflowConfig, environmentConfigs: EnvironmentConfig[]) { + try { + for (const trigger of config.triggers || []) { + const environmentConfigForTrigger = environmentConfigs.find((c) => c.name === trigger.environment) + trigger.namespace = getNamespace(environmentConfigForTrigger!, trigger.namespace) + } + } catch (err) { + throw new ConfigurationError(`Invalid namespace in trigger for workflow ${config.name}: ${err.message}`, { err }) + } +} diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index a9f331c089..3314dba1b3 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -19,7 +19,14 @@ import { TreeCache } from "./cache" import { builtinPlugins } from "./plugins/plugins" import { Module, getModuleCacheContext, getModuleKey, ModuleConfigMap, moduleFromConfig } from "./types/module" import { pluginModuleSchema, ModuleTypeMap } from "./types/plugin/plugin" -import { SourceConfig, ProjectConfig, resolveProjectConfig, pickEnvironment, OutputSpec } from "./config/project" +import { + SourceConfig, + ProjectConfig, + resolveProjectConfig, + pickEnvironment, + OutputSpec, + EnvironmentConfig, +} from "./config/project" import { findByName, pickKeys, getPackageVersion, getNames, findByNames } from "./util/util" import { ConfigurationError, PluginError, RuntimeError } from "./exceptions" import { VcsHandler, ModuleVersion } from "./vcs/vcs" @@ -114,7 +121,7 @@ export interface GardenParams { clientAuthToken: string | null dotIgnoreFiles: string[] environmentName: string - allEnvironmentNames: string[] + environmentConfigs: EnvironmentConfig[] namespace?: string gardenDirPath: string log: LogEntry @@ -170,7 +177,7 @@ export class Garden { public readonly projectRoot: string public readonly projectName: string public readonly environmentName: string - public readonly allEnvironmentNames: string[] + public readonly environmentConfigs: EnvironmentConfig[] public readonly namespace?: string public readonly variables: DeepPrimitiveMap public readonly secrets: StringMap @@ -197,7 +204,7 @@ export class Garden { this.enterpriseDomain = params.enterpriseDomain this.sessionId = params.sessionId this.environmentName = params.environmentName - this.allEnvironmentNames = params.allEnvironmentNames + this.environmentConfigs = params.environmentConfigs this.namespace = params.namespace this.gardenDirPath = params.gardenDirPath this.log = params.log @@ -295,7 +302,6 @@ export class Garden { environmentStr = defaultEnvironment } - const environmentNames = config.environments.map((env) => env.name) const { environmentName, namespace, providers, variables, production } = await pickEnvironment( config, environmentStr @@ -337,7 +343,7 @@ export class Garden { projectRoot, projectName, environmentName, - allEnvironmentNames: environmentNames, + environmentConfigs: config.environments, namespace, variables, secrets, @@ -1160,10 +1166,11 @@ export class Garden { } const workflowConfigs = await this.getWorkflowConfigs() + const allEnvironmentNames = this.environmentConfigs.map((c) => c.name) return { environmentName: this.environmentName, - allEnvironmentNames: this.allEnvironmentNames, + allEnvironmentNames, namespace: this.namespace, providers, variables: this.variables, diff --git a/garden-service/test/unit/src/config/workflow.ts b/garden-service/test/unit/src/config/workflow.ts index 9bec9b3296..a6228d6d66 100644 --- a/garden-service/test/unit/src/config/workflow.ts +++ b/garden-service/test/unit/src/config/workflow.ts @@ -9,8 +9,14 @@ import { expect } from "chai" import { DEFAULT_API_VERSION } from "../../../../src/constants" import { expectError, makeTestGardenA, TestGarden } from "../../../helpers" -import { WorkflowConfig, resolveWorkflowConfig } from "../../../../src/config/workflow" +import { + WorkflowConfig, + resolveWorkflowConfig, + populateNamespaceForTriggers, + TriggerSpec, +} from "../../../../src/config/workflow" import { defaultContainerLimits } from "../../../../src/plugins/container/config" +import { EnvironmentConfig } from "../../../../src/config/project" describe("resolveWorkflowConfig", () => { let garden: TestGarden @@ -38,6 +44,7 @@ describe("resolveWorkflowConfig", () => { triggers: [ { environment: "local", + namespace: undefined, events: ["pull-request"], branches: ["feature*"], ignoreBranches: ["feature-ignored*"], @@ -138,4 +145,105 @@ describe("resolveWorkflowConfig", () => { (err) => expect(err.message).to.match(/Invalid environment in trigger for workflow workflow-a/) ) }) + + describe("populateNamespaceForTriggers", () => { + const trigger: TriggerSpec = { + environment: "test", + events: ["pull-request"], + branches: ["feature*"], + ignoreBranches: ["feature-ignored*"], + tags: ["v1*"], + ignoreTags: ["v1-ignored*"], + } + const config: WorkflowConfig = { + ...defaults, + apiVersion: DEFAULT_API_VERSION, + kind: "Workflow", + name: "workflow-a", + path: "/tmp/foo", + description: "Sample workflow", + steps: [{ description: "Deploy the stack", command: ["deploy"] }, { command: ["test"] }], + } + + it("should pass through a trigger without a namespace when namespacing is optional", () => { + const environmentConfigs: EnvironmentConfig[] = [ + { + name: "test", + namespacing: "optional", + variables: {}, + }, + ] + + // config's only trigger has no namespace defined + populateNamespaceForTriggers(config, environmentConfigs) + }) + + it("should throw if a trigger's environment requires a namespace, but none is specified", () => { + const environmentConfigs: EnvironmentConfig[] = [ + { + name: "test", + namespacing: "required", + variables: {}, + }, + ] + + expectError( + () => populateNamespaceForTriggers({ ...config, triggers: [trigger] }, environmentConfigs), + (err) => + expect(err.message).to.match( + /Invalid namespace in trigger for workflow workflow-a: Environment test requires a namespace/ + ) + ) + }) + + it("should throw if a trigger's environment does not allow namespaces, but one is specified", () => { + const environmentConfigs: EnvironmentConfig[] = [ + { + name: "test", + namespacing: "disabled", + variables: {}, + }, + ] + + const invalidTrigger = { ...trigger, namespace: "foo" } + + expectError( + () => populateNamespaceForTriggers({ ...config, triggers: [invalidTrigger] }, environmentConfigs), + (err) => + expect(err.message).to.match( + /Invalid namespace in trigger for workflow workflow-a: Environment test does not allow namespacing/ + ) + ) + }) + + it("should populate the trigger with a default namespace if one is defined", () => { + const environmentConfigs: EnvironmentConfig[] = [ + { + name: "test", + namespacing: "optional", + defaultNamespace: "foo", + variables: {}, + }, + ] + + const configToPopulate = { ...config, triggers: [trigger] } + populateNamespaceForTriggers(configToPopulate, environmentConfigs) + expect(configToPopulate.triggers![0].namespace).to.eql("foo") + }) + + it("should not override a trigger's specified namespace with a default namespace", () => { + const environmentConfigs: EnvironmentConfig[] = [ + { + name: "test", + namespacing: "optional", + defaultNamespace: "foo", + variables: {}, + }, + ] + + const configToPopulate = { ...config, triggers: [{ ...trigger, namespace: "bar" }] } + populateNamespaceForTriggers(configToPopulate, environmentConfigs) + expect(configToPopulate.triggers![0].namespace).to.eql("bar") + }) + }) })