UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

160 lines (152 loc) 6.19 kB
/* * Copyright © 2020 Atomist, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { logger } from "@atomist/automation-client/lib/util/logger"; import * as k8s from "@kubernetes/client-node"; import * as _ from "lodash"; import { decrypt, encrypt, } from "../support/crypto"; import { k8sErrMsg } from "../support/error"; import { logRetry } from "../support/retry"; import { applicationLabels } from "./labels"; import { metadataTemplate } from "./metadata"; import { patchHeaders } from "./patch"; import { appName, KubernetesApplication, KubernetesResourceRequest, KubernetesSdm, } from "./request"; import { logObject } from "./resource"; /** * Create application secrets if they do not exist. If a secret in * `req.secrets` exists, the secret is patched. The provided secrets * are merged through [[secretTemplate]] before creating/patching. If * `req.secrets` is false or any empty array, no secrets are modified. * * @param req Kuberenetes application request * @return Array of secret specs created/patched, which array may be empty */ export async function upsertSecrets(req: KubernetesResourceRequest): Promise<k8s.V1Secret[]> { const slug = appName(req); if (!req.secrets || req.secrets.length < 1) { logger.debug(`No secrets provided, will not create secrets for ${slug}`); return []; } const ss: k8s.V1Secret[] = await Promise.all(req.secrets.map(async secret => { const secretName = `${req.ns}/${secret.metadata.name}`; const spec = await secretTemplate(req, secret); try { await req.clients.core.readNamespacedSecret(secret.metadata.name, spec.metadata.namespace); } catch (e) { logger.debug(`Failed to read secret ${secretName}, creating: ${k8sErrMsg(e)}`); logger.info(`Creating secret ${slug} using '${logObject(spec)}'`); await logRetry(() => req.clients.core.createNamespacedSecret(spec.metadata.namespace, spec), `create secret ${secretName} for ${slug}`); return spec; } logger.info(`Secret ${secretName} exists, patching using '${logObject(spec)}'`); await logRetry(() => req.clients.core.patchNamespacedSecret(secret.metadata.name, spec.metadata.namespace, spec, undefined, undefined, undefined, undefined, patchHeaders(req)), `patch secret ${secretName} for ${slug}`); return spec; })); return ss; } /** * Add labels to a secret so we can delete it later. * * @param secret the unlabeled secret * @return the provided secret with appropriate labels */ export async function secretTemplate(req: KubernetesApplication & KubernetesSdm, secret: k8s.V1Secret): Promise<k8s.V1Secret> { const labels = applicationLabels({ ...req, component: "secret" }); const metadata = metadataTemplate({ namespace: req.ns, labels, }); const apiVersion = "v1"; const kind = "Secret"; const s: k8s.V1Secret = { type: "Opaque", metadata, }; _.merge(s, secret, { apiVersion, kind }); s.metadata.namespace = req.ns; return s; } /** * Create encoded opaque secret object from key/value pairs. * * @param secrets Key/value pairs of secrets, the values will be base64 encoded in the returned secret * @return Kubernetes secret object */ export function encodeSecret(name: string, data: { [key: string]: string }): k8s.V1Secret { const metadata = metadataTemplate({ name }); // avoid https://github.com/kubernetes-client/javascript/issues/52 const secret: Partial<k8s.V1Secret> = { apiVersion: "v1", kind: "Secret", type: "Opaque", metadata, data: {}, }; Object.keys(data).forEach(key => secret.data[key] = Buffer.from(data[key]).toString("base64")); return secret as k8s.V1Secret; } /** * Return a copy of the provided secret with its data values * encrypted. The provided secret should have its data values base64 * encoded. The provided secret is not modified. * * @param secret Kubernetes secret with base64 encoded data values * @return Kubernetes secret object with encrypted data values */ export async function encryptSecret(secret: k8s.V1Secret, key: string): Promise<k8s.V1Secret> { const encrypted = handleDataStrings(_.cloneDeep(secret)); for (const datum of Object.keys(encrypted.data)) { encrypted.data[datum] = await encrypt(encrypted.data[datum], key); } return encrypted; } /** * Return the provided secret with any data residing in the * stringData section base64 encoded and moved into the data section. Keys in * stringData will override existing keys in data. * @param secret Kubernetes secret with stringData elements * @return Kubernetes secret with stringData elements encoded and moved to data */ function handleDataStrings(secret: k8s.V1Secret): k8s.V1Secret { if (secret.stringData) { Object.keys(secret.stringData).forEach(key => secret.data[key] = Buffer.from(secret.stringData[key]).toString("base64")); delete secret.stringData; } return secret; } /** * Return a copy of the provided secret with its data valued * decrypted. The provided secret is not modified. * * @param secret Kubernetes secret with encrypted data values * @return Kubernetes secret object with base64 encoded data values */ export async function decryptSecret(secret: k8s.V1Secret, key: string): Promise<k8s.V1Secret> { const decrypted = _.cloneDeep(secret); for (const datum of Object.keys(secret.data)) { decrypted.data[datum] = await decrypt(secret.data[datum], key); } return decrypted; }