diff --git a/garden-service/src/actions.ts b/garden-service/src/actions.ts index 19d1f09264..1b12f7fb6c 100644 --- a/garden-service/src/actions.ts +++ b/garden-service/src/actions.ts @@ -251,9 +251,7 @@ export class ActionHelper implements TypeGuard { async hotReload(params: ModuleActionHelperParams>) : Promise { - return this.garden.hotReload(params.module.name, async () => { - return this.callModuleHandler(({ params, actionType: "hotReload" })) - }) + return this.callModuleHandler(({ params, actionType: "hotReload" })) } async testModule(params: ModuleActionHelperParams>): Promise { diff --git a/garden-service/src/commands/deploy.ts b/garden-service/src/commands/deploy.ts index bd7e507e89..dcaa7fdbe2 100644 --- a/garden-service/src/commands/deploy.ts +++ b/garden-service/src/commands/deploy.ts @@ -17,11 +17,13 @@ import { handleTaskResults, StringsParameter, } from "./base" -import { hotReloadAndLog, validateHotReloadOpt } from "./helpers" +import { validateHotReloadOpt } from "./helpers" import { getDependantTasksForModule, getHotReloadModuleNames } from "../tasks/helpers" import { TaskResults } from "../task-graph" import { processServices } from "../process" import { logHeader } from "../logger/util" +import { HotReloadTask } from "../tasks/hot-reload" +import { BaseTask } from "../tasks/base" const deployArgs = { services: new StringsParameter({ @@ -118,13 +120,14 @@ export class DeployCommand extends Command { forceBuild: opts["force-build"], }), changeHandler: async (module) => { - if (hotReloadModuleNames.has(module.name)) { - await hotReloadAndLog(garden, log, module) - } - return getDependantTasksForModule({ + const tasks: BaseTask[] = await getDependantTasksForModule({ garden, log, module, hotReloadServiceNames, force: true, forceBuild: opts["force-build"], fromWatch: true, includeDependants: true, }) + if (hotReloadModuleNames.has(module.name)) { + tasks.push(new HotReloadTask({ garden, log, module, force: true })) + } + return tasks }, }) diff --git a/garden-service/src/commands/dev.ts b/garden-service/src/commands/dev.ts index b876edd813..9757642574 100644 --- a/garden-service/src/commands/dev.ts +++ b/garden-service/src/commands/dev.ts @@ -16,7 +16,7 @@ import moment = require("moment") import { join } from "path" import { BaseTask } from "../tasks/base" -import { hotReloadAndLog, validateHotReloadOpt } from "./helpers" +import { validateHotReloadOpt } from "./helpers" import { getDependantTasksForModule, getHotReloadModuleNames } from "../tasks/helpers" import { Command, @@ -29,6 +29,7 @@ import { STATIC_DIR } from "../constants" import { processModules } from "../process" import { Module } from "../types/module" import { getTestTasks } from "../tasks/test" +import { HotReloadTask } from "../tasks/hot-reload" const ansiBannerPath = join(STATIC_DIR, "garden-banner-2.txt") @@ -96,21 +97,23 @@ export class DevCommand extends Command { const tasksForModule = (watch: boolean) => { return async (module: Module) => { + const tasks: BaseTask[] = [] const hotReload = hotReloadModuleNames.has(module.name) if (watch && hotReload) { - await hotReloadAndLog(garden, log, module) + tasks.push(new HotReloadTask({ garden, log, module, force: true })) } const testModules: Module[] = watch ? (await dependencyGraph.withDependantModules([module])) : [module] - const testTasks: BaseTask[] = flatten(await Bluebird.map( - testModules, m => getTestTasks({ garden, log, module: m }))) + tasks.push(...flatten( + await Bluebird.map(testModules, m => getTestTasks({ garden, log, module: m })), + )) - return testTasks.concat(await getDependantTasksForModule({ + tasks.push(...await getDependantTasksForModule({ garden, log, module, @@ -120,6 +123,8 @@ export class DevCommand extends Command { forceBuild: false, includeDependants: watch, })) + + return tasks } } diff --git a/garden-service/src/commands/helpers.ts b/garden-service/src/commands/helpers.ts index b703dc8108..002b921f11 100644 --- a/garden-service/src/commands/helpers.ts +++ b/garden-service/src/commands/helpers.ts @@ -6,12 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import chalk from "chalk" import dedent = require("dedent") -import { uniq, flatten } from "lodash" import { Garden } from "../garden" -import { Module } from "../types/module" -import { prepareRuntimeContext, Service } from "../types/service" +import { Service } from "../types/service" import { LogEntry } from "../logger/log-entry" // Returns true if validation succeeded, false otherwise. @@ -47,30 +44,3 @@ export async function validateHotReloadOpt( } } - -export async function hotReloadAndLog(garden: Garden, log: LogEntry, module: Module) { - const logEntry = log.info({ - section: module.name, - msg: "Hot reloading...", - status: "active", - }) - - const serviceDependencyNames = uniq(flatten(module.services.map(s => s.config.dependencies))) - const runtimeContext = await prepareRuntimeContext( - garden, logEntry, module, await garden.getServices(serviceDependencyNames), - ) - - try { - await garden.actions.hotReload({ log: logEntry, module, runtimeContext }) - } catch (err) { - log.setError() - throw err - } - - const msec = logEntry.getDuration(5) * 1000 - logEntry.setSuccess({ - msg: chalk.green(`Done (took ${msec} ms)`), - append: true, - }) - -} diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index cf1478a89a..f03b2dbe3b 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -67,7 +67,6 @@ import { import { VcsHandler, ModuleVersion } from "./vcs/base" import { GitHandler } from "./vcs/git" import { BuildDir } from "./build-dir" -import { HotReloadHandler, HotReloadScheduler } from "./hotReloadScheduler" import { DependencyGraph } from "./dependency-graph" import { TaskGraph, @@ -154,7 +153,6 @@ export class Garden { private readonly registeredPlugins: { [key: string]: PluginFactory } private readonly serviceNameIndex: { [key: string]: string } // service name -> module name private readonly taskNameIndex: { [key: string]: string } // task name -> module name - private readonly hotReloadScheduler: HotReloadScheduler private readonly taskGraph: TaskGraph private readonly watcher: Watcher @@ -211,7 +209,6 @@ export class Garden { this.taskGraph = new TaskGraph(this, this.log) this.actions = new ActionHelper(this) - this.hotReloadScheduler = new HotReloadScheduler() this.events = new EventBus() this.watcher = new Watcher(this, this.log) } @@ -349,10 +346,6 @@ export class Garden { return this.taskGraph.processTasks() } - async hotReload(moduleName: string, hotReloadHandler: HotReloadHandler) { - return this.hotReloadScheduler.requestHotReload(moduleName, hotReloadHandler) - } - /** * Enables the file watcher for the project. * Make sure to stop it using `.close()` when cleaning up or when watching is no longer needed. diff --git a/garden-service/src/hotReloadScheduler.ts b/garden-service/src/hotReloadScheduler.ts deleted file mode 100644 index 52693c0b68..0000000000 --- a/garden-service/src/hotReloadScheduler.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2018 Garden Technologies, Inc. - * - * 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/. - */ - -export type HotReloadHandler = () => Promise - -/** - * Serializes and de-duplicates hot reload requests per module to prevent race conditions and avoid - * unnecessary hot reloads. - * - * E.g. if two hot reload requests are submitted for a given module while it is being hot-reloaded, - * exactly one hot reload will be run after the currently executing hot reload completes. - * - * Note: All hot reload requests for a given moduleName are considered logically equivalent - * by this implementation. - */ -export class HotReloadScheduler { - private pending: { [moduleName: string]: HotReloadRequest } - - constructor() { - this.pending = {} - } - - requestHotReload(moduleName: string, hotReloadHandler: HotReloadHandler): Promise { - - const pendingRequest = this.pending[moduleName] - - const prom = new Promise((resolve, reject) => { - if (pendingRequest) { - pendingRequest.addPromiseCallbacks(resolve, reject) - } else { - this.pending[moduleName] = new HotReloadRequest(hotReloadHandler, resolve, reject) - } - }) - - /** - * We disable the no-floating-promises tslint rule when calling this.process, since we are, in fact, - * properly handling these promises, though the control flow here is somewhat unusual (by design). - */ - - // tslint:disable-next-line:no-floating-promises - this.process(moduleName) - - return prom - - } - - async process(moduleName: string) { - const request = this.pending[moduleName] - - if (!request) { - return - } - - delete this.pending[moduleName] - await request.process() - - // tslint:disable-next-line:no-floating-promises - this.process(moduleName) - } - -} - -type PromiseCallback = (any) => any - -class HotReloadRequest { - private handler: HotReloadHandler - private promiseCallbacks: { - resolve: PromiseCallback - reject: PromiseCallback, - }[] - - constructor(handler: HotReloadHandler, resolve: PromiseCallback, reject: PromiseCallback) { - this.handler = handler - this.promiseCallbacks = [{ resolve, reject }] - } - - addPromiseCallbacks(resolve: () => any, reject: () => any) { - this.promiseCallbacks.push({ resolve, reject }) - } - - async process() { - try { - const result = await this.handler() - for (const { resolve } of this.promiseCallbacks) { - resolve(result) - } - } catch (error) { - for (const { reject } of this.promiseCallbacks) { - reject(error) - } - } finally { - this.promiseCallbacks = [] - } - } - -} diff --git a/garden-service/src/plugins/kubernetes/actions.ts b/garden-service/src/plugins/kubernetes/actions.ts index 2636d82198..1726a17a1b 100644 --- a/garden-service/src/plugins/kubernetes/actions.ts +++ b/garden-service/src/plugins/kubernetes/actions.ts @@ -6,17 +6,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Bluebird from "bluebird" -import * as execa from "execa" import { includes } from "lodash" - import { DeploymentError, ConfigurationError } from "../../exceptions" -import { HotReloadResult, RunResult, TestResult } from "../../types/plugin/outputs" +import { RunResult, TestResult } from "../../types/plugin/outputs" import { ExecInServiceParams, GetServiceOutputsParams, GetTestResultParams, - HotReloadParams, RunModuleParams, TestModuleParams, DeleteServiceParams, @@ -30,18 +26,10 @@ import { KubeApi } from "./api" import { getAppNamespace, getMetadataNamespace } from "./namespace" import { kubectl } from "./kubectl" import { DEFAULT_TEST_TIMEOUT } from "../../constants" -import { - getContainerServiceStatus, - deleteContainerService, - rsyncSourcePath, - rsyncTargetPath, -} from "./deployment" +import { getContainerServiceStatus, deleteContainerService } from "./deployment" import { KubernetesProvider } from "./kubernetes" -import { getIngresses } from "./ingress" -import { rsyncPortName } from "./service" import { ServiceStatus } from "../../types/service" import { ValidateModuleParams } from "../../types/plugin/params" -import { waitForServices } from "./status" export async function validate(params: ValidateModuleParams) { const config = await validateContainerModule(params) @@ -137,41 +125,6 @@ export async function execInService(params: ExecInServiceParams return { code: res.code, output: res.output } } -export async function hotReload( - { ctx, log, runtimeContext, module, buildDependencies }: HotReloadParams, -): Promise { - const hotReloadConfig = module.spec.hotReload! - - const services = module.services - - if (!await waitForServices(ctx, log, runtimeContext, services, buildDependencies)) { - // Service deployment timed out, skip hot reload - return {} - } - - const api = new KubeApi(ctx.provider) - - const namespace = await getAppNamespace(ctx, ctx.provider) - - await Bluebird.map(services, async (service) => { - - const hostname = (await getIngresses(service, api))[0].hostname - - const rsyncNodePort = (await api.core - .readNamespacedService(service.name + "-nodeport", namespace)) - .body.spec.ports.find(p => p.name === rsyncPortName(service.name))! - .nodePort - - await Bluebird.map(hotReloadConfig.sync, async ({ source, target }) => { - const src = rsyncSourcePath(module.path, source) - const destination = `rsync://${hostname}:${rsyncNodePort}/volume/${rsyncTargetPath(target)}` - await execa("rsync", ["-vrptgo", src, destination]) - }) - }) - - return {} -} - export async function runModule( { ctx, module, command, ignoreError = true, interactive, runtimeContext, timeout, diff --git a/garden-service/src/plugins/kubernetes/deployment.ts b/garden-service/src/plugins/kubernetes/deployment.ts index 8118ca3a77..32bbc39382 100644 --- a/garden-service/src/plugins/kubernetes/deployment.ts +++ b/garden-service/src/plugins/kubernetes/deployment.ts @@ -19,7 +19,8 @@ import { DeployServiceParams, GetServiceStatusParams, PushModuleParams } from ". import { RuntimeContext, Service, ServiceStatus } from "../../types/service" import { helpers, ContainerModule, ContainerService, SyncSpec } from "../container" import { createIngresses, getIngresses } from "./ingress" -import { createServices, RSYNC_PORT, RSYNC_PORT_NAME } from "./service" +import { RSYNC_PORT, RSYNC_PORT_NAME } from "./hot-reload" +import { createServices } from "./service" import { waitForObjects, compareDeployedObjects } from "./status" import { applyMany, deleteObjectsByLabel } from "./kubectl" import { getAppNamespace } from "./namespace" @@ -113,7 +114,7 @@ export async function createContainerObjects( const api = new KubeApi(ctx.provider) const ingresses = await createIngresses(api, namespace, service) const deployment = await createDeployment(ctx.provider, service, runtimeContext, namespace, enableHotReload) - const kubeservices = await createServices(service, namespace, enableHotReload) + const kubeservices = await createServices(service, namespace) const objects = [deployment, ...kubeservices, ...ingresses] @@ -446,6 +447,11 @@ function configureHotReload(deployment, container, serviceSpec, moduleSpec, env, name: "garden-rsync", image: "eugenmayer/rsync", imagePullPolicy: "IfNotPresent", + env: [ + // This makes sure the server is accessible on any IP address, because CIDRs can be different across clusters. + // K8s can be trusted to secure the port. - JE + { name: "ALLOW", value: "0.0.0.0/0" }, + ], volumeMounts: [{ name: syncVolumeName, /** diff --git a/garden-service/src/plugins/kubernetes/hot-reload.ts b/garden-service/src/plugins/kubernetes/hot-reload.ts new file mode 100644 index 0000000000..b2dde63c4a --- /dev/null +++ b/garden-service/src/plugins/kubernetes/hot-reload.ts @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * 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 * as Bluebird from "bluebird" +import * as execa from "execa" +import { waitForServices } from "./status" +import { HotReloadParams } from "../../types/plugin/params" +import { ContainerModule } from "../container" +import { HotReloadResult } from "../../types/plugin/outputs" +import { getAppNamespace } from "./namespace" +import { rsyncSourcePath, rsyncTargetPath } from "./deployment" +import { kubectl } from "./kubectl" +import getPort = require("get-port") +import { RuntimeError } from "../../exceptions" + +export const RSYNC_PORT = 873 +export const RSYNC_PORT_NAME = "garden-rsync" + +export async function hotReload( + { ctx, log, runtimeContext, module, buildDependencies }: HotReloadParams, +): Promise { + const hotReloadConfig = module.spec.hotReload! + const services = module.services + + await waitForServices(ctx, log, runtimeContext, services, buildDependencies) + + const namespace = await getAppNamespace(ctx, ctx.provider) + + const procs = await Bluebird.map(services, async (service) => { + // Forward random free local port to the remote rsync container. + const rsyncLocalPort = await getPort() + + const targetDeployment = `deployment/${service.name}` + const portMapping = `${rsyncLocalPort}:${RSYNC_PORT}` + + log.debug( + `Forwarding local port ${rsyncLocalPort} to service ${service.name} sync container port ${RSYNC_PORT}`, + ) + + // TODO: use the API directly instead of kubectl (need to reverse engineer kubectl a bit to get how that works) + const proc = kubectl(ctx.provider.config.context, namespace) + .spawn(["port-forward", targetDeployment, portMapping]) + + return { service, proc, rsyncLocalPort } + // Need to do one port at a time to avoid conflicting local ports + }, { concurrency: 1 }) + + await Bluebird.map(procs, ({ service, proc, rsyncLocalPort }) => { + return new Promise((resolve, reject) => { + proc.on("error", (error) => { + reject(new RuntimeError(`Unexpected error while synchronising to service ${service.name}: ${error.message}`, { + error, + serviceName: service.name, + })) + }) + + proc.stdout.on("data", (line) => { + // This is the best indication that we have that the connection is up... + if (line.toString().includes("Forwarding from ")) { + Bluebird.map(hotReloadConfig.sync, ({ source, target }) => { + const src = rsyncSourcePath(module.path, source) + const destination = `rsync://localhost:${rsyncLocalPort}/volume/${rsyncTargetPath(target)}` + return execa("rsync", ["-vrptgo", src, destination]) + }) + .then(resolve) + .catch(reject) + .finally(() => !proc.killed && proc.kill()) + } + }) + }) + }) + + return {} +} diff --git a/garden-service/src/plugins/kubernetes/kubernetes.ts b/garden-service/src/plugins/kubernetes/kubernetes.ts index 634036a1a6..661ec2355b 100644 --- a/garden-service/src/plugins/kubernetes/kubernetes.ts +++ b/garden-service/src/plugins/kubernetes/kubernetes.ts @@ -9,11 +9,7 @@ import * as Joi from "joi" import dedent = require("dedent") -import { - joiArray, - joiIdentifier, - validate, -} from "../../config/common" +import { joiArray, joiIdentifier, validate } from "../../config/common" import { GardenPlugin } from "../../types/plugin/plugin" import { Provider, providerConfigBaseSchema, ProviderConfig } from "../../config/project" import { getExecTaskStatus } from "../exec" @@ -22,7 +18,6 @@ import { execInService, getServiceOutputs, getTestResult, - hotReload, testModule, runModule, runService, @@ -34,6 +29,7 @@ import { getSecret, setSecret, deleteSecret } from "./secrets" import { containerRegistryConfigSchema, ContainerRegistryConfig } from "../container" import { getRemoteEnvironmentStatus, prepareRemoteEnvironment, cleanupEnvironment } from "./init" import { getServiceLogs } from "./logs" +import { hotReload } from "./hot-reload" export const name = "kubernetes" diff --git a/garden-service/src/plugins/kubernetes/service.ts b/garden-service/src/plugins/kubernetes/service.ts index 45b65d1533..1a8b52234d 100644 --- a/garden-service/src/plugins/kubernetes/service.ts +++ b/garden-service/src/plugins/kubernetes/service.ts @@ -8,10 +8,7 @@ import { ContainerService } from "../container" -export const RSYNC_PORT = 873 -export const RSYNC_PORT_NAME = "garden-rsync" - -export async function createServices(service: ContainerService, namespace: string, enableHotReload: boolean) { +export async function createServices(service: ContainerService, namespace: string) { const services: any = [] const addService = (name: string, type: string, servicePorts: any[]) => { @@ -34,29 +31,23 @@ export async function createServices(service: ContainerService, namespace: strin } // first add internally exposed (ClusterIP) service - const internalPorts: any = [] const ports = service.spec.ports - for (const portSpec of ports) { - internalPorts.push({ + if (ports.length) { + addService(service.name, "ClusterIP", ports.map(portSpec => ({ name: portSpec.name, protocol: portSpec.protocol, targetPort: portSpec.containerPort, port: portSpec.servicePort, - }) - } - - if (internalPorts.length) { - addService(service.name, "ClusterIP", internalPorts) + }))) } // optionally add a NodePort service for externally open ports, if applicable // TODO: explore nicer ways to do this - const exposedPorts: any[] = ports.filter(portSpec => portSpec.nodePort) - - if (exposedPorts.length > 0 || enableHotReload) { + const exposedPorts = ports.filter(portSpec => portSpec.nodePort) - const nodePorts: any[] = exposedPorts.map(portSpec => ({ + if (exposedPorts.length > 0) { + const nodePorts = exposedPorts.map(portSpec => ({ // TODO: do the parsing and defaults when loading the yaml name: portSpec.name, protocol: portSpec.protocol, @@ -64,14 +55,6 @@ export async function createServices(service: ContainerService, namespace: strin nodePort: portSpec.nodePort, })) - if (enableHotReload) { - nodePorts.push({ - name: rsyncPortName(service.name), - protocol: "TCP", - port: RSYNC_PORT, - }) - } - addService(service.name + "-nodeport", "NodePort", nodePorts) } diff --git a/garden-service/src/plugins/kubernetes/status.ts b/garden-service/src/plugins/kubernetes/status.ts index c92ed51fc8..7eb529347d 100644 --- a/garden-service/src/plugins/kubernetes/status.ts +++ b/garden-service/src/plugins/kubernetes/status.ts @@ -411,25 +411,30 @@ export async function waitForObjects({ ctx, provider, service, objects, log }: W */ export async function waitForServices( ctx: PluginContext, log: LogEntry, runtimeContext: RuntimeContext, services: Service[], buildDependencies, -): Promise { - let ready +) { const startTime = new Date().getTime() while (true) { + const states = await Bluebird.map(services, async (service) => { + return { + service, + ...await getContainerServiceStatus({ + ctx, log, buildDependencies, service, runtimeContext, module: service.module, + }), + } + }) - ready = (await Bluebird.map(services, async (service) => { - const state = (await getContainerServiceStatus({ - ctx, log, buildDependencies, service, runtimeContext, module: service.module, - })).state - return state === "ready" || state === "outdated" - })).every(serviceReady => serviceReady) + const notReady = states.filter(s => s.state !== "ready" && s.state !== "outdated") - if (ready) { - return true + if (notReady.length === 0) { + return } + const notReadyNames = notReady.map(s => s.service.name).join(", ") + log.silly(`Waiting for services ${notReadyNames}`) + if (new Date().getTime() - startTime > KUBECTL_DEFAULT_TIMEOUT * 1000) { - return false + throw new DeploymentError(`Timed out waiting for services ${notReadyNames} to deploy`, { states }) } await sleep(2000) diff --git a/garden-service/src/tasks/helpers.ts b/garden-service/src/tasks/helpers.ts index 6ff4dab82e..323ab0cde3 100644 --- a/garden-service/src/tasks/helpers.ts +++ b/garden-service/src/tasks/helpers.ts @@ -14,6 +14,7 @@ import { Module } from "../types/module" import { Service } from "../types/service" import { DependencyGraphNode } from "../dependency-graph" import { LogEntry } from "../logger/log-entry" +import { BaseTask } from "./base" export async function getDependantTasksForModule( { garden, log, module, hotReloadServiceNames, force = false, forceBuild = false, @@ -22,7 +23,7 @@ export async function getDependantTasksForModule( garden: Garden, log: LogEntry, module: Module, hotReloadServiceNames: string[], force?: boolean, forceBuild?: boolean, fromWatch?: boolean, includeDependants?: boolean, }, -) { +): Promise { let buildTasks: BuildTask[] = [] let dependantBuildModules: Module[] = [] diff --git a/garden-service/src/tasks/hot-reload.ts b/garden-service/src/tasks/hot-reload.ts new file mode 100644 index 0000000000..1c7e53b632 --- /dev/null +++ b/garden-service/src/tasks/hot-reload.ts @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * 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 chalk from "chalk" +import { uniq, flatten } from "lodash" +import { LogEntry } from "../logger/log-entry" +import { BaseTask } from "./base" +import { prepareRuntimeContext } from "../types/service" +import { Garden } from "../garden" +import { DependencyGraphNodeType } from "../dependency-graph" +import { Module } from "../types/module" + +export interface DeployTaskParams { + garden: Garden + force: boolean + module: Module + log: LogEntry +} + +export class HotReloadTask extends BaseTask { + type = "hot-reload" + depType: DependencyGraphNodeType = "service" + + private module: Module + + constructor( + { garden, log, module, force }: DeployTaskParams, + ) { + super({ garden, log, force, version: module.version }) + this.module = module + } + + protected getName() { + return this.module.name + } + + getDescription() { + return `hot-reloading module ${this.module.name}` + } + + async process(): Promise<{}> { + const module = this.module + const log = this.log.info({ + section: module.name, + msg: "Hot reloading...", + status: "active", + }) + + const serviceDependencyNames = uniq(flatten(module.services.map(s => s.config.dependencies))) + const runtimeContext = await prepareRuntimeContext( + this.garden, log, module, await this.garden.getServices(serviceDependencyNames), + ) + + try { + await this.garden.actions.hotReload({ log, module, runtimeContext }) + } catch (err) { + log.setError() + throw err + } + + const msec = log.getDuration(5) * 1000 + log.setSuccess({ + msg: chalk.green(`Done (took ${msec} ms)`), + append: true, + }) + + return {} + } +}