diff --git a/core/src/plugins/kubernetes/api.ts b/core/src/plugins/kubernetes/api.ts index 6b20239499..a1ef38c485 100644 --- a/core/src/plugins/kubernetes/api.ts +++ b/core/src/plugins/kubernetes/api.ts @@ -633,6 +633,14 @@ export class KubeApi { // return the result body directly if applicable .then((res: any) => { if (isPlainObject(res) && res.hasOwnProperty("body")) { + // inexplicably, this API sometimes returns apiVersion and kind as undefined... + if (name === "listNamespacedPod" && res.body.items) { + res.body.items = res.body.items.map((pod: any) => { + pod.apiVersion = "v1" + pod.kind = "Pod" + return pod + }) + } return res["body"] } else { return res diff --git a/core/src/plugins/kubernetes/config.ts b/core/src/plugins/kubernetes/config.ts index f46faaaf19..2b5e42a589 100644 --- a/core/src/plugins/kubernetes/config.ts +++ b/core/src/plugins/kubernetes/config.ts @@ -704,9 +704,10 @@ export const configSchema = () => .unknown(false) export interface ServiceResourceSpec { - kind: HotReloadableKind + kind?: HotReloadableKind name?: string containerName?: string + podSelector?: { [key: string]: string } containerModule?: string hotReloadCommand?: string[] hotReloadArgs?: string[] @@ -729,34 +730,46 @@ export interface KubernetesTestSpec extends BaseTestSpec { resource: ServiceResourceSpec } +export const serviceResourceDescription = dedent` + This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the \`kind\` and \`name\` fields, or a Pod via the \`podSelector\` field. +` + export const serviceResourceSchema = () => - joi.object().keys({ - // TODO: consider allowing a `resource` field, that includes the kind and name (e.g. Deployment/my-deployment). - kind: joi - .string() - .valid(...hotReloadableKinds) - .default("Deployment") - .description("The type of Kubernetes resource to sync files to."), - name: joi.string().description( - deline`The name of the resource to sync to. If the module contains a single resource of the specified Kind, + joi + .object() + .keys({ + kind: joi + .string() + .valid(...hotReloadableKinds) + .default("Deployment") + .description("The type of Kubernetes resource to sync files to."), + name: joi.string().description( + deline`The name of the resource to sync to. If the module contains a single resource of the specified Kind, this can be omitted.` - ), - containerName: joi.string().description( - deline`The name of a container in the target. Specify this if the target contains more than one container - and the main container is not the first container in the spec.` - ), - }) + ), + containerName: joi + .string() + .description( + `The name of a container in the target. Specify this if the target contains more than one container and the main container is not the first container in the spec.` + ), + podSelector: joiStringMap(joi.string()).description( + dedent` + A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod with matching labels will be picked as a target, so make sure the labels will always match a specific Pod type. + ` + ), + }) + .oxor("podSelector", "kind") + .oxor("podSelector", "name") export const containerModuleSchema = () => joiIdentifier() .description( - deline`The Garden module that contains the sources for the container. This needs to be specified under - \`serviceResource\` in order to enable hot-reloading, but is not necessary for tasks and tests. + dedent` + The Garden module that contains the sources for the container. This needs to be specified under \`serviceResource\` in order to enable hot-reloading and dev mode, but is not necessary for tasks and tests. - Must be a \`container\` module, and for hot-reloading to work you must specify the \`hotReload\` field - on the container module. + Must be a \`container\` module, and for hot-reloading to work you must specify the \`hotReload\` field on the container module (not required for dev mode). - Note: If you specify a module here, you don't need to specify it additionally under \`build.dependencies\`` + _Note: If you specify a module here, you don't need to specify it additionally under \`build.dependencies\`._` ) .example("my-container-module") @@ -773,10 +786,12 @@ export const kubernetesTaskSchema = () => baseTaskSpecSchema() .keys({ resource: serviceResourceSchema().description( - dedent`The Deployment, DaemonSet or StatefulSet that Garden should use to execute this task. + dedent`The Deployment, DaemonSet, StatefulSet or Pod that Garden should use to execute this task. If not specified, the \`serviceResource\` configured on the module will be used. If neither is specified, an error will be thrown. + ${serviceResourceDescription} + The following pod spec fields from the service resource will be used (if present) when executing the task: ${runPodSpecWhitelistDescription}` ), @@ -800,10 +815,12 @@ export const kubernetesTestSchema = () => baseTestSpecSchema() .keys({ resource: serviceResourceSchema().description( - dedent`The Deployment, DaemonSet or StatefulSet that Garden should use to execute this test suite. + dedent`The Deployment, DaemonSet or StatefulSet or Pod that Garden should use to execute this test suite. If not specified, the \`serviceResource\` configured on the module will be used. If neither is specified, an error will be thrown. + ${serviceResourceDescription} + The following pod spec fields from the service resource will be used (if present) when executing the test suite: ${runPodSpecWhitelistDescription}` ), diff --git a/core/src/plugins/kubernetes/dev-mode.ts b/core/src/plugins/kubernetes/dev-mode.ts index 1244928c27..24959048b5 100644 --- a/core/src/plugins/kubernetes/dev-mode.ts +++ b/core/src/plugins/kubernetes/dev-mode.ts @@ -10,7 +10,7 @@ const AsyncLock = require("async-lock") import { containerDevModeSchema, ContainerDevModeSpec } from "../container/config" import { dedent, gardenAnnotationKey } from "../../util/string" import { fromPairs, set } from "lodash" -import { getResourceContainer } from "./util" +import { getResourceContainer, getResourcePodSpec } from "./util" import { HotReloadableResource } from "./hot-reload/hot-reload" import { LogEntry } from "../../logger/log-entry" import { joinWithPosix } from "../../util/fs" @@ -80,6 +80,12 @@ export function configureDevMode({ target, spec, containerName }: ConfigureDevMo return } + const podSpec = getResourcePodSpec(target) + + if (!podSpec) { + return + } + // Inject mutagen agent on init const gardenVolumeName = `garden` const gardenVolumeMount = { @@ -87,11 +93,11 @@ export function configureDevMode({ target, spec, containerName }: ConfigureDevMo mountPath: "/.garden", } - if (!target.spec.template.spec!.volumes) { - target.spec.template.spec!.volumes = [] + if (!podSpec.volumes) { + podSpec.volumes = [] } - target.spec.template.spec!.volumes.push({ + podSpec.volumes.push({ name: gardenVolumeName, emptyDir: {}, }) @@ -105,10 +111,10 @@ export function configureDevMode({ target, spec, containerName }: ConfigureDevMo volumeMounts: [gardenVolumeMount], } - if (!target.spec.template.spec!.initContainers) { - target.spec.template.spec!.initContainers = [] + if (!podSpec.initContainers) { + podSpec.initContainers = [] } - target.spec.template.spec!.initContainers.push(initContainer) + podSpec.initContainers.push(initContainer) if (!mainContainer.volumeMounts) { mainContainer.volumeMounts = [] @@ -160,7 +166,7 @@ export async function startDevModeSync({ } if (!containerName) { - containerName = target.spec.template.spec?.containers[0]?.name + containerName = getResourcePodSpec(target)?.containers[0]?.name } if (!containerName) { diff --git a/core/src/plugins/kubernetes/helm/config.ts b/core/src/plugins/kubernetes/helm/config.ts index 780b4b1b7d..9e328cafd3 100644 --- a/core/src/plugins/kubernetes/helm/config.ts +++ b/core/src/plugins/kubernetes/helm/config.ts @@ -33,6 +33,7 @@ import { namespaceNameSchema, containerModuleSchema, hotReloadArgsSchema, + serviceResourceDescription, } from "../config" import { posix } from "path" import { runPodSpecIncludeFields } from "../run" @@ -104,10 +105,12 @@ const runPodSpecWhitelistDescription = runPodSpecIncludeFields.map((f) => `* \`$ const helmTaskSchema = () => kubernetesTaskSchema().keys({ resource: helmServiceResourceSchema().description( - dedent`The Deployment, DaemonSet or StatefulSet that Garden should use to execute this task. + dedent`The Deployment, DaemonSet or StatefulSet or Pod that Garden should use to execute this task. If not specified, the \`serviceResource\` configured on the module will be used. If neither is specified, an error will be thrown. + ${serviceResourceDescription} + The following pod spec fields from the service resource will be used (if present) when executing the task: ${runPodSpecWhitelistDescription}` ), @@ -116,10 +119,12 @@ const helmTaskSchema = () => const helmTestSchema = () => kubernetesTestSchema().keys({ resource: helmServiceResourceSchema().description( - dedent`The Deployment, DaemonSet or StatefulSet that Garden should use to execute this test suite. + dedent`The Deployment, DaemonSet or StatefulSet or Pod that Garden should use to execute this test suite. If not specified, the \`serviceResource\` configured on the module will be used. If neither is specified, an error will be thrown. + ${serviceResourceDescription} + The following pod spec fields from the service resource will be used (if present) when executing the test suite: ${runPodSpecWhitelistDescription}` ), @@ -170,13 +175,13 @@ export const helmModuleSpecSchema = () => ), repo: joi.string().description("The repository URL to fetch the chart from."), serviceResource: helmServiceResourceSchema().description( - deline`The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module - (not to be confused with Kubernetes Service resources). - Because a Helm chart can contain any number of Kubernetes resources, this needs to be specified for certain - Garden features and commands to work, such as hot-reloading. + dedent` + The Deployment, DaemonSet or StatefulSet or Pod that Garden should regard as the _Garden service_ in this module (not to be confused with Kubernetes Service resources). + + ${serviceResourceDescription} - We currently map a Helm chart to a single Garden service, because all the resources in a Helm chart are - deployed at once.` + Because a Helm chart can contain any number of Kubernetes resources, this needs to be specified for certain Garden features and commands to work, such as hot-reloading. + ` ), skipDeploy: joi .boolean() diff --git a/core/src/plugins/kubernetes/helm/deployment.ts b/core/src/plugins/kubernetes/helm/deployment.ts index eefc707047..83de90fd0e 100644 --- a/core/src/plugins/kubernetes/helm/deployment.ts +++ b/core/src/plugins/kubernetes/helm/deployment.ts @@ -18,7 +18,7 @@ import { ContainerHotReloadSpec } from "../../container/config" import { DeployServiceParams } from "../../../types/plugin/service/deployService" import { DeleteServiceParams } from "../../../types/plugin/service/deleteService" import { getForwardablePorts, killPortForwards } from "../port-forward" -import { findServiceResource, getServiceResourceSpec } from "../util" +import { getServiceResource, getServiceResourceSpec } from "../util" import { getModuleNamespace, getModuleNamespaceStatus } from "../namespace" import { getHotReloadSpec, configureHotReload, getHotReloadContainerName } from "../hot-reload/helpers" import { configureDevMode, startDevModeSync } from "../dev-mode" @@ -40,13 +40,22 @@ export async function deployHelmService({ const k8sCtx = ctx as KubernetesPluginContext const provider = k8sCtx.provider + const namespaceStatus = await getModuleNamespaceStatus({ + ctx: k8sCtx, + log, + module, + provider: k8sCtx.provider, + }) + const namespace = namespaceStatus.namespaceName + const manifests = await getChartResources({ ctx: k8sCtx, module, devMode, hotReload, log, version: service.version }) if ((devMode && module.spec.devMode) || hotReload) { serviceResourceSpec = getServiceResourceSpec(module, getBaseModule(module)) - serviceResource = await findServiceResource({ + serviceResource = await getServiceResource({ ctx, log, + provider, module, manifests, resourceSpec: serviceResourceSpec, @@ -59,14 +68,6 @@ export async function deployHelmService({ const chartPath = await getChartPath(module) - const namespaceStatus = await getModuleNamespaceStatus({ - ctx: k8sCtx, - log, - module, - provider: k8sCtx.provider, - }) - const namespace = namespaceStatus.namespaceName - const releaseName = getReleaseName(module) const releaseStatus = await getReleaseStatus({ ctx: k8sCtx, service, releaseName, log, devMode, hotReload }) diff --git a/core/src/plugins/kubernetes/helm/exec.ts b/core/src/plugins/kubernetes/helm/exec.ts index 6fed04a3d3..ad40e884e1 100644 --- a/core/src/plugins/kubernetes/helm/exec.ts +++ b/core/src/plugins/kubernetes/helm/exec.ts @@ -10,7 +10,7 @@ import { includes } from "lodash" import { DeploymentError } from "../../../exceptions" import { getAppNamespace } from "../namespace" import { KubernetesPluginContext } from "../config" -import { execInWorkload, findServiceResource, getServiceResourceSpec } from "../util" +import { execInWorkload, getServiceResource, getServiceResourceSpec } from "../util" import { ExecInServiceParams } from "../../../types/plugin/service/execInService" import { HelmModule } from "./config" import { getServiceStatus } from "./status" @@ -44,9 +44,10 @@ export async function execInHelmService(params: ExecInServiceParams) version: service.version, }) - const serviceResource = await findServiceResource({ + const serviceResource = await getServiceResource({ ctx, log, + provider, module, manifests, resourceSpec: serviceResourceSpec, diff --git a/core/src/plugins/kubernetes/helm/run.ts b/core/src/plugins/kubernetes/helm/run.ts index 990975a3f1..21f56b9ce8 100644 --- a/core/src/plugins/kubernetes/helm/run.ts +++ b/core/src/plugins/kubernetes/helm/run.ts @@ -10,7 +10,7 @@ import { HelmModule } from "./config" import { PodRunner, runAndCopy } from "../run" import { getChartResources, getBaseModule } from "./common" import { - findServiceResource, + getServiceResource, getResourceContainer, getResourcePodSpec, getServiceResourceSpec, @@ -61,9 +61,10 @@ export async function runHelmModule({ } const manifests = await getChartResources({ ctx: k8sCtx, module, devMode: false, hotReload: false, log, version }) - const target = await findServiceResource({ + const target = await getServiceResource({ ctx: k8sCtx, log, + provider: k8sCtx.provider, manifests, module, resourceSpec, @@ -133,9 +134,10 @@ export async function runHelmTask(params: RunTaskParams): Promise): Prom }) const baseModule = getBaseModule(module) const resourceSpec = test.config.spec.resource || getServiceResourceSpec(module, baseModule) - const target = await findServiceResource({ ctx: k8sCtx, log, manifests, module, resourceSpec }) + const target = await getServiceResource({ + ctx: k8sCtx, + log, + provider: k8sCtx.provider, + manifests, + module, + resourceSpec, + }) const container = getResourceContainer(target, resourceSpec.containerName) const namespaceStatus = await getModuleNamespaceStatus({ ctx: k8sCtx, diff --git a/core/src/plugins/kubernetes/hot-reload/helpers.ts b/core/src/plugins/kubernetes/hot-reload/helpers.ts index 955539ba49..8f58097d71 100644 --- a/core/src/plugins/kubernetes/hot-reload/helpers.ts +++ b/core/src/plugins/kubernetes/hot-reload/helpers.ts @@ -13,11 +13,10 @@ import { deline, gardenAnnotationKey } from "../../../util/string" import { set, flatten } from "lodash" import { GardenService } from "../../../types/service" import { LogEntry } from "../../../logger/log-entry" -import { execInWorkload, getResourceContainer, getServiceResourceSpec } from "../util" +import { execInWorkload, getResourceContainer, getResourcePodSpec, getServiceResourceSpec } from "../util" import { getPortForward, killPortForward } from "../port-forward" import { rsyncPort, buildSyncVolumeName, rsyncPortName } from "../constants" import { KubernetesPluginContext } from "../config" -import { KubernetesWorkload } from "../types" import { normalizeLocalRsyncPath, normalizeRelativePath } from "../../../util/fs" import { syncWithOptions } from "../../../util/sync" import { GardenModule } from "../../../types/module" @@ -37,7 +36,7 @@ interface ConfigureHotReloadParams { } /** - * Configures the specified Deployment, DaemonSet or StatefulSet for hot-reloading. + * Configures the specified Deployment, DaemonSet, StatefulSet or Pod for hot-reloading. * * Adds an rsync sidecar container, an emptyDir volume to mount over module dir in app container, * and an initContainer to perform the initial population of the emptyDir volume. @@ -106,22 +105,30 @@ export function configureHotReload({ mainContainer.args = hotReloadArgs } - // These any casts are necessary because of flaws in the TS definitions in the client library. - if (!target.spec.template.spec!.volumes) { - target.spec.template.spec!.volumes = [] + const podSpec = getResourcePodSpec(target) + + if (!podSpec) { + throw new ConfigurationError( + `Attempting to configure resource ${target.kind}/${target.metadata.name} for hot reloading but it does not contain a Pod spec.`, + { target, hotReloadSpec } + ) + } + + if (!podSpec.volumes) { + podSpec.volumes = [] } - target.spec.template.spec!.volumes.push({ + podSpec.volumes.push({ name: buildSyncVolumeName, emptyDir: {}, }) - if (!target.spec.template.spec!.initContainers) { - target.spec.template.spec!.initContainers = [] + if (!podSpec.initContainers) { + podSpec.initContainers = [] } - target.spec.template.spec!.initContainers.push(initContainer) + podSpec.initContainers.push(initContainer) - target.spec.template.spec!.containers.push({ + podSpec.containers.push({ name: "garden-rsync", image: "gardendev/rsync:0.2.0", imagePullPolicy: "IfNotPresent", @@ -268,7 +275,7 @@ interface SyncToServiceParams { service: GardenService hotReloadSpec: ContainerHotReloadSpec namespace: string - workload: KubernetesWorkload + workload: HotReloadableResource log: LogEntry } diff --git a/core/src/plugins/kubernetes/hot-reload/hot-reload.ts b/core/src/plugins/kubernetes/hot-reload/hot-reload.ts index b08270d6e8..39652444b0 100644 --- a/core/src/plugins/kubernetes/hot-reload/hot-reload.ts +++ b/core/src/plugins/kubernetes/hot-reload/hot-reload.ts @@ -6,28 +6,27 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { V1Deployment, V1DaemonSet, V1StatefulSet } from "@kubernetes/client-node" import { ContainerModule } from "../../container/config" import { RuntimeError, ConfigurationError } from "../../../exceptions" import { gardenAnnotationKey } from "../../../util/string" import { sortBy } from "lodash" -import { GardenService } from "../../../types/service" import { LogEntry } from "../../../logger/log-entry" -import { findServiceResource } from "../util" +import { getServiceResource, getServiceResourceSpec } from "../util" import { getAppNamespace, getModuleNamespace } from "../namespace" import { KubernetesPluginContext } from "../config" import { HotReloadServiceParams, HotReloadServiceResult } from "../../../types/plugin/service/hotReloadService" -import { BaseResource, KubernetesResource, KubernetesWorkload } from "../types" +import { BaseResource, KubernetesPod, KubernetesResource, KubernetesWorkload } from "../types" import { createWorkloadManifest } from "../container/deployment" import { KubeApi } from "../api" import { PluginContext } from "../../../plugin-context" -import { getChartResources } from "../helm/common" -import { HelmModule } from "../helm/config" +import { getBaseModule, getChartResources } from "../helm/common" +import { HelmModule, HelmService } from "../helm/config" import { getManifests } from "../kubernetes-module/common" -import { KubernetesModule } from "../kubernetes-module/config" +import { KubernetesModule, KubernetesService } from "../kubernetes-module/config" import { getHotReloadSpec, syncToService } from "./helpers" +import { GardenModule } from "../../../types/module" -export type HotReloadableResource = KubernetesResource +export type HotReloadableResource = KubernetesWorkload | KubernetesPod export type HotReloadableKind = "Deployment" | "DaemonSet" | "StatefulSet" export const hotReloadableKinds: string[] = ["Deployment", "DaemonSet", "StatefulSet"] @@ -42,7 +41,7 @@ export async function hotReloadK8s({ service, }: { ctx: PluginContext - service: GardenService + service: KubernetesService | HelmService log: LogEntry module: KubernetesModule | HelmModule }): Promise { @@ -55,6 +54,7 @@ export async function hotReloadK8s({ }) let manifests: KubernetesResource[] + let baseModule: GardenModule | undefined = undefined if (module.type === "helm") { manifests = await getChartResources({ @@ -65,17 +65,19 @@ export async function hotReloadK8s({ log, version: service.version, }) + baseModule = getBaseModule(module) } else { const api = await KubeApi.factory(log, ctx, k8sCtx.provider) manifests = await getManifests({ ctx, api, log, module: module, defaultNamespace: namespace }) } - const resourceSpec = service.spec.serviceResource + const resourceSpec = getServiceResourceSpec(module, baseModule) const hotReloadSpec = getHotReloadSpec(service) - const workload = await findServiceResource({ + const workload = await getServiceResource({ ctx, log, + provider: k8sCtx.provider, module, manifests, resourceSpec, diff --git a/core/src/plugins/kubernetes/kubernetes-module/config.ts b/core/src/plugins/kubernetes/kubernetes-module/config.ts index 13a607f677..99edbfd109 100644 --- a/core/src/plugins/kubernetes/kubernetes-module/config.ts +++ b/core/src/plugins/kubernetes/kubernetes-module/config.ts @@ -24,6 +24,7 @@ import { namespaceNameSchema, containerModuleSchema, hotReloadArgsSchema, + serviceResourceDescription, } from "../config" import { ContainerModule } from "../../container/config" import { kubernetesDevModeSchema, KubernetesDevModeSpec } from "../dev-mode" @@ -87,7 +88,10 @@ export const kubernetesModuleSpecSchema = () => serviceResource: serviceResourceSchema() .description( dedent` - The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module (not to be confused with Kubernetes Service resources). + The Deployment, DaemonSet or StatefulSet or Pod that Garden should regard as the _Garden service_ in this module (not to be confused with Kubernetes Service resources). + + ${serviceResourceDescription} + Because a \`kubernetes\` module can contain any number of Kubernetes resources, this needs to be specified for certain Garden features and commands to work. ` ) diff --git a/core/src/plugins/kubernetes/kubernetes-module/exec.ts b/core/src/plugins/kubernetes/kubernetes-module/exec.ts index cf13f18d7b..b500ee2172 100644 --- a/core/src/plugins/kubernetes/kubernetes-module/exec.ts +++ b/core/src/plugins/kubernetes/kubernetes-module/exec.ts @@ -11,7 +11,7 @@ import { DeploymentError } from "../../../exceptions" import { KubernetesModule } from "./config" import { getAppNamespace } from "../namespace" import { KubernetesPluginContext } from "../config" -import { execInWorkload, findServiceResource, getServiceResourceSpec } from "../util" +import { execInWorkload, getServiceResource, getServiceResourceSpec } from "../util" import { getKubernetesServiceStatus } from "./handlers" import { ExecInServiceParams } from "../../../types/plugin/service/execInService" @@ -33,9 +33,10 @@ export async function execInKubernetesService(params: ExecInServiceParams 0) { const prepareResult = await prepareManifestsForSync({ - ctx, + ctx: k8sCtx, log, module, service, @@ -334,7 +332,7 @@ async function prepareManifestsForSync({ hotReload, manifests, }: { - ctx: PluginContext + ctx: KubernetesPluginContext service: KubernetesService | HelmService log: LogEntry module: KubernetesModule @@ -342,14 +340,16 @@ async function prepareManifestsForSync({ hotReload: boolean manifests: KubernetesResource[] }) { - let target: KubernetesResource + let target: HotReloadableResource try { const resourceSpec = getServiceResourceSpec(module, undefined) + target = cloneDeep( - await findServiceResource({ + await getServiceResource({ ctx, log, + provider: ctx.provider, module, manifests, resourceSpec, diff --git a/core/src/plugins/kubernetes/kubernetes-module/run.ts b/core/src/plugins/kubernetes/kubernetes-module/run.ts index e003a5c893..faf647a5a4 100644 --- a/core/src/plugins/kubernetes/kubernetes-module/run.ts +++ b/core/src/plugins/kubernetes/kubernetes-module/run.ts @@ -9,7 +9,7 @@ import { KubernetesModule } from "./config" import { runAndCopy } from "../run" import { - findServiceResource, + getServiceResource, getResourceContainer, getResourcePodSpec, getServiceResourceSpec, @@ -39,9 +39,10 @@ export async function runKubernetesTask(params: RunTaskParams) const { command, args } = task.spec const manifests = await getManifests({ ctx, api, log, module, defaultNamespace: namespace }) const resourceSpec = task.spec.resource || getServiceResourceSpec(module, undefined) - const target = await findServiceResource({ + const target = await getServiceResource({ ctx: k8sCtx, log, + provider: k8sCtx.provider, manifests, module, resourceSpec, diff --git a/core/src/plugins/kubernetes/kubernetes-module/test.ts b/core/src/plugins/kubernetes/kubernetes-module/test.ts index a482ba2c3c..403f081296 100644 --- a/core/src/plugins/kubernetes/kubernetes-module/test.ts +++ b/core/src/plugins/kubernetes/kubernetes-module/test.ts @@ -18,7 +18,7 @@ import { KubeApi } from "../api" import { getManifests } from "./common" import { getServiceResourceSpec, - findServiceResource, + getServiceResource, getResourceContainer, makePodName, getResourcePodSpec, @@ -39,7 +39,14 @@ export async function testKubernetesModule(params: TestModuleParams namespace: string provider: KubernetesProvider } diff --git a/core/src/plugins/kubernetes/types.ts b/core/src/plugins/kubernetes/types.ts index 77ffa5dedc..ad3a77f6cb 100644 --- a/core/src/plugins/kubernetes/types.ts +++ b/core/src/plugins/kubernetes/types.ts @@ -30,11 +30,11 @@ export interface BaseResource { // Because the Kubernetes API library types currently list all keys as optional, we use this type to wrap the // library types and make some fields required that are always required in the API. -export type KubernetesResource = +export type KubernetesResource = // Make these always required { apiVersion: string - kind: string + kind: K metadata: Partial & { name: string } @@ -77,3 +77,7 @@ export type KubernetesStatefulSet = KubernetesResource export type KubernetesPod = KubernetesResource export type KubernetesWorkload = KubernetesResource + +export function isPodResource(resource: KubernetesWorkload | KubernetesPod): resource is KubernetesPod { + return resource.kind === "Pod" +} diff --git a/core/src/plugins/kubernetes/util.ts b/core/src/plugins/kubernetes/util.ts index ae57dc00a4..61fe2feb02 100644 --- a/core/src/plugins/kubernetes/util.ts +++ b/core/src/plugins/kubernetes/util.ts @@ -13,7 +13,7 @@ import { apply as jsonMerge } from "json-merge-patch" import chalk from "chalk" import hasha from "hasha" -import { KubernetesResource, KubernetesWorkload, KubernetesPod, KubernetesServerResource } from "./types" +import { KubernetesResource, KubernetesWorkload, KubernetesPod, KubernetesServerResource, isPodResource } from "./types" import { splitLast, serializeValues, findByName } from "../../util/util" import { KubeApi, KubernetesError } from "./api" import { gardenAnnotationKey, base64, deline, stableStringify } from "../../util/string" @@ -31,6 +31,7 @@ import { ProviderMap } from "../../config/provider" import { PodRunner } from "./run" import { isSubset } from "../../util/is-subset" import { checkPodStatus } from "./status/pod" +import { getAppNamespace } from "./namespace" export const skopeoImage = "gardendev/skopeo:1.41.0-2" @@ -57,7 +58,7 @@ export async function getAllPods( defaultNamespace: string, resources: KubernetesResource[] ): Promise { - const pods: KubernetesServerResource[] = flatten( + const pods: KubernetesPod[] = flatten( await Bluebird.map(resources, async (resource) => { if (resource.apiVersion === "v1" && resource.kind === "Pod") { return [>resource] @@ -71,7 +72,7 @@ export async function getAllPods( }) ) - return []>pods + return pods } /** @@ -119,14 +120,23 @@ export function deduplicatePodsByLabel(pods: KubernetesServerResource[]) * Retrieve a list of pods based on the resource selector, deduplicated so that only the most recent * pod is returned when multiple pods with the same label are found. */ -export async function getCurrentWorkloadPods(api: KubeApi, namespace: string, resource: KubernetesWorkload) { +export async function getCurrentWorkloadPods( + api: KubeApi, + namespace: string, + resource: KubernetesWorkload | KubernetesPod +) { return deduplicatePodsByLabel(await getWorkloadPods(api, namespace, resource)) } /** - * Retrieve a list of pods based on the resource selector. + * Retrieve a list of pods based on the given resource/manifest. If passed a Pod manifest, it's read from the + * remote namespace and returned directly. */ -export async function getWorkloadPods(api: KubeApi, namespace: string, resource: KubernetesWorkload) { +export async function getWorkloadPods(api: KubeApi, namespace: string, resource: KubernetesWorkload | KubernetesPod) { + if (isPodResource(resource)) { + return [await api.core.readNamespacedPod(resource.metadata.name, resource.metadata.namespace || namespace)] + } + // We don't match on the garden.io/version label because it can fall out of sync during hot reloads const selector = omit(getSelectorFromResource(resource), gardenAnnotationKey("version")) const pods = await getPods(api, resource.metadata?.namespace || namespace, selector) @@ -182,12 +192,7 @@ export async function getPods( selectorString // labelSelector ) - return []>res.items.map((pod) => { - // inexplicably, the API sometimes returns apiVersion and kind as undefined... - pod.apiVersion = "v1" - pod.kind = "Pod" - return pod - }) + return []>res.items } /** @@ -211,7 +216,7 @@ export async function execInWorkload({ provider: KubernetesProvider log: LogEntry namespace: string - workload: KubernetesWorkload + workload: KubernetesWorkload | KubernetesPod command: string[] interactive: boolean }) { @@ -460,7 +465,7 @@ export async function getRunningDeploymentPod({ const pods = await getWorkloadPods(api, namespace, resource) const pod = sample(pods.filter((p) => checkPodStatus(p) === "ready")) if (!pod) { - throw new PluginError(`Could not find a running pod in deployment ${deploymentName}`, { + throw new PluginError(`Could not find a running Pod in Deployment ${deploymentName}`, { deploymentName, namespace, }) @@ -481,11 +486,9 @@ export function getStaticLabelsFromPod(pod: KubernetesPod): { [key: string]: str } export function getSelectorString(labels: { [key: string]: string }) { - let selectorString: string = "-l" - for (const label in labels) { - selectorString += `${label}=${labels[label]},` - } - return selectorString.trimEnd().slice(0, -1) + return Object.entries(labels) + .map(([k, v]) => `${k}=${v}`) + .join(",") } /** @@ -532,6 +535,7 @@ export function getServiceResourceSpec( interface GetServiceResourceParams { ctx: PluginContext log: LogEntry + provider: KubernetesProvider manifests: KubernetesResource[] module: HelmModule | KubernetesModule resourceSpec: ServiceResourceSpec @@ -546,22 +550,44 @@ interface GetServiceResourceParams { * * Throws an error if no valid resource spec is given, or the resource spec doesn't match any of the given resources. */ -export async function findServiceResource({ +export async function getServiceResource({ ctx, log, + provider, manifests, module, resourceSpec, }: GetServiceResourceParams): Promise { - const targetKind = resourceSpec.kind + const resourceMsgName = resourceSpec ? "resource" : "serviceResource" + + if (resourceSpec.podSelector && !isEmpty(resourceSpec.podSelector)) { + const api = await KubeApi.factory(log, ctx, provider) + const namespace = await getAppNamespace(ctx, log, provider) + + const pods = await getReadyPods(api, namespace, resourceSpec.podSelector) + const pod = sample(pods) + if (!pod) { + const selectorStr = getSelectorString(resourceSpec.podSelector) + throw new ConfigurationError( + chalk.red( + `Could not find any Pod matching provided podSelector (${selectorStr}) for ${resourceMsgName} in ` + + `${module.type} module ${chalk.white(module.name)}` + ), + { resourceSpec } + ) + } + return pod + } + let targetName = resourceSpec.name + let target: HotReloadableResource + const targetKind = resourceSpec.kind const chartResourceNames = manifests.map((o) => `${o.kind}/${o.metadata.name}`) - const applicableChartResources = manifests.filter((o) => o.kind === targetKind) - let target: HotReloadableResource + const applicableChartResources = manifests.filter((o) => o.kind === targetKind) - if (targetName) { + if (targetKind && targetName) { if (module.type === "helm" && targetName.includes("{{")) { // need to resolve the template string const chartPath = await getChartPath(module) @@ -591,8 +617,8 @@ export async function findServiceResource({ throw new ConfigurationError( chalk.red( deline`${module.type} module ${chalk.white(module.name)} contains multiple ${targetKind}s. - You must specify ${chalk.underline("resource.name")} or ${chalk.underline("serviceResource.name")} - in the module config in order to identify the correct ${targetKind} to use.` + You must specify a resource name in the appropriate config in order to identify the correct ${targetKind} + to use.` ), { resourceSpec, chartResourceNames } ) @@ -605,7 +631,7 @@ export async function findServiceResource({ } /** - * From the given Deployment, DaemonSet or StatefulSet resource, get either the first container spec, + * From the given Deployment, DaemonSet, StatefulSet or Pod resource, get either the first container spec, * or if `containerName` is specified, the one matching that name. */ export function getResourceContainer(resource: HotReloadableResource, containerName?: string): V1Container { @@ -630,8 +656,8 @@ export function getResourceContainer(resource: HotReloadableResource, containerN return container } -export function getResourcePodSpec(resource: HotReloadableResource): V1PodSpec | undefined { - return resource.spec.template.spec +export function getResourcePodSpec(resource: KubernetesWorkload | KubernetesPod): V1PodSpec | undefined { + return isPodResource(resource) ? resource.spec : resource.spec.template?.spec } const maxPodNameLength = 63 diff --git a/core/test/integ/src/plugins/kubernetes/container/build/buildkit.ts b/core/test/integ/src/plugins/kubernetes/container/build/buildkit.ts index e2059732a6..8f89f7cf37 100644 --- a/core/test/integ/src/plugins/kubernetes/container/build/buildkit.ts +++ b/core/test/integ/src/plugins/kubernetes/container/build/buildkit.ts @@ -22,7 +22,7 @@ import { buildDockerAuthConfig } from "../../../../../../../src/plugins/kubernet import { dockerAuthSecretKey } from "../../../../../../../src/plugins/kubernetes/constants" import { grouped } from "../../../../../../helpers" -describe("ensureBuildkit", () => { +grouped("cluster-buildkit").describe("ensureBuildkit", () => { let garden: Garden let provider: KubernetesProvider let ctx: PluginContext diff --git a/core/test/integ/src/plugins/kubernetes/hot-reload.ts b/core/test/integ/src/plugins/kubernetes/hot-reload.ts index 5129889593..0427d27ad3 100644 --- a/core/test/integ/src/plugins/kubernetes/hot-reload.ts +++ b/core/test/integ/src/plugins/kubernetes/hot-reload.ts @@ -14,7 +14,11 @@ import { ConfigGraph } from "../../../../../src/config-graph" import { getHelmTestGarden, buildHelmModules } from "./helm/common" import { getChartResources } from "../../../../../src/plugins/kubernetes/helm/common" import { KubernetesProvider, KubernetesPluginContext } from "../../../../../src/plugins/kubernetes/config" -import { findServiceResource, getServiceResourceSpec } from "../../../../../src/plugins/kubernetes/util" +import { + getServiceResource, + getResourcePodSpec, + getServiceResourceSpec, +} from "../../../../../src/plugins/kubernetes/util" import { getHotReloadSpec, getHotReloadContainerName, @@ -119,9 +123,10 @@ describe("configureHotReload", () => { }) const resourceSpec = getServiceResourceSpec(module, undefined) const hotReloadSpec = getHotReloadSpec(service) - const hotReloadTarget = await findServiceResource({ + const hotReloadTarget = await getServiceResource({ ctx, log, + provider, module, manifests, resourceSpec, @@ -132,7 +137,7 @@ describe("configureHotReload", () => { hotReloadSpec, target: hotReloadTarget, }) - const containers: any[] = hotReloadTarget.spec.template.spec?.containers || [] + const containers: any[] = getResourcePodSpec(hotReloadTarget)?.containers || [] // This is a second, non-main/resource container included by the Helm chart, which should not mount the sync volume. const secondContainer = containers.find((c) => c.name === "second-container") diff --git a/core/test/integ/src/plugins/kubernetes/run.ts b/core/test/integ/src/plugins/kubernetes/run.ts index 4ac3dfe0a7..14f3229510 100644 --- a/core/test/integ/src/plugins/kubernetes/run.ts +++ b/core/test/integ/src/plugins/kubernetes/run.ts @@ -24,7 +24,7 @@ import { ServiceResourceSpec, } from "../../../../../src/plugins/kubernetes/config" import { - findServiceResource, + getServiceResource, getResourceContainer, getServiceResourceSpec, getResourcePodSpec, @@ -33,8 +33,8 @@ import { import { getContainerTestGarden } from "./container/container" import { KubernetesPod, - KubernetesResource, KubernetesServerResource, + KubernetesWorkload, } from "../../../../../src/plugins/kubernetes/types" import { PluginContext } from "../../../../../src/plugin-context" import { LogEntry } from "../../../../../src/logger/log-entry" @@ -43,7 +43,7 @@ import { buildHelmModules, getHelmTestGarden } from "./helm/common" import { getBaseModule, getChartResources } from "../../../../../src/plugins/kubernetes/helm/common" import { getModuleNamespace } from "../../../../../src/plugins/kubernetes/namespace" import { GardenModule } from "../../../../../src/types/module" -import { V1Container, V1DaemonSet, V1Deployment, V1Pod, V1PodSpec, V1StatefulSet } from "@kubernetes/client-node" +import { V1Container, V1Pod, V1PodSpec } from "@kubernetes/client-node" import { getResourceRequirements } from "../../../../../src/plugins/kubernetes/container/util" import { ContainerResourcesSpec } from "../../../../../src/plugins/container/config" @@ -314,30 +314,27 @@ describe("kubernetes Pod runner functions", () => { }) it("throws if Pod is invalid", async () => { - const pod = { - apiVersion: "v1", - kind: "Pod", - metadata: { - name: "!&/$/%#/", - namespace, - }, - spec: { - containers: [ - { - name: "main", - image: "busybox", - command: ["sh", "-c", "echo foo"], - }, - ], - }, - } - runner = new PodRunner({ ctx, - pod, + pod: { + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "!&/$/%#/", + namespace, + }, + spec: { + containers: [ + { + name: "main", + image: "busybox", + command: ["sh", "-c", "echo foo"], + }, + ], + }, + }, namespace, api, - provider, }) @@ -588,7 +585,7 @@ describe("kubernetes Pod runner functions", () => { let helmManifests: any[] let helmBaseModule: GardenModule | undefined let helmResourceSpec: ServiceResourceSpec - let helmTarget: KubernetesResource + let helmTarget: KubernetesWorkload | KubernetesPod let helmContainer: V1Container let helmNamespace: string @@ -623,20 +620,21 @@ describe("kubernetes Pod runner functions", () => { }) helmBaseModule = getBaseModule(helmModule) helmResourceSpec = getServiceResourceSpec(helmModule, helmBaseModule) - helmTarget = await findServiceResource({ + helmNamespace = await getModuleNamespace({ ctx: helmCtx, log: helmLog, - manifests: helmManifests, module: helmModule, - resourceSpec: helmResourceSpec, + provider: helmCtx.provider, }) - helmContainer = getResourceContainer(helmTarget, helmResourceSpec.containerName) - helmNamespace = await getModuleNamespace({ + helmTarget = await getServiceResource({ ctx: helmCtx, log: helmLog, - module: helmModule, provider: helmCtx.provider, + manifests: helmManifests, + module: helmModule, + resourceSpec: helmResourceSpec, }) + helmContainer = getResourceContainer(helmTarget, helmResourceSpec.containerName) }) // These test cases should cover the `kubernetes` module type as well, since these helpers operate on manifests diff --git a/core/test/integ/src/plugins/kubernetes/util.ts b/core/test/integ/src/plugins/kubernetes/util.ts index 23b2d3a1a2..ddbf11e55b 100644 --- a/core/test/integ/src/plugins/kubernetes/util.ts +++ b/core/test/integ/src/plugins/kubernetes/util.ts @@ -14,12 +14,17 @@ import { ConfigGraph } from "../../../../../src/config-graph" import { Provider } from "../../../../../src/config/provider" import { DeployTask } from "../../../../../src/tasks/deploy" import { KubeApi } from "../../../../../src/plugins/kubernetes/api" -import { KubernetesConfig, KubernetesPluginContext } from "../../../../../src/plugins/kubernetes/config" +import { + KubernetesConfig, + KubernetesPluginContext, + ServiceResourceSpec, +} from "../../../../../src/plugins/kubernetes/config" import { getWorkloadPods, getServiceResourceSpec, - findServiceResource, + getServiceResource, getResourceContainer, + getResourcePodSpec, } from "../../../../../src/plugins/kubernetes/util" import { createWorkloadManifest } from "../../../../../src/plugins/kubernetes/container/deployment" import { emptyRuntimeContext } from "../../../../../src/runtime-context" @@ -27,16 +32,18 @@ import { getHelmTestGarden } from "./helm/common" import { deline } from "../../../../../src/util/string" import { getBaseModule, getChartResources } from "../../../../../src/plugins/kubernetes/helm/common" import { buildHelmModule } from "../../../../../src/plugins/kubernetes/helm/build" -import { HotReloadableResource } from "../../../../../src/plugins/kubernetes/hot-reload/hot-reload" import { LogEntry } from "../../../../../src/logger/log-entry" import { BuildTask } from "../../../../../src/tasks/build" import { getContainerTestGarden } from "./container/container" +import { KubernetesDeployment, KubernetesPod, KubernetesWorkload } from "../../../../../src/plugins/kubernetes/types" +import { getAppNamespace } from "../../../../../src/plugins/kubernetes/namespace" describe("util", () => { let helmGarden: TestGarden let helmGraph: ConfigGraph let ctx: KubernetesPluginContext let log: LogEntry + let api: KubeApi before(async () => { helmGarden = await getHelmTestGarden() @@ -45,6 +52,7 @@ describe("util", () => { ctx = (await helmGarden.getPluginContext(provider)) as KubernetesPluginContext helmGraph = await helmGarden.getConfigGraph(log) await buildModules() + api = await KubeApi.factory(helmGarden.log, ctx, ctx.provider) }) beforeEach(async () => { @@ -56,7 +64,7 @@ describe("util", () => { }) async function buildModules() { - const modules = await helmGraph.getModules() + const modules = helmGraph.getModules() const tasks = modules.map( (module) => new BuildTask({ garden: helmGarden, graph: helmGraph, log, module, force: false, _guard: true }) ) @@ -77,7 +85,6 @@ describe("util", () => { try { const graph = await garden.getConfigGraph(garden.log) const provider = (await garden.resolveProvider(garden.log, "local-kubernetes")) as Provider - const api = await KubeApi.factory(garden.log, ctx, provider) const service = graph.getService("simple-service") @@ -113,17 +120,52 @@ describe("util", () => { await garden.close() } }) + + it("should read a Pod from a namespace directly when given a Pod manifest", async () => { + const garden = await getContainerTestGarden("local") + + try { + const graph = await garden.getConfigGraph(garden.log) + const service = graph.getService("simple-service") + + const deployTask = new DeployTask({ + force: false, + forceBuild: false, + garden, + graph, + log: garden.log, + service, + devModeServiceNames: [], + hotReloadServiceNames: [], + }) + + const provider = (await garden.resolveProvider(garden.log, "local-kubernetes")) as Provider + await garden.processTasks([deployTask], { throwOnError: true }) + + const namespace = await getAppNamespace(ctx, log, provider) + const allPods = await api.core.listNamespacedPod(namespace) + + const pod = allPods.items[0] + + const pods = await getWorkloadPods(api, namespace, pod) + expect(pods.length).to.equal(1) + expect(pods[0].kind).to.equal("Pod") + expect(pods[0].metadata.name).to.equal(pod.metadata.name) + } finally { + await garden.close() + } + }) }) describe("getServiceResourceSpec", () => { it("should return the spec on the given module if it has no base module", async () => { - const module = await helmGraph.getModule("api") + const module = helmGraph.getModule("api") expect(getServiceResourceSpec(module, undefined)).to.eql(module.spec.serviceResource) }) it("should return the spec on the base module if there is none on the module", async () => { - const module = await helmGraph.getModule("api") - const baseModule = await helmGraph.getModule("postgres") + const module = helmGraph.getModule("api") + const baseModule = helmGraph.getModule("postgres") module.spec.base = "postgres" delete module.spec.serviceResource module.buildDependencies = { postgres: baseModule } @@ -131,8 +173,8 @@ describe("util", () => { }) it("should merge the specs if both module and base have specs", async () => { - const module = await helmGraph.getModule("api") - const baseModule = await helmGraph.getModule("postgres") + const module = helmGraph.getModule("api") + const baseModule = helmGraph.getModule("postgres") module.spec.base = "postgres" module.buildDependencies = { postgres: baseModule } expect(getServiceResourceSpec(module, baseModule)).to.eql({ @@ -143,7 +185,7 @@ describe("util", () => { }) it("should throw if there is no base module and the module has no serviceResource spec", async () => { - const module = await helmGraph.getModule("api") + const module = helmGraph.getModule("api") delete module.spec.serviceResource await expectError( () => getServiceResourceSpec(module, undefined), @@ -157,8 +199,8 @@ describe("util", () => { }) it("should throw if there is a base module but neither module has a spec", async () => { - const module = await helmGraph.getModule("api") - const baseModule = await helmGraph.getModule("postgres") + const module = helmGraph.getModule("api") + const baseModule = helmGraph.getModule("postgres") module.spec.base = "postgres" module.buildDependencies = { postgres: baseModule } delete module.spec.serviceResource @@ -175,7 +217,7 @@ describe("util", () => { }) }) - describe("findServiceResource", () => { + describe("getServiceResource", () => { it("should return the resource specified by serviceResource", async () => { const module = helmGraph.getModule("api") const manifests = await getChartResources({ @@ -186,18 +228,48 @@ describe("util", () => { log, version: module.version.versionString, }) - const resourceSpec = await getServiceResourceSpec(module, undefined) - const result = await findServiceResource({ + const result = await getServiceResource({ ctx, log, + provider: ctx.provider, module, manifests, - resourceSpec, + resourceSpec: getServiceResourceSpec(module, undefined), }) const expected = find(manifests, (r) => r.kind === "Deployment") expect(result).to.eql(expected) }) + it("should throw if no resourceSpec or serviceResource is specified", async () => { + const module = helmGraph.getModule("api") + const manifests = await getChartResources({ + ctx, + module, + devMode: false, + hotReload: false, + log, + version: module.version.versionString, + }) + delete module.spec.serviceResource + await expectError( + () => + getServiceResource({ + ctx, + log, + provider: ctx.provider, + module, + manifests, + resourceSpec: getServiceResourceSpec(module, undefined), + }), + (err) => + expect(stripAnsi(err.message)).to.equal( + deline`helm module api doesn't specify a serviceResource in its configuration. + You must specify a resource in the module config in order to use certain Garden features, + such as hot reloading, tasks and tests.` + ) + ) + }) + it("should throw if no resource of the specified kind is in the chart", async () => { const module = helmGraph.getModule("api") const manifests = await getChartResources({ @@ -214,9 +286,10 @@ describe("util", () => { } await expectError( () => - findServiceResource({ + getServiceResource({ ctx, log, + provider: ctx.provider, module, manifests, resourceSpec, @@ -241,9 +314,10 @@ describe("util", () => { } await expectError( () => - findServiceResource({ + getServiceResource({ ctx, log, + provider: ctx.provider, module, manifests, resourceSpec, @@ -264,12 +338,20 @@ describe("util", () => { }) const deployment = find(manifests, (r) => r.kind === "Deployment") manifests.push(deployment!) - const resourceSpec = getServiceResourceSpec(module, undefined) + await expectError( - () => findServiceResource({ ctx, log, module, manifests, resourceSpec }), + () => + getServiceResource({ + ctx, + log, + provider: ctx.provider, + module, + manifests, + resourceSpec: getServiceResourceSpec(module, undefined), + }), (err) => expect(stripAnsi(err.message)).to.equal( - "helm module api contains multiple Deployments. You must specify resource.name or serviceResource.name in the module config in order to identify the correct Deployment to use." + "helm module api contains multiple Deployments. You must specify a resource name in the appropriate config in order to identify the correct Deployment to use." ) ) }) @@ -286,17 +368,132 @@ describe("util", () => { version: module.version.versionString, }) module.spec.serviceResource.name = `{{ template "postgresql.master.fullname" . }}` - const resourceSpec = getServiceResourceSpec(module, undefined) - const result = await findServiceResource({ + const result = await getServiceResource({ ctx, log, + provider: ctx.provider, module, manifests, - resourceSpec, + resourceSpec: getServiceResourceSpec(module, undefined), }) const expected = find(manifests, (r) => r.kind === "StatefulSet") expect(result).to.eql(expected) }) + + context("podSelector", () => { + before(async () => { + const service = helmGraph.getService("api") + + const deployTask = new DeployTask({ + force: false, + forceBuild: false, + garden: helmGarden, + graph: helmGraph, + log: helmGarden.log, + service, + devModeServiceNames: [], + hotReloadServiceNames: [], + }) + + await helmGarden.processTasks([deployTask], { throwOnError: true }) + }) + + it("returns running Pod if one is found matching podSelector", async () => { + const module = helmGraph.getModule("api") + const resourceSpec: ServiceResourceSpec = { + podSelector: { + "app.kubernetes.io/name": "api", + "app.kubernetes.io/instance": "api-release", + }, + } + + const pod = await getServiceResource({ + ctx, + log, + provider: ctx.provider, + module, + manifests: [], + + resourceSpec, + }) + + expect(pod.kind).to.equal("Pod") + expect(pod.metadata.labels?.["app.kubernetes.io/name"]).to.equal("api") + expect(pod.metadata.labels?.["app.kubernetes.io/instance"]).to.equal("api-release") + }) + + it("throws if podSelector is set and no Pod is found matching the selector", async () => { + const module = helmGraph.getModule("api") + const resourceSpec: ServiceResourceSpec = { + podSelector: { + "app.kubernetes.io/name": "boo", + "app.kubernetes.io/instance": "foo", + }, + } + + await expectError( + () => + getServiceResource({ + ctx, + log, + provider: ctx.provider, + module, + manifests: [], + + resourceSpec, + }), + (err) => + expect(stripAnsi(err.message)).to.equal( + "Could not find any Pod matching provided podSelector (app.kubernetes.io/name=boo,app.kubernetes.io/instance=foo) for resource in helm module api" + ) + ) + }) + }) + }) + + describe("getResourcePodSpec", () => { + it("should return the spec for a Pod resource", () => { + const pod: KubernetesPod = { + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "foo", + namespace: "bar", + }, + spec: { + containers: [ + { + name: "main", + }, + ], + }, + } + expect(getResourcePodSpec(pod)).to.equal(pod.spec) + }) + + it("should returns the Pod template spec for a Deployment", () => { + const deployment: KubernetesDeployment = { + apiVersion: "v1", + kind: "Deployment", + metadata: { + name: "foo", + namespace: "bar", + }, + spec: { + selector: {}, + template: { + spec: { + containers: [ + { + name: "main", + }, + ], + }, + }, + }, + } + expect(getResourcePodSpec(deployment)).to.equal(deployment.spec.template.spec) + }) }) describe("getResourceContainer", () => { @@ -310,24 +507,44 @@ describe("util", () => { log, version: module.version.versionString, }) - return find(manifests, (r) => r.kind === "Deployment")! + return find(manifests, (r) => r.kind === "Deployment")! } it("should get the first container on the resource if no name is specified", async () => { const deployment = await getDeployment() - const expected = deployment.spec.template.spec!.containers[0] + const expected = deployment.spec.template?.spec!.containers[0] expect(getResourceContainer(deployment)).to.equal(expected) }) it("should pick the container by name if specified", async () => { const deployment = await getDeployment() - const expected = deployment.spec.template.spec!.containers[0] + const expected = deployment.spec.template?.spec!.containers[0] expect(getResourceContainer(deployment, "api")).to.equal(expected) }) + it("should return a container from a Pod resource", async () => { + const pod: KubernetesPod = { + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "foo", + namespace: "bar", + }, + spec: { + containers: [ + { + name: "main", + }, + ], + }, + } + const expected = pod.spec!.containers[0] + expect(getResourceContainer(pod)).to.equal(expected) + }) + it("should throw if no containers are in resource", async () => { const deployment = await getDeployment() - deployment.spec.template.spec!.containers = [] + deployment.spec.template!.spec!.containers = [] await expectError( () => getResourceContainer(deployment), (err) => expect(err.message).to.equal("Deployment api-release has no containers configured.") diff --git a/core/test/unit/src/plugins/kubernetes/hot-reload.ts b/core/test/unit/src/plugins/kubernetes/hot-reload.ts index 227d989113..f4df38239e 100644 --- a/core/test/unit/src/plugins/kubernetes/hot-reload.ts +++ b/core/test/unit/src/plugins/kubernetes/hot-reload.ts @@ -148,6 +148,121 @@ describe("configureHotReload", () => { }, }) }) + + it("should correctly augment a Pod resource", async () => { + const target = { + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "foo", + }, + spec: { + containers: [ + { + image: "garden-io/foo", + }, + ], + }, + } + + configureHotReload({ + target: target, + hotReloadSpec: { + sync: [ + { + source: "*", + target: "/app", + }, + ], + }, + hotReloadArgs: ["some", "args"], + }) + + expect(target).to.eql({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "foo", + annotations: { + "garden.io/hot-reload": "true", + }, + }, + spec: { + containers: [ + { + image: "garden-io/foo", + volumeMounts: [ + { + name: "garden-sync", + mountPath: "/app", + subPath: "root/app/", + }, + ], + ports: [], + args: ["some", "args"], + }, + { + name: "garden-rsync", + image: "gardendev/rsync:0.2.0", + imagePullPolicy: "IfNotPresent", + env: [ + { + name: "ALLOW", + value: "0.0.0.0/0", + }, + ], + readinessProbe: { + initialDelaySeconds: 2, + periodSeconds: 1, + timeoutSeconds: 3, + successThreshold: 1, + failureThreshold: 5, + tcpSocket: { port: (rsyncPortName) }, + }, + volumeMounts: [ + { + name: "garden-sync", + mountPath: "/data", + }, + ], + ports: [ + { + name: "garden-rsync", + protocol: "TCP", + containerPort: 873, + }, + ], + }, + ], + volumes: [ + { + name: "garden-sync", + emptyDir: {}, + }, + ], + initContainers: [ + { + name: "garden-sync-init", + image: "garden-io/foo", + command: [ + "/bin/sh", + "-c", + "mkdir -p /.garden/hot_reload/root && mkdir -p /.garden/hot_reload/tmp/app/ && " + + "cp -r /app/ /.garden/hot_reload/root/app/", + ], + env: [], + imagePullPolicy: "IfNotPresent", + volumeMounts: [ + { + name: "garden-sync", + mountPath: "/.garden/hot_reload", + }, + ], + }, + ], + }, + }) + }) }) describe("removeTrailingSlashes", () => { diff --git a/core/test/unit/src/plugins/kubernetes/util.ts b/core/test/unit/src/plugins/kubernetes/util.ts index b5a36b1028..4840198da6 100644 --- a/core/test/unit/src/plugins/kubernetes/util.ts +++ b/core/test/unit/src/plugins/kubernetes/util.ts @@ -18,7 +18,7 @@ import { makePodName, matchSelector, } from "../../../../../src/plugins/kubernetes/util" -import { KubernetesServerResource } from "../../../../../src/plugins/kubernetes/types" +import { KubernetesPod, KubernetesServerResource } from "../../../../../src/plugins/kubernetes/types" import { V1Pod } from "@kubernetes/client-node" import { sleep } from "../../../../../src/util/util" @@ -288,7 +288,7 @@ describe("getStaticLabelsFromPod", () => { }, }, spec: {}, - } as unknown) as KubernetesServerResource + } as unknown) as KubernetesPod const labels = getStaticLabelsFromPod(pod) @@ -307,7 +307,7 @@ describe("getSelectorString", () => { } const selectorString = getSelectorString(labels) - expect(selectorString).to.eql("-lmodule=a,service=a") + expect(selectorString).to.eql("module=a,service=a") }) }) diff --git a/docs/reference/module-types/helm.md b/docs/reference/module-types/helm.md index 439d73a305..6c758578a6 100644 --- a/docs/reference/module-types/helm.md +++ b/docs/reference/module-types/helm.md @@ -200,11 +200,14 @@ releaseName: # The repository URL to fetch the chart from. repo: -# The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module (not to be -# confused with Kubernetes Service resources). Because a Helm chart can contain any number of Kubernetes resources, -# this needs to be specified for certain Garden features and commands to work, such as hot-reloading. -# We currently map a Helm chart to a single Garden service, because all the resources in a Helm chart are deployed at -# once. +# The Deployment, DaemonSet or StatefulSet or Pod that Garden should regard as the _Garden service_ in this module +# (not to be confused with Kubernetes Service resources). +# +# This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` fields, +# or a Pod via the `podSelector` field. +# +# Because a Helm chart can contain any number of Kubernetes resources, this needs to be specified for certain Garden +# features and commands to work, such as hot-reloading. serviceResource: # The type of Kubernetes resource to sync files to. kind: Deployment @@ -213,6 +216,10 @@ serviceResource: # container is not the first container in the spec. containerName: + # A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod with + # matching labels will be picked as a target, so make sure the labels will always match a specific Pod type. + podSelector: + # The name of the resource to sync to. If the chart contains a single resource of the specified Kind, # this can be omitted. # @@ -223,10 +230,12 @@ serviceResource: name: # The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` - # in order to enable hot-reloading, but is not necessary for tasks and tests. + # in order to enable hot-reloading and dev mode, but is not necessary for tasks and tests. + # # Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the - # container module. - # Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` + # container module (not required for dev mode). + # + # _Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies`._ containerModule: # If specified, overrides the arguments for the main container when running in hot-reload mode. @@ -289,10 +298,13 @@ tasks: # `.garden/artifacts`. target: . - # The Deployment, DaemonSet or StatefulSet that Garden should use to execute this task. + # The Deployment, DaemonSet or StatefulSet or Pod that Garden should use to execute this task. # If not specified, the `serviceResource` configured on the module will be used. If neither is specified, # an error will be thrown. # + # This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` + # fields, or a Pod via the `podSelector` field. + # # The following pod spec fields from the service resource will be used (if present) when executing the task: # * `affinity` # * `automountServiceAccountToken` @@ -330,6 +342,11 @@ tasks: # main container is not the first container in the spec. containerName: + # A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod + # with matching labels will be picked as a target, so make sure the labels will always match a specific Pod + # type. + podSelector: + # The name of the resource to sync to. If the chart contains a single resource of the specified Kind, # this can be omitted. # @@ -341,10 +358,12 @@ tasks: name: # The Garden module that contains the sources for the container. This needs to be specified under - # `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. + # `serviceResource` in order to enable hot-reloading and dev mode, but is not necessary for tasks and tests. + # # Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the - # container module. - # Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` + # container module (not required for dev mode). + # + # _Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies`._ containerModule: # If specified, overrides the arguments for the main container when running in hot-reload mode. @@ -388,10 +407,13 @@ tests: # `.garden/artifacts`. target: . - # The Deployment, DaemonSet or StatefulSet that Garden should use to execute this test suite. + # The Deployment, DaemonSet or StatefulSet or Pod that Garden should use to execute this test suite. # If not specified, the `serviceResource` configured on the module will be used. If neither is specified, # an error will be thrown. # + # This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` + # fields, or a Pod via the `podSelector` field. + # # The following pod spec fields from the service resource will be used (if present) when executing the test suite: # * `affinity` # * `automountServiceAccountToken` @@ -429,6 +451,11 @@ tests: # main container is not the first container in the spec. containerName: + # A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod + # with matching labels will be picked as a target, so make sure the labels will always match a specific Pod + # type. + podSelector: + # The name of the resource to sync to. If the chart contains a single resource of the specified Kind, # this can be omitted. # @@ -440,10 +467,12 @@ tests: name: # The Garden module that contains the sources for the container. This needs to be specified under - # `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. + # `serviceResource` in order to enable hot-reloading and dev mode, but is not necessary for tasks and tests. + # # Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the - # container module. - # Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` + # container module (not required for dev mode). + # + # _Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies`._ containerModule: # If specified, overrides the arguments for the main container when running in hot-reload mode. @@ -925,8 +954,11 @@ The repository URL to fetch the chart from. ### `serviceResource` -The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module (not to be confused with Kubernetes Service resources). Because a Helm chart can contain any number of Kubernetes resources, this needs to be specified for certain Garden features and commands to work, such as hot-reloading. -We currently map a Helm chart to a single Garden service, because all the resources in a Helm chart are deployed at once. +The Deployment, DaemonSet or StatefulSet or Pod that Garden should regard as the _Garden service_ in this module (not to be confused with Kubernetes Service resources). + +This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` fields, or a Pod via the `podSelector` field. + +Because a Helm chart can contain any number of Kubernetes resources, this needs to be specified for certain Garden features and commands to work, such as hot-reloading. | Type | Required | | -------- | -------- | @@ -952,6 +984,16 @@ The name of a container in the target. Specify this if the target contains more | -------- | -------- | | `string` | No | +### `serviceResource.podSelector` + +[serviceResource](#serviceresource) > podSelector + +A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod with matching labels will be picked as a target, so make sure the labels will always match a specific Pod type. + +| Type | Required | +| -------- | -------- | +| `object` | No | + ### `serviceResource.name` [serviceResource](#serviceresource) > name @@ -972,9 +1014,11 @@ the string for the YAML to be parsed correctly. [serviceResource](#serviceresource) > containerModule -The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. -Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module. -Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` +The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading and dev mode, but is not necessary for tasks and tests. + +Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module (not required for dev mode). + +_Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies`._ | Type | Required | | -------- | -------- | @@ -1200,10 +1244,12 @@ tasks: [tasks](#tasks) > resource -The Deployment, DaemonSet or StatefulSet that Garden should use to execute this task. +The Deployment, DaemonSet or StatefulSet or Pod that Garden should use to execute this task. If not specified, the `serviceResource` configured on the module will be used. If neither is specified, an error will be thrown. +This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` fields, or a Pod via the `podSelector` field. + The following pod spec fields from the service resource will be used (if present) when executing the task: * `affinity` * `automountServiceAccountToken` @@ -1258,6 +1304,16 @@ The name of a container in the target. Specify this if the target contains more | -------- | -------- | | `string` | No | +### `tasks[].resource.podSelector` + +[tasks](#tasks) > [resource](#tasksresource) > podSelector + +A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod with matching labels will be picked as a target, so make sure the labels will always match a specific Pod type. + +| Type | Required | +| -------- | -------- | +| `object` | No | + ### `tasks[].resource.name` [tasks](#tasks) > [resource](#tasksresource) > name @@ -1278,9 +1334,11 @@ the string for the YAML to be parsed correctly. [tasks](#tasks) > [resource](#tasksresource) > containerModule -The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. -Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module. -Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` +The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading and dev mode, but is not necessary for tasks and tests. + +Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module (not required for dev mode). + +_Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies`._ | Type | Required | | -------- | -------- | @@ -1479,10 +1537,12 @@ tests: [tests](#tests) > resource -The Deployment, DaemonSet or StatefulSet that Garden should use to execute this test suite. +The Deployment, DaemonSet or StatefulSet or Pod that Garden should use to execute this test suite. If not specified, the `serviceResource` configured on the module will be used. If neither is specified, an error will be thrown. +This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` fields, or a Pod via the `podSelector` field. + The following pod spec fields from the service resource will be used (if present) when executing the test suite: * `affinity` * `automountServiceAccountToken` @@ -1537,6 +1597,16 @@ The name of a container in the target. Specify this if the target contains more | -------- | -------- | | `string` | No | +### `tests[].resource.podSelector` + +[tests](#tests) > [resource](#testsresource) > podSelector + +A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod with matching labels will be picked as a target, so make sure the labels will always match a specific Pod type. + +| Type | Required | +| -------- | -------- | +| `object` | No | + ### `tests[].resource.name` [tests](#tests) > [resource](#testsresource) > name @@ -1557,9 +1627,11 @@ the string for the YAML to be parsed correctly. [tests](#tests) > [resource](#testsresource) > containerModule -The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. -Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module. -Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` +The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading and dev mode, but is not necessary for tasks and tests. + +Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module (not required for dev mode). + +_Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies`._ | Type | Required | | -------- | -------- | diff --git a/docs/reference/module-types/kubernetes.md b/docs/reference/module-types/kubernetes.md index 3fedbedbc7..a0e9ba1eaa 100644 --- a/docs/reference/module-types/kubernetes.md +++ b/docs/reference/module-types/kubernetes.md @@ -197,8 +197,12 @@ devMode: # workload is used. containerName: -# The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module (not to be -# confused with Kubernetes Service resources). +# The Deployment, DaemonSet or StatefulSet or Pod that Garden should regard as the _Garden service_ in this module +# (not to be confused with Kubernetes Service resources). +# +# This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` fields, +# or a Pod via the `podSelector` field. +# # Because a `kubernetes` module can contain any number of Kubernetes resources, this needs to be specified for certain # Garden features and commands to work. serviceResource: @@ -213,11 +217,17 @@ serviceResource: # container is not the first container in the spec. containerName: + # A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod with + # matching labels will be picked as a target, so make sure the labels will always match a specific Pod type. + podSelector: + # The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` - # in order to enable hot-reloading, but is not necessary for tasks and tests. + # in order to enable hot-reloading and dev mode, but is not necessary for tasks and tests. + # # Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the - # container module. - # Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` + # container module (not required for dev mode). + # + # _Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies`._ containerModule: # If specified, overrides the arguments for the main container when running in hot-reload mode. @@ -250,10 +260,13 @@ tasks: # Maximum duration (in seconds) of the task's execution. timeout: null - # The Deployment, DaemonSet or StatefulSet that Garden should use to execute this task. + # The Deployment, DaemonSet, StatefulSet or Pod that Garden should use to execute this task. # If not specified, the `serviceResource` configured on the module will be used. If neither is specified, # an error will be thrown. # + # This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` + # fields, or a Pod via the `podSelector` field. + # # The following pod spec fields from the service resource will be used (if present) when executing the task: # * `affinity` # * `automountServiceAccountToken` @@ -295,6 +308,11 @@ tasks: # main container is not the first container in the spec. containerName: + # A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod + # with matching labels will be picked as a target, so make sure the labels will always match a specific Pod + # type. + podSelector: + # Set to false if you don't want the task's result to be cached. Use this if the task needs to be run any time # your project (or one or more of the task's dependants) is deployed. Otherwise the task is only re-run when its # version changes (i.e. the module or one of its dependencies is modified), or when you run `garden run task`. @@ -337,10 +355,13 @@ tests: # Maximum duration (in seconds) of the test run. timeout: null - # The Deployment, DaemonSet or StatefulSet that Garden should use to execute this test suite. + # The Deployment, DaemonSet or StatefulSet or Pod that Garden should use to execute this test suite. # If not specified, the `serviceResource` configured on the module will be used. If neither is specified, # an error will be thrown. # + # This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` + # fields, or a Pod via the `podSelector` field. + # # The following pod spec fields from the service resource will be used (if present) when executing the test suite: # * `affinity` # * `automountServiceAccountToken` @@ -382,6 +403,11 @@ tests: # main container is not the first container in the spec. containerName: + # A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod + # with matching labels will be picked as a target, so make sure the labels will always match a specific Pod + # type. + podSelector: + # The command/entrypoint used to run the test inside the container. command: @@ -849,7 +875,10 @@ Optionally specify the name of a specific container to sync to. If not specified ### `serviceResource` -The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module (not to be confused with Kubernetes Service resources). +The Deployment, DaemonSet or StatefulSet or Pod that Garden should regard as the _Garden service_ in this module (not to be confused with Kubernetes Service resources). + +This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` fields, or a Pod via the `podSelector` field. + Because a `kubernetes` module can contain any number of Kubernetes resources, this needs to be specified for certain Garden features and commands to work. | Type | Required | @@ -886,13 +915,25 @@ The name of a container in the target. Specify this if the target contains more | -------- | -------- | | `string` | No | +### `serviceResource.podSelector` + +[serviceResource](#serviceresource) > podSelector + +A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod with matching labels will be picked as a target, so make sure the labels will always match a specific Pod type. + +| Type | Required | +| -------- | -------- | +| `object` | No | + ### `serviceResource.containerModule` [serviceResource](#serviceresource) > containerModule -The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading, but is not necessary for tasks and tests. -Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module. -Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies` +The Garden module that contains the sources for the container. This needs to be specified under `serviceResource` in order to enable hot-reloading and dev mode, but is not necessary for tasks and tests. + +Must be a `container` module, and for hot-reloading to work you must specify the `hotReload` field on the container module (not required for dev mode). + +_Note: If you specify a module here, you don't need to specify it additionally under `build.dependencies`._ | Type | Required | | -------- | -------- | @@ -990,10 +1031,12 @@ Maximum duration (in seconds) of the task's execution. [tasks](#tasks) > resource -The Deployment, DaemonSet or StatefulSet that Garden should use to execute this task. +The Deployment, DaemonSet, StatefulSet or Pod that Garden should use to execute this task. If not specified, the `serviceResource` configured on the module will be used. If neither is specified, an error will be thrown. +This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` fields, or a Pod via the `podSelector` field. + The following pod spec fields from the service resource will be used (if present) when executing the task: * `affinity` * `automountServiceAccountToken` @@ -1058,6 +1101,16 @@ The name of a container in the target. Specify this if the target contains more | -------- | -------- | | `string` | No | +### `tasks[].resource.podSelector` + +[tasks](#tasks) > [resource](#tasksresource) > podSelector + +A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod with matching labels will be picked as a target, so make sure the labels will always match a specific Pod type. + +| Type | Required | +| -------- | -------- | +| `object` | No | + ### `tasks[].cacheResult` [tasks](#tasks) > cacheResult @@ -1229,10 +1282,12 @@ Maximum duration (in seconds) of the test run. [tests](#tests) > resource -The Deployment, DaemonSet or StatefulSet that Garden should use to execute this test suite. +The Deployment, DaemonSet or StatefulSet or Pod that Garden should use to execute this test suite. If not specified, the `serviceResource` configured on the module will be used. If neither is specified, an error will be thrown. +This can either reference a workload (i.e. a Deployment, DaemonSet or StatefulSet) via the `kind` and `name` fields, or a Pod via the `podSelector` field. + The following pod spec fields from the service resource will be used (if present) when executing the test suite: * `affinity` * `automountServiceAccountToken` @@ -1297,6 +1352,16 @@ The name of a container in the target. Specify this if the target contains more | -------- | -------- | | `string` | No | +### `tests[].resource.podSelector` + +[tests](#tests) > [resource](#testsresource) > podSelector + +A map of string key/value labels to match on any Pods in the namespace. When specified, a random ready Pod with matching labels will be picked as a target, so make sure the labels will always match a specific Pod type. + +| Type | Required | +| -------- | -------- | +| `object` | No | + ### `tests[].command[]` [tests](#tests) > command diff --git a/examples/vote-helm/vote-image/package.json b/examples/vote-helm/vote-image/package.json index 9ece927f50..25b5cd2279 100644 --- a/examples/vote-helm/vote-image/package.json +++ b/examples/vote-helm/vote-image/package.json @@ -7,7 +7,7 @@ "build": "vue-cli-service build", "test:unit": "vue-cli-service test:unit", "lint": "vue-cli-service lint", - "test:integ": "node_modules/mocha/bin/mocha tests/integ/test.js" + "test:integ": "node_modules/mocha/bin/mocha tests/integ/test.js --timeout 30000" }, "dependencies": { "axios": "^0.19.0",