From d560313cb0b0e2a782632383a472379f7c5a3d38 Mon Sep 17 00:00:00 2001 From: Thorarinn Sigurdsson Date: Fri, 15 Feb 2019 15:14:33 +0100 Subject: [PATCH] feat: add support for flat config style Projects and modules can now be configured in a flat style. That is, not nested under the project/module key, but with the entity type indicated by the new `kind` key, analogously to the YAML syntax for k8s object definitions. Also changed the (currently unused) schema version string format to `garden.io/[version]`. --- docs/reference/config.md | 8 +- garden-service/src/cli/cli.ts | 2 +- garden-service/src/config/base.ts | 91 +++++++++++++++++-- garden-service/src/config/module.ts | 4 +- garden-service/src/config/project.ts | 4 +- .../src/plugins/kubernetes/system.ts | 2 +- .../local/local-google-cloud-functions.ts | 2 +- .../src/plugins/openfaas/openfaas.ts | 2 +- .../data/test-project-flat-config/garden.yml | 19 ++++ .../invalid-config-kind/.garden-version | 4 + .../invalid-config-kind/garden.yml | 15 +++ garden-service/test/helpers.ts | 2 +- garden-service/test/src/config/base.ts | 66 ++++++++++++-- garden-service/test/src/plugins/container.ts | 14 +-- .../plugins/kubernetes/container/ingress.ts | 2 +- .../src/plugins/kubernetes/helm/config.ts | 2 +- 16 files changed, 202 insertions(+), 37 deletions(-) create mode 100644 garden-service/test/data/test-project-flat-config/garden.yml create mode 100644 garden-service/test/data/test-project-flat-config/invalid-config-kind/.garden-version create mode 100644 garden-service/test/data/test-project-flat-config/invalid-config-kind/garden.yml diff --git a/docs/reference/config.md b/docs/reference/config.md index 20be947a59f..78512f08f74 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -29,7 +29,7 @@ The schema version of this project's config (currently not used). | Type | Required | Allowed Values | | ---- | -------- | -------------- | -| `string` | Yes | "0" +| `string` | Yes | "garden.io/v0" ### `project.name` [project](#project) > name @@ -207,7 +207,7 @@ project: ## Project YAML schema ```yaml project: - apiVersion: '0' + apiVersion: garden.io/v0 name: defaultEnvironment: '' environmentDefaults: @@ -240,7 +240,7 @@ The schema version of this module's config (currently not used). | Type | Required | Allowed Values | | ---- | -------- | -------------- | -| `string` | Yes | "0" +| `string` | Yes | "garden.io/v0" ### `module.type` [module](#module) > type @@ -388,7 +388,7 @@ POSIX-style path or filename to copy the directory or file(s) to (defaults to sa ## Module YAML schema ```yaml module: - apiVersion: '0' + apiVersion: garden.io/v0 type: name: description: diff --git a/garden-service/src/cli/cli.ts b/garden-service/src/cli/cli.ts index 31b798e9410..92409613289 100644 --- a/garden-service/src/cli/cli.ts +++ b/garden-service/src/cli/cli.ts @@ -73,7 +73,7 @@ export const MOCK_CONFIG: GardenConfig = { dirname: "/", path: process.cwd(), project: { - apiVersion: "0", + apiVersion: "garden.io/v0", name: "mock-project", defaultEnvironment: "local", environments: defaultEnvironments, diff --git a/garden-service/src/config/base.ts b/garden-service/src/config/base.ts index 1693e5253c4..aa2e3532483 100644 --- a/garden-service/src/config/base.ts +++ b/garden-service/src/config/base.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { join, basename, sep, resolve } from "path" +import { join, basename, sep, resolve, relative } from "path" import { findByName, getNames, @@ -87,10 +87,91 @@ type ConfigDoc = { project?: ProjectConfig, } +export type ConfigKind = "Module" | "Project" +export const configKinds = new Set(["Module", "Project"]) + +const configKindSettings = { + Module: { + specKey: "module", + validationSchema: baseModuleSpecSchema, + }, + Project: { + specKey: "project", + validationSchema: projectSchema, + }, +} + /** * Each YAML document in a garden.yml file consists of a project definition and/or a module definition. + * + * A document can be structured according to either the (old) nested or the (new) flat style. + * + * In the nested style, the project/module's config is nested under the project/module key respectively. + * + * In the flat style, the project/module's config is at the top level, and the kind key is used to indicate + * whether the entity being configured is a project or a module (similar to the YAML syntax for k8s object + * definitions). The kind key is removed before validation, so that specs following both styles can be validated + * with the same schema. */ function prepareConfigDoc(spec: any, path: string, projectRoot: string): ConfigDoc { + + const kind = spec.kind + + if (!spec.kind) { + const preparedSpec = prepareScopedConfigDoc(spec, path) + // validate with scoped config schema + return validateWithPath({ + config: preparedSpec, + schema: configSchema, + configType: "config", + path, + projectRoot, + }) + } + + if (configKinds.has(kind)) { + const { specKey, validationSchema } = configKindSettings[kind] + delete spec.kind + const preparedSpec = prepareFlatConfigDoc(spec, path) + const validated = validateWithPath({ + config: preparedSpec, + schema: validationSchema, + configType: specKey, + path, + projectRoot, + }) + return { [specKey]: validated } + } else { + const relPath = `${relative(projectRoot, path)}/garden.yml` + throw new ConfigurationError(`Unknown config kind ${kind} in ${relPath}`, { kind, path: relPath }) + } + +} + +/** + * The new / flat configuration style. + * + * The spec defines either a project or a module (determined by its "kind" field). + */ +function prepareFlatConfigDoc(spec: any, path: string): ConfigDoc { + if (spec.kind === "Project") { + spec = prepareProjectConfig(spec, path) + } + + if (spec.kind === "Module") { + spec = prepareModuleConfig(spec, path) + } + + return spec +} + +/** + * The old / nested configuration style. + * + * The spec defines a project and/or a module, with the config for each nested under the "project" / "module" field, + * respectively. + */ +function prepareScopedConfigDoc(spec: any, path: string): ConfigDoc { if (spec.project) { spec.project = prepareProjectConfig(spec.project, path) } @@ -99,13 +180,7 @@ function prepareConfigDoc(spec: any, path: string, projectRoot: string): ConfigD spec.module = prepareModuleConfig(spec.module, path) } - return validateWithPath({ - config: spec, - schema: configSchema, - configType: "config", - path, - projectRoot, - }) + return spec } function prepareProjectConfig(projectSpec: any, path: string): ProjectConfig { diff --git a/garden-service/src/config/module.ts b/garden-service/src/config/module.ts index 443466536a9..45659747953 100644 --- a/garden-service/src/config/module.ts +++ b/garden-service/src/config/module.ts @@ -78,8 +78,8 @@ export interface BaseModuleSpec { export const baseModuleSpecSchema = Joi.object() .keys({ apiVersion: Joi.string() - .default("0") - .only("0") + .default("garden.io/v0") + .only("garden.io/v0") .description("The schema version of this module's config (currently not used)."), type: joiIdentifier() .required() diff --git a/garden-service/src/config/project.ts b/garden-service/src/config/project.ts index c2b9984a996..283bdaae948 100644 --- a/garden-service/src/config/project.ts +++ b/garden-service/src/config/project.ts @@ -124,8 +124,8 @@ export const projectNameSchema = joiIdentifier() export const projectSchema = Joi.object() .keys({ apiVersion: Joi.string() - .default("0") - .only("0") + .default("garden.io/v0") + .only("garden.io/v0") .description("The schema version of this project's config (currently not used)."), name: projectNameSchema, defaultEnvironment: Joi.string() diff --git a/garden-service/src/plugins/kubernetes/system.ts b/garden-service/src/plugins/kubernetes/system.ts index 6619cccf67c..7b870be179e 100644 --- a/garden-service/src/plugins/kubernetes/system.ts +++ b/garden-service/src/plugins/kubernetes/system.ts @@ -27,7 +27,7 @@ export async function getSystemGarden(provider: KubernetesProvider): Promise ({ }) return { - apiVersion: "0", + apiVersion: "garden.io/v0", allowPublish: true, build: { command: [], diff --git a/garden-service/src/plugins/openfaas/openfaas.ts b/garden-service/src/plugins/openfaas/openfaas.ts index 06a9ec04e5d..cca52b86588 100644 --- a/garden-service/src/plugins/openfaas/openfaas.ts +++ b/garden-service/src/plugins/openfaas/openfaas.ts @@ -467,7 +467,7 @@ export async function getOpenFaasGarden(ctx: PluginContext): Promise { dirname: "system", path: systemProjectPath, project: { - apiVersion: "0", + apiVersion: "garden.io/v0", name: `${ctx.projectName}-openfaas`, environmentDefaults: { providers: [], diff --git a/garden-service/test/data/test-project-flat-config/garden.yml b/garden-service/test/data/test-project-flat-config/garden.yml new file mode 100644 index 00000000000..acb8753c39d --- /dev/null +++ b/garden-service/test/data/test-project-flat-config/garden.yml @@ -0,0 +1,19 @@ +kind: Project +name: test-project-flat-config +environmentDefaults: + variables: + some: variable +environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other + +--- + +kind: Module +name: module-from-project-config +type: test +build: + command: [echo, project] \ No newline at end of file diff --git a/garden-service/test/data/test-project-flat-config/invalid-config-kind/.garden-version b/garden-service/test/data/test-project-flat-config/invalid-config-kind/.garden-version new file mode 100644 index 00000000000..1d754b71416 --- /dev/null +++ b/garden-service/test/data/test-project-flat-config/invalid-config-kind/.garden-version @@ -0,0 +1,4 @@ +{ + "latestCommit": "1234567890", + "dirtyTimestamp": null +} diff --git a/garden-service/test/data/test-project-flat-config/invalid-config-kind/garden.yml b/garden-service/test/data/test-project-flat-config/invalid-config-kind/garden.yml new file mode 100644 index 00000000000..635faf42aa5 --- /dev/null +++ b/garden-service/test/data/test-project-flat-config/invalid-config-kind/garden.yml @@ -0,0 +1,15 @@ +kind: Banana +name: module-a1 +type: test +services: + - name: service-a1 +build: + command: [echo, A1] + dependencies: + - module-from-project-config +tests: + - name: unit + command: [echo, OK] +tasks: + - name: task-a1 + command: [echo, OK] \ No newline at end of file diff --git a/garden-service/test/helpers.ts b/garden-service/test/helpers.ts index 7a98e379c26..c2476e3901c 100644 --- a/garden-service/test/helpers.ts +++ b/garden-service/test/helpers.ts @@ -220,7 +220,7 @@ export const testPluginC: PluginFactory = async (params) => { } const defaultModuleConfig: ModuleConfig = { - apiVersion: "0", + apiVersion: "garden.io/v0", type: "test", name: "test", path: "bla", diff --git a/garden-service/test/src/config/base.ts b/garden-service/test/src/config/base.ts index 6aa9de2f424..4f510c16dd0 100644 --- a/garden-service/test/src/config/base.ts +++ b/garden-service/test/src/config/base.ts @@ -11,6 +11,9 @@ const modulePathAMultiple = resolve(projectPathMultipleModules, "module-a") const projectPathDuplicateProjects = resolve(dataDir, "test-project-duplicate-project-config") +const projectPathFlat = resolve(dataDir, "test-project-flat-config") +const modulePathFlatInvalid = resolve(projectPathFlat, "invalid-config-kind") + describe("loadConfig", () => { it("should not throw an error if no file was found", async () => { @@ -19,7 +22,7 @@ describe("loadConfig", () => { expect(parsed).to.eql(undefined) }) - it("should throw a config error if the file couldn't be parsed°", async () => { + it("should throw a config error if the file couldn't be parsed", async () => { const projectPath = resolve(dataDir, "test-project-invalid-config") await expectError( async () => await loadConfig(projectPath, resolve(projectPath, "invalid-syntax-module")), @@ -42,7 +45,7 @@ describe("loadConfig", () => { const parsed = await loadConfig(projectPathA, projectPathA) expect(parsed!.project).to.eql({ - apiVersion: "0", + apiVersion: "garden.io/v0", name: "test-project-a", defaultEnvironment: "local", sources: [], @@ -73,7 +76,7 @@ describe("loadConfig", () => { expect(parsed!.modules).to.eql([ { - apiVersion: "0", + apiVersion: "garden.io/v0", name: "module-a", type: "test", description: undefined, @@ -106,7 +109,7 @@ describe("loadConfig", () => { const parsed = await loadConfig(projectPathMultipleModules, projectPathMultipleModules) expect(parsed!.project).to.eql({ - apiVersion: "0", + apiVersion: "garden.io/v0", defaultEnvironment: "local", environmentDefaults: { providers: [], @@ -134,7 +137,7 @@ describe("loadConfig", () => { }) expect(parsed!.modules).to.eql([{ - apiVersion: "0", + apiVersion: "garden.io/v0", name: "module-from-project-config", type: "test", description: undefined, @@ -155,7 +158,7 @@ describe("loadConfig", () => { expect(parsed!.modules).to.eql([ { - apiVersion: "0", + apiVersion: "garden.io/v0", name: "module-a1", type: "test", allowPublish: true, @@ -179,7 +182,7 @@ describe("loadConfig", () => { taskConfigs: [], }, { - apiVersion: "0", + apiVersion: "garden.io/v0", name: "module-a2", type: "test", allowPublish: true, @@ -200,6 +203,55 @@ describe("loadConfig", () => { ]) }) + it("should parse a config file using the flat config style", async () => { + const parsed = await loadConfig(projectPathFlat, projectPathFlat) + + expect(parsed!.project).to.eql({ + apiVersion: "garden.io/v0", + defaultEnvironment: "", + environmentDefaults: { + providers: [], + variables: { some: "variable" }, + }, + environments: [ + { + name: "local", + providers: [ + { name: "test-plugin" }, + { name: "test-plugin-b" }, + ], + variables: {}, + }, + { + name: "other", + providers: [], + variables: {}, + }, + ], + name: "test-project-flat-config", + sources: [], + }) + + expect(parsed!.modules).to.eql([{ + name: "module-from-project-config", + type: "test", + build: { + command: ["echo", "project"], + dependencies: [], + }, + apiVersion: "garden.io/v0", + allowPublish: true, + }]) + }) + + it("should throw an error when parsing a flat-style config using an unknown/invalid kind", async () => { + await expectError( + async () => await loadConfig(projectPathFlat, modulePathFlatInvalid), + (err) => { + expect(err.message).to.match(/Unknown config kind/) + }) + }) + it("should throw an error when parsing a config file defining multiple projects", async () => { await expectError( async () => await loadConfig(projectPathDuplicateProjects, projectPathDuplicateProjects), diff --git a/garden-service/test/src/plugins/container.ts b/garden-service/test/src/plugins/container.ts index e8a51a65b1e..0105fa650eb 100644 --- a/garden-service/test/src/plugins/container.ts +++ b/garden-service/test/src/plugins/container.ts @@ -34,7 +34,7 @@ describe("plugins.container", () => { command: [], dependencies: [], }, - apiVersion: "0", + apiVersion: "garden.io/v0", name: "test", outputs: {}, path: modulePath, @@ -137,7 +137,7 @@ describe("plugins.container", () => { dependencies: [], }, name: "test", - apiVersion: "0", + apiVersion: "garden.io/v0", outputs: {}, path: modulePath, type: "container", @@ -191,7 +191,7 @@ describe("plugins.container", () => { command: ["echo", "OK"], dependencies: [], }, - apiVersion: "0", + apiVersion: "garden.io/v0", name: "module-a", outputs: {}, path: modulePath, @@ -253,7 +253,7 @@ describe("plugins.container", () => { expect(result).to.eql({ allowPublish: false, build: { command: ["echo", "OK"], dependencies: [] }, - apiVersion: "0", + apiVersion: "garden.io/v0", name: "module-a", outputs: {}, path: modulePath, @@ -370,7 +370,7 @@ describe("plugins.container", () => { command: ["echo", "OK"], dependencies: [], }, - apiVersion: "0", + apiVersion: "garden.io/v0", name: "module-a", outputs: {}, path: modulePath, @@ -427,7 +427,7 @@ describe("plugins.container", () => { command: ["echo", "OK"], dependencies: [], }, - apiVersion: "0", + apiVersion: "garden.io/v0", name: "module-a", outputs: {}, path: modulePath, @@ -479,7 +479,7 @@ describe("plugins.container", () => { command: ["echo", "OK"], dependencies: [], }, - apiVersion: "0", + apiVersion: "garden.io/v0", name: "module-a", outputs: {}, path: modulePath, diff --git a/garden-service/test/src/plugins/kubernetes/container/ingress.ts b/garden-service/test/src/plugins/kubernetes/container/ingress.ts index 558573a4218..25195057ea6 100644 --- a/garden-service/test/src/plugins/kubernetes/container/ingress.ts +++ b/garden-service/test/src/plugins/kubernetes/container/ingress.ts @@ -336,7 +336,7 @@ describe("createIngresses", () => { command: [], dependencies: [], }, - apiVersion: "0", + apiVersion: "garden.io/v0", name: "test", outputs: {}, path: "/tmp", diff --git a/garden-service/test/src/plugins/kubernetes/helm/config.ts b/garden-service/test/src/plugins/kubernetes/helm/config.ts index a7d3bd626e7..6ad3a479aa1 100644 --- a/garden-service/test/src/plugins/kubernetes/helm/config.ts +++ b/garden-service/test/src/plugins/kubernetes/helm/config.ts @@ -43,7 +43,7 @@ describe("validateHelmModule", () => { command: [], }, description: "The API backend for the voting UI", - apiVersion: "0", + apiVersion: "garden.io/v0", name: "api", outputs: { "release-name": "api-release",