UNPKG

@webda/shell

Version:

Deploy a Webda app or configure it

252 lines 11.5 kB
import * as k8s from "@kubernetes/client-node"; import { CronService, FileUtils } from "@webda/core"; import { CronReplace, getKubernetesApiClient } from "@webda/kubernetes"; import * as crypto from "crypto"; import * as fs from "fs"; import jsonpath from "jsonpath"; import * as yaml from "yaml"; import { Deployer } from "./deployer.js"; export const K8S_DEFAULT_CRON_DEFINITION = `apiVersion: batch/v1 kind: CronJob metadata: name: \${cron.serviceName.toLowerCase()}-\${cron.method.toLowerCase()}-\${cron.cronId} spec: concurrencyPolicy: Forbid failedJobsHistoryLimit: 1 jobTemplate: spec: template: spec: containers: - image: \${resources.tag} imagePullPolicy: Always name: scheduled-job resources: {} command: ["/webda/node_modules/.bin/webda"] args: [ "--noCompile", "launch", "\${cron.serviceName}", "\${cron.method}", "\${...cron.args}", ] restartPolicy: Never securityContext: {} terminationGracePeriodSeconds: 30 schedule: \${cron.cron} successfulJobsHistoryLimit: 3 `; export function KubernetesObjectToURI({ apiVersion, metadata: { name, namespace }, kind }) { return `${apiVersion || "v1"}/${namespace || "default"}/${kind.toLowerCase()}s/${name}`; } const DEFAULT_API = { Deployment: "apps/v1" }; /** * @WebdaDeployer WebdaDeployer/Kubernetes */ export class Kubernetes extends Deployer { async loadDefaults() { var _a, _b; await super.loadDefaults(); (_a = this.resources).defaultNamespace ?? (_a.defaultNamespace = "default"); // Ensure resourcesFile is always an array (_b = this.resources).resourcesFiles ?? (_b.resourcesFiles = []); // Push resourcesFile to resourcesFiles array if (this.resources.resourcesFile && !this.resources.resourcesFiles.includes(this.resources.resourcesFile)) { this.resources.resourcesFiles.push(this.resources.resourcesFile); } } addAnnotation(spec) { jsonpath.value(spec, '$.metadata.annotations["webda.io/deployer"]', this.name); jsonpath.value(spec, '$.metadata.annotations["webda.io/deployment"]', this.manager.getDeploymentName()); jsonpath.value(spec, '$.metadata.annotations["webda.io/version"]', this.getApplication().getWebdaVersion()); jsonpath.value(spec, '$.metadata.annotations["webda.io/application.name"]', this.getApplication().getPackageDescription().name); jsonpath.value(spec, '$.metadata.annotations["webda.io/application.version"]', this.getApplication().getPackageDescription().version); } completeResource(resource) { var _a; if (!resource.kind || !resource.metadata || !resource.metadata.name) { return false; } (_a = resource.metadata).namespace ?? (_a.namespace = this.resources.defaultNamespace); if (!resource.apiVersion) { // Try to guess if (DEFAULT_API[resource.kind]) { resource.apiVersion = DEFAULT_API[resource.kind]; } else { resource.apiVersion = "v1"; } } return true; } async deploy() { // Create the Docker image if (this.resources.tag && this.resources.push) { this.logger.log("INFO", "Launching subdeployer Docker"); await this.manager.run("webdadeployer/container", this.resources); } this.logger.log("INFO", "Initializing Kubernetes Client"); // Check all resource this.resources.resourcesFiles.forEach(resourcesFile => { if (!resourcesFile.match(/\.(ya?ml|json)$/i) || !fs.existsSync(resourcesFile)) { throw new Error(`Resource file #${resourcesFile} does not exist or invalid format`); } }); // Load all type of configuration this.client = this.getClient(); // Manage CronJob - add resource if required if (this.resources.cronTemplate) { this.logger.log("INFO", "Adding CronJob resources"); // Load all existing Cron annotations let crons = CronService.loadAnnotations(this.manager.getWebda().getServices()); // Load CronJob resource template let resource; if (typeof this.resources.cronTemplate === "boolean") { resource = yaml.parse(K8S_DEFAULT_CRON_DEFINITION); } else if (typeof this.resources.cronTemplate === "string") { resource = FileUtils.load(this.resources.cronTemplate); } else { resource = this.resources.cronTemplate; } // Namespace where cronjob resource are created let cronNamespace = resource.metadata.namespace || "default"; // Add annotations to the CronJob template this.completeResource(resource); let cronDeployerId = crypto .createHash("sha256") .update(this.name + this.manager.getDeploymentName() + this.getApplication().getPackageDescription().name) .digest("hex"); jsonpath.value(resource, '$.metadata.annotations["webda.io/crondeployer"]', cronDeployerId); const k8sApi = this.getClient(k8s.BatchV1Api); let currentJobs = (await k8sApi.listNamespacedCronJob(resource.metadata.namespace || "default")).items.filter(i => i.metadata.annotations["webda.io/crondeployer"] === cronDeployerId); let currentJobsNamesMap = {}; currentJobs.forEach(i => (currentJobsNamesMap[i.metadata.name] = `${i.spec.schedule} ${i.metadata.annotations["webda.io/crondescription"]}`)); this.resources.resources = this.resources.resources || []; crons.forEach(cron => { this.parameters.cron = { ...cron, cronId: CronService.getCronId(cron, this.name + this.manager.getDeploymentName()) }; jsonpath.value(resource, '$.metadata.annotations["webda.io/cronid"]', this.parameters.cron.cronId); jsonpath.value(resource, '$.metadata.annotations["webda.io/crondescription"]', cron.toString()); let cronResource = CronReplace(resource, this.parameters.cron, this.getApplication(), { resources: this.resources, deployer: { name: this.name, type: this.type }, ...this.parameters }); this.resources.resources.push(cronResource); if (currentJobsNamesMap[cronResource.metadata.name] !== undefined) { delete currentJobsNamesMap[cronResource.metadata.name]; this.logger.log("INFO", `Updating CronJob ${cronResource.metadata.name}: ${cron.toString()}`); } else { this.logger.log("INFO", `Adding CronJob ${cronResource.metadata.name}: ${cron.toString()}`); } }); this.parameters.cron = undefined; // Remove any cronjob created by this deployer that is not required anymore for (let i in currentJobsNamesMap) { this.logger.log("INFO", `Deleting CronJob ${i}: ${currentJobsNamesMap[i]}`); // Delete resource await k8sApi.deleteNamespacedCronJob({ name: i, namespace: cronNamespace }); } } this.logger.log("INFO", "Manage patch resources"); // Patch resource for (let i in this.resources.patchResources) { let resource = this.resources.patchResources[i]; if (!this.completeResource(resource)) { this.logger.log("ERROR", `Resource #${i} of patchResources is incorrect (kind,metadata.name) are required`); continue; } try { // move to any // error TS2345: Argument of type 'KubernetesObject' is not assignable to parameter of type 'KubernetesObjectHeader<KubernetesObject> let spec = (await this.client.read(resource)); for (let prop in resource.patch) { let path = prop; if (!prop.startsWith("$.")) { path = "$." + path; } jsonpath.value(spec, path, resource.patch[prop]); } this.addAnnotation(spec); await this.client.patch(spec); } catch (err) { this.logger.log("ERROR", "Could not patch Kubernetes resource", KubernetesObjectToURI(resource)); } } this.logger.log("INFO", "Manage inline resources"); // Inline resource for (let i in this.resources.resources) { let resource = this.resources.resources[i]; if (!this.completeResource(resource)) { this.logger.log("ERROR", `Resource #${i} of resources is incorrect (kind,metadata.name) are required`); continue; } this.addAnnotation(resource); await this.upsertKubernetesObject(resource); } this.logger.log("INFO", "Manage file resources"); // File resource for (let i in this.resources.resourcesFiles) { let resourcesFile = this.resources.resourcesFiles[i]; let resources = FileUtils.load(resourcesFile); if (!Array.isArray(resources)) { resources = [resources]; } for (let j in resources) { let resource = this.replaceVariables(resources[j]); if (!this.completeResource(resource)) { this.logger.log("ERROR", `Resource invalid #${j} of resourcesFile`); continue; } await this.upsertKubernetesObject(resource); } } } getClient(api) { return getKubernetesApiClient(this.resources, api); } async upsertKubernetesObject(resource) { try { // try to get the resource, if it does not exist an error will be thrown and we will end up in the catch // block. await this.client.read(resource); this.logger.log("INFO", "Updating", resource.metadata); try { if (resource.kind === "Certificate" && resource.apiVersion === "certmanager.k8s.io/v1alpha1") { // Certificate are not patchable return; } // we got the resource, so it exists, so patch it await this.client.patch(resource); } catch (e) { if (e?.kind === "Status") { this.logger.log("ERROR", "Cannot patch", resource.metadata, e.message); } else { this.logger.log("ERROR", "Cannot patch", resource.metadata, e); } } } catch (e) { // we did not get the resource, so it does not exist, so create it await this.client.create(resource); } } } //# sourceMappingURL=kubernetes.js.map