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",