diff --git a/core/src/commands/exec.ts b/core/src/commands/exec.ts index a1d1904a3a..2933a80d88 100644 --- a/core/src/commands/exec.ts +++ b/core/src/commands/exec.ts @@ -16,6 +16,7 @@ import { executeAction } from "../graph/actions" import { NotFoundError } from "../exceptions" import { DeployStatus } from "../plugin/handlers/Deploy/get-status" import { createActionLog } from "../logger/log-entry" +import { K8_POD_DEFAULT_CONTAINER_ANNOTATION_KEY } from "../plugins/kubernetes/run" const execArgs = { deploy: new StringParameter({ @@ -40,6 +41,12 @@ const execOpts = { cliDefault: true, cliOnly: true, }), + target: new StringParameter({ + help: `Specify name of the target if a Deploy action consists of multiple components. _NOTE: This option is only relevant in certain scenarios and will be ignored otherwise._ For Kubernetes deploy actions, this is useful if a Deployment includes multiple containers, such as sidecar containers. By default, the container with \`${K8_POD_DEFAULT_CONTAINER_ANNOTATION_KEY}\` annotation or the first container is picked.`, + cliOnly: true, + defaultValue: undefined, + required: false, + }), } type Args = typeof execArgs @@ -74,6 +81,7 @@ export class ExecCommand extends Command { async action({ garden, log, args, opts }: CommandParams): Promise> { const deployName = args.deploy const command = this.getCommand(args) + const target = opts["target"] as string | undefined const graph = await garden.getConfigGraph({ log, emit: false }) const action = graph.getDeploy(deployName) @@ -138,6 +146,7 @@ export class ExecCommand extends Command { graph, action: executed, command, + target, interactive: opts.interactive, }) diff --git a/core/src/plugin/handlers/Deploy/exec.ts b/core/src/plugin/handlers/Deploy/exec.ts index d620a65dcb..ee02174165 100644 --- a/core/src/plugin/handlers/Deploy/exec.ts +++ b/core/src/plugin/handlers/Deploy/exec.ts @@ -16,6 +16,7 @@ import { Executed } from "../../../actions/types" interface ExecInDeployParams extends PluginDeployActionParamsBase { command: string[] interactive: boolean + target?: string } export interface ExecInDeployResult { diff --git a/core/src/plugins/kubernetes/container/exec.ts b/core/src/plugins/kubernetes/container/exec.ts index b45c8b58b7..a2701d0a7b 100644 --- a/core/src/plugins/kubernetes/container/exec.ts +++ b/core/src/plugins/kubernetes/container/exec.ts @@ -16,7 +16,7 @@ import { DeployActionHandler } from "../../../plugin/action-types" import { k8sGetContainerDeployStatus } from "./status" export const execInContainer: DeployActionHandler<"exec", ContainerDeployAction> = async (params) => { - const { ctx, log, action, command, interactive } = params + const { ctx, log, action, command, interactive, target: containerName } = params const k8sCtx = ctx const provider = k8sCtx.provider const status = await k8sGetContainerDeployStatus({ @@ -43,6 +43,7 @@ export const execInContainer: DeployActionHandler<"exec", ContainerDeployAction> log, namespace, workload: status.detail?.detail.workload, + containerName, command, interactive, }) diff --git a/core/src/plugins/kubernetes/kubernetes-type/common.ts b/core/src/plugins/kubernetes/kubernetes-type/common.ts index a99bc2a173..e7d4e45b09 100644 --- a/core/src/plugins/kubernetes/kubernetes-type/common.ts +++ b/core/src/plugins/kubernetes/kubernetes-type/common.ts @@ -52,7 +52,7 @@ export async function getManifests({ // remove *List objects const manifests = rawManifests.flatMap((manifest) => { - if (manifest.kind.endsWith("List")) { + if (manifest?.kind?.endsWith("List")) { if (!manifest.items || manifest.items.length === 0) { // empty list return [] diff --git a/core/src/plugins/kubernetes/kubernetes-type/exec.ts b/core/src/plugins/kubernetes/kubernetes-type/exec.ts index 2ee321451c..7da1b2b146 100644 --- a/core/src/plugins/kubernetes/kubernetes-type/exec.ts +++ b/core/src/plugins/kubernetes/kubernetes-type/exec.ts @@ -17,7 +17,7 @@ import { getKubernetesDeployStatus } from "./handlers" import chalk from "chalk" export const execInKubernetesDeploy: DeployActionHandler<"exec", KubernetesDeployAction> = async (params) => { - const { ctx, log, action, command, interactive } = params + const { ctx, log, action, command, interactive, target: containerName } = params const k8sCtx = ctx const provider = k8sCtx.provider @@ -64,5 +64,5 @@ export const execInKubernetesDeploy: DeployActionHandler<"exec", KubernetesDeplo }) } - return execInWorkload({ ctx, provider, log, namespace, workload: target, command, interactive }) + return execInWorkload({ ctx, provider, log, namespace, workload: target, containerName, command, interactive }) } diff --git a/core/src/plugins/kubernetes/run.ts b/core/src/plugins/kubernetes/run.ts index 9786f00b8f..f24f3790fd 100644 --- a/core/src/plugins/kubernetes/run.ts +++ b/core/src/plugins/kubernetes/run.ts @@ -46,6 +46,9 @@ import { LogLevel } from "../../logger/logger" import { getResourceEvents } from "./status/events" import stringify from "json-stringify-safe" +// ref: https://kubernetes.io/docs/reference/labels-annotations-taints/#kubectl-kubernetes-io-default-container +export const K8_POD_DEFAULT_CONTAINER_ANNOTATION_KEY = "kubectl.kubernetes.io/default-container" + /** * When a `podSpec` is passed to `runAndCopy`, only these fields will be used for the runner's pod spec * (and, in some cases, overridden/populated in `runAndCopy`). @@ -1047,7 +1050,29 @@ export class PodRunner extends PodRunnerParams { } const startedAt = new Date() - const containerName = container || this.pod.spec.containers[0].name + let containerName = container + if (!containerName) { + // if no container name is specified, check if the Pod has annotation kubectl.kubernetes.io/default-container + const defaultAnnotationContainer = this.pod.metadata.annotations + ? this.pod.metadata.annotations[K8_POD_DEFAULT_CONTAINER_ANNOTATION_KEY] + : undefined + + if (defaultAnnotationContainer) { + containerName = defaultAnnotationContainer + if (this.pod.spec.containers.length > 1) { + log.info( + // in case there are more than 1 containers and exec picks container with annotation + `Defaulted container ${containerName} due to the annotation ${K8_POD_DEFAULT_CONTAINER_ANNOTATION_KEY}.` + ) + } + } else { + containerName = this.pod.spec.containers[0].name + if (this.pod.spec.containers.length > 1) { + const allContainerNames = this.pod.spec.containers.map((c) => c.name) + log.info(`Defaulted container ${containerName} out of: ${allContainerNames.join(", ")}.`) + } + } + } log.debug(`Execing command in ${this.namespace}/Pod/${this.podName}/${containerName}: ${command.join(" ")}`) const startTime = new Date(Date.now()) diff --git a/core/src/plugins/kubernetes/util.ts b/core/src/plugins/kubernetes/util.ts index 4d80c13942..ff91ead2a6 100644 --- a/core/src/plugins/kubernetes/util.ts +++ b/core/src/plugins/kubernetes/util.ts @@ -252,6 +252,7 @@ export async function execInWorkload({ namespace, workload, command, + containerName, streamLogs = false, interactive, }: { @@ -261,6 +262,7 @@ export async function execInWorkload({ namespace: string workload: KubernetesWorkload | KubernetesPod command: string[] + containerName?: string streamLogs?: boolean interactive: boolean }) { @@ -285,6 +287,7 @@ export async function execInWorkload({ timeoutSec: 999999, tty: interactive, buffer: true, + containerName, } if (streamLogs) { diff --git a/core/test/unit/src/commands/exec.ts b/core/test/unit/src/commands/exec.ts index c87052bd3a..dc17acc9d8 100644 --- a/core/test/unit/src/commands/exec.ts +++ b/core/test/unit/src/commands/exec.ts @@ -27,6 +27,7 @@ describe("ExecCommand", () => { args, opts: withDefaultGlobalOpts({ interactive: false, + target: "", }), }) diff --git a/docs/reference/commands.md b/docs/reference/commands.md index bc1d0b705b..90afe94f06 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -1361,6 +1361,7 @@ Examples: | Argument | Alias | Type | Description | | -------- | ----- | ---- | ----------- | | `--interactive` | | boolean | Set to false to skip interactive mode and just output the command result + | `--target` | | string | Specify name of the target if a Deploy action consists of multiple components. _NOTE: This option is only relevant in certain scenarios and will be ignored otherwise._ For Kubernetes deploy actions, this is useful if a Deployment includes multiple containers, such as sidecar containers. By default, the container with `kubectl.kubernetes.io/default-container` annotation or the first container is picked. #### Outputs