diff --git a/cli/src/add-version-files.ts b/cli/src/add-version-files.ts index 4de7cb94ee..13bb349814 100644 --- a/cli/src/add-version-files.ts +++ b/cli/src/add-version-files.ts @@ -26,7 +26,7 @@ try { * Write .garden-version files for modules in garden-system/static. */ async function addVersionFiles() { - const garden = await Garden.factory(STATIC_DIR) + const garden = await Garden.factory(STATIC_DIR, { commandInfo: { name: "add-version-files", args: {}, opts: {} } }) const moduleConfigs = await garden.getRawModuleConfigs() diff --git a/core/src/cli/cli.ts b/core/src/cli/cli.ts index bb18f1e7cd..89b682ee32 100644 --- a/core/src/cli/cli.ts +++ b/core/src/cli/cli.ts @@ -41,7 +41,7 @@ import { GardenPluginCallback } from "../types/plugin/plugin" import { renderError } from "../logger/renderers" import { EnterpriseApi } from "../enterprise/api" -export async function makeDummyGarden(root: string, gardenOpts: GardenOpts = {}) { +export async function makeDummyGarden(root: string, gardenOpts: GardenOpts) { const environments = gardenOpts.environmentName ? [{ name: parseEnvironment(gardenOpts.environmentName).environment, defaultNamespace, variables: {} }] : defaultEnvironments diff --git a/core/src/config/project.ts b/core/src/config/project.ts index 124caaefb7..39b785480f 100644 --- a/core/src/config/project.ts +++ b/core/src/config/project.ts @@ -37,6 +37,7 @@ import { pathExists, readFile } from "fs-extra" import { resolve, basename, relative } from "path" import chalk = require("chalk") import { safeLoad } from "js-yaml" +import { CommandInfo } from "../plugin-context" export const defaultVarfilePath = "garden.env" export const defaultEnvVarfilePath = (environmentName: string) => `garden.${environmentName}.env` @@ -402,6 +403,7 @@ export function resolveProjectConfig({ branch, username, secrets, + commandInfo, }: { defaultEnvironment: string config: ProjectConfig @@ -409,6 +411,7 @@ export function resolveProjectConfig({ branch: string username: string secrets: PrimitiveMap + commandInfo: CommandInfo }): ProjectConfig { // Resolve template strings for non-environment-specific fields const { environments = [], name } = config @@ -421,7 +424,15 @@ export function resolveProjectConfig({ variables: config.variables, environments: [], }, - new ProjectConfigContext({ projectName: name, projectRoot: config.path, artifactsPath, branch, username, secrets }) + new ProjectConfigContext({ + projectName: name, + projectRoot: config.path, + artifactsPath, + branch, + username, + secrets, + commandInfo, + }) ) // Validate after resolving global fields @@ -500,6 +511,7 @@ export async function pickEnvironment({ branch, username, secrets, + commandInfo, }: { projectConfig: ProjectConfig envString: string @@ -507,6 +519,7 @@ export async function pickEnvironment({ branch: string username: string secrets: PrimitiveMap + commandInfo: CommandInfo }) { const { environments, name: projectName, path: projectRoot } = projectConfig @@ -539,6 +552,7 @@ export async function pickEnvironment({ username, variables: projectVariables, secrets, + commandInfo, }) ) diff --git a/core/src/config/template-contexts/project.ts b/core/src/config/template-contexts/project.ts index 1fe7f823d8..a91d373ed8 100644 --- a/core/src/config/template-contexts/project.ts +++ b/core/src/config/template-contexts/project.ts @@ -10,6 +10,7 @@ import { PrimitiveMap, joiIdentifierMap, joiStringMap, joiPrimitive, DeepPrimiti import { joi } from "../common" import { deline, dedent } from "../../util/string" import { schema, ConfigContext } from "./base" +import { CommandInfo } from "../../plugin-context" class LocalContext extends ConfigContext { @schema( @@ -112,6 +113,52 @@ class GitContext extends ConfigContext { } } +const commandHotExample = "${command.params contains 'hot-reload' && command.params.hot-reload contains 'my-service'}" + +class CommandContext extends ConfigContext { + @schema( + joi + .string() + .description( + dedent` + The currently running Garden CLI command, without positional arguments or option flags. This can be handy to e.g. change some variables based on whether you're running \`garden test\` or some other specific command. + + Note that this will currently always resolve to \`"run workflow"\` when running Workflows, as opposed to individual workflow step commands. This may be revisited at a later time, but currently all configuration is resolved once for all workflow steps. + ` + ) + .example("deploy") + ) + public name: string + + @schema( + joiStringMap(joi.any()) + .description( + dedent` + A map of all parameters set when calling the current command. This includes both positional arguments and option flags, and includes any default values set by the framework or specific command. This can be powerful if used right, but do take care since different parameters are only available in certain commands, some have array values etc. + + For example, to see if a service is in hot-reload mode, you might do something like \`${commandHotExample}\`. Notice that you currently need to check both for the existence of the parameter, and also to correctly handle the array value. + ` + ) + .example({ force: true, hot: ["my-service"] }) + ) + public params: DeepPrimitiveMap + + constructor(root: ConfigContext, commandInfo: CommandInfo) { + super(root) + this.name = commandInfo.name + this.params = { ...commandInfo.args, ...commandInfo.opts } + } +} + +interface DefaultEnvironmentContextParams { + projectName: string + projectRoot: string + artifactsPath: string + branch: string + username?: string + commandInfo: CommandInfo +} + /** * This context is available for template strings in the `defaultEnvironment` field in project configs. */ @@ -123,6 +170,9 @@ export class DefaultEnvironmentContext extends ConfigContext { ) public local: LocalContext + @schema(CommandContext.getSchema().description("Information about the currently running command and its arguments.")) + public command: CommandContext + @schema(ProjectContext.getSchema().description("Information about the Garden project.")) public project: ProjectContext @@ -137,26 +187,17 @@ export class DefaultEnvironmentContext extends ConfigContext { artifactsPath, branch, username, - }: { - projectName: string - projectRoot: string - artifactsPath: string - branch: string - username?: string - }) { + commandInfo, + }: DefaultEnvironmentContextParams) { super() this.local = new LocalContext(this, artifactsPath, projectRoot, username) this.git = new GitContext(this, branch) this.project = new ProjectContext(this, projectName) + this.command = new CommandContext(this, commandInfo) } } -export interface ProjectConfigContextParams { - projectName: string - projectRoot: string - artifactsPath: string - branch: string - username?: string +export interface ProjectConfigContextParams extends DefaultEnvironmentContextParams { secrets: PrimitiveMap } @@ -178,9 +219,9 @@ export class ProjectConfigContext extends DefaultEnvironmentContext { ) public secrets: PrimitiveMap - constructor({ projectName, projectRoot, artifactsPath, branch, username, secrets }: ProjectConfigContextParams) { - super({ projectName, projectRoot, artifactsPath, branch, username }) - this.secrets = secrets + constructor(params: ProjectConfigContextParams) { + super(params) + this.secrets = params.secrets } } @@ -212,16 +253,8 @@ export class EnvironmentConfigContext extends ProjectConfigContext { ) public secrets: PrimitiveMap - constructor({ - projectName, - projectRoot, - artifactsPath, - branch, - username, - variables, - secrets, - }: EnvironmentConfigContextParams) { - super({ projectName, projectRoot, artifactsPath, branch, username, secrets }) - this.variables = this.var = variables + constructor(params: EnvironmentConfigContextParams) { + super(params) + this.variables = this.var = params.variables } } diff --git a/core/src/config/template-contexts/workflow.ts b/core/src/config/template-contexts/workflow.ts index 8e81620209..ebc7b873f9 100644 --- a/core/src/config/template-contexts/workflow.ts +++ b/core/src/config/template-contexts/workflow.ts @@ -41,6 +41,7 @@ export class WorkflowConfigContext extends EnvironmentConfigContext { username: garden.username, variables: garden.variables, secrets: garden.secrets, + commandInfo: garden.commandInfo, }) const fullEnvName = garden.namespace ? `${garden.namespace}.${garden.environmentName}` : garden.environmentName diff --git a/core/src/docs/generate.ts b/core/src/docs/generate.ts index 745d55c8d4..db66c30a97 100644 --- a/core/src/docs/generate.ts +++ b/core/src/docs/generate.ts @@ -59,6 +59,7 @@ export async function writeConfigReferenceDocs(docsRoot: string, plugins: Garden { name: "terraform" }, ] const garden = await Garden.factory(__dirname, { + commandInfo: { name: "generate-docs", args: {}, opts: {} }, config: { path: __dirname, apiVersion: "garden.io/v0", diff --git a/core/src/garden.ts b/core/src/garden.ts index d2e336db6e..15980b3ada 100644 --- a/core/src/garden.ts +++ b/core/src/garden.ts @@ -116,7 +116,7 @@ export type ModuleActionMap = { } export interface GardenOpts { - commandInfo?: CommandInfo + commandInfo: CommandInfo config?: ProjectConfig disablePortForwards?: boolean environmentName?: string @@ -215,6 +215,7 @@ export class Garden { private readonly forceRefresh: boolean public readonly enterpriseApi: EnterpriseApi | null public readonly disablePortForwards: boolean + public readonly commandInfo: CommandInfo constructor(params: GardenParams) { this.buildStaging = params.buildStaging @@ -246,6 +247,7 @@ export class Garden { this.vcs = params.vcs this.forceRefresh = !!params.forceRefresh this.enterpriseApi = params.enterpriseApi || null + this.commandInfo = params.opts.commandInfo // make sure we're on a supported platform const currentPlatform = platform() @@ -287,129 +289,9 @@ export class Garden { static async factory( this: T, currentDirectory: string, - opts: GardenOpts = {} + opts: GardenOpts ): Promise> { - let { environmentName: environmentStr, config, gardenDirPath, plugins = [], disablePortForwards } = opts - - if (!config) { - config = await findProjectConfig(currentDirectory) - - if (!config) { - throw new ConfigurationError( - `Not a project directory (or any of the parent directories): ${currentDirectory}`, - { currentDirectory } - ) - } - } - - gardenDirPath = resolve(config.path, gardenDirPath || DEFAULT_GARDEN_DIR_NAME) - await ensureDir(gardenDirPath) - - const artifactsPath = resolve(gardenDirPath, "artifacts") - await ensureDir(artifactsPath) - - const _username = (await username()) || "" - const projectName = config.name - const log = opts.log || getLogger().placeholder() - - // Connect to the state storage - await ensureConnected() - - const { sources: projectSources, path: projectRoot } = config - - const vcsBranch = (await new GitHandler(projectRoot, gardenDirPath, []).getBranchName(log, projectRoot)) || "" - - const defaultEnvironmentName = resolveTemplateString( - config.defaultEnvironment, - new DefaultEnvironmentContext({ projectName, projectRoot, artifactsPath, branch: vcsBranch, username: _username }) - ) as string - - const defaultEnvironment = getDefaultEnvironmentName(defaultEnvironmentName, config) - - if (!environmentStr) { - environmentStr = defaultEnvironment - } - - const { environment: environmentName } = parseEnvironment(environmentStr) - - const sessionId = opts.sessionId || null - const projectId = config.id || null - let secrets: StringMap = {} - const enterpriseApi = opts.enterpriseApi || null - if (!opts.noEnterprise && enterpriseApi?.isUserLoggedIn) { - const enterpriseInitResult = await enterpriseInit({ log, projectId, enterpriseApi, environmentName }) - secrets = enterpriseInitResult.secrets - } - - config = resolveProjectConfig({ - defaultEnvironment: defaultEnvironmentName, - config, - artifactsPath, - branch: vcsBranch, - username: _username, - secrets, - }) - - const vcs = new GitHandler(projectRoot, gardenDirPath, config.dotIgnoreFiles) - - let { namespace, providers, variables, production } = await pickEnvironment({ - projectConfig: config, - envString: environmentStr, - artifactsPath, - branch: vcsBranch, - username: _username, - secrets, - }) - - // Allow overriding variables - variables = { ...variables, ...(opts.variables || {}) } - - const legacyBuildSync = - opts.legacyBuildSync === undefined ? gardenEnv.GARDEN_LEGACY_BUILD_STAGE : opts.legacyBuildSync - const buildDirCls = legacyBuildSync ? BuildDirRsync : BuildStaging - const buildDir = await buildDirCls.factory(projectRoot, gardenDirPath) - const workingCopyId = await getWorkingCopyId(gardenDirPath) - - // We always exclude the garden dir - const gardenDirExcludePattern = `${relative(projectRoot, gardenDirPath)}/**/*` - const moduleExcludePatterns = [ - ...((config.modules || {}).exclude || []), - gardenDirExcludePattern, - ...fixedProjectExcludes, - ] - - const garden = new this({ - artifactsPath, - vcsBranch, - sessionId, - disablePortForwards, - projectId, - projectRoot, - projectName, - environmentName, - environmentConfigs: config.environments, - namespace, - variables, - secrets, - projectSources, - buildStaging: buildDir, - production, - gardenDirPath, - opts, - outputs: config.outputs || [], - plugins, - providerConfigs: providers, - moduleExcludePatterns, - workingCopyId, - dotIgnoreFiles: config.dotIgnoreFiles, - moduleIncludePatterns: (config.modules || {}).include, - log, - username: _username, - vcs, - forceRefresh: opts.forceRefresh, - enterpriseApi, - }) as InstanceType - + const garden = new this(await resolveGardenParams(currentDirectory, opts)) as InstanceType return garden } @@ -1223,6 +1105,138 @@ export class Garden { //endregion } +export async function resolveGardenParams(currentDirectory: string, opts: GardenOpts): Promise { + let { environmentName: environmentStr, config, gardenDirPath, plugins = [], disablePortForwards } = opts + + if (!config) { + config = await findProjectConfig(currentDirectory) + + if (!config) { + throw new ConfigurationError(`Not a project directory (or any of the parent directories): ${currentDirectory}`, { + currentDirectory, + }) + } + } + + gardenDirPath = resolve(config.path, gardenDirPath || DEFAULT_GARDEN_DIR_NAME) + await ensureDir(gardenDirPath) + + const artifactsPath = resolve(gardenDirPath, "artifacts") + await ensureDir(artifactsPath) + + const _username = (await username()) || "" + const projectName = config.name + const log = opts.log || getLogger().placeholder() + + // Connect to the state storage + await ensureConnected() + + const { sources: projectSources, path: projectRoot } = config + const commandInfo = opts.commandInfo + + const vcsBranch = (await new GitHandler(projectRoot, gardenDirPath, []).getBranchName(log, projectRoot)) || "" + + const defaultEnvironmentName = resolveTemplateString( + config.defaultEnvironment, + new DefaultEnvironmentContext({ + projectName, + projectRoot, + artifactsPath, + branch: vcsBranch, + username: _username, + commandInfo, + }) + ) as string + + const defaultEnvironment = getDefaultEnvironmentName(defaultEnvironmentName, config) + + if (!environmentStr) { + environmentStr = defaultEnvironment + } + + const { environment: environmentName } = parseEnvironment(environmentStr) + + const sessionId = opts.sessionId || null + const projectId = config.id || null + let secrets: StringMap = {} + const enterpriseApi = opts.enterpriseApi || null + if (!opts.noEnterprise && enterpriseApi?.isUserLoggedIn) { + const enterpriseInitResult = await enterpriseInit({ log, projectId, enterpriseApi, environmentName }) + secrets = enterpriseInitResult.secrets + } + + config = resolveProjectConfig({ + defaultEnvironment: defaultEnvironmentName, + config, + artifactsPath, + branch: vcsBranch, + username: _username, + secrets, + commandInfo, + }) + + const vcs = new GitHandler(projectRoot, gardenDirPath, config.dotIgnoreFiles) + + let { namespace, providers, variables, production } = await pickEnvironment({ + projectConfig: config, + envString: environmentStr, + artifactsPath, + branch: vcsBranch, + username: _username, + secrets, + commandInfo, + }) + + // Allow overriding variables + variables = { ...variables, ...(opts.variables || {}) } + + const legacyBuildSync = + opts.legacyBuildSync === undefined ? gardenEnv.GARDEN_LEGACY_BUILD_STAGE : opts.legacyBuildSync + const buildDirCls = legacyBuildSync ? BuildDirRsync : BuildStaging + const buildDir = await buildDirCls.factory(projectRoot, gardenDirPath) + const workingCopyId = await getWorkingCopyId(gardenDirPath) + + // We always exclude the garden dir + const gardenDirExcludePattern = `${relative(projectRoot, gardenDirPath)}/**/*` + const moduleExcludePatterns = [ + ...((config.modules || {}).exclude || []), + gardenDirExcludePattern, + ...fixedProjectExcludes, + ] + + return { + artifactsPath, + vcsBranch, + sessionId, + disablePortForwards, + projectId, + projectRoot, + projectName, + environmentName, + environmentConfigs: config.environments, + namespace, + variables, + secrets, + projectSources, + buildStaging: buildDir, + production, + gardenDirPath, + opts, + outputs: config.outputs || [], + plugins, + providerConfigs: providers, + moduleExcludePatterns, + workingCopyId, + dotIgnoreFiles: config.dotIgnoreFiles, + moduleIncludePatterns: (config.modules || {}).include, + log, + username: _username, + vcs, + forceRefresh: opts.forceRefresh, + enterpriseApi, + } +} + /** * Dummy Garden class that doesn't scan for modules nor resolves providers. * Used by commands that have noProject=true. That is, commands that need diff --git a/core/src/plugin-context.ts b/core/src/plugin-context.ts index 55fc8342f5..2418ba118c 100644 --- a/core/src/plugin-context.ts +++ b/core/src/plugin-context.ts @@ -10,7 +10,7 @@ import { Garden } from "./garden" import { projectNameSchema, projectSourcesSchema, environmentNameSchema, SourceConfig } from "./config/project" import { Provider, providerSchema, GenericProviderConfig } from "./config/provider" import { deline } from "./util/string" -import { joi, joiVariables, PrimitiveMap, joiStringMap } from "./config/common" +import { joi, joiVariables, joiStringMap, DeepPrimitiveMap } from "./config/common" import { PluginTool } from "./util/ext-tools" type WrappedFromGarden = Pick< @@ -26,12 +26,12 @@ type WrappedFromGarden = Pick< export interface CommandInfo { name: string - args: PrimitiveMap - opts: PrimitiveMap + args: DeepPrimitiveMap + opts: DeepPrimitiveMap } export interface PluginContext extends WrappedFromGarden { - command?: CommandInfo + command: CommandInfo projectSources: SourceConfig[] provider: Provider tools: { [key: string]: PluginTool } @@ -74,7 +74,7 @@ export const pluginContextSchema = () => export async function createPluginContext( garden: Garden, provider: Provider, - command?: CommandInfo + command: CommandInfo ): Promise { return { command, diff --git a/core/src/tasks/publish.ts b/core/src/tasks/publish.ts index e8e8b1195d..2372c87211 100644 --- a/core/src/tasks/publish.ts +++ b/core/src/tasks/publish.ts @@ -14,11 +14,12 @@ import { BaseTask, TaskType } from "../tasks/base" import { Garden } from "../garden" import { LogEntry } from "../logger/log-entry" import { ConfigGraph } from "../config-graph" -import { ModuleConfigContext, schema, ConfigContext, ModuleConfigContextParams } from "../config/config-context" import { emptyRuntimeContext } from "../runtime-context" import { resolveTemplateString } from "../template-string" import { joi } from "../config/common" import { versionStringPrefix } from "../vcs/vcs" +import { ConfigContext, schema } from "../config/template-contexts/base" +import { ModuleConfigContext, ModuleConfigContextParams } from "../config/template-contexts/module" export interface PublishTaskParams { garden: Garden diff --git a/core/src/template-string.ts b/core/src/template-string.ts index e2d6161546..25914ac1ad 100644 --- a/core/src/template-string.ts +++ b/core/src/template-string.ts @@ -434,9 +434,6 @@ function buildLogicalExpression(head: any, tail: any) { if (leftRes && leftRes._error) { return leftRes } - if (rightRes && rightRes._error) { - return rightRes - } const left = getValue(leftRes) diff --git a/core/src/util/testing.ts b/core/src/util/testing.ts index b4540c06f8..1c8d7d1afa 100644 --- a/core/src/util/testing.ts +++ b/core/src/util/testing.ts @@ -7,7 +7,7 @@ */ import { keyBy, isEqual } from "lodash" -import { Garden } from "../garden" +import { Garden, GardenOpts, resolveGardenParams } from "../garden" import { StringMap, DeepPrimitiveMap } from "../config/common" import { GardenParams } from "../garden" import { ModuleConfig } from "../config/module" @@ -67,6 +67,10 @@ export class TestEventBus extends EventBus { } } +const defaultCommandinfo = { name: "test", args: {}, opts: {} } + +export type TestGardenOpts = Partial + export class TestGarden extends Garden { events: TestEventBus public secrets: StringMap // Not readonly, to allow setting secrets in tests @@ -77,6 +81,17 @@ export class TestGarden extends Garden { this.events = new TestEventBus() } + static async factory( + this: T, + currentDirectory: string, + opts?: TestGardenOpts + ): Promise> { + const garden = new this( + await resolveGardenParams(currentDirectory, { commandInfo: defaultCommandinfo, ...opts }) + ) as InstanceType + return garden + } + setModuleConfigs(moduleConfigs: ModuleConfig[]) { this.configsScanned = true this.moduleConfigs = keyBy(moduleConfigs, "name") diff --git a/core/test/helpers.ts b/core/test/helpers.ts index ce18b538d4..d6204f639c 100644 --- a/core/test/helpers.ts +++ b/core/test/helpers.ts @@ -22,7 +22,7 @@ import { RegisterPluginParam, ModuleAndRuntimeActionHandlers, } from "../src/types/plugin/plugin" -import { Garden, GardenOpts } from "../src/garden" +import { Garden } from "../src/garden" import { ModuleConfig } from "../src/config/module" import { mapValues, fromPairs } from "lodash" import { ModuleVersion } from "../src/vcs/vcs" @@ -43,7 +43,7 @@ import { RunTaskParams, RunTaskResult } from "../src/types/plugin/task/runTask" import { SuiteFunction, TestFunction } from "mocha" import { GardenError } from "../src/exceptions" import { AnalyticsGlobalConfig } from "../src/config-store" -import { TestGarden, EventLogEntry } from "../src/util/testing" +import { TestGarden, EventLogEntry, TestGardenOpts } from "../src/util/testing" import { Logger } from "../src/logger/logger" import { LogLevel } from "../src/logger/log-node" import { ExecInServiceParams, ExecInServiceResult } from "../src/types/plugin/service/execInService" @@ -333,7 +333,7 @@ export const makeTestModule = (params: Partial = {}) => { export const testPlugins = () => [testPlugin(), testPluginB(), testPluginC()] -export const makeTestGarden = async (projectRoot: string, opts: GardenOpts = {}) => { +export const makeTestGarden = async (projectRoot: string, opts: TestGardenOpts = {}) => { opts = { sessionId: uuidv4(), ...opts } const plugins = [...testPlugins(), ...(opts.plugins || [])] return TestGarden.factory(projectRoot, { ...opts, plugins }) diff --git a/core/test/integ/src/plugins/hadolint/hadolint.ts b/core/test/integ/src/plugins/hadolint/hadolint.ts index 1ad1c25c38..e56909129c 100644 --- a/core/test/integ/src/plugins/hadolint/hadolint.ts +++ b/core/test/integ/src/plugins/hadolint/hadolint.ts @@ -10,8 +10,7 @@ import tmp from "tmp-promise" import { ProjectConfig, defaultNamespace } from "../../../../../src/config/project" import execa = require("execa") import { DEFAULT_API_VERSION } from "../../../../../src/constants" -import { Garden } from "../../../../../src/garden" -import { getDataDir } from "../../../../helpers" +import { getDataDir, TestGarden } from "../../../../helpers" import { expect } from "chai" import stripAnsi from "strip-ansi" import { dedent } from "../../../../../src/util/string" @@ -58,7 +57,7 @@ describe("hadolint provider", () => { }) it("should add a hadolint module for each container module with a Dockerfile", async () => { - const garden = await Garden.factory(tmpPath, { + const garden = await TestGarden.factory(tmpPath, { plugins: [], config: projectConfigFoo, }) @@ -120,7 +119,7 @@ describe("hadolint provider", () => { ], }) - const garden = await Garden.factory(tmpPath, { + const garden = await TestGarden.factory(tmpPath, { plugins: [foo], config: { ...projectConfigFoo, @@ -158,7 +157,7 @@ describe("hadolint provider", () => { const path = getDataDir("hadolint") it("should format warnings and errors nicely", async () => { - const garden = await Garden.factory(tmpPath, { + const garden = await TestGarden.factory(tmpPath, { plugins: [], config: projectConfigFoo, }) @@ -212,7 +211,7 @@ describe("hadolint provider", () => { }) it("should prefer a .hadolint.yaml in the module root if it's available", async () => { - const garden = await Garden.factory(tmpPath, { + const garden = await TestGarden.factory(tmpPath, { plugins: [], config: projectConfigFoo, }) @@ -274,7 +273,7 @@ describe("hadolint provider", () => { }) it("should use a .hadolint.yaml in the project root if there's none in the module root", async () => { - const garden = await Garden.factory(tmpPath, { + const garden = await TestGarden.factory(tmpPath, { plugins: [], config: projectConfigFoo, }) @@ -334,7 +333,7 @@ describe("hadolint provider", () => { }) it("should set success=false with a linting warning if testFailureThreshold=warning", async () => { - const garden = await Garden.factory(tmpPath, { + const garden = await TestGarden.factory(tmpPath, { plugins: [], config: { ...projectConfigFoo, @@ -381,7 +380,7 @@ describe("hadolint provider", () => { }) it("should set success=true with a linting warning if testFailureThreshold=error", async () => { - const garden = await Garden.factory(tmpPath, { + const garden = await TestGarden.factory(tmpPath, { plugins: [], config: projectConfigFoo, }) @@ -425,7 +424,7 @@ describe("hadolint provider", () => { }) it("should set success=true with warnings and errors if testFailureThreshold=none", async () => { - const garden = await Garden.factory(tmpPath, { + const garden = await TestGarden.factory(tmpPath, { plugins: [], config: { ...projectConfigFoo, diff --git a/core/test/integ/src/plugins/octant/octant.ts b/core/test/integ/src/plugins/octant/octant.ts index c81a452333..4e2bc3c646 100644 --- a/core/test/integ/src/plugins/octant/octant.ts +++ b/core/test/integ/src/plugins/octant/octant.ts @@ -8,15 +8,14 @@ import { defaultNamespace } from "../../../../../src/config/project" import { DEFAULT_API_VERSION } from "../../../../../src/constants" -import { projectRootA } from "../../../../helpers" +import { projectRootA, TestGarden } from "../../../../helpers" import { expect } from "chai" import { got } from "../../../../../src/util/http" -import { Garden } from "../../../../../src/garden" describe("octant provider", () => { describe("getDashboardPage", () => { it("should start an octant process and return a URL to it", async () => { - const garden = await Garden.factory(projectRootA, { + const garden = await TestGarden.factory(projectRootA, { config: { apiVersion: DEFAULT_API_VERSION, kind: "Project", diff --git a/core/test/unit/src/actions.ts b/core/test/unit/src/actions.ts index 0c84446d52..fb9dfa7176 100644 --- a/core/test/unit/src/actions.ts +++ b/core/test/unit/src/actions.ts @@ -19,7 +19,6 @@ import { Service, ServiceState } from "../../../src/types/service" import { RuntimeContext, prepareRuntimeContext } from "../../../src/runtime-context" import { expectError, makeTestGardenA, stubModuleAction, projectRootA, TestGarden } from "../../helpers" import { ActionRouter } from "../../../src/actions" -import { Garden } from "../../../src/garden" import { LogEntry } from "../../../src/logger/log-entry" import { GardenModule } from "../../../src/types/module" import { ServiceLogEntry } from "../../../src/types/plugin/service/getServiceLogs" @@ -901,7 +900,7 @@ describe("ActionRouter", () => { ], }) - const _garden = await Garden.factory(path, { + const _garden = await TestGarden.factory(path, { plugins: [foo], config: { apiVersion: DEFAULT_API_VERSION, @@ -954,7 +953,7 @@ describe("ActionRouter", () => { ], }) - const _garden = await Garden.factory(path, { + const _garden = await TestGarden.factory(path, { plugins: [base, foo], config: { apiVersion: DEFAULT_API_VERSION, @@ -1019,7 +1018,7 @@ describe("ActionRouter", () => { ], }) - const _garden = await Garden.factory(path, { + const _garden = await TestGarden.factory(path, { plugins: [base, too, foo], config: { apiVersion: DEFAULT_API_VERSION, @@ -1088,7 +1087,7 @@ describe("ActionRouter", () => { ], }) - const _garden = await Garden.factory(path, { + const _garden = await TestGarden.factory(path, { plugins: [base, too, foo], config: { apiVersion: DEFAULT_API_VERSION, @@ -1145,7 +1144,7 @@ describe("ActionRouter", () => { ], }) - const _garden = await Garden.factory(path, { + const _garden = await TestGarden.factory(path, { plugins: [base, foo], config: { apiVersion: DEFAULT_API_VERSION, @@ -1213,7 +1212,7 @@ describe("ActionRouter", () => { ], }) - const _garden = await Garden.factory(path, { + const _garden = await TestGarden.factory(path, { plugins: [base, foo], config: projectConfigWithBase, }) @@ -1255,7 +1254,7 @@ describe("ActionRouter", () => { ], }) - const _garden = await Garden.factory(path, { + const _garden = await TestGarden.factory(path, { plugins: [base, foo], config: projectConfigWithBase, }) @@ -1311,7 +1310,7 @@ describe("ActionRouter", () => { ], }) - const _garden = await Garden.factory(path, { + const _garden = await TestGarden.factory(path, { plugins: [baseA, baseB, foo], config: { apiVersion: DEFAULT_API_VERSION, @@ -1403,7 +1402,7 @@ describe("ActionRouter", () => { const path = process.cwd() - const _garden = await Garden.factory(path, { + const _garden = await TestGarden.factory(path, { plugins: [baseA, baseB, foo], config: { apiVersion: DEFAULT_API_VERSION, diff --git a/core/test/unit/src/build-staging/build-staging.ts b/core/test/unit/src/build-staging/build-staging.ts index 8e78c55c8a..f1f15f03b7 100644 --- a/core/test/unit/src/build-staging/build-staging.ts +++ b/core/test/unit/src/build-staging/build-staging.ts @@ -16,8 +16,8 @@ import { makeTestGarden, dataDir, TestGarden, expectError } from "../../../helpe import { defaultConfigFilename, TempDirectory, makeTempDir, joinWithPosix } from "../../../../src/util/fs" import { BuildStaging, SyncParams } from "../../../../src/build-staging/build-staging" import { LogEntry } from "../../../../src/logger/log-entry" -import { GardenOpts } from "../../../../src/garden" import Bluebird from "bluebird" +import { TestGardenOpts } from "../../../../src/util/testing" /* Module dependency diagram for build-dir test project @@ -31,7 +31,7 @@ import Bluebird from "bluebird" const projectRoot = join(dataDir, "test-projects", "build-dir") -const makeGarden = async (opts: GardenOpts = {}) => { +const makeGarden = async (opts: TestGardenOpts = {}) => { return await makeTestGarden(projectRoot, opts) } diff --git a/core/test/unit/src/cli/cli.ts b/core/test/unit/src/cli/cli.ts index fa77a2bbf3..d91c4b161d 100644 --- a/core/test/unit/src/cli/cli.ts +++ b/core/test/unit/src/cli/cli.ts @@ -964,21 +964,26 @@ describe("cli", () => { describe("makeDummyGarden", () => { it("should initialise and resolve config graph in a directory with no project", async () => { - const garden = await makeDummyGarden(join(GARDEN_CORE_ROOT, "tmp", "foobarbas"), {}) + const garden = await makeDummyGarden(join(GARDEN_CORE_ROOT, "tmp", "foobarbas"), { + commandInfo: { name: "foo", args: {}, opts: {} }, + }) const dg = await garden.getConfigGraph(garden.log) expect(garden).to.be.ok expect(dg.getModules()).to.not.throw }) it("should correctly configure a dummy environment when a namespace is set", async () => { - const garden = await makeDummyGarden(join(GARDEN_CORE_ROOT, "tmp", "foobarbas"), { environmentName: "test.foo" }) + const garden = await makeDummyGarden(join(GARDEN_CORE_ROOT, "tmp", "foobarbas"), { + environmentName: "test.foo", + commandInfo: { name: "foo", args: {}, opts: {} }, + }) expect(garden).to.be.ok expect(garden.environmentName).to.equal("foo") }) it("should initialise and resolve config graph in a project with invalid config", async () => { const root = getDataDir("test-project-invalid-config") - const garden = await makeDummyGarden(root, {}) + const garden = await makeDummyGarden(root, { commandInfo: { name: "foo", args: {}, opts: {} } }) const dg = await garden.getConfigGraph(garden.log) expect(garden).to.be.ok expect(dg.getModules()).to.not.throw @@ -986,7 +991,7 @@ describe("cli", () => { it("should initialise and resolve config graph in a project with template strings", async () => { const root = getDataDir("test-project-templated") - const garden = await makeDummyGarden(root, {}) + const garden = await makeDummyGarden(root, { commandInfo: { name: "foo", args: {}, opts: {} } }) const dg = await garden.getConfigGraph(garden.log) expect(garden).to.be.ok expect(dg.getModules()).to.not.throw diff --git a/core/test/unit/src/commands/call.ts b/core/test/unit/src/commands/call.ts index 75582c4c87..cc9623babb 100644 --- a/core/test/unit/src/commands/call.ts +++ b/core/test/unit/src/commands/call.ts @@ -7,14 +7,13 @@ */ import { join } from "path" -import { Garden } from "../../../../src/garden" import { CallCommand } from "../../../../src/commands/call" import { expect } from "chai" import { GardenPlugin, createGardenPlugin } from "../../../../src/types/plugin/plugin" import { GetServiceStatusParams } from "../../../../src/types/plugin/service/getServiceStatus" import { ServiceStatus } from "../../../../src/types/service" import nock = require("nock") -import { configureTestModule, withDefaultGlobalOpts, dataDir, testModuleSpecSchema } from "../../../helpers" +import { configureTestModule, withDefaultGlobalOpts, dataDir, testModuleSpecSchema, TestGarden } from "../../../helpers" const testStatusesA: { [key: string]: ServiceStatus } = { "service-a": { @@ -112,7 +111,7 @@ describe("commands.call", () => { }) it("should find the ingress for a service and call it with the specified path", async () => { - const garden = await Garden.factory(projectRootB, { plugins: pluginsA }) + const garden = await TestGarden.factory(projectRootB, { plugins: pluginsA }) const log = garden.log const command = new CallCommand() @@ -135,7 +134,7 @@ describe("commands.call", () => { }) it("should default to the path '/' if that is exposed if no path is requested", async () => { - const garden = await Garden.factory(projectRootB, { plugins: pluginsA }) + const garden = await TestGarden.factory(projectRootB, { plugins: pluginsA }) const log = garden.log const command = new CallCommand() @@ -158,7 +157,7 @@ describe("commands.call", () => { }) it("should otherwise use the first defined ingress if no path is requested", async () => { - const garden = await Garden.factory(projectRootB, { plugins: pluginsA }) + const garden = await TestGarden.factory(projectRootB, { plugins: pluginsA }) const log = garden.log const command = new CallCommand() @@ -181,7 +180,7 @@ describe("commands.call", () => { }) it("should use the linkUrl if provided", async () => { - const garden = await Garden.factory(projectRootB, { plugins: pluginsB }) + const garden = await TestGarden.factory(projectRootB, { plugins: pluginsB }) const log = garden.log const command = new CallCommand() @@ -204,7 +203,7 @@ describe("commands.call", () => { }) it("should return the path for linkUrl", async () => { - const garden = await Garden.factory(projectRootB, { plugins: pluginsB }) + const garden = await TestGarden.factory(projectRootB, { plugins: pluginsB }) const log = garden.log const command = new CallCommand() @@ -227,7 +226,7 @@ describe("commands.call", () => { }) it("should error if service isn't running", async () => { - const garden = await Garden.factory(projectRootB, { plugins: pluginsA }) + const garden = await TestGarden.factory(projectRootB, { plugins: pluginsA }) const log = garden.log const command = new CallCommand() @@ -249,7 +248,7 @@ describe("commands.call", () => { }) it("should error if service has no ingresses", async () => { - const garden = await Garden.factory(projectRootB, { plugins: pluginsA }) + const garden = await TestGarden.factory(projectRootB, { plugins: pluginsA }) const log = garden.log const command = new CallCommand() @@ -271,7 +270,7 @@ describe("commands.call", () => { }) it("should error if service has no matching ingresses", async () => { - const garden = await Garden.factory(projectRootB, { plugins: pluginsA }) + const garden = await TestGarden.factory(projectRootB, { plugins: pluginsA }) const log = garden.log const command = new CallCommand() diff --git a/core/test/unit/src/commands/create/create-module.ts b/core/test/unit/src/commands/create/create-module.ts index 7742196976..644c092bf3 100644 --- a/core/test/unit/src/commands/create/create-module.ts +++ b/core/test/unit/src/commands/create/create-module.ts @@ -29,7 +29,7 @@ describe("CreateModuleCommand", () => { beforeEach(async () => { tmp = await makeTempDir() await exec("git", ["init"], { cwd: tmp.path }) - garden = await makeDummyGarden(tmp.path) + garden = await makeDummyGarden(tmp.path, { commandInfo: { name: "create module", args: {}, opts: {} } }) }) afterEach(async () => { diff --git a/core/test/unit/src/commands/create/create-project.ts b/core/test/unit/src/commands/create/create-project.ts index b7d517ed31..517dd2e647 100644 --- a/core/test/unit/src/commands/create/create-project.ts +++ b/core/test/unit/src/commands/create/create-project.ts @@ -24,7 +24,7 @@ describe("CreateProjectCommand", () => { beforeEach(async () => { tmp = await makeTempDir() await exec("git", ["init"], { cwd: tmp.path }) - garden = await makeDummyGarden(tmp.path) + garden = await makeDummyGarden(tmp.path, { commandInfo: { name: "create project", args: {}, opts: {} } }) }) afterEach(async () => { diff --git a/core/test/unit/src/commands/delete.ts b/core/test/unit/src/commands/delete.ts index 741f52e9ce..dc6438ffc6 100644 --- a/core/test/unit/src/commands/delete.ts +++ b/core/test/unit/src/commands/delete.ts @@ -7,8 +7,14 @@ */ import { DeleteSecretCommand, DeleteEnvironmentCommand, DeleteServiceCommand } from "../../../../src/commands/delete" -import { Garden } from "../../../../src/garden" -import { expectError, makeTestGardenA, getDataDir, configureTestModule, withDefaultGlobalOpts } from "../../../helpers" +import { + expectError, + makeTestGardenA, + getDataDir, + configureTestModule, + withDefaultGlobalOpts, + TestGarden, +} from "../../../helpers" import { expect } from "chai" import { ServiceStatus } from "../../../../src/types/service" import { EnvironmentStatus } from "../../../../src/types/plugin/provider/getEnvironmentStatus" @@ -115,7 +121,7 @@ describe("DeleteEnvironmentCommand", () => { const plugins = [testProvider] it("should delete environment with services", async () => { - const garden = await Garden.factory(projectRootB, { plugins }) + const garden = await TestGarden.factory(projectRootB, { plugins }) const log = garden.log const { result } = await command.action({ @@ -182,7 +188,7 @@ describe("DeleteServiceCommand", () => { const projectRootB = getDataDir("test-project-b") it("should return the status of the deleted service", async () => { - const garden = await Garden.factory(projectRootB, { plugins }) + const garden = await TestGarden.factory(projectRootB, { plugins }) const log = garden.log const { result } = await command.action({ @@ -202,7 +208,7 @@ describe("DeleteServiceCommand", () => { }) it("should return the status of the deleted services", async () => { - const garden = await Garden.factory(projectRootB, { plugins }) + const garden = await TestGarden.factory(projectRootB, { plugins }) const log = garden.log const { result } = await command.action({ diff --git a/core/test/unit/src/commands/get/get-task-result.ts b/core/test/unit/src/commands/get/get-task-result.ts index 1120b6af02..8800d8e43e 100644 --- a/core/test/unit/src/commands/get/get-task-result.ts +++ b/core/test/unit/src/commands/get/get-task-result.ts @@ -14,6 +14,7 @@ import { configureTestModule, testModuleSpecSchema, cleanProject, + TestGarden, } from "../../../../helpers" import { GetTaskResultCommand } from "../../../../../src/commands/get/get-task-result" import { expect } from "chai" @@ -65,7 +66,7 @@ describe("GetTaskResultCommand", () => { const command = new GetTaskResultCommand() beforeEach(async () => { - garden = await Garden.factory(projectRootB, { plugins: [testPlugin] }) + garden = await TestGarden.factory(projectRootB, { plugins: [testPlugin] }) log = garden.log }) diff --git a/core/test/unit/src/commands/login.ts b/core/test/unit/src/commands/login.ts index 16ce9f5d83..1cc94f1ae0 100644 --- a/core/test/unit/src/commands/login.ts +++ b/core/test/unit/src/commands/login.ts @@ -42,6 +42,7 @@ describe("LoginCommand", () => { const garden = await makeDummyGarden(getDataDir("test-projects", "login", "has-domain-and-id"), { noEnterprise: false, enterpriseApi, + commandInfo: { name: "foo", args: {}, opts: {} }, }) await command.action(makeCommandParams(garden)) @@ -55,6 +56,7 @@ describe("LoginCommand", () => { const garden = await makeDummyGarden(getDataDir("test-projects", "login", "has-domain"), { noEnterprise: false, enterpriseApi, + commandInfo: { name: "foo", args: {}, opts: {} }, }) await command.action(makeCommandParams(garden)) @@ -62,7 +64,9 @@ describe("LoginCommand", () => { }) it("should throw if the project doesn't have a domain", async () => { - const garden = await makeDummyGarden(getDataDir("test-projects", "login", "missing-domain")) + const garden = await makeDummyGarden(getDataDir("test-projects", "login", "missing-domain"), { + commandInfo: { name: "foo", args: {}, opts: {} }, + }) const command = new LoginCommand() await expectError( () => command.action(makeCommandParams(garden)), @@ -77,6 +81,7 @@ describe("LoginCommand", () => { const garden = await makeDummyGarden(getDataDir("test-projects", "login", "secret-in-project-variables"), { noEnterprise: false, enterpriseApi, + commandInfo: { name: "foo", args: {}, opts: {} }, }) await command.action(makeCommandParams(garden)) diff --git a/core/test/unit/src/commands/publish.ts b/core/test/unit/src/commands/publish.ts index cdaa1f4e23..7ed3aff646 100644 --- a/core/test/unit/src/commands/publish.ts +++ b/core/test/unit/src/commands/publish.ts @@ -10,7 +10,6 @@ import chalk from "chalk" import { it } from "mocha" import { join } from "path" import { expect } from "chai" -import { Garden } from "../../../../src/garden" import { PublishCommand } from "../../../../src/commands/publish" import { makeTestGardenA, @@ -18,6 +17,7 @@ import { withDefaultGlobalOpts, dataDir, testModuleSpecSchema, + TestGarden, } from "../../../helpers" import { taskResultOutputs } from "../../../helpers" import { createGardenPlugin } from "../../../../src/types/plugin/plugin" @@ -56,7 +56,7 @@ const testProvider = createGardenPlugin({ }) async function getTestGarden() { - const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) + const garden = await TestGarden.factory(projectRootB, { plugins: [testProvider] }) await garden.clearBuilds() return garden } diff --git a/core/test/unit/src/commands/tools.ts b/core/test/unit/src/commands/tools.ts index e545437489..4a38f9bb6f 100644 --- a/core/test/unit/src/commands/tools.ts +++ b/core/test/unit/src/commands/tools.ts @@ -234,7 +234,10 @@ describe("ToolsCommand", () => { }) it("should run a tool by name when run outside of a project", async () => { - const _garden: any = await makeDummyGarden(tmpDir.path, { noEnterprise: true }) + const _garden: any = await makeDummyGarden(tmpDir.path, { + noEnterprise: true, + commandInfo: { name: "foo", args: {}, opts: {} }, + }) _garden.registeredPlugins = [pluginA, pluginB] const { result } = await command.action({ diff --git a/core/test/unit/src/commands/util/hide-warning.ts b/core/test/unit/src/commands/util/hide-warning.ts index afa23a2f14..7d86d3a1b3 100644 --- a/core/test/unit/src/commands/util/hide-warning.ts +++ b/core/test/unit/src/commands/util/hide-warning.ts @@ -6,17 +6,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { withDefaultGlobalOpts, getLogMessages, projectRootA } from "../../../../helpers" +import { withDefaultGlobalOpts, getLogMessages, projectRootA, makeTestGarden } from "../../../../helpers" import { expect } from "chai" import { Warning } from "../../../../../src/db/entities/warning" import { getConnection } from "../../../../../src/db/connection" import { HideWarningCommand } from "../../../../../src/commands/util/hide-warning" -import { makeDummyGarden } from "../../../../../src/cli/cli" import { randomString } from "../../../../../src/util/string" describe("HideWarningCommand", () => { it("should hide a warning message", async () => { - const garden = await makeDummyGarden(projectRootA) + const garden = await makeTestGarden(projectRootA) const log = garden.log.placeholder() const cmd = new HideWarningCommand() const key = randomString(10) diff --git a/core/test/unit/src/commands/validate.ts b/core/test/unit/src/commands/validate.ts index 91a5503370..4f9481c186 100644 --- a/core/test/unit/src/commands/validate.ts +++ b/core/test/unit/src/commands/validate.ts @@ -7,9 +7,8 @@ */ import { join } from "path" -import { Garden } from "../../../../src/garden" import { ValidateCommand } from "../../../../src/commands/validate" -import { expectError, withDefaultGlobalOpts, dataDir, makeTestGardenA } from "../../../helpers" +import { expectError, withDefaultGlobalOpts, dataDir, makeTestGardenA, TestGarden } from "../../../helpers" describe("commands.validate", () => { it(`should successfully validate a test project`, async () => { @@ -30,12 +29,12 @@ describe("commands.validate", () => { it("should fail validating the bad-project project", async () => { const root = join(dataDir, "validate", "bad-project") - await expectError(async () => await Garden.factory(root), "configuration") + await expectError(async () => await TestGarden.factory(root), "configuration") }) it("should fail validating the bad-module project", async () => { const root = join(dataDir, "validate", "bad-module") - const garden = await Garden.factory(root) + const garden = await TestGarden.factory(root) const log = garden.log const command = new ValidateCommand() diff --git a/core/test/unit/src/config/project.ts b/core/test/unit/src/config/project.ts index 4341986334..447651f907 100644 --- a/core/test/unit/src/config/project.ts +++ b/core/test/unit/src/config/project.ts @@ -29,6 +29,8 @@ import { resolve, join } from "path" import stripAnsi from "strip-ansi" import { keyBy } from "lodash" +const commandInfo = { name: "test", args: {}, opts: {} } + describe("resolveProjectConfig", () => { it("should pass through a canonical project config", async () => { const defaultEnvironment = "default" @@ -53,6 +55,7 @@ describe("resolveProjectConfig", () => { branch: "main", username: "some-user", secrets: {}, + commandInfo, }) ).to.eql({ ...config, @@ -91,6 +94,7 @@ describe("resolveProjectConfig", () => { branch: "main", username: "some-user", secrets: {}, + commandInfo, }) ).to.eql({ ...config, @@ -145,6 +149,7 @@ describe("resolveProjectConfig", () => { branch: "main", username: "some-user", secrets: { foo: "banana" }, + commandInfo, }) ).to.eql({ ...config, @@ -219,6 +224,7 @@ describe("resolveProjectConfig", () => { branch: "main", username: "some-user", secrets: {}, + commandInfo, }) ).to.eql({ ...config, @@ -282,6 +288,7 @@ describe("resolveProjectConfig", () => { branch: "main", username: "some-user", secrets: {}, + commandInfo, }) expect(result.environments[0].variables).to.eql(config.environments[0].variables) @@ -310,6 +317,7 @@ describe("resolveProjectConfig", () => { branch: "main", username: "some-user", secrets: {}, + commandInfo, }) ).to.eql({ ...config, @@ -343,6 +351,7 @@ describe("resolveProjectConfig", () => { branch: "main", username: "some-user", secrets: {}, + commandInfo, }) ).to.eql({ ...config, @@ -395,6 +404,7 @@ describe("resolveProjectConfig", () => { branch: "main", username: "some-user", secrets: {}, + commandInfo, }) ).to.eql({ ...config, @@ -470,6 +480,7 @@ describe("resolveProjectConfig", () => { branch: "main", username: "some-user", secrets: {}, + commandInfo, }) ).to.eql({ ...config, @@ -540,6 +551,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }), "parameter" ) @@ -566,6 +578,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) ).to.eql({ environmentName: "default", @@ -609,6 +622,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) ).to.eql({ environmentName: "default", @@ -651,6 +665,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) ).to.eql({ environmentName: "default", @@ -708,6 +723,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) expect(result.variables).to.eql({ @@ -761,6 +777,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) expect(result.variables).to.eql({ @@ -808,6 +825,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) expect(result.variables).to.eql({ @@ -856,6 +874,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) expect(result.variables).to.eql({ @@ -904,6 +923,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) expect(result.variables).to.eql({ @@ -963,6 +983,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) expect(result.variables).to.eql({ @@ -1023,6 +1044,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) expect(result.variables).to.eql({ @@ -1055,6 +1077,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: { foo: "banana" }, + commandInfo, }) expect(result.variables).to.eql({ @@ -1086,6 +1109,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) }) @@ -1116,6 +1140,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) expect(keyBy(result.providers, "name")["my-provider"].a).to.equal("${var.missing}") @@ -1141,6 +1166,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) expect(result.variables).to.eql({ @@ -1200,6 +1226,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) expect(result.variables).to.eql({ @@ -1241,6 +1268,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }), (err) => expect(stripAnsi(err.message)).to.equal( @@ -1278,6 +1306,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }), (err) => expect(err.message).to.equal("Could not find varfile at path 'foo.env'") ) @@ -1312,6 +1341,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }), (err) => expect(err.message).to.equal("Could not find varfile at path 'foo.env'") ) @@ -1338,6 +1368,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) ).to.eql({ environmentName: "default", @@ -1369,6 +1400,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }) ).to.eql({ environmentName: "default", @@ -1400,6 +1432,7 @@ describe("pickEnvironment", () => { username, branch: "main", secrets: {}, + commandInfo, }) ).to.eql({ environmentName: "default", @@ -1432,6 +1465,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }), (err) => expect(err.message).to.equal( @@ -1462,6 +1496,7 @@ describe("pickEnvironment", () => { branch: "main", username, secrets: {}, + commandInfo, }), (err) => expect(stripAnsi(err.message)).to.equal( diff --git a/core/test/unit/src/config/template-contexts/module.ts b/core/test/unit/src/config/template-contexts/module.ts index 762f2be403..b8241e78df 100644 --- a/core/test/unit/src/config/template-contexts/module.ts +++ b/core/test/unit/src/config/template-contexts/module.ts @@ -35,6 +35,7 @@ describe("ModuleConfigContext", () => { garden["secrets"] = { someSecret: "someSecretValue" } const graph = await garden.getConfigGraph(garden.log) const modules = graph.getModules() + currentBranch = garden.vcsBranch c = new ModuleConfigContext({ garden, diff --git a/core/test/unit/src/config/template-contexts/project.ts b/core/test/unit/src/config/template-contexts/project.ts index 232837ba48..66c708d3c2 100644 --- a/core/test/unit/src/config/template-contexts/project.ts +++ b/core/test/unit/src/config/template-contexts/project.ts @@ -10,6 +10,7 @@ import { expect } from "chai" import stripAnsi = require("strip-ansi") import { ConfigContext } from "../../../../../src/config/template-contexts/base" import { ProjectConfigContext } from "../../../../../src/config/template-contexts/project" +import { resolveTemplateString } from "../../../../../src/template-string" type TestValue = string | ConfigContext | TestValues | TestValueFunction type TestValueFunction = () => TestValue | Promise @@ -116,4 +117,55 @@ describe("ProjectConfigContext", () => { resolved: "someuser", }) }) + + it("should resolve the command name", () => { + const c = new ProjectConfigContext({ + projectName: "some-project", + projectRoot: "/tmp", + artifactsPath: "/tmp", + branch: "main", + username: "SomeUser", + secrets: {}, + commandInfo: { name: "test", args: {}, opts: {} }, + }) + expect(c.resolve({ key: ["command", "name"], nodePath: [], opts: {} })).to.eql({ + resolved: "test", + }) + }) + + it("should resolve command params (positive)", () => { + const c = new ProjectConfigContext({ + projectName: "some-project", + projectRoot: "/tmp", + artifactsPath: "/tmp", + branch: "main", + username: "SomeUser", + secrets: {}, + commandInfo: { name: "deploy", args: {}, opts: { "hot-reload": ["my-service"] } }, + }) + + let result = resolveTemplateString( + "${command.name == 'deploy' && (command.params.hot-reload contains 'my-service')}", + c + ) + expect(result).to.be.true + }) + + it("should resolve command params (negative)", () => { + const c = new ProjectConfigContext({ + projectName: "some-project", + projectRoot: "/tmp", + artifactsPath: "/tmp", + branch: "main", + username: "SomeUser", + secrets: {}, + commandInfo: { name: "test", args: {}, opts: {} }, + }) + + let result = resolveTemplateString( + "${command.params contains 'hot-reload' && command.params.hot-reload contains 'my-service'}", + c + ) + expect(result).to.be.false + }) }) diff --git a/core/test/unit/src/config/template-contexts/workflow.ts b/core/test/unit/src/config/template-contexts/workflow.ts index ecb30e2d34..117f319e83 100644 --- a/core/test/unit/src/config/template-contexts/workflow.ts +++ b/core/test/unit/src/config/template-contexts/workflow.ts @@ -27,6 +27,7 @@ describe("WorkflowConfigContext", () => { before(async () => { garden = await makeTestGardenA() garden["secrets"] = { someSecret: "someSecretValue" } + currentBranch = garden.vcsBranch c = new WorkflowConfigContext(garden) }) diff --git a/core/test/unit/src/garden.ts b/core/test/unit/src/garden.ts index c53950c3d3..096619278a 100644 --- a/core/test/unit/src/garden.ts +++ b/core/test/unit/src/garden.ts @@ -1529,7 +1529,7 @@ describe("Garden", () => { (err) => { expect(err.message).to.equal("Failed resolving one or more providers:\n" + "- test") expect(stripAnsi(err.detail.messages[0])).to.equal( - "- test: Invalid template string (${bla.ble}): Could not find key bla. Available keys: environment, git, local, project, providers, secrets, var and variables." + "- test: Invalid template string (${bla.ble}): Could not find key bla. Available keys: command, environment, git, local, project, providers, secrets, var and variables." ) } ) diff --git a/core/test/unit/src/plugins/exec.ts b/core/test/unit/src/plugins/exec.ts index b19476b1f0..ef4a3e1d10 100644 --- a/core/test/unit/src/plugins/exec.ts +++ b/core/test/unit/src/plugins/exec.ts @@ -13,7 +13,7 @@ import { gardenPlugin, configureExecModule } from "../../../../src/plugins/exec" import { GARDEN_BUILD_VERSION_FILENAME, DEFAULT_API_VERSION } from "../../../../src/constants" import { LogEntry } from "../../../../src/logger/log-entry" import { keyBy } from "lodash" -import { getDataDir, makeTestModule, expectError } from "../../../helpers" +import { getDataDir, makeTestModule, expectError, TestGarden } from "../../../helpers" import { TaskTask } from "../../../../src/tasks/task" import { readModuleVersionFile } from "../../../../src/vcs/vcs" import { dataDir, makeTestGarden } from "../../../helpers" @@ -41,7 +41,7 @@ describe("exec plugin", () => { }) it("should run a script on init in the project root, if configured", async () => { - const _garden = await Garden.factory(garden.projectRoot, { + const _garden = await TestGarden.factory(garden.projectRoot, { plugins: [], config: { apiVersion: DEFAULT_API_VERSION, @@ -64,7 +64,7 @@ describe("exec plugin", () => { }) it("should throw if a script configured and exits with a non-zero code", async () => { - const _garden = await Garden.factory(garden.projectRoot, { + const _garden = await TestGarden.factory(garden.projectRoot, { plugins: [], config: { apiVersion: DEFAULT_API_VERSION, diff --git a/core/test/unit/src/plugins/kubernetes/kubernetes.ts b/core/test/unit/src/plugins/kubernetes/kubernetes.ts index ae42493f7f..987569f612 100644 --- a/core/test/unit/src/plugins/kubernetes/kubernetes.ts +++ b/core/test/unit/src/plugins/kubernetes/kubernetes.ts @@ -9,11 +9,11 @@ import { configureProvider, gardenPlugin } from "../../../../../src/plugins/kubernetes/kubernetes" import { KubernetesConfig, defaultResources, defaultStorage } from "../../../../../src/plugins/kubernetes/config" import { defaultSystemNamespace } from "../../../../../src/plugins/kubernetes/system" -import { makeDummyGarden } from "../../../../../src/cli/cli" import { expect } from "chai" import { TempDirectory, makeTempDir } from "../../../../helpers" import { providerFromConfig } from "../../../../../src/config/provider" import { Garden } from "../../../../../src/garden" +import { makeDummyGarden } from "../../../../../src/cli/cli" describe("kubernetes configureProvider", () => { const basicConfig: KubernetesConfig = { @@ -40,7 +40,7 @@ describe("kubernetes configureProvider", () => { beforeEach(async () => { tmpDir = await makeTempDir({ git: true }) - garden = await makeDummyGarden(tmpDir.path) + garden = await makeDummyGarden(tmpDir.path, { commandInfo: { name: "test", args: {}, opts: {} } }) }) afterEach(async () => { diff --git a/core/test/unit/src/tasks/deploy.ts b/core/test/unit/src/tasks/deploy.ts index 5490c1e79b..5b754d1230 100644 --- a/core/test/unit/src/tasks/deploy.ts +++ b/core/test/unit/src/tasks/deploy.ts @@ -20,6 +20,7 @@ import { DeployTask } from "../../../../src/tasks/deploy" import { DeployServiceParams } from "../../../../src/types/plugin/service/deployService" import { RunTaskParams } from "../../../../src/types/plugin/task/runTask" import { expect } from "chai" +import { TestGarden } from "../../../helpers" describe("DeployTask", () => { let tmpDir: tmp.DirectoryResult @@ -88,7 +89,7 @@ describe("DeployTask", () => { ], }) - garden = await Garden.factory(tmpDir.path, { config, plugins: [testPlugin] }) + garden = await TestGarden.factory(tmpDir.path, { config, plugins: [testPlugin] }) garden["moduleConfigs"] = { test: { diff --git a/core/test/unit/src/template-string.ts b/core/test/unit/src/template-string.ts index b433252cb0..d047fcb789 100644 --- a/core/test/unit/src/template-string.ts +++ b/core/test/unit/src/template-string.ts @@ -345,6 +345,12 @@ describe("resolveTemplateString", async () => { expect(res).to.equal(true) }) + it("should handle a logical AND where the first part is false but the second part is not resolvable", async () => { + // i.e. the 2nd clause should not need to be evaluated + const res = resolveTemplateString("${false && a}", new TestContext({})) + expect(res).to.equal(false) + }) + it("should handle a logical AND with an empty string as the first clause", async () => { const res = resolveTemplateString("${'' && true}", new TestContext({})) expect(res).to.equal("") diff --git a/docs/reference/template-strings.md b/docs/reference/template-strings.md index 69273eb19f..fe1b0692f1 100644 --- a/docs/reference/template-strings.md +++ b/docs/reference/template-strings.md @@ -108,6 +108,46 @@ Example: my-variable: ${local.usernameLowerCase} ``` +### `${command.*}` + +Information about the currently running command and its arguments. + +| Type | +| -------- | +| `object` | + +### `${command.name}` + +The currently running Garden CLI command, without positional arguments or option flags. This can be handy to e.g. change some variables based on whether you're running `garden test` or some other specific command. + +Note that this will currently always resolve to `"run workflow"` when running Workflows, as opposed to individual workflow step commands. This may be revisited at a later time, but currently all configuration is resolved once for all workflow steps. + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${command.name} +``` + +### `${command.params.*}` + +A map of all parameters set when calling the current command. This includes both positional arguments and option flags, and includes any default values set by the framework or specific command. This can be powerful if used right, but do take care since different parameters are only available in certain commands, some have array values etc. + +For example, to see if a service is in hot-reload mode, you might do something like `${command.params contains 'hot-reload' && command.params.hot-reload contains 'my-service'}`. Notice that you currently need to check both for the existence of the parameter, and also to correctly handle the array value. + +| Type | +| -------- | +| `object` | + +### `${command.params.}` + +| Type | +| ----- | +| `any` | + ### `${project.*}` Information about the Garden project. @@ -268,6 +308,46 @@ Example: my-variable: ${local.usernameLowerCase} ``` +### `${command.*}` + +Information about the currently running command and its arguments. + +| Type | +| -------- | +| `object` | + +### `${command.name}` + +The currently running Garden CLI command, without positional arguments or option flags. This can be handy to e.g. change some variables based on whether you're running `garden test` or some other specific command. + +Note that this will currently always resolve to `"run workflow"` when running Workflows, as opposed to individual workflow step commands. This may be revisited at a later time, but currently all configuration is resolved once for all workflow steps. + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${command.name} +``` + +### `${command.params.*}` + +A map of all parameters set when calling the current command. This includes both positional arguments and option flags, and includes any default values set by the framework or specific command. This can be powerful if used right, but do take care since different parameters are only available in certain commands, some have array values etc. + +For example, to see if a service is in hot-reload mode, you might do something like `${command.params contains 'hot-reload' && command.params.hot-reload contains 'my-service'}`. Notice that you currently need to check both for the existence of the parameter, and also to correctly handle the array value. + +| Type | +| -------- | +| `object` | + +### `${command.params.}` + +| Type | +| ----- | +| `any` | + ### `${project.*}` Information about the Garden project. @@ -460,6 +540,46 @@ Example: my-variable: ${local.usernameLowerCase} ``` +### `${command.*}` + +Information about the currently running command and its arguments. + +| Type | +| -------- | +| `object` | + +### `${command.name}` + +The currently running Garden CLI command, without positional arguments or option flags. This can be handy to e.g. change some variables based on whether you're running `garden test` or some other specific command. + +Note that this will currently always resolve to `"run workflow"` when running Workflows, as opposed to individual workflow step commands. This may be revisited at a later time, but currently all configuration is resolved once for all workflow steps. + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${command.name} +``` + +### `${command.params.*}` + +A map of all parameters set when calling the current command. This includes both positional arguments and option flags, and includes any default values set by the framework or specific command. This can be powerful if used right, but do take care since different parameters are only available in certain commands, some have array values etc. + +For example, to see if a service is in hot-reload mode, you might do something like `${command.params contains 'hot-reload' && command.params.hot-reload contains 'my-service'}`. Notice that you currently need to check both for the existence of the parameter, and also to correctly handle the array value. + +| Type | +| -------- | +| `object` | + +### `${command.params.}` + +| Type | +| ----- | +| `any` | + ### `${project.*}` Information about the Garden project. @@ -745,6 +865,46 @@ Example: my-variable: ${local.usernameLowerCase} ``` +### `${command.*}` + +Information about the currently running command and its arguments. + +| Type | +| -------- | +| `object` | + +### `${command.name}` + +The currently running Garden CLI command, without positional arguments or option flags. This can be handy to e.g. change some variables based on whether you're running `garden test` or some other specific command. + +Note that this will currently always resolve to `"run workflow"` when running Workflows, as opposed to individual workflow step commands. This may be revisited at a later time, but currently all configuration is resolved once for all workflow steps. + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${command.name} +``` + +### `${command.params.*}` + +A map of all parameters set when calling the current command. This includes both positional arguments and option flags, and includes any default values set by the framework or specific command. This can be powerful if used right, but do take care since different parameters are only available in certain commands, some have array values etc. + +For example, to see if a service is in hot-reload mode, you might do something like `${command.params contains 'hot-reload' && command.params.hot-reload contains 'my-service'}`. Notice that you currently need to check both for the existence of the parameter, and also to correctly handle the array value. + +| Type | +| -------- | +| `object` | + +### `${command.params.}` + +| Type | +| ----- | +| `any` | + ### `${project.*}` Information about the Garden project. @@ -1065,7 +1225,7 @@ The name of the parent module. ### `${template.*}` -Information about the ModuleTemplate used when generating the module. +Information about the undefined used when generating the module. | Type | | -------- | @@ -1073,7 +1233,7 @@ Information about the ModuleTemplate used when generating the module. ### `${template.name}` -The name of the ModuleTemplate being resolved. +The name of the undefined being resolved. | Type | | -------- | @@ -1081,7 +1241,7 @@ The name of the ModuleTemplate being resolved. ### `${inputs.*}` -The inputs provided to the module through a ModuleTemplate, if applicable. +The inputs provided to the module through a undefined, if applicable. | Type | Default | | -------- | ------- | @@ -1198,6 +1358,46 @@ Example: my-variable: ${local.usernameLowerCase} ``` +### `${command.*}` + +Information about the currently running command and its arguments. + +| Type | +| -------- | +| `object` | + +### `${command.name}` + +The currently running Garden CLI command, without positional arguments or option flags. This can be handy to e.g. change some variables based on whether you're running `garden test` or some other specific command. + +Note that this will currently always resolve to `"run workflow"` when running Workflows, as opposed to individual workflow step commands. This may be revisited at a later time, but currently all configuration is resolved once for all workflow steps. + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${command.name} +``` + +### `${command.params.*}` + +A map of all parameters set when calling the current command. This includes both positional arguments and option flags, and includes any default values set by the framework or specific command. This can be powerful if used right, but do take care since different parameters are only available in certain commands, some have array values etc. + +For example, to see if a service is in hot-reload mode, you might do something like `${command.params contains 'hot-reload' && command.params.hot-reload contains 'my-service'}`. Notice that you currently need to check both for the existence of the parameter, and also to correctly handle the array value. + +| Type | +| -------- | +| `object` | + +### `${command.params.}` + +| Type | +| ----- | +| `any` | + ### `${project.*}` Information about the Garden project. @@ -1518,7 +1718,7 @@ The name of the parent module. ### `${template.*}` -Information about the ModuleTemplate used when generating the module. +Information about the undefined used when generating the module. | Type | | -------- | @@ -1526,7 +1726,7 @@ Information about the ModuleTemplate used when generating the module. ### `${template.name}` -The name of the ModuleTemplate being resolved. +The name of the undefined being resolved. | Type | | -------- | @@ -1534,7 +1734,7 @@ The name of the ModuleTemplate being resolved. ### `${inputs.*}` -The inputs provided to the module through a ModuleTemplate, if applicable. +The inputs provided to the module through a undefined, if applicable. | Type | Default | | -------- | ------- | @@ -1648,6 +1848,46 @@ Example: my-variable: ${local.usernameLowerCase} ``` +### `${command.*}` + +Information about the currently running command and its arguments. + +| Type | +| -------- | +| `object` | + +### `${command.name}` + +The currently running Garden CLI command, without positional arguments or option flags. This can be handy to e.g. change some variables based on whether you're running `garden test` or some other specific command. + +Note that this will currently always resolve to `"run workflow"` when running Workflows, as opposed to individual workflow step commands. This may be revisited at a later time, but currently all configuration is resolved once for all workflow steps. + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${command.name} +``` + +### `${command.params.*}` + +A map of all parameters set when calling the current command. This includes both positional arguments and option flags, and includes any default values set by the framework or specific command. This can be powerful if used right, but do take care since different parameters are only available in certain commands, some have array values etc. + +For example, to see if a service is in hot-reload mode, you might do something like `${command.params contains 'hot-reload' && command.params.hot-reload contains 'my-service'}`. Notice that you currently need to check both for the existence of the parameter, and also to correctly handle the array value. + +| Type | +| -------- | +| `object` | + +### `${command.params.}` + +| Type | +| ----- | +| `any` | + ### `${project.*}` Information about the Garden project. diff --git a/sdk/testing.ts b/sdk/testing.ts index 333c2a4135..80de1c0574 100644 --- a/sdk/testing.ts +++ b/sdk/testing.ts @@ -6,15 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { GardenOpts } from "@garden-io/core/build/src/garden" -import { TestGarden } from "@garden-io/core/build/src/util/testing" +import { TestGarden, TestGardenOpts } from "@garden-io/core/build/src/util/testing" import { uuidv4 } from "@garden-io/core/build/src/util/util" import { Logger } from "@garden-io/core/build/src/logger/logger" import { LogLevel } from "@garden-io/core/build/src/logger/log-node" export { makeTempDir } from "@garden-io/core/build/src/util/fs" -export const makeTestGarden = async (projectRoot: string, opts: GardenOpts = {}): Promise => { +export const makeTestGarden = async (projectRoot: string, opts: TestGardenOpts = {}): Promise => { // Make sure Logger is initialized try { Logger.initialize({