Skip to content

Commit

Permalink
feat(core): provide secrets to template strings
Browse files Browse the repository at this point in the history
For projects connected with Garden Cloud, any secrets for the project in
the given environment can now be used in template strings.
  • Loading branch information
thsig committed May 5, 2020
1 parent 9f97a6f commit a6c89e2
Show file tree
Hide file tree
Showing 16 changed files with 448 additions and 124 deletions.
2 changes: 2 additions & 0 deletions garden-service/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,7 @@ export class ActionRouter implements TypeGuard {
garden: this.garden,
resolvedProviders: providers,
variables: this.garden.variables,
secrets: this.garden.secrets,
dependencyConfigs: modules,
dependencyVersions: fromPairs(modules.map((m) => [m.name, m.version])),
runtimeContext,
Expand Down Expand Up @@ -860,6 +861,7 @@ export class ActionRouter implements TypeGuard {
garden: this.garden,
resolvedProviders: providers,
variables: this.garden.variables,
secrets: this.garden.secrets,
dependencyConfigs: modules,
dependencyVersions: fromPairs(modules.map((m) => [m.name, m.version])),
runtimeContext,
Expand Down
1 change: 0 additions & 1 deletion garden-service/src/cloud/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export const makeAuthHeader = (clientAuthToken: string) => ({ "x-access-auth-tok
*/
export async function login(cloudDomain: string, log: LogEntry): Promise<string> {
const savedToken = await readAuthToken(log)

// Ping platform with saved token (if it exists)
if (savedToken) {
log.debug("Local client auth token found, verifying it with platform...")
Expand Down
32 changes: 32 additions & 0 deletions garden-service/src/cloud/secrets/garden-cloud/get-secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2018-2020 Garden Technologies, Inc. <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { got, GotResponse } from "../../../util/http"
import { GetSecretsParams } from ".."
import { StringMap } from "../../../config/common"

export async function getSecretsFromGardenCloud({
log,
projectId,
cloudDomain,
clientAuthToken,
environmentName,
}: GetSecretsParams): Promise<StringMap> {
try {
const url = `${cloudDomain}/secrets/project/${projectId}/env/${environmentName}`
const headers = { "x-access-auth-token": clientAuthToken }
const res = await got(url, { headers }).json<GotResponse<any>>()
if (res && res["status"] === "success") {
return res["data"]
}
return {}
} catch (err) {
log.error("An error occurred while fetching secrets for the project.")
return {}
}
}
33 changes: 33 additions & 0 deletions garden-service/src/cloud/secrets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2018-2020 Garden Technologies, Inc. <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { getSecretsFromGardenCloud } from "./garden-cloud/get-secret"
import { LogEntry } from "../../logger/log-entry"
import { StringMap } from "../../config/common"

export interface GetSecretsParams {
log: LogEntry
projectId: string
cloudDomain: string
clientAuthToken: string
environmentName: string
}

export async function getSecrets(params: GetSecretsParams): Promise<StringMap> {
const { log } = params
const secrets = await getSecretsFromGardenCloud(params)
const emptyKeys = Object.keys(secrets).filter((key) => !secrets[key])
if (emptyKeys.length > 0) {
const prefix =
emptyKeys.length === 1
? "The following secret key has an empty value"
: "The following secret keys have empty values"
log.error(`${prefix}: ${emptyKeys.sort().join(", ")}`)
}
return secrets
}
39 changes: 31 additions & 8 deletions garden-service/src/config/config-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ class LocalContext extends ConfigContext {
/**
* This context is available for template strings under the `project` key in configuration files.
*/

export class ProjectConfigContext extends ConfigContext {
@schema(
LocalContext.getSchema().description(
Expand Down Expand Up @@ -357,7 +358,17 @@ export class ProviderConfigContext extends ProjectConfigContext {
@schema(joiIdentifierMap(joiPrimitive()).description("Alias for the variables field."))
public var: DeepPrimitiveMap

constructor(garden: Garden, resolvedProviders: Provider[], variables: DeepPrimitiveMap) {
@schema(
joiStringMap(joi.string().description("The secret's value."))
.description("A map of all secrets for this project in the current environment.")
.meta({
internal: true,
keyPlaceholder: "<secret-name>",
})
)
public secrets: PrimitiveMap

constructor(garden: Garden, resolvedProviders: Provider[], variables: DeepPrimitiveMap, secrets: PrimitiveMap) {
super(garden.artifactsPath, garden.username)
const _this = this

Expand All @@ -369,6 +380,7 @@ export class ProviderConfigContext extends ProjectConfigContext {
)

this.var = this.variables = variables
this.secrets = secrets
}
}

Expand Down Expand Up @@ -571,6 +583,7 @@ export class ModuleConfigContext extends ProviderConfigContext {
garden,
resolvedProviders,
variables,
secrets,
moduleName,
dependencyConfigs,
dependencyVersions,
Expand All @@ -579,14 +592,15 @@ export class ModuleConfigContext extends ProviderConfigContext {
garden: Garden
resolvedProviders: Provider[]
variables: DeepPrimitiveMap
secrets: PrimitiveMap
moduleName?: string
dependencyConfigs: ModuleConfig[]
dependencyVersions: { [name: string]: ModuleVersion }
// We only supply this when resolving configuration in dependency order.
// Otherwise we pass `${runtime.*} template strings through for later resolution.
runtimeContext?: RuntimeContext
}) {
super(garden, resolvedProviders, variables)
super(garden, resolvedProviders, variables, secrets)

this.modules = new Map(
dependencyConfigs.map(
Expand All @@ -607,18 +621,27 @@ export class ModuleConfigContext extends ProviderConfigContext {
* This context is available for template strings under the `outputs` key in project configuration files.
*/
export class OutputConfigContext extends ModuleConfigContext {
constructor(
garden: Garden,
resolvedProviders: Provider[],
variables: DeepPrimitiveMap,
modules: Module[],
constructor({
garden,
resolvedProviders,
variables,
secrets,
modules,
runtimeContext,
}: {
garden: Garden
resolvedProviders: Provider[]
variables: DeepPrimitiveMap
secrets: PrimitiveMap
modules: Module[]
runtimeContext: RuntimeContext
) {
}) {
const versions = fromPairs(modules.map((m) => [m.name, m.version]))
super({
garden,
resolvedProviders,
variables,
secrets,
dependencyConfigs: modules,
dependencyVersions: versions,
runtimeContext,
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/config/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export async function getAllProviderDependencyNames(plugin: GardenPlugin, config
* Given a provider config, return implicit dependencies based on template strings.
*/
export async function getProviderTemplateReferences(config: ProviderConfig) {
const references = await collectTemplateReferences(config)
const references = collectTemplateReferences(config)
const deps: string[] = []

for (const key of references) {
Expand Down
32 changes: 29 additions & 3 deletions garden-service/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { TaskGraph, TaskResults, ProcessTasksOpts } from "./task-graph"
import { getLogger } from "./logger/logger"
import { PluginActionHandlers, GardenPlugin } from "./types/plugin/plugin"
import { loadConfig, findProjectConfig, prepareModuleResource } from "./config/base"
import { DeepPrimitiveMap } from "./config/common"
import { DeepPrimitiveMap, StringMap } from "./config/common"
import { validateSchema } from "./config/validation"
import { BaseTask } from "./tasks/base"
import { LocalConfigStore, ConfigStore, GlobalConfigStore } from "./config-store"
Expand Down Expand Up @@ -63,7 +63,9 @@ import { DependencyValidationGraph } from "./util/validate-dependencies"
import { Profile } from "./util/profiling"
import { readAuthToken, checkClientAuthToken } from "./cloud/auth"
import { ResolveModuleTask, getResolvedModules } from "./tasks/resolve-module"
import { getSecrets } from "./cloud/secrets"
import username from "username"
import { throwOnMissingSecretKeys } from "./template-string"

export interface ActionHandlerMap<T extends keyof PluginActionHandlers> {
[actionName: string]: PluginActionHandlers[T]
Expand Down Expand Up @@ -120,6 +122,7 @@ export interface GardenParams {
projectSources?: SourceConfig[]
providerConfigs: ProviderConfig[]
variables: DeepPrimitiveMap
secrets: StringMap
sessionId: string | null
username: string | undefined
vcs: VcsHandler
Expand Down Expand Up @@ -157,6 +160,7 @@ export class Garden {
public readonly projectName: string
public readonly environmentName: string
public readonly variables: DeepPrimitiveMap
public readonly secrets: StringMap
public readonly projectSources: SourceConfig[]
public readonly buildDir: BuildDir
public readonly gardenDirPath: string
Expand Down Expand Up @@ -190,6 +194,7 @@ export class Garden {
this.projectSources = params.projectSources || []
this.providerConfigs = params.providerConfigs
this.variables = params.variables
this.secrets = params.secrets
this.workingCopyId = params.workingCopyId
this.dotIgnoreFiles = params.dotIgnoreFiles
this.moduleIncludePatterns = params.moduleIncludePatterns
Expand Down Expand Up @@ -291,6 +296,7 @@ export class Garden {

const { id: projectId, domain: cloudDomain } = config

let secrets = {}
const clientAuthToken = await readAuthToken(log)
// If a client auth token exists in local storage, we assume that the user wants to be logged in to the platform.
if (clientAuthToken && !opts.noPlatform) {
Expand Down Expand Up @@ -320,7 +326,15 @@ export class Garden {
}
} else {
const tokenIsValid = await checkClientAuthToken(clientAuthToken, cloudDomain, log)
if (!tokenIsValid) {
if (tokenIsValid) {
secrets = await getSecrets({
projectId,
cloudDomain,
clientAuthToken,
log,
environmentName,
})
} else {
log.warn(deline`
You were previously logged in to the platform, but your session has expired or is invalid. Please run
${chalk.bold("garden login")} to continue using platform features, or run ${chalk.bold("garden logout")}
Expand All @@ -340,6 +354,7 @@ export class Garden {
projectName,
environmentName,
variables,
secrets,
projectSources,
buildDir,
production,
Expand Down Expand Up @@ -531,6 +546,8 @@ export class Garden {
names = getNames(rawConfigs)
}

throwOnMissingSecretKeys(Object.fromEntries(rawConfigs.map((c) => [c.name, c])), this.secrets, "Provider")

// As an optimization, we return immediately if all requested providers are already resolved
const alreadyResolvedProviders = names.map((name) => this.resolvedProviders[name]).filter(Boolean)
if (alreadyResolvedProviders.length === names.length) {
Expand Down Expand Up @@ -657,7 +674,14 @@ export class Garden {

async getOutputConfigContext(modules: Module[], runtimeContext: RuntimeContext) {
const providers = await this.resolveProviders()
return new OutputConfigContext(this, providers, this.variables, modules, runtimeContext)
return new OutputConfigContext({
garden: this,
resolvedProviders: providers,
variables: this.variables,
secrets: this.secrets,
modules,
runtimeContext,
})
}

/**
Expand All @@ -670,6 +694,7 @@ export class Garden {
const configs = await this.getRawModuleConfigs()

this.log.silly(`Resolving module configs`)
throwOnMissingSecretKeys(Object.fromEntries(configs.map((c) => [c.name, c])), this.secrets, "Module")

// Resolve the project module configs
const tasks = configs.map(
Expand Down Expand Up @@ -751,6 +776,7 @@ export class Garden {
garden: this,
resolvedProviders: providers,
variables: this.variables,
secrets: this.secrets,
dependencyConfigs: resolvedModules,
dependencyVersions: fromPairs(resolvedModules.map((m) => [m.name, m.version])),
runtimeContext,
Expand Down
11 changes: 9 additions & 2 deletions garden-service/src/outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export async function resolveProjectOutputs(garden: Garden, log: LogEntry): Prom
let needServices: string[] = []
let needTasks: string[] = []

const templateRefs = await collectTemplateReferences(garden.rawOutputs)
const templateRefs = collectTemplateReferences(garden.rawOutputs)

if (templateRefs.length === 0) {
// Nothing to resolve
Expand Down Expand Up @@ -62,7 +62,14 @@ export async function resolveProjectOutputs(garden: Garden, log: LogEntry): Prom
// No need to resolve any entities
return resolveTemplateStrings(
garden.rawOutputs,
new OutputConfigContext(garden, [], garden.variables, [], emptyRuntimeContext)
new OutputConfigContext({
garden,
resolvedProviders: [],
variables: garden.variables,
secrets: garden.secrets,
modules: [],
runtimeContext: emptyRuntimeContext,
})
)
}

Expand Down
1 change: 1 addition & 0 deletions garden-service/src/tasks/resolve-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class ResolveModuleConfigTask extends BaseTask {
garden: this.garden,
resolvedProviders: this.resolvedProviders,
variables: this.garden.variables,
secrets: this.garden.secrets,
moduleName: this.moduleConfig.name,
dependencyConfigs: [...dependencyConfigs, ...dependencyModules],
dependencyVersions: fromPairs(dependencyModules.map((m) => [m.name, m.version])),
Expand Down
7 changes: 6 additions & 1 deletion garden-service/src/tasks/resolve-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ export class ResolveProviderTask extends BaseTask {
return alreadyResolvedProviders
}

const context = new ProviderConfigContext(this.garden, resolvedProviders, this.garden.variables)
const context = new ProviderConfigContext(
this.garden,
resolvedProviders,
this.garden.variables,
this.garden.secrets
)

this.log.silly(`Resolving template strings for provider ${this.config.name}`)
let resolvedConfig = resolveTemplateStrings(this.config, context)
Expand Down
Loading

0 comments on commit a6c89e2

Please sign in to comment.