Skip to content

Commit

Permalink
refactor(k8s): make deployment and status checks much more robust
Browse files Browse the repository at this point in the history
This fully removes the unused `kubernetes-specs` module type and in
turn leverages Helm properly. Additionally several improvements were
made to rollout status checks, which now support most standard k8s
objects.
  • Loading branch information
edvald committed Jul 24, 2018
1 parent 3dece12 commit 97f7bf6
Show file tree
Hide file tree
Showing 20 changed files with 635 additions and 527 deletions.
1 change: 1 addition & 0 deletions garden-cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const MODULE_CONFIG_FILENAME = "garden.yml"
export const STATIC_DIR = resolve(__dirname, "..", "static")
export const GARDEN_DIR_NAME = ".garden"
export const LOGS_DIR = `${GARDEN_DIR_NAME}/logs`
export const GARDEN_BUILD_VERSION_FILENAME = ".garden-build-version"
export const GARDEN_VERSIONFILE_NAME = ".garden-version"
export const DEFAULT_NAMESPACE = "default"
export const DEFAULT_PORT_PROTOCOL = "TCP"
Expand Down
12 changes: 8 additions & 4 deletions garden-cli/src/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
Omit,
} from "./util/util"
import { ModuleVersion } from "./vcs/base"
import { RuntimeContext } from "./types/service"

