Skip to content

Commit

Permalink
refactor(k8s): avoid building Helm chart before getting deploy status
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald committed Dec 12, 2019
1 parent 3b438a4 commit aa06e8e
Show file tree
Hide file tree
Showing 14 changed files with 257 additions and 82 deletions.
16 changes: 11 additions & 5 deletions garden-service/src/plugins/kubernetes/helm/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function containsSource(config: HelmModuleConfig) {
/**
* Render the template in the specified Helm module (locally), and return all the resources in the chart.
*/
export async function getChartResources(ctx: PluginContext, module: Module, log: LogEntry) {
export async function getChartResources(ctx: PluginContext, module: Module, hotReload: boolean, log: LogEntry) {
const chartPath = await getChartPath(module)
const k8sCtx = <KubernetesPluginContext>ctx
const namespace = await getNamespace({
Expand All @@ -64,7 +64,7 @@ export async function getChartResources(ctx: PluginContext, module: Module, log:
releaseName,
"--namespace",
namespace,
...(await getValueFileArgs(module)),
...(await getValueArgs(module, hotReload)),
chartPath,
],
})
Expand Down Expand Up @@ -146,15 +146,21 @@ export function getGardenValuesPath(chartPath: string) {
/**
* Get the value files arguments that should be applied to any helm install/render command.
*/
export async function getValueFileArgs(module: HelmModule) {
export async function getValueArgs(module: HelmModule, hotReload: boolean) {
const chartPath = await getChartPath(module)
const gardenValuesPath = getGardenValuesPath(chartPath)

// The garden-values.yml file (which is created from the `values` field in the module config) takes precedence,
// so it's added to the end of the list.
const valueFiles = module.spec.valueFiles.map((f) => resolve(module.buildPath, f)).concat([gardenValuesPath])

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

if (hotReload) {
args.push("--set", "\\.garden.hotReload=true")
}

return args
}

/**
Expand Down Expand Up @@ -332,7 +338,7 @@ async function renderHelmTemplateString(
releaseName,
"--namespace",
namespace,
...(await getValueFileArgs(module)),
...(await getValueArgs(module, false)),
"-x",
tempFilePath,
chartPath,
Expand Down
10 changes: 5 additions & 5 deletions garden-service/src/plugins/kubernetes/helm/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
getChartResources,
findServiceResource,
getServiceResourceSpec,
getValueFileArgs,
getValueArgs,
} from "./common"
import { getReleaseStatus, HelmServiceStatus } from "./status"
import { configureHotReload, HotReloadableResource } from "../hot-reload"
Expand All @@ -28,7 +28,7 @@ import { DeployServiceParams } from "../../../types/plugin/service/deployService
import { DeleteServiceParams } from "../../../types/plugin/service/deleteService"
import { getForwardablePorts } from "../port-forward"

export async function deployService({
export async function deployHelmService({
ctx,
module,
service,
Expand All @@ -39,7 +39,7 @@ export async function deployService({
let hotReloadSpec: ContainerHotReloadSpec | null = null
let hotReloadTarget: HotReloadableResource | null = null

const chartResources = await getChartResources(ctx, module, log)
const chartResources = await getChartResources(ctx, module, hotReload, log)

if (hotReload) {
const resourceSpec = service.spec.serviceResource
Expand All @@ -53,14 +53,14 @@ export async function deployService({
const chartPath = await getChartPath(module)
const namespace = await getAppNamespace(k8sCtx, log, provider)
const releaseName = getReleaseName(module)
const releaseStatus = await getReleaseStatus(k8sCtx, module, releaseName, log)
const releaseStatus = await getReleaseStatus(k8sCtx, module, releaseName, log, hotReload)

const commonArgs = [
"--namespace",
namespace,
"--timeout",
module.spec.timeout.toString(10),
...(await getValueFileArgs(module)),
...(await getValueArgs(module, hotReload)),
]

if (releaseStatus.state === "missing") {
Expand Down
4 changes: 2 additions & 2 deletions garden-service/src/plugins/kubernetes/helm/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ModuleAndRuntimeActionHandlers } from "../../../types/plugin/plugin"
import { HelmModule, configureHelmModule } from "./config"
import { buildHelmModule } from "./build"
import { getServiceStatus } from "./status"
import { deployService, deleteService } from "./deployment"
import { deployHelmService, deleteService } from "./deployment"
import { getTestResult } from "../test-results"
import { runHelmTask, runHelmModule } from "./run"
import { hotReloadHelmChart } from "./hot-reload"
Expand All @@ -23,7 +23,7 @@ export const helmHandlers: Partial<ModuleAndRuntimeActionHandlers<HelmModule>> =
configure: configureHelmModule,
// TODO: add execInService handler
deleteService,
deployService,
deployService: deployHelmService,
getPortForward: getPortForwardHandler,
getServiceLogs,
getServiceStatus,
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/plugins/kubernetes/helm/hot-reload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function hotReloadHelmChart({
}: HotReloadServiceParams<HelmModule, ContainerModule>): Promise<HotReloadServiceResult> {
const hotReloadSpec = getHotReloadSpec(service)

const chartResources = await getChartResources(ctx, service.module, log)
const chartResources = await getChartResources(ctx, service.module, true, log)
const resourceSpec = service.spec.serviceResource

const workload = await findServiceResource({
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/plugins/kubernetes/helm/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function getServiceLogs(params: GetServiceLogsParams<HelmModule>) {
const provider = k8sCtx.provider
const namespace = await getAppNamespace(k8sCtx, log, provider)

const resources = await getChartResources(k8sCtx, module, log)
const resources = await getChartResources(k8sCtx, module, false, log)

return getAllLogs({ ...params, provider, defaultNamespace: namespace, resources })
}
4 changes: 2 additions & 2 deletions garden-service/src/plugins/kubernetes/helm/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function runHelmModule({
)
}

const chartResources = await getChartResources(k8sCtx, module, log)
const chartResources = await getChartResources(k8sCtx, module, false, log)
const target = await findServiceResource({
ctx: k8sCtx,
log,
Expand Down Expand Up @@ -96,7 +96,7 @@ export async function runHelmTask(params: RunTaskParams<HelmModule>): Promise<Ru
const k8sCtx = <KubernetesPluginContext>ctx

const { command, args } = task.spec
const chartResources = await getChartResources(k8sCtx, module, log)
const chartResources = await getChartResources(k8sCtx, module, false, log)
const resourceSpec = task.spec.resource || getServiceResourceSpec(module)
const target = await findServiceResource({
ctx: k8sCtx,
Expand Down
76 changes: 28 additions & 48 deletions garden-service/src/plugins/kubernetes/helm/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { ServiceStatus, ServiceState } from "../../../types/service"
import { ServiceStatus, ServiceState, ForwardablePort } from "../../../types/service"
import { GetServiceStatusParams } from "../../../types/plugin/service/getServiceStatus"
import { compareDeployedResources } from "../status/status"
import { KubeApi } from "../api"
import { getAppNamespace } from "../namespace"
import { LogEntry } from "../../../logger/log-entry"
import { helm } from "./helm-cli"
import { HelmModule } from "./config"
import { getChartResources, findServiceResource, getReleaseName } from "./common"
import { buildHelmModule } from "./build"
import { configureHotReload } from "../hot-reload"
import { getHotReloadSpec } from "./hot-reload"
import { getReleaseName } from "./common"
import { KubernetesPluginContext } from "../config"
import { getForwardablePorts } from "../port-forward"
import { KubernetesServerResource } from "../types"
import { safeLoadAll } from "js-yaml"

const helmStatusCodeMap: { [code: number]: ServiceState } = {
// see https://github.com/kubernetes/helm/blob/master/_proto/hapi/release/status.proto
Expand All @@ -44,52 +39,34 @@ export type HelmServiceStatus = ServiceStatus<HelmStatusDetail>
export async function getServiceStatus({
ctx,
module,
service,
log,
hotReload,
}: GetServiceStatusParams<HelmModule>): Promise<HelmServiceStatus> {
const k8sCtx = <KubernetesPluginContext>ctx
// need to build to be able to check the status
await buildHelmModule({ ctx: k8sCtx, module, log })

// first check if the installed objects on the cluster match the current code
const chartResources = await getChartResources(k8sCtx, module, log)
const provider = k8sCtx.provider
const namespace = await getAppNamespace(k8sCtx, log, provider)
const releaseName = getReleaseName(module)

const detail: HelmStatusDetail = {}
let state: ServiceState

if (hotReload) {
// If we're running with hot reload enabled, we need to alter the appropriate resources and then compare directly.
const target = await findServiceResource({ ctx: k8sCtx, log, chartResources, module })
const hotReloadSpec = getHotReloadSpec(service)
const resourceSpec = module.spec.serviceResource!

configureHotReload({
target,
hotReloadSpec,
hotReloadArgs: resourceSpec.hotReloadArgs,
containerName: resourceSpec.containerName,
})

const api = await KubeApi.factory(log, provider)

const comparison = await compareDeployedResources(k8sCtx, api, namespace, chartResources, log)
state = comparison.state
detail.remoteResources = comparison.remoteResources
} else {
// Otherwise we trust Helm to report the status of the chart.
try {
const helmStatus = await getReleaseStatus(k8sCtx, module, releaseName, log)
state = helmStatus.state
} catch (err) {
state = "missing"
}
try {
const helmStatus = await getReleaseStatus(k8sCtx, module, releaseName, log, hotReload)
state = helmStatus.state
} catch (err) {
state = "missing"
}

const forwardablePorts = getForwardablePorts(chartResources)
let forwardablePorts: ForwardablePort[] = []

if (state !== "missing") {
const deployedResources = safeLoadAll(
await helm({
ctx: k8sCtx,
log,
args: ["get", "manifest", releaseName],
})
)
forwardablePorts = getForwardablePorts(deployedResources)
}

return {
forwardablePorts,
Expand All @@ -103,33 +80,36 @@ export async function getReleaseStatus(
ctx: KubernetesPluginContext,
module: HelmModule,
releaseName: string,
log: LogEntry
log: LogEntry,
hotReload: boolean
): Promise<ServiceStatus> {
try {
log.silly(`Getting the release status for ${releaseName}`)
const res = JSON.parse(await helm({ ctx, log, args: ["status", releaseName, "--output", "json"] }))
const statusCode = res.info.status.code
let state = helmStatusCodeMap[statusCode]
let values = {}

if (state === "ready") {
// Make sure the right version is deployed
const deployedValues = JSON.parse(
values = JSON.parse(
await helm({
ctx,
log,
args: ["get", "values", releaseName, "--output", "json"],
})
)
const deployedVersion = deployedValues[".garden"] && deployedValues[".garden"].version
const deployedVersion = values[".garden"] && values[".garden"].version
const hotReloadEnabled = values[".garden"] && values[".garden"].hotReload === true

if (!deployedVersion || deployedVersion !== module.version.versionString) {
if ((hotReload && !hotReloadEnabled) || !deployedVersion || deployedVersion !== module.version.versionString) {
state = "outdated"
}
}

return {
state,
detail: res,
detail: { ...res, values },
}
} catch (_) {
// release doesn't exist
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/plugins/kubernetes/helm/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function testHelmModule(params: TestModuleParams<HelmModule>): Prom
const k8sCtx = <KubernetesPluginContext>ctx

// Get the container spec to use for running
const chartResources = await getChartResources(k8sCtx, module, log)
const chartResources = await getChartResources(k8sCtx, module, false, log)
const resourceSpec = testConfig.spec.resource || getServiceResourceSpec(module)
const target = await findServiceResource({ ctx: k8sCtx, log, chartResources, module, resourceSpec })
const container = getResourceContainer(target, resourceSpec.containerName)
Expand Down
19 changes: 18 additions & 1 deletion garden-service/test/data/test-projects/helm/api-image/Dockerfile
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
FROM busybox:1.31.0
# Using official python runtime base image
FROM python:2.7-alpine

# Set the application directory
WORKDIR /app

# Install our requirements.txt
ADD requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt

# Copy our code from the current folder to /app inside the container
ADD . /app

# Make port 80 available for links and/or publish
EXPOSE 80

# Define our command to be run when launching the container
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--log-file", "-", "--access-logfile", "-", "--workers", "4", "--keep-alive", "0"]
52 changes: 52 additions & 0 deletions garden-service/test/data/test-projects/helm/api-image/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from flask import Flask, render_template, request, make_response, g
from flask_cors import CORS
from redis import Redis
import os
import socket
import random
import json

option_a = os.getenv('OPTION_A', "Cats")
option_b = os.getenv('OPTION_B', "Dogs")
hostname = socket.gethostname()

app = Flask(__name__)
CORS(app)

def get_redis():
if not hasattr(g, 'redis'):
g.redis = Redis(host="redis-master", db=0, socket_timeout=5)
return g.redis

@app.route("/vote/", methods=['POST','GET'])
def vote():
voter_id = hex(random.getrandbits(64))[2:-1]

app.logger.info("received request")

vote = None

if request.method == 'POST':
redis = get_redis()
vote = request.form['vote']
data = json.dumps({'voter_id': voter_id, 'vote': vote})

redis.rpush('votes', data)
print("Registered vote")
response = app.response_class(
response=json.dumps(data),
status=200,
mimetype='application/json'
)
return response

response = app.response_class(
response=json.dumps({}),
status=404,
mimetype='application/json'
)
return response


if __name__ == "__main__":
app.run(host='0.0.0.0', port=80, debug=True, threaded=True)
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ hotReload:
sync:
- source: "*"
target: /app
include: [Dockerfile]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Flask
Redis
gunicorn
flask-cors
Loading

0 comments on commit aa06e8e

Please sign in to comment.