From ff4d3702cabd03542dbfb14f7360fcb4d6dcf7e8 Mon Sep 17 00:00:00 2001 From: Thorarinn Sigurdsson Date: Tue, 5 Feb 2019 16:48:55 +0100 Subject: [PATCH] feat: allow multiple modules in a single file More than one module can now be defined in the same garden.yml file. This is useful e.g. where more than one Dockerfile is used to build the same container (e.g. for development and production). Within a garden.yml file, each module definition lives within a YAML document (separated by `---`). --- docs/reference/config.md | 18 ++ docs/using-garden/configuration-files.md | 42 +++++ examples/multiple-modules/README.md | 31 ++++ examples/multiple-modules/garden.yml | 6 + .../node-service/.dockerignore | 4 + .../node-service/Dockerfile-dev | 14 ++ .../node-service/Dockerfile-prod | 14 ++ examples/multiple-modules/node-service/app.js | 12 ++ .../multiple-modules/node-service/garden.yml | 37 ++++ .../multiple-modules/node-service/main.js | 3 + .../node-service/package.json | 22 +++ garden-service/src/cli/cli.ts | 2 +- garden-service/src/config/base.ts | 174 +++++++++++------- garden-service/src/config/module.ts | 5 + garden-service/src/config/project.ts | 6 +- garden-service/src/garden.ts | 44 ++--- .../src/plugins/kubernetes/system.ts | 2 +- .../local/local-google-cloud-functions.ts | 1 + .../src/plugins/openfaas/openfaas.ts | 2 +- .../garden.yml | 30 +++ .../module-a/.garden-version | 4 + .../module-a/garden.yml | 47 +++++ .../module-b/garden.yml | 37 ++++ .../module-c/garden.yml | 14 ++ .../garden.yml | 19 ++ .../module-a/.garden-version | 4 + .../module-a/garden.yml | 31 ++++ .../module-b/garden.yml | 37 ++++ .../module-c/garden.yml | 14 ++ garden-service/test/helpers.ts | 1 + garden-service/test/src/config/base.ts | 153 +++++++++++++-- garden-service/test/src/garden.ts | 23 ++- garden-service/test/src/plugins/container.ts | 7 + .../plugins/kubernetes/container/ingress.ts | 1 + .../src/plugins/kubernetes/helm/config.ts | 1 + 35 files changed, 745 insertions(+), 117 deletions(-) create mode 100644 examples/multiple-modules/README.md create mode 100644 examples/multiple-modules/garden.yml create mode 100644 examples/multiple-modules/node-service/.dockerignore create mode 100644 examples/multiple-modules/node-service/Dockerfile-dev create mode 100644 examples/multiple-modules/node-service/Dockerfile-prod create mode 100644 examples/multiple-modules/node-service/app.js create mode 100644 examples/multiple-modules/node-service/garden.yml create mode 100644 examples/multiple-modules/node-service/main.js create mode 100644 examples/multiple-modules/node-service/package.json create mode 100644 garden-service/test/data/test-project-duplicate-project-config/garden.yml create mode 100644 garden-service/test/data/test-project-duplicate-project-config/module-a/.garden-version create mode 100644 garden-service/test/data/test-project-duplicate-project-config/module-a/garden.yml create mode 100644 garden-service/test/data/test-project-duplicate-project-config/module-b/garden.yml create mode 100644 garden-service/test/data/test-project-duplicate-project-config/module-c/garden.yml create mode 100644 garden-service/test/data/test-project-multiple-module-config/garden.yml create mode 100644 garden-service/test/data/test-project-multiple-module-config/module-a/.garden-version create mode 100644 garden-service/test/data/test-project-multiple-module-config/module-a/garden.yml create mode 100644 garden-service/test/data/test-project-multiple-module-config/module-b/garden.yml create mode 100644 garden-service/test/data/test-project-multiple-module-config/module-c/garden.yml diff --git a/docs/reference/config.md b/docs/reference/config.md index 673c28f11c..20be947a59 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -22,6 +22,14 @@ Configuration for a Garden project. This should be specified in the garden.yml f | Type | Required | | ---- | -------- | | `object` | Yes +### `project.apiVersion` +[project](#project) > apiVersion + +The schema version of this project's config (currently not used). + +| Type | Required | Allowed Values | +| ---- | -------- | -------------- | +| `string` | Yes | "0" ### `project.name` [project](#project) > name @@ -199,6 +207,7 @@ project: ## Project YAML schema ```yaml project: + apiVersion: '0' name: defaultEnvironment: '' environmentDefaults: @@ -224,6 +233,14 @@ Configure a module whose sources are located in this directory. | Type | Required | | ---- | -------- | | `object` | Yes +### `module.apiVersion` +[module](#module) > apiVersion + +The schema version of this module's config (currently not used). + +| Type | Required | Allowed Values | +| ---- | -------- | -------------- | +| `string` | Yes | "0" ### `module.type` [module](#module) > type @@ -371,6 +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' type: name: description: diff --git a/docs/using-garden/configuration-files.md b/docs/using-garden/configuration-files.md index 5f3b859b63..f893b4b0c0 100644 --- a/docs/using-garden/configuration-files.md +++ b/docs/using-garden/configuration-files.md @@ -192,6 +192,48 @@ in your tests. Tests can be run via `garden test`, as well as `garden dev`. +## Multiple modules in the same file + +Sometimes, it's useful to define several modules in the same `garden.yml` file. One common situation is where more than +one Dockerfile is in use (e.g. one for a development build and one for a production build). + +Another is when the dev configuration and the production configuration have different integration testing suites, +which may depend on different external services being available. + +To do this, simply add a document separator (`---`) between the module definitions. Here's a simple example: + +```yaml +module: + description: Hello world container - dev configuration + type: container + dockerfile: Dockerfile-dev + ... + tests: + - name: unit + args: [npm, test] + - name: integ + args: [npm, run, integ-dev] + dependencies: + - hello-function + - dev-integration-testing-backend + +--- + +module: + description: Hello world container - production configuration + type: container + dockerfile: Dockerfile-prod + ... + tests: + - name: unit + args: [npm, test] + - name: integ + args: [npm, run, integ-prod] + dependencies: + - hello-function + - prod-integration-testing-backend +``` + ## Next steps We highly recommend browsing through the [Example projects](../examples/README.md) to see different examples of how projects and modules can be configured. diff --git a/examples/multiple-modules/README.md b/examples/multiple-modules/README.md new file mode 100644 index 0000000000..4adee6c40b --- /dev/null +++ b/examples/multiple-modules/README.md @@ -0,0 +1,31 @@ +# Example project demonstrating several modules/Dockerfiles in one directory + +This project shows how you can configure several modules in a single directory. + +This is useful, for exmample, when you want to use more than one Dockerfile (e.g. one for development, one for production). + +```shell +$ garden deploy +Deploy 🚀 + +✔ dev → Building dev:602ae70cb8-1550064758... → Done (took 9.1 sec) +✔ prod → Building prod:602ae70cb8-1550064758... → Done (took 8.9 sec) +✔ prod → Deploying version 602ae70cb8-1550064758... → Done (took 4 sec) +✔ dev → Deploying version 602ae70cb8-1550064758... → Done (took 3.9 sec) + +Done! ✔️ + +$ garden call dev +✔ Sending HTTP GET request to http://multiple-modules.local.app.garden/hello-dev + +200 OK + +Greetings! This container was built with Dockerfile-dev. + +$ garden call prod +✔ Sending HTTP GET request to http://multiple-modules.local.app.garden/hello-prod + +200 OK + +Greetings! This container was built with Dockerfile-prod. +``` \ No newline at end of file diff --git a/examples/multiple-modules/garden.yml b/examples/multiple-modules/garden.yml new file mode 100644 index 0000000000..6163f53e1f --- /dev/null +++ b/examples/multiple-modules/garden.yml @@ -0,0 +1,6 @@ +project: + name: multiple-modules + environments: + - name: local + providers: + - name: local-kubernetes diff --git a/examples/multiple-modules/node-service/.dockerignore b/examples/multiple-modules/node-service/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/examples/multiple-modules/node-service/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/examples/multiple-modules/node-service/Dockerfile-dev b/examples/multiple-modules/node-service/Dockerfile-dev new file mode 100644 index 0000000000..2e911542e7 --- /dev/null +++ b/examples/multiple-modules/node-service/Dockerfile-dev @@ -0,0 +1,14 @@ +FROM node:9-alpine + +ENV PORT=8080 +ENV ENVIRONMENT=dev +ENV HELLO_PATH=/hello-dev +EXPOSE ${PORT} +WORKDIR /app + +ADD package.json /app +RUN npm install + +ADD . /app + +CMD ["npm", "start"] diff --git a/examples/multiple-modules/node-service/Dockerfile-prod b/examples/multiple-modules/node-service/Dockerfile-prod new file mode 100644 index 0000000000..4583ea16f6 --- /dev/null +++ b/examples/multiple-modules/node-service/Dockerfile-prod @@ -0,0 +1,14 @@ +FROM node:9-alpine + +ENV PORT=8080 +ENV ENVIRONMENT=prod +ENV HELLO_PATH=/hello-prod +EXPOSE ${PORT} +WORKDIR /app + +ADD package.json /app +RUN npm install + +ADD . /app + +CMD ["npm", "start"] diff --git a/examples/multiple-modules/node-service/app.js b/examples/multiple-modules/node-service/app.js new file mode 100644 index 0000000000..3769ece658 --- /dev/null +++ b/examples/multiple-modules/node-service/app.js @@ -0,0 +1,12 @@ +const express = require('express'); +const app = express(); + +// These environment variables are set differently in Dockerfile-dev and Dockerfile-prod +const envName = process.env.ENVIRONMENT; +const helloPath = process.env.HELLO_PATH + +const helloMsg = `Greetings! This container was built with Dockerfile-${envName}.`; + +app.get(helloPath, (req, res) => res.send(helloMsg)); + +module.exports = { app } diff --git a/examples/multiple-modules/node-service/garden.yml b/examples/multiple-modules/node-service/garden.yml new file mode 100644 index 0000000000..0f25705de7 --- /dev/null +++ b/examples/multiple-modules/node-service/garden.yml @@ -0,0 +1,37 @@ +module: + name: dev + description: Node service (dev mode) + dockerfile: Dockerfile-dev + type: container + services: + - name: dev + command: [npm, start] + ports: + - name: http + containerPort: 8080 + ingresses: + - path: /hello-dev + port: http + tests: + - name: unit + args: [npm, test] + +--- + +module: + name: prod + description: Node service (production mode) + dockerfile: Dockerfile-prod + type: container + services: + - name: prod + command: [npm, start] + ports: + - name: http + containerPort: 8080 + ingresses: + - path: /hello-prod + port: http + tests: + - name: unit + args: [npm, test] \ No newline at end of file diff --git a/examples/multiple-modules/node-service/main.js b/examples/multiple-modules/node-service/main.js new file mode 100644 index 0000000000..06833ec64f --- /dev/null +++ b/examples/multiple-modules/node-service/main.js @@ -0,0 +1,3 @@ +const { app } = require('./app'); + +app.listen(process.env.PORT, '0.0.0.0', () => console.log('Node service started')); diff --git a/examples/multiple-modules/node-service/package.json b/examples/multiple-modules/node-service/package.json new file mode 100644 index 0000000000..790546d484 --- /dev/null +++ b/examples/multiple-modules/node-service/package.json @@ -0,0 +1,22 @@ +{ + "name": "node-service", + "version": "1.0.0", + "description": "Simple Node.js docker service", + "main": "main.js", + "scripts": { + "start": "node main.js", + "test": "echo OK", + "integ": "node_modules/mocha/bin/mocha test/integ.js" + }, + "author": "garden.io ", + "license": "ISC", + "dependencies": { + "express": "^4.16.2", + "request": "^2.83.0", + "request-promise": "^4.2.2" + }, + "devDependencies": { + "mocha": "^5.1.1", + "supertest": "^3.0.0" + } +} diff --git a/garden-service/src/cli/cli.ts b/garden-service/src/cli/cli.ts index fbfce8261c..31b798e941 100644 --- a/garden-service/src/cli/cli.ts +++ b/garden-service/src/cli/cli.ts @@ -70,10 +70,10 @@ const DEFAULT_CLI_LOGGER_TYPE = LoggerType.fancy // For initializing garden without a project config export const MOCK_CONFIG: GardenConfig = { - version: "0", dirname: "/", path: process.cwd(), project: { + apiVersion: "0", name: "mock-project", defaultEnvironment: "local", environments: defaultEnvironments, diff --git a/garden-service/src/config/base.ts b/garden-service/src/config/base.ts index f21a928db0..1693e5253c 100644 --- a/garden-service/src/config/base.ts +++ b/garden-service/src/config/base.ts @@ -11,32 +11,26 @@ import { findByName, getNames, } from "../util/util" -import { baseModuleSpecSchema, ModuleConfig } from "./module" -import { validateWithPath } from "./common" -import { ConfigurationError } from "../exceptions" import * as Joi from "joi" import * as yaml from "js-yaml" import { readFile } from "fs-extra" -import { defaultEnvironments, ProjectConfig, projectSchema } from "../config/project" import { omit } from "lodash" +import { baseModuleSpecSchema, ModuleConfig } from "./module" +import { validateWithPath } from "./common" +import { ConfigurationError } from "../exceptions" +import { defaultEnvironments, ProjectConfig, projectSchema } from "../config/project" const CONFIG_FILENAME = "garden.yml" export interface GardenConfig { - version: string dirname: string path: string - module?: ModuleConfig + modules?: ModuleConfig[] project?: ProjectConfig } export const configSchema = Joi.object() .keys({ - // TODO: should this be called apiVersion? - version: Joi.string() - .default("0") - .only("0") - .description("The schema version of the config file (currently not used)."), dirname: Joi.string().meta({ internal: true }), path: Joi.string().meta({ internal: true }), module: baseModuleSpecSchema, @@ -52,7 +46,7 @@ export async function loadConfig(projectRoot: string, path: string): Promise prepareConfigDoc(s, path, projectRoot)) + + const projectSpecs = specs.filter(s => s.project) + + if (projectSpecs.length > 1) { + throw new ConfigurationError(`Multiple project declarations in ${path}`, { projectSpecs }) + } + + const project = projectSpecs[0] ? projectSpecs[0].project : undefined + const modules: ModuleConfig[] = specs.filter(s => s.module).map(s => s.module!) + + const dirname = basename(path) + + return { + dirname, + path, + modules: modules.length > 0 ? modules : undefined, + project, + } +} + +type ConfigDoc = { + module?: ModuleConfig, + project?: ProjectConfig, +} + +/** + * Each YAML document in a garden.yml file consists of a project definition and/or a module definition. + */ +function prepareConfigDoc(spec: any, path: string, projectRoot: string): ConfigDoc { + if (spec.project) { + spec.project = prepareProjectConfig(spec.project, path) + } + if (spec.module) { - /* - We allow specifying modules by name only as a shorthand: - - dependencies: - - foo-module - - name: foo-module // same as the above - */ - if (spec.module.build && spec.module.build.dependencies) { - spec.module.build.dependencies = spec.module.build.dependencies - .map(dep => (typeof dep) === "string" ? { name: dep } : dep) - } + spec.module = prepareModuleConfig(spec.module, path) } - const parsed = validateWithPath({ + return validateWithPath({ config: spec, schema: configSchema, configType: "config", - path: absPath, + path, projectRoot, }) +} - const dirname = basename(path) - const project = parsed.project - let moduleConfig = parsed.module - - if (project) { - // we include the default local environment unless explicitly overridden - for (const env of defaultEnvironments) { - if (!findByName(project.environments, env.name)) { - project.environments.push(env) - } - } +function prepareProjectConfig(projectSpec: any, path: string): ProjectConfig { + + const validatedSpec = validateWithPath({ + config: projectSpec, + schema: projectSchema, + configType: "project", + path, + projectRoot: path, // If there's a project spec, we can assume path === projectRoot. + }) - // the default environment is the first specified environment in the config, unless specified - const defaultEnvironment = project.defaultEnvironment - - if (defaultEnvironment === "") { - project.defaultEnvironment = project.environments[0].name - } else { - if (!findByName(project.environments, defaultEnvironment)) { - throw new ConfigurationError(`The specified default environment ${defaultEnvironment} is not defined`, { - defaultEnvironment, - availableEnvironments: getNames(project.environments), - }) - } + if (!validatedSpec.environments) { + validatedSpec.environments = defaultEnvironments + } + + // we include the default local environment unless explicitly overridden + for (const env of defaultEnvironments) { + if (!findByName(validatedSpec.environments, env.name)) { + validatedSpec.environments.push(env) } } - if (moduleConfig) { - // Built-in keys are validated here and the rest are put into the `spec` field - moduleConfig = { - allowPublish: moduleConfig.allowPublish, - build: moduleConfig.build, - description: moduleConfig.description, - name: moduleConfig.name, - outputs: {}, - path, - repositoryUrl: moduleConfig.repositoryUrl, - serviceConfigs: [], - spec: omit(moduleConfig, baseModuleSchemaKeys), - testConfigs: [], - type: moduleConfig.type, - taskConfigs: [], + // the default environment is the first specified environment in the config, unless specified + const defaultEnvironment = validatedSpec.defaultEnvironment + + if (defaultEnvironment === "") { + validatedSpec.defaultEnvironment = validatedSpec.environments[0].name + } else { + if (!findByName(validatedSpec.environments, defaultEnvironment)) { + throw new ConfigurationError(`The specified default environment ${defaultEnvironment} is not defined`, { + defaultEnvironment, + availableEnvironments: getNames(validatedSpec.environments), + }) } } - return { - version: parsed.version, - dirname, + return validatedSpec +} + +function prepareModuleConfig(moduleSpec: any, path: string): ModuleConfig { + // Built-in keys are validated here and the rest are put into the `spec` field + const module = { + apiVersion: moduleSpec.apiVersion, + allowPublish: moduleSpec.allowPublish, + build: moduleSpec.build, + description: moduleSpec.description, + name: moduleSpec.name, + outputs: {}, path, - module: moduleConfig, - project, + repositoryUrl: moduleSpec.repositoryUrl, + serviceConfigs: [], + spec: omit(moduleSpec, baseModuleSchemaKeys), + testConfigs: [], + type: moduleSpec.type, + taskConfigs: [], } + + /* + We allow specifying modules by name only as a shorthand: + + dependencies: + - foo-module + - name: foo-module // same as the above + */ + if (module.build && module.build.dependencies) { + module.build.dependencies = module.build.dependencies + .map(dep => (typeof dep) === "string" ? { name: dep } : dep) + } + + return module } export async function findProjectConfig(path: string): Promise { diff --git a/garden-service/src/config/module.ts b/garden-service/src/config/module.ts index e161ee8488..443466536a 100644 --- a/garden-service/src/config/module.ts +++ b/garden-service/src/config/module.ts @@ -65,6 +65,7 @@ export interface BuildConfig { export interface ModuleSpec { } export interface BaseModuleSpec { + apiVersion: string allowPublish: boolean build: BuildConfig description?: string @@ -76,6 +77,10 @@ export interface BaseModuleSpec { export const baseModuleSpecSchema = Joi.object() .keys({ + apiVersion: Joi.string() + .default("0") + .only("0") + .description("The schema version of this module's config (currently not used)."), type: joiIdentifier() .required() .description("The type of this module.") diff --git a/garden-service/src/config/project.ts b/garden-service/src/config/project.ts index 6719274641..c2b9984a99 100644 --- a/garden-service/src/config/project.ts +++ b/garden-service/src/config/project.ts @@ -87,6 +87,7 @@ export const projectSourcesSchema = joiArray(projectSourceSchema) .description("A list of remote sources to import into project.") export interface ProjectConfig { + apiVersion: string, name: string defaultEnvironment: string environmentDefaults: CommonEnvironmentConfig @@ -122,6 +123,10 @@ export const projectNameSchema = joiIdentifier() export const projectSchema = Joi.object() .keys({ + apiVersion: Joi.string() + .default("0") + .only("0") + .description("The schema version of this project's config (currently not used)."), name: projectNameSchema, defaultEnvironment: Joi.string() .default("", "") @@ -134,7 +139,6 @@ export const projectSchema = Joi.object() ), environments: joiArray(environmentConfigSchema.keys({ name: joiUserIdentifier().required() })) .unique("name") - .default(() => ({ ...defaultEnvironments }), safeDump(defaultEnvironments)) .description("A list of environments to configure for the project.") .example([defaultEnvironments, {}]), sources: projectSourcesSchema, diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index fd87caf771..a6d62d2e97 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -403,15 +403,18 @@ export class Garden { this.loadedPlugins[pluginName] = plugin for (const modulePath of plugin.modules || []) { - let moduleConfig = await this.loadModuleConfig(modulePath) - if (!moduleConfig) { - throw new PluginError(`Could not load module "${modulePath}" specified in plugin "${pluginName}"`, { + let moduleConfigs = await this.loadModuleConfigs(modulePath) + if (!moduleConfigs) { + throw new PluginError(`Could not load module(s) at "${modulePath}" specified in plugin "${pluginName}"`, { pluginName, modulePath, }) } - moduleConfig.plugin = pluginName - this.pluginModuleConfigs.push(moduleConfig) + + for (const moduleConfig of moduleConfigs) { + moduleConfig.plugin = pluginName + this.pluginModuleConfigs.push(moduleConfig) + } } const actions = plugin.actions || {} @@ -609,9 +612,9 @@ export class Garden { const rawConfigs: ModuleConfig[] = [...this.pluginModuleConfigs] await Bluebird.map(modulePaths, async path => { - const config = await this.loadModuleConfig(path) - if (config) { - rawConfigs.push(config) + const configs = await this.loadModuleConfigs(path) + if (configs) { + rawConfigs.push(...configs) } }) @@ -657,24 +660,23 @@ export class Garden { * * @param path Directory containing the module */ - private async loadModuleConfig(path: string): Promise { + private async loadModuleConfigs(path: string): Promise { const config = await loadConfig(this.projectRoot, resolve(this.projectRoot, path)) - if (!config || !config.module) { + if (!config || !config.modules) { return null } - const moduleConfig = cloneDeep(config.module) - - if (moduleConfig.repositoryUrl) { - moduleConfig.path = await this.loadExtSourcePath({ - name: moduleConfig.name, - repositoryUrl: moduleConfig.repositoryUrl, - sourceType: "module", - }) - } - - return moduleConfig + return Bluebird.map(cloneDeep(config.modules), async (moduleConfig) => { + if (moduleConfig.repositoryUrl) { + moduleConfig.path = await this.loadExtSourcePath({ + name: moduleConfig.name, + repositoryUrl: moduleConfig.repositoryUrl, + sourceType: "module", + }) + } + return moduleConfig + }) } //=========================================================================== diff --git a/garden-service/src/plugins/kubernetes/system.ts b/garden-service/src/plugins/kubernetes/system.ts index 53ad297fa7..6619cccf67 100644 --- a/garden-service/src/plugins/kubernetes/system.ts +++ b/garden-service/src/plugins/kubernetes/system.ts @@ -24,10 +24,10 @@ export async function getSystemGarden(provider: KubernetesProvider): Promise ({ }) return { + apiVersion: "0", allowPublish: true, build: { command: [], diff --git a/garden-service/src/plugins/openfaas/openfaas.ts b/garden-service/src/plugins/openfaas/openfaas.ts index df94093429..06a9ec04e5 100644 --- a/garden-service/src/plugins/openfaas/openfaas.ts +++ b/garden-service/src/plugins/openfaas/openfaas.ts @@ -464,10 +464,10 @@ export async function getOpenFaasGarden(ctx: PluginContext): Promise { return Garden.factory(systemProjectPath, { environmentName: "default", config: { - version: "0", dirname: "system", path: systemProjectPath, project: { + apiVersion: "0", name: `${ctx.projectName}-openfaas`, environmentDefaults: { providers: [], diff --git a/garden-service/test/data/test-project-duplicate-project-config/garden.yml b/garden-service/test/data/test-project-duplicate-project-config/garden.yml new file mode 100644 index 0000000000..075bcbf27a --- /dev/null +++ b/garden-service/test/data/test-project-duplicate-project-config/garden.yml @@ -0,0 +1,30 @@ +project: + name: test-project + environmentDefaults: + variables: + some: variable + environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other + +--- + +project: + name: test-project-duplicate + environmentDefaults: + variables: + some: variable + environments: + - name: local + providers: + - name: test-plugin + - name: other + +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-duplicate-project-config/module-a/.garden-version b/garden-service/test/data/test-project-duplicate-project-config/module-a/.garden-version new file mode 100644 index 0000000000..1d754b7141 --- /dev/null +++ b/garden-service/test/data/test-project-duplicate-project-config/module-a/.garden-version @@ -0,0 +1,4 @@ +{ + "latestCommit": "1234567890", + "dirtyTimestamp": null +} diff --git a/garden-service/test/data/test-project-duplicate-project-config/module-a/garden.yml b/garden-service/test/data/test-project-duplicate-project-config/module-a/garden.yml new file mode 100644 index 0000000000..f97df8e328 --- /dev/null +++ b/garden-service/test/data/test-project-duplicate-project-config/module-a/garden.yml @@ -0,0 +1,47 @@ +module: + 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] + +--- + +module: + name: module-a2 + type: test + services: + - name: service-a2 + build: + command: [echo, A2] + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-a2 + command: [echo, OK] + +--- + +module: + name: module-a3 + type: test + services: + - name: service-a3 + build: + command: [echo, A3] + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-a3 + command: [echo, OK] diff --git a/garden-service/test/data/test-project-duplicate-project-config/module-b/garden.yml b/garden-service/test/data/test-project-duplicate-project-config/module-b/garden.yml new file mode 100644 index 0000000000..254d3b6dd0 --- /dev/null +++ b/garden-service/test/data/test-project-duplicate-project-config/module-b/garden.yml @@ -0,0 +1,37 @@ +module: + name: module-b1 + type: test + services: + - name: service-b1 + dependencies: + - service-a1 + build: + command: [echo, B] + dependencies: + - module-a1 + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-b1 + command: [echo, OK] + +--- + +module: + name: module-b2 + type: test + services: + - name: service-b2 + dependencies: + - service-a1 + build: + command: [echo, B] + dependencies: + - module-a2 + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-b2 + command: [echo, OK] diff --git a/garden-service/test/data/test-project-duplicate-project-config/module-c/garden.yml b/garden-service/test/data/test-project-duplicate-project-config/module-c/garden.yml new file mode 100644 index 0000000000..2fb28d6314 --- /dev/null +++ b/garden-service/test/data/test-project-duplicate-project-config/module-c/garden.yml @@ -0,0 +1,14 @@ +module: + name: module-c + type: test + services: + - name: service-c + build: + dependencies: + - module-b1 + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-c + command: [echo, OK] diff --git a/garden-service/test/data/test-project-multiple-module-config/garden.yml b/garden-service/test/data/test-project-multiple-module-config/garden.yml new file mode 100644 index 0000000000..c8aed85f24 --- /dev/null +++ b/garden-service/test/data/test-project-multiple-module-config/garden.yml @@ -0,0 +1,19 @@ +project: + name: test-project-multiple-modules + environmentDefaults: + variables: + some: variable + environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other + +--- + +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-multiple-module-config/module-a/.garden-version b/garden-service/test/data/test-project-multiple-module-config/module-a/.garden-version new file mode 100644 index 0000000000..1d754b7141 --- /dev/null +++ b/garden-service/test/data/test-project-multiple-module-config/module-a/.garden-version @@ -0,0 +1,4 @@ +{ + "latestCommit": "1234567890", + "dirtyTimestamp": null +} diff --git a/garden-service/test/data/test-project-multiple-module-config/module-a/garden.yml b/garden-service/test/data/test-project-multiple-module-config/module-a/garden.yml new file mode 100644 index 0000000000..ba1fa405ed --- /dev/null +++ b/garden-service/test/data/test-project-multiple-module-config/module-a/garden.yml @@ -0,0 +1,31 @@ +module: + 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] + +--- + +module: + name: module-a2 + type: test + services: + - name: service-a2 + build: + command: [echo, A2] + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-a2 + command: [echo, OK] \ No newline at end of file diff --git a/garden-service/test/data/test-project-multiple-module-config/module-b/garden.yml b/garden-service/test/data/test-project-multiple-module-config/module-b/garden.yml new file mode 100644 index 0000000000..254d3b6dd0 --- /dev/null +++ b/garden-service/test/data/test-project-multiple-module-config/module-b/garden.yml @@ -0,0 +1,37 @@ +module: + name: module-b1 + type: test + services: + - name: service-b1 + dependencies: + - service-a1 + build: + command: [echo, B] + dependencies: + - module-a1 + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-b1 + command: [echo, OK] + +--- + +module: + name: module-b2 + type: test + services: + - name: service-b2 + dependencies: + - service-a1 + build: + command: [echo, B] + dependencies: + - module-a2 + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-b2 + command: [echo, OK] diff --git a/garden-service/test/data/test-project-multiple-module-config/module-c/garden.yml b/garden-service/test/data/test-project-multiple-module-config/module-c/garden.yml new file mode 100644 index 0000000000..2fb28d6314 --- /dev/null +++ b/garden-service/test/data/test-project-multiple-module-config/module-c/garden.yml @@ -0,0 +1,14 @@ +module: + name: module-c + type: test + services: + - name: service-c + build: + dependencies: + - module-b1 + tests: + - name: unit + command: [echo, OK] + tasks: + - name: task-c + command: [echo, OK] diff --git a/garden-service/test/helpers.ts b/garden-service/test/helpers.ts index 4b0db93d54..fa00dfe986 100644 --- a/garden-service/test/helpers.ts +++ b/garden-service/test/helpers.ts @@ -220,6 +220,7 @@ export const testPluginC: PluginFactory = async (params) => { } const defaultModuleConfig: ModuleConfig = { + apiVersion: "0", 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 bd81bd6b6b..6aa9de2f42 100644 --- a/garden-service/test/src/config/base.ts +++ b/garden-service/test/src/config/base.ts @@ -6,6 +6,11 @@ import { dataDir, expectError } from "../../helpers" const projectPathA = resolve(dataDir, "test-project-a") const modulePathA = resolve(projectPathA, "module-a") +const projectPathMultipleModules = resolve(dataDir, "test-project-multiple-module-config") +const modulePathAMultiple = resolve(projectPathMultipleModules, "module-a") + +const projectPathDuplicateProjects = resolve(dataDir, "test-project-duplicate-project-config") + describe("loadConfig", () => { it("should not throw an error if no file was found", async () => { @@ -37,6 +42,7 @@ describe("loadConfig", () => { const parsed = await loadConfig(projectPathA, projectPathA) expect(parsed!.project).to.eql({ + apiVersion: "0", name: "test-project-a", defaultEnvironment: "local", sources: [], @@ -65,32 +71,141 @@ describe("loadConfig", () => { it("should load and parse a module config", async () => { const parsed = await loadConfig(projectPathA, modulePathA) - expect(parsed!.module).to.eql({ - name: "module-a", + expect(parsed!.modules).to.eql([ + { + apiVersion: "0", + name: "module-a", + type: "test", + description: undefined, + repositoryUrl: undefined, + allowPublish: true, + build: { command: ["echo", "A"], dependencies: [] }, + outputs: {}, + path: modulePathA, + + spec: { + services: [{ name: "service-a" }], + tasks: [{ + name: "task-a", + command: ["echo", "OK"], + }], + tests: [{ + name: "unit", + command: ["echo", "OK"], + }], + }, + + serviceConfigs: [], + taskConfigs: [], + testConfigs: [], + }, + ]) + }) + + it("should load and parse a config file defining a project and a module", async () => { + const parsed = await loadConfig(projectPathMultipleModules, projectPathMultipleModules) + + expect(parsed!.project).to.eql({ + apiVersion: "0", + defaultEnvironment: "local", + environmentDefaults: { + providers: [], + variables: { + some: "variable", + }, + }, + environments: [ + { + name: "local", + providers: [ + { name: "test-plugin" }, + { name: "test-plugin-b" }, + ], + variables: {}, + }, + { + name: "other", + providers: [], + variables: {}, + }, + ], + name: "test-project-multiple-modules", + sources: [], + }) + + expect(parsed!.modules).to.eql([{ + apiVersion: "0", + name: "module-from-project-config", type: "test", description: undefined, repositoryUrl: undefined, allowPublish: true, - build: { command: ["echo", "A"], dependencies: [] }, + build: { command: ["echo", "project"], dependencies: [] }, outputs: {}, - path: modulePathA, - - spec: { - services: [{ name: "service-a" }], - tasks: [{ - name: "task-a", - command: ["echo", "OK"], - }], - tests: [{ - name: "unit", - command: ["echo", "OK"], - }], - }, - + path: projectPathMultipleModules, serviceConfigs: [], - taskConfigs: [], + spec: {}, testConfigs: [], - }) + taskConfigs: [], + }]) + }) + + it("should load and parse a config file defining multiple modules", async () => { + const parsed = await loadConfig(projectPathMultipleModules, modulePathAMultiple) + + expect(parsed!.modules).to.eql([ + { + apiVersion: "0", + name: "module-a1", + type: "test", + allowPublish: true, + description: undefined, + repositoryUrl: undefined, + build: { + command: ["echo", "A1"], + dependencies: [ + { name: "module-from-project-config", copy: [] }, + ], + }, + outputs: {}, + path: modulePathAMultiple, + serviceConfigs: [], + spec: { + services: [{ name: "service-a1" }], + tests: [{ name: "unit", command: ["echo", "OK"] }], + tasks: [{ name: "task-a1", command: ["echo", "OK"] }], + }, + testConfigs: [], + taskConfigs: [], + }, + { + apiVersion: "0", + name: "module-a2", + type: "test", + allowPublish: true, + description: undefined, + repositoryUrl: undefined, + build: { command: ["echo", "A2"], dependencies: [] }, + outputs: {}, + path: modulePathAMultiple, + serviceConfigs: [], + spec: { + services: [{ name: "service-a2" }], + tests: [{ name: "unit", command: ["echo", "OK"] }], + tasks: [{ name: "task-a2", command: ["echo", "OK"] }], + }, + testConfigs: [], + taskConfigs: [], + }, + ]) + }) + + it("should throw an error when parsing a config file defining multiple projects", async () => { + await expectError( + async () => await loadConfig(projectPathDuplicateProjects, projectPathDuplicateProjects), + (err) => { + expect(err.message).to.match(/Multiple project declarations/) + }) }) it("should return undefined if config file is not found", async () => { diff --git a/garden-service/test/src/garden.ts b/garden-service/test/src/garden.ts index 9480003599..50985f3242 100644 --- a/garden-service/test/src/garden.ts +++ b/garden-service/test/src/garden.ts @@ -130,6 +130,21 @@ describe("Garden", () => { expect(getNames(modules).sort()).to.eql(["module-a", "module-b", "module-c"]) }) + it("should scan and add modules for projects with configs defining multiple modules", async () => { + const garden = await makeTestGarden(resolve(dataDir, "test-project-multiple-module-config")) + await garden.scanModules() + + const modules = await garden.resolveModuleConfigs() + expect(getNames(modules).sort()).to.eql([ + "module-a1", + "module-a2", + "module-b1", + "module-b2", + "module-c", + "module-from-project-config", + ]) + }) + it("should scan and add modules for projects with external project sources", async () => { const garden = await makeTestGarden(resolve(dataDir, "test-project-ext-project-sources")) @@ -160,19 +175,19 @@ describe("Garden", () => { }) }) - describe("loadModuleConfig", () => { + describe("loadModuleConfigs", () => { it("should resolve module by absolute path", async () => { const garden = await makeTestGardenA() const path = join(projectRootA, "module-a") - const module = await (garden).loadModuleConfig(path) + const module = (await (garden).loadModuleConfigs(path))[0] expect(module!.name).to.equal("module-a") }) it("should resolve module by relative path to project root", async () => { const garden = await makeTestGardenA() - const module = await (garden).loadModuleConfig("./module-a") + const module = (await (garden).loadModuleConfigs("./module-a"))[0] expect(module!.name).to.equal("module-a") }) @@ -181,7 +196,7 @@ describe("Garden", () => { const garden = await makeTestGarden(projectRoot) stubGitCli() - const module = await (garden).loadModuleConfig("./module-a") + const module = (await (garden).loadModuleConfigs("./module-a"))[0] const repoUrlHash = hashRepoUrl(module!.repositoryUrl!) expect(module!.path).to.equal(join(projectRoot, ".garden", "sources", "module", `module-a--${repoUrlHash}`)) diff --git a/garden-service/test/src/plugins/container.ts b/garden-service/test/src/plugins/container.ts index ad4f924aea..e8a51a65b1 100644 --- a/garden-service/test/src/plugins/container.ts +++ b/garden-service/test/src/plugins/container.ts @@ -34,6 +34,7 @@ describe("plugins.container", () => { command: [], dependencies: [], }, + apiVersion: "0", name: "test", outputs: {}, path: modulePath, @@ -136,6 +137,7 @@ describe("plugins.container", () => { dependencies: [], }, name: "test", + apiVersion: "0", outputs: {}, path: modulePath, type: "container", @@ -189,6 +191,7 @@ describe("plugins.container", () => { command: ["echo", "OK"], dependencies: [], }, + apiVersion: "0", name: "module-a", outputs: {}, path: modulePath, @@ -250,6 +253,7 @@ describe("plugins.container", () => { expect(result).to.eql({ allowPublish: false, build: { command: ["echo", "OK"], dependencies: [] }, + apiVersion: "0", name: "module-a", outputs: {}, path: modulePath, @@ -366,6 +370,7 @@ describe("plugins.container", () => { command: ["echo", "OK"], dependencies: [], }, + apiVersion: "0", name: "module-a", outputs: {}, path: modulePath, @@ -422,6 +427,7 @@ describe("plugins.container", () => { command: ["echo", "OK"], dependencies: [], }, + apiVersion: "0", name: "module-a", outputs: {}, path: modulePath, @@ -473,6 +479,7 @@ describe("plugins.container", () => { command: ["echo", "OK"], dependencies: [], }, + apiVersion: "0", 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 987c65e35f..558573a421 100644 --- a/garden-service/test/src/plugins/kubernetes/container/ingress.ts +++ b/garden-service/test/src/plugins/kubernetes/container/ingress.ts @@ -336,6 +336,7 @@ describe("createIngresses", () => { command: [], dependencies: [], }, + apiVersion: "0", 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 0c891e3c55..a7d3bd626e 100644 --- a/garden-service/test/src/plugins/kubernetes/helm/config.ts +++ b/garden-service/test/src/plugins/kubernetes/helm/config.ts @@ -43,6 +43,7 @@ describe("validateHelmModule", () => { command: [], }, description: "The API backend for the voting UI", + apiVersion: "0", name: "api", outputs: { "release-name": "api-release",