UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

214 lines (205 loc) 7.58 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 { k8sErrMsg } from "../support/error"; import { logRetry } from "../support/retry"; import { K8sDeleteResponse, K8sListResponse, K8sObjectApi } from "./api"; import { loadKubeConfig } from "./config"; import { labelSelector } from "./labels"; import { appName, KubernetesDeleteResourceRequest, } from "./request"; import { logObject } from "./resource"; import { specSlug } from "./spec"; /** * Delete a resource if it exists. If the resource does not exist, * do nothing. * * @param spec Kuberenetes spec of resource to delete * @return DeleteResponse if object existed and was deleted, undefined if it did not exist */ export async function deleteSpec(spec: k8s.KubernetesObject): Promise<K8sDeleteResponse | undefined> { const slug = specSlug(spec); let client: K8sObjectApi; try { const kc = loadKubeConfig(); client = kc.makeApiClient(K8sObjectApi); } catch (e) { e.message = `Failed to create Kubernetes client: ${k8sErrMsg(e)}`; throw e; } try { await client.read(spec); } catch (e) { logger.debug(`Kubernetes resource ${slug} does not exist: ${k8sErrMsg(e)}`); return undefined; } logger.info(`Deleting resource ${slug} using '${logObject(spec)}'`); return logRetry(() => client.delete(spec), `delete resource ${slug}`); } /** Collection deleter for namespaced resources. */ export type K8sNamespacedLister = ( namespace: string, pretty?: string, allowWatchBookmarks?: boolean, continu?: string, fieldSelector?: string, labelSelector?: string, limit?: number, resourceVersion?: string, resourceVersionMatch?: string, timeoutSeconds?: number, watch?: boolean, options?: any, ) => Promise<K8sListResponse>; /** Collection deleter for cluster resources. */ export type K8sClusterLister = ( pretty?: string, allowWatchBookmarks?: boolean, continu?: string, fieldSelector?: string, labelSelector?: string, limit?: number, resourceVersion?: string, resourceVersionMatch?: string, timeoutSeconds?: number, watch?: boolean, options?: any, ) => Promise<K8sListResponse>; /** Collection deleter for namespaced resources. */ export type K8sNamespacedDeleter = ( name: string, namespace: string, pretty?: string, dryRun?: string, gracePeriodSeconds?: number, orphanDependents?: boolean, propagationPolicy?: string, body?: k8s.V1DeleteOptions, options?: any, ) => Promise<K8sDeleteResponse>; /** Collection deleter for cluster resources. */ export type K8sClusterDeleter = ( name: string, pretty?: string, dryRun?: string, gracePeriodSeconds?: number, orphanDependents?: boolean, propagationPolicy?: string, body?: k8s.V1DeleteOptions, options?: any, ) => Promise<K8sDeleteResponse>; /** Arguments for [[deleteAppResources]]. */ export interface DeleteAppResourcesArgBase { /** Resource kind, e.g., "Service". */ kind: string; /** Whether resource is cluster or namespace scoped. */ namespaced: boolean; /** Delete request object. */ req: KubernetesDeleteResourceRequest; /** API object to use as `this` for lister and deleter. */ api: k8s.CoreV1Api | k8s.AppsV1Api | k8s.NetworkingV1beta1Api | k8s.RbacAuthorizationV1Api; /** Resource collection deleting function. */ lister: K8sNamespacedLister | K8sClusterLister; /** Resource collection deleting function. */ deleter: K8sNamespacedDeleter | K8sClusterDeleter; } export interface DeleteAppResourcesArgNamespaced extends DeleteAppResourcesArgBase { namespaced: true; lister: K8sNamespacedLister; deleter: K8sNamespacedDeleter; } export interface DeleteAppResourcesArgCluster extends DeleteAppResourcesArgBase { namespaced: false; lister: K8sClusterLister; deleter: K8sClusterDeleter; } export type DeleteAppResourcesArg = DeleteAppResourcesArgNamespaced | DeleteAppResourcesArgCluster; /** * Delete resources associated with application described by `arg.req`, if * any exists. If no matching resources exist, do nothing. Return * ann array of deleted resources, which may be empty. * * @param arg Specification of what and how to delete for what application * @return Array of deleted resources */ export async function deleteAppResources(arg: DeleteAppResourcesArg): Promise<k8s.KubernetesObject[]> { const slug = appName(arg.req); const selector = labelSelector(arg.req); const toDelete: k8s.KubernetesObject[] = []; try { const limit = 500; let continu: string; do { let listResp: K8sListResponse; const args: [string?, boolean?, string?, string?, string?, number?] = [ undefined, undefined, continu, undefined, selector, limit, ]; if (arg.namespaced) { listResp = await arg.lister.call(arg.api, arg.req.ns, ...args); } else if (arg.namespaced === false) { listResp = await arg.lister.apply(arg.api, args); } toDelete.push( ...listResp.body.items.map(r => { r.kind = r.kind || arg.kind; // list response does not include kind return r; }), ); continu = listResp.body.metadata._continue; } while (!!continu); } catch (e) { e.message = `Failed to list ${arg.kind} for ${slug}: ${k8sErrMsg(e)}`; throw e; } const deleted: k8s.KubernetesObject[] = []; const errs: Error[] = []; for (const resource of toDelete) { const resourceSlug = arg.namespaced ? `${arg.kind}/${resource.metadata.namespace}/${resource.metadata.name}` : `${arg.kind}/${resource.metadata.name}`; logger.info(`Deleting ${resourceSlug} for ${slug}`); try { const args: [string?, string?, number?, boolean?, string?] = [ undefined, undefined, undefined, undefined, "Background", ]; if (arg.namespaced) { await arg.deleter.call(arg.api, resource.metadata.name, resource.metadata.namespace, ...args); } else if (arg.namespaced === false) { await arg.deleter.call(arg.api, resource.metadata.name, ...args); } deleted.push(resource); } catch (e) { e.message = `Failed to delete ${resourceSlug} for ${slug}: ${k8sErrMsg(e)}`; errs.push(e); } } if (errs.length > 0) { throw new Error(`Failed to delete ${arg.kind} resources for ${slug}: ${errs.map(e => e.message).join("; ")}`); } return deleted; }