Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
114 changes: 63 additions & 51 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 @@ -25,6 +26,7 @@ import { deployStateToActionState } from "../../../plugin/handlers/Deploy/get-st
import { isTruthy } from "../../../util/util.js"
import { ChildProcessError } 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 Error(`Configmap with garden metadata for release ${action.name} is empty`)
twelvemo marked this conversation as resolved.
Show resolved Hide resolved
}
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]