diff --git a/core/src/plugins/kubernetes/config.ts b/core/src/plugins/kubernetes/config.ts index 272df2f1ca..3545e5778d 100644 --- a/core/src/plugins/kubernetes/config.ts +++ b/core/src/plugins/kubernetes/config.ts @@ -22,7 +22,7 @@ import { import { PluginContext } from "../../plugin-context" import { deline } from "../../util/string" import { defaultSystemNamespace } from "./system" -import { hotReloadableKinds, HotReloadableKind } from "./hot-reload" +import { hotReloadableKinds, HotReloadableKind } from "./hot-reload/hot-reload" import { baseTaskSpecSchema, BaseTaskSpec, cacheResultSchema } from "../../config/task" import { baseTestSpecSchema, BaseTestSpec } from "../../config/test" import { ArtifactSpec } from "../../config/validation" @@ -620,6 +620,26 @@ export const serviceResourceSchema = () => ), }) +export const containerModuleSchema = () => + joiIdentifier() + .description( + deline`The Garden module that contains the sources for the container. This needs to be specified under + \`serviceResource\` in order to enable hot-reloading, but is not necessary for tasks and tests. + + Must be a \`container\` module, and for hot-reloading to work you must specify the \`hotReload\` field + on the container module. + + Note: If you specify a module here, you don't need to specify it additionally under \`build.dependencies\`` + ) + .example("my-container-module") + +export const hotReloadArgsSchema = () => + joi + .array() + .items(joi.string()) + .description("If specified, overrides the arguments for the main container when running in hot-reload mode.") + .example(["nodemon", "my-server.js"]) + export const kubernetesTaskSchema = () => baseTaskSpecSchema() .keys({ diff --git a/core/src/plugins/kubernetes/container/deployment.ts b/core/src/plugins/kubernetes/container/deployment.ts index d742853e59..a1d2ddb595 100644 --- a/core/src/plugins/kubernetes/container/deployment.ts +++ b/core/src/plugins/kubernetes/container/deployment.ts @@ -19,7 +19,6 @@ import { getAppNamespace } from "../namespace" import { PluginContext } from "../../../plugin-context" import { KubeApi } from "../api" import { KubernetesProvider, KubernetesPluginContext } from "../config" -import { configureHotReload } from "../hot-reload" import { KubernetesWorkload, KubernetesResource } from "../types" import { ConfigurationError } from "../../../exceptions" import { getContainerServiceStatus, ContainerServiceStatus } from "./status" @@ -33,6 +32,7 @@ import { RuntimeContext } from "../../../runtime-context" import { resolve } from "path" import { killPortForwards } from "../port-forward" import { prepareImagePullSecrets } from "../secrets" +import { configureHotReload } from "../hot-reload/helpers" export const DEFAULT_CPU_REQUEST = "10m" export const DEFAULT_MEMORY_REQUEST = "64Mi" diff --git a/core/src/plugins/kubernetes/container/handlers.ts b/core/src/plugins/kubernetes/container/handlers.ts index 5587fce585..cfed0bf4f8 100644 --- a/core/src/plugins/kubernetes/container/handlers.ts +++ b/core/src/plugins/kubernetes/container/handlers.ts @@ -7,7 +7,7 @@ */ import { deployContainerService, deleteService } from "./deployment" -import { hotReloadContainer } from "../hot-reload" +import { hotReloadContainer } from "../hot-reload/hot-reload" import { getServiceLogs } from "./logs" import { runContainerModule, runContainerService, runContainerTask } from "./run" import { execInService } from "./exec" diff --git a/core/src/plugins/kubernetes/helm/config.ts b/core/src/plugins/kubernetes/helm/config.ts index 3775a69660..79e9498f73 100644 --- a/core/src/plugins/kubernetes/helm/config.ts +++ b/core/src/plugins/kubernetes/helm/config.ts @@ -31,6 +31,8 @@ import { KubernetesTestSpec, KubernetesTaskSpec, namespaceSchema, + containerModuleSchema, + hotReloadArgsSchema, } from "../config" import { posix } from "path" @@ -88,22 +90,8 @@ const helmServiceResourceSchema = () => directly from the template in question in order to match it. Note that you may need to add single quotes around the string for the YAML to be parsed correctly.` ), - containerModule: joiIdentifier() - .description( - deline`The Garden module that contains the sources for the container. This needs to be specified under - \`serviceResource\` in order to enable hot-reloading for the chart, but is not necessary for tasks and tests. - - Must be a \`container\` module, and for hot-reloading to work you must specify the \`hotReload\` field - on the container module. - - Note: If you specify a module here, you don't need to specify it additionally under \`build.dependencies\`` - ) - .example("my-container-module"), - hotReloadArgs: joi - .array() - .items(joi.string()) - .description("If specified, overrides the arguments for the main container when running in hot-reload mode.") - .example(["nodemon", "my-server.js"]), + containerModule: containerModuleSchema(), + hotReloadArgs: hotReloadArgsSchema(), }) const helmTaskSchema = () => diff --git a/core/src/plugins/kubernetes/helm/deployment.ts b/core/src/plugins/kubernetes/helm/deployment.ts index 8e8b1d899b..dad5b51b57 100644 --- a/core/src/plugins/kubernetes/helm/deployment.ts +++ b/core/src/plugins/kubernetes/helm/deployment.ts @@ -11,16 +11,16 @@ import { helm } from "./helm-cli" import { HelmModule } from "./config" import { getChartPath, getReleaseName, getChartResources, getValueArgs, getBaseModule } from "./common" import { getReleaseStatus, HelmServiceStatus, getDeployedResources } from "./status" -import { configureHotReload, HotReloadableResource } from "../hot-reload" +import { HotReloadableResource } from "../hot-reload/hot-reload" import { apply, deleteResources } from "../kubectl" import { KubernetesPluginContext } from "../config" import { ContainerHotReloadSpec } from "../../container/config" -import { getHotReloadSpec, getHotReloadContainerName } from "./hot-reload" import { DeployServiceParams } from "../../../types/plugin/service/deployService" import { DeleteServiceParams } from "../../../types/plugin/service/deleteService" import { getForwardablePorts, killPortForwards } from "../port-forward" import { findServiceResource, getServiceResourceSpec } from "../util" import { getModuleNamespace } from "../namespace" +import { getHotReloadSpec, configureHotReload, getHotReloadContainerName } from "../hot-reload/helpers" export async function deployHelmService({ ctx, diff --git a/core/src/plugins/kubernetes/helm/handlers.ts b/core/src/plugins/kubernetes/helm/handlers.ts index e6e4068f9a..566f5b3432 100644 --- a/core/src/plugins/kubernetes/helm/handlers.ts +++ b/core/src/plugins/kubernetes/helm/handlers.ts @@ -13,7 +13,6 @@ import { getServiceStatus } from "./status" import { deployHelmService, deleteService } from "./deployment" import { getTestResult } from "../test-results" import { runHelmTask, runHelmModule } from "./run" -import { hotReloadHelmChart } from "./hot-reload" import { getServiceLogs } from "./logs" import { testHelmModule } from "./test" import { getPortForwardHandler } from "../port-forward" @@ -26,6 +25,7 @@ import { pathExists } from "fs-extra" import chalk = require("chalk") import { SuggestModulesParams, SuggestModulesResult } from "../../../types/plugin/module/suggestModules" import { getReleaseName } from "./common" +import { hotReloadK8s } from "../hot-reload/hot-reload" export const helmHandlers: Partial> = { build: buildHelmModule, @@ -57,7 +57,7 @@ export const helmHandlers: Partial> = getServiceStatus, getTaskResult, getTestResult, - hotReloadService: hotReloadHelmChart, + hotReloadService: hotReloadK8s, // TODO: add publishModule handler runModule: runHelmModule, runTask: runHelmTask, diff --git a/core/src/plugins/kubernetes/helm/hot-reload.ts b/core/src/plugins/kubernetes/helm/hot-reload.ts deleted file mode 100644 index f06c0a950d..0000000000 --- a/core/src/plugins/kubernetes/helm/hot-reload.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2018-2020 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 { HelmService, HelmModule } from "./config" -import { ConfigurationError } from "../../../exceptions" -import { deline } from "../../../util/string" -import { ContainerModule } from "../../container/config" -import { getChartResources, getBaseModule } from "./common" -import { findServiceResource, getServiceResourceSpec } from "../util" -import { syncToService } from "../hot-reload" -import { KubernetesPluginContext } from "../config" -import { HotReloadServiceParams, HotReloadServiceResult } from "../../../types/plugin/service/hotReloadService" -import { getModuleNamespace } from "../namespace" - -/** - * The hot reload action handler for Helm charts. - */ -export async function hotReloadHelmChart({ - ctx, - log, - module, - service, -}: HotReloadServiceParams): Promise { - const hotReloadSpec = getHotReloadSpec(service) - - const manifests = await getChartResources(ctx, service.module, true, log) - const baseModule = getBaseModule(module) - const resourceSpec = service.spec.serviceResource - - const workload = await findServiceResource({ - ctx, - log, - module, - baseModule, - manifests, - resourceSpec, - }) - - const k8sCtx = ctx as KubernetesPluginContext - const namespace = await getModuleNamespace({ - ctx: k8sCtx, - log, - module, - provider: k8sCtx.provider, - }) - - await syncToService({ - ctx: k8sCtx, - service, - hotReloadSpec, - workload, - log, - namespace, - }) - - return {} -} - -export function getHotReloadSpec(service: HelmService) { - const module = service.module - const baseModule = getBaseModule(module) - const resourceSpec = getServiceResourceSpec(module, baseModule) - - if (!resourceSpec || !resourceSpec.containerModule) { - throw new ConfigurationError( - `Module '${module.name}' must specify \`serviceResource.containerModule\` in order to enable hot-reloading.`, - { moduleName: module.name, resourceSpec } - ) - } - - if (service.sourceModule.type !== "container") { - throw new ConfigurationError( - deline` - Module '${resourceSpec.containerModule}', referenced on module '${module.name}' under - \`serviceResource.containerModule\`, is not a container module. - Please specify the appropriate container module that contains the sources for the resource.`, - { moduleName: module.name, sourceModuleType: service.sourceModule.type, resourceSpec } - ) - } - - // The sourceModule property is assigned in the Helm module validate action - const hotReloadSpec = service.sourceModule.spec.hotReload - - if (!hotReloadSpec) { - throw new ConfigurationError( - deline` - Module '${resourceSpec.containerModule}', referenced on module '${module.name}' under - \`serviceResource.containerModule\`, is not configured for hot-reloading. - Please specify \`hotReload\` on the '${resourceSpec.containerModule}' module in order to enable hot-reloading.`, - { moduleName: module.name, resourceSpec } - ) - } - - return hotReloadSpec -} - -/** - * Used to determine which container in the target resource to attach the hot reload sync volume to. - */ -export function getHotReloadContainerName(module: HelmModule) { - const baseModule = getBaseModule(module) - const resourceSpec = getServiceResourceSpec(module, baseModule) - return resourceSpec.containerName || module.name -} diff --git a/core/src/plugins/kubernetes/hot-reload.ts b/core/src/plugins/kubernetes/hot-reload/helpers.ts similarity index 75% rename from core/src/plugins/kubernetes/hot-reload.ts rename to core/src/plugins/kubernetes/hot-reload/helpers.ts index ee320c7cc8..ab2e952cf7 100644 --- a/core/src/plugins/kubernetes/hot-reload.ts +++ b/core/src/plugins/kubernetes/hot-reload/helpers.ts @@ -6,35 +6,29 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import Bluebird from "bluebird" -import normalizePath = require("normalize-path") -import { V1Deployment, V1DaemonSet, V1StatefulSet, V1Container } from "@kubernetes/client-node" -import { ContainerModule, ContainerHotReloadSpec } from "../container/config" -import { RuntimeError, ConfigurationError } from "../../exceptions" +import { V1Container } from "@kubernetes/client-node" +import { ContainerHotReloadSpec } from "../../container/config" +import { RuntimeError, ConfigurationError } from "../../../exceptions" import { resolve as resolvePath, dirname, posix } from "path" -import { deline, gardenAnnotationKey } from "../../util/string" -import { set, sortBy, flatten } from "lodash" -import { Service } from "../../types/service" -import { LogEntry } from "../../logger/log-entry" -import { getResourceContainer } from "./util" -import { execInWorkload } from "./container/exec" -import { getPortForward, killPortForward } from "./port-forward" -import { RSYNC_PORT } from "./constants" -import { getAppNamespace } from "./namespace" -import { KubernetesPluginContext } from "./config" -import { HotReloadServiceParams, HotReloadServiceResult } from "../../types/plugin/service/hotReloadService" -import { KubernetesResource, KubernetesWorkload } from "./types" -import { normalizeLocalRsyncPath, normalizeRelativePath } from "../../util/fs" -import { createWorkloadManifest } from "./container/deployment" -import { KubeApi } from "./api" -import { syncWithOptions } from "../../util/sync" -import { GardenModule } from "../../types/module" - -export type HotReloadableResource = KubernetesResource -export type HotReloadableKind = "Deployment" | "DaemonSet" | "StatefulSet" - -export const RSYNC_PORT_NAME = "garden-rsync" -export const hotReloadableKinds: HotReloadableKind[] = ["Deployment", "DaemonSet", "StatefulSet"] +import { deline, gardenAnnotationKey } from "../../../util/string" +import { set, flatten } from "lodash" +import { Service } from "../../../types/service" +import { LogEntry } from "../../../logger/log-entry" +import { getResourceContainer, getServiceResourceSpec } from "../util" +import { execInWorkload } from "../container/exec" +import { getPortForward, killPortForward } from "../port-forward" +import { RSYNC_PORT } from "../constants" +import { KubernetesPluginContext } from "../config" +import { KubernetesWorkload } from "../types" +import { normalizeLocalRsyncPath, normalizeRelativePath } from "../../../util/fs" +import { syncWithOptions } from "../../../util/sync" +import { GardenModule } from "../../../types/module" +import { getBaseModule } from "../helm/common" +import { HelmModule, HelmService } from "../helm/config" +import { KubernetesModule, KubernetesService } from "../kubernetes-module/config" +import { HotReloadableKind, HotReloadableResource, RSYNC_PORT_NAME } from "./hot-reload" +import Bluebird from "bluebird" +import normalizePath from "normalize-path" interface ConfigureHotReloadParams { target: HotReloadableResource @@ -169,73 +163,60 @@ export function configureHotReload({ target.spec.template.spec!.containers.push(rsyncContainer) } -/** - * The hot reload action handler for containers. - */ -export async function hotReloadContainer({ - ctx, - log, - service, - module, -}: HotReloadServiceParams): Promise { - const hotReloadSpec = module.spec.hotReload +export function getHotReloadSpec(service: KubernetesService | HelmService) { + const module = service.module - if (!hotReloadSpec) { + let baseModule: GardenModule | undefined = undefined + if (module.type === "helm") { + baseModule = getBaseModule(module) + } + + const resourceSpec = getServiceResourceSpec(module, baseModule) + + if (!resourceSpec || !resourceSpec.containerModule) { throw new ConfigurationError( - `Module ${module.name} must specify the \`hotReload\` key for service ${service.name} to be hot-reloadable.`, - { moduleName: module.name, serviceName: service.name } + `Module '${module.name}' must specify \`serviceResource.containerModule\` in order to enable hot-reloading.`, + { moduleName: module.name, resourceSpec } ) } - const k8sCtx = ctx as KubernetesPluginContext - const provider = k8sCtx.provider - const namespace = await getAppNamespace(k8sCtx, log, provider) - const api = await KubeApi.factory(log, ctx, provider) - - // Find the currently deployed workload by labels - const manifest = await createWorkloadManifest({ - api, - provider, - service, - runtimeContext: { envVars: {}, dependencies: [] }, - namespace, - enableHotReload: true, - production: k8sCtx.production, - log, - blueGreen: provider.config.deploymentStrategy === "blue-green", - }) - - const res = await api.listResources({ - log, - apiVersion: manifest.apiVersion, - kind: manifest.kind, - namespace, - labelSelector: { - [gardenAnnotationKey("service")]: service.name, - }, - }) + if (service.sourceModule.type !== "container") { + throw new ConfigurationError( + deline` + Module '${resourceSpec.containerModule}', referenced on module '${module.name}' under + \`serviceResource.containerModule\`, is not a container module. + Please specify the appropriate container module that contains the sources for the resource.`, + { moduleName: module.name, sourceModuleType: service.sourceModule.type, resourceSpec } + ) + } - const list = res.items.filter((r) => r.metadata.annotations![gardenAnnotationKey("hot-reload")] === "true") + // The sourceModule property is assigned in the Kubernetes module validate action + const hotReloadSpec = service.sourceModule.spec.hotReload - if (list.length === 0) { - throw new RuntimeError(`Unable to find deployed instance of service ${service.name} with hot-reloading enabled`, { - service, - listResult: res, - }) + if (!hotReloadSpec) { + throw new ConfigurationError( + deline` + Module '${resourceSpec.containerModule}', referenced on module '${module.name}' under + \`serviceResource.containerModule\`, is not configured for hot-reloading. + Please specify \`hotReload\` on the '${resourceSpec.containerModule}' module in order to enable hot-reloading.`, + { moduleName: module.name, resourceSpec } + ) } - const workload = sortBy(list, (r) => r.metadata.creationTimestamp)[list.length - 1] + return hotReloadSpec +} - await syncToService({ - log, - ctx: k8sCtx, - service, - workload, - hotReloadSpec, - namespace, - }) +/** + * Used to determine which container in the target resource to attach the hot reload sync volume to. + */ +export function getHotReloadContainerName(module: KubernetesModule | HelmModule) { + let baseModule: GardenModule | undefined = undefined + if (module.type === "helm") { + baseModule = getBaseModule(module) + } - return {} + const resourceSpec = getServiceResourceSpec(module, baseModule) + return resourceSpec.containerName || module.name } /** diff --git a/core/src/plugins/kubernetes/hot-reload/hot-reload.ts b/core/src/plugins/kubernetes/hot-reload/hot-reload.ts new file mode 100644 index 0000000000..fb8399957b --- /dev/null +++ b/core/src/plugins/kubernetes/hot-reload/hot-reload.ts @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2018-2020 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 { V1Deployment, V1DaemonSet, V1StatefulSet } from "@kubernetes/client-node" +import { ContainerModule } from "../../container/config" +import { RuntimeError, ConfigurationError } from "../../../exceptions" +import { gardenAnnotationKey } from "../../../util/string" +import { sortBy } from "lodash" +import { Service } from "../../../types/service" +import { LogEntry } from "../../../logger/log-entry" +import { findServiceResource } from "../util" +import { getAppNamespace, getModuleNamespace } from "../namespace" +import { KubernetesPluginContext } from "../config" +import { HotReloadServiceParams, HotReloadServiceResult } from "../../../types/plugin/service/hotReloadService" +import { BaseResource, KubernetesResource, KubernetesWorkload } from "../types" +import { createWorkloadManifest } from "../container/deployment" +import { KubeApi } from "../api" +import { GardenModule } from "../../../types/module" +import { PluginContext } from "../../../plugin-context" +import { getBaseModule, getChartResources } from "../helm/common" +import { HelmModule } from "../helm/config" +import { getManifests } from "../kubernetes-module/common" +import { KubernetesModule } from "../kubernetes-module/config" +import { getHotReloadSpec, syncToService } from "./helpers" + +export type HotReloadableResource = KubernetesResource +export type HotReloadableKind = "Deployment" | "DaemonSet" | "StatefulSet" + +export const RSYNC_PORT_NAME = "garden-rsync" +export const hotReloadableKinds: HotReloadableKind[] = ["Deployment", "DaemonSet", "StatefulSet"] + +/** + * The hot reload action handler for helm charts and kubernetes modules. + */ +export async function hotReloadK8s({ + ctx, + log, + module, + service, +}: { + ctx: PluginContext + service: Service + log: LogEntry + module: KubernetesModule | HelmModule +}): Promise { + const k8sCtx = ctx as KubernetesPluginContext + const namespace = await getModuleNamespace({ + ctx: k8sCtx, + log, + module, + provider: k8sCtx.provider, + }) + + let manifests: KubernetesResource[] + let baseModule: GardenModule | undefined = undefined + if (module.type === "helm") { + baseModule = getBaseModule(module) + manifests = await getChartResources(ctx, service.module, true, log) + } else { + const api = await KubeApi.factory(log, ctx, k8sCtx.provider) + manifests = await getManifests({ api, log, module: module, defaultNamespace: namespace }) + } + + const resourceSpec = service.spec.serviceResource + const hotReloadSpec = getHotReloadSpec(service) + + const workload = await findServiceResource({ + ctx, + log, + module, + baseModule, + manifests, + resourceSpec, + }) + + await syncToService({ + ctx: k8sCtx, + service, + hotReloadSpec, + workload, + log, + namespace, + }) + + return {} +} + +/** + * The hot reload action handler for containers. + */ +export async function hotReloadContainer({ + ctx, + log, + service, + module, +}: HotReloadServiceParams): Promise { + const hotReloadSpec = module.spec.hotReload + + if (!hotReloadSpec) { + throw new ConfigurationError( + `Module ${module.name} must specify the \`hotReload\` key for service ${service.name} to be hot-reloadable.`, + { moduleName: module.name, serviceName: service.name } + ) + } + + const k8sCtx = ctx as KubernetesPluginContext + const provider = k8sCtx.provider + const namespace = await getAppNamespace(k8sCtx, log, provider) + const api = await KubeApi.factory(log, ctx, provider) + + // Find the currently deployed workload by labels + const manifest = await createWorkloadManifest({ + api, + provider, + service, + runtimeContext: { envVars: {}, dependencies: [] }, + namespace, + enableHotReload: true, + production: k8sCtx.production, + log, + blueGreen: provider.config.deploymentStrategy === "blue-green", + }) + + const res = await api.listResources({ + log, + apiVersion: manifest.apiVersion, + kind: manifest.kind, + namespace, + labelSelector: { + [gardenAnnotationKey("service")]: service.name, + }, + }) + + const list = res.items.filter((r) => r.metadata.annotations![gardenAnnotationKey("hot-reload")] === "true") + + if (list.length === 0) { + throw new RuntimeError(`Unable to find deployed instance of service ${service.name} with hot-reloading enabled`, { + service, + listResult: res, + }) + } + + const workload = sortBy(list, (r) => r.metadata.creationTimestamp)[list.length - 1] + + await syncToService({ + log, + ctx: k8sCtx, + service, + workload, + hotReloadSpec, + namespace, + }) + + return {} +} diff --git a/core/src/plugins/kubernetes/kubernetes-module/config.ts b/core/src/plugins/kubernetes/kubernetes-module/config.ts index ca91ca35b9..78f9a258ae 100644 --- a/core/src/plugins/kubernetes/kubernetes-module/config.ts +++ b/core/src/plugins/kubernetes/kubernetes-module/config.ts @@ -22,12 +22,16 @@ import { KubernetesTestSpec, KubernetesTaskSpec, namespaceSchema, + containerModuleSchema, + hotReloadArgsSchema, } from "../config" +import { ContainerModule } from "../../container/config" // A Kubernetes Module always maps to a single Service export type KubernetesModuleSpec = KubernetesServiceSpec -export interface KubernetesModule extends GardenModule {} +export interface KubernetesModule + extends GardenModule {} export type KubernetesModuleConfig = KubernetesModule["_config"] export interface KubernetesServiceSpec { @@ -40,7 +44,7 @@ export interface KubernetesServiceSpec { tests: KubernetesTestSpec[] } -export type KubernetesService = Service +export type KubernetesService = Service const kubernetesResourceSchema = () => joi @@ -75,12 +79,17 @@ export const kubernetesModuleSpecSchema = () => \`files\` directive so that only the Kubernetes manifests get included. `), namespace: namespaceSchema(), - serviceResource: serviceResourceSchema().description( - deline`The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module - (not to be confused with Kubernetes Service resources). - Because a \`kubernetes\` can contain any number of Kubernetes resources, this needs to be specified for certain - Garden features and commands to work.` - ), + serviceResource: serviceResourceSchema() + .description( + deline`The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module + (not to be confused with Kubernetes Service resources). + Because a \`kubernetes-module\` can contain any number of Kubernetes resources, this needs to be specified for certain + Garden features and commands to work.` + ) + .keys({ + containerModule: containerModuleSchema(), + hotReloadArgs: hotReloadArgsSchema(), + }), tasks: joiArray(kubernetesTaskSchema()), tests: joiArray(kubernetesTestSchema()), }) @@ -88,16 +97,21 @@ export const kubernetesModuleSpecSchema = () => export async function configureKubernetesModule({ moduleConfig, }: ConfigureModuleParams): Promise> { + const { serviceResource } = moduleConfig.spec + const sourceModuleName = serviceResource ? serviceResource.containerModule : undefined + moduleConfig.serviceConfigs = [ { name: moduleConfig.name, dependencies: moduleConfig.spec.dependencies, disabled: moduleConfig.disabled, - hotReloadable: false, + // Note: We can't tell here if the source module supports hot-reloading, + // so we catch it in the handler if need be. + hotReloadable: !!sourceModuleName, + sourceModuleName, spec: moduleConfig.spec, }, ] - // Unless include is explicitly specified, we should just have it equal the `files` field if (!(moduleConfig.include || moduleConfig.exclude)) { moduleConfig.include = moduleConfig.spec.files diff --git a/core/src/plugins/kubernetes/kubernetes-module/handlers.ts b/core/src/plugins/kubernetes/kubernetes-module/handlers.ts index e8f996bf1d..5ebab0acdc 100644 --- a/core/src/plugins/kubernetes/kubernetes-module/handlers.ts +++ b/core/src/plugins/kubernetes/kubernetes-module/handlers.ts @@ -7,12 +7,12 @@ */ import Bluebird from "bluebird" -import { partition, uniq } from "lodash" +import { cloneDeep, partition, set, uniq } from "lodash" import { KubernetesModule, configureKubernetesModule, KubernetesService } from "./config" import { KubernetesPluginContext } from "../config" -import { KubernetesServerResource } from "../types" -import { ServiceStatus } from "../../../types/service" +import { BaseResource, KubernetesResource, KubernetesServerResource } from "../types" +import { Service, ServiceStatus } from "../../../types/service" import { compareDeployedResources, waitForResources } from "../status/status" import { KubeApi } from "../api" import { ModuleAndRuntimeActionHandlers } from "../../../types/plugin/plugin" @@ -31,6 +31,12 @@ import { runKubernetesTask } from "./run" import { getTestResult } from "../test-results" import { getTaskResult } from "../task-results" import { getModuleNamespace } from "../namespace" +import { hotReloadK8s } from "../hot-reload/hot-reload" +import { findServiceResource, getServiceResourceSpec } from "../util" +import { getHotReloadSpec, configureHotReload, getHotReloadContainerName } from "../hot-reload/helpers" +import { LogEntry } from "../../../logger/log-entry" +import { PluginContext } from "../../../plugin-context" +import { V1Deployment, V1DaemonSet, V1StatefulSet } from "@kubernetes/client-node" export const kubernetesHandlers: Partial> = { build, @@ -40,6 +46,7 @@ export const kubernetesHandlers: Partial): Promise { const k8sCtx = ctx const namespace = await getModuleNamespace({ @@ -76,8 +85,16 @@ export async function getKubernetesServiceStatus({ // because the build may not have been staged. // This means that manifests added via the `build.dependencies[].copy` field will not be included. const manifests = await getManifests({ api, log, module, defaultNamespace: namespace, readFromSrcDir: true }) + const preparedForHotReload = await prepareManifestsForHotReload({ + ctx, + log, + module, + service, + hotReload, + manifests, + }) - const { state, remoteResources } = await compareDeployedResources(k8sCtx, api, namespace, manifests, log) + const { state, remoteResources } = await compareDeployedResources(k8sCtx, api, namespace, preparedForHotReload, log) const forwardablePorts = getForwardablePorts(remoteResources) @@ -92,7 +109,7 @@ export async function getKubernetesServiceStatus({ export async function deployKubernetesService( params: DeployServiceParams ): Promise { - const { ctx, module, service, log } = params + const { ctx, module, service, log, hotReload } = params const k8sCtx = ctx const api = await KubeApi.factory(log, ctx, k8sCtx.provider) @@ -106,10 +123,8 @@ export async function deployKubernetesService( const manifests = await getManifests({ api, log, module, defaultNamespace: namespace }) - /** - * We separate out manifests for namespace resources, since we don't want to apply a prune selector - * when applying them. - */ + // We separate out manifests for namespace resources, since we don't want to apply a prune selector + // when applying them. const [namespaceManifests, otherManifests] = partition(manifests, (m) => m.kind === "Namespace") if (namespaceManifests.length > 0) { @@ -124,29 +139,29 @@ export async function deployKubernetesService( log, }) } + const pruneSelector = getSelector(service) if (otherManifests.length > 0) { - // Prune everything else - await apply({ log, ctx, provider: k8sCtx.provider, manifests: otherManifests, pruneSelector }) + const preparedForHotReload = await prepareManifestsForHotReload({ + ctx, + log, + module, + service, + hotReload, + manifests, + }) + + await apply({ log, ctx, provider: k8sCtx.provider, manifests: preparedForHotReload, pruneSelector }) await waitForResources({ namespace, ctx, provider: k8sCtx.provider, serviceName: service.name, - resources: otherManifests, + resources: preparedForHotReload, log, }) } - await waitForResources({ - namespace, - ctx, - provider: k8sCtx.provider, - serviceName: service.name, - resources: manifests, - log, - }) - const status = await getKubernetesServiceStatus(params) // Make sure port forwards work after redeployment @@ -224,3 +239,70 @@ async function getServiceLogs(params: GetServiceLogsParams) { function getSelector(service: KubernetesService) { return `${gardenAnnotationKey("service")}=${service.name}` } + +/** + * Looks for a hot reload target in a list of manifests. If found, the target is either + * configured for hot reloading or annotated with `hot-reload: false`. + * + * Returns the manifests with the original hot reload resource replaced by the modified spec + * + * No-op if no hot reload target found and hot reloading is not enabled. + */ +async function prepareManifestsForHotReload({ + ctx, + log, + module, + service, + hotReload, + manifests, +}: { + ctx: PluginContext + service: Service + log: LogEntry + module: KubernetesModule + hotReload: boolean + manifests: KubernetesResource[] +}) { + let hotReloadTarget: KubernetesResource + + try { + hotReloadTarget = cloneDeep( + await findServiceResource({ + ctx, + log, + module, + baseModule: undefined, + manifests, + resourceSpec: service.spec.serviceResource, + }) + ) + } catch (err) { + // This is only an error if we're actually trying to hot reload. + if (hotReload) { + throw err + } else { + // Nothing to do, so we return the original manifests + return manifests + } + } + + const hotReloadSpec = getHotReloadSpec(service) + + if (hotReload && hotReloadSpec) { + const resourceSpec = getServiceResourceSpec(module, undefined) + configureHotReload({ + target: hotReloadTarget, + hotReloadSpec, + hotReloadArgs: resourceSpec.hotReloadArgs, + containerName: getHotReloadContainerName(module), + }) + set(hotReloadTarget, ["metadata", "annotations", gardenAnnotationKey("hot-reload")], "true") + } else { + set(hotReloadTarget, ["metadata", "annotations", gardenAnnotationKey("hot-reload")], "false") + } + + // Replace the original hot reload resource with the modified spec + return manifests + .filter((m) => !(m.kind === hotReloadTarget!.kind && hotReloadTarget?.metadata.name === m.metadata.name)) + .concat(>hotReloadTarget) +} diff --git a/core/src/plugins/kubernetes/util.ts b/core/src/plugins/kubernetes/util.ts index 70093f5af8..e80278995b 100644 --- a/core/src/plugins/kubernetes/util.ts +++ b/core/src/plugins/kubernetes/util.ts @@ -26,7 +26,7 @@ import { PluginContext } from "../../plugin-context" import { HelmModule } from "./helm/config" import { KubernetesModule } from "./kubernetes-module/config" import { getChartPath, renderHelmTemplateString } from "./helm/common" -import { HotReloadableResource } from "./hot-reload" +import { HotReloadableResource } from "./hot-reload/hot-reload" import { ProviderMap } from "../../config/provider" export const skopeoImage = "gardendev/skopeo:1.41.0-1" diff --git a/core/src/plugins/kubernetes/volumes/persistentvolumeclaim.ts b/core/src/plugins/kubernetes/volumes/persistentvolumeclaim.ts index d334ae19b5..0abc4e2a6c 100644 --- a/core/src/plugins/kubernetes/volumes/persistentvolumeclaim.ts +++ b/core/src/plugins/kubernetes/volumes/persistentvolumeclaim.ts @@ -18,11 +18,12 @@ import { baseBuildSpecSchema } from "../../../config/module" import { ConfigureModuleParams } from "../../../types/plugin/module/configure" import { GetServiceStatusParams } from "../../../types/plugin/service/getServiceStatus" import { GardenModule } from "../../../types/module" -import { KubernetesModule, KubernetesModuleConfig, KubernetesService } from "../kubernetes-module/config" +import { KubernetesModule, KubernetesModuleConfig } from "../kubernetes-module/config" import { KubernetesResource } from "../types" import { getKubernetesServiceStatus, deployKubernetesService } from "../kubernetes-module/handlers" import { DeployServiceParams } from "../../../types/plugin/service/deployService" import { getModuleTypeUrl } from "../../../docs/common" +import { Service } from "../../../types/service" export interface PersistentVolumeClaimSpec extends BaseVolumeSpec { dependencies: string[] @@ -101,7 +102,7 @@ export const pvcModuleDefinition: ModuleTypeDefinition = { /** * Maps a `persistentvolumeclaim` module to a `kubernetes` module (so we can re-use those handlers). */ -function getKubernetesService(pvcModule: PersistentVolumeClaimModule): KubernetesService { +function getKubernetesService(pvcModule: PersistentVolumeClaimModule): Service { const pvcManifest: KubernetesResource = { apiVersion: "v1", kind: "PersistentVolumeClaim", diff --git a/core/test/data/test-projects/kubernetes-module/api-image/Dockerfile b/core/test/data/test-projects/kubernetes-module/api-image/Dockerfile new file mode 100644 index 0000000000..ad14ad92fb --- /dev/null +++ b/core/test/data/test-projects/kubernetes-module/api-image/Dockerfile @@ -0,0 +1,18 @@ +# Using official python runtime base image +FROM python:2.7-alpine + +# Set the application directory +WORKDIR /app + +# Install our requirements.txt +ADD requirements.txt /app/requirements.txt +RUN pip install -r requirements.txt + +# Copy our code from the current folder to /app inside the container +ADD . /app + +# Make port 80 available for links and/or publish +EXPOSE 80 + +# Define our command to be run when launching the container +CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--log-file", "-", "--access-logfile", "-", "--workers", "4", "--keep-alive", "0"] diff --git a/core/test/data/test-projects/kubernetes-module/api-image/app.py b/core/test/data/test-projects/kubernetes-module/api-image/app.py new file mode 100644 index 0000000000..6db2b765ee --- /dev/null +++ b/core/test/data/test-projects/kubernetes-module/api-image/app.py @@ -0,0 +1,52 @@ +from flask import Flask, render_template, request, make_response, g +from flask_cors import CORS +from redis import Redis +import os +import socket +import random +import json + +option_a = os.getenv('OPTION_A', "Cats") +option_b = os.getenv('OPTION_B', "Dogs") +hostname = socket.gethostname() + +app = Flask(__name__) +CORS(app) + +def get_redis(): + if not hasattr(g, 'redis'): + g.redis = Redis(host="redis-master", db=0, socket_timeout=5) + return g.redis + +@app.route("/vote/", methods=['POST','GET']) +def vote(): + voter_id = hex(random.getrandbits(64))[2:-1] + + app.logger.info("received request") + + vote = None + + if request.method == 'POST': + redis = get_redis() + vote = request.form['vote'] + data = json.dumps({'voter_id': voter_id, 'vote': vote}) + + redis.rpush('votes', data) + print("Registered vote") + response = app.response_class( + response=json.dumps(data), + status=200, + mimetype='application/json' + ) + return response + + response = app.response_class( + response=json.dumps({}), + status=404, + mimetype='application/json' + ) + return response + + +if __name__ == "__main__": + app.run(host='0.0.0.0', port=80, debug=True, threaded=True) diff --git a/core/test/data/test-projects/kubernetes-module/api-image/garden.yml b/core/test/data/test-projects/kubernetes-module/api-image/garden.yml new file mode 100644 index 0000000000..4552ec0fe7 --- /dev/null +++ b/core/test/data/test-projects/kubernetes-module/api-image/garden.yml @@ -0,0 +1,8 @@ +kind: Module +description: Image for the API backend for the voting UI +type: container +name: api-image +hotReload: + sync: + - source: "*" + target: /app diff --git a/core/test/data/test-projects/kubernetes-module/api-image/requirements.txt b/core/test/data/test-projects/kubernetes-module/api-image/requirements.txt new file mode 100644 index 0000000000..dcd270a579 --- /dev/null +++ b/core/test/data/test-projects/kubernetes-module/api-image/requirements.txt @@ -0,0 +1,4 @@ +Flask +Redis +gunicorn +flask-cors diff --git a/core/test/data/test-projects/kubernetes-module/with-source-module/garden.yml b/core/test/data/test-projects/kubernetes-module/with-source-module/garden.yml new file mode 100644 index 0000000000..a444e1fe55 --- /dev/null +++ b/core/test/data/test-projects/kubernetes-module/with-source-module/garden.yml @@ -0,0 +1,38 @@ +kind: Module +type: kubernetes +name: with-source-module +description: Simple Kubernetes module with minimum config that has a container source module +manifests: + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: api-deployment + labels: + app: api + spec: + replicas: 1 + selector: + matchLabels: + app: api + template: + metadata: + labels: + app: api + spec: + containers: + - name: api + image: ${modules.api-image.outputs.deployment-image-id} + args: [python, app.py] + ports: + - containerPort: 80 +serviceResource: + kind: Deployment + name: api-deployment + containerModule: api-image + containerName: api +tests: + - name: with-source-module-test + command: [sh, -c, "echo ok"] +tasks: + - name: with-source-module-task + command: [sh, -c, "echo ok"] diff --git a/core/test/integ/src/plugins/kubernetes/helm/hot-reload.ts b/core/test/integ/src/plugins/kubernetes/hot-reload.ts similarity index 84% rename from core/test/integ/src/plugins/kubernetes/helm/hot-reload.ts rename to core/test/integ/src/plugins/kubernetes/hot-reload.ts index 3b739dff27..6b0fcdebff 100644 --- a/core/test/integ/src/plugins/kubernetes/helm/hot-reload.ts +++ b/core/test/integ/src/plugins/kubernetes/hot-reload.ts @@ -8,16 +8,19 @@ import { expect } from "chai" -import { TestGarden, expectError } from "../../../../../helpers" -import { getHotReloadSpec, getHotReloadContainerName } from "../../../../../../src/plugins/kubernetes/helm/hot-reload" -import { deline } from "../../../../../../src/util/string" -import { ConfigGraph } from "../../../../../../src/config-graph" -import { getHelmTestGarden, buildHelmModules } from "./common" -import { getChartResources } from "../../../../../../src/plugins/kubernetes/helm/common" -import { PluginContext } from "../../../../../../src/plugin-context" -import { KubernetesProvider } from "../../../../../../src/plugins/kubernetes/config" -import { configureHotReload } from "../../../../../../src/plugins/kubernetes/hot-reload" -import { findServiceResource, getServiceResourceSpec } from "../../../../../../src/plugins/kubernetes/util" +import { TestGarden, expectError } from "../../../../helpers" +import { deline } from "../../../../../src/util/string" +import { ConfigGraph } from "../../../../../src/config-graph" +import { getHelmTestGarden, buildHelmModules } from "./helm/common" +import { getChartResources } from "../../../../../src/plugins/kubernetes/helm/common" +import { PluginContext } from "../../../../../src/plugin-context" +import { KubernetesProvider } from "../../../../../src/plugins/kubernetes/config" +import { findServiceResource, getServiceResourceSpec } from "../../../../../src/plugins/kubernetes/util" +import { + getHotReloadSpec, + getHotReloadContainerName, + configureHotReload, +} from "../../../../../src/plugins/kubernetes/hot-reload/helpers" describe("getHotReloadSpec", () => { let garden: TestGarden diff --git a/core/test/integ/src/plugins/kubernetes/kubernetes-module/config.ts b/core/test/integ/src/plugins/kubernetes/kubernetes-module/config.ts index 235a785092..90006f63fc 100644 --- a/core/test/integ/src/plugins/kubernetes/kubernetes-module/config.ts +++ b/core/test/integ/src/plugins/kubernetes/kubernetes-module/config.ts @@ -92,6 +92,7 @@ describe("validateKubernetesModule", () => { dependencies: [], disabled: false, hotReloadable: false, + sourceModuleName: undefined, name: "module-simple", spec: { build: { @@ -218,6 +219,195 @@ describe("validateKubernetesModule", () => { }) }) + it("should validate a Kubernetes module that has a source module", async () => { + const module = await garden.resolveModule("with-source-module") + const graph = await garden.getConfigGraph(garden.log) + const imageModule = graph.getModule("api-image") + const { versionString } = imageModule.version + + const serviceResource = { + kind: "Deployment", + name: "api-deployment", + containerModule: "api-image", + containerName: "api", + } + + const taskSpecs = [ + { + name: "with-source-module-task", + command: ["sh", "-c", "echo ok"], + cacheResult: true, + dependencies: [], + disabled: false, + timeout: null, + env: {}, + artifacts: [], + }, + ] + + const testSpecs = [ + { + name: "with-source-module-test", + command: ["sh", "-c", "echo ok"], + dependencies: [], + disabled: false, + timeout: null, + env: {}, + artifacts: [], + }, + ] + + expect(module._config).to.eql({ + allowPublish: true, + apiVersion: "garden.io/v0", + build: { + dependencies: [], + }, + configPath: resolve(ctx.projectRoot, "with-source-module", "garden.yml"), + description: "Simple Kubernetes module with minimum config that has a container source module", + disabled: false, + exclude: undefined, + generateFiles: undefined, + inputs: {}, + include: [], + kind: "Module", + name: "with-source-module", + path: resolve(ctx.projectRoot, "with-source-module"), + repositoryUrl: undefined, + serviceConfigs: [ + { + dependencies: [], + disabled: false, + hotReloadable: true, + sourceModuleName: "api-image", + name: "with-source-module", + spec: { + build: { + dependencies: [], + }, + dependencies: [], + files: [], + manifests: [ + { + apiVersion: "apps/v1", + kind: "Deployment", + metadata: { + labels: { + app: "api", + }, + name: "api-deployment", + }, + spec: { + replicas: 1, + selector: { + matchLabels: { + app: "api", + }, + }, + template: { + metadata: { + labels: { + app: "api", + }, + }, + spec: { + containers: [ + { + image: `api-image:${versionString}`, + args: ["python", "app.py"], + name: "api", + ports: [ + { + containerPort: 80, + }, + ], + }, + ], + }, + }, + }, + }, + ], + serviceResource, + tasks: taskSpecs, + tests: testSpecs, + }, + }, + ], + spec: { + build: { + dependencies: [], + }, + dependencies: [], + files: [], + manifests: [ + { + apiVersion: "apps/v1", + kind: "Deployment", + metadata: { + labels: { + app: "api", + }, + name: "api-deployment", + }, + spec: { + replicas: 1, + selector: { + matchLabels: { + app: "api", + }, + }, + template: { + metadata: { + labels: { + app: "api", + }, + }, + spec: { + containers: [ + { + image: `api-image:${versionString}`, + args: ["python", "app.py"], + name: "api", + ports: [ + { + containerPort: 80, + }, + ], + }, + ], + }, + }, + }, + }, + ], + serviceResource, + tasks: taskSpecs, + tests: testSpecs, + }, + taskConfigs: [ + { + name: "with-source-module-task", + cacheResult: true, + dependencies: [], + disabled: false, + spec: taskSpecs[0], + timeout: null, + }, + ], + testConfigs: [ + { + name: "with-source-module-test", + dependencies: [], + disabled: false, + spec: testSpecs[0], + timeout: null, + }, + ], + type: "kubernetes", + }) + }) + it("should set include to equal files if neither include nor exclude has been set", async () => { patchModuleConfig("module-simple", { spec: { files: ["manifest.yaml"] } }) const configInclude = await garden.resolveModule("module-simple") diff --git a/core/test/integ/src/plugins/kubernetes/kubernetes-module/handlers.ts b/core/test/integ/src/plugins/kubernetes/kubernetes-module/handlers.ts index f1c83a3819..1a903a192a 100644 --- a/core/test/integ/src/plugins/kubernetes/kubernetes-module/handlers.ts +++ b/core/test/integ/src/plugins/kubernetes/kubernetes-module/handlers.ts @@ -23,6 +23,11 @@ import { getDeployedResource } from "../../../../../../src/plugins/kubernetes/st import { ModuleConfig } from "../../../../../../src/config/module" import { KubernetesResource, BaseResource } from "../../../../../../src/plugins/kubernetes/types" import { DeleteServiceTask } from "../../../../../../src/tasks/delete-service" +import { deployKubernetesService } from "../../../../../../src/plugins/kubernetes/kubernetes-module/handlers" +import { emptyRuntimeContext } from "../../../../../../src/runtime-context" +import Bluebird from "bluebird" +import { buildHelmModules } from "../helm/common" +import { gardenAnnotationKey } from "../../../../../../src/util/string" describe("kubernetes-module handlers", () => { let tmpDir: tmp.DirectoryResult @@ -51,6 +56,13 @@ describe("kubernetes-module handlers", () => { return cloned } + const findDeployedResources = async (manifests: KubernetesResource[], logEntry: LogEntry) => { + const maybeDeployedObjects = await Bluebird.map(manifests, (resource) => + getDeployedResource(ctx, ctx.provider, resource, logEntry) + ) + return maybeDeployedObjects.filter((o) => o !== null) + } + before(async () => { garden = await getKubernetesTestGarden() moduleConfigBackup = await garden.getRawModuleConfigs() @@ -91,6 +103,9 @@ describe("kubernetes-module handlers", () => { type: "kubernetes", taskConfigs: [], } + + const graph = await garden.getConfigGraph(garden.log) + await buildHelmModules(garden, graph) }) after(async () => { @@ -99,6 +114,50 @@ describe("kubernetes-module handlers", () => { }) describe("deployKubernetesService", () => { + it("should toggle hot reload", async () => { + const graph = await garden.getConfigGraph(garden.log) + const service = graph.getService("with-source-module") + const namespace = await getModuleNamespace({ + ctx, + log, + module: service.module, + provider: ctx.provider, + skipCreate: true, + }) + const deployParams = { + ctx, + log: garden.log, + module: service.module, + service, + force: false, + hotReload: false, + runtimeContext: emptyRuntimeContext, + } + const manifests = await getManifests({ + api, + log, + module: service.module, + defaultNamespace: namespace, + readFromSrcDir: true, + }) + + // Deploy without hot reload + await deployKubernetesService(deployParams) + const res1 = await findDeployedResources(manifests, log) + + // Deploy with hot reload + await deployKubernetesService({ ...deployParams, hotReload: true }) + const res2 = await findDeployedResources(manifests, log) + + // // Deploy without hot reload again + await deployKubernetesService(deployParams) + const res3 = await findDeployedResources(manifests, log) + + expect(res1[0].metadata.annotations![gardenAnnotationKey("hot-reload")]).to.equal("false") + expect(res2[0].metadata.annotations![gardenAnnotationKey("hot-reload")]).to.equal("true") + expect(res3[0].metadata.annotations![gardenAnnotationKey("hot-reload")]).to.equal("false") + }) + it("should not delete previously deployed namespace resources", async () => { garden.setModuleConfigs([withNamespace(nsModuleConfig, "kubernetes-module-ns-1")]) let graph = await garden.getConfigGraph(log) diff --git a/core/test/integ/src/plugins/kubernetes/util.ts b/core/test/integ/src/plugins/kubernetes/util.ts index 7ddcac3333..521364e942 100644 --- a/core/test/integ/src/plugins/kubernetes/util.ts +++ b/core/test/integ/src/plugins/kubernetes/util.ts @@ -28,7 +28,7 @@ import { getHelmTestGarden } from "./helm/common" import { deline } from "../../../../../src/util/string" import { getBaseModule, getChartResources } from "../../../../../src/plugins/kubernetes/helm/common" import { buildHelmModule } from "../../../../../src/plugins/kubernetes/helm/build" -import { HotReloadableResource } from "../../../../../src/plugins/kubernetes/hot-reload" +import { HotReloadableResource } from "../../../../../src/plugins/kubernetes/hot-reload/hot-reload" import { LogEntry } from "../../../../../src/logger/log-entry" import { BuildTask } from "../../../../../src/tasks/build" import { getContainerTestGarden } from "./container/container" diff --git a/core/test/unit/src/plugins/kubernetes/hot-reload.ts b/core/test/unit/src/plugins/kubernetes/hot-reload.ts index 2fd19dba4a..787b787caa 100644 --- a/core/test/unit/src/plugins/kubernetes/hot-reload.ts +++ b/core/test/unit/src/plugins/kubernetes/hot-reload.ts @@ -9,21 +9,18 @@ import { platform } from "os" import { expect } from "chai" import td from "testdouble" -import { - HotReloadableResource, - rsyncSourcePath, - filesForSync, - RSYNC_PORT_NAME, -} from "../../../../../src/plugins/kubernetes/hot-reload" +import { HotReloadableResource, RSYNC_PORT_NAME } from "../../../../../src/plugins/kubernetes/hot-reload/hot-reload" -import { - removeTrailingSlashes, - makeCopyCommand, - configureHotReload, -} from "../../../../../src/plugins/kubernetes/hot-reload" import { setPlatform, makeTestGarden, TestGarden, getDataDir } from "../../../../helpers" import { ConfigGraph } from "../../../../../src/config-graph" import { cloneDeep } from "lodash" +import { + configureHotReload, + removeTrailingSlashes, + rsyncSourcePath, + makeCopyCommand, + filesForSync, +} from "../../../../../src/plugins/kubernetes/hot-reload/helpers" describe("configureHotReload", () => { it("should correctly augment a resource manifest with containers and volume for hot reloading", async () => { diff --git a/docs/reference/module-types/helm.md b/docs/reference/module-types/helm.md index 314743c0e8..03510028e5 100644 --- a/docs/reference/module-types/helm.md +++ b/docs/reference/module-types/helm.md @@ -174,7 +174,7 @@ serviceResource: name: # The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` - # in order to enable hot-reloading for the chart, but is not necessary for tasks and tests. + # in order to enable hot-reloading, but is not necessary for tasks and tests. # Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the # container module. # Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` @@ -259,7 +259,7 @@ tasks: name: # The Garden module that contains the sources for the container. This needs to be specified under - # `serviceResource` in order to enable hot-reloading for the chart, but is not necessary for tasks and tests. + # `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. # Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the # container module. # Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` @@ -325,7 +325,7 @@ tests: name: # The Garden module that contains the sources for the container. This needs to be specified under - # `serviceResource` in order to enable hot-reloading for the chart, but is not necessary for tasks and tests. + # `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. # Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the # container module. # Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` @@ -713,7 +713,7 @@ This can include a Helm template string, e.g. '{{ template "my-chart.fullname" . [serviceResource](#serviceresource) > containerModule -The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading for the chart, but is not necessary for tasks and tests. +The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module. Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` @@ -982,7 +982,7 @@ This can include a Helm template string, e.g. '{{ template "my-chart.fullname" . [tasks](#tasks) > [resource](#tasksresource) > containerModule -The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading for the chart, but is not necessary for tasks and tests. +The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module. Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` @@ -1224,7 +1224,7 @@ This can include a Helm template string, e.g. '{{ template "my-chart.fullname" . [tests](#tests) > [resource](#testsresource) > containerModule -The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading for the chart, but is not necessary for tasks and tests. +The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module. Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` diff --git a/docs/reference/module-types/kubernetes.md b/docs/reference/module-types/kubernetes.md index 20b331fe44..eaab8a7b64 100644 --- a/docs/reference/module-types/kubernetes.md +++ b/docs/reference/module-types/kubernetes.md @@ -153,8 +153,8 @@ files: [] namespace: # The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module (not to be -# confused with Kubernetes Service resources). Because a `kubernetes` can contain any number of Kubernetes resources, -# this needs to be specified for certain Garden features and commands to work. +# confused with Kubernetes Service resources). Because a `kubernetes-module` can contain any number of Kubernetes +# resources, this needs to be specified for certain Garden features and commands to work. serviceResource: # The type of Kubernetes resource to sync files to. kind: Deployment @@ -167,6 +167,16 @@ serviceResource: # container is not the first container in the spec. containerName: + # The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` + # in order to enable hot-reloading, but is not necessary for tasks and tests. + # Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the + # container module. + # Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` + containerModule: + + # If specified, overrides the arguments for the main container when running in hot-reload mode. + hotReloadArgs: + tasks: - # The name of the task. name: @@ -597,7 +607,7 @@ Deploy to a different namespace than the default one configured in the provider. ### `serviceResource` -The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module (not to be confused with Kubernetes Service resources). Because a `kubernetes` can contain any number of Kubernetes resources, this needs to be specified for certain Garden features and commands to work. +The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module (not to be confused with Kubernetes Service resources). Because a `kubernetes-module` can contain any number of Kubernetes resources, this needs to be specified for certain Garden features and commands to work. | Type | Required | | -------- | -------- | @@ -633,6 +643,46 @@ The name of a container in the target. Specify this if the target contains more | -------- | -------- | | `string` | No | +### `serviceResource.containerModule` + +[serviceResource](#serviceresource) > containerModule + +The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. +Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module. +Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` + +| Type | Required | +| -------- | -------- | +| `string` | No | + +Example: + +```yaml +serviceResource: + ... + containerModule: "my-container-module" +``` + +### `serviceResource.hotReloadArgs[]` + +[serviceResource](#serviceresource) > hotReloadArgs + +If specified, overrides the arguments for the main container when running in hot-reload mode. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | No | + +Example: + +```yaml +serviceResource: + ... + hotReloadArgs: + - nodemon + - my-server.js +``` + ### `tasks[]` | Type | Default | Required | diff --git a/examples/hot-reload-k8s/README.md b/examples/hot-reload-k8s/README.md new file mode 100644 index 0000000000..16a78fa62e --- /dev/null +++ b/examples/hot-reload-k8s/README.md @@ -0,0 +1,49 @@ +# Kubernetes Module Hot-Reload Example Project + +This examples demonstrates how to configure the `kubernetes` module type for hot reloading. + +The same pattern applies for `helm` modules. + +## Configuration + +The project contains two modules, a `node-image` module of type `container` that contains the source code and a `node-service` module of type `kubernetes`. The `node-image` is the source module to the `node-service`. + +To enable hot reloading, we first set the hot reloading spec on the `container` module like so: + +```yaml +kind: Module +type: container +# ... +hotReload: + sync: + - target: /app/ +``` + +In the `kubernetes` module, we then reference the image module in the `serviceResource.containerModule` field and reference the image ID in the container spec of the Pod template: + +```yaml +kind: Module +name: node-service +type: kubernetes +serviceResource: + kind: Deployment # <--- The kind of the K8s resource that should be considered the service resource + name: node-service # <--- The name of the K8s resource that should be considered the service resource + containerModule: node-image # <--- The container module that contains the source code that should be hot reloaded + containerName: node-service # <--- The name of the container in the K8s spec that we're syncing to + hotReloadArgs: [npm, run, dev] # <--- This is optional and allows you to override the hot reload args of the container module +manifests: + - apiVersion: apps/v1 + kind: Deployment + # .. + spec: + template: + # ... + spec: + containers: + - image: ${modules.node-image.outputs.deployment-image-id} # <--- Here we reference the container module image id + # ... +``` + +## Usage + +Run `garden deploy --hot node-service` to hot reload the `node-service` module. diff --git a/examples/hot-reload-k8s/garden.yml b/examples/hot-reload-k8s/garden.yml new file mode 100644 index 0000000000..b8162924bd --- /dev/null +++ b/examples/hot-reload-k8s/garden.yml @@ -0,0 +1,21 @@ +kind: Project +name: hot-reload-k8s +# defaultEnvironment: "remote" # Uncomment if you'd like the remote environment to be the default for this project. +environments: + - name: local + variables: + default-hostname: hot-reload-k8s.local.app.garden + - name: remote + variables: + default-hostname: ${local.username}-hot-reload-k8s.dev-1.sys.garden +providers: + - name: local-kubernetes + environments: [local] + defaultHostname: ${var.default-hostname} + - name: kubernetes + environments: [remote] + # Replace these values as appropriate + context: gke_garden-dev-200012_europe-west1-b_garden-dev-1 + namespace: hot-reload-k8s-testing-${local.username} + defaultHostname: ${var.default-hostname} + buildMode: cluster-docker diff --git a/examples/hot-reload-k8s/node-service/.dockerignore b/examples/hot-reload-k8s/node-service/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/examples/hot-reload-k8s/node-service/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/examples/hot-reload-k8s/node-service/Dockerfile b/examples/hot-reload-k8s/node-service/Dockerfile new file mode 100644 index 0000000000..1a4aad56bc --- /dev/null +++ b/examples/hot-reload-k8s/node-service/Dockerfile @@ -0,0 +1,13 @@ +FROM node:9-alpine + +ENV PORT=8080 +EXPOSE ${PORT} + +RUN npm install -g nodemon + +ADD . /app +WORKDIR /app + +RUN npm install + +CMD ["npm", "start"] diff --git a/examples/hot-reload-k8s/node-service/app.js b/examples/hot-reload-k8s/node-service/app.js new file mode 100644 index 0000000000..7df677c318 --- /dev/null +++ b/examples/hot-reload-k8s/node-service/app.js @@ -0,0 +1,14 @@ +const express = require("express") + +const app = express() + +app.get("/hello", (req, res) => { + res.json({ message: "Hello from Node!" }) +}) + +// This is the path GAE uses for health checks +app.get("/_ah/health", (req, res) => { + res.sendStatus(200) +}) + +module.exports = { app } diff --git a/examples/hot-reload-k8s/node-service/garden.yml b/examples/hot-reload-k8s/node-service/garden.yml new file mode 100644 index 0000000000..720d9fa6d4 --- /dev/null +++ b/examples/hot-reload-k8s/node-service/garden.yml @@ -0,0 +1,90 @@ +kind: Module +description: Node hot reload image +name: node-image +type: container +include: ["*"] +hotReload: + sync: + - target: /app/ + +--- + +kind: Module +description: K8s Module +name: node-service +include: [] +type: kubernetes +serviceResource: + kind: Deployment + containerModule: node-image + name: node-service + containerName: node-service + hotReloadArgs: [npm, run, dev] +manifests: + - apiVersion: apps/v1 + kind: Deployment + metadata: + labels: + service: node-service + name: node-service + spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + service: node-service + template: + metadata: + labels: + service: node-service + spec: + containers: + - image: ${modules.node-image.outputs.deployment-image-id} + imagePullPolicy: IfNotPresent + name: node-service + ports: + - containerPort: 8080 + name: http + protocol: TCP + resources: + limits: + cpu: "1" + memory: 1Gi + requests: + cpu: 10m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + dnsPolicy: ClusterFirst + restartPolicy: Always + - apiVersion: v1 + kind: Service + metadata: + labels: + service: node-service + name: node-service + spec: + selector: + service: node-service + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: 8080 + type: ClusterIP + - apiVersion: extensions/v1beta1 + kind: Ingress + metadata: + labels: + service: node-service + name: node-service + spec: + rules: + - host: ${var.default-hostname} + http: + paths: + - backend: + serviceName: node-service + servicePort: 8080 + path: /hello diff --git a/examples/hot-reload-k8s/node-service/main.js b/examples/hot-reload-k8s/node-service/main.js new file mode 100644 index 0000000000..c20d705f46 --- /dev/null +++ b/examples/hot-reload-k8s/node-service/main.js @@ -0,0 +1,3 @@ +const { app } = require("./app") + +app.listen(process.env.PORT, "0.0.0.0", () => console.log("App started")) diff --git a/examples/hot-reload-k8s/node-service/package-lock.json b/examples/hot-reload-k8s/node-service/package-lock.json new file mode 100644 index 0000000000..8c4c7f5734 --- /dev/null +++ b/examples/hot-reload-k8s/node-service/package-lock.json @@ -0,0 +1,374 @@ +{ + "name": "good-morning", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + } + } +} diff --git a/examples/hot-reload-k8s/node-service/package.json b/examples/hot-reload-k8s/node-service/package.json new file mode 100644 index 0000000000..c8971a680f --- /dev/null +++ b/examples/hot-reload-k8s/node-service/package.json @@ -0,0 +1,16 @@ +{ + "name": "node-service", + "version": "1.0.0", + "description": "Greeting service", + "main": "index.js", + "scripts": { + "start": "node main.js", + "dev": "nodemon main.js", + "test": "echo OK" + }, + "author": "garden.io ", + "license": "ISC", + "dependencies": { + "express": "^4.17.1" + } +} diff --git a/examples/hot-reload/README.md b/examples/hot-reload/README.md index bc2305bb37..e9d5720020 100644 --- a/examples/hot-reload/README.md +++ b/examples/hot-reload/README.md @@ -2,7 +2,7 @@ This example showcases Garden's hot-reloading functionality. -When using the `local-kuberbetes` or `kubernetes` providers, container modules can be configured to hot-reload their running services when the module's source files change without redeploying. In essence, hot-reloading copies source files into the appropriate running containers (local or remote) when code is changed by the user. +When using the `local-kubernetes` or `kubernetes` providers, container modules can be configured to hot-reload their running services when the module's source files change without redeploying. In essence, hot-reloading copies source files into the appropriate running containers (local or remote) when code is changed by the user. For example, services that can be run with a file system watcher that automatically update the running application process when sources change (e.g. nodemon, Django, Ruby on Rails, and most popular web app frameworks) are a natural fit for this feature. @@ -13,25 +13,24 @@ This project contains a single service called `node-service`. When running, the In the `garden.yml` file of the `node-service` module we first enable hot-reloading and specify the target directory it should hot-reload changed sourcefiles into: ```yaml -... +# ... hotReload: sync: - target: /app/ - -... - +# ... ``` + We also tell the module which command should be run if hot-reloading is enabled to start the service: ```yaml -... - hotReloadArgs: [npm, run, dev] -... +# ... +hotReloadArgs: [npm, run, dev] +# ... ``` ## Usage -Hot-reloading is *not* enabled by default. To spin up your Garden project with hot-reloading enabled for a particular module, use the `--hot` switch when invoking `garden dev` (or `garden deploy`): +Hot-reloading is _not_ enabled by default. To spin up your Garden project with hot-reloading enabled for a particular module, use the `--hot` switch when invoking `garden dev` (or `garden deploy`): ```sh garden dev --hot=node-service @@ -73,4 +72,4 @@ And you can verify the change by running `garden call node-service` again: } ``` -Check out the [docs](https://docs.garden.io/guides/hot-reload) for more information on hot-reloading. Hot-reloading also works with spring-boot, for which we have a dedicated [example project](https://github.com/garden-io/garden/tree/master/examples/spring-boot-hot-reload). \ No newline at end of file +Check out the [docs](https://docs.garden.io/guides/hot-reload) for more information on hot-reloading. Hot-reloading also works with spring-boot, for which we have a dedicated [example project](https://github.com/garden-io/garden/tree/master/examples/spring-boot-hot-reload).