diff --git a/docs/reference/commands.md b/docs/reference/commands.md index b5f28c73b4..792d6a799d 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -449,6 +449,39 @@ Examples: | `--follow` | `-f` | boolean | Continuously stream new logs from the service(s). | `--tail` | `-t` | number | Number of lines to show for each service. Defaults to -1, showing all log lines. +### garden migrate + +Migrate `garden.yml` configuration files to version v0.11.x + +Scans the project for `garden.yml` configuration files and updates those that are not compatible with version v0.11. +By default the command prints the updated versions to the terminal. You can optionally update the files in place with the `write` flag. + +Note: This command does not validate the configs per se. It will simply try to convert a given configuration file so that +it is compatible with version v0.11 or greater, regardless of whether that file was ever a valid Garden config. It is therefore +recommended that this is used on existing `garden.yml` files that were valid in version v0.10.x. + +Examples: + + garden migrate # scans all garden.yml files and prints the updated versions along with the paths to them. + garden migrate --write # scans all garden.yml files and overwrites them with the updated versions. + garden migrate ./garden.yml # scans the provided garden.yml file and prints the updated version. + +##### Usage + + garden migrate [configPaths] [options] + +##### Arguments + +| Argument | Required | Description | +| -------- | -------- | ----------- | + | `configPaths` | No | Specify the path to a `garden.yml` file to convert. Use comma as a separator to specify multiple files. + +##### Options + +| Argument | Alias | Type | Description | +| -------- | ----- | ---- | ----------- | + | `--write` | | boolean | Update the `garden.yml` in place. + ### garden options Print global options. diff --git a/garden-service/src/commands/commands.ts b/garden-service/src/commands/commands.ts index 593161af6c..c0a66013a4 100644 --- a/garden-service/src/commands/commands.ts +++ b/garden-service/src/commands/commands.ts @@ -15,6 +15,7 @@ import { DevCommand } from "./dev" import { GetCommand } from "./get/get" import { LinkCommand } from "./link/link" import { LogsCommand } from "./logs" +import { MigrateCommand } from "./migrate" import { PublishCommand } from "./publish" import { RunCommand } from "./run/run" import { ScanCommand } from "./scan" @@ -39,6 +40,7 @@ export const coreCommands: Command[] = [ new GetCommand(), new LinkCommand(), new LogsCommand(), + new MigrateCommand(), new OptionsCommand(), new PluginsCommand(), new PublishCommand(), diff --git a/garden-service/src/commands/migrate.ts b/garden-service/src/commands/migrate.ts new file mode 100644 index 0000000000..f54e69f394 --- /dev/null +++ b/garden-service/src/commands/migrate.ts @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Command, CommandParams, CommandResult, BooleanParameter, StringsParameter } from "./base" +import yaml, { safeDump } from "js-yaml" +import { dedent } from "../util/string" +import { readFile, writeFile } from "fs-extra" +import { cloneDeep, isEqual } from "lodash" +import { ConfigurationError, RuntimeError } from "../exceptions" +import { basename, resolve, parse } from "path" +import { findConfigPathsInPath, getConfigFilePath } from "../util/fs" +import { GitHandler } from "../vcs/git" +import { DEFAULT_GARDEN_DIR_NAME } from "../constants" +import { exec } from "../util/util" +import { LoggerType } from "../logger/logger" +import Bluebird from "bluebird" + +const migrateOptions = { + write: new BooleanParameter({ help: "Update the `garden.yml` in place." }), +} + +const migrateArguments = { + configPaths: new StringsParameter({ + help: "Specify the path to a `garden.yml` file to convert. Use comma as a separator to specify multiple files.", + }), +} + +type Args = typeof migrateArguments +type Opts = typeof migrateOptions + +interface UpdatedConfig { + path: string + specs: any[] +} + +export interface MigrateCommandResult { + updatedConfigs: UpdatedConfig[] +} + +export class MigrateCommand extends Command { + name = "migrate" + noProject = true + loggerType: LoggerType = "basic" + arguments = migrateArguments + options = migrateOptions + help = "Migrate `garden.yml` configuration files to version v0.11.x" + + description = dedent` + Scans the project for \`garden.yml\` configuration files and updates those that are not compatible with version v0.11. + By default the command prints the updated versions to the terminal. You can optionally update the files in place with the \`write\` flag. + + Note: This command does not validate the configs per se. It will simply try to convert a given configuration file so that + it is compatible with version v0.11 or greater, regardless of whether that file was ever a valid Garden config. It is therefore + recommended that this is used on existing \`garden.yml\` files that were valid in version v0.10.x. + + Examples: + + garden migrate # scans all garden.yml files and prints the updated versions along with the paths to them. + garden migrate --write # scans all garden.yml files and overwrites them with the updated versions. + garden migrate ./garden.yml # scans the provided garden.yml file and prints the updated version. + + ` + + async action({ log, args, opts }: CommandParams): Promise> { + // opts.root defaults to current directory + const root = await findRoot(opts.root) + if (!root) { + throw new ConfigurationError(`Not a project directory (or any of the parent directories): ${opts.root}`, { + root: opts.root, + }) + } + + const updatedConfigs: { path: string; specs: any[] }[] = [] + + let configPaths: string[] = [] + if (args.configPaths && args.configPaths.length > 0) { + configPaths = args.configPaths.map((path) => resolve(root, path)) + } else { + const vcs = new GitHandler(resolve(root, DEFAULT_GARDEN_DIR_NAME), []) + configPaths = await findConfigPathsInPath({ + dir: root, + vcs, + log, + }) + } + + // Iterate over configs and update specs if needed + for (const configPath of configPaths) { + const specs = await readYaml(configPath) + const updatedSpecs = specs.map((spec) => + [spec] + .map((s) => applyFlatStyle(s)) + .map((s) => removeLocalOpenFaas(s)) + .map((s) => removeEnvironmentDefaults(s, configPath)) + .pop() + ) + + // Nothing to do + if (isEqual(specs, updatedSpecs)) { + continue + } + + updatedConfigs.push({ + path: configPath, + specs: updatedSpecs, + }) + } + + // Throw if any config files have been modified so that user changes don't get overwritten + if (opts.write) { + const dirtyConfigs = await Bluebird.map(updatedConfigs, async ({ path }) => { + const modified = !!( + await exec("git", ["ls-files", "-m", "--others", "--exclude-standard", path], { cwd: root }) + ).stdout + if (modified) { + return path + } + return null + }).filter(Boolean) + if (dirtyConfigs.length > 0) { + const msg = dedent` + Config files at the following paths are dirty:\n + ${dirtyConfigs.join("\n")} + + Please commit them before applying this command with the --write flag + ` + throw new RuntimeError(msg, { dirtyConfigs }) + } + } + + // Iterate over updated configs and print or write + for (const { path, specs } of updatedConfigs) { + const out = dumpSpec(specs) + + if (opts.write) { + log.info(`Updating file at path ${path}`) + await writeFile(path, out) + } else { + if (configPaths.length > 1) { + log.info(`# Updated config for garden.yml file at path ${path}`) + } + log.info(out) + } + } + + if (updatedConfigs.length === 0) { + log.info("Nothing to update.") + } else if (opts.write) { + log.info("") + log.info("Finished updating config files. Please review the changes before commiting them.") + } + + return { result: { updatedConfigs } } + } +} + +/** + * Dump JSON specs to YAML. Join specs by `---`. + */ +export function dumpSpec(specs: any[]) { + return specs.map((spec) => safeDump(spec)).join("\n---\n\n") +} + +/** + * Recursively search for the project root by checking if the path has a project level `garden.yml` file + */ +async function findRoot(path: string): Promise { + const configFilePath = await getConfigFilePath(path) + let isProjectRoot = false + try { + const rawSpecs = await readYaml(configFilePath) + isProjectRoot = rawSpecs.find((spec) => !!spec.project || spec.kind === "Project") + } catch (err) { + // no op + } + if (isProjectRoot) { + return path + } + + // We're at the file system root and no project file was found + if (parse(path).root) { + return null + } + return findRoot(resolve(path, "..")) +} + +/** + * Read the contents of a YAML file and dump to JSON + */ +async function readYaml(path: string) { + let rawSpecs: any[] + const fileData = await readFile(path) + + try { + rawSpecs = yaml.safeLoadAll(fileData.toString()) || [] + } catch (err) { + throw new ConfigurationError(`Could not parse ${basename(path)} in directory ${path} as valid YAML`, err) + } + + // Ignore empty resources + return rawSpecs.filter(Boolean) +} + +/** + * Returns a spec with the flat config style. + * + * That is, this: + * ```yaml + * project: + * providers: + * ... + * ``` + * becomes: + * ```yaml + * kind: Project: + * providers: + * ... + * ``` + */ +function applyFlatStyle(spec: any) { + if (spec.project) { + const project = cloneDeep(spec.project) + return { + kind: "Project", + ...project, + } + } else if (spec.module) { + const module = cloneDeep(spec.module) + return { + kind: "Module", + ...module, + } + } + return cloneDeep(spec) +} + +/** + * Returns a spec with `local-openfaas` set to `openfaas` at both the provider and module type level. + * Remove the `local-openfaas` provider if `openfaas` is already configured. + */ +function removeLocalOpenFaas(spec: any) { + const clone = cloneDeep(spec) + const isProject = spec.kind === "Project" + + // Remove local-openfaas from modules + if (spec.type === "local-openfaas") { + clone.type = "openfaas" + } + + // Remove local-openfaas from projects + if (isProject) { + let hasOpenfaas = false + + // Provider nested under environment + if ((spec.environments || []).length > 0) { + for (const [envIdx, env] of spec.environments.entries()) { + if (!env.providers) { + continue + } + + for (const [providerIdx, provider] of env.providers.entries()) { + hasOpenfaas = !!env.providers.find((p) => p.name === "openfaas") + if (provider.name === "local-openfaas" && hasOpenfaas) { + // openfaas provider is already configured so we remove the local-openfaas provider + clone.environments[envIdx].providers.splice(providerIdx, 1) + } else if (provider.name === "local-openfaas") { + // otherwise we rename it + clone.environments[envIdx].providers[providerIdx].name = "openfaas" + } + } + } + } + + // Provider nested under environment + if (spec.providers) { + hasOpenfaas = !!spec.providers.find((p) => p.name === "openfaas") + for (const [providerIdx, provider] of spec.providers.entries()) { + if (provider.name === "local-openfaas" && hasOpenfaas) { + clone.providers.splice(providerIdx, 1) + } else if (provider.name === "local-openfaas") { + clone.providers[providerIdx].name = "openfaas" + } + } + } + } + return clone +} + +/** + * Returns a spec with the `environmentDefaults` field removed and its contents mapped + * to their respective top-level keys. + */ +function removeEnvironmentDefaults(spec: any, path: string) { + const clone = cloneDeep(spec) + + if (spec.environmentDefaults) { + if (spec.environmentDefaults.varfile) { + if (spec.varfile) { + const msg = dedent` + Found a project level \`varfile\` field with value ${spec.varfile} in config at path ${path} + when attempting to re-assign the \`varfile\` field under the + \`environmentDefaults\` directive (with value ${spec.environmentDefaults.varfile}). + Please resolve manually and then run this command again. + ` + throw new ConfigurationError(msg, { path }) + } else { + clone.varfile = spec.environmentDefaults.varfile + } + } + if (spec.environmentDefaults.variables) { + // Merge variables + clone.variables = { + ...(spec.variables || {}), + ...spec.environmentDefaults.variables, + } + } + + if (spec.environmentDefaults.providers) { + const providers = cloneDeep(spec.providers) || [] + const envProviders = cloneDeep(spec.environmentDefaults.providers) + clone.providers = [...providers, ...envProviders] + } + delete clone.environmentDefaults + } + return clone +} diff --git a/garden-service/test/data/test-projects/v10-configs-errors/garden.yml b/garden-service/test/data/test-projects/v10-configs-errors/garden.yml new file mode 100644 index 0000000000..6a701c4997 --- /dev/null +++ b/garden-service/test/data/test-projects/v10-configs-errors/garden.yml @@ -0,0 +1,10 @@ +kind: Project +name: test-project-v10-config-errors +environments: + - name: local + - name: other +providers: + - name: test-plugin + environments: [local] + - name: test-plugin-b + environments: [other] diff --git a/garden-service/test/data/test-projects/v10-configs-errors/project-varfile/garden.yml b/garden-service/test/data/test-projects/v10-configs-errors/project-varfile/garden.yml new file mode 100644 index 0000000000..da1b0e9c06 --- /dev/null +++ b/garden-service/test/data/test-projects/v10-configs-errors/project-varfile/garden.yml @@ -0,0 +1,13 @@ +kind: Project +name: test-project-v10-config-errors-varfile +environments: + - name: local + - name: other +environmentDefaults: + varfile: foo.env +varfile: bar.env +providers: + - name: test-plugin + environments: [local] + - name: test-plugin-b + environments: [other] diff --git a/garden-service/test/data/test-projects/v10-configs/garden.yml b/garden-service/test/data/test-projects/v10-configs/garden.yml new file mode 100644 index 0000000000..a05f8e3f20 --- /dev/null +++ b/garden-service/test/data/test-projects/v10-configs/garden.yml @@ -0,0 +1,109 @@ +# 0 +kind: Project +name: test-project-v10-config-noop +environments: + - name: local + - name: other +providers: + - name: test-plugin + environments: [local] + - name: test-plugin-b + environments: [other] + +--- + +# 1 +project: + name: test-project-v10-config-nested + environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other + +--- + +# 2 +kind: Project +name: test-project-v10-config-env-defaults +variables: + some: var +environmentDefaults: + varfile: foobar + variables: + foo: bar + providers: + - name: test-plugin-c + context: foo + environments: ["local", "dev"] +environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other + +--- + +# 3 +kind: Project +name: test-project-v10-config-local-openfaas +environments: + - name: local +providers: + - name: local-openfaas + +--- + +# 4 +kind: Project +name: test-project-v10-config-local-openfaas-nested +environments: + - name: local + providers: + - name: local-openfaas + +--- + +# 5 +module: + name: module-nested + type: test + build: + command: [echo, project] + +--- + +# 6 +kind: Module +name: module-local-openfaas +type: local-openfaas +build: + command: [echo, project] + +--- + +# 7 +kind: Project +name: test-project-v10-config-existing-openfaas-nested +environments: + - name: local + providers: + - name: local-openfaas + gatewayUrl: bar + - name: openfaas + gatewayUrl: foo + +--- + +# 8 +kind: Project +name: test-project-v10-config-existing-openfaas +environments: + - name: local +providers: + - name: local-openfaas + gatewayUrl: bar + - name: openfaas + gatewayUrl: foo \ No newline at end of file diff --git a/garden-service/test/data/test-projects/v10-configs/module-a/garden.yml b/garden-service/test/data/test-projects/v10-configs/module-a/garden.yml new file mode 100644 index 0000000000..46f859a168 --- /dev/null +++ b/garden-service/test/data/test-projects/v10-configs/module-a/garden.yml @@ -0,0 +1,5 @@ +kind: Module +name: module-a +type: local-openfaas +build: + command: [echo, project] diff --git a/garden-service/test/data/test-projects/v10-configs/module-b/garden.yml b/garden-service/test/data/test-projects/v10-configs/module-b/garden.yml new file mode 100644 index 0000000000..7622f33a6a --- /dev/null +++ b/garden-service/test/data/test-projects/v10-configs/module-b/garden.yml @@ -0,0 +1,5 @@ +kind: Module +name: module-b +type: local-openfaas +build: + command: [echo, project] diff --git a/garden-service/test/data/test-projects/v10-configs/module-noop/garden.yml b/garden-service/test/data/test-projects/v10-configs/module-noop/garden.yml new file mode 100644 index 0000000000..79630d8866 --- /dev/null +++ b/garden-service/test/data/test-projects/v10-configs/module-noop/garden.yml @@ -0,0 +1,5 @@ +kind: Module +name: module-noop +type: test +build: + command: [echo, project] diff --git a/garden-service/test/data/test-projects/v10-configs/nested/module-c/garden.yml b/garden-service/test/data/test-projects/v10-configs/nested/module-c/garden.yml new file mode 100644 index 0000000000..03cc2a996e --- /dev/null +++ b/garden-service/test/data/test-projects/v10-configs/nested/module-c/garden.yml @@ -0,0 +1,5 @@ +kind: Module +name: module-c +type: local-openfaas +build: + command: [echo, project] diff --git a/garden-service/test/unit/src/commands/migrate.ts b/garden-service/test/unit/src/commands/migrate.ts new file mode 100644 index 0000000000..6ba08dd90e --- /dev/null +++ b/garden-service/test/unit/src/commands/migrate.ts @@ -0,0 +1,446 @@ +import { expect } from "chai" +import { join } from "path" +import tmp from "tmp-promise" +import { dedent } from "../../../../src/util/string" +import cpy = require("cpy") +import { sortBy } from "lodash" +import { expectError, withDefaultGlobalOpts, dataDir, makeTestGardenA } from "../../../helpers" +import { MigrateCommand, MigrateCommandResult, dumpSpec } from "../../../../src/commands/migrate" +import { LogEntry } from "../../../../src/logger/log-entry" +import { Garden } from "../../../../src/garden" +import execa from "execa" + +describe("commands", () => { + describe("migrate", () => { + let tmpDir: tmp.DirectoryResult + const projectPath = join(dataDir, "test-projects", "v10-configs") + const projectPathErrors = join(dataDir, "test-projects", "v10-configs-errors") + const command = new MigrateCommand() + let garden: Garden + let log: LogEntry + + before(async () => { + garden = await makeTestGardenA() + log = garden.log + tmpDir = await tmp.dir({ unsafeCleanup: true }) + }) + + after(async () => { + await tmpDir.cleanup() + }) + + context("convert config", () => { + let result: MigrateCommandResult + + before(async () => { + // The Garden class is not used by the command so we just use any test Garden + + const res = await command.action({ + garden, + log, + headerLog: log, + footerLog: log, + args: { configPaths: [] }, + opts: withDefaultGlobalOpts({ + root: projectPath, + }), + }) + result = res.result! + }) + it("should scan for garden.yml files and convert them to v11 config", () => { + expect(result.updatedConfigs.map((c) => c.path).sort()).to.eql([ + join(projectPath, "garden.yml"), + join(projectPath, "module-a", "garden.yml"), + join(projectPath, "module-b", "garden.yml"), + join(projectPath, "nested", "module-c", "garden.yml"), + ]) + }) + it("should ignore configs that are already valid", () => { + expect(result.updatedConfigs.map((c) => c.path)).to.not.contain([ + join(projectPath, "module-noop", "garden.yml"), + ]) + }) + it("should not modify specs that are already valid", () => { + const noop = result.updatedConfigs[0].specs[0] + expect(noop).to.eql({ + kind: "Project", + name: "test-project-v10-config-noop", + environments: [ + { + name: "local", + }, + { + name: "other", + }, + ], + providers: [ + { + name: "test-plugin", + environments: ["local"], + }, + { + name: "test-plugin-b", + environments: ["other"], + }, + ], + }) + }) + it("should convert nested configs to the flat style", () => { + const nested = result.updatedConfigs[0].specs[1] + expect(nested).to.eql({ + kind: "Project", + name: "test-project-v10-config-nested", + environments: [ + { + name: "local", + providers: [ + { + name: "test-plugin", + }, + { + name: "test-plugin-b", + }, + ], + }, + { + name: "other", + }, + ], + }) + }) + it("should convert nested project configs to the flat style", () => { + const envDefaults = result.updatedConfigs[0].specs[2] + expect(envDefaults).to.eql({ + kind: "Project", + name: "test-project-v10-config-env-defaults", + varfile: "foobar", + variables: { + some: "var", + foo: "bar", + }, + providers: [ + { + name: "test-plugin-c", + context: "foo", + environments: ["local", "dev"], + }, + ], + environments: [ + { + name: "local", + providers: [ + { + name: "test-plugin", + }, + { + name: "test-plugin-b", + }, + ], + }, + { + name: "other", + }, + ], + }) + }) + it("should convert local-openfaas provider to openfaas", () => { + const localOpenfaasProvider = result.updatedConfigs[0].specs[3] + expect(localOpenfaasProvider).to.eql({ + kind: "Project", + name: "test-project-v10-config-local-openfaas", + environments: [ + { + name: "local", + }, + ], + providers: [ + { + name: "openfaas", + }, + ], + }) + }) + it("should convert local-openfaas provider to openfaas for providers nested under the environment field", () => { + const localOpenfaasProviderNested = result.updatedConfigs[0].specs[4] + expect(localOpenfaasProviderNested).to.eql({ + kind: "Project", + name: "test-project-v10-config-local-openfaas-nested", + environments: [ + { + name: "local", + providers: [ + { + name: "openfaas", + }, + ], + }, + ], + }) + }) + it("should convert nested module configs to the flat style", () => { + const moduleNested = result.updatedConfigs[0].specs[5] + expect(moduleNested).to.eql({ + kind: "Module", + name: "module-nested", + type: "test", + build: { + command: ["echo", "project"], + }, + }) + }) + it("should convert local-openfaas module to openfaas", () => { + const moduleOpenfaaas = result.updatedConfigs[0].specs[6] + expect(moduleOpenfaaas).to.eql({ + kind: "Module", + name: "module-local-openfaas", + type: "openfaas", + build: { + command: ["echo", "project"], + }, + }) + }) + it("should remove local-openfaas provider if openfaas already configured", () => { + const openfaasExistingNested = result.updatedConfigs[0].specs[7] + const openfaasExisting = result.updatedConfigs[0].specs[8] + expect(openfaasExistingNested).to.eql({ + kind: "Project", + name: "test-project-v10-config-existing-openfaas-nested", + environments: [ + { + name: "local", + providers: [ + { + name: "openfaas", + gatewayUrl: "foo", + }, + ], + }, + ], + }) + expect(openfaasExisting).to.eql({ + kind: "Project", + name: "test-project-v10-config-existing-openfaas", + environments: [ + { + name: "local", + }, + ], + providers: [ + { + name: "openfaas", + gatewayUrl: "foo", + }, + ], + }) + }) + it("should convert modules in their own config files", () => { + const modules = sortBy(result.updatedConfigs, "path").slice(1) + expect(modules).to.eql([ + { + path: join(projectPath, "module-a", "garden.yml"), + specs: [ + { + kind: "Module", + name: "module-a", + type: "openfaas", + build: { + command: ["echo", "project"], + }, + }, + ], + }, + { + path: join(projectPath, "module-b", "garden.yml"), + specs: [ + { + kind: "Module", + name: "module-b", + type: "openfaas", + build: { + command: ["echo", "project"], + }, + }, + ], + }, + { + path: join(projectPath, "nested", "module-c", "garden.yml"), + specs: [ + { + kind: "Module", + name: "module-c", + type: "openfaas", + build: { + command: ["echo", "project"], + }, + }, + ], + }, + ]) + }) + }) + it("should throw if it can't re-assign the environmentDefaults.varfile field", async () => { + await expectError( + () => + command.action({ + garden, + log, + headerLog: log, + footerLog: log, + args: { configPaths: ["./project-varfile/garden.yml"] }, + opts: withDefaultGlobalOpts({ + root: projectPathErrors, + }), + }), + (err) => { + expect(err.message).to.include("Found a project level `varfile` field") + } + ) + }) + it("should abort write if config file is dirty", async () => { + await execa("git", ["init"], { cwd: tmpDir.path }) + await cpy(join(projectPath, "garden.yml"), tmpDir.path) + + await expectError( + () => + command.action({ + garden, + log, + headerLog: log, + footerLog: log, + args: { configPaths: [] }, + opts: withDefaultGlobalOpts({ + write: true, + root: tmpDir.path, + }), + }), + (err) => { + expect(err.message).to.eql(dedent` + Config files at the following paths are dirty:\n + ${join(tmpDir.path, "garden.yml")} + + Please commit them before applying this command with the --write flag + `) + } + ) + }) + describe("dumpConfig", () => { + it("should return multiple specs as valid YAML", async () => { + const res = await command.action({ + garden, + log, + headerLog: log, + footerLog: log, + args: { configPaths: ["./garden.yml"] }, + opts: withDefaultGlobalOpts({ + root: projectPath, + }), + }) + const specs = res.result!.updatedConfigs[0].specs + expect(dumpSpec(specs)).to.eql(dedent` + kind: Project + name: test-project-v10-config-noop + environments: + - name: local + - name: other + providers: + - name: test-plugin + environments: + - local + - name: test-plugin-b + environments: + - other + + --- + + kind: Project + name: test-project-v10-config-nested + environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other + + --- + + kind: Project + name: test-project-v10-config-env-defaults + variables: + some: var + foo: bar + environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other + varfile: foobar + providers: + - name: test-plugin-c + context: foo + environments: + - local + - dev + + --- + + kind: Project + name: test-project-v10-config-local-openfaas + environments: + - name: local + providers: + - name: openfaas + + --- + + kind: Project + name: test-project-v10-config-local-openfaas-nested + environments: + - name: local + providers: + - name: openfaas + + --- + + kind: Module + name: module-nested + type: test + build: + command: + - echo + - project + + --- + + kind: Module + name: module-local-openfaas + type: openfaas + build: + command: + - echo + - project + + --- + + kind: Project + name: test-project-v10-config-existing-openfaas-nested + environments: + - name: local + providers: + - name: openfaas + gatewayUrl: foo + + --- + + kind: Project + name: test-project-v10-config-existing-openfaas + environments: + - name: local + providers: + - name: openfaas + gatewayUrl: foo\n + `) + }) + }) + }) +})