diff --git a/examples/gatsby-hot-reload/.dockerignore b/examples/gatsby-hot-reload/.dockerignore index 09667e36f8..4b2a80e208 100644 --- a/examples/gatsby-hot-reload/.dockerignore +++ b/examples/gatsby-hot-reload/.dockerignore @@ -1,3 +1,3 @@ node_modules -garden.yaml +garden.yml .git diff --git a/garden-service/src/commands/base.ts b/garden-service/src/commands/base.ts index ae3bed601b..04f632c12b 100644 --- a/garden-service/src/commands/base.ts +++ b/garden-service/src/commands/base.ts @@ -7,7 +7,7 @@ */ import Joi = require("joi") -import { GardenError, RuntimeError, InternalError } from "../exceptions" +import { GardenError, RuntimeError, InternalError, ParameterError } from "../exceptions" import { TaskResults } from "../task-graph" import { LoggerType } from "../logger/logger" import { ProcessResults } from "../process" @@ -16,8 +16,6 @@ import { LogEntry } from "../logger/log-entry" import { printFooter } from "../logger/util" import { GlobalOptions } from "../cli/cli" -export class ValidationError extends Error { } - export interface ParameterConstructor { help: string, required?: boolean, @@ -139,7 +137,10 @@ export class IntegerParameter extends Parameter { try { return parseInt(input, 10) } catch { - throw new ValidationError(`Could not parse "${input}" as integer`) + throw new ParameterError(`Could not parse "${input}" as integer`, { + expectedType: "integer", + input, + }) } } } @@ -164,7 +165,10 @@ export class ChoicesParameter extends Parameter { if (this.choices.includes(input)) { return input } else { - throw new ValidationError(`"${input}" is not a valid argument`) + throw new ParameterError(`"${input}" is not a valid argument`, { + expectedType: `One of: ${this.choices.join(", ")}`, + input, + }) } } diff --git a/garden-service/src/commands/get/get-debug-info.ts b/garden-service/src/commands/get/get-debug-info.ts index dbbe62212e..49902cf010 100644 --- a/garden-service/src/commands/get/get-debug-info.ts +++ b/garden-service/src/commands/get/get-debug-info.ts @@ -16,15 +16,12 @@ import { findProjectConfig } from "../../config/base" import { ensureDir, copy, remove, pathExists, writeFile } from "fs-extra" import { getPackageVersion } from "../../util/util" import { platform, release } from "os" -import { join, relative } from "path" +import { join, relative, basename } from "path" import execa = require("execa") import { LogEntry } from "../../logger/log-entry" import { deline } from "../../util/string" -import { getModulesPathsFromPath } from "../../util/fs" -import { - CONFIG_FILENAME, - ERROR_LOG_FILENAME, -} from "../../constants" +import { getModulesPathsFromPath, getConfigFilePath } from "../../util/fs" +import { ERROR_LOG_FILENAME } from "../../constants" import dedent = require("dedent") import { Garden } from "../../garden" import { zipFolder } from "../../util/archive" @@ -61,7 +58,9 @@ export async function collectBasicDebugInfo(root: string, gardenDirPath: string, await ensureDir(tempPath) // Copy project definition in tmp folder - await copy(join(root, CONFIG_FILENAME), join(tempPath, CONFIG_FILENAME)) + const projectConfigFilePath = await getConfigFilePath(root) + const projectConfigFilename = basename(projectConfigFilePath) + await copy(projectConfigFilePath, join(tempPath, projectConfigFilename)) // Check if error logs exist and copy it over if it does if (await pathExists(join(root, ERROR_LOG_FILENAME))) { await copy(join(root, ERROR_LOG_FILENAME), join(tempPath, ERROR_LOG_FILENAME)) @@ -74,7 +73,9 @@ export async function collectBasicDebugInfo(root: string, gardenDirPath: string, for (const servicePath of paths) { const tempServicePath = join(tempPath, relative(root, servicePath)) await ensureDir(tempServicePath) - await copy(join(servicePath, CONFIG_FILENAME), join(tempServicePath, CONFIG_FILENAME)) + const moduleConfigFilePath = await getConfigFilePath(servicePath) + const moduleConfigFilename = basename(moduleConfigFilePath) + await copy(moduleConfigFilePath, join(tempServicePath, moduleConfigFilename)) // Check if error logs exist and copy them over if they do if (await pathExists(join(servicePath, ERROR_LOG_FILENAME))) { diff --git a/garden-service/src/config/base.ts b/garden-service/src/config/base.ts index 49bf9ac4fd..390e586809 100644 --- a/garden-service/src/config/base.ts +++ b/garden-service/src/config/base.ts @@ -6,14 +6,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { join, sep, resolve, relative } from "path" +import { sep, resolve, relative, basename } from "path" import * as yaml from "js-yaml" import { readFile } from "fs-extra" import { omit, flatten, isPlainObject, find } from "lodash" import { baseModuleSpecSchema, ModuleResource } from "./module" import { ConfigurationError } from "../exceptions" -import { CONFIG_FILENAME, DEFAULT_API_VERSION } from "../constants" +import { DEFAULT_API_VERSION } from "../constants" import { ProjectResource } from "../config/project" +import { getConfigFilePath } from "../util/fs" export interface GardenResource { apiVersion: string @@ -26,7 +27,7 @@ const baseModuleSchemaKeys = Object.keys(baseModuleSpecSchema.describe().childre export async function loadConfig(projectRoot: string, path: string): Promise { // TODO: nicer error messages when load/validation fails - const absPath = join(path, CONFIG_FILENAME) + const absPath = await getConfigFilePath(path) let fileData: Buffer let rawSpecs: any[] @@ -40,7 +41,7 @@ export async function loadConfig(projectRoot: string, path: string): Promise prepareResources(s, path, projectRoot))) diff --git a/garden-service/src/constants.ts b/garden-service/src/constants.ts index d10989d07c..6200c0c14a 100644 --- a/garden-service/src/constants.ts +++ b/garden-service/src/constants.ts @@ -10,7 +10,6 @@ import { resolve, join } from "path" export const isPkg = !!(process).pkg -export const CONFIG_FILENAME = "garden.yml" export const LOCAL_CONFIG_FILENAME = "local-config.yml" export const GARDEN_SERVICE_ROOT = isPkg ? resolve(process.execPath, "..") : resolve(__dirname, "..", "..") export const STATIC_DIR = join(GARDEN_SERVICE_ROOT, "static") diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index bb67e1a904..f28bda3c0e 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -7,7 +7,7 @@ */ import Bluebird = require("bluebird") -import { parse, relative, resolve, sep, join } from "path" +import { parse, relative, resolve, sep } from "path" import { flatten, isString, cloneDeep, sortBy, set, zip } from "lodash" const AsyncLock = require("async-lock") @@ -35,12 +35,12 @@ import { BuildDependencyConfig, ModuleConfig, baseModuleSpecSchema, ModuleResour import { ModuleConfigContext, ContextResolveOpts } from "./config/config-context" import { createPluginContext } from "./plugin-context" import { ModuleAndRuntimeActions, Plugins, RegisterPluginParam } from "./types/plugin/plugin" -import { SUPPORTED_PLATFORMS, SupportedPlatform, CONFIG_FILENAME, DEFAULT_GARDEN_DIR_NAME } from "./constants" +import { SUPPORTED_PLATFORMS, SupportedPlatform, DEFAULT_GARDEN_DIR_NAME } from "./constants" import { platform, arch } from "os" import { LogEntry } from "./logger/log-entry" import { EventBus } from "./events" import { Watcher } from "./watch" -import { getIgnorer, Ignorer, getModulesPathsFromPath } from "./util/fs" +import { getIgnorer, Ignorer, getModulesPathsFromPath, getConfigFilePath } from "./util/fs" import { Provider, ProviderConfig, getProviderDependencies } from "./config/provider" import { ResolveProviderTask } from "./tasks/resolve-provider" import { ActionHelper } from "./actions" @@ -407,13 +407,13 @@ export class Garden { this.resolvedProviders = Object.values(taskResults).map(result => result.output) - for (const provider of this.resolvedProviders) { - for (const moduleConfig of provider.moduleConfigs) { + await Bluebird.map(this.resolvedProviders, async (provider) => + Bluebird.map(provider.moduleConfigs, async (moduleConfig) => { // Make sure module and all nested entities are scoped to the plugin moduleConfig.plugin = provider.name - this.addModule(moduleConfig) - } - } + return this.addModule(moduleConfig) + }), + ) }) return this.resolvedProviders @@ -622,9 +622,7 @@ export class Garden { } }) - for (const config of rawConfigs) { - this.addModule(config) - } + await Bluebird.map(rawConfigs, async (config) => this.addModule(config)) this.modulesScanned = true }) @@ -641,14 +639,14 @@ export class Garden { * Add a module config to the context, after validating and calling the appropriate configure plugin handler. * Template strings should be resolved on the config before calling this. */ - private addModule(config: ModuleConfig) { + private async addModule(config: ModuleConfig) { const key = getModuleKey(config.name, config.plugin) if (this.moduleConfigs[key]) { - const [pathA, pathB] = [ - relative(this.projectRoot, join(this.moduleConfigs[key].path, CONFIG_FILENAME)), - relative(this.projectRoot, join(config.path, CONFIG_FILENAME)), - ].sort() + const paths = [this.moduleConfigs[key].path, config.path] + const [pathA, pathB] = (await Bluebird + .map(paths, async (path) => relative(this.projectRoot, await getConfigFilePath(path)))) + .sort() throw new ConfigurationError( `Module ${key} is declared multiple times (in '${pathA}' and '${pathB}')`, diff --git a/garden-service/src/types/module.ts b/garden-service/src/types/module.ts index 4f0d52b5cb..a3d4d38f73 100644 --- a/garden-service/src/types/module.ts +++ b/garden-service/src/types/module.ts @@ -6,7 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { resolve } from "path" import { flatten, uniq, cloneDeep, keyBy } from "lodash" import { getNames } from "../util/util" import { TestSpec } from "../config/test" @@ -20,7 +19,7 @@ import * as Joi from "joi" import { joiArray, joiIdentifier, joiIdentifierMap } from "../config/common" import { ConfigGraph } from "../config-graph" import * as Bluebird from "bluebird" -import { CONFIG_FILENAME } from "../constants" +import { getConfigFilePath } from "../util/fs" export interface FileCopySpec { source: string @@ -96,7 +95,7 @@ export interface ModuleConfigMap { } export async function moduleFromConfig(garden: Garden, graph: ConfigGraph, config: ModuleConfig): Promise { - const configPath = resolve(config.path, CONFIG_FILENAME) + const configPath = await getConfigFilePath(config.path) const version = await garden.resolveVersion(config.name, config.build.dependencies) // Always include configuration file diff --git a/garden-service/src/util/fs.ts b/garden-service/src/util/fs.ts index 4ce313ae3d..35b2e64ae2 100644 --- a/garden-service/src/util/fs.ts +++ b/garden-service/src/util/fs.ts @@ -9,13 +9,16 @@ import klaw = require("klaw") import * as _spawn from "cross-spawn" import { pathExists, readFile } from "fs-extra" +import * as Bluebird from "bluebird" import minimatch = require("minimatch") import { some } from "lodash" import { join, basename, win32, posix, relative, parse } from "path" -import { CONFIG_FILENAME } from "../constants" +import { ValidationError } from "../exceptions" // NOTE: Importing from ignore/ignore doesn't work on Windows const ignore = require("ignore") +const VALID_CONFIG_FILENAMES = ["garden.yml", "garden.yaml"] + /* Warning: Don't make any async calls in the loop body when using this function, since this may cause funky concurrency behavior. @@ -50,6 +53,40 @@ export async function* scanDirectory(path: string, opts?: klaw.Options): AsyncIt } } +/** + * Returns the expected full path to the Garden config filename. + * + * If a valid config filename is found at the given path, it returns the full path to it. + * If no config file is found, it returns the path joined with the first value from the VALID_CONFIG_FILENAMES list. + * (The check for whether or not a project or a module has a valid config file at all is handled elsewehere.) + * + * Throws an error if there are more than one valid config filenames at the given path. + */ +export async function getConfigFilePath(path: string) { + const configFilePaths = await Bluebird + .map(VALID_CONFIG_FILENAMES, async (filename) => { + const configFilePath = join(path, filename) + return (await pathExists(configFilePath)) ? configFilePath : undefined + }) + .filter(Boolean) + + if (configFilePaths.length > 1) { + throw new ValidationError(`Found more than one Garden config file at ${path}.`, { + path, + configFilenames: configFilePaths.map(filePath => basename(filePath || "")).join(", "), + }) + } + + return configFilePaths[0] || join(path, VALID_CONFIG_FILENAMES[0]) +} + +/** + * Helper function to check whether a given filename is a valid Garden config filename + */ +export function isConfigFilename(filename: string) { + return VALID_CONFIG_FILENAMES.includes(filename) +} + export async function getChildDirNames(parentDir: string): Promise { let dirNames: string[] = [] // Filter on hidden dirs by default. We could make the filter function a param if needed later @@ -125,7 +162,7 @@ export async function getModulesPathsFromPath(dir: string, gardenDirPath: string const parsedPath = parse(item.path) - if (parsedPath.base !== CONFIG_FILENAME) { + if (!isConfigFilename(parsedPath.base)) { continue } diff --git a/garden-service/src/watch.ts b/garden-service/src/watch.ts index 18e0d394b6..22ce066a26 100644 --- a/garden-service/src/watch.ts +++ b/garden-service/src/watch.ts @@ -10,13 +10,13 @@ import { watch, FSWatcher } from "chokidar" import { parse, relative } from "path" import { pathToCacheContext } from "./cache" import { Module } from "./types/module" -import { CONFIG_FILENAME } from "./constants" import { Garden } from "./garden" import { LogEntry } from "./logger/log-entry" import * as klaw from "klaw" import { registerCleanupFunction } from "./util/util" import * as Bluebird from "bluebird" import { some } from "lodash" +import { isConfigFilename } from "./util/fs" export type ChangeHandler = (module: Module | null, configChanged: boolean) => Promise @@ -117,12 +117,12 @@ export class Watcher { const parsed = parse(path) const filename = parsed.base - if (filename === CONFIG_FILENAME || filename === ".gitignore" || filename === ".gardenignore") { + if (isConfigFilename(filename) || filename === ".gitignore" || filename === ".gardenignore") { this.invalidateCached(modules) if (changedModuleNames.length > 0) { this.garden.events.emit("moduleConfigChanged", { names: changedModuleNames, path }) - } else if (filename === CONFIG_FILENAME) { + } else if (isConfigFilename(filename)) { if (parsed.dir === this.garden.projectRoot) { this.garden.events.emit("projectConfigChanged", {}) } else { @@ -160,7 +160,7 @@ export class Watcher { klaw(path, scanOpts) .on("data", (item) => { const parsed = parse(item.path) - if (item.path !== path && parsed.base === CONFIG_FILENAME) { + if (item.path !== path && isConfigFilename(parsed.base)) { configChanged = true this.garden.events.emit("configAdded", { path: item.path }) } diff --git a/garden-service/test/helpers.ts b/garden-service/test/helpers.ts index 8869944bbb..056ed8de84 100644 --- a/garden-service/test/helpers.ts +++ b/garden-service/test/helpers.ts @@ -26,7 +26,7 @@ import { Garden, GardenOpts } from "../src/garden" import { ModuleConfig } from "../src/config/module" import { mapValues, fromPairs } from "lodash" import { ModuleVersion } from "../src/vcs/vcs" -import { CONFIG_FILENAME, GARDEN_SERVICE_ROOT } from "../src/constants" +import { GARDEN_SERVICE_ROOT } from "../src/constants" import { EventBus, Events } from "../src/events" import { ValueOf } from "../src/util/util" import { Ignorer } from "../src/util/fs" @@ -400,7 +400,10 @@ export function stubExtSources(garden: Garden) { } export function getExampleProjects() { - const names = readdirSync(examplesDir).filter(n => existsSync(join(examplesDir, n, CONFIG_FILENAME))) + const names = readdirSync(examplesDir).filter(n => { + const basePath = join(examplesDir, n) + return existsSync(join(basePath, "garden.yml")) || existsSync(join(basePath, "garden.yaml")) + }) return fromPairs(names.map(n => [n, join(examplesDir, n)])) } diff --git a/garden-service/test/unit/data/test-project-duplicate-yaml-file-extensions/garden.yaml b/garden-service/test/unit/data/test-project-duplicate-yaml-file-extensions/garden.yaml new file mode 100644 index 0000000000..bd1f17723f --- /dev/null +++ b/garden-service/test/unit/data/test-project-duplicate-yaml-file-extensions/garden.yaml @@ -0,0 +1,11 @@ +project: + name: test-project-a + environmentDefaults: + variables: + some: variable + environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other diff --git a/garden-service/test/unit/data/test-project-duplicate-yaml-file-extensions/garden.yml b/garden-service/test/unit/data/test-project-duplicate-yaml-file-extensions/garden.yml new file mode 100644 index 0000000000..bd1f17723f --- /dev/null +++ b/garden-service/test/unit/data/test-project-duplicate-yaml-file-extensions/garden.yml @@ -0,0 +1,11 @@ +project: + name: test-project-a + environmentDefaults: + variables: + some: variable + environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other diff --git a/garden-service/test/unit/data/test-project-yaml-file-extensions/garden.yaml b/garden-service/test/unit/data/test-project-yaml-file-extensions/garden.yaml new file mode 100644 index 0000000000..bd1f17723f --- /dev/null +++ b/garden-service/test/unit/data/test-project-yaml-file-extensions/garden.yaml @@ -0,0 +1,11 @@ +project: + name: test-project-a + environmentDefaults: + variables: + some: variable + environments: + - name: local + providers: + - name: test-plugin + - name: test-plugin-b + - name: other diff --git a/garden-service/test/unit/data/test-project-yaml-file-extensions/module-no-config/foo.yml b/garden-service/test/unit/data/test-project-yaml-file-extensions/module-no-config/foo.yml new file mode 100644 index 0000000000..d1ed9c6757 --- /dev/null +++ b/garden-service/test/unit/data/test-project-yaml-file-extensions/module-no-config/foo.yml @@ -0,0 +1,3 @@ +module: + name: module-foo + type: test \ No newline at end of file diff --git a/garden-service/test/unit/data/test-project-yaml-file-extensions/module-yaml/garden.yaml b/garden-service/test/unit/data/test-project-yaml-file-extensions/module-yaml/garden.yaml new file mode 100644 index 0000000000..8168b8bcbd --- /dev/null +++ b/garden-service/test/unit/data/test-project-yaml-file-extensions/module-yaml/garden.yaml @@ -0,0 +1,3 @@ +module: + name: module-yaml + type: test diff --git a/garden-service/test/unit/data/test-project-yaml-file-extensions/module-yml/garden.yml b/garden-service/test/unit/data/test-project-yaml-file-extensions/module-yml/garden.yml new file mode 100644 index 0000000000..a3f8fa94c7 --- /dev/null +++ b/garden-service/test/unit/data/test-project-yaml-file-extensions/module-yml/garden.yml @@ -0,0 +1,3 @@ +module: + name: module-yml + type: test diff --git a/garden-service/test/unit/src/build-dir.ts b/garden-service/test/unit/src/build-dir.ts index cb414b0f2c..4389e56ff3 100644 --- a/garden-service/test/unit/src/build-dir.ts +++ b/garden-service/test/unit/src/build-dir.ts @@ -4,7 +4,7 @@ import { pathExists, readdir } from "fs-extra" import { expect } from "chai" import { BuildTask } from "../../../src/tasks/build" import { makeTestGarden, dataDir } from "../../helpers" -import { CONFIG_FILENAME } from "../../../src/constants" +import { getConfigFilePath } from "../../../src/util/fs" /* Module dependency diagram for test-project-build-products @@ -52,7 +52,7 @@ describe("BuildDir", () => { const buildDirA = await garden.buildDir.buildPath(moduleA.name) const copiedPaths = [ - join(buildDirA, CONFIG_FILENAME), + await getConfigFilePath(buildDirA), join(buildDirA, "some-dir", "some-file"), ] diff --git a/garden-service/test/unit/src/commands/get/get-debug-info.ts b/garden-service/test/unit/src/commands/get/get-debug-info.ts index 4a9d8dbf02..8fc8c199ed 100644 --- a/garden-service/test/unit/src/commands/get/get-debug-info.ts +++ b/garden-service/test/unit/src/commands/get/get-debug-info.ts @@ -19,10 +19,11 @@ import { GetDebugInfoCommand, } from "../../../../../src/commands/get/get-debug-info" import { readdirSync, remove, pathExists, readJSONSync } from "fs-extra" -import { CONFIG_FILENAME, ERROR_LOG_FILENAME } from "../../../../../src/constants" +import { ERROR_LOG_FILENAME } from "../../../../../src/constants" import { join, relative } from "path" import { Garden } from "../../../../../src/garden" import { LogEntry } from "../../../../../src/logger/log-entry" +import { getConfigFilePath } from "../../../../../src/util/fs" const debugZipFileRegex = new RegExp(/debug-info-.*?.zip/) @@ -98,7 +99,7 @@ describe("GetDebugInfoCommand", () => { await collectBasicDebugInfo(garden.projectRoot, garden.gardenDirPath, log) // we first check if the main garden.yml exists - expect(await pathExists(join(gardenDebugTmp, CONFIG_FILENAME))).to.equal(true) + expect(await pathExists(await getConfigFilePath(gardenDebugTmp))).to.equal(true) const graph = await garden.getConfigGraph() // Check that each module config files have been copied over and @@ -110,7 +111,7 @@ describe("GetDebugInfoCommand", () => { expect(await pathExists(join(gardenDebugTmp, moduleRelativePath))).to.equal(true) // Checks config file is copied over - expect(await pathExists(join(gardenDebugTmp, moduleRelativePath, CONFIG_FILENAME))).to.equal(true) + expect(await pathExists(await getConfigFilePath(join(gardenDebugTmp, moduleRelativePath)))).to.equal(true) // Checks error logs are copied over if they exist if (await pathExists(join(module.path, ERROR_LOG_FILENAME))) { diff --git a/garden-service/test/unit/src/garden.ts b/garden-service/test/unit/src/garden.ts index aa9fbb8c79..788ff7faaa 100644 --- a/garden-service/test/unit/src/garden.ts +++ b/garden-service/test/unit/src/garden.ts @@ -51,6 +51,16 @@ describe("Garden", () => { expect(garden).to.be.ok }) + it("should initialize a project with config files with yaml and yml extensions", async () => { + const garden = await makeTestGarden(getDataDir("test-project-yaml-file-extensions")) + expect(garden).to.be.ok + }) + + it("should throw if a project has config files with yaml and yml extensions in the same dir", async () => { + const path = getDataDir("test-project-duplicate-yaml-file-extensions") + await expectError(async () => makeTestGarden(path), "validation") + }) + it("should parse and resolve the config from the project root", async () => { const garden = await makeTestGardenA() const projectRoot = garden.projectRoot @@ -688,6 +698,12 @@ describe("Garden", () => { ), ) }) + + it("should scan and add modules with config files with yaml and yml extensions", async () => { + const garden = await makeTestGarden(getDataDir("test-project-yaml-file-extensions")) + const modules = await garden.resolveModuleConfigs() + expect(getNames(modules).sort()).to.eql(["module-yaml", "module-yml"]) + }) }) describe("loadModuleConfigs", () => { diff --git a/garden-service/test/unit/src/util/fs.ts b/garden-service/test/unit/src/util/fs.ts index 80b41dd78e..9777f715b9 100644 --- a/garden-service/test/unit/src/util/fs.ts +++ b/garden-service/test/unit/src/util/fs.ts @@ -1,7 +1,16 @@ import { expect } from "chai" -import { join } from "path" -import { getDataDir } from "../../../helpers" -import { scanDirectory, toCygwinPath, getChildDirNames } from "../../../../src/util/fs" +import { join, basename } from "path" +import { getDataDir, expectError } from "../../../helpers" +import { + scanDirectory, + toCygwinPath, + getChildDirNames, + isConfigFilename, + getConfigFilePath, +} from "../../../../src/util/fs" + +const projectYamlFileExtensions = getDataDir("test-project-yaml-file-extensions") +const projectDuplicateYamlFileExtensions = getDataDir("test-project-duplicate-yaml-file-extensions") describe("util", () => { describe("scanDirectory", () => { @@ -53,4 +62,42 @@ describe("util", () => { expect(toCygwinPath(path)).to.equal("/cygdrive/c/some/path/") }) }) + + describe("getConfigFilePath", () => { + context("name of the file is garden.yml", () => { + it("should return the full path to the config file", async () => { + const testPath = join(projectYamlFileExtensions, "module-yml") + expect(await getConfigFilePath(testPath)).to.eql(join(testPath, "garden.yml")) + }) + }) + context("name of the file is garden.yaml", () => { + it("should return the full path to the config file", async () => { + const testPath = join(projectYamlFileExtensions, "module-yml") + expect(await getConfigFilePath(testPath)).to.eql(join(testPath, "garden.yml")) + }) + }) + it("should throw if multiple valid config files found at the given path", async () => { + await expectError(() => getConfigFilePath(projectDuplicateYamlFileExtensions), "validation") + }) + it("should return a valid default path if no config file found at the given path", async () => { + const testPath = join(projectYamlFileExtensions, "module-no-config") + const result = await getConfigFilePath(testPath) + expect(isConfigFilename(basename(result))).to.be.true + }) + }) + + describe("isConfigFilename", () => { + it("should return true if the name of the file is garden.yaml", async () => { + expect(await isConfigFilename("garden.yaml")).to.be.true + }) + it("should return true if the name of the file is garden.yml", async () => { + expect(await isConfigFilename("garden.yml")).to.be.true + }) + it("should return false otherwise", async () => { + const badNames = ["agarden.yml", "garden.ymla", "garden.yaaml", "garden.ml"] + for (const name of badNames) { + expect(isConfigFilename(name)).to.be.false + } + }) + }) }) diff --git a/garden-service/test/unit/src/watch.ts b/garden-service/test/unit/src/watch.ts index 60097e4b4b..dc4406273e 100644 --- a/garden-service/test/unit/src/watch.ts +++ b/garden-service/test/unit/src/watch.ts @@ -1,10 +1,10 @@ -import { resolve } from "path" +import { resolve, join } from "path" import { TestGarden, dataDir, makeTestGarden } from "../../helpers" import { expect } from "chai" import { CacheContext, pathToCacheContext } from "../../../src/cache" -import { CONFIG_FILENAME } from "../../../src/constants" import pEvent = require("p-event") import { createFile, remove, pathExists } from "fs-extra" +import { getConfigFilePath } from "../../../src/util/fs" describe("Watcher", () => { let garden: TestGarden @@ -39,7 +39,7 @@ describe("Watcher", () => { } it("should emit a moduleConfigChanged changed event when module config is changed", async () => { - const path = resolve(modulePath, CONFIG_FILENAME) + const path = await getConfigFilePath(modulePath) emitEvent("change", path) expect(garden.events.eventLog).to.eql([ { name: "moduleConfigChanged", payload: { names: ["module-a"], path } }, @@ -47,7 +47,7 @@ describe("Watcher", () => { }) it("should emit a moduleConfigChanged event when module config is changed and include field is set", async () => { - const path = resolve(includeModulePath, CONFIG_FILENAME) + const path = await getConfigFilePath(includeModulePath) emitEvent("change", path) expect(garden.events.eventLog).to.eql([ { name: "moduleConfigChanged", payload: { names: ["with-include"], path } }, @@ -55,37 +55,41 @@ describe("Watcher", () => { }) it("should clear all module caches when a module config is changed", async () => { - emitEvent("change", resolve(modulePath, CONFIG_FILENAME)) + const path = await getConfigFilePath(modulePath) + emitEvent("change", path) expect(garden.cache.getByContext(moduleContext)).to.eql(new Map()) }) it("should emit a projectConfigChanged changed event when project config is changed", async () => { - emitEvent("change", resolve(garden.projectRoot, CONFIG_FILENAME)) + const path = await getConfigFilePath(garden.projectRoot) + emitEvent("change", path) expect(garden.events.eventLog).to.eql([ { name: "projectConfigChanged", payload: {} }, ]) }) it("should emit a projectConfigChanged changed event when project config is removed", async () => { - emitEvent("unlink", resolve(garden.projectRoot, CONFIG_FILENAME)) + const path = await getConfigFilePath(garden.projectRoot) + emitEvent("unlink", path) expect(garden.events.eventLog).to.eql([ { name: "projectConfigChanged", payload: {} }, ]) }) it("should clear all module caches when project config is changed", async () => { - emitEvent("change", resolve(garden.projectRoot, CONFIG_FILENAME)) + const path = await getConfigFilePath(garden.projectRoot) + emitEvent("change", path) expect(garden.cache.getByContext(moduleContext)).to.eql(new Map()) }) it("should emit a configAdded event when adding a garden.yml file", async () => { - const path = resolve(garden.projectRoot, "module-b", CONFIG_FILENAME) + const path = await getConfigFilePath(join(garden.projectRoot, "module-b")) emitEvent("add", path) expect(await waitForEvent("configAdded")).to.eql({ path }) }) it("should emit a configRemoved event when removing a garden.yml file", async () => { - const path = resolve(garden.projectRoot, "module-b", CONFIG_FILENAME) + const path = await getConfigFilePath(join(garden.projectRoot, "module-b")) emitEvent("unlink", path) expect(garden.events.eventLog).to.eql([ { name: "configRemoved", payload: { path } }, @@ -155,7 +159,7 @@ describe("Watcher", () => { it("should emit a configAdded event when a directory is added that contains a garden.yml file", async () => { emitEvent("addDir", modulePath) expect(await waitForEvent("configAdded")).to.eql({ - path: resolve(modulePath, CONFIG_FILENAME), + path: await getConfigFilePath(modulePath), }) })