diff --git a/core/src/plugins/kubernetes/container/deployment.ts b/core/src/plugins/kubernetes/container/deployment.ts index cd1a8cebd3..649fed8788 100644 --- a/core/src/plugins/kubernetes/container/deployment.ts +++ b/core/src/plugins/kubernetes/container/deployment.ts @@ -237,6 +237,7 @@ export async function createWorkloadManifest({ production, }: CreateDeploymentParams): Promise { const spec = action.getSpec() + const mode = action.mode() let configuredReplicas = spec.replicas || DEFAULT_MINIMUM_REPLICAS diff --git a/core/src/plugins/kubernetes/kubernetes-type/handlers.ts b/core/src/plugins/kubernetes/kubernetes-type/handlers.ts index bd0dd2eabb..8738d0c555 100644 --- a/core/src/plugins/kubernetes/kubernetes-type/handlers.ts +++ b/core/src/plugins/kubernetes/kubernetes-type/handlers.ts @@ -598,7 +598,7 @@ async function configureSpecialModesForManifests({ log, }) } else if (mode === "sync" && spec.sync && !isEmpty(spec.sync)) { - // The "sync-mode" annotation is set in `configureDevMode`. + // The "sync-mode" annotation is already set. return configureSyncMode({ ctx, log, diff --git a/core/src/plugins/kubernetes/sync.ts b/core/src/plugins/kubernetes/sync.ts index a376038fd0..b851f5cad9 100644 --- a/core/src/plugins/kubernetes/sync.ts +++ b/core/src/plugins/kubernetes/sync.ts @@ -303,7 +303,7 @@ export async function configureSyncMode({ spec, }: { ctx: PluginContext - log: Log + log: ActionLog provider: KubernetesProvider action: Resolved defaultTarget: KubernetesTargetResourceSpec | undefined @@ -461,10 +461,12 @@ export async function configureSyncMode({ if (!podSpec.initContainers) { podSpec.initContainers = [] } + if (!podSpec.imagePullSecrets) { podSpec.imagePullSecrets = [] } const k8sSyncUtilImageName = getK8sSyncUtilImageName() + if (!podSpec.initContainers.find((c) => c.image === k8sSyncUtilImageName)) { const initContainer: V1Container = { name: k8sSyncUtilContainerName, @@ -479,7 +481,8 @@ export async function configureSyncMode({ volumeMounts: [gardenVolumeMount], } podSpec.initContainers.push(initContainer) - podSpec.imagePullSecrets.push(...provider.config.imagePullSecrets) + + podSpec.imagePullSecrets.push(...provider.config.imagePullSecrets.map((s) => ({ name: s.name }))) } if (!targetContainer.volumeMounts) { diff --git a/core/test/integ/src/plugins/kubernetes/sync-mode.ts b/core/test/integ/src/plugins/kubernetes/sync-mode.ts index b7cb7a6b91..bdb91caad2 100644 --- a/core/test/integ/src/plugins/kubernetes/sync-mode.ts +++ b/core/test/integ/src/plugins/kubernetes/sync-mode.ts @@ -12,222 +12,240 @@ const { mkdirp, pathExists, readFile, remove, writeFile } = fsExtra import { join } from "path" import type { ConfigGraph } from "../../../../../src/graph/config-graph.js" import { k8sGetContainerDeployStatus } from "../../../../../src/plugins/kubernetes/container/status.js" -import type { Log } from "../../../../../src/logger/log-entry.js" +import type { ActionLog, Log } from "../../../../../src/logger/log-entry.js" import { createActionLog } from "../../../../../src/logger/log-entry.js" import type { KubernetesPluginContext, KubernetesProvider } from "../../../../../src/plugins/kubernetes/config.js" import { getMutagenMonitor, Mutagen } from "../../../../../src/mutagen.js" -import type { KubernetesWorkload } from "../../../../../src/plugins/kubernetes/types.js" +import type { KubernetesWorkload, SyncableRuntimeAction } from "../../../../../src/plugins/kubernetes/types.js" import { execInWorkload } from "../../../../../src/plugins/kubernetes/util.js" import { dedent } from "../../../../../src/util/string.js" import { sleep } from "../../../../../src/util/util.js" import { getContainerTestGarden } from "./container/container.js" import { + configureSyncMode, convertContainerSyncSpec, convertKubernetesModuleDevModeSpec, } from "../../../../../src/plugins/kubernetes/sync.js" import type { HelmModuleConfig } from "../../../../../src/plugins/kubernetes/helm/module-config.js" import type { KubernetesModuleConfig } from "../../../../../src/plugins/kubernetes/kubernetes-type/module-config.js" import type { TestGarden } from "../../../../helpers.js" -import { cleanProject } from "../../../../helpers.js" +import { cleanProject, expectError, getDataDir, makeTestGarden } from "../../../../helpers.js" import type { ContainerDeployActionConfig } from "../../../../../src/plugins/container/moduleConfig.js" import { resolveAction } from "../../../../../src/graph/actions.js" import { DeployTask } from "../../../../../src/tasks/deploy.js" import { MUTAGEN_DIR_NAME } from "../../../../../src/constants.js" +import { getK8sSyncUtilImageName } from "../../../../../src/plugins/kubernetes/constants.js" +import type { Action, Resolved } from "../../../../../src/actions/types.js" +import stripAnsi from "strip-ansi" describe("sync mode deployments and sync behavior", () => { - let garden: TestGarden - let cleanup: (() => void) | undefined - let graph: ConfigGraph - let ctx: KubernetesPluginContext - let provider: KubernetesProvider - - const execInPod = async (command: string[], log: Log, workload: KubernetesWorkload) => { - const execRes = await execInWorkload({ - command, - ctx, - provider, - log, - namespace: provider.config.namespace!.name!, - workload, - interactive: false, - }) - return execRes - } - - before(async () => { - await init("local") - }) + describe("sync mode deployments", () => { + const environmentName = "local" + let garden: TestGarden + let cleanup: (() => void) | undefined + let graph: ConfigGraph + let ctx: KubernetesPluginContext + let provider: KubernetesProvider - after(async () => { - if (cleanup) { - cleanup() - } - }) - - beforeEach(async () => { - graph = await garden.getConfigGraph({ log: garden.log, emit: false }) - }) - - afterEach(async () => { - if (garden) { - garden.close() - const dataDir = join(garden.gardenDirPath, MUTAGEN_DIR_NAME) - await getMutagenMonitor({ log: garden.log, dataDir }).stop() - await cleanProject(garden.gardenDirPath) + const execInPod = async (command: string[], log: Log, workload: KubernetesWorkload) => { + const execRes = await execInWorkload({ + command, + ctx, + provider, + log, + namespace: provider.config.namespace!.name!, + workload, + interactive: false, + }) + return execRes } - }) - const init = async (environmentName: string) => { - ;({ garden, cleanup } = await getContainerTestGarden(environmentName, { noTempDir: true })) - graph = await garden.getConfigGraph({ - log: garden.log, - emit: false, - noCache: true, - actionModes: { - sync: ["deploy.sync-mode"], - }, - }) - provider = await garden.resolveProvider({ log: garden.log, name: "local-kubernetes" }) - ctx = ( - await garden.getPluginContext({ provider, templateContext: undefined, events: undefined }) - ) - } - - // todo: fix this test, It works locally, fails on ci - it.skip("should deploy a service in sync mode and successfully set a two-way sync", async () => { - await init("local") - const action = graph.getDeploy("sync-mode") - const log = garden.log - const deployTask = new DeployTask({ - garden, - graph, - log, - action, - force: true, - startSync: true, + after(async () => { + if (cleanup) { + cleanup() + } }) - await garden.processTasks({ tasks: [deployTask], throwOnError: true }) - const resolvedAction = await garden.resolveAction({ - action, - log: garden.log, - graph: await garden.getConfigGraph({ log: garden.log, emit: false, actionModes: { sync: ["deploy.sync-mode"] } }), + beforeEach(async () => { + ;({ garden, cleanup } = await getContainerTestGarden(environmentName, { noTempDir: true })) + graph = await garden.getConfigGraph({ + log: garden.log, + emit: false, + noCache: true, + actionModes: { + sync: ["deploy.sync-mode"], + }, + }) + provider = await garden.resolveProvider({ log: garden.log, name: "local-kubernetes" }) + ctx = ( + await garden.getPluginContext({ provider, templateContext: undefined, events: undefined }) + ) }) - const actionLog = createActionLog({ log, actionName: action.name, actionKind: action.kind }) - const status = await k8sGetContainerDeployStatus({ - ctx, - action: resolvedAction, - log: actionLog, + afterEach(async () => { + if (garden) { + garden.close() + const dataDir = join(garden.gardenDirPath, MUTAGEN_DIR_NAME) + await getMutagenMonitor({ log: garden.log, dataDir }).stop() + await cleanProject(garden.gardenDirPath) + } }) - expect(status.detail?.mode).to.equal("sync") - - const workload = status.detail?.detail.workload! - - // First, we create a file locally and verify that it gets synced into the pod - const actionPath = action.sourcePath() - await writeFile(join(actionPath, "made_locally"), "foo") - await sleep(300) - const execRes = await execInPod(["/bin/sh", "-c", "cat /tmp/made_locally"], log, workload) - expect(execRes.output.trim()).to.eql("foo") - - // Then, we create a file in the pod and verify that it gets synced back - await execInPod(["/bin/sh", "-c", "echo bar > /tmp/made_in_pod"], log, workload) - await sleep(500) - const localPath = join(actionPath, "made_in_pod") - expect(await pathExists(localPath)).to.eql(true) - expect((await readFile(localPath)).toString().trim()).to.eql("bar") - - // This is to make sure that the two-way sync doesn't recreate the local files we're about to delete here. - const actions = await garden.getActionRouter() - await actions.deploy.delete({ graph, log: actionLog, action: resolvedAction }) - - // Clean up the files we created locally - for (const filename of ["made_locally", "made_in_pod"]) { - try { - await remove(join(actionPath, filename)) - } catch {} - } - }) - // todo: fix this test, It works locally, fails on ci. - it.skip("should apply ignore rules from the sync spec and the provider-level sync defaults", async () => { - await init("local") - const action = graph.getDeploy("sync-mode") - - // We want to ignore the following directories (all at module root) - // somedir - // prefix-a <--- matched by provider-level default excludes - // nested/prefix-b <--- matched by provider-level default excludes - - action["_config"].spec.sync!.paths[0].mode = "one-way-replica" - action["_config"].spec.sync!.paths[0].exclude = ["somedir"] - const log = garden.log - const deployTask = new DeployTask({ - garden, - graph, - log, - action, - force: true, - startSync: true, - }) + // todo: fix this test, It works locally, fails on ci + it("should deploy a service in sync mode and successfully set a two-way sync", async () => { + const action = graph.getDeploy("sync-mode") + const log = garden.log + const deployTask = new DeployTask({ + garden, + graph, + log, + action, + force: true, + startSync: true, + }) - await garden.processTasks({ tasks: [deployTask], throwOnError: true }) - const resolvedAction = await garden.resolveAction({ - action, - log: garden.log, - graph: await garden.getConfigGraph({ log: garden.log, emit: false, actionModes: { sync: ["deploy.sync-mode"] } }), - }) - const actionLog = createActionLog({ log, actionName: action.name, actionKind: action.kind }) - const status = await k8sGetContainerDeployStatus({ - ctx, - action: resolvedAction, - log: actionLog, + await garden.processTasks({ tasks: [deployTask], throwOnError: true }) + const resolvedAction = await garden.resolveAction({ + action, + log: garden.log, + graph: await garden.getConfigGraph({ + log: garden.log, + emit: false, + actionModes: { sync: ["deploy.sync-mode"] }, + }), + }) + const actionLog = createActionLog({ log, actionName: action.name, actionKind: action.kind }) + + const status = await k8sGetContainerDeployStatus({ + ctx, + action: resolvedAction, + log: actionLog, + }) + + expect(status.detail?.mode).to.equal("sync") + + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + const workload = status.detail?.detail.workload! + + // First, we create a file locally and verify that it gets synced into the pod + const actionPath = action.sourcePath() + await writeFile(join(actionPath, "made_locally"), "foo") + await sleep(300) + const execRes = await execInPod(["/bin/sh", "-c", "cat /tmp/made_locally"], log, workload) + expect(execRes.output.trim()).to.eql("foo") + + // Then, we create a file in the pod and verify that it gets synced back + await execInPod(["/bin/sh", "-c", "echo bar > /tmp/made_in_pod"], log, workload) + await sleep(500) + const localPath = join(actionPath, "made_in_pod") + expect(await pathExists(localPath)).to.eql(true) + expect((await readFile(localPath)).toString().trim()).to.eql("bar") + + // This is to make sure that the two-way sync doesn't recreate the local files we're about to delete here. + const actions = await garden.getActionRouter() + await actions.deploy.delete({ graph, log: actionLog, action: resolvedAction }) + + // Clean up the files we created locally + for (const filename of ["made_locally", "made_in_pod"]) { + try { + await remove(join(actionPath, filename)) + } catch {} + } }) - const workload = status.detail?.detail.workload! - const actionPath = action.sourcePath() - - // First, we create a non-ignored file locally - await writeFile(join(actionPath, "made_locally"), "foo") - - // Then, we create files in each of the directories we intended to ignore in the `exclude` spec above, and - // verify that they didn't get synced into the pod. - await mkdirp(join(actionPath, "somedir")) - await writeFile(join(actionPath, "somedir", "file"), "foo") - await mkdirp(join(actionPath, "prefix-a")) - await writeFile(join(actionPath, "prefix-a", "file"), "foo") - await mkdirp(join(actionPath, "nested", "prefix-b")) - await writeFile(join(actionPath, "nested", "prefix-b", "file"), "foo") - - await sleep(1000) - const mutagen = new Mutagen({ ctx, log }) - await mutagen.flushAllSyncs(log) - - const ignoreExecRes = await execInPod(["/bin/sh", "-c", "ls -a /tmp /tmp/nested"], log, workload) - // Clean up the files we created locally - for (const filename of ["made_locally", "somedir", "prefix-a", "nested"]) { - try { - await remove(join(actionPath, filename)) - } catch {} - } + // todo: fix this test, It works locally, fails on ci. + it("should apply ignore rules from the sync spec and the provider-level sync defaults", async () => { + const action = graph.getDeploy("sync-mode") + + // We want to ignore the following directories (all at module root) + // somedir + // prefix-a <--- matched by provider-level default excludes + // nested/prefix-b <--- matched by provider-level default excludes + + action["_config"].spec.sync!.paths[0].mode = "one-way-replica" + action["_config"].spec.sync!.paths[0].exclude = ["somedir"] + const log = garden.log + const deployTask = new DeployTask({ + garden, + graph, + log, + action, + force: true, + startSync: true, + }) - expect(ignoreExecRes.output.trim()).to.eql(dedent` - /tmp: - . - .. - Dockerfile - garden.yml - made_locally - nested - - /tmp/nested: - . - .. - `) + await garden.processTasks({ tasks: [deployTask], throwOnError: true }) + const resolvedAction = await garden.resolveAction({ + action, + log: garden.log, + graph: await garden.getConfigGraph({ + log: garden.log, + emit: false, + actionModes: { sync: ["deploy.sync-mode"] }, + }), + }) + const actionLog = createActionLog({ log, actionName: action.name, actionKind: action.kind }) + const status = await k8sGetContainerDeployStatus({ + ctx, + action: resolvedAction, + log: actionLog, + }) + + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + const workload = status.detail?.detail.workload! + const actionPath = action.sourcePath() + + // First, we create a non-ignored file locally + await writeFile(join(actionPath, "made_locally"), "foo") + + // Then, we create files in each of the directories we intended to ignore in the `exclude` spec above, and + // verify that they didn't get synced into the pod. + await mkdirp(join(actionPath, "somedir")) + await writeFile(join(actionPath, "somedir", "file"), "foo") + await mkdirp(join(actionPath, "prefix-a")) + await writeFile(join(actionPath, "prefix-a", "file"), "foo") + await mkdirp(join(actionPath, "nested", "prefix-b")) + await writeFile(join(actionPath, "nested", "prefix-b", "file"), "foo") + + await sleep(1000) + const mutagen = new Mutagen({ ctx, log }) + await mutagen.flushAllSyncs(log) + + const ignoreExecRes = await execInPod(["/bin/sh", "-c", "ls -a /tmp /tmp/nested"], log, workload) + // Clean up the files we created locally + for (const filename of ["made_locally", "somedir", "prefix-a", "nested"]) { + try { + await remove(join(actionPath, filename)) + } catch {} + } + + expect(ignoreExecRes.output.trim()).to.eql(dedent` + /tmp: + . + .. + Dockerfile + garden.yml + made_locally + nested + + /tmp/nested: + . + .. + `) + }) }) describe("convertKubernetesModuleDevModeSpec", () => { + const environmentName = "local" + const root = getDataDir("test-projects", "container") + let garden: TestGarden + let graph: ConfigGraph + + beforeEach(async () => { + garden = await makeTestGarden(root, { environmentString: environmentName, noTempDir: true }) + graph = await garden.getConfigGraph({ log: garden.log, emit: false }) + }) + it("should return a simple sync spec converted from a kubernetes or helm module", async () => { // Since the sync specs for both `kubernetes` and `helm` modules have the type // `KubernetesModuleDevModeSpec`, we don't need separate test cases for each of those two module types here. @@ -352,6 +370,22 @@ describe("sync mode deployments and sync behavior", () => { }) describe("convertContainerDevModeSpec", () => { + const environmentName = "local" + const root = getDataDir("test-projects", "container") + let garden: TestGarden + let graph: ConfigGraph + let ctx: KubernetesPluginContext + let provider: KubernetesProvider + + beforeEach(async () => { + garden = await makeTestGarden(root, { environmentString: environmentName, noTempDir: true }) + provider = await garden.resolveProvider({ log: garden.log, name: "local-kubernetes" }) + ctx = ( + await garden.getPluginContext({ provider, templateContext: undefined, events: undefined }) + ) + graph = await garden.getConfigGraph({ log: garden.log, emit: false }) + }) + it("converts a sync spec from a container Deploy action", async () => { garden.setModuleConfigs([]) garden.setActionConfigs([ @@ -404,4 +438,591 @@ describe("sync mode deployments and sync behavior", () => { }) }) }) + + describe("configureSyncMode", () => { + const environmentName = "local" + const root = getDataDir("test-projects", "container") + let garden: TestGarden + let graph: ConfigGraph + let ctx: KubernetesPluginContext + let provider: KubernetesProvider + let actionRaw: Action + let actionLog: ActionLog + let action: Resolved + + // It's enough to initialise this once for the tests in this block + before(async () => { + garden = await makeTestGarden(root, { environmentString: environmentName, noTempDir: true }) + provider = await garden.resolveProvider({ log: garden.log, name: "local-kubernetes" }) + ctx = ( + await garden.getPluginContext({ provider, templateContext: undefined, events: undefined }) + ) + graph = await garden.getConfigGraph({ log: garden.log, emit: false }) + actionRaw = graph.getDeploy("sync-mode") + actionLog = createActionLog({ log: garden.log, actionName: actionRaw.name, actionKind: actionRaw.kind }) + action = await garden.resolveAction({ + action: actionRaw, + log: garden.log, + graph: await garden.getConfigGraph({ + log: garden.log, + emit: false, + actionModes: { sync: ["deploy.sync-mode"] }, + }), + }) + }) + + it("should return all manifests with the target resources modified and the updated resource itself", async () => { + const manifests = [ + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { + name: "sync-mode", + }, + spec: { + template: { + spec: { + containers: [{ name: "sync-mode" }], + }, + }, + }, + }, + { + kind: "ConfigMap", + apiVersion: "v1", + metadata: { + name: "my-configmap", + }, + }, + ] + + const res = await configureSyncMode({ + ctx, + log: actionLog, + provider, + action, + manifests, + defaultTarget: undefined, + spec: { + paths: [ + { + target: { + kind: "Deployment", + name: "sync-mode", + }, + sourcePath: join(action.sourcePath(), "src"), + containerPath: "/app/src", + }, + ], + }, + }) + + expect(res).to.eql({ + updated: [ + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { + name: "sync-mode", + annotations: { + "garden.io/mode": "sync", + }, + }, + spec: { + template: { + spec: { + containers: [ + { + name: "sync-mode", + volumeMounts: [ + { + name: "garden", + mountPath: "/.garden", + }, + ], + }, + ], + volumes: [ + { + name: "garden", + emptyDir: {}, + }, + ], + initContainers: [ + { + name: "garden-sync-init", + image: getK8sSyncUtilImageName(), + command: ["/bin/sh", "-c", "'cp' '/usr/local/bin/mutagen-agent' '/.garden/mutagen-agent'"], + imagePullPolicy: "IfNotPresent", + volumeMounts: [ + { + name: "garden", + mountPath: "/.garden", + }, + ], + }, + ], + imagePullSecrets: [], + }, + }, + }, + }, + ], + manifests: [ + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { + name: "sync-mode", + annotations: { + "garden.io/mode": "sync", + }, + }, + spec: { + template: { + spec: { + containers: [ + { + name: "sync-mode", + volumeMounts: [ + { + name: "garden", + mountPath: "/.garden", + }, + ], + }, + ], + volumes: [ + { + name: "garden", + emptyDir: {}, + }, + ], + initContainers: [ + { + name: "garden-sync-init", + image: getK8sSyncUtilImageName(), + command: ["/bin/sh", "-c", "'cp' '/usr/local/bin/mutagen-agent' '/.garden/mutagen-agent'"], + imagePullPolicy: "IfNotPresent", + volumeMounts: [ + { + name: "garden", + mountPath: "/.garden", + }, + ], + }, + ], + imagePullSecrets: [], + }, + }, + }, + }, + { + kind: "ConfigMap", + apiVersion: "v1", + metadata: { + name: "my-configmap", + }, + }, + ], + }) + }) + it("should correctly set image pull secrets from the provider config on the Pod spec", async () => { + const manifests = [ + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { + name: "sync-mode", + }, + spec: { + template: { + spec: { + containers: [{ name: "sync-mode" }], + }, + }, + }, + }, + ] + + const res = await configureSyncMode({ + ctx, + log: actionLog, + provider: { + ...provider, + config: { + ...provider.config, + imagePullSecrets: [ + { + name: "secret-a", + namespace: "the-secret-namespace", + }, + { + name: "secret-b", + namespace: "the-secret-namespace", + }, + ], + }, + }, + action, + manifests, + defaultTarget: undefined, + spec: { + paths: [ + { + target: { + kind: "Deployment", + name: "sync-mode", + }, + sourcePath: join(action.sourcePath(), "src"), + containerPath: "/app/src", + }, + ], + }, + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((res.updated[0]).spec.template.spec.imagePullSecrets).to.eql([ + { + name: "secret-a", + }, + { + name: "secret-b", + }, + ]) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((res.manifests[0]).spec.template.spec.imagePullSecrets).to.eql([ + { + name: "secret-a", + }, + { + name: "secret-b", + }, + ]) + }) + it("should not overwrite existing image pull secrets", async () => { + const manifests = [ + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { + name: "sync-mode", + }, + spec: { + template: { + spec: { + containers: [{ name: "sync-mode" }], + imagePullSecrets: [ + { + name: "deployment-secret", + }, + ], + }, + }, + }, + }, + ] + + const res = await configureSyncMode({ + ctx, + log: actionLog, + provider: { + ...provider, + config: { + ...provider.config, + imagePullSecrets: [ + { + name: "provider-secret", + namespace: "the-secret-namespace", + }, + ], + }, + }, + action, + manifests, + defaultTarget: undefined, + spec: { + paths: [ + { + target: { + kind: "Deployment", + name: "sync-mode", + }, + sourcePath: join(action.sourcePath(), "src"), + containerPath: "/app/src", + }, + ], + }, + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((res.updated[0]).spec.template.spec.imagePullSecrets).to.eql([ + { + name: "deployment-secret", + }, + { + name: "provider-secret", + }, + ]) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((res.manifests[0]).spec.template.spec.imagePullSecrets).to.eql([ + { + name: "deployment-secret", + }, + { + name: "provider-secret", + }, + ]) + }) + it("should handle overrides without a target when defaultTarget is provided", async () => { + const manifests = [ + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { name: "sync-mode" }, + spec: { + template: { + spec: { + containers: [{ name: "sync-mode", image: "original-image" }], + }, + }, + }, + }, + ] + + const res = await configureSyncMode({ + ctx, + log: actionLog, + provider, + action, + manifests, + defaultTarget: { kind: "Deployment", name: "sync-mode" }, + spec: { + overrides: [ + { + image: "new-image", + }, + ], + }, + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((res.updated[0]).spec.template.spec.containers[0].image).to.equal("new-image") + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((res.manifests[0]).spec.template.spec.containers[0].image).to.equal("new-image") + }) + it("should throw an error when override has no target and no defaultTarget", async () => { + const manifests = [ + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { name: "sync-mode" }, + spec: { + template: { + spec: { + containers: [{ name: "sync-mode" }], + }, + }, + }, + }, + ] + + await expectError( + async () => + await configureSyncMode({ + ctx, + log: actionLog, + provider, + action, + manifests, + defaultTarget: undefined, + spec: { + overrides: [ + { + image: "new-image", + }, + ], + }, + }), + (err) => + expect(stripAnsi(err.message)).to.contain( + stripAnsi(`Sync override configuration on ${action.longDescription()} doesn't specify a target`) + ) + ) + }) + it("should handle sync paths with podSelector", async () => { + const manifests = [ + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { name: "sync-mode" }, + spec: { + selector: { matchLabels: { app: "sync-mode" } }, + template: { + metadata: { labels: { app: "sync-mode" } }, + spec: { + containers: [{ name: "sync-mode" }], + }, + }, + }, + }, + ] + + const res = await configureSyncMode({ + ctx, + log: actionLog, + provider, + action, + manifests, + defaultTarget: undefined, + spec: { + paths: [ + { + target: { podSelector: { app: "sync-mode" } }, + sourcePath: join(action.sourcePath(), "src"), + containerPath: "/app/src", + }, + ], + }, + }) + + // Verify that the manifest was not modified (podSelector doesn't modify manifests) + expect(res.manifests).to.deep.equal(manifests) + }) + it("should throw an error when sync path has no target and no defaultTarget", async () => { + const manifests = [ + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { name: "sync-mode" }, + spec: { + template: { + spec: { + containers: [{ name: "sync-mode" }], + }, + }, + }, + }, + ] + + await expectError( + async () => + configureSyncMode({ + ctx, + log: actionLog, + provider, + action, + manifests, + defaultTarget: undefined, + spec: { + paths: [ + { + sourcePath: join(action.sourcePath(), "src"), + containerPath: "/app/src", + }, + ], + }, + }), + (err) => + expect(stripAnsi(err.message)).to.contain( + stripAnsi(`Sync configuration on ${action.longDescription()} doesn't specify a target`) + ) + ) + }) + it("should handle overrides with command and args", async () => { + const manifests = [ + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { name: "sync-mode" }, + spec: { + template: { + spec: { + containers: [{ name: "sync-mode" }], + }, + }, + }, + }, + ] + + const res = await configureSyncMode({ + ctx, + log: actionLog, + provider, + action, + manifests, + defaultTarget: { kind: "Deployment", name: "sync-mode" }, + spec: { + overrides: [ + { + command: ["npm", "start"], + args: ["--port", "8080"], + }, + ], + }, + }) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const container = (res.updated[0]).spec.template.spec.containers[0] + expect(container.command).to.deep.equal(["npm", "start"]) + expect(container.args).to.deep.equal(["--port", "8080"]) + }) + it("should handle multiple targets and overrides", async () => { + const manifests = [ + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { name: "sync-mode-1" }, + spec: { + template: { + spec: { + containers: [{ name: "sync-mode-1" }], + }, + }, + }, + }, + { + kind: "Deployment", + apiVersion: "apps/v1", + metadata: { name: "sync-mode-2" }, + spec: { + template: { + spec: { + containers: [{ name: "sync-mode-2" }], + }, + }, + }, + }, + ] + + const res = await configureSyncMode({ + ctx, + log: actionLog, + provider, + action, + manifests, + defaultTarget: undefined, + spec: { + overrides: [ + { + target: { kind: "Deployment", name: "sync-mode-1" }, + image: "new-image-1", + }, + { + target: { kind: "Deployment", name: "sync-mode-2" }, + image: "new-image-2", + }, + ], + }, + }) + + expect(res.updated).to.have.length(2) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((res.updated[0]).spec.template.spec.containers[0].image).to.equal("new-image-1") + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((res.updated[1]).spec.template.spec.containers[0].image).to.equal("new-image-2") + }) + }) })