From a7722b58eae12a81b4999aed325004d15543cd91 Mon Sep 17 00:00:00 2001 From: Vladimir Vagaytsev Date: Fri, 8 Jul 2022 14:35:14 +0200 Subject: [PATCH] feat(k8s): local mode for helm modules (#3033) --- core/src/plugins/kubernetes/helm/build.ts | 2 +- core/src/plugins/kubernetes/helm/common.ts | 35 +-- core/src/plugins/kubernetes/helm/config.ts | 2 + .../src/plugins/kubernetes/helm/deployment.ts | 66 ++++-- core/src/plugins/kubernetes/helm/exec.ts | 1 + core/src/plugins/kubernetes/helm/handlers.ts | 8 +- core/src/plugins/kubernetes/helm/run.ts | 17 +- core/src/plugins/kubernetes/helm/status.ts | 114 ++++++---- core/src/plugins/kubernetes/helm/test.ts | 9 +- .../kubernetes/hot-reload/hot-reload.ts | 1 + .../kubernetes/kubernetes-module/handlers.ts | 2 +- core/src/plugins/kubernetes/local-mode.ts | 14 +- .../container/local-mode/garden.yml | 5 + .../backend-image/.dockerignore | 4 + .../backend-image/.gardenignore | 27 +++ .../helm-local-mode/backend-image/.gitignore | 30 +++ .../helm-local-mode/backend-image/Dockerfile | 11 + .../helm-local-mode/backend-image/garden.yml | 4 + .../helm-local-mode/backend-image/main.go | 21 ++ .../helm-local-mode/backend/.dockerignore | 4 + .../helm-local-mode/backend/.gardenignore | 27 +++ .../helm-local-mode/backend/.gitignore | 30 +++ .../helm-local-mode/backend/.helmignore | 22 ++ .../helm-local-mode/backend/Chart.yaml | 5 + .../helm-local-mode/backend/garden.yml | 36 +++ .../backend/templates/NOTES.txt | 21 ++ .../backend/templates/_helpers.tpl | 32 +++ .../backend/templates/deployment.yaml | 44 ++++ .../backend/templates/ingress.yaml | 44 ++++ .../backend/templates/service.yaml | 19 ++ .../helm-local-mode/backend/values.yaml | 49 ++++ .../helm-local-mode/frontend/.dockerignore | 4 + .../helm-local-mode/frontend/.gardenignore | 1 + .../helm-local-mode/frontend/Dockerfile | 12 + .../helm-local-mode/frontend/app.js | 27 +++ .../helm-local-mode/frontend/garden.yml | 27 +++ .../helm-local-mode/frontend/main.js | 3 + .../helm-local-mode/frontend/package.json | 22 ++ .../helm-local-mode/frontend/test/integ.js | 16 ++ .../test-projects/helm-local-mode/garden.yml | 38 ++++ .../kubernetes/container/deployment.ts | 215 +++++++++++++----- .../src/plugins/kubernetes/helm/common.ts | 64 +++++- .../src/plugins/kubernetes/helm/config.ts | 2 +- .../src/plugins/kubernetes/helm/deployment.ts | 112 ++++++++- .../integ/src/plugins/kubernetes/helm/run.ts | 2 +- .../integ/src/plugins/kubernetes/helm/test.ts | 4 +- .../src/plugins/kubernetes/hot-reload.ts | 1 + .../kubernetes/kubernetes-module/handlers.ts | 59 ++++- core/test/integ/src/plugins/kubernetes/run.ts | 1 + .../test/integ/src/plugins/kubernetes/util.ts | 7 + docs/guides/running-service-in-local-mode.md | 39 ++-- docs/reference/module-types/container.md | 6 +- docs/reference/module-types/helm.md | 125 ++++++++++ docs/reference/module-types/jib-container.md | 6 +- docs/reference/module-types/kubernetes.md | 6 +- .../reference/module-types/maven-container.md | 6 +- examples/local-mode-helm/README.md | 45 ++++ .../backend-image/.dockerignore | 4 + .../backend-image/.gardenignore | 27 +++ .../local-mode-helm/backend-image/.gitignore | 30 +++ .../local-mode-helm/backend-image/Dockerfile | 11 + .../local-mode-helm/backend-image/garden.yml | 4 + .../local-mode-helm/backend-image/main.go | 21 ++ .../local-mode-helm/backend-local/.gitignore | 30 +++ examples/local-mode-helm/backend-local/go.mod | 3 + .../local-mode-helm/backend-local/main.go | 21 ++ .../local-mode-helm/backend/.dockerignore | 4 + .../local-mode-helm/backend/.gardenignore | 27 +++ examples/local-mode-helm/backend/.gitignore | 30 +++ examples/local-mode-helm/backend/.helmignore | 22 ++ examples/local-mode-helm/backend/Chart.yaml | 5 + examples/local-mode-helm/backend/garden.yml | 30 +++ .../backend/templates/NOTES.txt | 21 ++ .../backend/templates/_helpers.tpl | 32 +++ .../backend/templates/deployment.yaml | 44 ++++ .../backend/templates/ingress.yaml | 44 ++++ .../backend/templates/service.yaml | 19 ++ examples/local-mode-helm/backend/values.yaml | 49 ++++ .../local-mode-helm/frontend/.dockerignore | 4 + .../local-mode-helm/frontend/.gardenignore | 1 + examples/local-mode-helm/frontend/Dockerfile | 12 + examples/local-mode-helm/frontend/app.js | 27 +++ examples/local-mode-helm/frontend/garden.yml | 27 +++ examples/local-mode-helm/frontend/main.js | 3 + .../local-mode-helm/frontend/package.json | 22 ++ .../local-mode-helm/frontend/test/integ.js | 16 ++ examples/local-mode-helm/garden.yml | 38 ++++ examples/local-mode-k8s/README.md | 2 +- examples/local-mode/README.md | 2 +- plugins/conftest/index.ts | 1 + 90 files changed, 1999 insertions(+), 190 deletions(-) create mode 100644 core/test/data/test-projects/helm-local-mode/backend-image/.dockerignore create mode 100644 core/test/data/test-projects/helm-local-mode/backend-image/.gardenignore create mode 100644 core/test/data/test-projects/helm-local-mode/backend-image/.gitignore create mode 100644 core/test/data/test-projects/helm-local-mode/backend-image/Dockerfile create mode 100644 core/test/data/test-projects/helm-local-mode/backend-image/garden.yml create mode 100644 core/test/data/test-projects/helm-local-mode/backend-image/main.go create mode 100644 core/test/data/test-projects/helm-local-mode/backend/.dockerignore create mode 100644 core/test/data/test-projects/helm-local-mode/backend/.gardenignore create mode 100644 core/test/data/test-projects/helm-local-mode/backend/.gitignore create mode 100644 core/test/data/test-projects/helm-local-mode/backend/.helmignore create mode 100644 core/test/data/test-projects/helm-local-mode/backend/Chart.yaml create mode 100644 core/test/data/test-projects/helm-local-mode/backend/garden.yml create mode 100644 core/test/data/test-projects/helm-local-mode/backend/templates/NOTES.txt create mode 100644 core/test/data/test-projects/helm-local-mode/backend/templates/_helpers.tpl create mode 100644 core/test/data/test-projects/helm-local-mode/backend/templates/deployment.yaml create mode 100644 core/test/data/test-projects/helm-local-mode/backend/templates/ingress.yaml create mode 100644 core/test/data/test-projects/helm-local-mode/backend/templates/service.yaml create mode 100644 core/test/data/test-projects/helm-local-mode/backend/values.yaml create mode 100644 core/test/data/test-projects/helm-local-mode/frontend/.dockerignore create mode 100644 core/test/data/test-projects/helm-local-mode/frontend/.gardenignore create mode 100644 core/test/data/test-projects/helm-local-mode/frontend/Dockerfile create mode 100644 core/test/data/test-projects/helm-local-mode/frontend/app.js create mode 100644 core/test/data/test-projects/helm-local-mode/frontend/garden.yml create mode 100644 core/test/data/test-projects/helm-local-mode/frontend/main.js create mode 100644 core/test/data/test-projects/helm-local-mode/frontend/package.json create mode 100644 core/test/data/test-projects/helm-local-mode/frontend/test/integ.js create mode 100644 core/test/data/test-projects/helm-local-mode/garden.yml create mode 100644 examples/local-mode-helm/README.md create mode 100644 examples/local-mode-helm/backend-image/.dockerignore create mode 100644 examples/local-mode-helm/backend-image/.gardenignore create mode 100644 examples/local-mode-helm/backend-image/.gitignore create mode 100644 examples/local-mode-helm/backend-image/Dockerfile create mode 100644 examples/local-mode-helm/backend-image/garden.yml create mode 100644 examples/local-mode-helm/backend-image/main.go create mode 100644 examples/local-mode-helm/backend-local/.gitignore create mode 100644 examples/local-mode-helm/backend-local/go.mod create mode 100644 examples/local-mode-helm/backend-local/main.go create mode 100644 examples/local-mode-helm/backend/.dockerignore create mode 100644 examples/local-mode-helm/backend/.gardenignore create mode 100644 examples/local-mode-helm/backend/.gitignore create mode 100644 examples/local-mode-helm/backend/.helmignore create mode 100644 examples/local-mode-helm/backend/Chart.yaml create mode 100644 examples/local-mode-helm/backend/garden.yml create mode 100644 examples/local-mode-helm/backend/templates/NOTES.txt create mode 100644 examples/local-mode-helm/backend/templates/_helpers.tpl create mode 100644 examples/local-mode-helm/backend/templates/deployment.yaml create mode 100644 examples/local-mode-helm/backend/templates/ingress.yaml create mode 100644 examples/local-mode-helm/backend/templates/service.yaml create mode 100644 examples/local-mode-helm/backend/values.yaml create mode 100644 examples/local-mode-helm/frontend/.dockerignore create mode 100644 examples/local-mode-helm/frontend/.gardenignore create mode 100644 examples/local-mode-helm/frontend/Dockerfile create mode 100644 examples/local-mode-helm/frontend/app.js create mode 100644 examples/local-mode-helm/frontend/garden.yml create mode 100644 examples/local-mode-helm/frontend/main.js create mode 100644 examples/local-mode-helm/frontend/package.json create mode 100644 examples/local-mode-helm/frontend/test/integ.js create mode 100644 examples/local-mode-helm/garden.yml diff --git a/core/src/plugins/kubernetes/helm/build.ts b/core/src/plugins/kubernetes/helm/build.ts index f847e61a1c..a21860d8db 100644 --- a/core/src/plugins/kubernetes/helm/build.ts +++ b/core/src/plugins/kubernetes/helm/build.ts @@ -9,7 +9,7 @@ import { move } from "fs-extra" import tmp from "tmp-promise" import { HelmModule } from "./config" -import { containsBuildSource, getChartPath, getBaseModule } from "./common" +import { containsBuildSource, getBaseModule, getChartPath } from "./common" import { helm } from "./helm-cli" import { ConfigurationError } from "../../../exceptions" import { deline } from "../../../util/string" diff --git a/core/src/plugins/kubernetes/helm/common.ts b/core/src/plugins/kubernetes/helm/common.ts index a93df84d73..941d15c5e7 100644 --- a/core/src/plugins/kubernetes/helm/common.ts +++ b/core/src/plugins/kubernetes/helm/common.ts @@ -6,10 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { isPlainObject, flatten, cloneDeep } from "lodash" +import { cloneDeep, flatten, isPlainObject } from "lodash" import { join, resolve } from "path" -import { pathExists, writeFile, remove, readFile } from "fs-extra" -import cryptoRandomString = require("crypto-random-string") +import { pathExists, readFile, remove, writeFile } from "fs-extra" import { apply as jsonMerge } from "json-merge-patch" import { PluginContext } from "../../../plugin-context" @@ -22,11 +21,12 @@ import { HelmModule, HelmModuleConfig } from "./config" import { ConfigurationError, PluginError } from "../../../exceptions" import { GardenModule } from "../../../types/module" import { deline, tailString } from "../../../util/string" -import { getAnnotation, flattenResources } from "../util" +import { flattenResources, getAnnotation } from "../util" import { KubernetesPluginContext } from "../config" import { RunResult } from "../../../types/plugin/base" import { MAX_RUN_RESULT_LOG_LENGTH } from "../constants" import { dumpYaml } from "../../../util/util" +import cryptoRandomString = require("crypto-random-string") const gardenValuesFilename = "garden-values.yml" @@ -69,6 +69,7 @@ interface GetChartResourcesParams { module: GardenModule devMode: boolean hotReload: boolean + localMode: boolean log: LogEntry version: string } @@ -90,7 +91,7 @@ export async function getChartResources(params: GetChartResourcesParams) { * Renders the given Helm module and returns a multi-document YAML string. */ export async function renderTemplates(params: GetChartResourcesParams): Promise { - const { ctx, module, devMode, hotReload, version, log } = params + const { ctx, module, devMode, hotReload, localMode, version, log } = params const { namespace, releaseName, chartPath } = await prepareTemplates(params) log.debug("Preparing chart...") @@ -100,6 +101,7 @@ export async function renderTemplates(params: GetChartResourcesParams): Promise< module, devMode, hotReload, + localMode, version, log, namespace, @@ -160,7 +162,7 @@ export async function prepareTemplates({ } export async function prepareManifests(params: PrepareManifestsParams): Promise { - const { ctx, module, devMode, hotReload, log, namespace, releaseName, chartPath } = params + const { ctx, module, devMode, hotReload, localMode, log, namespace, releaseName, chartPath } = params const res = await helm({ ctx, log, @@ -177,7 +179,7 @@ export async function prepareManifests(params: PrepareManifestsParams): Promise< "json", "--timeout", module.spec.timeout.toString(10) + "s", - ...(await getValueArgs(module, devMode, hotReload)), + ...(await getValueArgs(module, devMode, hotReload, localMode)), ], }) @@ -264,7 +266,7 @@ export function getGardenValuesPath(chartPath: string) { /** * Get the value files arguments that should be applied to any helm install/render command. */ -export async function getValueArgs(module: HelmModule, devMode: boolean, hotReload: boolean) { +export async function getValueArgs(module: HelmModule, devMode: boolean, hotReload: boolean, localMode: boolean) { const chartPath = await getChartPath(module) const gardenValuesPath = getGardenValuesPath(chartPath) @@ -274,11 +276,16 @@ export async function getValueArgs(module: HelmModule, devMode: boolean, hotRelo const args = flatten(valueFiles.map((f) => ["--values", f])) - if (devMode) { - args.push("--set", "\\.garden.devMode=true") - } - if (hotReload) { - args.push("--set", "\\.garden.hotReload=true") + // Local mode always takes precedence over dev mode + if (localMode) { + args.push("--set", "\\.garden.localMode=true") + } else { + if (devMode) { + args.push("--set", "\\.garden.devMode=true") + } + if (hotReload) { + args.push("--set", "\\.garden.hotReload=true") + } } return args @@ -329,7 +336,7 @@ export async function renderHelmTemplateString( "--namespace", namespace, "--dependency-update", - ...(await getValueArgs(module, false, false)), + ...(await getValueArgs(module, false, false, false)), "--show-only", relPath, chartPath, diff --git a/core/src/plugins/kubernetes/helm/config.ts b/core/src/plugins/kubernetes/helm/config.ts index 4cec715808..43a96834e1 100644 --- a/core/src/plugins/kubernetes/helm/config.ts +++ b/core/src/plugins/kubernetes/helm/config.ts @@ -28,6 +28,7 @@ import { hotReloadArgsSchema, kubernetesDevModeSchema, KubernetesDevModeSpec, + kubernetesLocalModeSchema, KubernetesLocalModeSpec, kubernetesTaskSchema, KubernetesTaskSpec, @@ -176,6 +177,7 @@ export const helmModuleSpecSchema = () => "List of names of services that should be deployed before this chart." ), devMode: kubernetesDevModeSchema(), + localMode: kubernetesLocalModeSchema(), include: joiModuleIncludeDirective(dedent` If neither \`include\` nor \`exclude\` is set, and the module has local chart sources, Garden automatically sets \`include\` to: \`["*", "charts/**/*", "templates/**/*"]\`. diff --git a/core/src/plugins/kubernetes/helm/deployment.ts b/core/src/plugins/kubernetes/helm/deployment.ts index a04504a013..6ee84b8405 100644 --- a/core/src/plugins/kubernetes/helm/deployment.ts +++ b/core/src/plugins/kubernetes/helm/deployment.ts @@ -11,20 +11,20 @@ import { waitForResources } from "../status/status" import { helm } from "./helm-cli" import { HelmModule } from "./config" import { + filterManifests, + getBaseModule, getChartPath, getReleaseName, getValueArgs, - getBaseModule, - prepareTemplates, prepareManifests, - filterManifests, + prepareTemplates, } from "./common" import { + gardenCloudAECPauseAnnotation, + getPausedResources, getReleaseStatus, - HelmServiceStatus, getRenderedResources, - getPausedResources, - gardenCloudAECPauseAnnotation, + HelmServiceStatus, } from "./status" import { SyncableResource } from "../hot-reload/hot-reload" import { apply, deleteResources } from "../kubectl" @@ -35,9 +35,10 @@ import { DeleteServiceParams } from "../../../types/plugin/service/deleteService import { getForwardablePorts, killPortForwards } from "../port-forward" import { getServiceResource, getServiceResourceSpec } from "../util" import { getModuleNamespace, getModuleNamespaceStatus } from "../namespace" -import { getHotReloadSpec, configureHotReload, getHotReloadContainerName } from "../hot-reload/helpers" +import { configureHotReload, getHotReloadContainerName, getHotReloadSpec } from "../hot-reload/helpers" import { configureDevMode, startDevModeSync } from "../dev-mode" import { KubeApi } from "../api" +import { configureLocalMode, startServiceInLocalMode } from "../local-mode" export async function deployHelmService({ ctx, @@ -47,6 +48,7 @@ export async function deployHelmService({ force, devMode, hotReload, + localMode, }: DeployServiceParams): Promise { let hotReloadSpec: ContainerHotReloadSpec | null = null let serviceResourceSpec: ServiceResourceSpec | null = null @@ -69,20 +71,30 @@ export async function deployHelmService({ module, devMode, hotReload, + localMode, log, version: service.version, }) const chartPath = await getChartPath(module) const releaseName = getReleaseName(module) - const releaseStatus = await getReleaseStatus({ ctx: k8sCtx, module, service, releaseName, log, devMode, hotReload }) + const releaseStatus = await getReleaseStatus({ + ctx: k8sCtx, + module, + service, + releaseName, + log, + devMode, + hotReload, + localMode, + }) const commonArgs = [ "--namespace", namespace, "--timeout", module.spec.timeout.toString(10) + "s", - ...(await getValueArgs(module, devMode, hotReload)), + ...(await getValueArgs(module, devMode, hotReload, localMode)), ] if (module.spec.atomicInstall) { @@ -135,6 +147,7 @@ export async function deployHelmService({ module, devMode, hotReload, + localMode, version: service.version, namespace: preparedTemplates.namespace, releaseName: preparedTemplates.releaseName, @@ -142,7 +155,7 @@ export async function deployHelmService({ }) const manifests = await filterManifests(preparedManifests) - if ((devMode && module.spec.devMode) || hotReload) { + if ((devMode && module.spec.devMode) || hotReload || (localMode && module.spec.localMode)) { serviceResourceSpec = getServiceResourceSpec(module, getBaseModule(module)) serviceResource = await getServiceResource({ ctx, @@ -155,12 +168,23 @@ export async function deployHelmService({ } // Because we need to modify the Deployment, and because there is currently no reliable way to do that before - // installing/upgrading via Helm, we need to separately update the target here for dev-mode/hot-reload. - if (devMode && service.spec.devMode && serviceResourceSpec && serviceResource) { + // installing/upgrading via Helm, we need to separately update the target here for dev-mode/hot-reload/local-mode. + // Local mode always takes precedence over dev mode. + if (localMode && service.spec.localMode && serviceResourceSpec && serviceResource) { + await configureLocalMode({ + ctx, + spec: service.spec.localMode, + targetResource: serviceResource, + gardenService: service, + log, + containerName: service.spec.localMode.containerName, + }) + await apply({ log, ctx, api, provider, manifests: [serviceResource], namespace }) + } else if (devMode && service.spec.devMode && serviceResourceSpec && serviceResource) { configureDevMode({ target: serviceResource, spec: service.spec.devMode, - containerName: service.spec.devMode?.containerName, + containerName: service.spec.devMode.containerName, }) await apply({ log, ctx, api, provider, manifests: [serviceResource], namespace }) } else if (hotReload && hotReloadSpec && serviceResourceSpec && serviceResource) { @@ -185,12 +209,24 @@ export async function deployHelmService({ timeoutSec: module.spec.timeout, }) - const forwardablePorts = getForwardablePorts(manifests, service) + // Local mode has its own port-forwarding configuration + const forwardablePorts = localMode && service.spec.localMode ? [] : getForwardablePorts(manifests, service) // Make sure port forwards work after redeployment killPortForwards(service, forwardablePorts || [], log) - if (devMode && service.spec.devMode && serviceResource && serviceResourceSpec) { + // Local mode always takes precedence over dev mode. + if (localMode && service.spec.localMode && serviceResource && serviceResourceSpec) { + await startServiceInLocalMode({ + ctx, + spec: service.spec.localMode, + targetResource: serviceResource, + gardenService: service, + namespace, + log, + containerName: service.spec.localMode.containerName, + }) + } else if (devMode && service.spec.devMode && serviceResource && serviceResourceSpec) { await startDevModeSync({ ctx, log, diff --git a/core/src/plugins/kubernetes/helm/exec.ts b/core/src/plugins/kubernetes/helm/exec.ts index 3883f15616..1954f844d9 100644 --- a/core/src/plugins/kubernetes/helm/exec.ts +++ b/core/src/plugins/kubernetes/helm/exec.ts @@ -41,6 +41,7 @@ export async function execInHelmService(params: ExecInServiceParams) module, devMode: false, hotReload: false, + localMode: false, log, version: service.version, }) diff --git a/core/src/plugins/kubernetes/helm/handlers.ts b/core/src/plugins/kubernetes/helm/handlers.ts index a18b12e0b9..e463cb655d 100644 --- a/core/src/plugins/kubernetes/helm/handlers.ts +++ b/core/src/plugins/kubernetes/helm/handlers.ts @@ -7,12 +7,12 @@ */ import { ModuleAndRuntimeActionHandlers } from "../../../types/plugin/plugin" -import { HelmModule, configureHelmModule } from "./config" +import { configureHelmModule, HelmModule } from "./config" import { buildHelmModule } from "./build" import { getServiceStatus } from "./status" -import { deployHelmService, deleteService } from "./deployment" +import { deleteService, deployHelmService } from "./deployment" import { getTestResult } from "../test-results" -import { runHelmTask, runHelmModule } from "./run" +import { runHelmModule, runHelmTask } from "./run" import { getServiceLogs } from "./logs" import { testHelmModule } from "./test" import { getPortForwardHandler } from "../port-forward" @@ -22,11 +22,11 @@ import { KubernetesPluginContext } from "../config" import { getModuleNamespace } from "../namespace" import { join } from "path" 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" import { execInHelmService } from "./exec" +import chalk = require("chalk") export const helmHandlers: Partial> = { build: buildHelmModule, diff --git a/core/src/plugins/kubernetes/helm/run.ts b/core/src/plugins/kubernetes/helm/run.ts index 5f49c73c07..6501cd4ece 100644 --- a/core/src/plugins/kubernetes/helm/run.ts +++ b/core/src/plugins/kubernetes/helm/run.ts @@ -8,13 +8,14 @@ import { HelmModule } from "./config" import { PodRunner, runAndCopy } from "../run" -import { getChartResources, getBaseModule } from "./common" +import { getBaseModule, getChartResources } from "./common" import { - getServiceResource, getResourceContainer, getResourcePodSpec, + getServiceResource, getServiceResourceSpec, makePodName, + prepareEnvVars, } from "../util" import { ConfigurationError } from "../../../exceptions" import { KubernetesPluginContext } from "../config" @@ -23,7 +24,6 @@ import { RunModuleParams } from "../../../types/plugin/module/runModule" import { RunResult } from "../../../types/plugin/base" import { RunTaskParams, RunTaskResult } from "../../../types/plugin/task/runTask" import { uniqByName } from "../../../util/util" -import { prepareEnvVars } from "../util" import { KubeApi } from "../api" import { getModuleNamespaceStatus } from "../namespace" import { DEFAULT_TASK_TIMEOUT } from "../../../constants" @@ -60,7 +60,15 @@ export async function runHelmModule({ ) } - const manifests = await getChartResources({ ctx: k8sCtx, module, devMode: false, hotReload: false, log, version }) + const manifests = await getChartResources({ + ctx: k8sCtx, + module, + devMode: false, + hotReload: false, + localMode: false, + log, + version, + }) const target = await getServiceResource({ ctx: k8sCtx, log, @@ -130,6 +138,7 @@ export async function runHelmTask(params: RunTaskParams): Promise): Promise { const k8sCtx = ctx const releaseName = getReleaseName(module) @@ -63,11 +64,22 @@ export async function getServiceStatus({ }) let deployedWithDevModeOrHotReloading: boolean | undefined + let deployedWithLocalMode: boolean | undefined try { - helmStatus = await getReleaseStatus({ ctx: k8sCtx, module, service, releaseName, log, devMode, hotReload }) + helmStatus = await getReleaseStatus({ + ctx: k8sCtx, + module, + service, + releaseName, + log, + devMode, + hotReload, + localMode, + }) state = helmStatus.state deployedWithDevModeOrHotReloading = helmStatus.devMode + deployedWithLocalMode = helmStatus.localMode } catch (err) { state = "missing" } @@ -78,46 +90,64 @@ export async function getServiceStatus({ if (state !== "missing") { const deployedResources = await getRenderedResources({ ctx: k8sCtx, module, releaseName, log }) - forwardablePorts = getForwardablePorts(deployedResources, service) + forwardablePorts = !!deployedWithLocalMode ? [] : getForwardablePorts(deployedResources, service) ingresses = getK8sIngresses(deployedResources) - if (state === "ready" && devMode && service.spec.devMode) { - // Need to start the dev-mode sync here, since the deployment handler won't be called. - const baseModule = getBaseModule(module) - const serviceResourceSpec = getServiceResourceSpec(module, baseModule) - const target = await getServiceResource({ - ctx: k8sCtx, - log, - provider: k8sCtx.provider, - module, - manifests: deployedResources, - resourceSpec: serviceResourceSpec, - }) - - // Make sure we don't fail if the service isn't actually properly configured (we don't want to throw in the - // status handler, generally) - if (isConfiguredForDevMode(target)) { - const namespace = - target.metadata.namespace || - (await getModuleNamespace({ - ctx: k8sCtx, - log, - module, - provider: k8sCtx.provider, - })) + if (state === "ready") { + // Local mode always takes precedence over dev mode + if (localMode && service.spec.localMode) { + const baseModule = getBaseModule(module) + const serviceResourceSpec = getServiceResourceSpec(module, baseModule) + const target = await getServiceResource({ + ctx: k8sCtx, + log, + provider: k8sCtx.provider, + module, + manifests: deployedResources, + resourceSpec: serviceResourceSpec, + }) - await startDevModeSync({ - ctx, + if (!isConfiguredForLocalMode(target)) { + state = "outdated" + } + } else if (devMode && service.spec.devMode) { + // Need to start the dev-mode sync here, since the deployment handler won't be called. + const baseModule = getBaseModule(module) + const serviceResourceSpec = getServiceResourceSpec(module, baseModule) + const target = await getServiceResource({ + ctx: k8sCtx, log, - moduleRoot: service.sourceModule.path, - namespace, - target, - spec: service.spec.devMode, - containerName: service.spec.devMode.containerName, - serviceName: service.name, + provider: k8sCtx.provider, + module, + manifests: deployedResources, + resourceSpec: serviceResourceSpec, }) - } else { - state = "outdated" + + // Make sure we don't fail if the service isn't actually properly configured (we don't want to throw in the + // status handler, generally) + if (isConfiguredForDevMode(target)) { + const namespace = + target.metadata.namespace || + (await getModuleNamespace({ + ctx: k8sCtx, + log, + module, + provider: k8sCtx.provider, + })) + + await startDevModeSync({ + ctx, + log, + moduleRoot: service.sourceModule.path, + namespace, + target, + spec: service.spec.devMode, + containerName: service.spec.devMode.containerName, + serviceName: service.name, + }) + } else { + state = "outdated" + } } } } @@ -128,6 +158,7 @@ export async function getServiceStatus({ version: state === "ready" ? service.version : undefined, detail, devMode: deployedWithDevModeOrHotReloading, + localMode: deployedWithLocalMode, namespaceStatuses: [namespaceStatus], ingresses, } @@ -169,6 +200,7 @@ export async function getReleaseStatus({ log, devMode, hotReload, + localMode, }: { ctx: KubernetesPluginContext module: HelmModule @@ -177,6 +209,7 @@ export async function getReleaseStatus({ log: LogEntry devMode: boolean hotReload: boolean + localMode: boolean }): Promise { try { log.silly(`Getting the release status for ${releaseName}`) @@ -194,6 +227,7 @@ export async function getReleaseStatus({ let devModeEnabled = false let hotReloadEnabled = false + let localModeEnabled = false if (state === "ready") { // Make sure the right version is deployed @@ -209,10 +243,13 @@ export async function getReleaseStatus({ const deployedVersion = values[".garden"] && values[".garden"].version devModeEnabled = values[".garden"] && values[".garden"].devMode === true hotReloadEnabled = values[".garden"] && values[".garden"].hotReload === true + localModeEnabled = values[".garden"] && values[".garden"].localMode === true if ( (devMode && !devModeEnabled) || (hotReload && !hotReloadEnabled) || + (localMode && !localModeEnabled) || + (!localMode && localModeEnabled) || // this is still a valid case for local-mode !deployedVersion || deployedVersion !== service.version ) { @@ -231,6 +268,7 @@ export async function getReleaseStatus({ state, detail: { ...res, values }, devMode: devModeEnabled || hotReloadEnabled, + localMode: localModeEnabled, } } catch (err) { if (err.message.includes("release: not found")) { diff --git a/core/src/plugins/kubernetes/helm/test.ts b/core/src/plugins/kubernetes/helm/test.ts index cc3f73e4f4..55c8b5b86c 100644 --- a/core/src/plugins/kubernetes/helm/test.ts +++ b/core/src/plugins/kubernetes/helm/test.ts @@ -10,16 +10,16 @@ import { DEFAULT_TEST_TIMEOUT } from "../../../constants" import { storeTestResult } from "../test-results" import { HelmModule } from "./config" import { runAndCopy } from "../run" -import { getChartResources, getBaseModule } from "./common" +import { getBaseModule, getChartResources } from "./common" import { KubernetesPluginContext } from "../config" import { TestModuleParams } from "../../../types/plugin/module/testModule" import { TestResult } from "../../../types/plugin/module/getTestResult" import { - getServiceResourceSpec, - getServiceResource, getResourceContainer, - makePodName, getResourcePodSpec, + getServiceResource, + getServiceResourceSpec, + makePodName, } from "../util" import { getModuleNamespaceStatus } from "../namespace" @@ -33,6 +33,7 @@ export async function testHelmModule(params: TestModuleParams): Prom module, devMode: false, hotReload: false, + localMode: false, log, version: test.version, }) diff --git a/core/src/plugins/kubernetes/hot-reload/hot-reload.ts b/core/src/plugins/kubernetes/hot-reload/hot-reload.ts index 17fff68200..1e8a9d9602 100644 --- a/core/src/plugins/kubernetes/hot-reload/hot-reload.ts +++ b/core/src/plugins/kubernetes/hot-reload/hot-reload.ts @@ -63,6 +63,7 @@ export async function hotReloadK8s({ module: service.module, devMode: false, hotReload: true, + localMode: false, log, version: service.version, }) diff --git a/core/src/plugins/kubernetes/kubernetes-module/handlers.ts b/core/src/plugins/kubernetes/kubernetes-module/handlers.ts index 849ed9c0b5..4258b561f7 100644 --- a/core/src/plugins/kubernetes/kubernetes-module/handlers.ts +++ b/core/src/plugins/kubernetes/kubernetes-module/handlers.ts @@ -22,7 +22,7 @@ import { KubernetesPluginContext, ServiceResourceSpec } from "../config" import { configureDevMode, startDevModeSync } from "../dev-mode" import { HelmService } from "../helm/config" import { configureHotReload, getHotReloadContainerName, getHotReloadSpec } from "../hot-reload/helpers" -import { SyncableResource, hotReloadK8s } from "../hot-reload/hot-reload" +import { hotReloadK8s, SyncableResource } from "../hot-reload/hot-reload" import { apply, deleteObjectsBySelector, KUBECTL_DEFAULT_TIMEOUT } from "../kubectl" import { streamK8sLogs } from "../logs" import { getModuleNamespace, getModuleNamespaceStatus } from "../namespace" diff --git a/core/src/plugins/kubernetes/local-mode.ts b/core/src/plugins/kubernetes/local-mode.ts index 66205a6897..f43e84b3db 100644 --- a/core/src/plugins/kubernetes/local-mode.ts +++ b/core/src/plugins/kubernetes/local-mode.ts @@ -37,7 +37,9 @@ import { HelmService } from "./helm/config" import getPort = require("get-port") import touch = require("touch") -export const localModeGuideLink = "https://docs.garden.io/guides/running-service-in-local-mode.md" +// export const localModeGuideLink = "https://docs.garden.io/guides/running-service-in-local-mode.md" +export const localModeGuideLink = + "https://github.com/garden-io/garden/blob/master/docs/guides/running-service-in-local-mode.md" const localhost = "127.0.0.1" @@ -303,7 +305,7 @@ function prepareLocalModePorts(): V1ContainerPort[] { } /** - * Patches the target Kubernetes Workload or Pod manifest by adding localMode-specific settings + * Patches the target Kubernetes Workload or Pod manifest by changing localMode-specific settings * like ports, environment variables, probes, etc. * * @param targetManifest the Kubernetes workload manifest to be patched @@ -311,7 +313,7 @@ function prepareLocalModePorts(): V1ContainerPort[] { * @param localModeEnvVars the list of localMode-specific environment variables * @param localModePorts the list of localMode-specific ports (e.g. ssh port for tunnel setup) */ -function patchHotReloadableManifest( +function patchSyncableManifest( targetManifest: SyncableResource, containerName: string, localModeEnvVars: PrimitiveMap, @@ -321,8 +323,8 @@ function patchHotReloadableManifest( // use reverse proxy container image targetContainer.image = reverseProxyImageName - // delete the original container arguments, the proxy container won't recognize them - delete targetContainer.args + // erase the original container arguments, the proxy container won't recognize them + targetContainer.args = [] const extraEnvVars = prepareEnvVars(localModeEnvVars) if (!targetContainer.env) { @@ -381,7 +383,7 @@ export async function configureLocalMode(configParams: ConfigureLocalModeParams) const localModeEnvVars = await prepareLocalModeEnvVars(targetContainer, keyPair) const localModePorts = prepareLocalModePorts() - patchHotReloadableManifest(targetResource, targetContainer.name, localModeEnvVars, localModePorts) + patchSyncableManifest(targetResource, targetContainer.name, localModeEnvVars, localModePorts) } const attemptsLeft = ({ maxRetries, minTimeoutMs, retriesLeft }: RetryInfo): string => { diff --git a/core/test/data/test-projects/container/local-mode/garden.yml b/core/test/data/test-projects/container/local-mode/garden.yml index f596c1f1c8..86440f6b70 100644 --- a/core/test/data/test-projects/container/local-mode/garden.yml +++ b/core/test/data/test-projects/container/local-mode/garden.yml @@ -12,6 +12,11 @@ services: localMode: localPort: 8090 command: [] + # this is here to test that local mode always take precedence over dev mode + devMode: + sync: + - target: / + mode: one-way healthCheck: httpGet: path: ${var.ingressPath} diff --git a/core/test/data/test-projects/helm-local-mode/backend-image/.dockerignore b/core/test/data/test-projects/helm-local-mode/backend-image/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend-image/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/core/test/data/test-projects/helm-local-mode/backend-image/.gardenignore b/core/test/data/test-projects/helm-local-mode/backend-image/.gardenignore new file mode 100644 index 0000000000..eb086d61c3 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend-image/.gardenignore @@ -0,0 +1,27 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.vscode/settings.json +webserver/*server* diff --git a/core/test/data/test-projects/helm-local-mode/backend-image/.gitignore b/core/test/data/test-projects/helm-local-mode/backend-image/.gitignore new file mode 100644 index 0000000000..a365e5b54c --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend-image/.gitignore @@ -0,0 +1,30 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.vscode/settings.json +webserver/*server* + +# Generated files +.*.yml diff --git a/core/test/data/test-projects/helm-local-mode/backend-image/Dockerfile b/core/test/data/test-projects/helm-local-mode/backend-image/Dockerfile new file mode 100644 index 0000000000..e993313d66 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend-image/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.18.3-alpine3.16 + +ENV PORT=8080 +EXPOSE ${PORT} +WORKDIR /app + +COPY main.go . + +RUN go mod init main && go build -o main . + +ENTRYPOINT ["./main"] diff --git a/core/test/data/test-projects/helm-local-mode/backend-image/garden.yml b/core/test/data/test-projects/helm-local-mode/backend-image/garden.yml new file mode 100644 index 0000000000..88abd56790 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend-image/garden.yml @@ -0,0 +1,4 @@ +kind: Module +description: Image for the backend service +type: container +name: backend-image diff --git a/core/test/data/test-projects/helm-local-mode/backend-image/main.go b/core/test/data/test-projects/helm-local-mode/backend-image/main.go new file mode 100644 index 0000000000..ef8e18dd11 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend-image/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { +fmt.Println("Incoming request") + fmt.Fprint(w, "Hello from Helm Chart Go!") +} + +func main() { + http.HandleFunc("/hello-backend", handler) + fmt.Println("Server running at port 8080...") + + err := http.ListenAndServe(":8080", nil) + if err != nil { + panic(err) + } +} diff --git a/core/test/data/test-projects/helm-local-mode/backend/.dockerignore b/core/test/data/test-projects/helm-local-mode/backend/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/core/test/data/test-projects/helm-local-mode/backend/.gardenignore b/core/test/data/test-projects/helm-local-mode/backend/.gardenignore new file mode 100644 index 0000000000..eb086d61c3 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/.gardenignore @@ -0,0 +1,27 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.vscode/settings.json +webserver/*server* diff --git a/core/test/data/test-projects/helm-local-mode/backend/.gitignore b/core/test/data/test-projects/helm-local-mode/backend/.gitignore new file mode 100644 index 0000000000..a365e5b54c --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/.gitignore @@ -0,0 +1,30 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.vscode/settings.json +webserver/*server* + +# Generated files +.*.yml diff --git a/core/test/data/test-projects/helm-local-mode/backend/.helmignore b/core/test/data/test-projects/helm-local-mode/backend/.helmignore new file mode 100644 index 0000000000..50af031725 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/core/test/data/test-projects/helm-local-mode/backend/Chart.yaml b/core/test/data/test-projects/helm-local-mode/backend/Chart.yaml new file mode 100644 index 0000000000..aa1d183184 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: backend +version: 0.1.0 diff --git a/core/test/data/test-projects/helm-local-mode/backend/garden.yml b/core/test/data/test-projects/helm-local-mode/backend/garden.yml new file mode 100644 index 0000000000..78b5470fff --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/garden.yml @@ -0,0 +1,36 @@ +kind: Module +name: backend +description: Helm chart for the backend service +type: helm + +localMode: + localPort: 8090 + # starts the local application + command: [ ] + containerName: backend + +# this is here to test that local mode always take precedence over dev mode +devMode: + sync: + - target: /app + mode: one-way + +serviceResource: + kind: Deployment + containerModule: backend-image + +build: + dependencies: [ "backend-image" ] + +values: + image: + repository: ${modules.backend-image.outputs.deployment-image-name} + tag: ${modules.backend-image.version} + ingress: + enabled: true + paths: [ "/hello-backend" ] + hosts: [ "backend.${var.baseHostname}" ] + +tasks: + - name: test + command: [ "sh", "-c", "echo task output" ] diff --git a/core/test/data/test-projects/helm-local-mode/backend/templates/NOTES.txt b/core/test/data/test-projects/helm-local-mode/backend/templates/NOTES.txt new file mode 100644 index 0000000000..2e5dad0038 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/templates/NOTES.txt @@ -0,0 +1,21 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range $.Values.ingress.paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "backend.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ include "backend.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "backend.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "backend.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/core/test/data/test-projects/helm-local-mode/backend/templates/_helpers.tpl b/core/test/data/test-projects/helm-local-mode/backend/templates/_helpers.tpl new file mode 100644 index 0000000000..8f7a7a3f80 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "backend.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "backend.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "backend.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/core/test/data/test-projects/helm-local-mode/backend/templates/deployment.yaml b/core/test/data/test-projects/helm-local-mode/backend/templates/deployment.yaml new file mode 100644 index 0000000000..0ce22bd056 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/templates/deployment.yaml @@ -0,0 +1,44 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: {{ include "backend.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "backend.name" . }} + helm.sh/chart: {{ include "backend.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "backend.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "backend.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: [ npm, run, serve ] + ports: + - name: http + containerPort: 8080 + protocol: TCP + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/core/test/data/test-projects/helm-local-mode/backend/templates/ingress.yaml b/core/test/data/test-projects/helm-local-mode/backend/templates/ingress.yaml new file mode 100644 index 0000000000..90e892506a --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/templates/ingress.yaml @@ -0,0 +1,44 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "backend.fullname" . -}} +{{- $ingressPaths := .Values.ingress.paths -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app.kubernetes.io/name: {{ include "backend.name" . }} + helm.sh/chart: {{ include "backend.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + ingressClassName: {{ .Values.ingress.ingressClassName }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . | quote }} + http: + paths: + {{- range $ingressPaths }} + - path: {{ . }} + pathType: Prefix + backend: + service: + name: {{ $fullName }} + port: + number: 80 + {{- end }} + {{- end }} +{{- end }} diff --git a/core/test/data/test-projects/helm-local-mode/backend/templates/service.yaml b/core/test/data/test-projects/helm-local-mode/backend/templates/service.yaml new file mode 100644 index 0000000000..d417ca11e7 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "backend.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "backend.name" . }} + helm.sh/chart: {{ include "backend.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: {{ include "backend.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} diff --git a/core/test/data/test-projects/helm-local-mode/backend/values.yaml b/core/test/data/test-projects/helm-local-mode/backend/values.yaml new file mode 100644 index 0000000000..29d3707d7f --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/backend/values.yaml @@ -0,0 +1,49 @@ +# Default values for vote. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: backend-image + tag: stable + pullPolicy: IfNotPresent + +nameOverride: "" +fullnameOverride: "" + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + ingressClassName: nginx + paths: [] + hosts: + - backend-chart.local + tls: [ ] + # - secretName: backend-chart-tls + # hosts: + # - backend-chart.local + +resources: { } + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m +# memory: 128Mi + +nodeSelector: { } + +tolerations: [ ] + +affinity: { } diff --git a/core/test/data/test-projects/helm-local-mode/frontend/.dockerignore b/core/test/data/test-projects/helm-local-mode/frontend/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/frontend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/core/test/data/test-projects/helm-local-mode/frontend/.gardenignore b/core/test/data/test-projects/helm-local-mode/frontend/.gardenignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/frontend/.gardenignore @@ -0,0 +1 @@ +node_modules diff --git a/core/test/data/test-projects/helm-local-mode/frontend/Dockerfile b/core/test/data/test-projects/helm-local-mode/frontend/Dockerfile new file mode 100644 index 0000000000..55ccabda7e --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:12.22.6-alpine + +ENV PORT=8080 +EXPOSE ${PORT} +WORKDIR /app + +COPY package.json /app +RUN npm install + +COPY . /app + +CMD ["npm", "start"] diff --git a/core/test/data/test-projects/helm-local-mode/frontend/app.js b/core/test/data/test-projects/helm-local-mode/frontend/app.js new file mode 100644 index 0000000000..a721f38b9c --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/frontend/app.js @@ -0,0 +1,27 @@ +const express = require('express'); +const request = require('request-promise') +const app = express(); + +const backendServiceEndpoint = `http://backend/hello-backend` + +app.get('/hello-frontend', (req, res) => res.send('Hello from the frontend!')); + +app.get('/call-backend', (req, res) => { + // Query the backend and return the response + request.get(backendServiceEndpoint) + .then(message => { + message = `Backend says: '${message}'` + res.json({ + message, + }) + }) + .catch(err => { + res.statusCode = 500 + res.json({ + error: err, + message: "Unable to reach service at " + backendServiceEndpoint, + }) + }); +}); + +module.exports = { app } diff --git a/core/test/data/test-projects/helm-local-mode/frontend/garden.yml b/core/test/data/test-projects/helm-local-mode/frontend/garden.yml new file mode 100644 index 0000000000..8db227776b --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/frontend/garden.yml @@ -0,0 +1,27 @@ +kind: Module +name: frontend +description: Frontend service container +type: container +services: + - name: frontend + ports: + - name: http + containerPort: 8080 + healthCheck: + httpGet: + path: /hello-frontend + port: http + ingresses: + - path: /hello-frontend + port: http + - path: /call-backend + port: http + dependencies: + - backend +tests: + - name: unit + args: [npm, test] + - name: integ + args: [npm, run, integ] + dependencies: + - frontend diff --git a/core/test/data/test-projects/helm-local-mode/frontend/main.js b/core/test/data/test-projects/helm-local-mode/frontend/main.js new file mode 100644 index 0000000000..ab66491126 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/frontend/main.js @@ -0,0 +1,3 @@ +const { app } = require('./app'); + +app.listen(process.env.PORT, '0.0.0.0', () => console.log('Frontend service started')); diff --git a/core/test/data/test-projects/helm-local-mode/frontend/package.json b/core/test/data/test-projects/helm-local-mode/frontend/package.json new file mode 100644 index 0000000000..e3da030191 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "frontend", + "version": "1.0.0", + "description": "Simple Node.js docker service", + "main": "main.js", + "scripts": { + "start": "node main.js", + "test": "echo OK", + "integ": "node_modules/mocha/bin/mocha test/integ.js" + }, + "author": "garden.io ", + "license": "ISC", + "dependencies": { + "express": "^4.16.2", + "request": "^2.83.0", + "request-promise": "^4.2.2" + }, + "devDependencies": { + "mocha": "^5.1.1", + "supertest": "^3.0.0" + } +} diff --git a/core/test/data/test-projects/helm-local-mode/frontend/test/integ.js b/core/test/data/test-projects/helm-local-mode/frontend/test/integ.js new file mode 100644 index 0000000000..8b0ba5b60f --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/frontend/test/integ.js @@ -0,0 +1,16 @@ +const supertest = require("supertest") +const { app } = require("../app") + +describe('GET /call-backend', () => { + const agent = supertest.agent(app) + + it('should respond with a message from the backend service', (done) => { + agent + .get("/call-backend") + .expect(200, { message: "Backend says: 'Hello from 1st Go!'" }) + .end((err) => { + if (err) return done(err) + done() + }) + }) +}) diff --git a/core/test/data/test-projects/helm-local-mode/garden.yml b/core/test/data/test-projects/helm-local-mode/garden.yml new file mode 100644 index 0000000000..8728be9aa0 --- /dev/null +++ b/core/test/data/test-projects/helm-local-mode/garden.yml @@ -0,0 +1,38 @@ +kind: Project +name: local-mode-helm +# defaultEnvironment: "remote" # Uncomment if you'd like the remote environment to be the default for this project. +environments: + - name: local + defaultNamespace: local-mode-helm-default + variables: + baseHostname: ${project.name}.local.app.garden + - name: remote + defaultNamespace: local-mode-helm-${var.userId} + variables: + baseHostname: ${project.name}-${var.userId}.dev-1.sys.garden + - name: testing + defaultNamespace: local-mode-helm-testing-${var.userId} + variables: + baseHostname: ${project.name}-testing-${var.userId}.dev-1.sys.garden +providers: + - name: local-kubernetes + environments: [ local ] + namespace: ${environment.namespace} + buildMode: local-docker + - name: kubernetes + environments: [ remote ] + # Replace these values as appropriate + context: ${var.remoteContext} + namespace: ${environment.namespace} + defaultHostname: ${var.userId}-local-mode.dev-1.sys.garden + buildMode: kaniko + - name: kubernetes + environments: [ testing ] + # Replace these values as appropriate + context: ${var.remoteContext} + namespace: ${environment.namespace} + defaultHostname: ${var.userId}-testing-local-mode.dev-1.sys.garden + buildMode: kaniko +variables: + userId: ${local.env.CIRCLE_BUILD_NUM || local.username} + remoteContext: gke_garden-ci_europe-west1-b_core-ci diff --git a/core/test/integ/src/plugins/kubernetes/container/deployment.ts b/core/test/integ/src/plugins/kubernetes/container/deployment.ts index 9e239cdbf6..5f472e4b28 100644 --- a/core/test/integ/src/plugins/kubernetes/container/deployment.ts +++ b/core/test/integ/src/plugins/kubernetes/container/deployment.ts @@ -17,7 +17,7 @@ import { } from "../../../../../../src/plugins/kubernetes/container/deployment" import { KubernetesPluginContext, KubernetesProvider } from "../../../../../../src/plugins/kubernetes/config" import { V1ConfigMap, V1Secret } from "@kubernetes/client-node" -import { KubernetesResource } from "../../../../../../src/plugins/kubernetes/types" +import { KubernetesResource, KubernetesWorkload } from "../../../../../../src/plugins/kubernetes/types" import { cloneDeep, keyBy } from "lodash" import { getContainerTestGarden } from "./container" import { DeployTask } from "../../../../../../src/tasks/deploy" @@ -36,8 +36,8 @@ import { PROXY_CONTAINER_SSH_TUNNEL_PORT_NAME, PROXY_CONTAINER_USER_NAME, } from "../../../../../../src/plugins/kubernetes/constants" -import stripAnsi = require("strip-ansi") import { LocalModeEnv } from "../../../../../../src/plugins/kubernetes/local-mode" +import stripAnsi = require("strip-ansi") describe("kubernetes container deployment handlers", () => { let garden: Garden @@ -68,41 +68,18 @@ describe("kubernetes container deployment handlers", () => { await init("local") }) - it("Workflow should have ssh container port when in local mode", async () => { - const service = graph.getService("local-mode") - - const { workload } = await createContainerManifests({ - ctx, - api, - service, - log: garden.log, - runtimeContext: emptyRuntimeContext, - enableDevMode: false, - enableHotReload: false, - enableLocalMode: true, // <---- - blueGreen: false, - }) - + function expectSshContainerPort(workload: KubernetesWorkload) { const appContainerSpec = workload.spec.template?.spec?.containers.find((c) => c.name === "local-mode") const workloadSshPort = appContainerSpec!.ports!.find((p) => p.name === PROXY_CONTAINER_SSH_TUNNEL_PORT_NAME) expect(workloadSshPort!.containerPort).to.eql(PROXY_CONTAINER_SSH_TUNNEL_PORT) - }) - - it("Workflow should have extra env vars for proxy container when in local mode", async () => { - const service = graph.getService("local-mode") + } - const { workload } = await createContainerManifests({ - ctx, - api, - service, - log: garden.log, - runtimeContext: emptyRuntimeContext, - enableDevMode: false, - enableHotReload: false, - enableLocalMode: true, // <---- - blueGreen: false, - }) + function expectEmptyContainerArgs(workload: KubernetesWorkload) { + const appContainerSpec = workload.spec.template?.spec?.containers.find((c) => c.name === "local-mode") + expect(appContainerSpec!.args).to.eql([]) + } + function expectContainerEnvVars(workload: KubernetesWorkload) { const appContainerSpec = workload.spec.template?.spec?.containers.find((c) => c.name === "local-mode") const env = appContainerSpec!.env! @@ -115,44 +92,160 @@ describe("kubernetes container deployment handlers", () => { const publicKeyEnvVar = env.find((v) => v.name === LocalModeEnv.GARDEN_PROXY_CONTAINER_PUBLIC_KEY)!.value expect(!!publicKeyEnvVar).to.be.true - }) + } - it("should remove liveness probes when in local mode", async () => { - const service = graph.getService("local-mode") + function expectNoProbes(workload: KubernetesWorkload) { + const appContainerSpec = workload.spec.template?.spec?.containers.find((c) => c.name === "local-mode") + expect(appContainerSpec!.livenessProbe).to.be.undefined + expect(appContainerSpec!.readinessProbe).to.be.undefined + } - const { workload } = await createContainerManifests({ - ctx, - api, - service, - log: garden.log, - runtimeContext: emptyRuntimeContext, - enableDevMode: false, - enableHotReload: false, - enableLocalMode: true, // <---- - blueGreen: false, + context("with localMode only", () => { + it("Workflow should have ssh container port when in local mode", async () => { + const service = graph.getService("local-mode") + + const { workload } = await createContainerManifests({ + ctx, + api, + service, + log: garden.log, + runtimeContext: emptyRuntimeContext, + enableDevMode: false, + enableHotReload: false, + enableLocalMode: true, // <---- + blueGreen: false, + }) + + expectSshContainerPort(workload) }) - const appContainerSpec = workload.spec.template?.spec?.containers.find((c) => c.name === "local-mode") - expect(appContainerSpec!.livenessProbe).to.be.undefined + it("Workflow should have empty container args when in local mode", async () => { + const service = graph.getService("local-mode") + + const { workload } = await createContainerManifests({ + ctx, + api, + service, + log: garden.log, + runtimeContext: emptyRuntimeContext, + enableDevMode: false, + enableHotReload: false, + enableLocalMode: true, // <---- + blueGreen: false, + }) + + expectEmptyContainerArgs(workload) + }) + + it("Workflow should have extra env vars for proxy container when in local mode", async () => { + const service = graph.getService("local-mode") + + const { workload } = await createContainerManifests({ + ctx, + api, + service, + log: garden.log, + runtimeContext: emptyRuntimeContext, + enableDevMode: false, + enableHotReload: false, + enableLocalMode: true, // <---- + blueGreen: false, + }) + + expectContainerEnvVars(workload) + }) + + it("Workflow should not have liveness and readiness probes when in local mode", async () => { + const service = graph.getService("local-mode") + + const { workload } = await createContainerManifests({ + ctx, + api, + service, + log: garden.log, + runtimeContext: emptyRuntimeContext, + enableDevMode: false, + enableHotReload: false, + enableLocalMode: true, // <---- + blueGreen: false, + }) + + expectNoProbes(workload) + }) }) - it("should remove readiness probes when in local mode", async () => { - const service = graph.getService("local-mode") + context("localMode always takes precedence over devMode", () => { + it("Workflow should have ssh container port when in local mode", async () => { + const service = graph.getService("local-mode") - const { workload } = await createContainerManifests({ - ctx, - api, - service, - log: garden.log, - runtimeContext: emptyRuntimeContext, - enableDevMode: false, - enableHotReload: false, - enableLocalMode: true, // <---- - blueGreen: false, + const { workload } = await createContainerManifests({ + ctx, + api, + service, + log: garden.log, + runtimeContext: emptyRuntimeContext, + enableDevMode: true, // <---- + enableHotReload: false, + enableLocalMode: true, // <---- + blueGreen: false, + }) + + expectSshContainerPort(workload) }) - const appContainerSpec = workload.spec.template?.spec?.containers.find((c) => c.name === "local-mode") - expect(appContainerSpec!.readinessProbe).to.be.undefined + it("Workflow should have empty container args when in local mode", async () => { + const service = graph.getService("local-mode") + + const { workload } = await createContainerManifests({ + ctx, + api, + service, + log: garden.log, + runtimeContext: emptyRuntimeContext, + enableDevMode: true, // <---- + enableHotReload: false, + enableLocalMode: true, // <---- + blueGreen: false, + }) + + expectEmptyContainerArgs(workload) + }) + + it("Workflow should have extra env vars for proxy container when in local mode", async () => { + const service = graph.getService("local-mode") + + const { workload } = await createContainerManifests({ + ctx, + api, + service, + log: garden.log, + runtimeContext: emptyRuntimeContext, + enableDevMode: true, // <---- + enableHotReload: false, + enableLocalMode: true, // <---- + blueGreen: false, + }) + + expectContainerEnvVars(workload) + }) + + it("Workflow should not have liveness and readiness probes when in local mode", async () => { + const service = graph.getService("local-mode") + + const { workload } = await createContainerManifests({ + ctx, + api, + service, + log: garden.log, + runtimeContext: emptyRuntimeContext, + enableDevMode: true, // <---- + enableHotReload: false, + enableLocalMode: true, // <---- + blueGreen: false, + }) + + expectNoProbes(workload) + }) }) }) diff --git a/core/test/integ/src/plugins/kubernetes/helm/common.ts b/core/test/integ/src/plugins/kubernetes/helm/common.ts index b5f7255757..6e9b7ea3c4 100644 --- a/core/test/integ/src/plugins/kubernetes/helm/common.ts +++ b/core/test/integ/src/plugins/kubernetes/helm/common.ts @@ -6,24 +6,23 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { TestGarden, makeTestGarden, dataDir, expectError } from "../../../../../helpers" +import { dataDir, expectError, makeTestGarden, TestGarden } from "../../../../../helpers" import { resolve } from "path" import { expect } from "chai" import { first, uniq } from "lodash" - import { containsSource, - getChartResources, + getBaseModule, getChartPath, - getReleaseName, + getChartResources, getGardenValuesPath, - getBaseModule, + getReleaseName, getValueArgs, renderTemplates, } from "../../../../../../src/plugins/kubernetes/helm/common" import { LogEntry } from "../../../../../../src/logger/log-entry" import { BuildTask } from "../../../../../../src/tasks/build" -import { deline, dedent } from "../../../../../../src/util/string" +import { dedent, deline } from "../../../../../../src/util/string" import { ConfigGraph } from "../../../../../../src/config-graph" import { KubernetesPluginContext } from "../../../../../../src/plugins/kubernetes/config" import { safeLoadAll } from "js-yaml" @@ -46,6 +45,21 @@ export async function getHelmTestGarden() { return garden } +let helmLocalModeTestGarden: TestGarden + +export async function getHelmLocalModeTestGarden() { + if (helmLocalModeTestGarden) { + return helmLocalModeTestGarden + } + + const projectRoot = resolve(dataDir, "test-projects", "helm-local-mode") + const garden = await makeTestGarden(projectRoot) + + helmLocalModeTestGarden = garden + + return garden +} + export async function buildHelmModules(garden: Garden | TestGarden, graph: ConfigGraph) { const modules = graph.getModules() const tasks = modules.map( @@ -110,6 +124,7 @@ describe("Helm common functions", () => { module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -236,6 +251,7 @@ ${expectedIngressOutput} module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -258,6 +274,7 @@ ${expectedIngressOutput} module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -427,6 +444,7 @@ ${expectedIngressOutput} module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -447,6 +465,7 @@ ${expectedIngressOutput} module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -460,6 +479,7 @@ ${expectedIngressOutput} module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -569,14 +589,14 @@ ${expectedIngressOutput} const module = graph.getModule("api") module.spec.valueFiles = [] const gardenValuesPath = getGardenValuesPath(module.buildPath) - expect(await getValueArgs(module, false, false)).to.eql(["--values", gardenValuesPath]) + expect(await getValueArgs(module, false, false, false)).to.eql(["--values", gardenValuesPath]) }) it("should add a --set flag if devMode=true", async () => { const module = graph.getModule("api") module.spec.valueFiles = [] const gardenValuesPath = getGardenValuesPath(module.buildPath) - expect(await getValueArgs(module, true, false)).to.eql([ + expect(await getValueArgs(module, true, false, false)).to.eql([ "--values", gardenValuesPath, "--set", @@ -588,7 +608,7 @@ ${expectedIngressOutput} const module = graph.getModule("api") module.spec.valueFiles = [] const gardenValuesPath = getGardenValuesPath(module.buildPath) - expect(await getValueArgs(module, false, true)).to.eql([ + expect(await getValueArgs(module, false, true, false)).to.eql([ "--values", gardenValuesPath, "--set", @@ -596,12 +616,36 @@ ${expectedIngressOutput} ]) }) + it("should add a --set flag if localMode=true", async () => { + const module = graph.getModule("api") + module.spec.valueFiles = [] + const gardenValuesPath = getGardenValuesPath(module.buildPath) + expect(await getValueArgs(module, false, false, true)).to.eql([ + "--values", + gardenValuesPath, + "--set", + "\\.garden.localMode=true", + ]) + }) + + it("localMode should always take precedence over devMode when add a --set flag", async () => { + const module = graph.getModule("api") + module.spec.valueFiles = [] + const gardenValuesPath = getGardenValuesPath(module.buildPath) + expect(await getValueArgs(module, true, false, true)).to.eql([ + "--values", + gardenValuesPath, + "--set", + "\\.garden.localMode=true", + ]) + }) + it("should return a --values arg for each valueFile configured", async () => { const module = graph.getModule("api") module.spec.valueFiles = ["foo.yaml", "bar.yaml"] const gardenValuesPath = getGardenValuesPath(module.buildPath) - expect(await getValueArgs(module, false, false)).to.eql([ + expect(await getValueArgs(module, false, false, false)).to.eql([ "--values", resolve(module.buildPath, "foo.yaml"), "--values", diff --git a/core/test/integ/src/plugins/kubernetes/helm/config.ts b/core/test/integ/src/plugins/kubernetes/helm/config.ts index d6107db639..6f01671804 100644 --- a/core/test/integ/src/plugins/kubernetes/helm/config.ts +++ b/core/test/integ/src/plugins/kubernetes/helm/config.ts @@ -10,7 +10,7 @@ import { resolve } from "path" import { expect } from "chai" import { cloneDeep, omit } from "lodash" -import { TestGarden, expectError } from "../../../../../helpers" +import { expectError, TestGarden } from "../../../../../helpers" import { PluginContext } from "../../../../../../src/plugin-context" import { dedent } from "../../../../../../src/util/string" import { defaultBuildTimeout, ModuleConfig } from "../../../../../../src/config/module" diff --git a/core/test/integ/src/plugins/kubernetes/helm/deployment.ts b/core/test/integ/src/plugins/kubernetes/helm/deployment.ts index 94f9417dcc..b425a4512e 100644 --- a/core/test/integ/src/plugins/kubernetes/helm/deployment.ts +++ b/core/test/integ/src/plugins/kubernetes/helm/deployment.ts @@ -19,13 +19,117 @@ import { } from "../../../../../../src/plugins/kubernetes/helm/status" import { getReleaseName } from "../../../../../../src/plugins/kubernetes/helm/common" import { KubeApi } from "../../../../../../src/plugins/kubernetes/api" -import { getHelmTestGarden, buildHelmModules } from "./common" +import { buildHelmModules, getHelmLocalModeTestGarden, getHelmTestGarden } from "./common" import { ConfigGraph } from "../../../../../../src/config-graph" import { isWorkload } from "../../../../../../src/plugins/kubernetes/util" import Bluebird from "bluebird" import { CloudApi } from "../../../../../../src/cloud/api" import { resolve } from "path" import { getLogger } from "../../../../../../src/logger/logger" +import { LocalModeProcessRegistry } from "../../../../../../src/plugins/kubernetes/local-mode" + +describe("deployHelmService in local-mode", () => { + let garden: TestGarden + let provider: KubernetesProvider + let ctx: KubernetesPluginContext + let graph: ConfigGraph + + before(async () => { + garden = await getHelmLocalModeTestGarden() + provider = await garden.resolveProvider(garden.log, "local-kubernetes") + ctx = await garden.getPluginContext(provider) + graph = await garden.getConfigGraph({ log: garden.log, emit: false }) + await buildHelmModules(garden, graph) + }) + + after(async () => { + // shut down local app and tunnels to avoid retrying after redeploy + LocalModeProcessRegistry.getInstance().shutdown() + const actions = await garden.getActionRouter() + await actions.deleteServices(graph, garden.log) + if (garden) { + await garden.close() + } + }) + + it("should deploy a chart with localMode enabled", async () => { + graph = await garden.getConfigGraph({ log: garden.log, emit: false }) + const service = graph.getService("backend") + + const releaseName = getReleaseName(service.module) + await deployHelmService({ + ctx, + log: garden.log, + module: service.module, + service, + force: false, + devMode: false, + hotReload: false, + localMode: true, // <----- + runtimeContext: emptyRuntimeContext, + }) + + const status = await getReleaseStatus({ + ctx, + module: service.module, + service, + releaseName, + log: garden.log, + devMode: false, + hotReload: false, + localMode: true, // <----- + }) + + expect(status.state).to.equal("ready") + expect(status.localMode).to.be.true + expect(status.devMode).to.be.false + expect(status.detail["values"][".garden"]).to.eql({ + moduleName: "backend", + projectName: garden.projectName, + version: service.version, + localMode: true, + }) + }) + + it("localMode should always take precedence over devMode", async () => { + graph = await garden.getConfigGraph({ log: garden.log, emit: false }) + const service = graph.getService("backend") + + const releaseName = getReleaseName(service.module) + await deployHelmService({ + ctx, + log: garden.log, + module: service.module, + service, + force: false, + devMode: true, // <----- + hotReload: false, + localMode: true, // <----- + runtimeContext: emptyRuntimeContext, + }) + + const status = await getReleaseStatus({ + ctx, + module: service.module, + service, + releaseName, + log: garden.log, + devMode: false, + hotReload: false, + localMode: true, // <----- + }) + + expect(status.state).to.equal("ready") + expect(status.localMode).to.be.true + expect(status.devMode).to.be.false + expect(status.detail["values"][".garden"]).to.eql({ + moduleName: "backend", + projectName: garden.projectName, + version: service.version, + localMode: true, + }) + }) +}) describe("deployHelmService", () => { let garden: TestGarden @@ -74,6 +178,7 @@ describe("deployHelmService", () => { log: garden.log, devMode: false, hotReload: false, + localMode: false, }) expect(releaseStatus.state).to.equal("ready") @@ -116,6 +221,7 @@ describe("deployHelmService", () => { log: garden.log, devMode: false, hotReload: true, // <---- + localMode: false, }) expect(status.state).to.equal("ready") @@ -152,6 +258,7 @@ describe("deployHelmService", () => { log: garden.log, devMode: true, // <----- hotReload: false, + localMode: false, }) expect(status.state).to.equal("ready") @@ -191,6 +298,7 @@ describe("deployHelmService", () => { log: garden.log, devMode: false, hotReload: false, + localMode: false, }) expect(status.state).to.equal("ready") @@ -236,6 +344,7 @@ describe("deployHelmService", () => { log: gardenWithCloudApi.log, devMode: false, hotReload: false, + localMode: false, }) expect(releaseStatus.state).to.equal("ready") @@ -285,6 +394,7 @@ describe("deployHelmService", () => { log: gardenWithCloudApi.log, devMode: false, hotReload: false, + localMode: false, }) expect(releaseStatusAfterScaleDown.state).to.equal("outdated") }) diff --git a/core/test/integ/src/plugins/kubernetes/helm/run.ts b/core/test/integ/src/plugins/kubernetes/helm/run.ts index f77c594591..71a94905c9 100644 --- a/core/test/integ/src/plugins/kubernetes/helm/run.ts +++ b/core/test/integ/src/plugins/kubernetes/helm/run.ts @@ -8,7 +8,7 @@ import { expect } from "chai" -import { TestGarden, expectError } from "../../../../../helpers" +import { expectError, TestGarden } from "../../../../../helpers" import { ConfigGraph } from "../../../../../../src/config-graph" import { getHelmTestGarden } from "./common" import { TaskTask } from "../../../../../../src/tasks/task" diff --git a/core/test/integ/src/plugins/kubernetes/helm/test.ts b/core/test/integ/src/plugins/kubernetes/helm/test.ts index ca43420086..28b6a2875d 100644 --- a/core/test/integ/src/plugins/kubernetes/helm/test.ts +++ b/core/test/integ/src/plugins/kubernetes/helm/test.ts @@ -8,14 +8,14 @@ import { expect } from "chai" -import { TestGarden, expectError } from "../../../../../helpers" +import { expectError, TestGarden } from "../../../../../helpers" import { ConfigGraph } from "../../../../../../src/config-graph" import { getHelmTestGarden } from "./common" import { TestTask } from "../../../../../../src/tasks/test" import { findByName } from "../../../../../../src/util/util" import { emptyDir, pathExists } from "fs-extra" import { join } from "path" -import { testFromModule, testFromConfig } from "../../../../../../src/types/test" +import { testFromConfig, testFromModule } from "../../../../../../src/types/test" describe("testHelmModule", () => { let garden: TestGarden diff --git a/core/test/integ/src/plugins/kubernetes/hot-reload.ts b/core/test/integ/src/plugins/kubernetes/hot-reload.ts index 008facb866..acabe5f120 100644 --- a/core/test/integ/src/plugins/kubernetes/hot-reload.ts +++ b/core/test/integ/src/plugins/kubernetes/hot-reload.ts @@ -118,6 +118,7 @@ describe("configureHotReload", () => { module, devMode: false, hotReload: true, + localMode: false, log, version: service.version, }) 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 9450a21944..d110ce0633 100644 --- a/core/test/integ/src/plugins/kubernetes/kubernetes-module/handlers.ts +++ b/core/test/integ/src/plugins/kubernetes/kubernetes-module/handlers.ts @@ -300,23 +300,76 @@ describe("kubernetes-module handlers", () => { readFromSrcDir: true, }) - // Deploy without dev mode + // Deploy without local mode await deployKubernetesService(deployParams) const res1 = await findDeployedResources(manifests, log) - // Deploy with dev mode + // Deploy with local mode await deployKubernetesService({ ...deployParams, localMode: true }) const res2 = await findDeployedResources(manifests, log) // shut down local app and tunnels to avoid retrying after redeploy LocalModeProcessRegistry.getInstance().shutdown() - // Deploy without dev mode again + // Deploy without local mode again + await deployKubernetesService(deployParams) + const res3 = await findDeployedResources(manifests, log) + + expect(res1[0].metadata.annotations![gardenAnnotationKey("local-mode")]).to.equal("false") + expect(res2[0].metadata.annotations![gardenAnnotationKey("local-mode")]).to.equal("true") + expect(res3[0].metadata.annotations![gardenAnnotationKey("local-mode")]).to.equal("false") + }) + + it("localMode should always take precedence over devMode", async () => { + const graph = await garden.getConfigGraph({ log: garden.log, emit: false }) + 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, + devMode: false, + hotReload: false, + localMode: false, + runtimeContext: emptyRuntimeContext, + } + const manifests = await getManifests({ + ctx, + api, + log, + module: service.module, + defaultNamespace: namespace, + readFromSrcDir: true, + }) + + // Deploy without local mode + await deployKubernetesService(deployParams) + const res1 = await findDeployedResources(manifests, log) + + // Deploy with local mode + await deployKubernetesService({ ...deployParams, localMode: true, devMode: true }) + const res2 = await findDeployedResources(manifests, log) + // shut down local app and tunnels to avoid retrying after redeploy + LocalModeProcessRegistry.getInstance().shutdown() + + // Deploy without local mode again await deployKubernetesService(deployParams) const res3 = await findDeployedResources(manifests, log) expect(res1[0].metadata.annotations![gardenAnnotationKey("local-mode")]).to.equal("false") expect(res2[0].metadata.annotations![gardenAnnotationKey("local-mode")]).to.equal("true") expect(res3[0].metadata.annotations![gardenAnnotationKey("local-mode")]).to.equal("false") + + expect(res1[0].metadata.annotations![gardenAnnotationKey("dev-mode")]).to.equal("false") + expect(res2[0].metadata.annotations![gardenAnnotationKey("dev-mode")]).to.equal("false") + expect(res3[0].metadata.annotations![gardenAnnotationKey("dev-mode")]).to.equal("false") }) it("should not delete previously deployed namespace resources", async () => { diff --git a/core/test/integ/src/plugins/kubernetes/run.ts b/core/test/integ/src/plugins/kubernetes/run.ts index bed761aa34..bafa90fd07 100644 --- a/core/test/integ/src/plugins/kubernetes/run.ts +++ b/core/test/integ/src/plugins/kubernetes/run.ts @@ -559,6 +559,7 @@ describe("kubernetes Pod runner functions", () => { module: helmModule, devMode: false, hotReload: false, + localMode: false, log: helmLog, version: helmModule.version.versionString, }) diff --git a/core/test/integ/src/plugins/kubernetes/util.ts b/core/test/integ/src/plugins/kubernetes/util.ts index aa4a376abd..7e5db638b6 100644 --- a/core/test/integ/src/plugins/kubernetes/util.ts +++ b/core/test/integ/src/plugins/kubernetes/util.ts @@ -228,6 +228,7 @@ describe("util", () => { module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -250,6 +251,7 @@ describe("util", () => { module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -280,6 +282,7 @@ describe("util", () => { module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -308,6 +311,7 @@ describe("util", () => { module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -336,6 +340,7 @@ describe("util", () => { module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -367,6 +372,7 @@ describe("util", () => { module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) @@ -508,6 +514,7 @@ describe("util", () => { module, devMode: false, hotReload: false, + localMode: false, log, version: module.version.versionString, }) diff --git a/docs/guides/running-service-in-local-mode.md b/docs/guides/running-service-in-local-mode.md index e8e82af10b..3e7a69efaa 100644 --- a/docs/guides/running-service-in-local-mode.md +++ b/docs/guides/running-service-in-local-mode.md @@ -14,6 +14,19 @@ By configuring a Garden service in _local mode_, one can replace a target Kubernetes service in a k8s cluster with a local service (i.e. an application running on your local machine). +_Local mode_ feature is only supported by certain module types and providers. + +### Supported module types + +* [`container`](./container-modules.md) +* [`kubernetes`](../reference/module-types/kubernetes.md) +* [`helm`](../reference/module-types/helm.md) + +### Supported providers + +* [`kubernetes`](../reference/providers/kubernetes.md) +* [`local kubernetes`](../reference/providers/local-kubernetes.md) + ## Pre-requisites Local mode uses `kubectl` port-forwarding and plain SSH port forwarding under the hood. @@ -25,8 +38,9 @@ Requirements for the local machine environment: ## Current limitations -This is the initial release of **experimental** feature. The _local mode_ feature design and implementation is still in -progress. So, there is a number of functional limitations in the first release: +This is the first release of _local mode_ feature which supports [`container`](./container-modules.md), +[`kubernetes`](../reference/module-types/kubernetes.md) and [`helm`](../reference/module-types/helm.md) module types. +There is a number of functional limitations in this release: * **Windows compatibility.** The _local mode_ is not supported natively for Windows OS. It should be used with WSL in Windows environments. @@ -34,14 +48,8 @@ progress. So, there is a number of functional limitations in the first release: first `TCP` port from the list of ports or just the first one if no `TCP` ports defined. Thus, if the service needs to talk to some data sources like databases, message brokers, etc. then all these services are assumed to be running locally. -* The _local mode_ is supported only by [`container`](./container-modules.md) - and [`kubernetes`](../reference/module-types/kubernetes.md) module types. - Support for the [`helm`](../reference/module-types/helm.md) module type will be added soon. -* Only one container can be run in local mode for each [`kubernetes`](../reference/module-types/kubernetes.md) service ( - the same will be the case for [`helm`](../reference/module-types/helm.md) services when local mode is implemented - there). -* The _local mode_ is supported by [`kubernetes`](../reference/providers/kubernetes.md) - and [`local kubernetes`](../reference/providers/local-kubernetes.md) providers. +* Only one container can be run in local mode for each [`kubernetes`](../reference/module-types/kubernetes.md) or + [`helm`](../reference/module-types/helm.md) service. * The _local mode_ leaves the proxy container deployed in the target k8s cluster after exit. The affected services must be re-deployed manually by using `garden deploy`. @@ -128,7 +136,7 @@ An example can be found in the [`local-mode project`](../../examples/local-mode) ```yaml kind: Module name: backend -type: kubernetes +type: kubernetes # this example looks the same for helm modules (i.e. with `type: helm`) localMode: localPort: 8090 command: [ "../backend-local/main" ] @@ -143,10 +151,11 @@ serviceResource: ``` A `kubernetes` module example can be found in the [`local-mode-k8s project`](../../examples/local-mode-k8s). +A `helm` module example can be found in the [`local-mode-helm project`](../../examples/local-mode-helm). ## Deploying with local mode -To deploy your services with local mode enabled, you can use the `deploy` command: +To deploy your services with _local mode_ enabled, you can use `deploy` or `dev` commands: ```sh # Deploy specific services in local mode: @@ -165,5 +174,7 @@ garden dev --local=myservice,my-other-service garden dev --local ``` -Once you quit/terminate the Garden command, all port-forwards established by the command will be stopped, but the -services (both local and remote ones) will still be left running. +_Local mode_ always runs in persistent mode, it means that the Garden process won't exit until it's terminated +explicitly. All port-forwards established by _local mode_ will be stopped on the process exit. The local application +will be stopped if it was started via the `localMode.command` configuration option. Otherwise, if the local application +was started manually, it will continue running. diff --git a/docs/reference/module-types/container.md b/docs/reference/module-types/container.md index 4bd054b246..df90cc6196 100644 --- a/docs/reference/module-types/container.md +++ b/docs/reference/module-types/container.md @@ -310,7 +310,9 @@ services: # # Health checks are disabled for services running in local mode. # - # See the [Local Mode guide](https://docs.garden.io/guides/running-service-in-local-mode.md) for more information. + # See the [Local Mode + # guide](https://github.com/garden-io/garden/blob/master/docs/guides/running-service-in-local-mode.md) for more + # information. localMode: # The working port of the local application. localPort: @@ -1407,7 +1409,7 @@ Local mode always takes the precedence over dev mode if there are any conflictin Health checks are disabled for services running in local mode. -See the [Local Mode guide](https://docs.garden.io/guides/running-service-in-local-mode.md) for more information. +See the [Local Mode guide](https://github.com/garden-io/garden/blob/master/docs/guides/running-service-in-local-mode.md) for more information. | Type | Required | | -------- | -------- | diff --git a/docs/reference/module-types/helm.md b/docs/reference/module-types/helm.md index 9825764d1b..846479100d 100644 --- a/docs/reference/module-types/helm.md +++ b/docs/reference/module-types/helm.md @@ -240,6 +240,48 @@ devMode: # workload is used. containerName: +# Configures the local application which will send and receive network requests instead of the target resource +# specified by `serviceResource`. +# +# Note that `serviceResource` must also be specified to enable local mode. Local mode configuration for the +# `kubernetes` module type relies on the `serviceResource.kind` and `serviceResource.name` fields to select a target +# Kubernetes resource. +# +# The `serviceResource.containerName` field is not used by local mode configuration. +# Note that `localMode` uses its own field `containerName` to specify a target container name explicitly. +# +# The selected container of the target Kubernetes resource will be replaced by a proxy container which runs an SSH +# server to proxy requests. +# Reverse port-forwarding will be automatically configured to route traffic to the locally deployed application and +# back. +# +# Local mode is enabled by setting the `--local` option on the `garden deploy` or `garden dev` commands. +# Local mode always takes the precedence over dev mode if there are any conflicting service names. +# +# Health checks are disabled for services running in local mode. +# +# See the [Local Mode +# guide](https://github.com/garden-io/garden/blob/master/docs/guides/running-service-in-local-mode.md) for more +# information. +localMode: + # The working port of the local application. + localPort: + + # The command to run the local application. If not present, then the local application should be started manually. + command: + + # Specifies restarting policy for the local application. By default, the local application will be restarting + # infinitely with 1000ms between attempts. + restart: + # Delay in milliseconds between the local application restart attempts. The default value is 1000ms. + delayMsec: 1000 + + # Max number of the local application restarts. Unlimited by default. + max: .inf + + # The name of the target container. The first available container will be used if this field is not defined. + containerName: + # A valid Kubernetes namespace name. Must be a valid RFC1035/RFC1123 (DNS) label (may contain lowercase letters, # numbers and dashes, must start with a letter, and cannot end with a dash) and must not be longer than 63 characters. namespace: @@ -1086,6 +1128,89 @@ Optionally specify the name of a specific container to sync to. If not specified | -------- | -------- | | `string` | No | +### `localMode` + +Configures the local application which will send and receive network requests instead of the target resource specified by `serviceResource`. + +Note that `serviceResource` must also be specified to enable local mode. Local mode configuration for the `kubernetes` module type relies on the `serviceResource.kind` and `serviceResource.name` fields to select a target Kubernetes resource. + +The `serviceResource.containerName` field is not used by local mode configuration. +Note that `localMode` uses its own field `containerName` to specify a target container name explicitly. + +The selected container of the target Kubernetes resource will be replaced by a proxy container which runs an SSH server to proxy requests. +Reverse port-forwarding will be automatically configured to route traffic to the locally deployed application and back. + +Local mode is enabled by setting the `--local` option on the `garden deploy` or `garden dev` commands. +Local mode always takes the precedence over dev mode if there are any conflicting service names. + +Health checks are disabled for services running in local mode. + +See the [Local Mode guide](https://github.com/garden-io/garden/blob/master/docs/guides/running-service-in-local-mode.md) for more information. + +| Type | Required | +| -------- | -------- | +| `object` | No | + +### `localMode.localPort` + +[localMode](#localmode) > localPort + +The working port of the local application. + +| Type | Required | +| -------- | -------- | +| `number` | No | + +### `localMode.command[]` + +[localMode](#localmode) > command + +The command to run the local application. If not present, then the local application should be started manually. + +| Type | Required | +| --------------- | -------- | +| `array[string]` | No | + +### `localMode.restart` + +[localMode](#localmode) > restart + +Specifies restarting policy for the local application. By default, the local application will be restarting infinitely with 1000ms between attempts. + +| Type | Default | Required | +| -------- | ------------------------------- | -------- | +| `object` | `{"delayMsec":1000,"max":null}` | No | + +### `localMode.restart.delayMsec` + +[localMode](#localmode) > [restart](#localmoderestart) > delayMsec + +Delay in milliseconds between the local application restart attempts. The default value is 1000ms. + +| Type | Default | Required | +| -------- | ------- | -------- | +| `number` | `1000` | No | + +### `localMode.restart.max` + +[localMode](#localmode) > [restart](#localmoderestart) > max + +Max number of the local application restarts. Unlimited by default. + +| Type | Default | Required | +| -------- | ------- | -------- | +| `number` | `null` | No | + +### `localMode.containerName` + +[localMode](#localmode) > containerName + +The name of the target container. The first available container will be used if this field is not defined. + +| Type | Required | +| -------- | -------- | +| `string` | No | + ### `namespace` A valid Kubernetes namespace name. Must be a valid RFC1035/RFC1123 (DNS) label (may contain lowercase letters, numbers and dashes, must start with a letter, and cannot end with a dash) and must not be longer than 63 characters. diff --git a/docs/reference/module-types/jib-container.md b/docs/reference/module-types/jib-container.md index 90806cbb7d..7f2950cdd6 100644 --- a/docs/reference/module-types/jib-container.md +++ b/docs/reference/module-types/jib-container.md @@ -331,7 +331,9 @@ services: # # Health checks are disabled for services running in local mode. # - # See the [Local Mode guide](https://docs.garden.io/guides/running-service-in-local-mode.md) for more information. + # See the [Local Mode + # guide](https://github.com/garden-io/garden/blob/master/docs/guides/running-service-in-local-mode.md) for more + # information. localMode: # The working port of the local application. localPort: @@ -1478,7 +1480,7 @@ Local mode always takes the precedence over dev mode if there are any conflictin Health checks are disabled for services running in local mode. -See the [Local Mode guide](https://docs.garden.io/guides/running-service-in-local-mode.md) for more information. +See the [Local Mode guide](https://github.com/garden-io/garden/blob/master/docs/guides/running-service-in-local-mode.md) for more information. | Type | Required | | -------- | -------- | diff --git a/docs/reference/module-types/kubernetes.md b/docs/reference/module-types/kubernetes.md index 26b31a3ccb..aa55c5ad1b 100644 --- a/docs/reference/module-types/kubernetes.md +++ b/docs/reference/module-types/kubernetes.md @@ -246,7 +246,9 @@ devMode: # # Health checks are disabled for services running in local mode. # -# See the [Local Mode guide](https://docs.garden.io/guides/running-service-in-local-mode.md) for more information. +# See the [Local Mode +# guide](https://github.com/garden-io/garden/blob/master/docs/guides/running-service-in-local-mode.md) for more +# information. localMode: # The working port of the local application. localPort: @@ -1038,7 +1040,7 @@ Local mode always takes the precedence over dev mode if there are any conflictin Health checks are disabled for services running in local mode. -See the [Local Mode guide](https://docs.garden.io/guides/running-service-in-local-mode.md) for more information. +See the [Local Mode guide](https://github.com/garden-io/garden/blob/master/docs/guides/running-service-in-local-mode.md) for more information. | Type | Required | | -------- | -------- | diff --git a/docs/reference/module-types/maven-container.md b/docs/reference/module-types/maven-container.md index 517bca485b..ebc798937f 100644 --- a/docs/reference/module-types/maven-container.md +++ b/docs/reference/module-types/maven-container.md @@ -310,7 +310,9 @@ services: # # Health checks are disabled for services running in local mode. # - # See the [Local Mode guide](https://docs.garden.io/guides/running-service-in-local-mode.md) for more information. + # See the [Local Mode + # guide](https://github.com/garden-io/garden/blob/master/docs/guides/running-service-in-local-mode.md) for more + # information. localMode: # The working port of the local application. localPort: @@ -1417,7 +1419,7 @@ Local mode always takes the precedence over dev mode if there are any conflictin Health checks are disabled for services running in local mode. -See the [Local Mode guide](https://docs.garden.io/guides/running-service-in-local-mode.md) for more information. +See the [Local Mode guide](https://github.com/garden-io/garden/blob/master/docs/guides/running-service-in-local-mode.md) for more information. | Type | Required | | -------- | -------- | diff --git a/examples/local-mode-helm/README.md b/examples/local-mode-helm/README.md new file mode 100644 index 0000000000..f7f7c85ef6 --- /dev/null +++ b/examples/local-mode-helm/README.md @@ -0,0 +1,45 @@ +# Local mode for `helm` modules + +A very basic demo project for Garden [local mode](../../docs/guides/running-service-in-local-mode.md) for `helm` +module type. + +This project is based on the [local-mode-k8s](../local-mode-k8s). The only difference is that this one has the `backend` +service defined as a `helm` module. The backend service can be started in the _local mode_. + +The local backend service implementation can be found in [backend-local](./backend-local). + +To build the local backend service, locate to its directory and run the following commands: + +```shell +# optional command to re-generate `main.mod` file +go mod init main +# build binary +go build -o main +# make the binary executable +chmod +x main +``` + +To start the backend in local mode, try the following commands: + +```shell +# To start a specific service in local mode +garden deploy --local=backend + +# To start both services in local mode +garden deploy --local +garden deploy --local=* +``` + +Alternatively, you can also run the local mode using the `garden dev` command: + +```shell +# To start a specific service in local mode +garden dev --local=backend + +# To start both services in local mode +garden dev --local +garden dev --local=* +``` + +To verify the result, call the corresponding ingress URLs of the `frontend` and `backend` applications. The local +backend implementation returns a different message in a response. diff --git a/examples/local-mode-helm/backend-image/.dockerignore b/examples/local-mode-helm/backend-image/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/examples/local-mode-helm/backend-image/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/examples/local-mode-helm/backend-image/.gardenignore b/examples/local-mode-helm/backend-image/.gardenignore new file mode 100644 index 0000000000..eb086d61c3 --- /dev/null +++ b/examples/local-mode-helm/backend-image/.gardenignore @@ -0,0 +1,27 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.vscode/settings.json +webserver/*server* diff --git a/examples/local-mode-helm/backend-image/.gitignore b/examples/local-mode-helm/backend-image/.gitignore new file mode 100644 index 0000000000..a365e5b54c --- /dev/null +++ b/examples/local-mode-helm/backend-image/.gitignore @@ -0,0 +1,30 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.vscode/settings.json +webserver/*server* + +# Generated files +.*.yml diff --git a/examples/local-mode-helm/backend-image/Dockerfile b/examples/local-mode-helm/backend-image/Dockerfile new file mode 100644 index 0000000000..e993313d66 --- /dev/null +++ b/examples/local-mode-helm/backend-image/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.18.3-alpine3.16 + +ENV PORT=8080 +EXPOSE ${PORT} +WORKDIR /app + +COPY main.go . + +RUN go mod init main && go build -o main . + +ENTRYPOINT ["./main"] diff --git a/examples/local-mode-helm/backend-image/garden.yml b/examples/local-mode-helm/backend-image/garden.yml new file mode 100644 index 0000000000..88abd56790 --- /dev/null +++ b/examples/local-mode-helm/backend-image/garden.yml @@ -0,0 +1,4 @@ +kind: Module +description: Image for the backend service +type: container +name: backend-image diff --git a/examples/local-mode-helm/backend-image/main.go b/examples/local-mode-helm/backend-image/main.go new file mode 100644 index 0000000000..ef8e18dd11 --- /dev/null +++ b/examples/local-mode-helm/backend-image/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { +fmt.Println("Incoming request") + fmt.Fprint(w, "Hello from Helm Chart Go!") +} + +func main() { + http.HandleFunc("/hello-backend", handler) + fmt.Println("Server running at port 8080...") + + err := http.ListenAndServe(":8080", nil) + if err != nil { + panic(err) + } +} diff --git a/examples/local-mode-helm/backend-local/.gitignore b/examples/local-mode-helm/backend-local/.gitignore new file mode 100644 index 0000000000..a365e5b54c --- /dev/null +++ b/examples/local-mode-helm/backend-local/.gitignore @@ -0,0 +1,30 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.vscode/settings.json +webserver/*server* + +# Generated files +.*.yml diff --git a/examples/local-mode-helm/backend-local/go.mod b/examples/local-mode-helm/backend-local/go.mod new file mode 100644 index 0000000000..c0ecf8b685 --- /dev/null +++ b/examples/local-mode-helm/backend-local/go.mod @@ -0,0 +1,3 @@ +module main + +go 1.17 diff --git a/examples/local-mode-helm/backend-local/main.go b/examples/local-mode-helm/backend-local/main.go new file mode 100644 index 0000000000..4d50486378 --- /dev/null +++ b/examples/local-mode-helm/backend-local/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Println("Incoming request") + fmt.Fprint(w, "Hello from Local Go!") +} + +func main() { + http.HandleFunc("/hello-backend", handler) + fmt.Println("Server running at port 8090...") + + err := http.ListenAndServe(":8090", nil) + if err != nil { + panic(err) + } +} diff --git a/examples/local-mode-helm/backend/.dockerignore b/examples/local-mode-helm/backend/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/examples/local-mode-helm/backend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/examples/local-mode-helm/backend/.gardenignore b/examples/local-mode-helm/backend/.gardenignore new file mode 100644 index 0000000000..eb086d61c3 --- /dev/null +++ b/examples/local-mode-helm/backend/.gardenignore @@ -0,0 +1,27 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.vscode/settings.json +webserver/*server* diff --git a/examples/local-mode-helm/backend/.gitignore b/examples/local-mode-helm/backend/.gitignore new file mode 100644 index 0000000000..a365e5b54c --- /dev/null +++ b/examples/local-mode-helm/backend/.gitignore @@ -0,0 +1,30 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.vscode/settings.json +webserver/*server* + +# Generated files +.*.yml diff --git a/examples/local-mode-helm/backend/.helmignore b/examples/local-mode-helm/backend/.helmignore new file mode 100644 index 0000000000..50af031725 --- /dev/null +++ b/examples/local-mode-helm/backend/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/examples/local-mode-helm/backend/Chart.yaml b/examples/local-mode-helm/backend/Chart.yaml new file mode 100644 index 0000000000..aa1d183184 --- /dev/null +++ b/examples/local-mode-helm/backend/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: backend +version: 0.1.0 diff --git a/examples/local-mode-helm/backend/garden.yml b/examples/local-mode-helm/backend/garden.yml new file mode 100644 index 0000000000..732731bb67 --- /dev/null +++ b/examples/local-mode-helm/backend/garden.yml @@ -0,0 +1,30 @@ +kind: Module +name: backend +description: Helm chart for the backend service +type: helm + +localMode: + localPort: 8090 + # starts the local application + command: [ "../backend-local/main" ] + containerName: backend + +serviceResource: + kind: Deployment + containerModule: backend-image + +build: + dependencies: [ "backend-image" ] + +values: + image: + repository: ${modules.backend-image.outputs.deployment-image-name} + tag: ${modules.backend-image.version} + ingress: + enabled: true + paths: [ "/hello-backend" ] + hosts: [ "backend.${var.baseHostname}" ] + +tasks: + - name: test + command: [ "sh", "-c", "echo task output" ] diff --git a/examples/local-mode-helm/backend/templates/NOTES.txt b/examples/local-mode-helm/backend/templates/NOTES.txt new file mode 100644 index 0000000000..2e5dad0038 --- /dev/null +++ b/examples/local-mode-helm/backend/templates/NOTES.txt @@ -0,0 +1,21 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range $.Values.ingress.paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "backend.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ include "backend.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "backend.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "backend.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/examples/local-mode-helm/backend/templates/_helpers.tpl b/examples/local-mode-helm/backend/templates/_helpers.tpl new file mode 100644 index 0000000000..8f7a7a3f80 --- /dev/null +++ b/examples/local-mode-helm/backend/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "backend.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "backend.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "backend.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/examples/local-mode-helm/backend/templates/deployment.yaml b/examples/local-mode-helm/backend/templates/deployment.yaml new file mode 100644 index 0000000000..0ce22bd056 --- /dev/null +++ b/examples/local-mode-helm/backend/templates/deployment.yaml @@ -0,0 +1,44 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: {{ include "backend.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "backend.name" . }} + helm.sh/chart: {{ include "backend.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "backend.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "backend.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: [ npm, run, serve ] + ports: + - name: http + containerPort: 8080 + protocol: TCP + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/examples/local-mode-helm/backend/templates/ingress.yaml b/examples/local-mode-helm/backend/templates/ingress.yaml new file mode 100644 index 0000000000..90e892506a --- /dev/null +++ b/examples/local-mode-helm/backend/templates/ingress.yaml @@ -0,0 +1,44 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "backend.fullname" . -}} +{{- $ingressPaths := .Values.ingress.paths -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app.kubernetes.io/name: {{ include "backend.name" . }} + helm.sh/chart: {{ include "backend.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + ingressClassName: {{ .Values.ingress.ingressClassName }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . | quote }} + http: + paths: + {{- range $ingressPaths }} + - path: {{ . }} + pathType: Prefix + backend: + service: + name: {{ $fullName }} + port: + number: 80 + {{- end }} + {{- end }} +{{- end }} diff --git a/examples/local-mode-helm/backend/templates/service.yaml b/examples/local-mode-helm/backend/templates/service.yaml new file mode 100644 index 0000000000..d417ca11e7 --- /dev/null +++ b/examples/local-mode-helm/backend/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "backend.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "backend.name" . }} + helm.sh/chart: {{ include "backend.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: {{ include "backend.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} diff --git a/examples/local-mode-helm/backend/values.yaml b/examples/local-mode-helm/backend/values.yaml new file mode 100644 index 0000000000..29d3707d7f --- /dev/null +++ b/examples/local-mode-helm/backend/values.yaml @@ -0,0 +1,49 @@ +# Default values for vote. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: backend-image + tag: stable + pullPolicy: IfNotPresent + +nameOverride: "" +fullnameOverride: "" + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + ingressClassName: nginx + paths: [] + hosts: + - backend-chart.local + tls: [ ] + # - secretName: backend-chart-tls + # hosts: + # - backend-chart.local + +resources: { } + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m +# memory: 128Mi + +nodeSelector: { } + +tolerations: [ ] + +affinity: { } diff --git a/examples/local-mode-helm/frontend/.dockerignore b/examples/local-mode-helm/frontend/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/examples/local-mode-helm/frontend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/examples/local-mode-helm/frontend/.gardenignore b/examples/local-mode-helm/frontend/.gardenignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/examples/local-mode-helm/frontend/.gardenignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/local-mode-helm/frontend/Dockerfile b/examples/local-mode-helm/frontend/Dockerfile new file mode 100644 index 0000000000..55ccabda7e --- /dev/null +++ b/examples/local-mode-helm/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:12.22.6-alpine + +ENV PORT=8080 +EXPOSE ${PORT} +WORKDIR /app + +COPY package.json /app +RUN npm install + +COPY . /app + +CMD ["npm", "start"] diff --git a/examples/local-mode-helm/frontend/app.js b/examples/local-mode-helm/frontend/app.js new file mode 100644 index 0000000000..a721f38b9c --- /dev/null +++ b/examples/local-mode-helm/frontend/app.js @@ -0,0 +1,27 @@ +const express = require('express'); +const request = require('request-promise') +const app = express(); + +const backendServiceEndpoint = `http://backend/hello-backend` + +app.get('/hello-frontend', (req, res) => res.send('Hello from the frontend!')); + +app.get('/call-backend', (req, res) => { + // Query the backend and return the response + request.get(backendServiceEndpoint) + .then(message => { + message = `Backend says: '${message}'` + res.json({ + message, + }) + }) + .catch(err => { + res.statusCode = 500 + res.json({ + error: err, + message: "Unable to reach service at " + backendServiceEndpoint, + }) + }); +}); + +module.exports = { app } diff --git a/examples/local-mode-helm/frontend/garden.yml b/examples/local-mode-helm/frontend/garden.yml new file mode 100644 index 0000000000..8db227776b --- /dev/null +++ b/examples/local-mode-helm/frontend/garden.yml @@ -0,0 +1,27 @@ +kind: Module +name: frontend +description: Frontend service container +type: container +services: + - name: frontend + ports: + - name: http + containerPort: 8080 + healthCheck: + httpGet: + path: /hello-frontend + port: http + ingresses: + - path: /hello-frontend + port: http + - path: /call-backend + port: http + dependencies: + - backend +tests: + - name: unit + args: [npm, test] + - name: integ + args: [npm, run, integ] + dependencies: + - frontend diff --git a/examples/local-mode-helm/frontend/main.js b/examples/local-mode-helm/frontend/main.js new file mode 100644 index 0000000000..ab66491126 --- /dev/null +++ b/examples/local-mode-helm/frontend/main.js @@ -0,0 +1,3 @@ +const { app } = require('./app'); + +app.listen(process.env.PORT, '0.0.0.0', () => console.log('Frontend service started')); diff --git a/examples/local-mode-helm/frontend/package.json b/examples/local-mode-helm/frontend/package.json new file mode 100644 index 0000000000..e3da030191 --- /dev/null +++ b/examples/local-mode-helm/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "frontend", + "version": "1.0.0", + "description": "Simple Node.js docker service", + "main": "main.js", + "scripts": { + "start": "node main.js", + "test": "echo OK", + "integ": "node_modules/mocha/bin/mocha test/integ.js" + }, + "author": "garden.io ", + "license": "ISC", + "dependencies": { + "express": "^4.16.2", + "request": "^2.83.0", + "request-promise": "^4.2.2" + }, + "devDependencies": { + "mocha": "^5.1.1", + "supertest": "^3.0.0" + } +} diff --git a/examples/local-mode-helm/frontend/test/integ.js b/examples/local-mode-helm/frontend/test/integ.js new file mode 100644 index 0000000000..8b0ba5b60f --- /dev/null +++ b/examples/local-mode-helm/frontend/test/integ.js @@ -0,0 +1,16 @@ +const supertest = require("supertest") +const { app } = require("../app") + +describe('GET /call-backend', () => { + const agent = supertest.agent(app) + + it('should respond with a message from the backend service', (done) => { + agent + .get("/call-backend") + .expect(200, { message: "Backend says: 'Hello from 1st Go!'" }) + .end((err) => { + if (err) return done(err) + done() + }) + }) +}) diff --git a/examples/local-mode-helm/garden.yml b/examples/local-mode-helm/garden.yml new file mode 100644 index 0000000000..8728be9aa0 --- /dev/null +++ b/examples/local-mode-helm/garden.yml @@ -0,0 +1,38 @@ +kind: Project +name: local-mode-helm +# defaultEnvironment: "remote" # Uncomment if you'd like the remote environment to be the default for this project. +environments: + - name: local + defaultNamespace: local-mode-helm-default + variables: + baseHostname: ${project.name}.local.app.garden + - name: remote + defaultNamespace: local-mode-helm-${var.userId} + variables: + baseHostname: ${project.name}-${var.userId}.dev-1.sys.garden + - name: testing + defaultNamespace: local-mode-helm-testing-${var.userId} + variables: + baseHostname: ${project.name}-testing-${var.userId}.dev-1.sys.garden +providers: + - name: local-kubernetes + environments: [ local ] + namespace: ${environment.namespace} + buildMode: local-docker + - name: kubernetes + environments: [ remote ] + # Replace these values as appropriate + context: ${var.remoteContext} + namespace: ${environment.namespace} + defaultHostname: ${var.userId}-local-mode.dev-1.sys.garden + buildMode: kaniko + - name: kubernetes + environments: [ testing ] + # Replace these values as appropriate + context: ${var.remoteContext} + namespace: ${environment.namespace} + defaultHostname: ${var.userId}-testing-local-mode.dev-1.sys.garden + buildMode: kaniko +variables: + userId: ${local.env.CIRCLE_BUILD_NUM || local.username} + remoteContext: gke_garden-ci_europe-west1-b_core-ci diff --git a/examples/local-mode-k8s/README.md b/examples/local-mode-k8s/README.md index 3f848eaf0c..4f60993b9f 100644 --- a/examples/local-mode-k8s/README.md +++ b/examples/local-mode-k8s/README.md @@ -1,4 +1,4 @@ -# Local mode +# Local mode for `kubernetes` modules A very basic demo project for Garden [local mode](../../docs/guides/running-service-in-local-mode.md) for `kubernetes` module type. diff --git a/examples/local-mode/README.md b/examples/local-mode/README.md index 2e6310cc74..8955407f8c 100644 --- a/examples/local-mode/README.md +++ b/examples/local-mode/README.md @@ -1,4 +1,4 @@ -# Local mode +# Local mode for `container` modules A very basic demo project for Garden [local mode](../../docs/guides/running-service-in-local-mode.md) for `container` module type. diff --git a/plugins/conftest/index.ts b/plugins/conftest/index.ts index 21bffe9794..1bbdffedce 100644 --- a/plugins/conftest/index.ts +++ b/plugins/conftest/index.ts @@ -231,6 +231,7 @@ export const gardenPlugin = () => module: sourceModule, devMode: false, hotReload: false, + localMode: false, log, version: sourceModule.version.versionString, })