Skip to content

Commit

Permalink
feat: add support for flat config style
Browse files Browse the repository at this point in the history
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]`.
  • Loading branch information
thsig committed Feb 19, 2019
1 parent ff4d370 commit fecde8b
Show file tree
Hide file tree
Showing 16 changed files with 202 additions and 37 deletions.
8 changes: 4 additions & 4 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -207,7 +207,7 @@ project:
## Project YAML schema
```yaml
project:
apiVersion: '0'
apiVersion: garden.io/v0
name:
defaultEnvironment: ''
environmentDefaults:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
91 changes: 83 additions & 8 deletions garden-service/src/config/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions garden-service/src/config/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions garden-service/src/config/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/plugins/kubernetes/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function getSystemGarden(provider: KubernetesProvider): Promise<Gar
dirname: "system",
path: systemProjectPath,
project: {
apiVersion: "0",
apiVersion: "garden.io/v0",
name: "garden-system",
environmentDefaults: {
providers: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const gardenPlugin = (): GardenPlugin => ({
})

return {
apiVersion: "0",
apiVersion: "garden.io/v0",
allowPublish: true,
build: {
command: [],
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/plugins/openfaas/openfaas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ export async function getOpenFaasGarden(ctx: PluginContext): Promise<Garden> {
dirname: "system",
path: systemProjectPath,
project: {
apiVersion: "0",
apiVersion: "garden.io/v0",
name: `${ctx.projectName}-openfaas`,
environmentDefaults: {
providers: [],
Expand Down
19 changes: 19 additions & 0 deletions garden-service/test/data/test-project-flat-config/garden.yml
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"latestCommit": "1234567890",
"dirtyTimestamp": null
}
Original file line number Diff line number Diff line change
@@ -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]
2 changes: 1 addition & 1 deletion garden-service/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
66 changes: 59 additions & 7 deletions garden-service/test/src/config/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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")),
Expand All @@ -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: [],
Expand Down Expand Up @@ -73,7 +76,7 @@ describe("loadConfig", () => {

expect(parsed!.modules).to.eql([
{
apiVersion: "0",
apiVersion: "garden.io/v0",
name: "module-a",
type: "test",
description: undefined,
Expand Down Expand Up @@ -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: [],
Expand Down Expand Up @@ -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,
Expand All @@ -155,7 +158,7 @@ describe("loadConfig", () => {

expect(parsed!.modules).to.eql([
{
apiVersion: "0",
apiVersion: "garden.io/v0",
name: "module-a1",
type: "test",
allowPublish: true,
Expand All @@ -179,7 +182,7 @@ describe("loadConfig", () => {
taskConfigs: [],
},
{
apiVersion: "0",
apiVersion: "garden.io/v0",
name: "module-a2",
type: "test",
allowPublish: true,
Expand All @@ -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),
Expand Down
Loading

0 comments on commit fecde8b

Please sign in to comment.