@controlplane/cli
Version:
Control Plane Corporation CLI
493 lines • 25.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_IMAGE_TAG = void 0;
const path = require("path");
const objects_1 = require("../../util/objects");
const composeResource_1 = require("./composeResource");
const util_1 = require("../util");
const image_1 = require("../../commands/image");
const identity_1 = require("./identity");
const resolver_1 = require("../../commands/resolver");
const fs_1 = require("fs");
const io_1 = require("../../util/io");
const DEFAULT_CPU_COUNT = 42;
const MIN_CPU_WITH_GPU = 2000;
const DEFAULT_MEMORY = 128;
const MIN_MEMORY_WITH_GPU = 7168;
const AVAILABLE_PROTOCOLS = ['http', 'http2', 'tcp', 'grpc'];
exports.DEFAULT_IMAGE_TAG = '1.0';
const DEFAULT_SECRET_TARGET = '/run/secrets/';
class Service extends composeResource_1.default {
constructor(name, body, context, networks, volumes, secrets, composeObject, composePath, registry, profile) {
var _a, _b;
super(name, 'workload', body, context, composePath);
this.volumes = [];
this.secrets = [];
this.networkServices = [];
this.registry = registry;
this.profile = profile;
// saving networks
for (const networkName of (0, objects_1.toArray)(this.body.networks)) {
const network = networks.find((net) => net.name === networkName);
if (network) {
network.addService(this);
}
}
// saving volumes
for (const volumeData of (0, objects_1.toArray)(this.body.volumes)) {
// Gathering data from volume entry
let volumeName;
let mountPath;
if (typeof volumeData === 'string') {
[volumeName, mountPath] = volumeData.split(':');
}
else {
volumeName = volumeData.source;
mountPath = volumeData.target;
}
// Use dedicated volume if exists
const volume = volumes.find((vol) => vol.name === volumeName);
if (volume) {
this.volumes.push({ volume, mountPath });
continue;
}
// File bind mount should convert into a secret
const volumePath = path.join(composePath, volumeName);
if ((0, fs_1.existsSync)(volumePath) && (0, fs_1.statSync)(volumePath).isFile()) {
if (!this.body.secrets) {
this.body.secrets = [];
}
this.body.secrets.push({ source: volumeName, target: mountPath });
}
}
// saving secrets
for (const secret of (0, objects_1.toArray)((_a = this.body) === null || _a === void 0 ? void 0 : _a.secrets).concat((0, objects_1.toArray)((_b = this.body) === null || _b === void 0 ? void 0 : _b.configs))) {
// Finding source and target
let source, target;
if (typeof secret === 'string') {
source = secret;
target = DEFAULT_SECRET_TARGET + secret;
}
else {
source = secret.source;
if (secret.target.includes('/')) {
target = secret.target;
}
else {
target = DEFAULT_SECRET_TARGET + secret.target;
}
}
// find secret object
let secretObj = secrets.find((s) => s.name === source);
if (!secretObj) {
secretObj = (0, util_1.createSecretForBindMount)(composePath, source, secrets, this.context);
}
this.secrets.push({ secret: secretObj, mountPath: target });
// ensures identity is created
if (!this.identity) {
this.identity = new identity_1.default(this.getName() + '-identity', `Identity to allow ${this.getName()} to reveal secrets`, this.context);
}
secretObj.addIdentity(this.identity);
}
// Extending from other services
if (this.body.extends) {
const parentServiceName = this.body.extends.service;
// Setting default parent compose objects to current compose
let parentComposePath = composePath;
let parentComposeObject = composeObject;
let parentNetworks = networks;
let parentSecrets = secrets;
let parentVolumes = volumes;
// Deriving from other compose object if file specified
if (this.body.extends.file) {
parentComposePath = path.join(composePath, this.body.extends.file);
try {
const parentComposeBody = (0, fs_1.readFileSync)(parentComposePath, 'utf-8');
parentComposeObject = (0, io_1.loadObject)(parentComposeBody);
}
catch (e) {
throw new Error(`There was a problem reading ${parentComposePath}`);
}
// Gathering networks, secrets, volumes from parent compose
const parentResources = (0, util_1.getResourcesFromCompose)(composeObject, parentComposePath, context, registry, profile);
parentNetworks = parentResources.networks;
parentSecrets = parentResources.secrets;
parentVolumes = parentResources.volumes;
}
const parentServiceBody = parentComposeObject.services[parentServiceName];
const parentService = new Service(parentServiceName, parentServiceBody, context, parentNetworks, parentVolumes, parentSecrets, parentComposeObject, parentComposePath, registry, profile);
this.extendFrom(parentService, volumes, secrets);
}
// Setting image field
this.setImage();
}
resourceIssues() {
var _a, _b, _c, _d;
// Image and build specified
if (((_a = this.body) === null || _a === void 0 ? void 0 : _a.image) && ((_b = this.body) === null || _b === void 0 ? void 0 : _b.build)) {
this.issues.push(`ERROR: ${this.getName()} specifies both an image and build property. Can only specify one`);
}
// No image or build specified
if (!((_c = this.body) === null || _c === void 0 ? void 0 : _c.build) && !((_d = this.body) === null || _d === void 0 ? void 0 : _d.image)) {
this.issues.push(`ERROR: No image specified for service: ${this.getName()}`);
}
// Refers to a directory for bind mount
for (const volumeData of (0, objects_1.toArray)(this.body.volumes)) {
let volumeName;
if (typeof volumeData === 'string') {
volumeName = volumeData.split(':')[0];
}
else {
volumeName = volumeData.source;
}
// check if volumeName is a directory
const volumePath = path.join(this.composePath, volumeName);
if ((0, fs_1.existsSync)(volumePath) && (0, fs_1.statSync)(volumePath).isDirectory()) {
this.issues.push(`ERROR: Directory bind mount found (${volumeName}) in ${this.name}\nPlease replace with individual file bind mounts`);
}
}
}
async build() {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
if (!((_a = this.body) === null || _a === void 0 ? void 0 : _a.build)) {
if ((_b = this.body) === null || _b === void 0 ? void 0 : _b.image) {
return;
}
throw new Error(`No image specified for service: ${this.name}`);
}
// create image
let dockerfile;
let buildPath = this.composePath;
const imagePath = path.join(this.registry, this.generateImageName());
if (typeof ((_c = this.body) === null || _c === void 0 ? void 0 : _c.build) === 'string') {
buildPath = path.join(buildPath, (_d = this.body) === null || _d === void 0 ? void 0 : _d.build);
dockerfile = path.join(buildPath, 'Dockerfile');
}
else {
// build is given in context form
buildPath = path.join(buildPath, (_f = (_e = this.body) === null || _e === void 0 ? void 0 : _e.build) === null || _f === void 0 ? void 0 : _f.context);
const dockerFileName = (_j = (_h = (_g = this.body) === null || _g === void 0 ? void 0 : _g.build) === null || _h === void 0 ? void 0 : _h.dockerfile) !== null && _j !== void 0 ? _j : 'Dockerfile';
if ((_l = (_k = this.body) === null || _k === void 0 ? void 0 : _k.build) === null || _l === void 0 ? void 0 : _l.dockerfile) {
dockerfile = (_o = (_m = this.body) === null || _m === void 0 ? void 0 : _m.build) === null || _o === void 0 ? void 0 : _o.dockerfile;
}
else {
dockerfile = path.join(buildPath, dockerFileName);
}
}
const buildArgs = {
dockerfile,
dir: buildPath,
};
await (0, image_1.dockerBuild)(buildArgs, imagePath);
await (0, image_1.dockerPush)(this.profile, imagePath);
}
setImage() {
var _a, _b;
if ((_a = this.body) === null || _a === void 0 ? void 0 : _a.image) {
this.image = (_b = this.body) === null || _b === void 0 ? void 0 : _b.image;
return;
}
this.image = (0, resolver_1.resolveToLink)('image', this.generateImageName(), this.context);
}
extendFrom(service, volumes, secrets) {
var _a, _b, _c, _d;
// mappings (keys that only merge into main service if they dont exist)
this.body.healthcheck = (_a = this.body.healthcheck) !== null && _a !== void 0 ? _a : service.body.healthcheck;
this.body.labels = (_b = this.body.labels) !== null && _b !== void 0 ? _b : service.body.labels;
this.body.image = (_c = this.body.image) !== null && _c !== void 0 ? _c : service.body.image;
this.body.build = (_d = this.body.build) !== null && _d !== void 0 ? _d : service.body.build;
// Merge environment into this environment
const serviceEnvironment = service.getEnv();
if (!this.body.environment)
this.body.envionment = service.body.environment;
else if (Array.isArray(this.body.environment)) {
// if environment is array of elements, prepend service environment
this.body.environment = [...serviceEnvironment.map(({ name, value }) => `${name}=${value}`), ...this.body.environment];
}
else {
const serviceEnvObj = {};
serviceEnvironment.forEach(({ name, value }) => {
serviceEnvObj[name] = value;
});
this.body.environment = (0, objects_1.merge)(serviceEnvObj, this.body.environment);
}
// Merge volumes (find volumes with different container paths
service.volumes.forEach(({ volume, mountPath }) => {
var _a;
// Add volume if mount path does not exist already
if (!this.volumes.find((v) => v.mountPath === mountPath)) {
// Find same volume (volume with same name) or replace
const thisVolume = (_a = volumes.find((v) => v.name === volume.name)) !== null && _a !== void 0 ? _a : volume;
this.volumes.push({ volume: thisVolume, mountPath });
// Adding volume to global volumes if it doesnt exist
if (!volumes.includes(volume))
volumes.push(volume);
}
});
// Merge ports
const servicePorts = (0, objects_1.toArray)(service.getPorts());
const servicePortsText = servicePorts.map(({ protocol, number }) => `${number}${protocol ? '/' + protocol : ''}`);
this.body.ports = [...servicePortsText, ...(0, objects_1.toArray)(this.body.ports)];
// Merge secrets
service.secrets.forEach(({ secret, mountPath }) => {
var _a;
// Add volume if mount path does not exist already
if (!this.secrets.find((v) => v.mountPath === mountPath)) {
// Find same volume (volume with same name) or replace
const thisSecret = (_a = secrets.find((v) => v.name === secret.name)) !== null && _a !== void 0 ? _a : secret;
this.secrets.push({ secret: thisSecret, mountPath });
// Adding volume to global secrets if it doesnt exist
if (!secrets.includes(secret))
secrets.push(secret);
}
});
}
addService(service) {
if (this.networkServices.includes(service)) {
return;
}
this.networkServices.push(service);
}
getContainerName() {
var _a, _b;
return (_b = (_a = this.body) === null || _a === void 0 ? void 0 : _a.container_name.replace(/_/g, '-')) !== null && _b !== void 0 ? _b : this.getName();
}
getNetworkMode() {
if (this.body.network_mode === undefined) {
return undefined;
}
else if (this.body.network_mode.startsWith('service:')) {
const [_, serviceName] = this.body.network_mode.split(':');
return serviceName;
}
else {
return this.body.network_mode;
}
}
generateImageName() {
return `${this.getName().toLowerCase()}:${exports.DEFAULT_IMAGE_TAG}`;
}
getWorkloadType() {
if (this.volumes.length > 0) {
return 'stateful';
}
const ports = this.getPorts();
// Serverless if only one HTTP port is exposed
if (ports && ports.length === 1) {
return 'serverless';
}
return 'standard';
}
getGPU() {
var _a, _b, _c, _d, _e, _f;
if ((_c = (_b = (_a = this.body) === null || _a === void 0 ? void 0 : _a.deploy) === null || _b === void 0 ? void 0 : _b.reservations) === null || _c === void 0 ? void 0 : _c.devices) {
// Find gpu in devices
const gpuDevice = (_f = (_e = (_d = this.body) === null || _d === void 0 ? void 0 : _d.deploy) === null || _e === void 0 ? void 0 : _e.reservations) === null || _f === void 0 ? void 0 : _f.devices.find((d) => d.capabilities.includes('gpu') && d.number >= 1);
// Returning only available GPU option
if (gpuDevice) {
return {
nvidia: {
model: 't4',
quantity: 1,
},
};
}
}
return undefined;
}
getCPU() {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
let cpu;
if ((_d = (_c = (_b = (_a = this.body) === null || _a === void 0 ? void 0 : _a.deploy) === null || _b === void 0 ? void 0 : _b.resources) === null || _c === void 0 ? void 0 : _c.limits) === null || _d === void 0 ? void 0 : _d.cpus) {
cpu = (0, util_1.convertToMilicore)((_h = (_g = (_f = (_e = this.body) === null || _e === void 0 ? void 0 : _e.deploy) === null || _f === void 0 ? void 0 : _f.resources) === null || _g === void 0 ? void 0 : _g.limits) === null || _h === void 0 ? void 0 : _h.cpus);
}
else if ((_j = this.body) === null || _j === void 0 ? void 0 : _j.cpu_count) {
cpu = (0, util_1.convertToMilicore)((_k = this.body) === null || _k === void 0 ? void 0 : _k.cpu_count);
}
else {
cpu = DEFAULT_CPU_COUNT;
}
if (this.getGPU()) {
cpu = Math.max(cpu, MIN_CPU_WITH_GPU);
}
return cpu + 'm';
}
getRAM() {
var _a, _b, _c, _d, _e, _f, _g, _h;
let mem;
if ((_d = (_c = (_b = (_a = this.body) === null || _a === void 0 ? void 0 : _a.deploy) === null || _b === void 0 ? void 0 : _b.resources) === null || _c === void 0 ? void 0 : _c.limits) === null || _d === void 0 ? void 0 : _d.memory) {
mem = (0, util_1.convertToMebibytes)((_h = (_g = (_f = (_e = this.body) === null || _e === void 0 ? void 0 : _e.deploy) === null || _f === void 0 ? void 0 : _f.resources) === null || _g === void 0 ? void 0 : _g.limits) === null || _h === void 0 ? void 0 : _h.memory);
}
else {
mem = DEFAULT_MEMORY;
}
// Upgrading memory if needed by GPU
if (this.getGPU()) {
mem = Math.max(mem, MIN_MEMORY_WITH_GPU);
}
return mem + 'Mi';
}
getEnv() {
var _a, _b;
const environment = (_a = this.body.environment) !== null && _a !== void 0 ? _a : {};
const env = (0, util_1.convertToEnvironment)(environment);
for (const envFile of (0, objects_1.toArray)((_b = this.body) === null || _b === void 0 ? void 0 : _b.env_file)) {
const envFilePath = path.join(this.composePath, envFile);
(0, util_1.mergeEnv)(env, (0, util_1.envFileToEnv)(envFilePath));
}
return env;
}
getPorts() {
const bodyPorts = (0, objects_1.toArray)(this.body.ports).concat((0, objects_1.toArray)(this.body.expose));
return (0, util_1.convertToPorts)(bodyPorts, AVAILABLE_PROTOCOLS);
}
getCapacityAI() {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7;
// Capacity AI not allowed if GPU enabled
if (this.getGPU() || this.getWorkloadType() == 'stateful')
return false;
// Enabling Capacity AI if resource reservations (min) is less than limit (max)
if (((_d = (_c = (_b = (_a = this.body) === null || _a === void 0 ? void 0 : _a.deploy) === null || _b === void 0 ? void 0 : _b.resources) === null || _c === void 0 ? void 0 : _c.reservations) === null || _d === void 0 ? void 0 : _d.cpus) && ((_h = (_g = (_f = (_e = this.body) === null || _e === void 0 ? void 0 : _e.deploy) === null || _f === void 0 ? void 0 : _f.resources) === null || _g === void 0 ? void 0 : _g.limits) === null || _h === void 0 ? void 0 : _h.cpus)) {
if ((0, util_1.convertToMilicore)((_m = (_l = (_k = (_j = this.body) === null || _j === void 0 ? void 0 : _j.deploy) === null || _k === void 0 ? void 0 : _k.resources) === null || _l === void 0 ? void 0 : _l.reservations) === null || _m === void 0 ? void 0 : _m.cpus) < (0, util_1.convertToMilicore)((_r = (_q = (_p = (_o = this.body) === null || _o === void 0 ? void 0 : _o.deploy) === null || _p === void 0 ? void 0 : _p.resources) === null || _q === void 0 ? void 0 : _q.limits) === null || _r === void 0 ? void 0 : _r.cpus))
return true;
}
if (((_v = (_u = (_t = (_s = this.body) === null || _s === void 0 ? void 0 : _s.deploy) === null || _t === void 0 ? void 0 : _t.resources) === null || _u === void 0 ? void 0 : _u.reservations) === null || _v === void 0 ? void 0 : _v.memory) && ((_z = (_y = (_x = (_w = this.body) === null || _w === void 0 ? void 0 : _w.deploy) === null || _x === void 0 ? void 0 : _x.resources) === null || _y === void 0 ? void 0 : _y.limits) === null || _z === void 0 ? void 0 : _z.memory)) {
if ((0, util_1.convertToMebibytes)((_3 = (_2 = (_1 = (_0 = this.body) === null || _0 === void 0 ? void 0 : _0.deploy) === null || _1 === void 0 ? void 0 : _1.resources) === null || _2 === void 0 ? void 0 : _2.reservations) === null || _3 === void 0 ? void 0 : _3.memory) <
(0, util_1.convertToMebibytes)((_7 = (_6 = (_5 = (_4 = this.body) === null || _4 === void 0 ? void 0 : _4.deploy) === null || _5 === void 0 ? void 0 : _5.resources) === null || _6 === void 0 ? void 0 : _6.limits) === null || _7 === void 0 ? void 0 : _7.memory))
return true;
}
// Disable by default
return false;
}
getAutoScaling() {
var _a, _b, _c, _d, _e, _f;
return {
minScale: (_c = (_b = (_a = this.body) === null || _a === void 0 ? void 0 : _a.deploy) === null || _b === void 0 ? void 0 : _b.replicas) !== null && _c !== void 0 ? _c : undefined,
maxScale: (_f = (_e = (_d = this.body) === null || _d === void 0 ? void 0 : _d.deploy) === null || _e === void 0 ? void 0 : _e.replicas) !== null && _f !== void 0 ? _f : undefined,
};
}
getVolumes() {
const volumes = [];
for (const volume of this.volumes) {
volumes.push({ path: volume.mountPath, uri: volume.volume.getUri(), recoveryPolicy: 'retain' });
}
for (const secret of this.secrets) {
volumes.push({ path: secret.mountPath, uri: secret.secret.getUri(), recoveryPolicy: 'retain' });
}
return volumes;
}
getArgs() {
var _a, _b, _c;
if (Array.isArray((_a = this.body) === null || _a === void 0 ? void 0 : _a.command)) {
return (_b = this.body) === null || _b === void 0 ? void 0 : _b.command;
}
else if ((_c = this.body) === null || _c === void 0 ? void 0 : _c.command) {
return this.body.command.split(' ');
}
return this.body.command;
}
getCommand() {
var _a, _b, _c;
if (Array.isArray((_a = this.body) === null || _a === void 0 ? void 0 : _a.entrypoint)) {
return (_b = this.body) === null || _b === void 0 ? void 0 : _b.entrypoint.join(' ');
}
else {
return (_c = this.body) === null || _c === void 0 ? void 0 : _c.entrypoint;
}
}
getReadinessProbe() {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
return {
exec: {
command: (0, util_1.healthCheckCommandToCplnCommand)((_b = (_a = this.body) === null || _a === void 0 ? void 0 : _a.healthcheck) === null || _b === void 0 ? void 0 : _b.test),
},
failureThreshold: (_d = (_c = this.body) === null || _c === void 0 ? void 0 : _c.healthcheck) === null || _d === void 0 ? void 0 : _d.retries,
initialDelaySeconds: (0, util_1.durationToSeconds)((_f = (_e = this.body) === null || _e === void 0 ? void 0 : _e.healthcheck) === null || _f === void 0 ? void 0 : _f.startPeriod),
periodSeconds: (0, util_1.durationToSeconds)((_h = (_g = this.body) === null || _g === void 0 ? void 0 : _g.healthcheck) === null || _h === void 0 ? void 0 : _h.interval),
timeoutSeconds: (0, util_1.durationToSeconds)((_k = (_j = this.body) === null || _j === void 0 ? void 0 : _j.healthcheck) === null || _k === void 0 ? void 0 : _k.timeout),
};
}
// allows external networks if ports are defined OR network_mode == 'host'
getInboundAllowCIDR() {
var _a, _b, _c, _d;
const CIDRs = [];
if ((((_a = this.body) === null || _a === void 0 ? void 0 : _a.ports) !== undefined && ((_b = this.body) === null || _b === void 0 ? void 0 : _b.ports.length)) || (((_c = this.body) === null || _c === void 0 ? void 0 : _c.network_mode) && ((_d = this.body) === null || _d === void 0 ? void 0 : _d.network_mode) == 'host')) {
CIDRs.push('0.0.0.0/0');
}
return CIDRs;
}
// all workloads should have full access to outbound internet
getOutboundAllowCIDR() {
var _a, _b;
if (((_a = this.body) === null || _a === void 0 ? void 0 : _a.network_mode) && ((_b = this.body) === null || _b === void 0 ? void 0 : _b.network_mode) === 'none') {
return [];
}
return ['0.0.0.0/0'];
}
getExternalFirewallConfig() {
return {
inboundAllowCIDR: this.getInboundAllowCIDR(),
outboundAllowCIDR: this.getOutboundAllowCIDR(),
};
}
getInternalFirewallConfig() {
return {
inboundAllowType: 'workload-list',
inboundAllowWorkload: this.networkServices.map((service) => service.toSelfLink()),
};
}
isReadinessDisabled() {
var _a, _b, _c, _d, _e;
return (((_b = (_a = this.body) === null || _a === void 0 ? void 0 : _a.healthcheck) === null || _b === void 0 ? void 0 : _b.disable) === true ||
(Array.isArray((_d = (_c = this.body) === null || _c === void 0 ? void 0 : _c.healthcheck) === null || _d === void 0 ? void 0 : _d.test) &&
this.body.healthcheck.test.length >= 1 &&
this.body.healthcheck.test[0] === 'NONE') ||
((_e = this.body) === null || _e === void 0 ? void 0 : _e.healthcheck) === undefined);
}
toContainer() {
return {
name: this.getContainerName(),
cpu: this.getCPU(),
memory: this.getRAM(),
gpu: this.getGPU(),
env: this.getEnv(),
ports: this.getPorts(),
args: this.getArgs(),
command: this.getCommand(),
workingDir: this.body.working_dir,
image: this.image,
volumes: this.getVolumes(),
readinessProbe: this.isReadinessDisabled() ? undefined : this.getReadinessProbe(),
};
}
toWorkload() {
return {
kind: 'workload',
name: this.getName(),
description: this.getName(),
spec: {
type: this.getWorkloadType(),
containers: [this.toContainer()],
defaultOptions: {
capacityAI: this.getCapacityAI(),
autoscaling: this.getAutoScaling(),
},
identityLink: this.identity !== undefined ? this.identity.toLink() : undefined,
firewallConfig: {
external: this.getExternalFirewallConfig(),
internal: this.getInternalFirewallConfig(),
},
},
};
}
toResource() {
const resources = [];
if (this.identity) {
resources.push(this.identity.toResource());
}
resources.push(this.toWorkload());
return resources;
}
}
exports.default = Service;
//# sourceMappingURL=service.js.map