Skip to content

Commit

Permalink
refactor(k8s): update kubernetes API library to 0.10.1 and refactor w…
Browse files Browse the repository at this point in the history
…rapper

This should overall make the API methods more nice and easy to use, and
imports some good fixes that we can use.
  • Loading branch information
edvald committed Jun 20, 2019
1 parent 68e1ccc commit bd54a4e
Show file tree
Hide file tree
Showing 18 changed files with 280 additions and 327 deletions.
247 changes: 50 additions & 197 deletions garden-service/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion garden-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"static"
],
"dependencies": {
"@kubernetes/client-node": "0.8.2",
"@kubernetes/client-node": "0.10.1",
"@types/koa-static": "^4.0.1",
"JSONStream": "^1.3.5",
"analytics-node": "3.3.0",
Expand Down Expand Up @@ -169,6 +169,7 @@
"nyc": "^14.1.1",
"p-event": "^4.1.0",
"pegjs": "^0.10.0",
"replace-in-file": "^4.1.0",
"shx": "^0.3.2",
"snyk": "^1.163.3",
"testdouble": "^3.11.0",
Expand Down
117 changes: 76 additions & 41 deletions garden-service/src/plugins/kubernetes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,35 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

// No idea why tslint complains over this line
// tslint:disable-next-line:no-unused
import { IncomingMessage } from "http"
import { resolve } from "url"
import {
KubeConfig,
Core_v1Api,
Extensions_v1beta1Api,
RbacAuthorization_v1Api,
Apps_v1Api,
Apiextensions_v1beta1Api,
V1Secret,
Policy_v1beta1Api,
CoreApi,
ApisApi,
V1APIGroup,
V1APIVersions,
V1APIResource,
CoreV1Api,
ExtensionsV1beta1Api,
RbacAuthorizationV1Api,
AppsV1Api,
ApiextensionsV1beta1Api,
PolicyV1beta1Api,
KubernetesObject,
} from "@kubernetes/client-node"
import AsyncLock = require("async-lock")
import request = require("request-promise")
import requestErrors = require("request-promise/errors")
import { safeLoad, safeDump } from "js-yaml"

import { Omit } from "../../util/util"
import { zip, omitBy, isObject, keyBy } from "lodash"
import { zip, omitBy, isObject, isPlainObject, keyBy } from "lodash"
import { GardenBaseError, RuntimeError, ConfigurationError } from "../../exceptions"
import { KubernetesResource } from "./types"
import { KubernetesResource, KubernetesServerResource, KubernetesServerList } from "./types"
import { LogEntry } from "../../logger/log-entry"
import { kubectl } from "./kubectl"

Expand Down Expand Up @@ -60,23 +64,23 @@ const apiInfoLock = new AsyncLock()

// NOTE: be warned, the API of the client library is very likely to change

type K8sApi = Core_v1Api
| Extensions_v1beta1Api
| RbacAuthorization_v1Api
| Apps_v1Api
| Apiextensions_v1beta1Api
| Policy_v1beta1Api
type K8sApi = CoreV1Api
| ExtensionsV1beta1Api
| RbacAuthorizationV1Api
| AppsV1Api
| ApiextensionsV1beta1Api
| PolicyV1beta1Api
type K8sApiConstructor<T extends K8sApi> = new (basePath?: string) => T

const apiTypes: { [key: string]: K8sApiConstructor<any> } = {
apiExtensions: Apiextensions_v1beta1Api,
apiExtensions: ApiextensionsV1beta1Api,
apis: ApisApi,
apps: Apps_v1Api,
core: Core_v1Api,
apps: AppsV1Api,
core: CoreV1Api,
coreApi: CoreApi,
extensions: Extensions_v1beta1Api,
policy: Policy_v1beta1Api,
rbac: RbacAuthorization_v1Api,
extensions: ExtensionsV1beta1Api,
policy: PolicyV1beta1Api,
rbac: RbacAuthorizationV1Api,
}

const crudMap = {
Expand All @@ -99,15 +103,39 @@ export class KubernetesError extends GardenBaseError {
response?: any
}

interface List {
items?: Array<any>
}

type WrappedList<T extends List> = T["items"] extends Array<infer V> ? KubernetesServerList<V> : KubernetesServerList

// This describes the API classes on KubeApi after they've been wrapped with KubeApi.wrapApi()
type WrappedApi<T> = {
// Wrap each API method
[P in keyof T]:
T[P] extends (...args: infer A) => Promise<{ response: IncomingMessage, body: infer U }>
? (
// If so we wrap it and return the `body` part of the output directly and...
// If it's a list, we cast to a KubernetesServerList, which in turn wraps the array type
U extends List ? (...args: A) => Promise<WrappedList<U>> :
// If it's a resource, we wrap it as a KubernetesResource which makes some attributes required
// (as they should be)
U extends KubernetesObject ? (...args: A) => Promise<KubernetesServerResource<U>> :
// Otherwise we keep the body output type as-is
(...args: A) => Promise<U>
) :
T[P]
}

export class KubeApi {
public apiExtensions: Apiextensions_v1beta1Api
public apis: ApisApi
public apps: Apps_v1Api
public core: Core_v1Api
public coreApi: CoreApi
public extensions: Extensions_v1beta1Api
public policy: Policy_v1beta1Api
public rbac: RbacAuthorization_v1Api
public apiExtensions: WrappedApi<ApiextensionsV1beta1Api>
public apis: WrappedApi<ApisApi>
public apps: WrappedApi<AppsV1Api>
public core: WrappedApi<CoreV1Api>
public coreApi: WrappedApi<CoreApi>
public extensions: WrappedApi<ExtensionsV1beta1Api>
public policy: WrappedApi<PolicyV1beta1Api>
public rbac: WrappedApi<RbacAuthorizationV1Api>

constructor(public context: string, private config: KubeConfig) {
const cluster = this.config.getCurrentCluster()
Expand All @@ -121,7 +149,7 @@ export class KubeApi {

for (const [name, cls] of Object.entries(apiTypes)) {
const api = new cls(cluster.server)
this[name] = this.proxyApi(api, this.config)
this[name] = this.wrapApi(api, this.config)
}
}

Expand All @@ -140,7 +168,7 @@ export class KubeApi {
const coreApi = await this.coreApi.getAPIVersions()
const apis = await this.apis.getAPIVersions()

const coreGroups: V1APIGroup[] = coreApi.body.versions.map(version => ({
const coreGroups: V1APIGroup[] = coreApi.versions.map(version => ({
apiVersion: "v1",
kind: "ApiGroup",
name: version,
Expand All @@ -158,10 +186,10 @@ export class KubeApi {
version,
},
],
serverAddressByClientCIDRs: coreApi.body.serverAddressByClientCIDRs,
serverAddressByClientCIDRs: coreApi.serverAddressByClientCIDRs,
}))

const groups = coreGroups.concat(apis.body.groups)
const groups = coreGroups.concat(apis.groups)
const groupMap: ApiGroupMap = {}

for (const group of groups) {
Expand All @@ -171,7 +199,7 @@ export class KubeApi {
}

const info = {
coreApi: coreApi.body,
coreApi,
groups,
groupMap,
resources: {},
Expand Down Expand Up @@ -202,7 +230,7 @@ export class KubeApi {
async getApiResourceInfo(log: LogEntry, manifest: KubernetesResource): Promise<ApiResourceInfo> {
const apiInfo = await this.getApiInfo()
const group = await this.getApiGroup(manifest)
const groupId = group.preferredVersion.groupVersion
const groupId = group.preferredVersion!.groupVersion

const lockKey = `${this.context}/${groupId}`
const resourceMap = apiInfo.resources[groupId] || await apiInfoLock.acquire(lockKey, async () => {
Expand Down Expand Up @@ -260,7 +288,7 @@ export class KubeApi {
log.silly(`Fetching Kubernetes resource ${manifest.apiVersion}/${manifest.kind}/${name}`)

const { group, resource } = await this.getApiResourceInfo(log, manifest)
const groupId = group.preferredVersion.groupVersion
const groupId = group.preferredVersion!.groupVersion
const basePath = getGroupBasePath(groupId)

const apiPath = resource.namespaced
Expand Down Expand Up @@ -300,7 +328,7 @@ export class KubeApi {
/**
* Wrapping the API objects to deal with bugs.
*/
private proxyApi<T extends K8sApi>(api: T, config: KubeConfig): T {
private wrapApi<T extends K8sApi>(api: T, config: KubeConfig): T {
api.setDefaultAuthentication(config)

return new Proxy(api, {
Expand All @@ -321,13 +349,20 @@ export class KubeApi {
target["defaultHeaders"] = defaultHeaders

if (typeof output.then === "function") {
// the API errors are not properly formed Error objects
return output.catch((err: Error) => {
throw wrapError(err)
})
} else {
return output
// return the result body direcly
.then((res: any) => {
if (isPlainObject(res) && res["body"] !== undefined) {
return res["body"]
}
})
// the API errors are not properly formed Error objects
.catch((err: Error) => {
throw wrapError(err)
})
}

return output
}
},
})
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/plugins/kubernetes/container/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ async function getBuilderPodName(provider: KubernetesProvider, log: LogEntry) {
const api = await KubeApi.factory(log, provider.config.context)

const builderStatusRes = await api.apps.readNamespacedDeployment(dockerDaemonDeploymentName, systemNamespace)
const builderPods = await getPods(api, systemNamespace, builderStatusRes.body.spec.selector.matchLabels)
const builderPods = await getPods(api, systemNamespace, builderStatusRes.spec.selector.matchLabels)
const pod = builderPods[0]

if (!pod) {
Expand Down
13 changes: 8 additions & 5 deletions garden-service/src/plugins/kubernetes/container/ingress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { KubeApi } from "../api"
import { ConfigurationError, PluginError } from "../../../exceptions"
import { ensureSecret } from "../secrets"
import { getHostnamesFromPem } from "../../../util/tls"
import { KubernetesResource } from "../types"
import { V1Secret } from "@kubernetes/client-node"

interface ServiceIngressWithCert extends ServiceIngress {
spec: ContainerIngressSpec
Expand Down Expand Up @@ -136,10 +138,10 @@ async function getCertificateHostnames(api: KubeApi, cert: IngressTlsCertificate
return certificateHostnames[cert.name]
} else {
// pull secret via secret ref from k8s
let res
let secret: KubernetesResource<V1Secret>

try {
res = await api.core.readNamespacedSecret(cert.secretRef.name, cert.secretRef.namespace)
secret = await api.core.readNamespacedSecret(cert.secretRef.name, cert.secretRef.namespace)
} catch (err) {
if (err.code === 404) {
throw new ConfigurationError(
Expand All @@ -150,16 +152,17 @@ async function getCertificateHostnames(api: KubeApi, cert: IngressTlsCertificate
throw err
}
}
const secret = res.body

if (!secret.data["tls.crt"] || !secret.data["tls.key"]) {
const data = secret.data!

if (!data["tls.crt"] || !data["tls.key"]) {
throw new ConfigurationError(
`Secret '${cert.secretRef.name}' is not a valid TLS secret (missing tls.crt and/or tls.key).`,
cert,
)
}

const crtData = Buffer.from(secret.data["tls.crt"], "base64").toString()
const crtData = Buffer.from(data["tls.crt"], "base64").toString()

try {
return getHostnamesFromPem(crtData)
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/plugins/kubernetes/container/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export async function execInService(params: ExecInServiceParams<ContainerModule>
undefined,
`service=${service.name}`,
)
const pod = podsRes.body.items[0]
const pod = podsRes.items[0]

if (!pod) {
// This should not happen because of the prior status check, but checking to be sure
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/plugins/kubernetes/helm/tiller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ async function getTillerResources(
return safeLoadAll(tillerManifests)
}

function getRoleResources(namespace: string): KubernetesResource[] {
function getRoleResources(namespace: string) {
return [
{
apiVersion: "v1",
Expand Down
7 changes: 3 additions & 4 deletions garden-service/src/plugins/kubernetes/hot-reload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@

import * as Bluebird from "bluebird"
import * as execa from "execa"
import { V1Deployment, V1DaemonSet, V1StatefulSet, V1ObjectMeta } from "@kubernetes/client-node"
import { V1Deployment, V1DaemonSet, V1StatefulSet } from "@kubernetes/client-node"
import { ContainerModule, ContainerHotReloadSpec } from "../container/config"
import { RuntimeError, ConfigurationError } from "../../exceptions"
import { resolve as resolvePath, normalize, dirname } from "path"
import { Omit } from "../../util/util"
import { deline } from "../../util/string"
import { set } from "lodash"
import { Service } from "../../types/service"
Expand All @@ -24,11 +23,11 @@ import { RSYNC_PORT } from "./constants"
import { getAppNamespace } from "./namespace"
import { KubernetesPluginContext } from "./config"
import { HotReloadServiceParams, HotReloadServiceResult } from "../../types/plugin/service/hotReloadService"
import { KubernetesResource } from "./types"

export const RSYNC_PORT_NAME = "garden-rsync"

export type HotReloadableResource = Omit<V1Deployment | V1DaemonSet | V1StatefulSet, "status" | "metadata">
& { metadata: Partial<V1ObjectMeta> }
export type HotReloadableResource = KubernetesResource<V1Deployment | V1DaemonSet | V1StatefulSet>

export type HotReloadableKind = "Deployment" | "DaemonSet" | "StatefulSet"

Expand Down
4 changes: 2 additions & 2 deletions garden-service/src/plugins/kubernetes/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export async function ensureNamespace(api: KubeApi, namespace: string) {
created[namespace] = "pending"
const namespacesStatus = await api.core.listNamespace()

for (const n of namespacesStatus.body.items) {
for (const n of namespacesStatus.items) {
if (n.status.phase === "Active") {
created[n.metadata.name] = "created"
}
Expand Down Expand Up @@ -131,7 +131,7 @@ export function getMetadataNamespace(ctx: PluginContext, log: LogEntry, provider

export async function getAllNamespaces(api: KubeApi): Promise<string[]> {
const allNamespaces = await api.core.listNamespace()
return allNamespaces.body.items
return allNamespaces.items
.map(n => n.metadata.name)
}

Expand Down
7 changes: 4 additions & 3 deletions garden-service/src/plugins/kubernetes/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getMetadataNamespace } from "./namespace"
import { GetSecretParams } from "../../types/plugin/provider/getSecret"
import { SetSecretParams } from "../../types/plugin/provider/setSecret"
import { DeleteSecretParams } from "../../types/plugin/provider/deleteSecret"
import { KubernetesResource } from "./types"

export async function getSecret({ ctx, log, key }: GetSecretParams) {
const k8sCtx = <KubernetesPluginContext>ctx
Expand All @@ -23,7 +24,7 @@ export async function getSecret({ ctx, log, key }: GetSecretParams) {

try {
const res = await api.core.readNamespacedSecret(key, ns)
return { value: Buffer.from(res.body.data.value, "base64").toString() }
return { value: Buffer.from(res.data!.value, "base64").toString() }
} catch (err) {
if (err.code === 404) {
return { value: null }
Expand Down Expand Up @@ -87,10 +88,10 @@ export async function deleteSecret({ ctx, log, key }: DeleteSecretParams) {
* Make sure the specified secret exists in the target namespace, copying it if necessary.
*/
export async function ensureSecret(api: KubeApi, secretRef: SecretRef, targetNamespace: string) {
let secret: V1Secret
let secret: KubernetesResource<V1Secret>

try {
secret = (await api.core.readNamespacedSecret(secretRef.name, secretRef.namespace)).body
secret = await api.core.readNamespacedSecret(secretRef.name, secretRef.namespace)
} catch (err) {
if (err.code === 404) {
throw new ConfigurationError(
Expand Down
Loading

0 comments on commit bd54a4e

Please sign in to comment.