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: add basic support for helm charts #118

Merged
merged 6 commits into from
Jun 14, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,414 changes: 807 additions & 607 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dist"
],
"dependencies": {
"@kubernetes/client-node": "^0.3.0",
"ansi-escapes": "^3.1.0",
"async-exit-hook": "^2.0.1",
"axios": "^0.18.0",
Expand All @@ -34,6 +35,7 @@
"dockerode": "^2.5.5",
"elegant-spinner": "^1.0.1",
"escape-string-regexp": "^1.0.5",
"execa": "^0.10.0",
"fb-watchman": "^2.0.0",
"fs-extra": "^6.0.1",
"has-ansi": "^3.0.0",
Expand All @@ -44,7 +46,6 @@
"js-yaml": "^3.12.0",
"json-stringify-safe": "^5.0.1",
"klaw": "^2.1.1",
"kubernetes-client": "^5.3.0",
"log-symbols": "^2.2.0",
"moment": "^2.22.2",
"node-emoji": "^1.8.1",
Expand All @@ -68,6 +69,7 @@
"@types/chai": "^4.1.3",
"@types/dedent": "^0.7.0",
"@types/dockerode": "^2.5.4",
"@types/execa": "^0.9.0",
"@types/fs-extra": "^5.0.3",
"@types/gulp": "^4.0.5",
"@types/handlebars": "^4.0.38",
Expand Down
10 changes: 5 additions & 5 deletions src/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,15 +486,15 @@ export function createPluginContext(garden: Garden): PluginContext {

getStatus: async () => {
const envStatus: EnvironmentStatusMap = await ctx.getEnvironmentStatus({})
const services = await ctx.getServices()
const services = keyBy(await ctx.getServices(), "name")

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

return {
providers: envStatus,
services: keyBy(serviceStatus, "name"),
services: serviceStatus,
}
},

Expand Down
102 changes: 69 additions & 33 deletions src/plugins/kubernetes/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,10 @@ import {
helpers,
} from "../container"
import { values, every, uniq } from "lodash"
import { deserializeKeys, serializeKeys, splitFirst, sleep } from "../../util/util"
import { deserializeValues, serializeValues, splitFirst, sleep } from "../../util/util"
import { ServiceStatus } from "../../types/service"
import { joiIdentifier } from "../../types/common"
import {
apiGetOrNull,
apiPostOrPut,
coreApi,
} from "./api"
import {
Expand Down Expand Up @@ -97,10 +95,10 @@ export async function getEnvironmentStatus({ ctx, provider }: GetEnvironmentStat
}

const metadataNamespace = getMetadataNamespace(ctx, provider)
const namespacesStatus = await coreApi(context).namespaces.get()
const namespacesStatus = await coreApi(context).listNamespace()
const namespace = await getAppNamespace(ctx, provider)

for (const n of namespacesStatus.items) {
for (const n of namespacesStatus.body.items) {
if (n.metadata.name === namespace && n.status.phase === "Active") {
statusDetail.namespaceReady = true
}
Expand Down Expand Up @@ -154,7 +152,8 @@ export async function destroyEnvironment({ ctx, provider, env }: DestroyEnvironm

try {
// Note: Need to call the delete method with an empty object
await coreApi(context).namespaces(namespace).delete({})
// TODO: any cast is required until https://github.com/kubernetes-client/javascript/issues/52 is fixed
await coreApi(context).deleteNamespace(namespace, <any>{})
} catch (err) {
entry.setError(err.message)
const availableNamespaces = await getAllAppNamespaces(context)
Expand Down Expand Up @@ -207,12 +206,16 @@ export async function execInService(
}

// get a running pod
let res = await coreApi(context).namespaces(namespace).pods.get({
qs: {
labelSelector: `service=${service.name}`,
},
})
const pod = res.items[0]
// NOTE: the awkward function signature called out here: https://github.com/kubernetes-client/javascript/issues/53
const podsRes = await coreApi(context).listNamespacedPod(
namespace,
undefined,
undefined,
undefined,
undefined,
`service=${service.name}`,
)
const pod = podsRes.body.items[0]

if (!pod) {
// This should not happen because of the prior status check, but checking to be sure
Expand All @@ -222,7 +225,7 @@ export async function execInService(
}

// exec in the pod via kubectl
res = await kubectl(context, namespace).tty(["exec", "-it", pod.metadata.name, "--", ...command])
const res = await kubectl(context, namespace).tty(["exec", "-it", pod.metadata.name, "--", ...command])

return { code: res.code, output: res.output }
}
Expand Down Expand Up @@ -301,21 +304,26 @@ export async function testModule(
const ns = getMetadataNamespace(ctx, provider)
const resultKey = getTestResultKey(module, testName, result.version)
const body = {
body: {
apiVersion: "v1",
kind: "ConfigMap",
metadata: {
name: resultKey,
annotations: {
"garden.io/generated": "true",
},
apiVersion: "v1",
kind: "ConfigMap",
metadata: {
name: resultKey,
annotations: {
"garden.io/generated": "true",
},
type: "generic",
data: serializeKeys(testResult),
},
data: serializeValues(testResult),
}

await apiPostOrPut(coreApi(context).namespaces(ns).configmaps, resultKey, body)
try {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be cleaner to abstract this repeated try/catch pattern around the coreApi calls with something similar to the old apiPostOrPut function that was removed in this commit?

8ccd9a1#diff-1300351cb8f4f405710841426a313b36L37

Maybe something like await coreApiRequest(tryFn, catchFn, statusCode)?

await coreApi(context).createNamespacedConfigMap(ns, <any>body)
} catch (err) {
if (err.response && err.response.statusCode === 409) {
await coreApi(context).patchNamespacedConfigMap(resultKey, ns, body)
} else {
throw err
}
}

return testResult
}
Expand All @@ -326,8 +334,17 @@ export async function getTestResult(
const context = provider.config.context
const ns = getMetadataNamespace(ctx, provider)
const resultKey = getTestResultKey(module, testName, version)
const res = await apiGetOrNull(coreApi(context).namespaces(ns).configmaps, resultKey)
return res && <TestResult>deserializeKeys(res.data)

try {
const res = await coreApi(context).readNamespacedConfigMap(resultKey, ns)
return <TestResult>deserializeValues(res.body.data)
} catch (err) {
if (err.response && err.response.statusCode === 404) {
return null
} else {
throw err
}
}
}

export async function getServiceLogs(
Expand Down Expand Up @@ -373,21 +390,30 @@ export async function getServiceLogs(
export async function getConfig({ ctx, provider, key }: GetConfigParams) {
const context = provider.config.context
const ns = getMetadataNamespace(ctx, provider)
const res = await apiGetOrNull(coreApi(context).namespaces(ns).secrets, key.join("."))
const value = res && Buffer.from(res.data.value, "base64").toString()
return { value }

try {
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) {
return { value: null }
} else {
throw err
}
}
}

export async function setConfig({ ctx, provider, key, value }: SetConfigParams) {
// we store configuration in a separate metadata namespace, so that configs aren't cleared when wiping the namespace
const context = provider.config.context
const ns = getMetadataNamespace(ctx, provider)
const name = key.join(".")
const body = {
body: {
apiVersion: "v1",
kind: "Secret",
metadata: {
name: key,
name,
annotations: {
"garden.io/generated": "true",
},
Expand All @@ -397,18 +423,28 @@ export async function setConfig({ ctx, provider, key, value }: SetConfigParams)
},
}

await apiPostOrPut(coreApi(context).namespaces(ns).secrets, key.join("."), body)
try {
await coreApi(context).createNamespacedSecret(ns, <any>body)
} catch (err) {
if (err.response && err.response.statusCode === 409) {
await coreApi(context).patchNamespacedSecret(name, ns, body)
} else {
throw err
}
}

return {}
}

export async function deleteConfig({ ctx, provider, key }: DeleteConfigParams) {
const context = provider.config.context
const ns = getMetadataNamespace(ctx, provider)
const name = key.join(".")

try {
await coreApi(context).namespaces(ns).secrets(key.join(".")).delete()
await coreApi(context).deleteNamespacedSecret(name, ns, <any>{})
} catch (err) {
if (err.code === 404) {
if (err.response && err.response.statusCode === 404) {
return { found: false }
} else {
throw err
Expand Down
83 changes: 47 additions & 36 deletions src/plugins/kubernetes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,65 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import * as K8s from "kubernetes-client"
import { KubeConfig, Core_v1Api, Extensions_v1beta1Api, RbacAuthorization_v1Api } from "@kubernetes/client-node"
import { join } from "path"
import { readFileSync } from "fs"
import { safeLoad } from "js-yaml"
import { zip } from "lodash"

const cachedParams = {}
let kubeConfigStr: string
let kubeConfig: any

function getParams(context: string, namespace?: string) {
let params = cachedParams[namespace || ""]
const configs: { [context: string]: KubeConfig } = {}

if (!params) {
const config = K8s.config.loadKubeconfig()
params = <any>K8s.config.fromKubeconfig(config, context)
// NOTE: be warned, the API of the client library is very likely to change

params.promises = true
params.namespace = namespace
function getConfig(context: string): KubeConfig {
if (!kubeConfigStr) {
kubeConfigStr = readFileSync(process.env.KUBECONFIG || join(process.env.HOME || "/home", ".kube", "config"))
.toString()
kubeConfig = safeLoad(kubeConfigStr)
}

if (!configs[context]) {
const kc = new KubeConfig()

kc.loadFromString(kubeConfigStr)
kc.setCurrentContext(context)

cachedParams[namespace || ""] = params
// FIXME: need to patch a bug in the library here (https://github.com/kubernetes-client/javascript/pull/54)
for (const [a, b] of zip(kubeConfig["clusters"] || [], kc.clusters)) {
if (a && a["cluster"]["insecure-skip-tls-verify"] === true) {
(<any>b).skipTLSVerify = true
}
}

configs[context] = kc
}

return params
return configs[context]
}

export function coreApi(context: string, namespace?: string): any {
return new K8s.Core(getParams(context, namespace))
}
export function coreApi(context: string) {
const config = getConfig(context)
const k8sApi = new Core_v1Api(config.getCurrentCluster().server)
k8sApi.setDefaultAuthentication(config)

export function extensionsApi(context: string, namespace?: string): any {
return new K8s.Extensions(getParams(context, namespace))
return k8sApi
}

export async function apiPostOrPut(api: any, name: string, body: object) {
try {
await api.post(body)
} catch (err) {
if (err.code === 409) {
await api(name).put(body)
} else {
throw err
}
}
export function extensionsApi(context: string) {
const config = getConfig(context)
const k8sApi = new Extensions_v1beta1Api(config.getCurrentCluster().server)
k8sApi.setDefaultAuthentication(config)

return k8sApi
}

export async function apiGetOrNull(api: any, name: string) {
try {
return await api(name).get()
} catch (err) {
if (err.code === 404) {
return null
} else {
throw err
}
}
export function rbacApi(context: string) {
const config = getConfig(context)
const k8sApi = new RbacAuthorization_v1Api(config.getCurrentCluster().server)
k8sApi.setDefaultAuthentication(config)

return k8sApi
}
6 changes: 5 additions & 1 deletion src/plugins/kubernetes/kubectl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ export class Kubectl {
const _reject = (msg: string) => {
const dataStr = data ? data.toString() : null
const details = extend({ args, preparedArgs, msg, data: dataStr }, <any>out)
const err = new RuntimeError(`Failed running 'kubectl ${args.join(" ")}'`, details)

const err = new RuntimeError(
`Failed running 'kubectl ${preparedArgs.join(" ")}': ${out.output}`,
details,
)
reject(err)
}

Expand Down
21 changes: 10 additions & 11 deletions src/plugins/kubernetes/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@ import { name as providerName } from "./kubernetes"
import { AuthenticationError } from "../../exceptions"

export async function createNamespace(context: string, namespace: string) {
await coreApi(context).namespaces.post({
body: {
apiVersion: "v1",
kind: "Namespace",
metadata: {
name: namespace,
annotations: {
"garden.io/generated": "true",
},
// TODO: the types for all the create functions in the library are currently broken
await coreApi(context).createNamespace(<any>{
apiVersion: "v1",
kind: "Namespace",
metadata: {
name: namespace,
annotations: {
"garden.io/generated": "true",
},
},
})
Expand Down Expand Up @@ -62,8 +61,8 @@ export function getMetadataNamespace(ctx: PluginContext, provider: KubernetesPro
}

export async function getAllAppNamespaces(context: string): Promise<string[]> {
const allNamespaces = await coreApi(context).namespaces.get()
return allNamespaces.items
const allNamespaces = await coreApi(context).listNamespace()
return allNamespaces.body.items
.map(n => n.metadata.name)
.filter(n => n.startsWith("garden--"))
}
Loading