UNPKG

@appsemble/node-utils

Version:

NodeJS utilities used by Appsemble internally.

276 lines 11.5 kB
import { logger } from '@appsemble/node-utils'; import { appIdLabel, deleteResource, formatEnv, formatSecretName, formatServiceName, getContainerNamespace, getKubeConfig, handleKubernetesError, isWhitelisted, validateContainerResources, } from './helpers.js'; import { generateDeploymentAndServiceSpecs } from './specs.js'; /** * Creates or updates secret. * Secret resource is named after the app name + id, where whitespace * is replaced by '-'. * * @param secretName Key of the secret to be created * @param value The secret value * @param appName Name of the app * @param appId Id of the app. */ export async function updateNamespacedSecret(secretName, value, appName, appId) { // If testing, don't create Kubernetes resources if (process.env.TEST) { return; } const { coreApi } = getKubeConfig(); const namespace = getContainerNamespace(); const formattedName = formatSecretName(appName, appId); const secretSpec = { metadata: { name: formattedName, namespace, }, data: { [secretName]: Buffer.from(value).toString('base64') }, }; let existing; try { existing = (await coreApi.readNamespacedSecret(formattedName, namespace)).body; } catch (error) { handleKubernetesError(error); } if (existing) { logger.silly(`Secret ${formattedName} already exists`); if (!existing.data) { existing.data = {}; } existing.data[secretName] = Buffer.from(value).toString('base64'); try { const { body: updatedSecret } = await coreApi.replaceNamespacedSecret(formattedName, namespace, existing); logger.info(`Secret ${updatedSecret.metadata?.name} updated successfully`); } catch (error) { handleKubernetesError(error); } return; } try { logger.verbose(`Creating secret ${secretSpec.metadata?.name} ...`); const { body: secret } = await coreApi.createNamespacedSecret(namespace, secretSpec); logger.info(`Secret ${secret.metadata?.name} created successfully`); } catch (error) { handleKubernetesError(error); } } export async function deleteSecret(appName, appId, key) { if (process.env.TEST) { return; } const { coreApi } = getKubeConfig(); const namespace = getContainerNamespace(); const secretName = formatSecretName(appName, appId); if (key) { try { const { body: secret } = await coreApi.readNamespacedSecret(secretName, namespace); logger.verbose(`Deleteing key ${key} of secret ${secretName} from namespace ${namespace} ...`); if (!(secret.data && key in secret.data)) { logger.warn(`Key ${key} does not exist on secret ${secret.metadata?.name}`); return; } delete secret.data[key]; await coreApi.replaceNamespacedSecret(secretName, namespace, secret); logger.verbose(`Deleted key ${key} of secret ${secretName} from namespace ${namespace}.`); } catch (error) { logger.error(`Error deleting ${key ? `key ${key} from` : ''} secret ${secretName}`); handleKubernetesError(error); } } else { await deleteResource('secret', namespace, secretName); } } export async function deleteCompanionContainers(serviceName) { if (process.env.TEST) { return; } const namespace = getContainerNamespace(); await deleteResource('service', namespace, serviceName); await deleteResource('deployment', namespace, serviceName); } export async function updateCompanionContainers(definitions, appName, appId, registry) { // If testing, don't create Kubernetes resources if (process.env.TEST) { return; } const { appsApi, coreApi } = getKubeConfig(); const namespace = getContainerNamespace(); let services; logger.silly(`Listing services in namespace ${namespace} ...`); try { services = (await coreApi.listNamespacedService(namespace)).body; } catch (error) { logger.error(`Error listing services in namespace ${namespace}`); handleKubernetesError(error); return; } // Update or create deployment and service for (const def of definitions) { const serviceName = formatServiceName(def.name, appName, appId); const existing = services?.items?.find((s) => s.metadata?.name === serviceName); // Update if (existing) { logger.verbose(`Updating resources for ${existing.metadata?.name} in namespace ${namespace}`); const patchOptions = [ // @ts-expect-error 18048 variable is possibly undefined (strictNullChecks) existing.metadata.name, namespace, [ { op: 'replace', path: '/metadata', // @ts-expect-error 18048 variable is possibly undefined (strictNullChecks) value: { ...def.metadata, name: existing.metadata.name }, }, { op: 'replace', path: '/spec/ports/0/targetPort', value: def.port ?? 8080, }, { op: 'add', path: '/metadata/labels', value: { appId }, }, ], undefined, undefined, undefined, undefined, undefined, { headers: { 'Content-Type': 'application/json-patch+json' } }, ]; // Update service try { const updatedService = await coreApi.patchNamespacedService(...patchOptions); logger.info(`Service ${updatedService?.body?.metadata?.name} updated`); } catch (error) { logger.error('Error updating service:'); handleKubernetesError(error); } // Update deployment const resources = validateContainerResources(def.resources); patchOptions[2] = [ { op: 'replace', path: '/metadata', // @ts-expect-error 18048 variable is possibly undefined (strictNullChecks) value: { ...def.metadata, name: existing.metadata.name }, }, { op: 'replace', path: '/spec/template/spec/containers/0/ports/0/containerPort', value: def.port ?? 8080, }, { op: 'replace', path: '/spec/template/spec/containers/0/env', value: [...formatEnv(def.env ?? [], appName, appId)], }, { op: 'add', path: '/metadata/labels', value: { appId }, }, { op: 'replace', path: '/spec/template/spec/containers/0/resources', value: resources, }, ]; try { const updatedDeployment = await appsApi.patchNamespacedDeployment(...patchOptions); logger.info(`Deployment ${updatedDeployment?.body?.metadata?.name} updated`); } catch (error) { logger.error('Error updating deployment:'); handleKubernetesError(error); } // Update pod try { const { body } = await coreApi.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, `appId=${appId}`); if (body?.items?.length === 0) { logger.silly(`Deployment ${serviceName} has 0 pods, skipping ...`); continue; } const pod = body?.items?.find((i) => i.metadata?.labels?.app === existing.metadata?.name); // Set base labels, such as selector, pod name, pod hash const props = { ...def.metadata, labels: { ...(def.metadata ? def.metadata.labels : null), app: pod?.metadata?.labels?.app, 'pod-template-hash': pod?.metadata?.labels?.['pod-template-hash'], appId, }, name: pod?.metadata?.name, }; // @ts-expect-error 2322 null is not assignable to type (strictNullChecks) patchOptions[0] = pod?.metadata?.name; patchOptions[2] = [ { op: 'replace', path: '/metadata', value: props, }, ]; const updatedPod = await coreApi.patchNamespacedPod(...patchOptions); logger.verbose(`Pod updated: ${updatedPod?.body?.metadata?.name}`); } catch (error) { handleKubernetesError(error); } } // Create else { const { deployment, service } = generateDeploymentAndServiceSpecs(def, serviceName, appName, appId, registry); // @ts-expect-error 2345 argument of type is not assignable to parameter of type // (strictNullChecks) if (isWhitelisted(deployment.spec?.template.spec?.containers[0].image)) { logger.verbose(`Creating resources in namespace ${namespace} ...`); try { const deploymentObj = await appsApi.createNamespacedDeployment(namespace, deployment); logger.info(`Deployment created: ${deploymentObj.body?.metadata?.name}`); } catch (error) { handleKubernetesError(error); return; } try { const serviceObj = await coreApi.createNamespacedService(namespace, service); logger.info(`Service created: ${serviceObj.body.metadata?.name}`); } catch (error) { handleKubernetesError(error); } } else { logger.error(`Image ${deployment.spec?.template.spec?.containers[0].image} is not in the repository whitelist!`); } } } // Delete companions removed from the definition for (const service of services.items) { if (service.spec?.type === 'ExternalName') { continue; } if (service.metadata?.labels && appIdLabel in service.metadata.labels && service.metadata.labels[appIdLabel] === appId) { const existing = definitions.find((d) => formatServiceName(d.name, appName, appId) === service.metadata?.name); if (!existing) { // @ts-expect-error 2345 argument of type is not assignable to parameter of type // (strictNullChecks) await deleteCompanionContainers(service.metadata?.name); } } } } //# sourceMappingURL=operations.js.map