diff --git a/.circleci/config.yml b/.circleci/config.yml index fc8db7eb03..d4aac5e26e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,6 +12,7 @@ orbs: shared-env-config: &shared-env-config GARDEN_DISABLE_VERSION_CHECK: "true" GARDEN_DISABLE_ANALYTICS: "true" + GARDEN_K8S_BUILD_SYNC_MODE: "mutagen" # Configuration for our node jobs node-config: &node-config @@ -604,7 +605,7 @@ jobs: # - Need to run with sudo to work with microk8s, because CircleCI doesn't allow us to log out # and back in to add the circleci user to the microk8s group. # - We currently don't support in-cluster building on microk8s. - GARDEN_SKIP_TESTS="cluster-docker kaniko remote-only" sudo -E npm run integ + sudo -E GARDEN_SKIP_TESTS="cluster-docker kaniko remote-only" npm run _integ - run: name: Plugin tests command: sudo -E npm run test:plugins @@ -673,7 +674,7 @@ jobs: - run: name: Integ tests # Note: We skip tests that only work for remote environments - command: cd core && yarn run integ-local + command: cd core && yarn integ-minikube - run: name: Plugin tests command: yarn run test:plugins diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65f031f0bb..471febf811 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -145,7 +145,7 @@ yarn test -- -g "taskGraph" # run only tests with descriptions matching "taskGr Integration tests are run with: ```sh -yarn run integ +yarn integ-local ``` End-to-end tests are run with: diff --git a/core/package.json b/core/package.json index 1d6c7626a0..deaec66be0 100644 --- a/core/package.json +++ b/core/package.json @@ -247,6 +247,7 @@ "typescript": "^4.3.5" }, "scripts": { + "_integ": "mocha --opts test/mocha.integ.opts", "build": "gulp pegjs", "check-package-lock": "git diff-index --quiet HEAD -- yarn.lock || (echo 'yarn.lock is dirty!' && exit 1)", "clean": "shx rm -rf build", @@ -254,10 +255,10 @@ "fix-format": "prettier --write \"{src,test}/**/*.ts\"", "lint": "tslint -p .", "migration:generate": "typeorm migration:generate --config ormconfig.js -n", - "integ": "mocha --opts test/mocha.integ.opts", - "integ-kind": "GARDEN_INTEG_TEST_MODE=local GARDEN_SKIP_TESTS=\"cluster-docker cluster-buildkit cluster-buildkit-rootless kaniko remote-only\" mocha --opts test/mocha.integ.opts", - "integ-local": "GARDEN_INTEG_TEST_MODE=local GARDEN_SKIP_TESTS=remote-only mocha --opts test/mocha.integ.opts", - "integ-remote": "GARDEN_INTEG_TEST_MODE=remote GARDEN_SKIP_TESTS=local-only mocha --opts test/mocha.integ.opts", + "integ-kind": "GARDEN_INTEG_TEST_MODE=local GARDEN_SKIP_TESTS=\"cluster-docker cluster-buildkit cluster-buildkit-rootless kaniko remote-only\" yarn run _integ", + "integ-local": "GARDEN_LOGGER_TYPE=basic GARDEN_LOG_LEVEL=debug GARDEN_INTEG_TEST_MODE=local GARDEN_SKIP_TESTS=\"cluster-docker\" yarn run _integ", + "integ-minikube": "GARDEN_INTEG_TEST_MODE=local GARDEN_SKIP_TESTS=remote-only yarn run _integ", + "integ-remote": "GARDEN_INTEG_TEST_MODE=remote GARDEN_SKIP_TESTS=local-only yarn run _integ", "e2e": "cd test/e2e && ../../../bin/garden test", "e2e-project": "node build/test/e2e/e2e-project.js", "test": "mocha --opts test/mocha.opts" diff --git a/core/src/constants.ts b/core/src/constants.ts index ddd59eaff9..8c37a2ef6f 100644 --- a/core/src/constants.ts +++ b/core/src/constants.ts @@ -60,6 +60,7 @@ export const gardenEnv = { GARDEN_ENABLE_PROFILING: env.get("GARDEN_ENABLE_PROFILING").required(false).asBool(), GARDEN_ENVIRONMENT: env.get("GARDEN_ENVIRONMENT").required(false).asString(), GARDEN_EXPERIMENTAL_BUILD_STAGE: env.get("GARDEN_EXPERIMENTAL_BUILD_STAGE").required(false).asBool(), + GARDEN_K8S_BUILD_SYNC_MODE: env.get("GARDEN_K8S_BUILD_SYNC_MODE").required(false).default("rsync").asString(), GARDEN_LEGACY_BUILD_STAGE: env.get("GARDEN_LEGACY_BUILD_STAGE").required(false).asBool(), GARDEN_LOG_LEVEL: env.get("GARDEN_LOG_LEVEL").required(false).asString(), GARDEN_LOGGER_TYPE: env.get("GARDEN_LOGGER_TYPE").required(false).asString(), diff --git a/core/src/logger/renderers.ts b/core/src/logger/renderers.ts index 98fede5985..ecbea28578 100644 --- a/core/src/logger/renderers.ts +++ b/core/src/logger/renderers.ts @@ -93,7 +93,7 @@ export function renderSymbolBasic(entry: LogEntry): string { let { symbol, status } = entry.getLatestMessage() if (symbol === "empty") { - return " " + return "  " } if (status === "active" && !symbol) { symbol = "info" @@ -106,7 +106,7 @@ export function renderSymbol(entry: LogEntry): string { const { symbol } = entry.getLatestMessage() if (symbol === "empty") { - return " " + return "  " } return symbol ? `${logSymbols[symbol]} ` : "" } diff --git a/core/src/plugin-context.ts b/core/src/plugin-context.ts index 9baeb74f21..6c05a656bd 100644 --- a/core/src/plugin-context.ts +++ b/core/src/plugin-context.ts @@ -14,6 +14,8 @@ import { joi, joiVariables, joiStringMap, DeepPrimitiveMap } from "./config/comm import { PluginTool } from "./util/ext-tools" import { ConfigContext, ContextResolveOpts } from "./config/template-contexts/base" import { resolveTemplateStrings } from "./template-string/template-string" +import { LogEntry } from "./logger/log-entry" +import { logEntrySchema } from "./types/plugin/base" type WrappedFromGarden = Pick< Garden, @@ -37,6 +39,7 @@ type ResolveTemplateStringsOpts = Omit export interface PluginContext extends WrappedFromGarden { command: CommandInfo + log: LogEntry projectSources: SourceConfig[] provider: Provider resolveTemplateStrings: (o: T, opts?: ResolveTemplateStringsOpts) => T @@ -64,6 +67,7 @@ export const pluginContextSchema = () => The absolute path of the project's Garden dir. This is the directory the contains builds, logs and other meta data. A custom path can be set when initialising the Garden class. Defaults to \`.garden\`. `), + log: logEntrySchema(), production: joi .boolean() .default(false) @@ -93,6 +97,7 @@ export async function createPluginContext( command, environmentName: garden.environmentName, gardenDirPath: garden.gardenDirPath, + log: garden.log, projectName: garden.projectName, projectRoot: garden.projectRoot, projectSources: garden.getProjectSources(), diff --git a/core/src/plugins/container/config.ts b/core/src/plugins/container/config.ts index a66a29d0c5..207a99b540 100644 --- a/core/src/plugins/container/config.ts +++ b/core/src/plugins/container/config.ts @@ -186,8 +186,18 @@ export interface DevModeSyncSpec { target: string mode: SyncMode exclude?: string[] + defaultFileMode?: number + defaultDirectoryMode?: number + defaultOwner?: number | string + defaultGroup?: number | string } +const permissionsDocs = + "See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information." + +const ownerDocs = + "Specify either an integer ID or a string name. See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more information." + const devModeSyncSchema = () => hotReloadSyncSchema().keys({ exclude: joi @@ -198,10 +208,33 @@ const devModeSyncSchema = () => mode: joi .string() .allow("one-way", "one-way-replica", "two-way") + .only() .default("one-way") .description( "The sync mode to use for the given paths. Allowed options: `one-way`, `one-way-replica`, `two-way`." ), + defaultFileMode: joi + .number() + .min(0) + .max(0o777) + .description( + "The default permission bits, specified as an octal, to set on files at the sync target. Defaults to 0600 (user read/write). " + + permissionsDocs + ), + defaultDirectoryMode: joi + .number() + .min(0) + .max(0o777) + .description( + "The default permission bits, specified as an octal, to set on directories at the sync target. Defaults to 0700 (user read/write). " + + permissionsDocs + ), + defaultOwner: joi + .alternatives(joi.number().integer(), joi.string()) + .description("Set the default owner of files and directories at the target. " + ownerDocs), + defaultGroup: joi + .alternatives(joi.number().integer(), joi.string()) + .description("Set the default group on files and directories at the target. " + ownerDocs), }) export interface ContainerDevModeSpec { @@ -222,11 +255,11 @@ export const containerDevModeSchema = () => .items(devModeSyncSchema()) .description("Specify one or more source files or directories to automatically sync with the running container."), }).description(dedent` - **EXPERIMENTAL** - Specifies which files or directories to sync to which paths inside the running containers of the service when it's in dev mode, and overrides for the container command and/or arguments. Dev mode is enabled when running the \`garden dev\` command, and by setting the \`--dev\` flag on the \`garden deploy\` command. + + See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more information. `) export type ContainerServiceConfig = ServiceConfig diff --git a/core/src/plugins/kubernetes/commands/cleanup-cluster-registry.ts b/core/src/plugins/kubernetes/commands/cleanup-cluster-registry.ts index afeb21455f..5b1d5582ea 100644 --- a/core/src/plugins/kubernetes/commands/cleanup-cluster-registry.ts +++ b/core/src/plugins/kubernetes/commands/cleanup-cluster-registry.ts @@ -309,9 +309,9 @@ async function runRegistryGarbageCollection(ctx: KubernetesPluginContext, api: K }, { retries: 3, - onFailedAttempt: async () => { - log.warn("Failed to patch deployment, retrying in 5 seconds...") - await sleep(5) + minTimeout: 2000, + onFailedAttempt: async (err) => { + log.warn(`Failed to patch deployment. ${err.retriesLeft} attempts left.`) }, } ) diff --git a/core/src/plugins/kubernetes/constants.ts b/core/src/plugins/kubernetes/constants.ts index d6bd6b6d5b..333ba65b69 100644 --- a/core/src/plugins/kubernetes/constants.ts +++ b/core/src/plugins/kubernetes/constants.ts @@ -24,7 +24,7 @@ export const inClusterRegistryHostname = "127.0.0.1:5000" export const gardenUtilDaemonDeploymentName = "garden-util-daemon" export const dockerDaemonDeploymentName = "garden-docker-daemon" -export const k8sUtilImageName = "gardendev/k8s-util:0.4.0" +export const k8sUtilImageName = "gardendev/k8s-util:0.5.1" export const dockerDaemonContainerName = "docker-daemon" export const skopeoDaemonContainerName = "util" diff --git a/core/src/plugins/kubernetes/container/build/buildkit.ts b/core/src/plugins/kubernetes/container/build/buildkit.ts index 9de36735f7..fde84cebfa 100644 --- a/core/src/plugins/kubernetes/container/build/buildkit.ts +++ b/core/src/plugins/kubernetes/container/build/buildkit.ts @@ -96,6 +96,7 @@ export const buildkitBuildHandler: BuildHandler = async (params) => { const { contextPath } = await syncToBuildSync({ ...params, + ctx: ctx as KubernetesPluginContext, api, namespace, deploymentName: buildkitDeploymentName, diff --git a/core/src/plugins/kubernetes/container/build/cluster-docker.ts b/core/src/plugins/kubernetes/container/build/cluster-docker.ts index c9823923aa..b6397cf37c 100644 --- a/core/src/plugins/kubernetes/container/build/cluster-docker.ts +++ b/core/src/plugins/kubernetes/container/build/cluster-docker.ts @@ -85,6 +85,7 @@ export const clusterDockerBuild: BuildHandler = async (params) => { const { contextPath } = await syncToBuildSync({ ...params, + ctx: ctx as KubernetesPluginContext, api, namespace: systemNamespace, deploymentName: sharedBuildSyncDeploymentName, diff --git a/core/src/plugins/kubernetes/container/build/common.ts b/core/src/plugins/kubernetes/container/build/common.ts index adfe88e636..1f1111b365 100644 --- a/core/src/plugins/kubernetes/container/build/common.ts +++ b/core/src/plugins/kubernetes/container/build/common.ts @@ -21,7 +21,7 @@ import { rsyncPortName, } from "../../constants" import { KubeApi } from "../../api" -import { KubernetesProvider } from "../../config" +import { KubernetesPluginContext, KubernetesProvider } from "../../config" import { PodRunner } from "../../run" import { PluginContext } from "../../../../plugin-context" import { resolve } from "path" @@ -34,10 +34,14 @@ import { getInClusterRegistryHostname } from "../../init" import { prepareDockerAuth } from "../../init" import chalk from "chalk" import { V1Container } from "@kubernetes/client-node" +import { gardenEnv } from "../../../../constants" +import { ensureMutagenSync, flushMutagenSync, getKubectlExecDestination, terminateMutagenSync } from "../../mutagen" +import { randomString } from "../../../../util/string" const inClusterRegistryPort = 5000 export const sharedBuildSyncDeploymentName = "garden-build-sync" +export const utilContainerName = "util" export const utilRsyncPort = 8730 export const commonSyncArgs = [ @@ -62,6 +66,7 @@ export type BuildStatusHandler = (params: GetBuildStatusParams) export type BuildHandler = (params: BuildModuleParams) => Promise interface SyncToSharedBuildSyncParams extends BuildModuleParams { + ctx: KubernetesPluginContext api: KubeApi namespace: string deploymentName: string @@ -71,39 +76,98 @@ interface SyncToSharedBuildSyncParams extends BuildModuleParams export async function syncToBuildSync(params: SyncToSharedBuildSyncParams) { const { ctx, module, log, api, namespace, deploymentName, rsyncPort } = params + // Because we're syncing to a shared volume, we need to scope by a unique ID + const contextPath = `/garden-build/${ctx.workingCopyId}/${module.name}/` + const buildSyncPod = await getRunningDeploymentPod({ api, deploymentName, namespace, }) - // Sync the build context to the remote sync service - // -> Get a tunnel to the service - log.setState("Syncing sources to cluster...") - const syncFwd = await getPortForward({ - ctx, - log, - namespace, - targetResource: `Pod/${buildSyncPod.metadata.name}`, - port: rsyncPort, - }) - // -> Run rsync - const buildRoot = resolve(module.buildPath, "..") - // The '/./' trick is used to automatically create the correct target directory with rsync: - // https://stackoverflow.com/questions/1636889/rsync-how-can-i-configure-it-to-create-target-directory-on-server - let src = normalizeLocalRsyncPath(`${buildRoot}`) + `/./${module.name}/` - const destination = `rsync://localhost:${syncFwd.localPort}/volume/${ctx.workingCopyId}/` - const syncArgs = [...commonSyncArgs, "--relative", "--delete", "--temp-dir", "/tmp", src, destination] - - log.debug(`Syncing from ${src} to ${destination}`) - // We retry a couple of times, because we may get intermittent connection issues or concurrency issues - await pRetry(() => exec("rsync", syncArgs), { - retries: 3, - minTimeout: 500, - }) + if (gardenEnv.GARDEN_K8S_BUILD_SYNC_MODE === "mutagen") { + // Sync using mutagen + const key = `build-sync-${module.name}-${randomString(8)}` + const targetPath = `/data/${ctx.workingCopyId}/${module.name}` + + // Make sure the target path exists + const runner = new PodRunner({ + ctx, + provider: ctx.provider, + api, + pod: buildSyncPod, + namespace, + }) - // Because we're syncing to a shared volume, we need to scope by a unique ID - const contextPath = `/garden-build/${ctx.workingCopyId}/${module.name}/` + await runner.exec({ + log, + command: ["sh", "-c", "mkdir -p " + targetPath], + containerName: utilContainerName, + buffer: true, + }) + + try { + const resourceName = `Deployment/${deploymentName}` + + log.debug(`Syncing from ${module.buildPath} to ${resourceName}`) + + // -> Create the sync + await ensureMutagenSync({ + log, + key, + logSection: module.name, + sourceDescription: `Module ${module.name} build path`, + targetDescription: "Build sync Pod", + config: { + alpha: module.buildPath, + beta: await getKubectlExecDestination({ + ctx, + log, + namespace, + containerName: utilContainerName, + resourceName, + targetPath, + }), + mode: "one-way-replica", + ignore: [], + }, + }) + + // -> Flush the sync once + await flushMutagenSync(log, key) + log.debug(`Sync from ${module.buildPath} to ${resourceName} completed`) + } finally { + // -> Terminate the sync + await terminateMutagenSync(log, key) + log.debug(`Sync connection terminated`) + } + } else { + // Sync the build context to the remote sync service + // -> Get a tunnel to the service + log.setState("Syncing sources to cluster...") + const syncFwd = await getPortForward({ + ctx, + log, + namespace, + targetResource: `Pod/${buildSyncPod.metadata.name}`, + port: rsyncPort, + }) + + // -> Run rsync + const buildRoot = resolve(module.buildPath, "..") + // The '/./' trick is used to automatically create the correct target directory with rsync: + // https://stackoverflow.com/questions/1636889/rsync-how-can-i-configure-it-to-create-target-directory-on-server + let src = normalizeLocalRsyncPath(`${buildRoot}`) + `/./${module.name}/` + const destination = `rsync://localhost:${syncFwd.localPort}/volume/${ctx.workingCopyId}/` + const syncArgs = [...commonSyncArgs, "--relative", "--delete", "--temp-dir", "/tmp", src, destination] + + log.debug(`Syncing from ${src} to ${destination}`) + // We retry a couple of times, because we may get intermittent connection issues or concurrency issues + await pRetry(() => exec("rsync", syncArgs), { + retries: 3, + minTimeout: 500, + }) + } return { contextPath } } @@ -288,7 +352,7 @@ function isLocalHostname(hostname: string) { export function getUtilContainer(authSecretName: string): V1Container { return { - name: "util", + name: utilContainerName, image: k8sUtilImageName, imagePullPolicy: "IfNotPresent", command: ["/rsync-server.sh"], diff --git a/core/src/plugins/kubernetes/container/build/kaniko.ts b/core/src/plugins/kubernetes/container/build/kaniko.ts index 9af3628cad..4f43764800 100644 --- a/core/src/plugins/kubernetes/container/build/kaniko.ts +++ b/core/src/plugins/kubernetes/container/build/kaniko.ts @@ -111,6 +111,7 @@ export const kanikoBuild: BuildHandler = async (params) => { await syncToBuildSync({ ...params, + ctx: ctx as KubernetesPluginContext, api, namespace: projectNamespace, deploymentName: utilDeploymentName, diff --git a/core/src/plugins/kubernetes/container/deployment.ts b/core/src/plugins/kubernetes/container/deployment.ts index e1e3982a4f..9f6cd475ba 100644 --- a/core/src/plugins/kubernetes/container/deployment.ts +++ b/core/src/plugins/kubernetes/container/deployment.ts @@ -95,11 +95,12 @@ export async function startContainerDevSync({ await startDevModeSync({ ctx, - log: log.info({ section: service.name, symbol: "info", msg: chalk.gray(`Starting sync`) }), + log, moduleRoot: service.module.path, namespace, target, spec: service.spec.devMode, + serviceName: service.name, }) } diff --git a/core/src/plugins/kubernetes/dev-mode.ts b/core/src/plugins/kubernetes/dev-mode.ts index 24959048b5..85be44771f 100644 --- a/core/src/plugins/kubernetes/dev-mode.ts +++ b/core/src/plugins/kubernetes/dev-mode.ts @@ -6,35 +6,21 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -const AsyncLock = require("async-lock") import { containerDevModeSchema, ContainerDevModeSpec } from "../container/config" import { dedent, gardenAnnotationKey } from "../../util/string" -import { fromPairs, set } from "lodash" +import { set } from "lodash" import { getResourceContainer, getResourcePodSpec } from "./util" import { HotReloadableResource } from "./hot-reload/hot-reload" import { LogEntry } from "../../logger/log-entry" import { joinWithPosix } from "../../util/fs" import chalk from "chalk" -import { pathExists, readFile, writeFile } from "fs-extra" import { PluginContext } from "../../plugin-context" -import { join } from "path" -import { safeDump, safeLoad } from "js-yaml" import { ConfigurationError } from "../../exceptions" -import { ensureMutagenDaemon, killSyncDaemon } from "./mutagen" +import { ensureMutagenSync, getKubectlExecDestination, mutagenAgentPath, mutagenConfigLock } from "./mutagen" import { joiIdentifier } from "../../config/common" import { KubernetesPluginContext } from "./config" -import { prepareConnectionOpts } from "./kubectl" -import { sleep } from "../../util/util" const syncUtilImageName = "gardendev/k8s-sync:0.1.1" -const mutagenAgentPath = "/.garden/mutagen-agent" - -interface ActiveSync { - spec: ContainerDevModeSpec -} - -const activeSyncs: { [key: string]: ActiveSync } = {} -const syncStartLock = new AsyncLock() interface ConfigureDevModeParams { target: HotReloadableResource @@ -52,14 +38,14 @@ export const kubernetesDevModeSchema = () => `Optionally specify the name of a specific container to sync to. If not specified, the first container in the workload is used.` ), }).description(dedent` - **EXPERIMENTAL** - Specifies which files or directories to sync to which paths inside the running containers of the service when it's in dev mode, and overrides for the container command and/or arguments. Note that \`serviceResource\` must also be specified to enable dev mode. Dev mode is enabled when running the \`garden dev\` command, and by setting the \`--dev\` flag on the \`garden deploy\` command. -`) + + See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more information. + `) /** * Configures the specified Deployment, DaemonSet or StatefulSet for dev mode. @@ -105,7 +91,6 @@ export function configureDevMode({ target, spec, containerName }: ConfigureDevMo const initContainer = { name: "garden-dev-init", image: syncUtilImageName, - // TODO: inject agent + SSH server command: ["/bin/sh", "-c", "cp /usr/local/bin/mutagen-agent " + mutagenAgentPath], imagePullPolicy: "IfNotPresent", volumeMounts: [gardenVolumeMount], @@ -123,17 +108,12 @@ export function configureDevMode({ target, spec, containerName }: ConfigureDevMo mainContainer.volumeMounts.push(gardenVolumeMount) } -const mutagenModeMap = { - "one-way": "one-way-safe", - "one-way-replica": "one-way-replica", - "two-way": "two-way-safe", -} - interface StartDevModeSyncParams extends ConfigureDevModeParams { ctx: PluginContext log: LogEntry moduleRoot: string namespace: string + serviceName: string } export async function startDevModeSync({ @@ -144,20 +124,16 @@ export async function startDevModeSync({ namespace, spec, target, + serviceName, }: StartDevModeSyncParams) { if (spec.sync.length === 0) { return } namespace = target.metadata.namespace || namespace const resourceName = `${target.kind}/${target.metadata.name}` - const key = `${target.kind}--${namespace}--${target.metadata.name}` - - return syncStartLock.acquire("start-sync", async () => { - // Check for already active sync - if (activeSyncs[key]) { - return activeSyncs[key] - } + const keyBase = `${target.kind}--${namespace}--${target.metadata.name}` + return mutagenConfigLock.acquire("start-sync", async () => { // Validate the target if (target.metadata.annotations?.[gardenAnnotationKey("dev-mode")] !== "true") { throw new ConfigurationError(`Resource ${resourceName} is not deployed in dev mode`, { @@ -175,120 +151,45 @@ export async function startDevModeSync({ }) } - const kubectl = ctx.tools["kubernetes.kubectl"] - const kubectlPath = await kubectl.getPath(log) + const k8sCtx = ctx - const mutagen = ctx.tools["kubernetes.mutagen"] - let dataDir = await ensureMutagenDaemon(log, mutagen) + let i = 0 - const k8sCtx = ctx + for (const s of spec.sync) { + const key = `${keyBase}-${i}` - // Configure Mutagen with all the syncs - const syncConfigs = fromPairs( - spec.sync.map((s, i) => { - const connectionOpts = prepareConnectionOpts({ - provider: k8sCtx.provider, - namespace, - }) - const command = [ - kubectlPath, - "exec", - "-i", - ...connectionOpts, - "--container", - containerName, - `${target.kind}/${target.metadata.name}`, - "--", - mutagenAgentPath, - "synchronizer", - ] - - const syncConfig = { - alpha: joinWithPosix(moduleRoot, s.source), - beta: `exec:'${command.join(" ")}':${s.target}`, - mode: mutagenModeMap[s.mode], - ignore: { - paths: s.exclude || [], - }, - } - - log.info( - chalk.gray( - `→ Syncing ${chalk.white(s.source)} to ${chalk.white(s.target)} in ${chalk.white(resourceName)} (${s.mode})` - ) - ) - - return [`${key}-${i}`, syncConfig] + const alpha = joinWithPosix(moduleRoot, s.source) + const beta = await getKubectlExecDestination({ + ctx: k8sCtx, + log, + namespace, + containerName, + resourceName: `${target.kind}/${target.metadata.name}`, + targetPath: s.target, }) - ) - let config: any = { - sync: {}, - } + const sourceDescription = chalk.white(s.source) + const targetDescription = `${chalk.white(s.target)} in ${chalk.white(resourceName)}` + const description = `${sourceDescription} to ${targetDescription}` + + ctx.log.info({ symbol: "info", section: serviceName, msg: chalk.gray(`Syncing ${description} (${s.mode})`) }) + + await ensureMutagenSync({ + // Prefer to log to the main view instead of the handler log context + log: ctx.log, + key, + logSection: serviceName, + sourceDescription, + targetDescription, + config: { + alpha, + beta, + mode: s.mode, + ignore: s.exclude || [], + }, + }) - // Commit the configuration to the Mutagen daemon - - let loops = 0 - const maxRetries = 10 - while (true) { - // When deploying Helm services with dev mode, sometimes the first deployment (e.g. when the namespace has just - // been created) will fail because the daemon can't connect to the pod (despite the call to `waitForResources`) - // in the Helm deployment handler. - // - // In addition, when several services are deployed with dev mode, we occasionally need to retry restarting the - // mutagen daemon after the first try (we need to restart it to reload the updated mutagen project, which - // needs to contain representations of all the sync specs). - // - // When either of those happens, we simply kill the mutagen daemon, wait, and try again (up to a fixed number - // of retries). - // - // TODO: Maybe there's a more elegant way to do this? - try { - const configPath = join(dataDir, "mutagen.yml") - - if (await pathExists(configPath)) { - config = safeLoad((await readFile(configPath)).toString()) - } - - config.sync = { ...config.sync, ...syncConfigs } - - await writeFile(configPath, safeDump(config)) - - await mutagen.exec({ - cwd: dataDir, - args: ["project", "start"], - log, - env: { - MUTAGEN_DATA_DIRECTORY: dataDir, - }, - }) - break - } catch (err) { - const unableToConnect = err.message.match(/unable to connect to beta/) - const alreadyRunning = err.message.match(/project already running/) - if ((unableToConnect || alreadyRunning) && loops < 10) { - loops += 1 - if (unableToConnect) { - log.setState(`Synchronization daemon failed to connect, retrying (attempt ${loops}/${maxRetries})...`) - } else if (alreadyRunning) { - log.setState(`Project already running, retrying (attempt ${loops}/${maxRetries})...`) - } - await killSyncDaemon(false) - await sleep(2000 + loops * 500) - dataDir = await ensureMutagenDaemon(log, mutagen) - } else { - log.setError(err.message) - throw err - } - } + i++ } - log.setSuccess("Synchronization daemon started") - - // TODO: Attach to Mutagen GRPC to poll for sync updates - - const sync: ActiveSync = { spec } - activeSyncs[key] = sync - - return sync }) } diff --git a/core/src/plugins/kubernetes/helm/deployment.ts b/core/src/plugins/kubernetes/helm/deployment.ts index 83de90fd0e..51e42e2e1f 100644 --- a/core/src/plugins/kubernetes/helm/deployment.ts +++ b/core/src/plugins/kubernetes/helm/deployment.ts @@ -22,7 +22,6 @@ import { getServiceResource, getServiceResourceSpec } from "../util" import { getModuleNamespace, getModuleNamespaceStatus } from "../namespace" import { getHotReloadSpec, configureHotReload, getHotReloadContainerName } from "../hot-reload/helpers" import { configureDevMode, startDevModeSync } from "../dev-mode" -import chalk from "chalk" export async function deployHelmService({ ctx, @@ -136,12 +135,13 @@ export async function deployHelmService({ if (devMode && service.spec.devMode && serviceResource && serviceResourceSpec) { await startDevModeSync({ ctx, - log: log.info({ section: service.name, symbol: "info", msg: chalk.gray(`Starting sync`) }), + log, moduleRoot: service.sourceModule.path, namespace: serviceResource.metadata.namespace || namespace, target: serviceResource, spec: service.spec.devMode, containerName: service.spec.devMode.containerName, + serviceName: service.name, }) } diff --git a/core/src/plugins/kubernetes/helm/status.ts b/core/src/plugins/kubernetes/helm/status.ts index 010da24f1d..aedda49e87 100644 --- a/core/src/plugins/kubernetes/helm/status.ts +++ b/core/src/plugins/kubernetes/helm/status.ts @@ -17,7 +17,6 @@ import { getForwardablePorts } from "../port-forward" import { KubernetesServerResource } from "../types" import { getModuleNamespace, getModuleNamespaceStatus } from "../namespace" import { getServiceResource, getServiceResourceSpec } from "../util" -import chalk from "chalk" import { startDevModeSync } from "../dev-mode" import { gardenAnnotationKey } from "../../../util/string" @@ -97,12 +96,13 @@ export async function getServiceStatus({ await startDevModeSync({ ctx, - log: log.info({ section: service.name, symbol: "info", msg: chalk.gray(`Starting sync`) }), + log, moduleRoot: service.sourceModule.path, namespace, target, spec: service.spec.devMode, containerName: service.spec.devMode.containerName, + serviceName: service.name, }) } else { state = "outdated" diff --git a/core/src/plugins/kubernetes/kubernetes-module/handlers.ts b/core/src/plugins/kubernetes/kubernetes-module/handlers.ts index e2a17d0957..d7f61f44b6 100644 --- a/core/src/plugins/kubernetes/kubernetes-module/handlers.ts +++ b/core/src/plugins/kubernetes/kubernetes-module/handlers.ts @@ -7,7 +7,6 @@ */ import Bluebird from "bluebird" -import chalk from "chalk" import { cloneDeep, partition, set, uniq } from "lodash" import { LogEntry } from "../../../logger/log-entry" import { NamespaceStatus } from "../../../types/plugin/base" @@ -111,12 +110,13 @@ export async function getKubernetesServiceStatus({ if (target.metadata.annotations?.[gardenAnnotationKey("dev-mode")] === "true") { await startDevModeSync({ ctx, - log: log.info({ section: service.name, symbol: "info", msg: chalk.gray(`Starting sync`) }), + log, moduleRoot: service.sourceModule.path, namespace, target, spec: service.spec.devMode, containerName: service.spec.devMode.containerName, + serviceName: service.name, }) } else { state = "outdated" @@ -205,12 +205,13 @@ export async function deployKubernetesService( if (devMode && service.spec.devMode && target) { await startDevModeSync({ ctx, - log: log.info({ section: service.name, symbol: "info", msg: chalk.gray(`Starting sync`) }), + log, moduleRoot: service.sourceModule.path, namespace, target, spec: service.spec.devMode, containerName: service.spec.devMode.containerName, + serviceName: service.name, }) } diff --git a/core/src/plugins/kubernetes/mutagen.ts b/core/src/plugins/kubernetes/mutagen.ts index ae99ae2fb5..d8fe5c94d1 100644 --- a/core/src/plugins/kubernetes/mutagen.ts +++ b/core/src/plugins/kubernetes/mutagen.ts @@ -6,21 +6,66 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +const AsyncLock = require("async-lock") import chalk from "chalk" import { join } from "path" -import { mkdirp, remove, removeSync } from "fs-extra" +import { mkdirp, pathExists, remove, removeSync } from "fs-extra" import respawn from "respawn" import { LogEntry } from "../../logger/log-entry" import { PluginToolSpec } from "../../types/plugin/tools" import { PluginTool } from "../../util/ext-tools" import { makeTempDir, TempDirectory } from "../../util/fs" -import { registerCleanupFunction } from "../../util/util" +import { registerCleanupFunction, sleep } from "../../util/util" +import { GardenBaseError } from "../../exceptions" +import { prepareConnectionOpts } from "./kubectl" +import { KubernetesPluginContext } from "./config" +import pRetry from "p-retry" const maxRestarts = 10 +const monitorDelay = 2000 +const mutagenLogSection = "" +export const mutagenAgentPath = "/.garden/mutagen-agent" let daemonProc: any let mutagenTmp: TempDirectory +export const mutagenModeMap = { + "one-way": "one-way-safe", + "one-way-replica": "one-way-replica", + "two-way": "two-way-safe", +} + +interface SyncConfig { + alpha: string + beta: string + mode: keyof typeof mutagenModeMap + ignore: string[] + defaultOwner?: number | string + defaultGroup?: number | string + defaultFileMode?: number + defaultDirectoryMode?: number +} + +interface ActiveSync { + sourceDescription: string + targetDescription: string + logSection: string + sourceConnected: boolean + targetConnected: boolean + config: SyncConfig + lastProblems: string[] + lastSyncCount: number + mutagenParameters: string[] +} + +let activeSyncs: { [key: string]: ActiveSync } = {} + +export class MutagenError extends GardenBaseError { + type = "mutagen" +} + +export const mutagenConfigLock = new AsyncLock() + registerCleanupFunction("kill-sync-daaemon", () => { stopDaemonProc() mutagenTmp && removeSync(mutagenTmp.path) @@ -35,89 +80,442 @@ export async function killSyncDaemon(clearTmpDir = true) { if (clearTmpDir) { mutagenTmp && (await remove(mutagenTmp.path)) } + + activeSyncs = {} } function stopDaemonProc() { try { daemonProc?.stop() + daemonProc = undefined } catch {} } -export async function ensureMutagenDaemon(log: LogEntry, mutagen: PluginTool) { - if (!mutagenTmp) { - mutagenTmp = await makeTempDir() - } +export async function ensureMutagenDaemon(log: LogEntry) { + return mutagenConfigLock.acquire("start-daemon", async () => { + if (!mutagenTmp) { + mutagenTmp = await makeTempDir() + } - const dataDir = mutagenTmp.path + const dataDir = mutagenTmp.path - if (daemonProc && daemonProc.status === "running") { - return dataDir - } + if (daemonProc && daemonProc.status === "running") { + return dataDir + } - const mutagenPath = await mutagen.getPath(log) + const mutagenPath = await mutagen.getPath(log) - await mkdirp(dataDir) + await mkdirp(dataDir) - daemonProc = respawn([mutagenPath, "daemon", "run"], { - cwd: dataDir, - name: "mutagen", - env: { - MUTAGEN_DATA_DIRECTORY: dataDir, - }, - maxRestarts, - sleep: 1000, - kill: 500, - stdio: "pipe", - fork: false, - }) + daemonProc = respawn([mutagenPath, "daemon", "run"], { + cwd: dataDir, + name: "mutagen", + env: { + MUTAGEN_DATA_DIRECTORY: dataDir, + MUTAGEN_LOG_LEVEL: "debug", + }, + maxRestarts, + sleep: 3000, + kill: 500, + stdio: "pipe", + fork: false, + }) - const crashMessage = `Synchronization daemon has crashed ${maxRestarts} times. Aborting.` + const crashMessage = `Synchronization daemon has crashed ${maxRestarts} times. Aborting.` - daemonProc.on("crash", () => { - log.root.warn(chalk.yellow(crashMessage)) - }) + daemonProc.on("crash", () => { + log.warn(chalk.yellow(crashMessage)) + }) + + daemonProc.on("exit", (code: number) => { + if (code !== 0) { + log.warn({ + symbol: "empty", + section: mutagenLogSection, + msg: chalk.yellow(`Synchronization daemon exited with code ${code}.`), + }) + // Remove the lock file + const daemonLockFilePath = join(dataDir, "daemon", "daemon.lock") + removeSync(daemonLockFilePath) + } + }) + + const handleOutput = (data: Buffer) => { + const str = data.toString().trim() + // This is a little dumb, to detect if the log line starts with a timestamp, but ya know... + // it'll basically work for the next 979 years :P. + const msg = chalk.gray(str.startsWith("2") ? str.split(" ").slice(3).join(" ") : str) + if (msg.includes("Unable")) { + log.warn({ symbol: "warning", section: mutagenLogSection, msg }) + } else { + log.silly({ symbol: "empty", section: mutagenLogSection, msg }) + } + } - // TODO: Reenable. This log line creates too much noise when daemon restarts are required during deployments - // (see dev-mode.ts). - // daemonProc.on("exit", (code: number) => { - // if (code !== 0) { - // log.root.warn(chalk.yellow(`Synchronization daemon exited with code ${code}.`)) - // } - // }) + daemonProc.on("stdout", handleOutput) + daemonProc.on("stderr", handleOutput) - daemonProc.on("stdout", (data: Buffer) => { - log.silly({ section: "mutagen", msg: data.toString() }) - }) - daemonProc.on("stderr", (data: Buffer) => { - log.silly({ section: "mutagen", msg: data.toString() }) + return new Promise((resolve, reject) => { + let resolved = false + + daemonProc.on("spawn", () => { + if (resolved) { + log.info({ + symbol: "empty", + section: mutagenLogSection, + msg: chalk.green("Synchronization daemon re-started"), + }) + } + }) + + daemonProc.once("spawn", () => { + setTimeout(() => { + if (daemonProc?.status === "running") { + resolved = true + resolve(dataDir) + } + }, 500) + }) + + daemonProc.once("crash", () => { + if (!resolved) { + reject(crashMessage) + } + }) + + daemonProc.start() + }) }) +} + +export async function execMutagenCommand(log: LogEntry, args: string[]) { + let dataDir = await ensureMutagenDaemon(log) + + let loops = 0 + const maxRetries = 10 - return new Promise((resolve, reject) => { - let resolved = false + while (true) { + // Keep trying for a bit in case we can't connect to the daemon + try { + const res = await mutagen.exec({ + cwd: dataDir, + args, + log, + env: { + MUTAGEN_DATA_DIRECTORY: dataDir, + }, + }) + startMutagenMonitor(log) + return res + } catch (err) { + const unableToConnect = err.message.match(/unable to connect to daemon/) - daemonProc.once("spawn", () => { - if (resolved) { - return + if (unableToConnect && loops < 10) { + loops += 1 + if (unableToConnect) { + log.warn({ + symbol: "empty", + section: mutagenLogSection, + msg: chalk.gray(`Could not connect to sync daemon, retrying (attempt ${loops}/${maxRetries})...`), + }) + } + await killSyncDaemon(false) + await sleep(2000 + loops * 500) + dataDir = await ensureMutagenDaemon(log) + } else { + throw err } + } + } +} - setTimeout(() => { - if (daemonProc.status === "running") { - resolved = true - resolve(dataDir) +interface ScanProblem { + path: string + error: string +} + +interface ConflictChange { + path: string + new?: { + kind: number + digest?: string + target?: string + executable?: boolean + } +} + +interface SyncConflict { + root: string + alphaChanges?: ConflictChange[] + betaChanges?: ConflictChange[] +} + +interface SyncListEntry { + session: { + identifier: string + version: number + creationTime: { + seconds: number + nanos: number + } + creatingVersionMinor: number + alpha: { + path: string + } + beta: { + path: string + } + configuration: { + synchronizationMode: number + } + configurationAlpha: any + configurationBeta: any + name: string + paused?: boolean + } + status?: number + alphaConnected?: boolean + betaConnected?: boolean + alphaScanProblems?: ScanProblem[] + betaScanProblems?: ScanProblem[] + successfulSynchronizationCycles?: number + conflicts?: SyncConflict[] + excludedConflicts?: number +} + +let monitorInterval: NodeJS.Timeout + +function checkMutagen(log: LogEntry) { + getActiveMutagenSyncs(log) + .then((syncs) => { + for (const sync of syncs) { + const problems: string[] = [ + ...(sync.alphaScanProblems || []).map((p) => `Error scanning sync source, path ${p.path}: ${p.error}`), + ...(sync.betaScanProblems || []).map((p) => `Error scanning sync target, path ${p.path}: ${p.error}`), + ] + + const activeSync = activeSyncs[sync.session.name] + if (!activeSync) { + continue } - }, 500) - }) - daemonProc.once("crash", () => { - if (!resolved) { - reject(crashMessage) + const { logSection: section } = activeSync + + for (const problem of problems) { + if (!activeSync.lastProblems.includes(problem)) { + log.warn({ symbol: "warning", section, msg: chalk.yellow(problem) }) + } + } + + if (sync.alphaConnected && !activeSync.sourceConnected) { + log.info({ + symbol: "info", + section, + msg: chalk.gray(`Connected to sync source ${activeSync.sourceDescription}`), + }) + activeSync.sourceConnected = true + } + + if (sync.betaConnected && !activeSync.targetConnected) { + log.info({ + symbol: "success", + section, + msg: chalk.gray(`Connected to sync target ${activeSync.targetDescription}`), + }) + activeSync.targetConnected = true + } + + const syncCount = sync.successfulSynchronizationCycles || 0 + const description = `from ${activeSync.sourceDescription} to ${activeSync.targetDescription}` + + if (syncCount > activeSync.lastSyncCount) { + if (activeSync.lastSyncCount === 0) { + log.info({ + symbol: "success", + section, + msg: chalk.gray(`Completed initial sync ${description}`), + }) + } else { + log.info({ symbol: "info", section, msg: chalk.gray(`Synchronized ${description}`) }) + } + } + + activeSync.lastProblems = problems + activeSync.lastSyncCount = syncCount } }) + .catch((err) => { + log.debug({ + symbol: "warning", + section: mutagenLogSection, + msg: "Unable to get status from sync daemon: " + err.message, + }) + }) +} + +export function startMutagenMonitor(log: LogEntry) { + if (!monitorInterval) { + monitorInterval = setInterval(() => checkMutagen(log), monitorDelay) + } +} + +/** + * List the currently active syncs in the mutagen daemon. + */ +export async function getActiveMutagenSyncs(log: LogEntry): Promise { + const res = await execMutagenCommand(log, ["sync", "list", "--output=json", "--auto-start=false"]) + + // TODO: validate further + let parsed: any = {} + + try { + parsed = JSON.parse(res.stdout) + } catch (err) { + throw new MutagenError(`Could not parse response from mutagen sync list: ${res.stdout}`, { res }) + } + + if (!parsed.sessions) { + throw new MutagenError(`Unexpected response from mutagen sync list: ${parsed}`, { res, parsed }) + } + + return parsed.sessions +} + +/** + * Make sure the specified sync is active. Does nothing if a sync is already active with the same key. + * (When configuration changes, the whole daemon is reset). + */ +export async function ensureMutagenSync({ + log, + logSection, + key, + sourceDescription, + targetDescription, + config, +}: { + log: LogEntry + logSection: string + key: string + sourceDescription: string + targetDescription: string + config: SyncConfig +}) { + if (activeSyncs[key]) { + return + } + + return mutagenConfigLock.acquire("configure", async () => { + const active = await getActiveMutagenSyncs(log) + const existing = active.find((s: any) => s.name === key) + + if (!existing) { + const { alpha, beta, ignore, mode, defaultOwner, defaultGroup, defaultDirectoryMode, defaultFileMode } = config + + const ignoreFlags = ignore.flatMap((i) => ["-i", i]) + const syncMode = mutagenModeMap[mode] + const params = [alpha, beta, "--name", key, "--auto-start=false", "--sync-mode", syncMode, ...ignoreFlags] - daemonProc.start() + if (defaultOwner) { + params.push("--default-owner", defaultOwner.toString()) + } + if (defaultGroup) { + params.push("--default-group", defaultGroup.toString()) + } + if (defaultFileMode) { + params.push("--default-file-mode", modeToString(defaultFileMode)) + } + if (defaultDirectoryMode) { + params.push("--default-directory-mode", modeToString(defaultDirectoryMode)) + } + + // Might need to retry + await pRetry(() => execMutagenCommand(log, ["sync", "create", ...params]), { + retries: 5, + minTimeout: 1000, + onFailedAttempt: (err) => { + log.warn( + `Failed to start sync from ${sourceDescription} to ${targetDescription}. ${err.retriesLeft} attempts left.` + ) + }, + }) + + activeSyncs[key] = { + sourceDescription, + targetDescription, + logSection, + sourceConnected: await isValidLocalPath(config.alpha), + targetConnected: await isValidLocalPath(config.beta), + config, + lastProblems: [], + lastSyncCount: 0, + mutagenParameters: params, + } + } }) } +/** + * Remove the specified sync (by name) from the sync daemon. + */ +export async function terminateMutagenSync(log: LogEntry, key: string) { + return mutagenConfigLock.acquire("configure", async () => { + try { + await execMutagenCommand(log, ["sync", "delete", key, "--auto-start=false"]) + delete activeSyncs[key] + } catch (err) { + // Ignore other errors, which should mean the sync wasn't found + if (err.message.includes("unable to connect to daemon")) { + throw err + } + } + }) +} +/** + * Remove the specified sync (by name) from the sync daemon. + */ +export async function flushMutagenSync(log: LogEntry, key: string) { + await execMutagenCommand(log, ["sync", "flush", key, "--auto-start=false"]) +} + +export async function getKubectlExecDestination({ + ctx, + log, + namespace, + containerName, + resourceName, + targetPath, +}: { + ctx: KubernetesPluginContext + log: LogEntry + namespace: string + containerName: string + resourceName: string + targetPath: string +}) { + const kubectl = ctx.tools["kubernetes.kubectl"] + const kubectlPath = await kubectl.getPath(log) + + const connectionOpts = prepareConnectionOpts({ + provider: ctx.provider, + namespace, + }) + + const command = [ + kubectlPath, + "exec", + "-i", + ...connectionOpts, + "--container", + containerName, + resourceName, + "--", + mutagenAgentPath, + "synchronizer", + ] + + return `exec:'${command.join(" ")}':${targetPath}` +} + export const mutagenCliSpec: PluginToolSpec = { name: "mutagen", description: "The mutagen synchronization tool.", @@ -128,8 +526,8 @@ export const mutagenCliSpec: PluginToolSpec = { platform: "darwin", architecture: "amd64", url: - "https://github.com/garden-io/mutagen/releases/download/v0.12.0-garden-alpha1/mutagen_darwin_amd64_v0.12.0-beta2.tar.gz", - sha256: "de45df05e6eddb4ad9672da8240ca43302cd901d8d58b627ca8a26c94d1f24bf", + "https://github.com/garden-io/mutagen/releases/download/v0.12.0-garden-alpha2/mutagen_darwin_amd64_v0.12.0-beta3.tar.gz", + sha256: "e31cebb5c4cbd1a1320e56b111416389e9eed911233b40c93801547c1eec0563", extract: { format: "tar", targetPath: "mutagen", @@ -139,8 +537,8 @@ export const mutagenCliSpec: PluginToolSpec = { platform: "linux", architecture: "amd64", url: - "https://github.com/garden-io/mutagen/releases/download/v0.12.0-garden-alpha1/mutagen_linux_amd64_v0.12.0-beta2.tar.gz", - sha256: "b423dc5fd396b174a53dcf348ccc229169976a3ea390a2ce4ba9d7a3d13c2619", + "https://github.com/garden-io/mutagen/releases/download/v0.12.0-garden-alpha2/mutagen_linux_amd64_v0.12.0-beta3.tar.gz", + sha256: "09a0dbccbbd784324707ba12002a6bc90395f0cd73daab83d6cda7432b4973f3", extract: { format: "tar", targetPath: "mutagen", @@ -150,8 +548,8 @@ export const mutagenCliSpec: PluginToolSpec = { platform: "windows", architecture: "amd64", url: - "https://github.com/garden-io/mutagen/releases/download/v0.12.0-garden-alpha1/mutagen_windows_amd64_v0.12.0-beta2.tar.gz", - sha256: "f526221a1078cbad48115b0d02c7e2c0118f2b3d46778d19717c654c7096f242", + "https://github.com/garden-io/mutagen/releases/download/v0.12.0-garden-alpha2/mutagen_windows_amd64_v0.12.0-beta3.zip", + sha256: "9482646380a443b72aa38b3569c71c73d91ddde7c57a10de3d48b0b727cb8bff", extract: { format: "tar", targetPath: "mutagen.exe", @@ -159,3 +557,19 @@ export const mutagenCliSpec: PluginToolSpec = { }, ], } + +const mutagen = new PluginTool(mutagenCliSpec) + +/** + * Returns true if the given sync point is a filesystem path that exists. + */ +async function isValidLocalPath(syncPoint: string) { + return pathExists(syncPoint) +} + +/** + * Converts an octal permission mask to string. + */ +function modeToString(mode: number) { + return "0" + mode.toString(8) +} diff --git a/core/test/data/test-projects/kubernetes-module/api-image/app.py b/core/test/data/test-projects/kubernetes-module/api-image/app.py index 6db2b765ee..20fe7a752f 100644 --- a/core/test/data/test-projects/kubernetes-module/api-image/app.py +++ b/core/test/data/test-projects/kubernetes-module/api-image/app.py @@ -18,6 +18,13 @@ def get_redis(): g.redis = Redis(host="redis-master", db=0, socket_timeout=5) return g.redis +@app.route("/api", methods=['GET']) +def hello(): + return app.response_class( + response="Hello, I am the api service", + status=200, + ) + @app.route("/vote/", methods=['POST','GET']) def vote(): voter_id = hex(random.getrandbits(64))[2:-1] diff --git a/core/test/data/test-projects/kubernetes-module/with-source-module/garden.yml b/core/test/data/test-projects/kubernetes-module/with-source-module/garden.yml index 2c5c18b295..59af14d9d8 100644 --- a/core/test/data/test-projects/kubernetes-module/with-source-module/garden.yml +++ b/core/test/data/test-projects/kubernetes-module/with-source-module/garden.yml @@ -2,10 +2,12 @@ kind: Module type: kubernetes name: with-source-module description: Simple Kubernetes module with minimum config that has a container source module + devMode: sync: - - source: "*" - target: /app + - source: "*" + target: /app + manifests: - apiVersion: apps/v1 kind: Deployment @@ -29,11 +31,17 @@ manifests: args: [python, app.py] ports: - containerPort: 80 + readinessProbe: + httpGet: + path: /api + port: 80 + serviceResource: kind: Deployment name: api-deployment containerModule: api-image containerName: api + tests: - name: with-source-module-test command: [sh, -c, "echo ok"] diff --git a/core/test/integ/src/plugins/kubernetes/commands/pull-image.ts b/core/test/integ/src/plugins/kubernetes/commands/pull-image.ts index 0d088b8e5e..ae3b01d196 100644 --- a/core/test/integ/src/plugins/kubernetes/commands/pull-image.ts +++ b/core/test/integ/src/plugins/kubernetes/commands/pull-image.ts @@ -63,11 +63,11 @@ describe("pull-image plugin command", () => { expect(imageHash.stdout.trim()).to.equal("ok") } - grouped("cluster-docker", "remote-only").context("using an external cluster registry", () => { + grouped("kaniko", "remote-only").context("using an external cluster registry with kaniko", () => { let module: GardenModule before(async () => { - await init("cluster-docker-remote-registry") + await init("kaniko-remote-registry") module = graph.getModule("remote-registry-test") @@ -88,11 +88,11 @@ describe("pull-image plugin command", () => { }) }) - grouped("cluster-docker").context("using the in cluster registry", () => { + grouped("kaniko").context("using the in cluster registry with kaniko", () => { let module: GardenModule before(async () => { - await init("cluster-docker") + await init("kaniko") module = graph.getModule("simple-service") @@ -113,7 +113,7 @@ describe("pull-image plugin command", () => { }) }) - grouped("cluster-buildkit", "remote-only").context("using an external cluster registry", () => { + grouped("cluster-buildkit", "remote-only").context("using an external cluster registry with buildkit", () => { let module: GardenModule before(async () => { @@ -138,7 +138,7 @@ describe("pull-image plugin command", () => { }) }) - grouped("cluster-buildkit").context("using the in cluster registry", () => { + grouped("cluster-buildkit").context("using the in cluster registry with buildkit", () => { let module: GardenModule before(async () => { diff --git a/core/test/integ/src/plugins/kubernetes/container/build/build.ts b/core/test/integ/src/plugins/kubernetes/container/build/build.ts index 4fac18dbf6..99eca3adb6 100644 --- a/core/test/integ/src/plugins/kubernetes/container/build/build.ts +++ b/core/test/integ/src/plugins/kubernetes/container/build/build.ts @@ -340,6 +340,7 @@ describe("kubernetes build flow", () => { module, }), (err) => { + console.log(err) expect(err.message).to.include("pull access denied") } ) diff --git a/core/test/integ/src/plugins/kubernetes/container/container.ts b/core/test/integ/src/plugins/kubernetes/container/container.ts index d31d469e40..c5f1bc867b 100644 --- a/core/test/integ/src/plugins/kubernetes/container/container.ts +++ b/core/test/integ/src/plugins/kubernetes/container/container.ts @@ -27,7 +27,7 @@ import { clusterInit } from "../../../../../../src/plugins/kubernetes/commands/c import { testFromConfig, testFromModule } from "../../../../../../src/types/test" const root = getDataDir("test-projects", "container") -const defaultEnvironment = process.env.GARDEN_INTEG_TEST_MODE === "remote" ? "cluster-docker" : "local" +const defaultEnvironment = process.env.GARDEN_INTEG_TEST_MODE === "remote" ? "kaniko" : "local" const initializedEnvs: string[] = [] let localInstance: Garden 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 d439a5185e..39d2b92c62 100644 --- a/core/test/integ/src/plugins/kubernetes/kubernetes-module/handlers.ts +++ b/core/test/integ/src/plugins/kubernetes/kubernetes-module/handlers.ts @@ -247,7 +247,7 @@ describe("kubernetes-module handlers", () => { readFromSrcDir: true, }) - // // Deploy without dev mode + // Deploy without dev mode await deployKubernetesService(deployParams) const res1 = await findDeployedResources(manifests, log) @@ -255,7 +255,7 @@ describe("kubernetes-module handlers", () => { await deployKubernetesService({ ...deployParams, devMode: true }) const res2 = await findDeployedResources(manifests, log) - // // Deploy without hot reload again + // Deploy without dev mode again await deployKubernetesService(deployParams) const res3 = await findDeployedResources(manifests, log) diff --git a/docs/README.md b/docs/README.md index 325432b04b..072bb817af 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,6 +30,7 @@ ## 🌿 Guides * [Cloud Provider Set-up](./guides/cloud-provider-setup.md) +* [Code Synchronization (Dev Mode)](./guides/code-synchronization-dev-mode.md) * [Container Modules](./guides/container-modules.md) * [Helm Charts](./guides/using-helm-charts.md) * [Hot Reload](./guides/hot-reload.md) diff --git a/docs/guides/code-synchronization-dev-mode.md b/docs/guides/code-synchronization-dev-mode.md new file mode 100644 index 0000000000..7829790bc5 --- /dev/null +++ b/docs/guides/code-synchronization-dev-mode.md @@ -0,0 +1,105 @@ +# Code Synchronization (Dev Mode) + +You can synchronize your code (and other files) to and from running containers using _dev mode_. + +Dev mode works similarly to the older [hot reloading functionality](./hot-reload.md), but is much faster and more reliable. It also supports bidirectional syncing, which enables you to sync new/changed files from your containers to your local machine. + +This new sync mode uses [Mutagen](https://mutagen.io/) under the hood. Garden automatically takes care of fetching Mutagen, so you don't need to install any dependencies yourself to make use of dev mode. + +Dev mode sync is not affected by includes/excludes, which makes it more flexible than hot reloading. For example, you can use it to sync your `build`/`dist` directory into your container while running local, incremental builds (without having to remove those directories from your ignorefiles). + +{% hint style="warning" %} +Please make sure to specify any paths that should not be synced, by setting the `exclude` field on each configured sync! Otherwise you may end up syncing large directories and even run into application errors. +{% endhint %} + +## Configuration + +To configure a service for dev mode, add `devMode` to your module/service configuration to specify your sync targets: + +### Configuring dev mode for `container` modules + +```yaml +kind: Module +description: Node greeting service +name: node-service +type: container +services: + - name: node-service + args: [npm, start] + devMode: + command: [npm, run, dev] # Overrides the container's default when the service is deployed in dev mode + sync: + # Source/target configuration for dev mode is the same as for hot reloading. + - target: /app + # Make sure to specify any paths that should not be synced! + exclude: [node_modules] + # You can use several sync specs for the same service. + - source: /tmp/somedir + target: /somedir + ... +``` + +### Configuring dev mode for `kubernetes` and `helm` modules + +```yaml +kind: Module +type: kubernetes # this example looks the same for helm modules (i.e. with `type: helm`) +name: node-service +# For `kubernetes` and `helm` modules, the `devMode` field is located at the top level. +devMode: + command: [npm, run, dev] + sync: + - target: /app + - source: /tmp/somedir + target: /somedir +serviceResource: + kind: Deployment + name: node-service-deployment + containerModule: node-service-image + containerName: node-service +... +``` + +## Deploying with dev mode + +To deploy your services with dev mode enabled, you can use the `deploy` or `dev` commands: + +```sh +# Deploy specific services in dev mode: +garden deploy --dev myservice +garden deploy --dev myservice,my-other-service + +# Deploy all applicable services in dev mode: +garden deploy --dev=* + +# The dev command deploys services in dev mode by default: +garden dev myservice +``` + +## Permissions and ownership + +In certain cases you may need to set a specific owner/group or permission bits on the synced files and directories at the target. + +To do this, you can set a few options on each sync: + +```yaml +kind: Module +description: Node greeting service +name: node-service +type: container +services: + - name: node-service + args: [npm, start] + devMode: + command: [npm, run, dev] + sync: + - target: /app + exclude: [node_modules] + defaultOwner: 1000 # <- set an integer user ID or a string name + defaultGroup: 1000 # <- set an integer group ID or a string name + defaultFileMode: 0666 # <- set the permission bits (as octals) for synced files + defaultDirectoryMode: 0777 # <- set the permission bits (as octals) for synced directories + ... +``` + +These options are passed directly to Mutagen. For more information, please see the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions). diff --git a/docs/guides/hot-reload.md b/docs/guides/hot-reload.md index a532655e08..aadeea4106 100644 --- a/docs/guides/hot-reload.md +++ b/docs/guides/hot-reload.md @@ -1,7 +1,7 @@ # Hot Reload -{% hint style="info" %} -Check out the section below on the brand-new, faster (and still experimental) dev mode—which includes bidirectional sync! +{% hint style="warning" %} +We've now deprecated the older "hot reload" mechanism and replaced it with the new _dev mode_. See the [Code Synchronization guide](./code-synchronization-dev-mode.md) for details. {% endhint %} When the `local-kubernetes` or `kubernetes` provider is used, `container` modules can be configured to hot-reload their running services when the module's sources change (i.e. without redeploying). In essence, hot-reloading copies syncs files into the appropriate running containers (local or remote) when code is changed by the user, and optionally runs a post-sync command inside the container. @@ -57,7 +57,7 @@ Lastly, `hotReloadArgs` specifies the arguments to use to run the container (whe ## Adding a `postSyncCommand` -A `postSyncCommand` can also be added to a module's hot reload configuration. This command is executed inside the running container during each hot reload, after syncing is completed (as the name suggests). +A `postSyncCommand` can also be added to a module's hot reload configuration. This command is executed inside the running container during each hot reload, after syncing is completed (as the name suggests). Following is a snippet from the `hot-reload-post-sync-command` example project. Here, a `postSyncCommand` is used to `touch` a file, updating its modification time. This way, `nodemon` only has to watch one file to keep the running application up to date. See the `hot-reload-post-sync-command` example for more details and a fuller discussion. @@ -76,65 +76,3 @@ services: hotReloadArgs: [npm, run, dev] # Runs modemon main.js --watch hotreloadfile ... ``` - -## Dev mode (experimental) - -Dev mode works similarly to hot reloading, but is much faster and more reliable. It also supports bidirectional syncing, which enables you to sync new/changed files from your containers to your local machine. - -This new sync mode uses [Mutagen](https://mutagen.io/) under the hood. Garden automatically takes care of fetching Mutagen, so you don't need to install any dependencies yourself to make use of dev mode. - -Dev mode sync is not affected by includes/excludes, which makes it more flexible than hot reloading. For example, you can use it to sync your `build`/`dist` directory into your container while running local, incremental builds (without having to remove those directories from your ignorefiles). - -Eventually, the plan is to deprecate hot reloading in favor of dev mode. - -Dev mode opens up exciting, productive new ways to set up your inner dev loop with Garden. Happy hacking! - -Dev mode is currently supported for `container`, `kubernetes` and `helm` modules. - -To configure a service for dev mode, add `devMode` to your module/service configuration: - -### `container` module example -```yaml -kind: Module -description: Node greeting service -name: node-service -type: container -services: - - name: node-service - args: [npm, start] - devMode: - command: [npm, run, dev] # Overrides the container's default when the service is deployed in dev mode - sync: - # Source/target configuration for dev mode is the same as for hot reloading. - - target: /app - # You can use several sync specs for the same service. - - source: /tmp/somedir - target: /somedir - ... -``` - -### Configuring dev mode for `kubernetes` and `helm` modules -```yaml -kind: Module -type: kubernetes # this example looks the same for helm modules (i.e. with `type: helm`) -name: node-service -# For `kubernetes` and `helm` modules, the `devMode` field is located at the top level. -devMode: - command: [npm, run, dev] - sync: - - target: /app - - source: /tmp/somedir - target: /somedir -serviceResource: - kind: Deployment - name: node-service-deployment - containerModule: node-service-image - containerName: node-service -... -``` -To deploy your services with dev mode enabled, you can use the `deploy` or `dev` commands: -``` -garden deploy --dev myservice -garden deploy --dev myservice,my-other-service -garden dev myservice # the dev command deploys services in dev mode by default -``` diff --git a/docs/reference/module-types/container.md b/docs/reference/module-types/container.md index 704502bbaa..f9521774a0 100644 --- a/docs/reference/module-types/container.md +++ b/docs/reference/module-types/container.md @@ -221,13 +221,14 @@ services: # all providers. daemon: false - # **EXPERIMENTAL** - # # Specifies which files or directories to sync to which paths inside the running containers of the service when # it's in dev mode, and overrides for the container command and/or arguments. # # Dev mode is enabled when running the `garden dev` command, and by setting the `--dev` flag on the `garden # deploy` command. + # + # See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more + # information. devMode: # Override the default container arguments when in dev mode. args: @@ -251,6 +252,28 @@ services: # The sync mode to use for the given paths. Allowed options: `one-way`, `one-way-replica`, `two-way`. mode: one-way + # The default permission bits, specified as an octal, to set on files at the sync target. Defaults to 0600 + # (user read/write). See the [Mutagen + # docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + defaultFileMode: + + # The default permission bits, specified as an octal, to set on directories at the sync target. Defaults to + # 0700 (user read/write). See the [Mutagen + # docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + defaultDirectoryMode: + + # Set the default owner of files and directories at the target. Specify either an integer ID or a string + # name. See the [Mutagen + # docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more + # information. + defaultOwner: + + # Set the default group on files and directories at the target. Specify either an integer ID or a string + # name. See the [Mutagen + # docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more + # information. + defaultGroup: + # List of ingress endpoints that the service exposes. ingresses: - # Annotations to attach to the ingress (Note: May not be applicable to all providers) @@ -1089,12 +1112,12 @@ Whether to run the service as a daemon (to ensure exactly one instance runs per [services](#services) > devMode -**EXPERIMENTAL** - Specifies which files or directories to sync to which paths inside the running containers of the service when it's in dev mode, and overrides for the container command and/or arguments. Dev mode is enabled when running the `garden dev` command, and by setting the `--dev` flag on the `garden deploy` command. +See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more information. + | Type | Required | | -------- | -------- | | `object` | No | @@ -1197,9 +1220,49 @@ services: The sync mode to use for the given paths. Allowed options: `one-way`, `one-way-replica`, `two-way`. -| Type | Default | Required | -| -------- | ----------- | -------- | -| `string` | `"one-way"` | No | +| Type | Allowed Values | Default | Required | +| -------- | --------------------------------------- | ----------- | -------- | +| `string` | "one-way", "one-way-replica", "two-way" | `"one-way"` | Yes | + +### `services[].devMode.sync[].defaultFileMode` + +[services](#services) > [devMode](#servicesdevmode) > [sync](#servicesdevmodesync) > defaultFileMode + +The default permission bits, specified as an octal, to set on files at the sync target. Defaults to 0600 (user read/write). See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + +| Type | Required | +| -------- | -------- | +| `number` | No | + +### `services[].devMode.sync[].defaultDirectoryMode` + +[services](#services) > [devMode](#servicesdevmode) > [sync](#servicesdevmodesync) > defaultDirectoryMode + +The default permission bits, specified as an octal, to set on directories at the sync target. Defaults to 0700 (user read/write). See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + +| Type | Required | +| -------- | -------- | +| `number` | No | + +### `services[].devMode.sync[].defaultOwner` + +[services](#services) > [devMode](#servicesdevmode) > [sync](#servicesdevmodesync) > defaultOwner + +Set the default owner of files and directories at the target. Specify either an integer ID or a string name. See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more information. + +| Type | Required | +| ----------------- | -------- | +| `number | string` | No | + +### `services[].devMode.sync[].defaultGroup` + +[services](#services) > [devMode](#servicesdevmode) > [sync](#servicesdevmodesync) > defaultGroup + +Set the default group on files and directories at the target. Specify either an integer ID or a string name. See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more information. + +| Type | Required | +| ----------------- | -------- | +| `number | string` | No | ### `services[].ingresses[]` diff --git a/docs/reference/module-types/helm.md b/docs/reference/module-types/helm.md index 6c758578a6..cae598627b 100644 --- a/docs/reference/module-types/helm.md +++ b/docs/reference/module-types/helm.md @@ -154,8 +154,6 @@ chartPath: . # List of names of services that should be deployed before this chart. dependencies: [] -# **EXPERIMENTAL** -# # Specifies which files or directories to sync to which paths inside the running containers of the service when it's # in dev mode, and overrides for the container command and/or arguments. # @@ -163,6 +161,9 @@ dependencies: [] # # Dev mode is enabled when running the `garden dev` command, and by setting the `--dev` flag on the `garden deploy` # command. +# +# See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more +# information. devMode: # Override the default container arguments when in dev mode. args: @@ -186,6 +187,26 @@ devMode: # The sync mode to use for the given paths. Allowed options: `one-way`, `one-way-replica`, `two-way`. mode: one-way + # The default permission bits, specified as an octal, to set on files at the sync target. Defaults to 0600 (user + # read/write). See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) + # for more information. + defaultFileMode: + + # The default permission bits, specified as an octal, to set on directories at the sync target. Defaults to 0700 + # (user read/write). See the [Mutagen + # docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + defaultDirectoryMode: + + # Set the default owner of files and directories at the target. Specify either an integer ID or a string name. + # See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for + # more information. + defaultOwner: + + # Set the default group on files and directories at the target. Specify either an integer ID or a string name. + # See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for + # more information. + defaultGroup: + # Optionally specify the name of a specific container to sync to. If not specified, the first container in the # workload is used. containerName: @@ -807,14 +828,14 @@ List of names of services that should be deployed before this chart. ### `devMode` -**EXPERIMENTAL** - Specifies which files or directories to sync to which paths inside the running containers of the service when it's in dev mode, and overrides for the container command and/or arguments. Note that `serviceResource` must also be specified to enable dev mode. Dev mode is enabled when running the `garden dev` command, and by setting the `--dev` flag on the `garden deploy` command. +See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more information. + | Type | Required | | -------- | -------- | | `object` | No | @@ -914,9 +935,49 @@ devMode: The sync mode to use for the given paths. Allowed options: `one-way`, `one-way-replica`, `two-way`. -| Type | Default | Required | -| -------- | ----------- | -------- | -| `string` | `"one-way"` | No | +| Type | Allowed Values | Default | Required | +| -------- | --------------------------------------- | ----------- | -------- | +| `string` | "one-way", "one-way-replica", "two-way" | `"one-way"` | Yes | + +### `devMode.sync[].defaultFileMode` + +[devMode](#devmode) > [sync](#devmodesync) > defaultFileMode + +The default permission bits, specified as an octal, to set on files at the sync target. Defaults to 0600 (user read/write). See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + +| Type | Required | +| -------- | -------- | +| `number` | No | + +### `devMode.sync[].defaultDirectoryMode` + +[devMode](#devmode) > [sync](#devmodesync) > defaultDirectoryMode + +The default permission bits, specified as an octal, to set on directories at the sync target. Defaults to 0700 (user read/write). See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + +| Type | Required | +| -------- | -------- | +| `number` | No | + +### `devMode.sync[].defaultOwner` + +[devMode](#devmode) > [sync](#devmodesync) > defaultOwner + +Set the default owner of files and directories at the target. Specify either an integer ID or a string name. See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more information. + +| Type | Required | +| ----------------- | -------- | +| `number | string` | No | + +### `devMode.sync[].defaultGroup` + +[devMode](#devmode) > [sync](#devmodesync) > defaultGroup + +Set the default group on files and directories at the target. Specify either an integer ID or a string name. See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more information. + +| Type | Required | +| ----------------- | -------- | +| `number | string` | No | ### `devMode.containerName` diff --git a/docs/reference/module-types/kubernetes.md b/docs/reference/module-types/kubernetes.md index a0e9ba1eaa..f60ade3c83 100644 --- a/docs/reference/module-types/kubernetes.md +++ b/docs/reference/module-types/kubernetes.md @@ -161,8 +161,6 @@ files: [] # numbers and dashes, must start with a letter, and cannot end with a dash) and must not be longer than 63 characters. namespace: -# **EXPERIMENTAL** -# # Specifies which files or directories to sync to which paths inside the running containers of the service when it's # in dev mode, and overrides for the container command and/or arguments. # @@ -170,6 +168,9 @@ namespace: # # Dev mode is enabled when running the `garden dev` command, and by setting the `--dev` flag on the `garden deploy` # command. +# +# See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more +# information. devMode: # Override the default container arguments when in dev mode. args: @@ -193,6 +194,26 @@ devMode: # The sync mode to use for the given paths. Allowed options: `one-way`, `one-way-replica`, `two-way`. mode: one-way + # The default permission bits, specified as an octal, to set on files at the sync target. Defaults to 0600 (user + # read/write). See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) + # for more information. + defaultFileMode: + + # The default permission bits, specified as an octal, to set on directories at the sync target. Defaults to 0700 + # (user read/write). See the [Mutagen + # docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + defaultDirectoryMode: + + # Set the default owner of files and directories at the target. Specify either an integer ID or a string name. + # See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for + # more information. + defaultOwner: + + # Set the default group on files and directories at the target. Specify either an integer ID or a string name. + # See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for + # more information. + defaultGroup: + # Optionally specify the name of a specific container to sync to. If not specified, the first container in the # workload is used. containerName: @@ -752,14 +773,14 @@ A valid Kubernetes namespace name. Must be a valid RFC1035/RFC1123 (DNS) label ( ### `devMode` -**EXPERIMENTAL** - Specifies which files or directories to sync to which paths inside the running containers of the service when it's in dev mode, and overrides for the container command and/or arguments. Note that `serviceResource` must also be specified to enable dev mode. Dev mode is enabled when running the `garden dev` command, and by setting the `--dev` flag on the `garden deploy` command. +See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more information. + | Type | Required | | -------- | -------- | | `object` | No | @@ -859,9 +880,49 @@ devMode: The sync mode to use for the given paths. Allowed options: `one-way`, `one-way-replica`, `two-way`. -| Type | Default | Required | -| -------- | ----------- | -------- | -| `string` | `"one-way"` | No | +| Type | Allowed Values | Default | Required | +| -------- | --------------------------------------- | ----------- | -------- | +| `string` | "one-way", "one-way-replica", "two-way" | `"one-way"` | Yes | + +### `devMode.sync[].defaultFileMode` + +[devMode](#devmode) > [sync](#devmodesync) > defaultFileMode + +The default permission bits, specified as an octal, to set on files at the sync target. Defaults to 0600 (user read/write). See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + +| Type | Required | +| -------- | -------- | +| `number` | No | + +### `devMode.sync[].defaultDirectoryMode` + +[devMode](#devmode) > [sync](#devmodesync) > defaultDirectoryMode + +The default permission bits, specified as an octal, to set on directories at the sync target. Defaults to 0700 (user read/write). See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + +| Type | Required | +| -------- | -------- | +| `number` | No | + +### `devMode.sync[].defaultOwner` + +[devMode](#devmode) > [sync](#devmodesync) > defaultOwner + +Set the default owner of files and directories at the target. Specify either an integer ID or a string name. See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more information. + +| Type | Required | +| ----------------- | -------- | +| `number | string` | No | + +### `devMode.sync[].defaultGroup` + +[devMode](#devmode) > [sync](#devmodesync) > defaultGroup + +Set the default group on files and directories at the target. Specify either an integer ID or a string name. See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more information. + +| Type | Required | +| ----------------- | -------- | +| `number | string` | No | ### `devMode.containerName` diff --git a/docs/reference/module-types/maven-container.md b/docs/reference/module-types/maven-container.md index 21f67dfba4..9db7b9ed03 100644 --- a/docs/reference/module-types/maven-container.md +++ b/docs/reference/module-types/maven-container.md @@ -219,13 +219,14 @@ services: # all providers. daemon: false - # **EXPERIMENTAL** - # # Specifies which files or directories to sync to which paths inside the running containers of the service when # it's in dev mode, and overrides for the container command and/or arguments. # # Dev mode is enabled when running the `garden dev` command, and by setting the `--dev` flag on the `garden # deploy` command. + # + # See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more + # information. devMode: # Override the default container arguments when in dev mode. args: @@ -249,6 +250,28 @@ services: # The sync mode to use for the given paths. Allowed options: `one-way`, `one-way-replica`, `two-way`. mode: one-way + # The default permission bits, specified as an octal, to set on files at the sync target. Defaults to 0600 + # (user read/write). See the [Mutagen + # docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + defaultFileMode: + + # The default permission bits, specified as an octal, to set on directories at the sync target. Defaults to + # 0700 (user read/write). See the [Mutagen + # docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + defaultDirectoryMode: + + # Set the default owner of files and directories at the target. Specify either an integer ID or a string + # name. See the [Mutagen + # docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more + # information. + defaultOwner: + + # Set the default group on files and directories at the target. Specify either an integer ID or a string + # name. See the [Mutagen + # docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more + # information. + defaultGroup: + # List of ingress endpoints that the service exposes. ingresses: - # Annotations to attach to the ingress (Note: May not be applicable to all providers) @@ -1097,12 +1120,12 @@ Whether to run the service as a daemon (to ensure exactly one instance runs per [services](#services) > devMode -**EXPERIMENTAL** - Specifies which files or directories to sync to which paths inside the running containers of the service when it's in dev mode, and overrides for the container command and/or arguments. Dev mode is enabled when running the `garden dev` command, and by setting the `--dev` flag on the `garden deploy` command. +See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more information. + | Type | Required | | -------- | -------- | | `object` | No | @@ -1205,9 +1228,49 @@ services: The sync mode to use for the given paths. Allowed options: `one-way`, `one-way-replica`, `two-way`. -| Type | Default | Required | -| -------- | ----------- | -------- | -| `string` | `"one-way"` | No | +| Type | Allowed Values | Default | Required | +| -------- | --------------------------------------- | ----------- | -------- | +| `string` | "one-way", "one-way-replica", "two-way" | `"one-way"` | Yes | + +### `services[].devMode.sync[].defaultFileMode` + +[services](#services) > [devMode](#servicesdevmode) > [sync](#servicesdevmodesync) > defaultFileMode + +The default permission bits, specified as an octal, to set on files at the sync target. Defaults to 0600 (user read/write). See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + +| Type | Required | +| -------- | -------- | +| `number` | No | + +### `services[].devMode.sync[].defaultDirectoryMode` + +[services](#services) > [devMode](#servicesdevmode) > [sync](#servicesdevmodesync) > defaultDirectoryMode + +The default permission bits, specified as an octal, to set on directories at the sync target. Defaults to 0700 (user read/write). See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information. + +| Type | Required | +| -------- | -------- | +| `number` | No | + +### `services[].devMode.sync[].defaultOwner` + +[services](#services) > [devMode](#servicesdevmode) > [sync](#servicesdevmodesync) > defaultOwner + +Set the default owner of files and directories at the target. Specify either an integer ID or a string name. See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more information. + +| Type | Required | +| ----------------- | -------- | +| `number | string` | No | + +### `services[].devMode.sync[].defaultGroup` + +[services](#services) > [devMode](#servicesdevmode) > [sync](#servicesdevmodesync) > defaultGroup + +Set the default group on files and directories at the target. Specify either an integer ID or a string name. See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more information. + +| Type | Required | +| ----------------- | -------- | +| `number | string` | No | ### `services[].ingresses[]` diff --git a/images/k8s-sync/Dockerfile b/images/k8s-sync/Dockerfile index 0ea6686488..5fb34dc739 100644 --- a/images/k8s-sync/Dockerfile +++ b/images/k8s-sync/Dockerfile @@ -3,6 +3,9 @@ FROM alpine:3.13.5 RUN apk add --no-cache curl # Get mutagen agent -RUN curl -fsSL "https://github.com/garden-io/mutagen/releases/download/v0.12.0-garden-alpha1/mutagen-agent-linux-amd64.tar.gz" \ +RUN curl -fsSL "https://github.com/garden-io/mutagen/releases/download/v0.12.0-garden-alpha2/agent_linux_amd64_v0.12.0-beta3.tar.gz" \ | tar xz --to-stdout mutagen-agent \ - > /usr/local/bin/mutagen-agent && chmod +x /usr/local/bin/mutagen-agent + > /usr/local/bin/mutagen-agent && \ + chmod +x /usr/local/bin/mutagen-agent && \ + mkdir -p /.garden && \ + ln -s /usr/local/bin/mutagen-agent /.garden/mutagen-agent diff --git a/images/k8s-sync/garden.yml b/images/k8s-sync/garden.yml index 145c3b7039..6c92044ccc 100644 --- a/images/k8s-sync/garden.yml +++ b/images/k8s-sync/garden.yml @@ -2,5 +2,5 @@ kind: Module type: container name: k8s-sync description: Used by the kubernetes provider for dev mode sync setup -image: gardendev/k8s-sync:0.1.1 +image: gardendev/k8s-sync:0.1.2 dockerfile: Dockerfile diff --git a/images/k8s-util/Dockerfile b/images/k8s-util/Dockerfile index 904bd143cb..0e6d9cd813 100644 --- a/images/k8s-util/Dockerfile +++ b/images/k8s-util/Dockerfile @@ -1,6 +1,7 @@ -FROM alpine:3.13.5 +ARG BASE_IMAGE +FROM ${BASE_IMAGE} -RUN apk add --no-cache curl rsync skopeo +RUN apk add --no-cache rsync skopeo RUN cd /usr/local/bin && \ curl -O https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com/0.4.0/linux-amd64/docker-credential-ecr-login && \ chmod +x docker-credential-ecr-login diff --git a/images/k8s-util/garden.yml b/images/k8s-util/garden.yml index cc4a0257c2..13f5bcfc86 100644 --- a/images/k8s-util/garden.yml +++ b/images/k8s-util/garden.yml @@ -2,5 +2,9 @@ kind: Module type: container name: k8s-util description: Used by the kubernetes provider for build-related activities -image: gardendev/k8s-util:0.4.0 +image: gardendev/k8s-util:0.5.1 dockerfile: Dockerfile +build: + dependencies: [k8s-sync] +buildArgs: + BASE_IMAGE: ${modules.k8s-sync.outputs.local-image-id}