Skip to content

Commit

Permalink
feat(workflows): add namespacing support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
thsig authored and edvald committed Jun 24, 2020
1 parent 6383558 commit 7b0c6f2
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 29 deletions.
4 changes: 4 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
14 changes: 14 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down
50 changes: 31 additions & 19 deletions garden-service/src/config/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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 } })

Expand Down
23 changes: 21 additions & 2 deletions garden-service/src/config/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -204,6 +205,7 @@ export const triggerEvents = [

export interface TriggerSpec {
environment: string
namespace?: string
events?: string[]
branches?: string[]
tags?: string[]
Expand All @@ -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))
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -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 })
}
}
21 changes: 14 additions & 7 deletions garden-service/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -114,7 +121,7 @@ export interface GardenParams {
clientAuthToken: string | null
dotIgnoreFiles: string[]
environmentName: string
allEnvironmentNames: string[]
environmentConfigs: EnvironmentConfig[]
namespace?: string
gardenDirPath: string
log: LogEntry
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -337,7 +343,7 @@ export class Garden {
projectRoot,
projectName,
environmentName,
allEnvironmentNames: environmentNames,
environmentConfigs: config.environments,
namespace,
variables,
secrets,
Expand Down Expand Up @@ -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,
Expand Down
110 changes: 109 additions & 1 deletion garden-service/test/unit/src/config/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -38,6 +44,7 @@ describe("resolveWorkflowConfig", () => {
triggers: [
{
environment: "local",
namespace: undefined,
events: ["pull-request"],
branches: ["feature*"],
ignoreBranches: ["feature-ignored*"],
Expand Down Expand Up @@ -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")
})
})
})

0 comments on commit 7b0c6f2

Please sign in to comment.