@appsemble/node-utils
Version:
NodeJS utilities used by Appsemble internally.
178 lines • 6.64 kB
JavaScript
import { logger } from '@appsemble/node-utils';
import { AppsV1Api, CoreV1Api, HttpError, KubeConfig, } from '@kubernetes/client-node';
export const maxCPU = process.env.MAX_CONTAINER_CPU ?? 3;
// In gigabytes
export const maxMemoryGi = process.env.MAX_CONTAINER_MEMORY ?? 3;
export const appIdLabel = 'appId';
export const resourceDefaults = { memory: '128Mi', cpu: '0.1' };
export function getKubeConfig() {
const kubeconfig = new KubeConfig();
kubeconfig.loadFromDefault();
const appsApi = kubeconfig.makeApiClient(AppsV1Api);
const coreApi = kubeconfig.makeApiClient(CoreV1Api);
return { appsApi, coreApi, kubeconfig };
}
export function getContainerNamespace() {
return `companion-containers-${process.env.SERVICE_NAME ?? 'appsemble'}`;
}
export function formatServiceName(containerName, appName, appId) {
const serviceName = `${containerName}-${appName}-${appId}`.replaceAll(' ', '-').toLowerCase();
return serviceName;
}
export function formatSecretName(appName, appId) {
return `${appName}-${appId}`.toLocaleLowerCase().replaceAll(' ', '-');
}
export function handleKubernetesError(error) {
if (error instanceof HttpError) {
const { statusCode } = error;
if (statusCode) {
logger.warn(`Kubernetes error with status code ${statusCode}:`);
}
logger.warn(error.body);
return;
}
logger.error(error);
}
export async function deleteResource(type, namespace, name) {
if (process.env.TEST) {
return;
}
const { appsApi, coreApi } = getKubeConfig();
try {
logger.silly(`Deleting ${type} '${name}' from namespace ${namespace} ... `);
switch (type) {
case 'deployment':
await appsApi.deleteNamespacedDeployment(name, namespace, undefined, undefined, undefined, undefined, 'Background');
break;
case 'service':
await coreApi.deleteNamespacedService(name, namespace);
break;
default:
await coreApi.deleteNamespacedSecret(name, namespace);
}
logger.verbose(`Deleted ${type} '${name}' from namespace ${namespace}`);
}
catch (error) {
handleKubernetesError(error);
}
}
/**
* Accepts a url used to call a companion container.
*
* @param url Url for internal communication to a container.
* @returns the metadata used to form the url.
* @example input: `containername-appname-1.companion-containers.svc.cluster.local`
* result: containername-appname-1, 1, appname, appsemble, appsemble-or-namespace
*/
export function parseServiceUrl(url) {
const elements = url.replace('http://', '').split('.');
const deploymentName = elements[0];
const namespace = elements[1];
const appId = deploymentName.split('-').pop();
const appName = deploymentName.replace(`-${appId}`, '');
// @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
return { deploymentName, appId, appName, namespace };
}
export function validateContainerResources(resources) {
// Validate Memory
if (!resources || !resources.limits) {
return { limits: resourceDefaults };
}
const result = resources;
if (resources.limits.memory) {
const unit = result.limits.memory.slice(-2);
const number = result.limits.memory.replace(unit, '');
const parsedValue = Number.parseFloat(number);
const conditionsMemory = [
Number.isNaN(parsedValue),
String(parsedValue) !== number,
parsedValue < 0,
unit !== 'Mi' && unit !== 'Gi',
unit === 'Gi' && parsedValue > maxMemoryGi,
unit === 'Mi' && parsedValue > maxMemoryGi * 1024,
];
if (conditionsMemory.some(Boolean)) {
result.limits.memory = resourceDefaults.memory;
}
}
else {
result.limits.memory = resourceDefaults.memory;
}
if (resources.limits.cpu) {
const parsedValue = Number.parseFloat(resources.limits.cpu);
let cpu = 0;
const pattern = /\b([1-9]|[1-9]\d{1,2})m\b/;
if (pattern.test(resources.limits.cpu)) {
cpu = Number.parseFloat(resources.limits.cpu.slice(0, -1)) / 1000;
}
else if (!Number.isNaN(parsedValue)) {
if (String(parsedValue) === String(resources.limits.cpu)) {
cpu = parsedValue;
}
else {
result.limits.cpu = resourceDefaults.cpu;
}
}
if (cpu > maxCPU || cpu <= 0) {
result.limits.cpu = resourceDefaults.cpu;
}
}
else {
result.limits.cpu = resourceDefaults.cpu;
}
return result;
}
export function isWhitelisted(fullImageName) {
if (process.env.HOST !== 'https://appsemble.app') {
return true;
}
// Allowed registries
// Overwrite the default by setting the
// `WHITELIST_REGISTRIES` env variable
// As space separated list of domains
const whitelistRegistries = process.env.WHITELIST_REGISTRIES
? process.env.WHITELIST_REGISTRIES.split(' ')
: ['registry.hub.docker.com/library/', 'docker.io/library/', 'registry.gitlab.com/appsemble/'];
let whitelisted = false;
const parts = fullImageName.split('/');
// If only image name is provided, it will be pulled from the official docker library
if (parts.length === 1) {
whitelisted = true;
}
else if (parts.length === 2) {
const [namespace] = parts;
// If namespace is 'library', it's an official image
if (namespace === 'library') {
whitelisted = true;
}
}
else if (whitelistRegistries.some((domain) => fullImageName.startsWith(domain))) {
whitelisted = true;
}
if (whitelisted) {
logger.silly(`Image ${fullImageName} found in a whitelisted repository`);
}
else {
logger.error(`Image ${fullImageName} is not in any whitelisted repository`);
}
return whitelisted;
}
export function formatEnv(input, appName, appId) {
const env = [];
if (input && input.length > 0) {
for (const entry of input) {
if (entry.useValueFromSecret) {
env.push({
name: entry.name,
valueFrom: {
secretKeyRef: { key: entry.value, name: formatSecretName(appName, appId) },
},
});
continue;
}
env.push({ name: entry.name, value: entry.value });
}
}
return env;
}
//# sourceMappingURL=helpers.js.map