From 55e7308d28bb5a5a7a4774b0c678c76845cf6064 Mon Sep 17 00:00:00 2001 From: Vladimir Vagaytsev <10628074+vvagaytsev@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:17:25 +0100 Subject: [PATCH] fix: validate secrets before action resolution (#6822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: validate secrets before action resolution Validate missing secrets when instantiating a `ResolveActionTask` , rather than when adding configs. This would ensure we only get the error when missing secrets would actually cause a failure for the given command, while still failing relatively early even if the action happens to use another action’s outputs. * test: ensure that `ResolveActionTask` constructor throws correct error if action references missing secrets when user is not logged in * test: ensure that `scanAndAddConfigs` does not throw if an action references missing secrets The missing secrets check was moved to `ResolveActionTask` constructor. * test: ensure that `scanAndAddConfigs` does not throw if an action references missing secrets The missing secrets check was moved to `ResolveActionTask` constructor. * test: ensure that `scanAndAddConfigs` does not throw if a module references missing secrets * test: ensure that `scanAndAddConfigs` does not throw if a workflow references missing secrets * test: remove unused directory * test: rename test projects * test: create surrounding context for missing secrets in "scanAndAddConfig" tests suite * fix: fail-fast before workflow execution if any step references missing secrets * test: restore the state of the shared test data after the secrets test * test: add assertions for Garden state * Not logged in * Has no secrets * refactor: extract helper function to compose error message * chore: fix lint error * refactor: convert positional args to param object in `throwOnMissingSecretKeys` * refactor: convert positional args to param object in `detectMissingSecretKeys` * refactor: extract function to create error message footer * chore: make function `throwOnMissingSecretKeys` aware of the login state * test: fix test to expect an error * chore: more informative error message footer on missing secrets The error message depends on the login status to be less confusing. * test: ensure that `ResolveActionTask` constructor throws correct error if module references missing secrets * fix: skip `${secrets.*}` references evaluation in `ModuleResolver` Some secrets might be missing and not resolvable now, because the missing secrets check was moved to `ResolveActionTask` constructor. * chore: add a link to the secrets guide in the error message * improvement: better error message if the secrets have not been fetched * test: re-work negative tests a bit --- core/src/commands/workflow.ts | 11 ++ core/src/config/secrets.ts | 106 ++++++++++++------ core/src/garden.ts | 25 ++--- core/src/resolve-module.ts | 8 +- core/src/tasks/resolve-action.ts | 22 +++- .../data/missing-secrets/action/garden.yml | 8 ++ .../missing-secrets/action/run.garden.yml | 6 + .../data/missing-secrets/module/garden.yml | 4 +- .../data/missing-secrets/workflow/garden.yml | 4 +- .../workflow/module-a/garden.yml | 17 --- core/test/unit/src/commands/workflow.ts | 53 +++++++++ core/test/unit/src/garden.ts | 34 +++++- core/test/unit/src/tasks/resolve-action.ts | 66 +++++++++++ core/test/unit/src/template-string.ts | 62 ++++++++-- 14 files changed, 337 insertions(+), 89 deletions(-) create mode 100644 core/test/data/missing-secrets/action/garden.yml create mode 100644 core/test/data/missing-secrets/action/run.garden.yml delete mode 100644 core/test/data/missing-secrets/workflow/module-a/garden.yml diff --git a/core/src/commands/workflow.ts b/core/src/commands/workflow.ts index c5a06db12b..7527271fd8 100644 --- a/core/src/commands/workflow.ts +++ b/core/src/commands/workflow.ts @@ -34,6 +34,8 @@ import { getCustomCommands } from "./custom.js" import { getBuiltinCommands } from "./commands.js" import { styles } from "../logger/styles.js" import { deepEvaluate } from "../template/evaluate.js" +import { throwOnMissingSecretKeys } from "../config/secrets.js" +import { RemoteSourceConfigContext } from "../config/template-contexts/project.js" const { ensureDir, writeFile } = fsExtra @@ -79,6 +81,15 @@ export class WorkflowCommand extends Command { // Prepare any configured files before continuing const workflow = await garden.getWorkflowConfig(args.workflow) + throwOnMissingSecretKeys({ + configs: [workflow], + context: new RemoteSourceConfigContext(garden, garden.variables), + secrets: garden.secrets, + prefix: workflow.kind, + isLoggedIn: garden.isLoggedIn(), + log, + }) + // Merge any workflow-level environment variables into process.env. for (const [key, value] of Object.entries(toEnvVars(workflow.envVars))) { process.env[key] = value diff --git a/core/src/config/secrets.ts b/core/src/config/secrets.ts index 6c82ff6099..69c6cc6e6b 100644 --- a/core/src/config/secrets.ts +++ b/core/src/config/secrets.ts @@ -17,31 +17,33 @@ import difference from "lodash-es/difference.js" import { ConfigurationError } from "../exceptions.js" import { CONTEXT_RESOLVE_KEY_NOT_FOUND } from "../template/ast.js" -/** - * Gathers secret references in configs and throws an error if one or more referenced secrets isn't present (or has - * blank values) in the provided secrets map. - * - * Prefix should be e.g. "Module" or "Provider" (used when generating error messages). - */ -export function throwOnMissingSecretKeys( - configs: ObjectWithName[], - context: ConfigContext, - secrets: StringMap, - prefix: string, - log?: Log -) { - const allMissing: [string, ContextKeySegment[]][] = [] // [[key, missing keys]] - for (const config of configs) { - const missing = detectMissingSecretKeys(config, context, secrets) - if (missing.length > 0) { - allMissing.push([config.name, missing]) - } +const secretsGuideLink = "https://cloud.docs.garden.io/features/secrets" + +function getMessageFooter({ loadedKeys, isLoggedIn }: { loadedKeys: string[]; isLoggedIn: boolean }) { + if (!isLoggedIn) { + return `You are not logged in. Log in to get access to Secrets in Garden Cloud. See also ${secretsGuideLink}` } - if (allMissing.length === 0) { - return + if (loadedKeys.length === 0) { + return deline` + Note: You can manage secrets in Garden Cloud. No secrets have been defined for the current project and environment. See also ${secretsGuideLink} + ` + } else { + return `Secret keys with loaded values: ${loadedKeys.join(", ")}` } +} +function composeErrorMessage({ + allMissing, + secrets, + prefix, + isLoggedIn, +}: { + allMissing: [string, ContextKeySegment[]][] + secrets: StringMap + prefix: string + isLoggedIn: boolean +}): string { const descriptions = allMissing.map(([key, missing]) => `${prefix} ${key}: ${missing.join(", ")}`) /** * Secret keys with empty values should have resulted in an error by this point, but we filter on keys with @@ -50,15 +52,9 @@ export function throwOnMissingSecretKeys( const loadedKeys = Object.entries(secrets) .filter(([_key, value]) => value) .map(([key, _value]) => key) - let footer: string - if (loadedKeys.length === 0) { - footer = deline` - Note: No secrets have been loaded. If you have defined secrets for the current project and environment in Garden - Cloud, this may indicate a problem with your configuration. - ` - } else { - footer = `Secret keys with loaded values: ${loadedKeys.join(", ")}` - } + + const footer = getMessageFooter({ loadedKeys, isLoggedIn }) + const errMsg = dedent` The following secret names were referenced in configuration, but are missing from the secrets loaded remotely: @@ -66,9 +62,47 @@ export function throwOnMissingSecretKeys( ${footer} ` + return errMsg +} + +/** + * Gathers secret references in configs and throws an error if one or more referenced secrets isn't present (or has + * blank values) in the provided secrets map. + * + * Prefix should be e.g. "Module" or "Provider" (used when generating error messages). + */ +export function throwOnMissingSecretKeys({ + configs, + context, + secrets, + prefix, + isLoggedIn, + log, +}: { + configs: ObjectWithName[] + context: ConfigContext + secrets: StringMap + prefix: string + isLoggedIn: boolean + log?: Log +}) { + const allMissing: [string, ContextKeySegment[]][] = [] // [[key, missing keys]] + for (const config of configs) { + const missing = detectMissingSecretKeys({ obj: config, context, secrets }) + if (missing.length > 0) { + allMissing.push([config.name, missing]) + } + } + + if (allMissing.length === 0) { + return + } + + const errMsg = composeErrorMessage({ allMissing, secrets, prefix, isLoggedIn }) if (log) { log.silly(() => errMsg) } + throw new ConfigurationError({ message: errMsg }) } @@ -76,11 +110,15 @@ export function throwOnMissingSecretKeys( * Collects template references to secrets in obj, and returns an array of any secret keys referenced in it that * aren't present (or have blank values) in the provided secrets map. */ -export function detectMissingSecretKeys( - obj: ObjectWithName, - context: ConfigContext, +export function detectMissingSecretKeys({ + obj, + context, + secrets, +}: { + obj: ObjectWithName + context: ConfigContext secrets: StringMap -): ContextKeySegment[] { +}): ContextKeySegment[] { const requiredKeys: ContextKeySegment[] = [] const generator = getContextLookupReferences( visitAll({ diff --git a/core/src/garden.ts b/core/src/garden.ts index 61cf949f21..4fa8e88799 100644 --- a/core/src/garden.ts +++ b/core/src/garden.ts @@ -805,13 +805,14 @@ export class Garden { providerNames = getNames(rawConfigs) } - throwOnMissingSecretKeys( - rawConfigs, - new RemoteSourceConfigContext(this, this.variables), - this.secrets, - "Provider", - log - ) + throwOnMissingSecretKeys({ + configs: rawConfigs, + context: new RemoteSourceConfigContext(this, this.variables), + secrets: this.secrets, + prefix: "Provider", + isLoggedIn: this.isLoggedIn(), + log, + }) // As an optimization, we return immediately if all requested providers are already resolved const alreadyResolvedProviders = providerNames.map((name) => this.resolvedProviders[name]).filter(Boolean) @@ -1440,16 +1441,6 @@ export class Garden { ) const groupedResources = groupBy(allResources, "kind") - for (const [kind, configs] of Object.entries(groupedResources)) { - throwOnMissingSecretKeys( - configs, - new RemoteSourceConfigContext(this, this.variables), - this.secrets, - kind, - this.log - ) - } - let rawModuleConfigs = [...((groupedResources.Module as ModuleConfig[]) || [])] const rawWorkflowConfigs = (groupedResources.Workflow as WorkflowConfig[]) || [] const rawConfigTemplateResources = (groupedResources[configTemplateKind] as ConfigTemplateResource[]) || [] diff --git a/core/src/resolve-module.ts b/core/src/resolve-module.ts index 5c8973e383..ab3979d6b4 100644 --- a/core/src/resolve-module.ts +++ b/core/src/resolve-module.ts @@ -1236,17 +1236,17 @@ function partiallyEvaluateModule(config: Input, co /** * Returns false if the unresolved template value contains runtime references, in order to skip resolving it at this point. */ - const skipRuntimeReferences = (value: UnresolvedTemplateValue) => { + const skipRuntimeAndSecretsReferences = (value: UnresolvedTemplateValue) => { if ( someReferences({ value, context, opts: {}, onlyEssential: true, - matcher: (ref) => ref.keyPath[0] === "runtime", + matcher: (ref) => ref.keyPath[0] === "runtime" || ref.keyPath[0] === "secrets", }) ) { - return false // do not evaluate runtime references + return false // do not evaluate runtime and secrets references } return true @@ -1262,7 +1262,7 @@ function partiallyEvaluateModule(config: Input, co keepEscapingInTemplateStrings: true, }, }, - skipRuntimeReferences + skipRuntimeAndSecretsReferences ) // any leftover unresolved template values are now turned back into the raw form diff --git a/core/src/tasks/resolve-action.ts b/core/src/tasks/resolve-action.ts index 68e8222b22..c3cb6f1fd5 100644 --- a/core/src/tasks/resolve-action.ts +++ b/core/src/tasks/resolve-action.ts @@ -6,7 +6,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import type { ActionTaskStatusParams, BaseTask, ValidResultType, ActionTaskProcessParams } from "./base.js" +import type { + ActionTaskStatusParams, + BaseTask, + ValidResultType, + ActionTaskProcessParams, + BaseActionTaskParams, +} from "./base.js" import { BaseActionTask } from "./base.js" import { Profile } from "../util/profiling.js" import type { @@ -31,6 +37,8 @@ import { describeActionConfig } from "../actions/base.js" import { InputContext } from "../config/template-contexts/input.js" import { VariablesContext } from "../config/template-contexts/variables.js" import type { GroupConfig } from "../config/group.js" +import { throwOnMissingSecretKeys } from "../config/secrets.js" +import { RemoteSourceConfigContext } from "../config/template-contexts/project.js" export interface ResolveActionResults extends ValidResultType { state: ActionState @@ -48,6 +56,18 @@ export class ResolveActionTask extends BaseActionTask) { + super(params) + throwOnMissingSecretKeys({ + configs: [this.action.getConfig()], + context: new RemoteSourceConfigContext(params.garden, params.garden.variables), + secrets: params.garden.secrets, + prefix: this.action.kind, + isLoggedIn: params.garden.isLoggedIn(), + log: this.log, + }) + } + getDescription() { return `resolve ${this.action.longDescription()}` } diff --git a/core/test/data/missing-secrets/action/garden.yml b/core/test/data/missing-secrets/action/garden.yml new file mode 100644 index 0000000000..b244d95f3a --- /dev/null +++ b/core/test/data/missing-secrets/action/garden.yml @@ -0,0 +1,8 @@ +apiVersion: garden.io/v1 +kind: Project +name: test-project-missing-secrets-in-action +environments: + - name: local +providers: + - name: local-kubernetes + environments: [ local ] diff --git a/core/test/data/missing-secrets/action/run.garden.yml b/core/test/data/missing-secrets/action/run.garden.yml new file mode 100644 index 0000000000..807786f9bd --- /dev/null +++ b/core/test/data/missing-secrets/action/run.garden.yml @@ -0,0 +1,6 @@ +kind: Run +name: run-with-missing-secrets +type: exec +description: This should not fail while config scan +spec: + command: [ "echo", "${secrets.missing}" ] diff --git a/core/test/data/missing-secrets/module/garden.yml b/core/test/data/missing-secrets/module/garden.yml index 52c2cd2224..9638357dff 100644 --- a/core/test/data/missing-secrets/module/garden.yml +++ b/core/test/data/missing-secrets/module/garden.yml @@ -1,5 +1,5 @@ kind: Project -name: test-project-missing-secrets +name: test-project-missing-secrets-in-module environments: - name: local providers: @@ -8,4 +8,4 @@ providers: - name: test-plugin-b environments: [local] variables: - some: variable \ No newline at end of file + some: variable diff --git a/core/test/data/missing-secrets/workflow/garden.yml b/core/test/data/missing-secrets/workflow/garden.yml index ef6e23c170..ce7809e001 100644 --- a/core/test/data/missing-secrets/workflow/garden.yml +++ b/core/test/data/missing-secrets/workflow/garden.yml @@ -1,5 +1,5 @@ kind: Project -name: test-project-missing-secrets +name: test-project-missing-secrets-in-workflow environments: - name: local providers: @@ -15,4 +15,4 @@ variables: kind: Workflow name: test-workflow steps: - - command: [deploy, "${secrets.missing}"] \ No newline at end of file + - command: [deploy, "${secrets.missing}"] diff --git a/core/test/data/missing-secrets/workflow/module-a/garden.yml b/core/test/data/missing-secrets/workflow/module-a/garden.yml deleted file mode 100644 index af88619a40..0000000000 --- a/core/test/data/missing-secrets/workflow/module-a/garden.yml +++ /dev/null @@ -1,17 +0,0 @@ -kind: Module -name: module-a -type: test -services: - - name: service-a -build: - command: [echo, A] -tests: - - name: unit - command: [echo, OK] - - name: integration - command: [echo, OK] - dependencies: - - service-a -tasks: - - name: task-a - command: [echo, OK] diff --git a/core/test/unit/src/commands/workflow.ts b/core/test/unit/src/commands/workflow.ts index 74072a4aa2..624dbc1876 100644 --- a/core/test/unit/src/commands/workflow.ts +++ b/core/test/unit/src/commands/workflow.ts @@ -500,6 +500,7 @@ describe("RunWorkflowCommand", () => { const data = await readFile(filePath) expect(data.toString()).to.equal(garden.secrets.test) + delete garden.secrets.test }) it("should throw if a file references a secret that doesn't exist", async () => { @@ -544,6 +545,58 @@ describe("RunWorkflowCommand", () => { }) }) + it("should throw before execution if any step references missing secrets", async () => { + const name = "workflow-with-missing-secrets" + const configs: WorkflowConfig[] = [ + { + apiVersion: GardenApiVersion.v0, + name, + kind: "Workflow", + internal: { + basePath: garden.projectRoot, + }, + files: [], + envVars: {}, + resources: defaultWorkflowResources, + steps: [ + { + name: "init", + script: "echo init", + }, + { + name: "secrets", + script: "echo secrets ${secrets.missing}", + }, + { + name: "end", + script: "echo end", + }, + ], + }, + ] + // @ts-expect-error todo: correct types for unresolved configs + const parsedConfigs = parseTemplateCollection({ + // @ts-expect-error todo: correct types for unresolved configs + value: configs, + source: { path: [] }, + }) as WorkflowConfig[] + + garden.setRawWorkflowConfigs(parsedConfigs) + + // TestGarden is not logged in to Cloud and has no secrets + expect(garden.isLoggedIn()).to.be.false + expect(garden.secrets).to.be.empty + + await expectError(() => cmd.action({ ...defaultParams, args: { workflow: name } }), { + contains: [ + "The following secret names were referenced in configuration, but are missing from the secrets loaded remotely", + `Workflow ${name}: missing`, + "You are not logged in. Log in to get access to Secrets in Garden Cloud.", + "See also https://cloud.docs.garden.io/features/secrets", + ], + }) + }) + it("should throw if attempting to write a file to an existing directory path", async () => { garden.setRawWorkflowConfigs([ { diff --git a/core/test/unit/src/garden.ts b/core/test/unit/src/garden.ts index 48a88492cf..8d818f6d4c 100644 --- a/core/test/unit/src/garden.ts +++ b/core/test/unit/src/garden.ts @@ -3307,11 +3307,6 @@ describe("Garden", () => { }) }) - it.skip("should throw an error if references to missing secrets are present in a module config", async () => { - const garden = await makeTestGarden(getDataDir("missing-secrets", "module")) - await expectError(() => garden.scanAndAddConfigs(), { contains: "Module module-a: missing" }) - }) - it("should throw when apiVersion v0 is set in a project with action configs", async () => { const garden = await makeTestGarden(getDataDir("test-projects", "config-action-kind-v0")) @@ -3328,6 +3323,35 @@ describe("Garden", () => { expect.fail("Expected scanAndAddConfigs not to throw") } }) + + describe("missing secrets", () => { + it("should not throw when an action config references missing secrets", async () => { + const garden = await makeTestGarden(getDataDir("missing-secrets", "action")) + try { + await garden.scanAndAddConfigs() + } catch (err) { + expect.fail("Expected scanAndAddConfigs not to throw") + } + }) + + it("should not throw when a module config references missing secrets", async () => { + const garden = await makeTestGarden(getDataDir("missing-secrets", "module")) + try { + await garden.scanAndAddConfigs() + } catch (err) { + expect.fail("Expected scanAndAddConfigs not to throw") + } + }) + + it("should not throw when a workflow config references missing secrets", async () => { + const garden = await makeTestGarden(getDataDir("missing-secrets", "workflow")) + try { + await garden.scanAndAddConfigs() + } catch (err) { + expect.fail("Expected scanAndAddConfigs not to throw") + } + }) + }) }) describe("resolveModules", () => { diff --git a/core/test/unit/src/tasks/resolve-action.ts b/core/test/unit/src/tasks/resolve-action.ts index bc59579385..abe32e86d3 100644 --- a/core/test/unit/src/tasks/resolve-action.ts +++ b/core/test/unit/src/tasks/resolve-action.ts @@ -22,6 +22,7 @@ import { getAllTaskResults, getDefaultProjectConfig, } from "../../../helpers.js" +import { DEFAULT_BUILD_TIMEOUT_SEC, GardenApiVersion } from "../../../../src/constants.js" describe("ResolveActionTask", () => { let garden: TestGarden @@ -47,6 +48,71 @@ describe("ResolveActionTask", () => { }) } + describe("handling missing secrets in constructor", () => { + it("should throw if an action references missing secrets", async () => { + garden.setPartialActionConfigs([ + { + kind: "Run", + type: "test", + name: "run-with-missing-secrets", + spec: { + command: ["echo", "${secrets.missing}"], + }, + }, + ]) + + expect(garden.secrets).to.be.empty + expect(garden.isLoggedIn()).to.be.false + + await expectError(() => getTask("Run", "run-with-missing-secrets"), { + contains: [ + "The following secret names were referenced in configuration, but are missing from the secrets loaded remotely", + "Run run-with-missing-secrets: missing", + "You are not logged in. Log in to get access to Secrets in Garden Cloud.", + "See also https://cloud.docs.garden.io/features/secrets", + ], + }) + }) + + it("should throw if a module references missing secrets", async () => { + garden.setPartialModuleConfigs([ + { + apiVersion: GardenApiVersion.v0, + kind: "Module", + type: "test", + name: "module-with-missing-secrets", + allowPublish: false, + disabled: false, + path: garden.projectRoot, + build: { dependencies: [], timeout: DEFAULT_BUILD_TIMEOUT_SEC }, + serviceConfigs: [], + taskConfigs: [], + testConfigs: [], + spec: { + tasks: [ + { + name: "task-with-missing-secrets", + command: ["echo", "${secrets.missing}"], + }, + ], + }, + }, + ]) + + expect(garden.secrets).to.be.empty + expect(garden.isLoggedIn()).to.be.false + + await expectError(() => getTask("Run", "task-with-missing-secrets"), { + contains: [ + "The following secret names were referenced in configuration, but are missing from the secrets loaded remotely", + "Run task-with-missing-secrets: missing", + "You are not logged in. Log in to get access to Secrets in Garden Cloud.", + "See also https://cloud.docs.garden.io/features/secrets", + ], + }) + }) + }) + describe("resolveStatusDependencies", () => { it("returns an empty list", async () => { garden.setPartialActionConfigs([ diff --git a/core/test/unit/src/template-string.ts b/core/test/unit/src/template-string.ts index b6e1199172..96ba952d76 100644 --- a/core/test/unit/src/template-string.ts +++ b/core/test/unit/src/template-string.ts @@ -3009,8 +3009,24 @@ describe("throwOnMissingSecretKeys", () => { source: { path: [] }, } as const) - throwOnMissingSecretKeys(configs, new TestContext({}), {}, "Module") - throwOnMissingSecretKeys(configs, new TestContext({}), { someSecret: "123" }, "Module") + try { + throwOnMissingSecretKeys({ + configs, + context: new TestContext({}), + secrets: {}, + prefix: "Module", + isLoggedIn: true, + }) + throwOnMissingSecretKeys({ + configs, + context: new TestContext({}), + secrets: { someSecret: "123" }, + prefix: "Module", + isLoggedIn: true, + }) + } catch (err) { + expect.fail("Expected throwOnMissingSecretKeys not to throw") + } }) it("should not throw an error if secrets are optional in an expression", () => { @@ -3025,8 +3041,24 @@ describe("throwOnMissingSecretKeys", () => { source: { path: [] }, } as const) - throwOnMissingSecretKeys(configs, new TestContext({}), {}, "Module") - throwOnMissingSecretKeys(configs, new TestContext({}), { someSecret: "123" }, "Module") + try { + throwOnMissingSecretKeys({ + configs, + context: new TestContext({}), + secrets: {}, + prefix: "Module", + isLoggedIn: true, + }) + throwOnMissingSecretKeys({ + configs, + context: new TestContext({}), + secrets: { someSecret: "123" }, + prefix: "Module", + isLoggedIn: true, + }) + } catch (err) { + expect.fail("Expected throwOnMissingSecretKeys not to throw") + } }) it("should throw an error if one or more secrets is missing", () => { @@ -3047,8 +3079,16 @@ describe("throwOnMissingSecretKeys", () => { source: { path: [] }, } as const) + // The `isLoggedIn` flag affects the error message, assume we're logged in here void expectError( - () => throwOnMissingSecretKeys(configs, new TestContext({}), { b: "123" }, "Module"), + () => + throwOnMissingSecretKeys({ + configs, + context: new TestContext({}), + secrets: { b: "123" }, + prefix: "Module", + isLoggedIn: true, + }), (err) => { expect(err.message).to.match(/Module moduleA: a/) expect(err.message).to.match(/Module moduleB: a, c/) @@ -3056,12 +3096,20 @@ describe("throwOnMissingSecretKeys", () => { } ) + // The `isLoggedIn` flag affects the error message, assume we're logged in here void expectError( - () => throwOnMissingSecretKeys(configs, new TestContext({}), {}, "Module"), + () => + throwOnMissingSecretKeys({ + configs, + context: new TestContext({}), + secrets: {}, + prefix: "Module", + isLoggedIn: true, + }), (err) => { expect(err.message).to.match(/Module moduleA: a, b/) expect(err.message).to.match(/Module moduleB: a, b, c/) - expect(err.message).to.match(/Note: No secrets have been loaded./) + expect(err.message).to.match(/Note: You can manage secrets in Garden Cloud./) } ) })