export type PluginContextGuard = {
readonly [P in keyof (PluginActionParams | ModuleActionParams<any>)]: (...args: any[]) => Promise<any>
Expand All @@ -95,7 +96,8 @@ export type PluginContextParams<T extends PluginActionParamsBase> = Omit<T, keyo
export type PluginContextModuleParams<T extends PluginModuleActionParamsBase> =
Omit<T, "module" | keyof PluginActionContextParams> & { moduleName: string }
export type PluginContextServiceParams<T extends PluginServiceActionParamsBase> =
Omit<T, "module" | "service" | keyof PluginActionContextParams> & { serviceName: string }
Omit<T, "module" | "service" | "runtimeContext" | keyof PluginActionContextParams>
& { serviceName: string, runtimeContext?: RuntimeContext }

export type WrappedFromGarden = Pick<Garden,
"projectName" |
Expand Down Expand Up @@ -231,6 +233,7 @@ export function createPluginContext(garden: Garden): PluginContext {
...<object>omit(params, ["moduleName"]),
module,
service: await service.resolveConfig({ provider, ...runtimeContext }),
runtimeContext,
}

return (<Function>handler)(handlerParams)
Expand Down Expand Up @@ -483,9 +486,10 @@ export function createPluginContext(garden: Garden): PluginContext {
const envStatus: EnvironmentStatusMap = await ctx.getEnvironmentStatus({})
const services = keyBy(await ctx.getServices(), "name")

const serviceStatus = await Bluebird.props(mapValues(services,
(service: Service<any>) => ctx.getServiceStatus({ serviceName: service.name }),
))
const serviceStatus = await Bluebird.props(mapValues(services, async (service: Service) => {
const runtimeContext = await service.prepareRuntimeContext()
return ctx.getServiceStatus({ serviceName: service.name, runtimeContext })
}))

return {
providers: envStatus,
Expand Down
6 changes: 3 additions & 3 deletions garden-cli/src/plugins/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ import {
} from "../types/test"
import { spawn } from "../util/util"
import { writeVersionFile, readVersionFile, getVersionString } from "../vcs/base"
import { GARDEN_BUILD_VERSION_FILENAME } from "../constants"
import execa = require("execa")

export const name = "generic"
export const buildVersionFilename = ".garden-build-version"

export interface GenericTestSpec extends BaseTestSpec {
command: string[],
Expand Down Expand Up @@ -100,7 +100,7 @@ export async function getGenericModuleBuildStatus({ module }: GetModuleBuildStat
return { ready: true }
}

const buildVersionFilePath = join(await module.getBuildPath(), buildVersionFilename)
const buildVersionFilePath = join(await module.getBuildPath(), GARDEN_BUILD_VERSION_FILENAME)
const builtVersion = await readVersionFile(buildVersionFilePath)
const moduleVersion = await module.getVersion()

Expand All @@ -127,7 +127,7 @@ export async function buildGenericModule({ module }: BuildModuleParams<GenericMo
)

// keep track of which version has been built
const buildVersionFilePath = join(buildPath, buildVersionFilename)
const buildVersionFilePath = join(buildPath, GARDEN_BUILD_VERSION_FILENAME)
const version = await module.getVersion()
await writeVersionFile(buildVersionFilePath, {
latestCommit: version.versionString,
Expand Down
4 changes: 2 additions & 2 deletions garden-cli/src/plugins/google/google-cloud-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const gardenPlugin = (): GardenPlugin => ({
parseModule: parseGcfModule,

async deployService(
{ ctx, provider, module, service, env }: DeployServiceParams<GcfModule>,
{ ctx, provider, module, service, env, runtimeContext }: DeployServiceParams<GcfModule>,
) {
// TODO: provide env vars somehow to function
const project = getProject(service, provider)
Expand All @@ -125,7 +125,7 @@ export const gardenPlugin = (): GardenPlugin => ({
"--trigger-http",
])

return getServiceStatus({ ctx, provider, module, service, env })
return getServiceStatus({ ctx, provider, module, service, env, runtimeContext })
},

async getServiceOutputs({ service, provider }: GetServiceOutputsParams<GcfModule>) {
Expand Down
54 changes: 15 additions & 39 deletions garden-cli/src/plugins/kubernetes/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,12 @@

import * as inquirer from "inquirer"
import * as Joi from "joi"
import * as split from "split"
import moment = require("moment")

import { DeploymentError, NotFoundError, TimeoutError } from "../../exceptions"
import {
GetServiceLogsResult,
LoginStatus,
} from "../../types/plugin/outputs"
import {
RunResult,
TestResult,
} from "../../types/plugin/outputs"
import { GetServiceLogsResult, LoginStatus } from "../../types/plugin/outputs"
import { RunResult, TestResult } from "../../types/plugin/outputs"
import {
ConfigureEnvironmentParams,
DeleteConfigParams,
Expand All @@ -27,43 +23,28 @@ import {
GetEnvironmentStatusParams,
GetServiceLogsParams,
GetServiceOutputsParams,
GetServiceStatusParams,
GetTestResultParams,
PluginActionParamsBase,
RunModuleParams,
SetConfigParams,
TestModuleParams,
} from "../../types/plugin/params"
import { ModuleVersion } from "../../vcs/base"
import {
ContainerModule,
helpers,
} from "../container"
import { ContainerModule, helpers } from "../container"
import { uniq } from "lodash"
import { deserializeValues, serializeValues, splitFirst, sleep } from "../../util/util"
import { ServiceStatus } from "../../types/service"
import { joiIdentifier } from "../../types/common"
import {
coreApi,
} from "./api"
import { coreApi } from "./api"
import {
getAppNamespace,
getMetadataNamespace,
getAllGardenNamespaces,
} from "./namespace"
import {
KUBECTL_DEFAULT_TIMEOUT,
kubectl,
} from "./kubectl"
import { KUBECTL_DEFAULT_TIMEOUT, kubectl } from "./kubectl"
import { DEFAULT_TEST_TIMEOUT } from "../../constants"
import * as split from "split"
import moment = require("moment")
import { EntryStyle, LogSymbolType } from "../../logger/types"
import {
checkDeploymentStatus,
} from "./status"

import { name as providerName } from "./kubernetes"
import { getContainerServiceStatus } from "./deployment"

const MAX_STORED_USERNAMES = 5

Expand Down Expand Up @@ -102,11 +83,6 @@ export async function configureEnvironment({ }: ConfigureEnvironmentParams) {
return {}
}

export async function getServiceStatus(params: GetServiceStatusParams<ContainerModule>): Promise<ServiceStatus> {
// TODO: hash and compare all the configuration files (otherwise internal changes don't get deployed)
return await checkDeploymentStatus(params)
}

export async function destroyEnvironment({ ctx, provider }: DestroyEnvironmentParams) {
const { context } = provider.config
const namespace = await getAppNamespace(ctx, provider)
Expand Down Expand Up @@ -155,10 +131,10 @@ export async function getServiceOutputs({ service }: GetServiceOutputsParams<Con
}

export async function execInService(
{ ctx, provider, module, service, env, command }: ExecInServiceParams<ContainerModule>,
{ ctx, provider, module, service, env, command, runtimeContext }: ExecInServiceParams<ContainerModule>,
) {
const context = provider.config.context
const status = await getServiceStatus({ ctx, provider, module, service, env })
const status = await getContainerServiceStatus({ ctx, provider, module, service, env, runtimeContext })
const namespace = await getAppNamespace(ctx, provider)

// TODO: this check should probably live outside of the plugin
Expand Down Expand Up @@ -282,7 +258,7 @@ export async function testModule(
try {
await coreApi(context).createNamespacedConfigMap(ns, <any>body)
} catch (err) {
if (err.response && err.response.statusCode === 409) {
if (err.code === 409) {
await coreApi(context).patchNamespacedConfigMap(resultKey, ns, body)
} else {
throw err
Expand All @@ -303,7 +279,7 @@ export async function getTestResult(
const res = await coreApi(context).readNamespacedConfigMap(resultKey, ns)
return <TestResult>deserializeValues(res.body.data)
} catch (err) {
if (err.response && err.response.statusCode === 404) {
if (err.code === 404) {
return null
} else {
throw err
Expand Down Expand Up @@ -359,7 +335,7 @@ export async function getConfig({ ctx, provider, key }: GetConfigParams) {
const res = await coreApi(context).readNamespacedSecret(key.join("."), ns)
return { value: Buffer.from(res.body.data.value, "base64").toString() }
} catch (err) {
if (err.response && err.response.statusCode === 404) {
if (err.code === 404) {
return { value: null }
} else {
throw err
Expand Down Expand Up @@ -390,7 +366,7 @@ export async function setConfig({ ctx, provider, key, value }: SetConfigParams)
try {
await coreApi(context).createNamespacedSecret(ns, <any>body)
} catch (err) {
if (err.response && err.response.statusCode === 409) {
if (err.code === 409) {
await coreApi(context).patchNamespacedSecret(name, ns, body)
} else {
throw err
Expand All @@ -408,7 +384,7 @@ export async function deleteConfig({ ctx, provider, key }: DeleteConfigParams) {
try {
await coreApi(context).deleteNamespacedSecret(name, ns, <any>{})
} catch (err) {
if (err.response && err.response.statusCode === 404) {
if (err.code === 404) {
return { found: false }
} else {
throw err
Expand Down
77 changes: 71 additions & 6 deletions garden-cli/src/plugins/kubernetes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,23 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { KubeConfig, Core_v1Api, Extensions_v1beta1Api, RbacAuthorization_v1Api } from "@kubernetes/client-node"
import {
KubeConfig,
Core_v1Api,
Extensions_v1beta1Api,
RbacAuthorization_v1Api,
Apps_v1Api,
} from "@kubernetes/client-node"
import { join } from "path"
import { readFileSync } from "fs"
import { safeLoad } from "js-yaml"
import {
zip,
isFunction,
omitBy,
isObject,
} from "lodash"
import { GardenBaseError } from "../../exceptions"
import { GardenBaseError, ConfigurationError } from "../../exceptions"
import { KubernetesObject } from "./helm"

let kubeConfigStr: string
let kubeConfig: any
Expand Down Expand Up @@ -62,6 +69,12 @@ export function extensionsApi(context: string) {
return proxyApi(k8sApi, config)
}

export function appsApi(context: string) {
const config = getConfig(context)
const k8sApi = new Apps_v1Api(config.getCurrentCluster().server)
return proxyApi(k8sApi, config)
}

export function rbacApi(context: string) {
const config = getConfig(context)
const k8sApi = new RbacAuthorization_v1Api(config.getCurrentCluster().server)
Expand All @@ -78,7 +91,7 @@ export class KubernetesError extends GardenBaseError {
/**
* Wrapping the API objects to deal with bugs.
*/
function proxyApi<T extends Core_v1Api | Extensions_v1beta1Api | RbacAuthorization_v1Api>(
function proxyApi<T extends Core_v1Api | Extensions_v1beta1Api | RbacAuthorization_v1Api | Apps_v1Api>(
api: T, config: KubeConfig,
): T {
api.setDefaultAuthentication(config)
Expand All @@ -87,10 +100,9 @@ function proxyApi<T extends Core_v1Api | Extensions_v1beta1Api | RbacAuthorizati
if (!err.message) {
const wrapped = new KubernetesError(`Got error from Kubernetes API - ${err.body.message}`, {
body: err.body,
request: omitBy(err.response.request, (v, k) => isFunction(v) || k[0] === "_"),
request: omitBy(err.response.request, (v, k) => isObject(v) || k[0] === "_"),
})
wrapped.code = err.response.statusCode
wrapped.response = err.response
throw wrapped
} else {
throw err
Expand Down Expand Up @@ -124,3 +136,56 @@ function proxyApi<T extends Core_v1Api | Extensions_v1beta1Api | RbacAuthorizati
},
})
}

export async function apiReadBySpec(namespace: string, context: string, spec: KubernetesObject) {
// this is just awful, sorry. any better ideas? - JE
const name = spec.metadata.name

const core = coreApi(context)
const ext = extensionsApi(context)
const apps = appsApi(context)
const rbac = rbacApi(context)

switch (spec.kind) {
case "ConfigMap":
return core.readNamespacedConfigMap(name, namespace)
case "Endpoints":
return core.readNamespacedEndpoints(name, namespace)
case "LimitRange":
return core.readNamespacedLimitRange(name, namespace)
case "PersistentVolumeClaim":
return core.readNamespacedPersistentVolumeClaim(name, namespace)
case "Pod":
return core.readNamespacedPod(name, namespace)
case "PodTemplate":
return core.readNamespacedPodTemplate(name, namespace)
case "ReplicationController":
return core.readNamespacedReplicationController(name, namespace)
case "ResourceQuota":
return core.readNamespacedResourceQuota(name, namespace)
case "Secret":
return core.readNamespacedSecret(name, namespace)
case "Service":
return core.readNamespacedService(name, namespace)
case "ServiceAccount":
return core.readNamespacedServiceAccount(name, namespace)
case "DaemonSet":
return ext.readNamespacedDaemonSet(name, namespace)
case "Deployment":
return ext.readNamespacedDeployment(name, namespace)
case "Ingress":
return ext.readNamespacedIngress(name, namespace)
case "ReplicaSet":
return ext.readNamespacedReplicaSet(name, namespace)
case "StatefulSet":
return apps.readNamespacedStatefulSet(name, namespace)
case "Role":
return rbac.readNamespacedRole(name, namespace)
case "RoleBinding":
return rbac.readNamespacedRoleBinding(name, namespace)
default:
throw new ConfigurationError(`Unsupported Kubernetes spec kind: ${spec.kind}`, {
spec,
})
}
}
Loading

0 comments on commit 97f7bf6

Please sign in to comment.