UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

493 lines 25.9 kB
"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