diff --git a/garden-service/src/actions.ts b/garden-service/src/actions.ts index 5fceb84b8a..dccbbe3093 100644 --- a/garden-service/src/actions.ts +++ b/garden-service/src/actions.ts @@ -381,7 +381,7 @@ export class ActionHelper implements TypeGuard { } } - private async callActionHandler( + private async callActionHandler>( { params, actionType, pluginName, defaultHandler }: { params: ActionHelperParams, @@ -402,7 +402,7 @@ export class ActionHelper implements TypeGuard { return (handler)(handlerParams) } - private async callModuleHandler>( + private async callModuleHandler>( { params, actionType, defaultHandler }: { params: ModuleActionHelperParams, actionType: T, defaultHandler?: ModuleActions[T] }, ): Promise { diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index d37f46ba37..134b73de24 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -28,6 +28,7 @@ import { sortBy, difference, find, + findIndex, } from "lodash" const AsyncLock = require("async-lock") @@ -44,7 +45,7 @@ import { pluginModuleSchema, pluginSchema, } from "./types/plugin/plugin" -import { Environment, SourceConfig, defaultProvider, Provider } from "./config/project" +import { Environment, SourceConfig, defaultProvider, ProviderConfig, Provider } from "./config/project" import { findByName, getIgnorer, @@ -418,7 +419,7 @@ export class Garden { this.registeredPlugins[name] = factory } - private async loadPlugin(pluginName: string, config: object) { + private async loadPlugin(pluginName: string, config: ProviderConfig) { const factory = this.registeredPlugins[pluginName] if (!factory) { @@ -428,12 +429,11 @@ export class Garden { }) } - let plugin + let plugin: GardenPlugin try { plugin = await factory({ projectName: this.projectName, - config, log: this.log, }) } catch (error) { @@ -447,18 +447,6 @@ export class Garden { this.loadedPlugins[pluginName] = plugin - // allow plugins to extend their own config (that gets passed to action handlers) - const providerConfig = findByName(this.environment.providers, pluginName) - if (providerConfig) { - extend(providerConfig, plugin.config, config) - } else { - const provider: Provider = { - name: pluginName, - config: extend({ name: pluginName }, plugin.config, config), - } - this.environment.providers.push(provider) - } - for (const modulePath of plugin.modules || []) { let moduleConfig = await this.resolveModule(modulePath) if (!moduleConfig) { @@ -486,6 +474,32 @@ export class Garden { handler && this.addModuleActionHandler(pluginName, actionType, moduleType, handler) } } + + // allow plugins to be configured more than once + // (to support extending config for fixed plugins and environment defaults) + let providerIndex = findIndex(this.environment.providers, ["name", pluginName]) + let providerConfig: ProviderConfig = providerIndex === -1 + ? config + : this.environment.providers[providerIndex].config + + extend(providerConfig, config) + + // call configureProvider action if provided + const configureHandler = actions.configureProvider + if (configureHandler) { + const configureOutput = await configureHandler({ config: providerConfig }) + providerConfig = configureOutput.config + } + + if (plugin.configSchema) { + providerConfig = validate(providerConfig, plugin.configSchema, { context: `${pluginName} configuration` }) + } + + if (providerIndex === -1) { + this.environment.providers.push({ name: pluginName, config: providerConfig }) + } else { + this.environment.providers[providerIndex].config = providerConfig + } } private getPlugin(pluginName: string) { @@ -839,7 +853,7 @@ export class Garden { @param force - add the module again, even if it's already registered */ async addModule(config: ModuleConfig, force = false) { - const validateHandler = await this.getModuleActionHandler({ actionType: "validate", moduleType: config.type }) + const validateHandler = await this.getModuleActionHandler({ actionType: "configure", moduleType: config.type }) const ctx = this.getPluginContext(validateHandler["pluginName"]) config = await validateHandler({ ctx, moduleConfig: config }) diff --git a/garden-service/src/plugins/container/container.ts b/garden-service/src/plugins/container/container.ts index af7cb94249..d0ee34ccfb 100644 --- a/garden-service/src/plugins/container/container.ts +++ b/garden-service/src/plugins/container/container.ts @@ -15,7 +15,7 @@ import { GardenPlugin } from "../../types/plugin/plugin" import { BuildModuleParams, GetBuildStatusParams, - ValidateModuleParams, + ConfigureModuleParams, HotReloadServiceParams, PublishModuleParams, } from "../../types/plugin/params" @@ -23,7 +23,7 @@ import { keyBy } from "lodash" import { containerHelpers } from "./helpers" import { ContainerModule, containerModuleSpecSchema } from "./config" -export async function validateContainerModule({ ctx, moduleConfig }: ValidateModuleParams) { +export async function configureContainerModule({ ctx, moduleConfig }: ConfigureModuleParams) { moduleConfig.spec = validateWithPath({ config: moduleConfig.spec, schema: containerModuleSpecSchema, @@ -148,7 +148,7 @@ export async function validateContainerModule({ ctx, moduleConfig }: ValidateMod export const gardenPlugin = (): GardenPlugin => ({ moduleActions: { container: { - validate: validateContainerModule, + configure: configureContainerModule, async getBuildStatus({ module, log }: GetBuildStatusParams) { const identifier = await containerHelpers.imageExistsLocally(module) diff --git a/garden-service/src/plugins/exec.ts b/garden-service/src/plugins/exec.ts index 247ca3a279..11d9ed59d0 100644 --- a/garden-service/src/plugins/exec.ts +++ b/garden-service/src/plugins/exec.ts @@ -21,15 +21,16 @@ import { Module } from "../types/module" import { BuildResult, BuildStatus, - ValidateModuleResult, TestResult, RunTaskResult, + ConfigureModuleResult, } from "../types/plugin/outputs" import { BuildModuleParams, GetBuildStatusParams, - ValidateModuleParams, - TestModuleParams, RunTaskParams, + TestModuleParams, + RunTaskParams, + ConfigureModuleParams, } from "../types/plugin/params" import { CommonServiceSpec } from "../config/service" import { BaseTestSpec, baseTestSpecSchema } from "../config/test" @@ -84,9 +85,9 @@ export const execModuleSpecSchema = Joi.object() export interface ExecModule extends Module { } -export async function parseExecModule( - { ctx, moduleConfig }: ValidateModuleParams, -): Promise { +export async function configureExecModule( + { ctx, moduleConfig }: ConfigureModuleParams, +): Promise { moduleConfig.spec = validateWithPath({ config: moduleConfig.spec, @@ -225,7 +226,7 @@ export async function runExecTask(params: RunTaskParams): Promise export const execPlugin: GardenPlugin = { moduleActions: { exec: { - validate: parseExecModule, + configure: configureExecModule, getBuildStatus: getExecModuleBuildStatus, build: buildExecModule, runTask: runExecTask, diff --git a/garden-service/src/plugins/google/google-cloud-functions.ts b/garden-service/src/plugins/google/google-cloud-functions.ts index fe47821019..d3548fa84e 100644 --- a/garden-service/src/plugins/google/google-cloud-functions.ts +++ b/garden-service/src/plugins/google/google-cloud-functions.ts @@ -11,12 +11,12 @@ import { validateWithPath, } from "../../config/common" import { Module } from "../../types/module" -import { ValidateModuleResult } from "../../types/plugin/outputs" +import { ConfigureModuleResult } from "../../types/plugin/outputs" import { DeployServiceParams, GetServiceOutputsParams, GetServiceStatusParams, - ValidateModuleParams, + ConfigureModuleParams, } from "../../types/plugin/params" import { ServiceState, ServiceStatus, ingressHostnameSchema, Service } from "../../types/service" import { @@ -79,9 +79,9 @@ function getGcfProject(service: Service, provider: Provi return service.spec.project || provider.config["default-project"] || null } -export async function parseGcfModule( - { ctx, moduleConfig }: ValidateModuleParams, -): Promise> { +export async function configureGcfModule( + { ctx, moduleConfig }: ConfigureModuleParams, +): Promise> { // TODO: check that each function exists at the specified path moduleConfig.spec = validateWithPath({ @@ -116,7 +116,7 @@ export const gardenPlugin = (): GardenPlugin => ({ }, moduleActions: { "google-cloud-function": { - validate: parseGcfModule, + configure: configureGcfModule, async deployService(params: DeployServiceParams) { const { ctx, service } = params diff --git a/garden-service/src/plugins/kubernetes/container/handlers.ts b/garden-service/src/plugins/kubernetes/container/handlers.ts index b435e0f603..3e86d5c2de 100644 --- a/garden-service/src/plugins/kubernetes/container/handlers.ts +++ b/garden-service/src/plugins/kubernetes/container/handlers.ts @@ -12,15 +12,15 @@ import { getServiceLogs } from "./logs" import { execInService, runContainerModule, runContainerService, runContainerTask } from "./run" import { testContainerModule } from "./test" import { ConfigurationError } from "../../../exceptions" -import { validateContainerModule } from "../../container/container" +import { configureContainerModule } from "../../container/container" import { KubernetesProvider } from "../kubernetes" -import { ValidateModuleParams } from "../../../types/plugin/params" +import { ConfigureModuleParams } from "../../../types/plugin/params" import { getContainerServiceStatus, getServiceOutputs } from "./status" import { getTestResult } from "../test" import { ContainerModule } from "../../container/config" -async function validate(params: ValidateModuleParams) { - const config = await validateContainerModule(params) +async function configure(params: ConfigureModuleParams) { + const config = await configureContainerModule(params) // validate ingress specs const provider: KubernetesProvider = params.ctx.provider @@ -49,6 +49,7 @@ async function validate(params: ValidateModuleParams) { } export const containerHandlers = { + configure, deployService: deployContainerService, deleteService, execInService, @@ -62,5 +63,4 @@ export const containerHandlers = { runService: runContainerService, runTask: runContainerTask, testModule: testContainerModule, - validate, } diff --git a/garden-service/src/plugins/kubernetes/helm/config.ts b/garden-service/src/plugins/kubernetes/helm/config.ts index a44d28c178..081bfd9d0f 100644 --- a/garden-service/src/plugins/kubernetes/helm/config.ts +++ b/garden-service/src/plugins/kubernetes/helm/config.ts @@ -20,8 +20,8 @@ import { joiUserIdentifier, } from "../../../config/common" import { Module, FileCopySpec } from "../../../types/module" -import { ValidateModuleParams } from "../../../types/plugin/params" -import { ValidateModuleResult } from "../../../types/plugin/outputs" +import { ConfigureModuleParams } from "../../../types/plugin/params" +import { ConfigureModuleResult } from "../../../types/plugin/outputs" import { containsSource } from "./common" import { ConfigurationError } from "../../../exceptions" import { deline } from "../../../util/string" @@ -205,8 +205,8 @@ export const helmModuleSpecSchema = Joi.object().keys({ ), }) -export async function validateHelmModule({ ctx, moduleConfig }: ValidateModuleParams) - : Promise> { +export async function validateHelmModule({ ctx, moduleConfig }: ConfigureModuleParams) + : Promise> { moduleConfig.spec = validateWithPath({ config: moduleConfig.spec, schema: helmModuleSpecSchema, diff --git a/garden-service/src/plugins/kubernetes/helm/handlers.ts b/garden-service/src/plugins/kubernetes/helm/handlers.ts index 401de4cdff..09e43b081d 100644 --- a/garden-service/src/plugins/kubernetes/helm/handlers.ts +++ b/garden-service/src/plugins/kubernetes/helm/handlers.ts @@ -8,7 +8,7 @@ import { ModuleAndRuntimeActions } from "../../../types/plugin/plugin" import { getExecModuleBuildStatus } from "../../exec" -import { HelmModule, validateHelmModule } from "./config" +import { HelmModule, validateHelmModule as configureHelmModule } from "./config" import { buildHelmModule } from "./build" import { getServiceStatus, getServiceOutputs } from "./status" import { deployService, deleteService } from "./deployment" @@ -20,6 +20,7 @@ import { testHelmModule } from "./test" export const helmHandlers: Partial> = { build: buildHelmModule, + configure: configureHelmModule, // TODO: add execInService handler deleteService, deployService, @@ -33,5 +34,4 @@ export const helmHandlers: Partial> = { runModule: runHelmModule, runTask: runHelmTask, testModule: testHelmModule, - validate: validateHelmModule, } diff --git a/garden-service/src/plugins/kubernetes/kubernetes.ts b/garden-service/src/plugins/kubernetes/kubernetes.ts index c1bfc0f8b4..c57a4d2a4b 100644 --- a/garden-service/src/plugins/kubernetes/kubernetes.ts +++ b/garden-service/src/plugins/kubernetes/kubernetes.ts @@ -9,7 +9,7 @@ import * as Joi from "joi" import dedent = require("dedent") -import { joiArray, joiIdentifier, validate } from "../../config/common" +import { joiArray, joiIdentifier } from "../../config/common" import { GardenPlugin } from "../../types/plugin/plugin" import { Provider, providerConfigBaseSchema, ProviderConfig } from "../../config/project" import { helmHandlers } from "./helm/handlers" @@ -139,11 +139,9 @@ const configSchema = kubernetesConfigBase _system: Joi.any().meta({ internal: true }), }) -export function gardenPlugin({ config }: { config: KubernetesConfig }): GardenPlugin { - config = validate(config, configSchema, { context: "kubernetes provider config" }) - +export function gardenPlugin(): GardenPlugin { return { - config, + configSchema, actions: { getEnvironmentStatus: getRemoteEnvironmentStatus, prepareEnvironment: prepareRemoteEnvironment, diff --git a/garden-service/src/plugins/kubernetes/local.ts b/garden-service/src/plugins/kubernetes/local.ts index 28a65a308e..5ec41e1eba 100644 --- a/garden-service/src/plugins/kubernetes/local.ts +++ b/garden-service/src/plugins/kubernetes/local.ts @@ -10,17 +10,16 @@ import * as execa from "execa" import { safeLoad } from "js-yaml" import * as Joi from "joi" import { join } from "path" -import { GardenPlugin } from "../../types/plugin/plugin" -import { validate } from "../../config/common" +import { GardenPlugin, PluginFactoryParams } from "../../types/plugin/plugin" import { gardenPlugin as k8sPlugin, KubernetesBaseConfig, kubernetesConfigBase, - KubernetesConfig, } from "./kubernetes" import { readFile } from "fs-extra" import { homedir } from "os" import { getLocalEnvironmentStatus, prepareLocalEnvironment } from "./init" +import { ConfigureProviderParams } from "../../types/plugin/params" // TODO: split this into separate plugins to handle Docker for Mac and Minikube @@ -73,79 +72,83 @@ const configSchema = kubernetesConfigBase export const name = "local-kubernetes" -export async function gardenPlugin({ projectName, config, log }): Promise { - config = validate(config, configSchema, { context: "local-kubernetes provider config" }) - - let context = config.context - let defaultHostname = config.defaultHostname - - if (!context) { - // automatically detect supported kubectl context if not explicitly configured - const kubeConfig = await getKubeConfig() - const currentContext = kubeConfig["current-context"] - - if (currentContext && supportedContexts.includes(currentContext)) { - // prefer current context if set and supported - context = currentContext - log.debug({ section: name, msg: `Using current context: ${context}` }) - } else if (kubeConfig.contexts) { - const availableContexts = kubeConfig.contexts.map(c => c.name) - - for (const supportedContext of supportedContexts) { - if (availableContexts.includes(supportedContext)) { - context = supportedContext - log.debug({ section: name, msg: `Using detected context: ${context}` }) - break +export function gardenPlugin({ projectName, log }: PluginFactoryParams): GardenPlugin { + const plugin = k8sPlugin() + + plugin.configSchema = configSchema + + plugin.actions!.configureProvider = async ({ config }: ConfigureProviderParams) => { + let context = config.context + let defaultHostname = config.defaultHostname + + if (!context) { + // automatically detect supported kubectl context if not explicitly configured + const kubeConfig = await getKubeConfig() + const currentContext = kubeConfig["current-context"] + + if (currentContext && supportedContexts.includes(currentContext)) { + // prefer current context if set and supported + context = currentContext + log.debug({ section: name, msg: `Using current context: ${context}` }) + } else if (kubeConfig.contexts) { + const availableContexts = kubeConfig.contexts.map(c => c.name) + + for (const supportedContext of supportedContexts) { + if (availableContexts.includes(supportedContext)) { + context = supportedContext + log.debug({ section: name, msg: `Using detected context: ${context}` }) + break + } } } } - } - if (!context) { - context = supportedContexts[0] - log.debug({ section: name, msg: `No kubectl context auto-detected, using default: ${context}` }) - } + if (!context) { + context = supportedContexts[0] + log.debug({ section: name, msg: `No kubectl context auto-detected, using default: ${context}` }) + } + + if (context === "minikube") { + await execa("minikube", ["config", "set", "WantUpdateNotification", "false"]) - if (context === "minikube") { - await execa("minikube", ["config", "set", "WantUpdateNotification", "false"]) + if (!defaultHostname) { + // use the nip.io service to give a hostname to the instance, if none is explicitly configured + const minikubeIp = await execa.stdout("minikube", ["ip"]) + defaultHostname = `${projectName}.${minikubeIp}.nip.io` + } - if (!defaultHostname) { - // use the nip.io service to give a hostname to the instance, if none is explicitly configured - const minikubeIp = await execa.stdout("minikube", ["ip"]) - defaultHostname = `${projectName}.${minikubeIp}.nip.io` + await Promise.all([ + // TODO: wait for ingress addon to be ready, if it was previously disabled + execa("minikube", ["addons", "enable", "ingress"]), + setMinikubeDockerEnv(), + ]) + } else { + if (!defaultHostname) { + defaultHostname = `${projectName}.local.app.garden` + } } - await Promise.all([ - // TODO: wait for ingress addon to be ready, if it was previously disabled - execa("minikube", ["addons", "enable", "ingress"]), - setMinikubeDockerEnv(), - ]) - } else { - if (!defaultHostname) { - defaultHostname = `${projectName}.local.app.garden` + config = { + name: config.name, + context, + defaultHostname, + deploymentRegistry: { + hostname: "foo.garden", // this is not used by this plugin, but required by the base plugin + namespace: "_", + }, + forceSsl: false, + imagePullSecrets: config.imagePullSecrets, + ingressHttpPort: 80, + ingressHttpsPort: 443, + ingressClass: "nginx", + namespace: config.namespace || projectName, + tlsCertificates: config.tlsCertificates, + _system: config._system, } - } - const k8sConfig: KubernetesConfig = { - name: config.name, - context, - defaultHostname, - deploymentRegistry: { - hostname: "foo.garden", // this is not used by this plugin, but required by the base plugin - namespace: "_", - }, - forceSsl: false, - imagePullSecrets: config.imagePullSecrets, - ingressHttpPort: 80, - ingressHttpsPort: 443, - ingressClass: config.ingressClass, - namespace: config.namespace || projectName, - tlsCertificates: config.tlsCertificates, - _system: config._system, + return { name: config.name, config } } - const plugin = k8sPlugin({ config: k8sConfig }) - // override the environment configuration steps plugin.actions!.getEnvironmentStatus = getLocalEnvironmentStatus plugin.actions!.prepareEnvironment = prepareLocalEnvironment diff --git a/garden-service/src/plugins/local/local-google-cloud-functions.ts b/garden-service/src/plugins/local/local-google-cloud-functions.ts index e27ec44a7e..497198b244 100644 --- a/garden-service/src/plugins/local/local-google-cloud-functions.ts +++ b/garden-service/src/plugins/local/local-google-cloud-functions.ts @@ -6,11 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { ValidateModuleParams } from "../../types/plugin/params" +import { ConfigureModuleParams } from "../../types/plugin/params" import { join } from "path" import { GcfModule, - parseGcfModule, + configureGcfModule, } from "../google/google-cloud-functions" import { GardenPlugin, @@ -33,8 +33,8 @@ export const gardenPlugin = (): GardenPlugin => ({ moduleActions: { "google-cloud-function": { - async validate(params: ValidateModuleParams) { - const parsed = await parseGcfModule(params) + async configure(params: ConfigureModuleParams) { + const parsed = await configureGcfModule(params) // convert the module and services to containers to run locally const serviceConfigs: ServiceConfig[] = parsed.serviceConfigs.map((s) => { diff --git a/garden-service/src/plugins/openfaas/openfaas.ts b/garden-service/src/plugins/openfaas/openfaas.ts index 43a1f70a3d..0b079c0e0a 100644 --- a/garden-service/src/plugins/openfaas/openfaas.ts +++ b/garden-service/src/plugins/openfaas/openfaas.ts @@ -15,11 +15,11 @@ import { Garden } from "../../garden" import { PluginContext } from "../../plugin-context" import { joiArray, validate, PrimitiveMap } from "../../config/common" import { Module } from "../../types/module" -import { ValidateModuleResult } from "../../types/plugin/outputs" +import { ConfigureModuleResult } from "../../types/plugin/outputs" import { PrepareEnvironmentParams, GetEnvironmentStatusParams, - ValidateModuleParams, + ConfigureModuleParams, DeleteServiceParams, GetServiceLogsParams, } from "../../types/plugin/params" @@ -106,10 +106,9 @@ const configSchema = providerConfigBaseSchema type OpenFaasProvider = Provider -export function gardenPlugin({ config }: { config: OpenFaasConfig }): GardenPlugin { - config = validate(config, configSchema, { context: "OpenFaaS provider config" }) - +export function gardenPlugin(): GardenPlugin { return { + configSchema, modules: [join(STATIC_DIR, "openfaas", "templates")], actions: { async getEnvironmentStatus({ ctx, log }: GetEnvironmentStatusParams) { @@ -162,7 +161,7 @@ export function gardenPlugin({ config }: { config: OpenFaasConfig }): GardenPlug }, moduleActions: { openfaas: { - async validate({ moduleConfig }: ValidateModuleParams): Promise { + async configure({ moduleConfig }: ConfigureModuleParams): Promise { moduleConfig.spec = validate( moduleConfig.spec, openfaasModuleSpecSchame, diff --git a/garden-service/src/types/plugin/outputs.ts b/garden-service/src/types/plugin/outputs.ts index 994ba4c3d6..bd4b9b9952 100644 --- a/garden-service/src/types/plugin/outputs.ts +++ b/garden-service/src/types/plugin/outputs.ts @@ -13,6 +13,13 @@ import { Module } from "../module" import { ServiceStatus } from "../service" import { moduleConfigSchema, ModuleConfig } from "../../config/module" import { DashboardPage, dashboardPagesSchema } from "../../config/dashboard" +import { ProviderConfig, providerConfigBaseSchema, Provider } from "../../config/project" + +export interface ConfigureProviderResult extends Provider { } +export const configureProviderResultSchema = Joi.object() + .keys({ + config: providerConfigBaseSchema, + }) export interface EnvironmentStatus { ready: boolean @@ -142,7 +149,7 @@ export const moduleTypeDescriptionSchema = Joi.object() ), }) -export type ValidateModuleResult = +export type ConfigureModuleResult = ModuleConfig< T["spec"], T["serviceConfigs"][0]["spec"], @@ -150,7 +157,7 @@ export type ValidateModuleResult = T["taskConfigs"][0]["spec"] > -export const validateModuleResultSchema = moduleConfigSchema +export const configureModuleResultSchema = moduleConfigSchema export interface BuildResult { buildLog?: string @@ -308,6 +315,8 @@ export const taskStatusSchema = Joi.object() }) export interface PluginActionOutputs { + configureProvider: Promise + getEnvironmentStatus: Promise prepareEnvironment: Promise cleanupEnvironment: Promise @@ -335,7 +344,7 @@ export interface TaskActionOutputs { export interface ModuleActionOutputs extends ServiceActionOutputs { describeType: Promise - validate: Promise + configure: Promise getBuildStatus: Promise build: Promise pushModule: Promise diff --git a/garden-service/src/types/plugin/params.ts b/garden-service/src/types/plugin/params.ts index f3473adf05..485d969a70 100644 --- a/garden-service/src/types/plugin/params.ts +++ b/garden-service/src/types/plugin/params.ts @@ -19,6 +19,7 @@ import { EnvironmentStatus, ServiceLogEntry, environmentStatusSchema } from "./o import { moduleConfigSchema } from "../../config/module" import { testConfigSchema } from "../../config/test" import { taskSchema } from "../../config/task" +import { ProviderConfig } from "../../config/project" export interface PluginActionContextParams { ctx: PluginContext @@ -70,16 +71,10 @@ const taskActionParamsSchema = moduleActionParamsSchema /** * Plugin actions */ -export interface DescribeModuleTypeParams { } -export const describeModuleTypeParamsSchema = Joi.object() - .keys({}) - -export interface ValidateModuleParams { - ctx: PluginContext - log?: LogEntry - moduleConfig: T["_ConfigType"] +export interface ConfigureProviderParams { + config: T } -export const validateModuleParamsSchema = Joi.object() +export const configureProviderParamsSchema = Joi.object() .keys({ ctx: pluginContextSchema .required(), @@ -131,6 +126,8 @@ export interface DeleteSecretParams extends PluginActionParamsBase { export const deleteSecretParamsSchema = getSecretParamsSchema export interface PluginActionParams { + configureProvider: ConfigureProviderParams + getEnvironmentStatus: GetEnvironmentStatusParams prepareEnvironment: PrepareEnvironmentParams cleanupEnvironment: CleanupEnvironmentParams @@ -143,6 +140,24 @@ export interface PluginActionParams { /** * Module actions */ +export interface DescribeModuleTypeParams { } +export const describeModuleTypeParamsSchema = Joi.object() + .keys({}) + +export interface ConfigureModuleParams { + ctx: PluginContext + logEntry?: LogEntry + moduleConfig: T["_ConfigType"] +} +export const configureModuleParamsSchema = Joi.object() + .keys({ + ctx: pluginContextSchema + .required(), + logEntry: logEntrySchema, + moduleConfig: moduleConfigSchema + .required(), + }) + export interface GetBuildStatusParams extends PluginModuleActionParamsBase { } export const getBuildStatusParamsSchema = moduleActionParamsSchema @@ -336,7 +351,7 @@ export interface TaskActionParams { export interface ModuleActionParams { describeType: DescribeModuleTypeParams, - validate: ValidateModuleParams + configure: ConfigureModuleParams getBuildStatus: GetBuildStatusParams build: BuildModuleParams pushModule: PushModuleParams diff --git a/garden-service/src/types/plugin/plugin.ts b/garden-service/src/types/plugin/plugin.ts index 9e8cf29678..5503c2de60 100644 --- a/garden-service/src/types/plugin/plugin.ts +++ b/garden-service/src/types/plugin/plugin.ts @@ -18,15 +18,14 @@ import { Module } from "../module" import { serviceStatusSchema } from "../service" import { serviceOutputsSchema } from "../../config/service" import { LogNode } from "../../logger/log-node" -import { Provider } from "../../config/project" import { ModuleActionParams, PluginActionParams, ServiceActionParams, TaskActionParams, + getEnvironmentStatusParamsSchema, prepareEnvironmentParamsSchema, cleanupEnvironmentParamsSchema, - getEnvironmentStatusParamsSchema, getSecretParamsSchema, setSecretParamsSchema, deleteSecretParamsSchema, @@ -38,7 +37,7 @@ import { getServiceLogsParamsSchema, runServiceParamsSchema, describeModuleTypeParamsSchema, - validateModuleParamsSchema, + configureModuleParamsSchema, getBuildStatusParamsSchema, buildModuleParamsSchema, pushModuleParamsSchema, @@ -49,6 +48,7 @@ import { publishModuleParamsSchema, getTaskStatusParamsSchema, runTaskParamsSchema, + configureProviderParamsSchema, } from "./params" import { buildModuleResultSchema, @@ -71,10 +71,11 @@ import { TaskActionOutputs, setSecretResultSchema, testResultSchema, - validateModuleResultSchema, + configureModuleResultSchema, publishModuleResultSchema, taskStatusSchema, runTaskResultSchema, + configureProviderResultSchema, } from "./outputs" export type PluginActions = { @@ -108,6 +109,21 @@ export interface PluginActionDescription { } export const pluginActionDescriptions: { [P in PluginActionName]: PluginActionDescription } = { + configureProvider: { + description: dedent` + Validate and transform the given provider configuration. + + Note that this does not need to perform structural schema validation (the framework does that + automatically), but should in turn perform semantic validation to make sure the configuration is sane. + + This can also be used to further specify the semantics of the provider, including dependencies. + + Important: This action is called on most executions of Garden commands, so it should return quickly + and avoid performing expensive processing or network calls. + `, + paramsSchema: configureProviderParamsSchema, + resultSchema: configureProviderResultSchema, + }, getEnvironmentStatus: { description: dedent` Check if the current environment is ready for use by this plugin. Use this action in combination @@ -287,13 +303,16 @@ export const moduleActionDescriptions: should not specify the built-in fields (such as \`name\`, \`type\` and \`description\`). Used when auto-generating framework documentation. + + This action is called on every execution of Garden, so it should return quickly and avoid doing + any network calls. `, paramsSchema: describeModuleTypeParamsSchema, resultSchema: moduleTypeDescriptionSchema, }, - validate: { + configure: { description: dedent` - Validate and optionally transform the given module configuration. + Validate and transform the given module configuration. Note that this does not need to perform structural schema validation (the framework does that automatically), but should in turn perform semantic validation to make sure the configuration is sane. @@ -302,9 +321,12 @@ export const moduleActionDescriptions: configuration and test configuration. Since services and tests are not specified using built-in framework configuration fields, this action needs to specify those via the \`serviceConfigs\` and \`testConfigs\` output keys. + + This action is called on every execution of Garden, so it should return quickly and avoid doing + any network calls. `, - paramsSchema: validateModuleParamsSchema, - resultSchema: validateModuleResultSchema, + paramsSchema: configureModuleParamsSchema, + resultSchema: configureModuleResultSchema, }, getBuildStatus: { @@ -407,7 +429,7 @@ export const pluginActionNames: PluginActionName[] = Object. export const moduleActionNames: ModuleActionName[] = Object.keys(moduleActionDescriptions) export interface GardenPlugin { - config?: object + configSchema?: Joi.Schema, configKeys?: string[] modules?: string[] @@ -416,14 +438,13 @@ export interface GardenPlugin { moduleActions?: { [moduleType: string]: Partial } } -export interface PluginFactoryParams { - config: T["config"], +export interface PluginFactoryParams { log: LogNode, projectName: string, } -export interface PluginFactory { - (params: PluginFactoryParams): GardenPlugin | Promise +export interface PluginFactory { + (params: PluginFactoryParams): GardenPlugin | Promise } export type RegisterPluginParam = string | PluginFactory export interface Plugins { @@ -432,12 +453,8 @@ export interface Plugins { export const pluginSchema = Joi.object() .keys({ - config: Joi.object() - .meta({ extendable: true }) - .description( - "Plugins may use this key to override or augment their configuration " + - "(as specified in the garden.yml provider configuration.", - ), + // TODO: make this an OpenAPI schema for portability + configSchema: Joi.object({ isJoi: Joi.boolean().only(true).required() }).unknown(true), modules: joiArray(Joi.string()) .description( "Plugins may optionally provide paths to Garden modules that are loaded as part of the plugin. " + diff --git a/garden-service/test/helpers.ts b/garden-service/test/helpers.ts index 0987646ef7..da7c5048c3 100644 --- a/garden-service/test/helpers.ts +++ b/garden-service/test/helpers.ts @@ -27,7 +27,7 @@ import { mapValues, fromPairs } from "lodash" import { DeleteSecretParams, GetSecretParams, - ValidateModuleParams, + ConfigureModuleParams, RunModuleParams, RunServiceParams, RunTaskParams, @@ -92,7 +92,7 @@ export const testModuleSpecSchema = containerModuleSpecSchema tasks: joiArray(testModuleTaskSchema), }) -export async function validateTestModule({ moduleConfig }: ValidateModuleParams) { +export async function configureTestModule({ moduleConfig }: ConfigureModuleParams) { moduleConfig.spec = validate( moduleConfig.spec, testModuleSpecSchema, @@ -126,7 +126,7 @@ export async function validateTestModule({ moduleConfig }: ValidateModuleParams) } export const testPlugin: PluginFactory = (): GardenPlugin => { - const _config = {} + const secrets = {} return { actions: { @@ -135,17 +135,17 @@ export const testPlugin: PluginFactory = (): GardenPlugin => { }, async setSecret({ key, value }: SetSecretParams) { - _config[key] = value + secrets[key] = value return {} }, async getSecret({ key }: GetSecretParams) { - return { value: _config[key] || null } + return { value: secrets[key] || null } }, async deleteSecret({ key }: DeleteSecretParams) { - if (_config[key]) { - delete _config[key] + if (secrets[key]) { + delete secrets[key] return { found: true } } else { return { found: false } @@ -155,7 +155,7 @@ export const testPlugin: PluginFactory = (): GardenPlugin => { moduleActions: { test: { testModule: testExecModule, - validate: validateTestModule, + configure: configureTestModule, build: buildExecModule, runModule, diff --git a/garden-service/test/src/actions.ts b/garden-service/test/src/actions.ts index bcbab6a9cd..65ef6be6c0 100644 --- a/garden-service/test/src/actions.ts +++ b/garden-service/test/src/actions.ts @@ -13,7 +13,7 @@ import { ServiceLogEntry } from "../../src/types/plugin/outputs" import { LogEntry } from "../../src/logger/log-entry" import { describeModuleTypeParamsSchema, - validateModuleParamsSchema, + configureModuleParamsSchema, getBuildStatusParamsSchema, buildModuleParamsSchema, pushModuleParamsSchema, @@ -389,8 +389,8 @@ const testPlugin: PluginFactory = async () => ({ } }, - validate: async (params) => { - validate(params, validateModuleParamsSchema) + configure: async (params) => { + validate(params, configureModuleParamsSchema) const serviceConfigs = params.moduleConfig.spec.services.map(spec => ({ name: spec.name, diff --git a/garden-service/test/src/commands/call.ts b/garden-service/test/src/commands/call.ts index 649c596681..0a5818f52d 100644 --- a/garden-service/test/src/commands/call.ts +++ b/garden-service/test/src/commands/call.ts @@ -6,7 +6,7 @@ import { PluginFactory } from "../../../src/types/plugin/plugin" import { GetServiceStatusParams } from "../../../src/types/plugin/params" import { ServiceStatus } from "../../../src/types/service" import nock = require("nock") -import { validateTestModule } from "../../helpers" +import { configureTestModule } from "../../helpers" const testProvider: PluginFactory = () => { const testStatuses: { [key: string]: ServiceStatus } = { @@ -39,7 +39,7 @@ const testProvider: PluginFactory = () => { return { moduleActions: { - test: { validate: validateTestModule, getServiceStatus }, + test: { configure: configureTestModule, getServiceStatus }, }, } } diff --git a/garden-service/test/src/commands/delete.ts b/garden-service/test/src/commands/delete.ts index 9eab970c1a..ef6224a6e2 100644 --- a/garden-service/test/src/commands/delete.ts +++ b/garden-service/test/src/commands/delete.ts @@ -6,7 +6,7 @@ import { import { Garden } from "../../../src/garden" import { EnvironmentStatus } from "../../../src/types/plugin/outputs" import { PluginFactory } from "../../../src/types/plugin/plugin" -import { expectError, makeTestGardenA, getDataDir, validateTestModule } from "../../helpers" +import { expectError, makeTestGardenA, getDataDir, configureTestModule } from "../../helpers" import { expect } from "chai" import { ServiceStatus } from "../../../src/types/service" import { DeleteServiceParams } from "../../../src/types/plugin/params" @@ -99,7 +99,7 @@ describe("DeleteServiceCommand", () => { return { moduleActions: { test: { - validate: validateTestModule, + configure: configureTestModule, deleteService, }, }, diff --git a/garden-service/test/src/commands/deploy.ts b/garden-service/test/src/commands/deploy.ts index 74303b1087..504a99a3f7 100644 --- a/garden-service/test/src/commands/deploy.ts +++ b/garden-service/test/src/commands/deploy.ts @@ -3,16 +3,14 @@ import { Garden } from "../../../src/garden" import { DeployCommand } from "../../../src/commands/deploy" import { expect } from "chai" import { buildExecModule } from "../../../src/plugins/exec" -import { - PluginFactory, -} from "../../../src/types/plugin/plugin" +import { PluginFactory } from "../../../src/types/plugin/plugin" import { DeployServiceParams, GetServiceStatusParams, RunTaskParams, } from "../../../src/types/plugin/params" import { ServiceState, ServiceStatus } from "../../../src/types/service" -import { taskResultOutputs, validateTestModule } from "../../helpers" +import { taskResultOutputs, configureTestModule } from "../../helpers" import { RunTaskResult } from "../../../src/types/plugin/outputs" const placeholderTimestamp = new Date() @@ -73,7 +71,7 @@ const testProvider: PluginFactory = () => { return { moduleActions: { test: { - validate: validateTestModule, + configure: configureTestModule, build: buildExecModule, deployService, getServiceStatus, diff --git a/garden-service/test/src/commands/publish.ts b/garden-service/test/src/commands/publish.ts index 50807f3a93..46e5de4003 100644 --- a/garden-service/test/src/commands/publish.ts +++ b/garden-service/test/src/commands/publish.ts @@ -6,7 +6,7 @@ import * as td from "testdouble" import { Garden } from "../../../src/garden" import { PluginFactory } from "../../../src/types/plugin/plugin" import { PublishCommand } from "../../../src/commands/publish" -import { makeTestGardenA, validateTestModule } from "../../helpers" +import { makeTestGardenA, configureTestModule } from "../../helpers" import { expectError, taskResultOutputs } from "../../helpers" import { ModuleVersion } from "../../../src/vcs/base" import { LogEntry } from "../../../src/logger/log-entry" @@ -29,7 +29,7 @@ const testProvider: PluginFactory = () => { return { moduleActions: { test: { - validate: validateTestModule, + configure: configureTestModule, getBuildStatus, build, publishModule, diff --git a/garden-service/test/src/plugins/container.ts b/garden-service/test/src/plugins/container.ts index 5ff7d5587f..27664f19f8 100644 --- a/garden-service/test/src/plugins/container.ts +++ b/garden-service/test/src/plugins/container.ts @@ -23,7 +23,7 @@ describe("plugins.container", () => { const relDockerfilePath = "docker-dir/Dockerfile" const handler = gardenPlugin() - const validate = handler.moduleActions!.container!.validate! + const configure = handler.moduleActions!.container!.configure! const build = handler.moduleActions!.container!.build! const publishModule = handler.moduleActions!.container!.publishModule! const getBuildStatus = handler.moduleActions!.container!.getBuildStatus! @@ -69,7 +69,7 @@ describe("plugins.container", () => { }) async function getTestModule(moduleConfig: ContainerModuleConfig) { - const parsed = await validate({ ctx, moduleConfig }) + const parsed = await configure({ ctx, moduleConfig }) return moduleFromConfig(garden, parsed) } @@ -241,7 +241,7 @@ describe("plugins.container", () => { testConfigs: [], } - const result = await validate({ ctx, moduleConfig }) + const result = await configure({ ctx, moduleConfig }) expect(result).to.eql({ allowPublish: false, @@ -349,7 +349,7 @@ describe("plugins.container", () => { moduleConfig.spec.dockerfile = "path/to/non-existing/Dockerfile" await expectError( - () => validate({ ctx, moduleConfig }), + () => configure({ ctx, moduleConfig }), "configuration", ) }) @@ -404,7 +404,7 @@ describe("plugins.container", () => { } await expectError( - () => validate({ ctx, moduleConfig }), + () => configure({ ctx, moduleConfig }), "configuration", ) }) @@ -454,7 +454,7 @@ describe("plugins.container", () => { } await expectError( - () => validate({ ctx, moduleConfig }), + () => configure({ ctx, moduleConfig }), "configuration", ) }) @@ -501,7 +501,7 @@ describe("plugins.container", () => { } await expectError( - () => validate({ ctx, moduleConfig }), + () => configure({ ctx, moduleConfig }), "configuration", ) }) diff --git a/garden-service/test/src/plugins/kubernetes/container/ingress.ts b/garden-service/test/src/plugins/kubernetes/container/ingress.ts index 546f49f317..c8e96bad64 100644 --- a/garden-service/test/src/plugins/kubernetes/container/ingress.ts +++ b/garden-service/test/src/plugins/kubernetes/container/ingress.ts @@ -290,7 +290,7 @@ const wildcardDomainCertSecret = { describe("createIngresses", () => { const projectRoot = resolve(dataDir, "test-project-container") const handler = gardenPlugin() - const validate = handler.moduleActions!.container!.validate! + const configure = handler.moduleActions!.container!.configure! let garden: Garden @@ -354,7 +354,7 @@ describe("createIngresses", () => { } const ctx = await garden.getPluginContext("container") - const parsed = await validate({ ctx, moduleConfig }) + const parsed = await configure({ ctx, moduleConfig }) const module = await moduleFromConfig(garden, parsed) return {