Skip to content

Commit

Permalink
feat(helm): store garden metadata in configmap instead of helm values (
Browse files Browse the repository at this point in the history
…#5827)

* feat(helm): store garden metadata in configmap instead of helm values

* chore: fix test and add undefined check

* chore: use runtimeError
  • Loading branch information
twelvemo authored Mar 14, 2024
1 parent 1e70718 commit adcf968
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 161 deletions.
18 changes: 8 additions & 10 deletions core/src/plugins/kubernetes/helm/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,17 @@ import type { RunResult } from "../../../plugin/base.js"
import { MAX_RUN_RESULT_LOG_LENGTH } from "../constants.js"
import { safeDumpYaml } from "../../../util/serialization.js"
import type { HelmDeployAction } from "./config.js"
import type { Resolved } from "../../../actions/types.js"
import type { ActionMode, Resolved } from "../../../actions/types.js"

export const helmChartYamlFilename = "Chart.yaml"

export type HelmGardenMetadataConfigMapData = {
mode: ActionMode
version: string
actionName: string
projectName: string
}

interface Chart {
apiVersion: string
dependencies?: { name: string }[]
Expand Down Expand Up @@ -94,13 +101,6 @@ export async function prepareTemplates({ ctx, action, log }: PrepareTemplatesPar
// Merge with the base module's values, if applicable
const { chart, values } = action.getSpec()

// Add Garden metadata
values[".garden"] = {
moduleName: action.name,
projectName: ctx.projectName,
version: action.versionString(),
}

const valuesPath = await temporaryWrite(safeDumpYaml(values))
log.silly(() => `Wrote chart values to ${valuesPath}`)

Expand Down Expand Up @@ -284,8 +284,6 @@ export async function getValueArgs({

const args = flatten(valueFiles.map((f) => ["--values", f]))

args.push("--set", "\\.garden.mode=" + action.mode())

return args
}

Expand Down
26 changes: 25 additions & 1 deletion core/src/plugins/kubernetes/helm/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { waitForResources } from "../status/status.js"
import { helm } from "./helm-cli.js"
import type { HelmGardenMetadataConfigMapData } from "./common.js"
import { filterManifests, getReleaseName, getValueArgs, prepareManifests, prepareTemplates } from "./common.js"
import { gardenCloudAECPauseAnnotation, getPausedResources, getReleaseStatus, getRenderedResources } from "./status.js"
import { apply, deleteResources } from "../kubectl.js"
Expand All @@ -23,6 +24,7 @@ import type { HelmDeployAction } from "./config.js"
import { isEmpty } from "lodash-es"
import { getK8sIngresses } from "../status/ingress.js"
import { toGardenError } from "../../../exceptions.js"
import { upsertConfigMap } from "../util.js"

export const helmDeploy: DeployActionHandler<"deploy", HelmDeployAction> = async (params) => {
const { ctx, action, log, force } = params
Expand Down Expand Up @@ -100,6 +102,22 @@ export const helmDeploy: DeployActionHandler<"deploy", HelmDeployAction> = async
}
}

//create or upsert configmap with garden metadata
const gardenMetadata: HelmGardenMetadataConfigMapData = {
actionName: action.name,
projectName: ctx.projectName,
version: action.versionString(),
mode: action.mode(),
}

await upsertConfigMap({
api,
namespace,
key: `garden-helm-metadata-${action.name}`,
labels: {},
data: gardenMetadata,
})

const preparedManifests = await prepareManifests({
ctx: k8sCtx,
log,
Expand Down Expand Up @@ -202,7 +220,13 @@ export const deleteHelmDeploy: DeployActionHandler<"delete", HelmDeployAction> =
const resources = await getRenderedResources({ ctx: k8sCtx, action, releaseName, log })

await helm({ ctx: k8sCtx, log, namespace, args: ["uninstall", releaseName], emitLogEvents: true })

try {
// remove configmap with garden metadata
const api = await KubeApi.factory(log, ctx, provider)
await api.core.deleteNamespacedConfigMap({ namespace, name: `garden-helm-metadata-${action.name}` })
} catch (error) {
log.warn(`Failed to remove configmap with garden metadata for deploy: ${action.name}.`)
}
// Wait for resources to terminate
await deleteResources({ log, ctx, provider, resources, namespace })

Expand Down
116 changes: 64 additions & 52 deletions core/src/plugins/kubernetes/helm/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type { ForwardablePort, ServiceIngress, DeployState, ServiceStatus } from "../../../types/service.js"
import type { Log } from "../../../logger/log-entry.js"
import { helm } from "./helm-cli.js"
import type { HelmGardenMetadataConfigMapData } from "./common.js"
import { getReleaseName, loadTemplate } from "./common.js"
import type { KubernetesPluginContext } from "../config.js"
import { getForwardablePorts } from "../port-forward.js"
Expand All @@ -23,8 +24,9 @@ import type { HelmDeployAction } from "./config.js"
import type { ActionMode, Resolved } from "../../../actions/types.js"
import { deployStateToActionState } from "../../../plugin/handlers/Deploy/get-status.js"
import { isTruthy } from "../../../util/util.js"
import { ChildProcessError } from "../../../exceptions.js"
import { ChildProcessError, RuntimeError } from "../../../exceptions.js"
import { gardenAnnotationKey } from "../../../util/string.js"
import { deserializeValues } from "../../../util/serialization.js"

export const gardenCloudAECPauseAnnotation = gardenAnnotationKey("aec-status")

Expand Down Expand Up @@ -178,15 +180,17 @@ export async function getReleaseStatus({
releaseName: string
log: Log
}): Promise<ServiceStatus> {
let state: DeployState = "unknown"
let gardenMetadata: HelmGardenMetadataConfigMapData
const namespace = await getActionNamespace({
ctx,
log,
action,
provider: ctx.provider,
})

try {
log.silly(() => `Getting the release status for ${releaseName}`)
const namespace = await getActionNamespace({
ctx,
log,
action,
provider: ctx.provider,
})

const res = JSON.parse(
await helm({
ctx,
Expand All @@ -197,50 +201,7 @@ export async function getReleaseStatus({
emitLogEvents: false,
})
)

let state = helmStatusMap[res.info.status] || "unknown"
let values = {}

let deployedMode: ActionMode = "default"

if (state === "ready") {
// Make sure the right version is deployed
const helmResponse = await helm({
ctx,
log,
namespace,
args: ["get", "values", releaseName, "--output", "json"],
// do not send JSON output to Garden Cloud or CLI verbose log
emitLogEvents: false,
})
values = JSON.parse(helmResponse)

let deployedVersion: string | undefined = undefined
// JSON.parse can return null
if (values === null) {
log.verbose(`No helm values returned for release ${releaseName}. Is this release managed outside of garden?`)
state = "outdated"
} else {
deployedVersion = values[".garden"]?.version
deployedMode = values[".garden"]?.mode

if (action.mode() !== deployedMode || !deployedVersion || deployedVersion !== action.versionString()) {
state = "outdated"
}
}

// If ctx.cloudApi is defined, the user is logged in and they might be trying to deploy to an environment
// that could have been paused by Garden Cloud's AEC functionality. We therefore make sure to check for
// the annotations Garden Cloud adds to Helm Deployments and StatefulSets when pausing an environment.
if (ctx.cloudApi && (await isPaused({ ctx, namespace, action, releaseName, log }))) {
state = "outdated"
}
}

return {
state,
detail: { ...res, values, mode: deployedMode },
}
state = helmStatusMap[res.info.status] || "unknown"
} catch (err) {
if (!(err instanceof ChildProcessError)) {
throw err
Expand All @@ -251,6 +212,35 @@ export async function getReleaseStatus({
throw err
}
}
// get garden metadata from configmap in action namespace
try {
gardenMetadata = await getHelmGardenMetadataConfigMapData({ ctx, action, log, namespace })
} catch (err) {
log.verbose(`No configmap returned for release ${releaseName}. Is this release managed outside of garden?`)
return { state: "outdated", detail: {} }
}

// Make sure the right version is deployed
const deployedVersion = gardenMetadata.version
const deployedMode = gardenMetadata.mode

if (state === "ready") {
if (action.mode() !== deployedMode || !deployedVersion || deployedVersion !== action.versionString()) {
state = "outdated"
}
}

// If ctx.cloudApi is defined, the user is logged in and they might be trying to deploy to an environment
// that could have been paused by Garden Cloud's AEC functionality. We therefore make sure to check for
// the annotations Garden Cloud adds to Helm Deployments and StatefulSets when pausing an environment.
if (ctx.cloudApi && (await isPaused({ ctx, namespace, action, releaseName, log }))) {
state = "outdated"
}

return {
state,
detail: { gardenMetadata, mode: deployedMode },
}
}

/**
Expand Down Expand Up @@ -297,3 +287,25 @@ async function isPaused({
}) {
return (await getPausedResources({ ctx, action, namespace, releaseName, log })).length > 0
}

export async function getHelmGardenMetadataConfigMapData({
ctx,
action,
log,
namespace,
}: {
ctx: KubernetesPluginContext
action: Resolved<HelmDeployAction>
log: Log
namespace: string
}): Promise<HelmGardenMetadataConfigMapData> {
const api = await KubeApi.factory(log, ctx, ctx.provider)
const gardenMetadataConfigMap = await api.core.readNamespacedConfigMap({
name: `garden-helm-metadata-${action.name}`,
namespace,
})
if (!gardenMetadataConfigMap.data) {
throw new RuntimeError({ message: `Configmap with garden metadata for release ${action.name} is empty` })
}
return deserializeValues(gardenMetadataConfigMap.data) as HelmGardenMetadataConfigMapData
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
kind: Module
kind: Build
description: Image for the backend service
type: container
name: backend-image
66 changes: 30 additions & 36 deletions core/test/data/test-projects/helm-local-mode/backend/garden.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,35 @@
kind: Module
kind: Deploy
name: backend
description: Helm chart for the backend service
type: helm
dependencies:
- build.backend-image
spec:
localMode:
ports:
- local: 8090
remote: 8080
# starts the local application
command: [ ]
target:
kind: Deployment
name: backend
containerName: backend

localMode:
ports:
- local: 8090
remote: 8080
# starts the local application
command: [ ]
target:
kind: Deployment
name: backend
containerName: backend
# this is here to test that local mode always take precedence over sync mode
sync:
paths:
- target:
kind: Deployment
name: backend
containerPath: /app
mode: one-way

# this is here to test that local mode always take precedence over sync mode
sync:
paths:
- target: /app
mode: one-way

serviceResource:
kind: Deployment
containerModule: backend-image

build:
dependencies: [ "backend-image" ]

values:
image:
repository: ${modules.backend-image.outputs.deployment-image-name}
tag: ${modules.backend-image.version}
ingress:
enabled: true
paths: [ "/hello-backend" ]
hosts: [ "backend.${var.baseHostname}" ]

tasks:
- name: test
command: [ "sh", "-c", "echo task output" ]
values:
image:
repository: ${actions.build.backend-image.outputs.deployment-image-name}
tag: ${actions.build.backend-image.version}
ingress:
enabled: true
paths: [ "/hello-backend" ]
hosts: [ "backend.${var.baseHostname}" ]
58 changes: 34 additions & 24 deletions core/test/data/test-projects/helm-local-mode/frontend/garden.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
kind: Module
kind: Deploy
name: frontend
description: Frontend service container
type: container
services:
- name: frontend
ports:
- name: http
containerPort: 8080
healthCheck:
httpGet:
path: /hello-frontend
port: http
ingresses:
- path: /hello-frontend
port: http
- path: /call-backend
port: http
dependencies:
- backend
tests:
- name: unit
args: [npm, test]
- name: integ
args: [npm, run, integ]
dependencies:
- frontend
dependencies:
- deploy.backend
spec:
ports:
- name: http
containerPort: 8080
healthCheck:
httpGet:
path: /hello-frontend
port: http
ingresses:
- path: /hello-frontend
port: http
- path: /call-backend
port: http
---
kind: Test
name: frontend-unit
description: Frontend service unit tests
type: container
dependencies:
- deploy.frontend
spec:
command: [npm, test]
---
kind: Test
name: frontend-integ
description: Frontend service integ tests
type: container
dependencies:
- deploy.frontend
spec:
command: [npm, run, integ]
Loading

0 comments on commit adcf968

Please sign in to comment.