@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
214 lines (205 loc) • 7.58 kB
text/typescript
/*
* 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;
}