diff --git a/garden-service/package-lock.json b/garden-service/package-lock.json index 33c287c83f..3750b94f08 100644 --- a/garden-service/package-lock.json +++ b/garden-service/package-lock.json @@ -870,6 +870,16 @@ "@types/tough-cookie": "*" } }, + "@types/request-promise": { + "version": "4.1.42", + "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.42.tgz", + "integrity": "sha512-b8li55sEZ00BXZstZ3d8WOi48dnapTqB1VufEG9Qox0nVI2JVnTVT1Mw4JbBa1j+1sGVX/qJ0R4WDv4v2GjT0w==", + "dev": true, + "requires": { + "@types/bluebird": "*", + "@types/request": "*" + } + }, "@types/rx": { "version": "4.1.1", "resolved": "http://registry.npmjs.org/@types/rx/-/rx-4.1.1.tgz", @@ -3798,7 +3808,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": false, - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "aproba": { "version": "1.2.0", @@ -3819,12 +3830,14 @@ "balanced-match": { "version": "1.0.0", "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3839,17 +3852,20 @@ "code-point-at": { "version": "1.1.0", "resolved": false, - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": false, - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": false, - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3966,7 +3982,8 @@ "inherits": { "version": "2.0.3", "resolved": false, - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true }, "ini": { "version": "1.3.5", @@ -3978,6 +3995,7 @@ "version": "1.0.0", "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3992,6 +4010,7 @@ "version": "3.0.4", "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3999,12 +4018,14 @@ "minimist": { "version": "0.0.8", "resolved": false, - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true }, "minipass": { "version": "2.2.4", "resolved": false, "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4023,6 +4044,7 @@ "version": "0.5.1", "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4103,7 +4125,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": false, - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4115,6 +4138,7 @@ "version": "1.4.0", "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, "requires": { "wrappy": "1" } @@ -4200,7 +4224,8 @@ "safe-buffer": { "version": "5.1.1", "resolved": false, - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4236,6 +4261,7 @@ "version": "1.0.2", "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4255,6 +4281,7 @@ "version": "3.0.1", "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4298,12 +4325,14 @@ "wrappy": { "version": "1.0.2", "resolved": false, - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true }, "yallist": { "version": "3.0.2", "resolved": false, - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "optional": true } } }, @@ -7490,6 +7519,7 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -7859,7 +7889,8 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "dev": true, + "optional": true }, "is-builtin-module": { "version": "1.0.0", @@ -7955,6 +7986,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -8007,7 +8039,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true + "dev": true, + "optional": true }, "lru-cache": { "version": "4.1.3", @@ -8310,7 +8343,8 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true + "dev": true, + "optional": true }, "require-directory": { "version": "2.1.1", @@ -9764,6 +9798,25 @@ "uuid": "^3.3.2" } }, + "request-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.4.tgz", + "integrity": "sha512-8wgMrvE546PzbR5WbYxUQogUnUDfM0S7QIFZMID+J73vdFARkFy+HElj4T+MWYhpXwlLp0EQ8Zoj8xUA0he4Vg==", + "requires": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "request-promise-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "requires": { + "lodash": "^4.17.11" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -10874,6 +10927,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, "stream-exhaust": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", diff --git a/garden-service/package.json b/garden-service/package.json index bcd0ee39ed..8f7203dd0b 100644 --- a/garden-service/package.json +++ b/garden-service/package.json @@ -75,6 +75,7 @@ "p-queue": "^3.0.0", "path-is-inside": "^1.0.2", "request": "^2.88.0", + "request-promise": "^4.2.4", "split": "^1.0.1", "string-width": "^3.0.0", "strip-ansi": "^5.0.0", @@ -133,6 +134,7 @@ "@types/path-is-inside": "^1.0.0", "@types/prettyjson": "0.0.28", "@types/request": "^2.48.1", + "@types/request-promise": "^4.1.42", "@types/string-width": "^2.0.0", "@types/supertest": "^2.0.7", "@types/tar": "^4.0.0", diff --git a/garden-service/src/plugins/kubernetes/api.ts b/garden-service/src/plugins/kubernetes/api.ts index 7cb3278dcc..7438c911e9 100644 --- a/garden-service/src/plugins/kubernetes/api.ts +++ b/garden-service/src/plugins/kubernetes/api.ts @@ -17,7 +17,7 @@ import { Policy_v1beta1Api, } from "@kubernetes/client-node" import { join } from "path" -import request = require("request") +import request = require("request-promise") import { readFileSync, pathExistsSync } from "fs-extra" import { safeLoad } from "js-yaml" import { zip, omitBy, isObject } from "lodash" @@ -25,6 +25,8 @@ import { GardenBaseError } from "../../exceptions" import { homedir } from "os" import { KubernetesResource } from "./types" import * as dedent from "dedent" +import { LogEntry } from "../../logger/log-entry" +import { splitLast, findByName } from "../../util/util" let kubeConfigStr: string let kubeConfig: any @@ -89,7 +91,7 @@ export class KubeApi { } } - async readBySpec(namespace: string, spec: KubernetesResource) { + async readBySpec(namespace: string, spec: KubernetesResource, log: LogEntry) { // this is just awful, sorry. any better ideas? - JE const name = spec.metadata.name @@ -139,15 +141,78 @@ export class KubeApi { case "PodDisruptionBudget": return this.policy.readNamespacedPodDisruptionBudget(name, namespace) default: + // Handle CRDs const apiVersion = spec.apiVersion - const url = `${this.config.getCurrentCluster()!.server}/apis/${apiVersion}` + - `/namespaces/${namespace}/${spec.kind.toLowerCase()}/${name || spec.metadata.name}` + const baseUrl = `${this.config.getCurrentCluster()!.server}/apis/${apiVersion}` - const opts: request.Options = { method: "get", url, json: true } + const [group, version] = splitLast(apiVersion, "/") + + if (!group || !version) { + throw new KubernetesError(`Invalid apiVersion ${apiVersion}`, { spec }) + } + + let url: string + + if (!group.includes(".") && group.endsWith("k8s.io")) { + // Looks like a built-in object + // TODO: this is awful, need to find out where to look this up... + let plural: string + + if (spec.kind.endsWith("s")) { + plural = spec.kind + "es" + } else if (spec.kind.endsWith("y")) { + plural = spec.kind.slice(0, spec.kind.length - 1) + "ies" + } else { + plural = spec.kind + "s" + } + // /apis/networking.istio.io/v1alpha3/namespaces/gis-backend/virtualservices/gis-elasticsearch-master + // /apis/networking.istio.io/v1alpha3/namespaces/gis-backend/virtualservices/gis-elasticsearch-master + url = spec.metadata.namespace + ? `${baseUrl}/namespaces/${namespace}/${plural}/${name}` + : `${baseUrl}/${plural}/${name}` + + } else { + // Must be a CRD then... + const crd = await this.findCrd(group, version, spec.kind) + + const plural = crd.spec.names.plural + url = crd.spec.scope === "Namespaced" + ? `${baseUrl}/namespaces/${namespace}/${plural}/${name}` + : `${baseUrl}/${plural}/${name}` + } + + log.silly(`GET ${url}`) + + const opts: request.Options = { method: "get", url, json: true, resolveWithFullResponse: true } this.config.applyToRequest(opts) - return request(opts) + try { + return await request(opts) + } catch (err) { + wrapError(err) + } + } + } + + async findCrd(group: string, version: string, kind: string) { + const crds = (await this.apiExtensions.listCustomResourceDefinition()).body + + for (const crd of crds.items) { + if ( + crd.spec.group === group && + crd.status.acceptedNames.kind === kind && + findByName(crd.spec.versions, version) + ) { + return crd + } } + + throw new KubernetesError(`Could not find resource type ${group}/${version}/${kind}`, { + group, + version, + kind, + availableCrds: crds.items, + }) } async upsert( diff --git a/garden-service/src/plugins/kubernetes/helm/tiller.ts b/garden-service/src/plugins/kubernetes/helm/tiller.ts index 330380ddb6..d4973c03b9 100644 --- a/garden-service/src/plugins/kubernetes/helm/tiller.ts +++ b/garden-service/src/plugins/kubernetes/helm/tiller.ts @@ -30,7 +30,7 @@ export async function checkTillerStatus(ctx: PluginContext, provider: Kubernetes ...await getTillerResources(ctx, provider, log), ] - const statuses = await checkResourceStatuses(api, namespace, resources) + const statuses = await checkResourceStatuses(api, namespace, resources, log) return combineStates(statuses.map(s => s.state)) } diff --git a/garden-service/src/plugins/kubernetes/status.ts b/garden-service/src/plugins/kubernetes/status.ts index eb86336a45..d604b763a3 100644 --- a/garden-service/src/plugins/kubernetes/status.ts +++ b/garden-service/src/plugins/kubernetes/status.ts @@ -9,9 +9,9 @@ import { DeploymentError } from "../../exceptions" import { PluginContext } from "../../plugin-context" import { ServiceState, combineStates } from "../../types/service" -import { sleep } from "../../util/util" +import { sleep, encodeYamlMulti } from "../../util/util" import { KubeApi, KubernetesError } from "./api" -import { KUBECTL_DEFAULT_TIMEOUT } from "./kubectl" +import { KUBECTL_DEFAULT_TIMEOUT, kubectl } from "./kubectl" import { getAppNamespace } from "./namespace" import * as Bluebird from "bluebird" import { KubernetesResource } from "./types" @@ -45,7 +45,8 @@ interface WorkloadStatus { type Workload = V1Deployment | V1DaemonSet | V1StatefulSet interface ObjHandler { - (api: KubeApi, namespace: string, obj: KubernetesResource, resourceVersion?: number): Promise + (api: KubeApi, namespace: string, obj: KubernetesResource, log: LogEntry, resourceVersion?: number) + : Promise } const podLogLines = 20 @@ -114,7 +115,7 @@ async function checkPodStatus(obj: KubernetesResource, pods: V1Pod[]): Promise { const out: WorkloadStatus = { state: "unhealthy", @@ -125,7 +126,7 @@ export async function checkWorkloadStatus( let statusRes: Workload try { - statusRes = (await api.readBySpec(namespace, obj)).body + statusRes = (await api.readBySpec(namespace, obj, log)).body } catch (err) { if (err.code && err.code === 404) { // service is not running @@ -299,21 +300,21 @@ export async function checkWorkloadStatus( * Check if the specified Kubernetes objects are deployed and fully rolled out */ export async function checkResourceStatuses( - api: KubeApi, namespace: string, resources: KubernetesResource[], prevStatuses?: WorkloadStatus[], + api: KubeApi, namespace: string, resources: KubernetesResource[], log: LogEntry, prevStatuses?: WorkloadStatus[], ): Promise { return Bluebird.map(resources, async (obj, i) => { - return checkResourceStatus(api, namespace, obj, prevStatuses && prevStatuses[i]) + return checkResourceStatus(api, namespace, obj, log, prevStatuses && prevStatuses[i]) }) } export async function checkResourceStatus( - api: KubeApi, namespace: string, resource: KubernetesResource, prevStatus?: WorkloadStatus, + api: KubeApi, namespace: string, resource: KubernetesResource, log: LogEntry, prevStatus?: WorkloadStatus, ) { const handler = objHandlers[resource.kind] let status: WorkloadStatus if (handler) { try { - status = await handler(api, namespace, resource, prevStatus && prevStatus.resourceVersion) + status = await handler(api, namespace, resource, log, prevStatus && prevStatus.resourceVersion) } catch (err) { // We handle 404s specifically since this might be invoked before some objects are deployed if (err.code === 404) { @@ -363,7 +364,7 @@ export async function waitForResources({ ctx, provider, serviceName, resources: await sleep(2000 + 1000 * loops) loops += 1 - const statuses = await checkResourceStatuses(api, namespace, objects, prevStatuses) + const statuses = await checkResourceStatuses(api, namespace, objects, log, prevStatuses) for (const status of statuses) { if (status.lastError) { @@ -419,11 +420,11 @@ interface ComparisonResult { * Check if each of the given Kubernetes objects matches what's installed in the cluster */ export async function compareDeployedObjects( - ctx: PluginContext, api: KubeApi, namespace: string, objects: KubernetesResource[], log: LogEntry, + ctx: KubernetesPluginContext, api: KubeApi, namespace: string, resources: KubernetesResource[], log: LogEntry, ): Promise { - const k8sCtx = ctx - const maybeDeployedObjects = await Bluebird.map(objects, obj => getDeployedObject(k8sCtx, k8sCtx.provider, obj)) + // First check if any resources are missing from the cluster. + const maybeDeployedObjects = await Bluebird.map(resources, obj => getDeployedObject(ctx, ctx.provider, obj, log)) const deployedObjects = maybeDeployedObjects.filter(o => o !== null) const result: ComparisonResult = { @@ -433,20 +434,59 @@ export async function compareDeployedObjects( const logDescription = (obj: KubernetesResource) => `${obj.kind}/${obj.metadata.name}` - const missingObjectNames = zip(objects, maybeDeployedObjects) + const missingObjectNames = zip(resources, maybeDeployedObjects) .filter(([_, deployed]) => !deployed) .map(([obj, _]) => logDescription(obj!)) - if (missingObjectNames.length > 0) { - // One or more objects is not deployed. - log.silly(`Resource(s) ${missingObjectNames.join(", ")} missing from cluster`) + if (missingObjectNames.length === resources.length) { + // All resources missing. + log.verbose(`All resources missing from cluster`) result.state = "missing" return result + } else if (missingObjectNames.length > 0) { + // One or more objects missing. + log.verbose(`Resource(s) ${missingObjectNames.join(", ")} missing from cluster`) + result.state = "outdated" + return result } + // From here, the state can only be "ready" or "outdated", so we proceed to compare the old & new specs. + + // First we try using `kubectl diff`, to avoid potential normalization issues (i.e. false negatives). This errors + // with exit code 1 if there is a mismatch, but may also fail with the same exit code for a number of other reasons, + // including the cluster not supporting dry-runs, certain CRDs not supporting dry-runs etc. + const yamlResources = await encodeYamlMulti(resources) + + try { + await kubectl(ctx.provider.config.context, namespace) + .call(["diff", "-f", "-"], { data: Buffer.from(yamlResources) }) + + // If the commands exits succesfully, the check was successful and the diff is empty. + log.verbose(`kubectl diff indicates all resources match the deployed resources.`) + result.state = "ready" + return result + } catch (err) { + // Exited with non-zero code. Check for error messages on stderr. If one is there, the command was unable to + // complete the check, so we fall back to our own mechanism. Otherwise the command worked, but one or more resources + // are missing or outdated. + if ( + !err.detail || !err.detail.result + || (!!err.detail.result.stderr && err.detail.result.stderr.trim() !== "exit status 1") + ) { + log.verbose(`kubectl diff failed: ${err.message}`) + } else { + log.verbose(`kubectl diff indicates one or more resources are outdated.`) + log.silly(err.detail.result.stdout) + result.state = "outdated" + return result + } + } + + // Using kubectl diff didn't work, so we fall back to our own comparison check, which works in _most_ cases, + // but doesn't exhaustively handle normalization issues. const deployedObjectStatuses: WorkloadStatus[] = await Bluebird.map( deployedObjects, - async (obj) => checkResourceStatus(api, namespace, obj, undefined)) + async (obj) => checkResourceStatus(api, namespace, obj, log, undefined)) const deployedStates = deployedObjectStatuses.map(s => s.state) if (deployedStates.find(s => s !== "ready")) { @@ -464,10 +504,7 @@ export async function compareDeployedObjects( return result } - // From here, the state can only be "ready" or "outdated", so we proceed to compare the old & new specs. - - for (let [newSpec, existingSpec] of zip(objects, deployedObjects) as KubernetesResource[][]) { - + for (let [newSpec, existingSpec] of zip(resources, deployedObjects) as KubernetesResource[][]) { // the API version may implicitly change when deploying existingSpec.apiVersion = newSpec.apiVersion @@ -485,15 +522,15 @@ export async function compareDeployedObjects( delete newSpec.spec.clusterIP } - // handle properties that are omitted in the response because they have the default value - // (another design issue in the K8s API) // NOTE: this approach won't fly in the long run, but hopefully we can climb out of this mess when // `kubectl diff` is ready, or server-side apply/diff is ready - if (newSpec.kind === "DaemonSet") { + if (newSpec.kind === "DaemonSet" || newSpec.kind === "Deployment" || newSpec.kind == "StatefulSet") { + // handle properties that are omitted in the response because they have the default value + // (another design issue in the K8s API) if (newSpec.spec.minReadySeconds === 0) { delete newSpec.spec.minReadySeconds } - if (newSpec.spec.template.spec.hostNetwork === false) { + if (newSpec.spec.template && newSpec.spec.template.spec && newSpec.spec.template.spec.hostNetwork === false) { delete newSpec.spec.template.spec.hostNetwork } } @@ -524,13 +561,13 @@ export async function compareDeployedObjects( } async function getDeployedObject( - ctx: PluginContext, provider: KubernetesProvider, obj: KubernetesResource, + ctx: PluginContext, provider: KubernetesProvider, obj: KubernetesResource, log: LogEntry, ): Promise { const api = new KubeApi(provider.config.context) const namespace = obj.metadata.namespace || await getAppNamespace(ctx, provider) try { - const res = await api.readBySpec(namespace, obj) + const res = await api.readBySpec(namespace, obj, log) return res.body } catch (err) { if (err.code === 404) { diff --git a/garden-service/src/plugins/openfaas/openfaas.ts b/garden-service/src/plugins/openfaas/openfaas.ts index c2bf964136..17c57fd4e9 100644 --- a/garden-service/src/plugins/openfaas/openfaas.ts +++ b/garden-service/src/plugins/openfaas/openfaas.ts @@ -330,7 +330,7 @@ async function writeStackFile( }) } -async function getServiceStatus({ ctx, module, service }: GetServiceStatusParams) { +async function getServiceStatus({ ctx, module, service, log }: GetServiceStatusParams) { const openFaasCtx = ctx const k8sProvider = getK8sProvider(openFaasCtx) @@ -359,7 +359,7 @@ async function getServiceStatus({ ctx, module, service }: GetServiceStatusParams const container: any = findByName(deployment.spec.template.spec.containers, service.name) const envVersion = findByName(container.env, "GARDEN_VERSION") const version = envVersion ? envVersion.value : undefined - const status = await checkWorkloadStatus(api, namespace, deployment) + const status = await checkWorkloadStatus(api, namespace, deployment, log) return { state: status.state, diff --git a/garden-service/src/util/util.ts b/garden-service/src/util/util.ts index e6d3375975..19f1a014e6 100644 --- a/garden-service/src/util/util.ts +++ b/garden-service/src/util/util.ts @@ -221,12 +221,15 @@ export function spawn(cmd: string, args: string[], opts: SpawnOpts = {}) { }) if (data) { + // This may happen if the spawned process errors while we're still writing data. + proc.stdin!.on("error", () => { }) + proc.stdin!.end(data) } } return new Promise((resolve, reject) => { - let _timeout + let _timeout: NodeJS.Timeout const _reject = (err: GardenError) => { extend(err.detail, result) @@ -285,6 +288,14 @@ export function splitFirst(s: string, delimiter: string) { return [parts[0], parts.slice(1).join(delimiter)] } +/** + * Splits the input string on the last occurrence of `delimiter`. + */ +export function splitLast(s: string, delimiter: string) { + const parts = s.split(delimiter) + return [parts.slice(0, parts.length - 1).join(delimiter), parts[parts.length - 1]] +} + /** * Recursively process all values in the given input, * walking through all object keys _and array items_.