UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

1,261 lines 66.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GetCron = exports.StartCron = exports.Cron = exports.Run = exports.StartStop = exports.ForceRedeployment = exports.GetWorkload = exports.Create = exports.Open = exports.ConnectExec = exports.withReplicaNameOption = exports.WorkloadCmd = void 0; const WebSocket = require("ws"); const _ = require("lodash"); const open = require("open"); const options_1 = require("./options"); const resolver_1 = require("./resolver"); const command_1 = require("../cli/command"); const deployment_1 = require("./deployment"); const resultFetcher_1 = require("../rest/resultFetcher"); const functions_1 = require("../util/functions"); const objects_1 = require("../util/objects"); const client_1 = require("../session/client"); const names_1 = require("../util/names"); const time_1 = require("../util/time"); const logger_1 = require("../util/logger"); const workload_1 = require("../util/workload"); const tags_1 = require("./tags"); const links_1 = require("../util/links"); const io_1 = require("../util/io"); const terminal_server_1 = require("../terminal-server/terminal-server"); const helpers_1 = require("../terminal-server/helpers"); const types_1 = require("../terminal-server/types"); const generic_1 = require("./generic"); class WorkloadCmd extends command_1.Command { constructor() { super(...arguments); this.command = 'workload'; this.aliases = ['w']; this.describe = 'Manage workloads within a global virtual cloud'; } builder(yargs) { const schema = { rels: ['gvc'], }; const resolver = (0, resolver_1.kindResolver)('workload'); const opts = [options_1.withGvcOptions, options_1.withStandardOptions]; const commandName = 'workload'; const commandNamePlural = 'workloads'; const commandNameA = 'a workload'; return (yargs .demandCommand() .version(false) .help() // generic .command(new generic_1.Edit(commandName, resolver, ...opts).toYargs()) .command(new generic_1.Patch(commandName, resolver, ...opts).toYargs()) .command(new generic_1.Query(commandNamePlural, resolver, schema, ...opts).toYargs()) .command(new generic_1.Delete(commandNamePlural, resolver, ...opts).toYargs()) .command(new generic_1.Eventlog(commandName, resolver, ...opts).toYargs()) .command(new generic_1.Tag(commandNamePlural, resolver, ...opts).toYargs()) .command(new generic_1.ListPermissions(commandNameA, resolver, ...opts).toYargs()) .command(new generic_1.ViewAccessReport(commandName, resolver, ...opts).toYargs()) .command(new generic_1.Clone(commandName, resolver, ...opts).toYargs()) .command(new generic_1.Update(commandName, resolver, [ { path: 'description', }, { path: 'tags.<key>', }, { path: 'spec.identityLink', itemLinkResolver: (0, resolver_1.kindResolver)('identity'), }, // Container { path: 'spec.containers.<name>.image', }, { path: 'spec.containers.<name>.workingDir', }, { path: 'spec.containers.<name>.metrics.port', }, { path: 'spec.containers.<name>.metrics.path', }, { path: 'spec.containers.<name>.cpu', }, { path: 'spec.containers.<name>.memory', }, { path: 'spec.containers.<name>.command', }, { path: 'spec.containers.<name>.args', array: true, }, // TODO volume { path: 'spec.containers.<name>.env.<name>.value', }, { path: 'spec.containers.<name>.inheritEnv', type: 'boolean', }, // Firewall { path: 'spec.firewallConfig.external.inboundAllowCIDR', array: true, }, { path: 'spec.firewallConfig.external.outboundAllowHostname', array: true, }, { path: 'spec.firewallConfig.external.outboundAllowCIDR', array: true, }, { path: 'spec.firewallConfig.internal.inboundAllowType', choices: ['none', 'same-gvc', 'same-org', 'workload-list'], }, { path: 'spec.firewallConfig.internal.inboundAllowWorkload', itemLinkResolver: (0, resolver_1.kindResolver)('workload'), array: true, }, // Default Options { path: 'spec.defaultOptions.autoscaling.metric', choices: ['concurrency', 'cpu', 'rps'], }, { path: 'spec.defaultOptions.autoscaling.target', type: 'number', }, { path: 'spec.defaultOptions.autoscaling.minScale', type: 'number', }, { path: 'spec.defaultOptions.autoscaling.maxScale', type: 'number', }, { path: 'spec.defaultOptions.autoscaling.scaleToZeroDelay', type: 'number', }, { path: 'spec.defaultOptions.autoscaling.maxConcurrency', type: 'number', }, { path: 'spec.defaultOptions.timeoutSeconds', type: 'number', }, { path: 'spec.defaultOptions.capacityAI', type: 'boolean', }, { path: 'spec.defaultOptions.debug', type: 'boolean', }, { path: 'spec.defaultOptions.suspend', type: 'boolean', }, // TODO Local Options // Job Spec { path: 'spec.job.schedule', }, { path: 'spec.job.concurrencyPolicy', choices: ['Forbid', 'Replace'], }, { path: 'spec.job.historyLimit', type: 'number', }, { path: 'spec.job.restartPolicy', choices: ['OnFailure', 'Never'], }, { path: 'spec.job.activeDeadlineSeconds', type: 'number', }, // Load Balancer { path: 'spec.loadBalancer.direct.enabled', type: 'boolean', }, { path: 'spec.loadBalancer.direct.ipSet', }, { path: 'spec.loadBalancer.geoLocation.enabled', type: 'boolean', }, { path: 'spec.loadBalancer.geoLocation.headers.asn', }, { path: 'spec.loadBalancer.geoLocation.headers.city', }, { path: 'spec.loadBalancer.geoLocation.headers.country', }, { path: 'spec.loadBalancer.geoLocation.headers.region', }, ], ...opts).toYargs()) // specific .command(new GetWorkload(resolver, ...opts).toYargs()) .command(new Open(resolver).toYargs()) .command(new Create(resolver).toYargs()) .command(new GetReplicas(resolver).toYargs()) .command(new ConnectExec(resolver, 'connect').toYargs()) .command(new ConnectExec(resolver, 'exec').toYargs()) .command(new ForceRedeployment(resolver).toYargs()) .command(new StartStop(resolver, 'start').toYargs()) .command(new StartStop(resolver, 'stop').toYargs()) .command(new Run(resolver).toYargs()) .command(new Cron(resolver).toYargs()) .command(new Replica(resolver).toYargs()) // deployment related .command(new deployment_1.ListDeployments(resolver).toYargs())); } handle() { } } exports.WorkloadCmd = WorkloadCmd; function withReplicaNameOption(isRequired) { return function (yargs) { return yargs.options({ 'replica-name': { type: 'string', requiresArg: true, demandOption: isRequired, description: 'Name of the replica', }, }); }; } exports.withReplicaNameOption = withReplicaNameOption; class ReplicaCommandBase extends command_1.Command { async getReplicas(resolve, args) { var _a; let result = { kind: 'list', itemKind: 'replicas', items: [], links: [], }; if (args.location) { args.location = (0, objects_1.toArray)(args.location); } // Fetch deployments let workloadLink = resolve.resourceLink(args.ref, this.session.context); const deployments = await this.client.get(`${workloadLink}/deployment`); for (const _deployment of deployments.items) { const deployment = _deployment; // Filter locations if (args.location && !args.location.includes(deployment.name)) { continue; } // Extract the deployment endpoint let remoteEndpoint = (_a = deployment.status) === null || _a === void 0 ? void 0 : _a.remote; // Check if the deployment remote endpoint is defined, and show an error accordingly if (!remoteEndpoint) { client_1.wire.debug(`WARNING: Location '${deployment.name}' endpoint is unavailable yet. Skipping...`); continue; } // Override remote endpoint with what is specified in the env variable if (process.env.TERMINAL_SERVER_URL) { remoteEndpoint = process.env.TERMINAL_SERVER_URL; } const replicaListLink = `${remoteEndpoint}/replicas/org/${this.session.context.org}/gvc/${this.session.context.gvc}/workload/${args.ref}`; const replicaList = await this.client.get(replicaListLink); result.items.push(...replicaList.items); result.links.push(...replicaList.links); } if (args.replicaName) { result.items = result.items.filter((item) => item.name === args.replicaName); } return result; } } class GetReplicas extends ReplicaCommandBase { constructor(resolve) { super(); this.resolve = resolve; this.command = 'get-replicas <ref>'; this.describe = 'Get the replicas of the referenced workload in a given location'; this.deprecated = `This subcommand is deprecated, use 'replica get' instead.`; } builder(yargs) { return (0, functions_1.pipe)( // generic_1.withSingleRef, generic_1.withMultipleLocationsOption, options_1.withAllOptions)(yargs); } async handle(args) { const replicaList = await this.getReplicas(this.resolve, args); this.session.outFormat(replicaList); } } class Replica extends command_1.Command { constructor(resolve) { super(); this.resolve = resolve; this.command = 'replica'; this.describe = 'Manage workload replicas'; } builder(yargs) { return (yargs .demandCommand() .version(false) .help() // specific .command(new GetReplica(this.resolve).toYargs()) .command(new StopReplica(this.resolve).toYargs())); } handle() { } } class GetReplica extends ReplicaCommandBase { constructor(resolve) { super(); this.resolve = resolve; this.command = 'get <ref>'; this.describe = 'Get the replica of the referenced workload in a given location'; } builder(yargs) { return (0, functions_1.pipe)( // generic_1.withSingleRef, withReplicaNameOption(false), generic_1.withMultipleLocationsOption, options_1.withAllOptions)(yargs); } async handle(args) { const replicaList = await this.getReplicas(this.resolve, args); if (this.session.format.max) { replicaList.items = replicaList.items.slice(0, this.session.format.max); } this.session.outFormat(replicaList); } } class StopReplica extends ReplicaCommandBase { constructor(resolve) { super(); this.resolve = resolve; this.command = 'stop <ref>'; this.describe = 'Stop the replica of the referenced workload in a given location'; } builder(yargs) { return (0, functions_1.pipe)( // generic_1.withSingleRef, withReplicaNameOption(true), (0, generic_1.withSingleLocationOption)(true), options_1.withAllOptions)(yargs); } async handle(args) { const workloadSelfLink = this.resolve.resourceLink(args.ref, this.session.context); const failures = []; const accumulator = { kind: 'list', itemKind: 'stopReplica', items: [], links: [], }; try { const command = { type: 'stopReplica', spec: { replica: args.replicaName, location: args.location, }, }; const response = await this.client.axios.post(`${workloadSelfLink}/-command`, command); if (response.status === 201) { const resource = { workloadName: args.ref, location: args.location, replica: args.replicaName, status: 'Stopping', startTime: new Date(response.headers.date), }; accumulator.items.push(resource); } else { throw new Error(`Unexpected status code ${response.status} while stop the replica ${args.replicaName} in location ${location}`); } } catch (e) { failures.push(e); } await this.session.outFormat(accumulator); if (failures.length > 0) { this.session.abort({ error: failures }); } } } function withTerminalOptions(yargs) { return yargs.options({ replica: { requiresArg: true, description: 'Replica of the deployment', }, container: { requiresArg: true, description: 'Container name of the workload', }, }); } function withConnectOptions(yargs) { return yargs.options({ shell: { requiresArg: false, description: 'Shell to open on replica', default: 'bash', alias: 's', }, }); } function withExecOptions(yargs) { return yargs.options({ '-': { requiresArg: true, description: 'Command to execute on replica', }, stdin: { boolean: true, default: false, description: 'Pass stdin to the container', alias: 'i', }, tty: { boolean: true, default: false, description: 'Stdin is a TTY', alias: 't', }, quiet: { boolean: true, default: false, description: 'Only print output from the remote session', alias: 'q', }, }); } class ConnectExec extends command_1.Command { constructor(resolve, type) { super(); this.resolve = resolve; this.type = type; this.command = `${this.type} <ref>`; this.describe = this.type === 'connect' ? 'Connect to a replica of the workload' : 'Exec a command on a replica of the workload'; } builder(yargs) { return (0, functions_1.pipe)( // generic_1.withSingleRef, (0, generic_1.withSingleLocationOption)(false), withTerminalOptions, this.type === 'connect' ? withConnectOptions : withExecOptions, options_1.withAllOptions)(yargs); } async handle(args) { let isInteractiveSession = false; let lastMessage = ''; let pingInterval; let terminateTimeout; // Read command from args const cmd = args['--']; // Require command for type exec if (this.type === 'exec' && (!cmd || !Array.isArray(cmd) || cmd.length < 1)) { this.session.abort({ message: 'ERROR: A command and args are required' }); } // Get terminal configuration const terminalServer = new terminal_server_1.TerminalServer(this.session, this.client); const terminalConfig = await terminalServer.createConfig(args.ref, args.location, args.replica, args.container); // Configure settings depending on command type if (this.type === 'connect') { terminalConfig.request.shell = args.shell || 'bash'; terminalConfig.request.stdin = true; terminalConfig.request.tty = true; terminalConfig.request.quiet = true; } else if (this.type === 'exec') { terminalConfig.request.command = cmd; terminalConfig.request.stdin = args.stdin; terminalConfig.request.tty = args.tty; terminalConfig.request.quiet = args.quiet; } // If tty is true, then this is definitely an interactive terminal session if (terminalConfig.request.tty) { isInteractiveSession = true; terminalConfig.request.width = process.stdout.columns; terminalConfig.request.height = process.stdout.rows; // Enable raw mode, which will process each keystroke as it happens if (process.stdin.isTTY) { process.stdin.setRawMode(true); } } // Initialize a new WebSocket connection var client = new WebSocket(terminalConfig.remoteSocket, { origin: 'http://localhost', }); // Ensure terminal resize events are handled efficiently and do not trigger // too frequently, which could cause performance issues const onProcessResize = (0, helpers_1.terminalResizeDebounce)(client); // Play ping pong only in an interactive terminal session if (isInteractiveSession) { pingInterval = setInterval(() => { // Ping the terminal server client.ping(); client_1.wire.debug('>>>>>>> WS Ping Sent'); // Start a timeout, which will terminate the socket connection after PONG_DEADLINE_MS amount of time terminateTimeout = setTimeout(() => { client.terminate(); client_1.wire.debug('>>>>>>> WS Session Terminated'); }, types_1.PONG_DEADLINE_MS); }, types_1.PING_INTERVAL_MS); client.on('pong', () => { client_1.wire.debug('<<<<<<< WS Pong Received'); // Once pong is received, cancel the terminate timeout if (terminateTimeout) { clearTimeout(terminateTimeout); terminateTimeout = null; } }); } client.on('open', () => { client_1.wire.debug('<<<<<<< WS Open Event with Token: ' + (0, client_1.convertAuthTokenForDebug)(terminalConfig.request.token)); // Initialize Request client.send(JSON.stringify(terminalConfig.request)); if (isInteractiveSession) { // Subscribe to the resize event process.stdout.on('resize', onProcessResize); // Subscribe to the data event to send every user input to the remote terminal process.stdin.on('data', (chunk) => { const message = { type: 'data', buffer: Array.from(chunk), width: process.stdout.columns, height: process.stdout.rows, }; client.send(JSON.stringify(message)); }); } else { // Subscribe to the data event to send standard input to the remote terminal process.stdin.on('data', (chunk) => { client.send(chunk, { fin: false, binary: true }); }); // Subscribe to the end event to signal end of file process.stdin.on('end', () => { client.send(Buffer.from([]), { fin: true }); }); } }); client.on('message', (event) => { client_1.wire.debug('<<<<<<< WS Message Received'); const msg = event.toString(); // Store the last message received if (msg.trim()) { lastMessage = msg; } // Echo the remote terminal message to the user process.stdout.write(msg); }); client.on('error', (event) => { // Stop playing ping pong if (pingInterval) { clearInterval(pingInterval); } process.stderr.write('error: ' + event.message); }); client.on('close', () => { client_1.wire.debug('<<<<<<< WS Close Event info'); // Unsubscribe to resize event for when session is interactive if (isInteractiveSession) { process.stdout.off('resize', onProcessResize); } // Stop playing ping pong if (pingInterval) { clearInterval(pingInterval); } if (lastMessage.includes('Failed to create terminal session: command terminated with exit code')) { try { const code = Number(lastMessage.split('exit code ')[1]); process.exit(code); } catch (e) { process.exit(1); } } if (lastMessage.includes('Failed to create terminal session: Internal error occurred:')) { process.exit(1); } process.exit(0); }); } } exports.ConnectExec = ConnectExec; class Open extends command_1.Command { constructor(resolve) { super(); this.resolve = resolve; this.command = 'open <ref>'; this.describe = "Open the referenced workload's endpoint in your browser"; } builder(yargs) { return (0, functions_1.pipe)( // generic_1.withSingleRef, options_1.withAllOptions)(yargs); } async handle(args) { let link = this.resolve.resourceLink(args.ref, this.session.context); const body = await this.client.get(link); const endpoint = body.status.endpoint; if (endpoint) { open(endpoint, { wait: false, }); } } } exports.Open = Open; class Create extends command_1.Command { constructor(resolve) { super(); this.resolve = resolve; this.command = 'create'; this.describe = 'Create a new workload'; } builder(yargs) { return (0, functions_1.pipe)( // (yargs) => { return yargs.options({ name: { describe: 'Name of the new workload', requiresArg: true, demandOption: true, }, description: { alias: 'desc', describe: 'Optional description, defaults to the name if not set', }, type: { describe: 'Workload type', default: 'serverless', requiresArg: true, choices: ['serverless', 'standard'], }, image: { describe: 'Name of the container image', requiresArg: true, demandOption: true, }, port: { describe: 'Port to expose', default: 8080, requiresArg: true, number: true, }, env: { describe: 'Environment variables in KEY=VALUE format', requiresArg: true, multiple: true, }, public: { describe: 'Unconstrained ingress & egress for the workload', boolean: true, }, identity: { describe: 'Attach the named identity to the workload spec', requiresArg: true, }, 'enable-debug': { describe: 'Enables debug response headers when the headers "x-cpln-debug: true" is in the request.', default: false, boolean: true, }, 'inherit-env': { describe: 'Inherits the environment variables set at GVC level.', default: false, boolean: true, }, 'container-name': { describe: 'Name of the container item', requiresArg: true, }, cpu: { describe: 'Allocate CPU resources', requiresArg: true, default: '50m', }, memory: { alias: 'mem', describe: 'Allocate Memory', requiresArg: true, default: '128Mi', }, volume: { describe: 'Mount Object Store (S3, GCS, AzureBlob) buckets as file system. E.g. s3://backups@/mnt/storage', requiresArg: true, multiple: true, }, }); }, generic_1.withTagOptions, options_1.withAllOptions)(yargs); } async handle(args) { var _a, _b, _c; let link = this.resolve.parentLink(this.session.context); let fwc = undefined; if (args.public) { fwc = { external: { inboundAllowCIDR: ['0.0.0.0/0'], outboundAllowCIDR: ['0.0.0.0/0'], }, internal: { inboundAllowType: 'same-org', }, }; } let dfo = undefined; if (args.enableDebug) { dfo = { debug: true, }; } const inferredName = args.image.split('/').pop().split('@').shift().split(':').shift().toLowerCase(); const container = { name: args.containerName || inferredName, image: args.image, env: (0, objects_1.toEnv)((_a = (0, objects_1.toArray)(args.env)) !== null && _a !== void 0 ? _a : []), inheritEnv: args.inheritEnv, cpu: args.cpu, memory: args.memory, volumes: (0, objects_1.toVolumes)((_b = (0, objects_1.toArray)(args.volume)) !== null && _b !== void 0 ? _b : []), ports: [{ protocol: 'http', number: args.port }], }; const req = { name: args.name, description: (_c = args.description) !== null && _c !== void 0 ? _c : args.name, tags: (0, generic_1.fromTagOptions)(args), spec: { type: args.type, firewallConfig: fwc, defaultOptions: dfo, containers: [container], }, }; if (args.identity) { req.spec.identityLink = '//identity/' + args.identity; } const body = await this.client.create(link, req); this.session.outFormat(body); } } exports.Create = Create; class GetWorkload extends generic_1.Get { constructor(resolve, ...funcs) { super('workloads', resolve, ...funcs); this.describe = 'Retrieve one or more referenced workloads'; } async list(args) { let link; if (args.allGvcs) { link = this.resolve.homeLink(this.session.context); } else { link = this.resolve.parentLink(this.session.context); } const body = await this.client.get(link); await (0, resultFetcher_1.fetchPages)(this.client, this.session.format.max, body); for (let item of body.items) { item = await this.insertReadyState(item); } this.session.outFormat(body); } async get(args) { var _a; args.ref = _.uniq((0, objects_1.toArray)(args.ref)); if (((_a = args.ref) === null || _a === void 0 ? void 0 : _a.length) == 1) { const link = this.resolve.resourceLink(args.ref[0], this.session.context); let body = await this.client.get(link); body = await this.insertReadyState(body); this.session.outFormat(body); return; } const accumulator = { kind: 'list', itemKind: this.resolve.kind, items: [], links: [], }; for (let ref of args.ref) { const link = this.resolve.resourceLink(ref, this.session.context); let body = await this.client.get(link); body = await this.insertReadyState(body); accumulator.items.push(body); } if (accumulator.items.length > 0) { this.session.outFormat(accumulator); } } async insertReadyState(body) { const deploymentLink = body.links.find((l) => l.rel === 'deployment').href; const deploymentRes = await this.client.get(deploymentLink); const healthObject = (0, workload_1.getWorkloadHealth)(deploymentRes.items, body); body.status = Object.assign({}, body.status, healthObject); return body; } builder(yargs) { return (0, functions_1.pipe)(super.builder.bind(this), (yargs) => { return yargs.options({ 'all-gvcs': { boolean: true, describe: 'Show workloads from all the global virtual clouds within the current or overridden organization', }, }); })(yargs); } } exports.GetWorkload = GetWorkload; class ForceRedeployment extends command_1.Command { constructor(resolve) { super(); this.resolve = resolve; this.command = 'force-redeployment <ref...>'; this.describe = 'Force redeployment of the workload(s)'; } builder(yargs) { return (0, functions_1.pipe)( // generic_1.withMultipleRefs, options_1.withAllOptions)(yargs); } async handle(args) { var _a, _b, _c; args.ref = _.uniq((0, objects_1.toArray)(args.ref)); let failedResponse; const accumulator = { kind: 'list', itemKind: this.resolve.kind, items: [], links: [], }; for (let ref of args.ref) { try { const workloadLink = this.resolve.resourceLink(ref, this.session.context); const body = { tags: { 'cpln/deployTimestamp': new Date().toISOString() } }; const workload = await this.client.patch(workloadLink, body); accumulator.items.push(workload); } catch (e) { failedResponse = ((_a = e.response) === null || _a === void 0 ? void 0 : _a.data) || e; this.session.err(((_c = (_b = e.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) || e.message); } } if (accumulator.items.length > 0) { this.session.outFormat(accumulator); } if (failedResponse) { this.session.outFormat(failedResponse, { output: 'json' }); process.exit(1); } } } exports.ForceRedeployment = ForceRedeployment; class StartStop extends command_1.Command { constructor(resolve, type) { super(); this.resolve = resolve; this.type = type; this.command = `${this.type} <ref...>`; this.describe = `${this.type[0].toUpperCase()}${this.type.substring(1)} the workload(s)`; } builder(yargs) { return (0, functions_1.pipe)( // generic_1.withMultipleRefs, options_1.withAllOptions)(yargs); } async handle(args) { var _a, _b, _c; args.ref = _.uniq((0, objects_1.toArray)(args.ref)); let failedResponse; const accumulator = { kind: 'list', itemKind: this.resolve.kind, items: [], links: [], }; for (let ref of args.ref) { try { const workloadLink = this.resolve.resourceLink(ref, this.session.context); let workload = await this.client.get(workloadLink); const body = { spec: { defaultOptions: { suspend: this.type === 'stop' } } }; if (workload.spec.localOptions && Array.isArray(workload.spec.localOptions) && workload.spec.localOptions.length > 0) { body.spec.localOptions = []; for (const localOption of workload.spec.localOptions) { body.spec.localOptions.push({ location: localOption.location, suspend: this.type === 'stop', }); } } workload = await this.client.patch(workloadLink, body); accumulator.items.push(workload); } catch (e) { failedResponse = ((_a = e.response) === null || _a === void 0 ? void 0 : _a.data) || e; this.session.err(((_c = (_b = e.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) || e.message); } } if (accumulator.items.length > 0) { this.session.outFormat(accumulator); } if (failedResponse) { this.session.outFormat(failedResponse, { output: 'json' }); process.exit(1); } } } exports.StartStop = StartStop; function withRunOptions(yargs) { return yargs.options({ clone: { describe: 'Clone a workload', requiresArg: true, }, tag: { description: 'Attach tags (e.g., --tag drink=water)', multiple: true, }, image: { describe: 'Override image', requiresArg: true, }, interactive: { describe: 'Make the session interactive', boolean: true, default: false, alias: 'i', }, remove: { describe: 'Deletes the workload after the command is run', boolean: true, default: false, alias: 'rm', }, cpu: { describe: 'Set allocated CPU for the main container', requiresArg: true, }, memory: { alias: 'mem', describe: 'Set allocated memory for the main container', requiresArg: true, }, env: { describe: 'Environment variables in KEY=VALUE format', requiresArg: true, multiple: true, }, command: { alias: 'c', describe: 'Container command', requiresArg: true, }, arg: { alias: 'a', describe: 'Container args', requiresArg: true, multiple: true, }, shell: { requiresArg: false, description: 'Shell to use, only valid when interactive flag is true', default: 'bash', alias: 's', }, location: { describe: 'Location to run the command', requiresArg: true, }, container: { describe: 'Which container to run the command in, only used when "clone" option is used', requiresArg: true, }, }); } class Run extends command_1.Command { constructor(resolve) { super(); this.resolve = resolve; this.command = 'run'; this.describe = 'Run a command with a workload instance'; this.isWorkloadDeleteRequested = false; this.isWorkloadCreated = false; this.workloadSelfLink = ''; this.workloadName = ''; } builder(yargs) { return (0, functions_1.pipe)( // withRunOptions, options_1.withAllOptions)(yargs); } async handle(args) { var _a, _b; let isInteractiveSession = false; let sentCommand = false; let lastMessage = ''; let terminateTimeout; let pingInterval; // Determine where user wants to delet workload after use / error this.isWorkloadDeleteRequested = args.remove || false; // Resolve GVC link const gvcSelfLink = (0, resolver_1.resolveToLink)('gvc', this.session.context.gvc, this.session.context); // Validate org and gvc if (!this.session.context.org) { this.session.abort({ message: 'ERROR: An organization is required. Please, specify one using the --org option.' }); } if (!this.session.context.gvc) { this.session.abort({ message: 'ERROR: A GVC is required. Please, specify one using the --gvc option.' }); } // Read command from args const cmd = args['--']; // Require command for type exec if (!cmd || !Array.isArray(cmd) || cmd.length < 1) { logger_1.logger.error('Run - Command was not provided'); this.session.abort({ message: 'ERROR: A command and args are required' }); } try { logger_1.logger.info(`Run - GVC Link: ${gvcSelfLink}`); // Fetch GVC const gvc = await this.client.get(gvcSelfLink); // Determine location self link let locationSelfLink = ''; if (args.location) { locationSelfLink = (0, resolver_1.resolveToLink)('location', args.location, this.session.context); } else if (((_b = (_a = gvc.spec) === null || _a === void 0 ? void 0 : _a.staticPlacement) === null || _b === void 0 ? void 0 : _b.locationLinks) && gvc.spec.staticPlacement.locationLinks.length > 0) { locationSelfLink = gvc.spec.staticPlacement.locationLinks[0]; } else { this.session.abort({ message: 'ERROR: Gvc has no locations' }); } logger_1.logger.info('Run - Location Link: ' + locationSelfLink); // Create workload const workload = await this.createWorkload(gvcSelfLink, locationSelfLink, args); // Delete workload on terminal termination process.on('SIGINT', async () => { logger_1.logger.info('\nCaught SIGINT'); await this.tryDeleteWorkload(); process.exit(); }); // Get terminal config const terminalConfig = await this.createTerminalConfig(workload, locationSelfLink, args.container); // Assumption - I guess we wait a minute here before establishing a WebSocket connection because of some backend issue // TODO remove this after backend fix await (0, time_1.sleep)(1 * 60 * 1000); logger_1.logger.info('Run - Starting Websocket Connection'); if (args.interactive) { terminalConfig.request.shell = args.shell || "sh -c 'bash -i || zsh -i || sh -i'"; terminalConfig.request.stdin = true; terminalConfig.request.tty = true; terminalConfig.request.quiet = true; } else { terminalConfig.request.command = cmd; terminalConfig.request.stdin = true; terminalConfig.request.tty = false; terminalConfig.request.quiet = false; } // If tty is true, then this is definitely an interactive terminal session if (terminalConfig.request.tty) { isInteractiveSession = true; terminalConfig.request.width = process.stdout.columns; terminalConfig.request.height = process.stdout.rows; // Enable raw mode, which will process each keystroke as it happens if (process.stdin.isTTY) { process.stdin.setRawMode(true); } } // Initialize a new WebSocket connection client_1.wire.debug('>>>>>>> Creating WS'); const clientStartTick = new Date().getTime(); const client = new WebSocket(terminalConfig.remoteSocket, { origin: 'http://localhost' }); client_1.wire.debug('>>>>>>> WS Created'); // Ensure terminal resize events are handled efficiently and do not trigger // too frequently, which could cause performance issues const onProcessResize = (0, helpers_1.terminalResizeDebounce)(client); if (isInteractiveSession) { pingInterval = setInterval(() => { // Ping the terminal server client_1.wire.debug('>>>>>>> WS Ping Sending'); client.ping(); client_1.wire.debug('>>>>>>> WS Ping Sent'); // Start a timeout, which will terminate the socket connection after PONG_DEADLINE_MS amount of time terminateTimeout = setTimeout(() => { client.terminate(); }, types_1.PONG_DEADLINE_MS); }, types_1.PING_INTERVAL_MS); client.on('pong', () => { client_1.wire.debug('<<<<<<< WS Pong Received'); // Once pong is received, cancel the terminate timeout if (terminateTimeout) { clearTimeout(terminateTimeout); terminateTimeout = null; } }); } async function initializeClient() { client_1.wire.debug(`>>>>>>> Initializing client. Ready state is: ${client.readyState}`); // Recursively check for WebSocket connection read state if (client.readyState !== 1) { logger_1.logger.info(`RUN - Ready State is: ${client.readyState}`); await (0, time_1.sleep)(5 * 1000); logger_1.logger.info(`RUN - Retrying initialization of client`); initializeClient(); return; } // Initialize Request client.send(JSON.stringify(terminalConfig.request)); if (isInteractiveSession) { // Subscribe to resize event process.stdout.on('resize', onProcessResize); process.stdin.on('data', (chunk) => { const message = { type: 'data', buffer: Array.from(chunk), width: process.stdout.columns, height: process.stdout.rows, }; client.send(JSON.stringify(message)); }); } else { process.stdin.on('data', (chunk) => { client.send(chunk, { fin: false, binary: true }); }); process.stdin.on('end', () => { client.send(Buffer.from([]), { fin: true }); }); } } client.on('open', () => { const duration = new Date().getTime() - clientStartTick; client_1.wire.debug('<<<<<<< WS Open Event with Token: ' + (0, client_1.convertAuthTokenForDebug)(terminalConfig.request.token)); client_1.wire.debug('<<<<<<< WS Open Event Duration: ' + duration); initializeClient(); }); client.on('message', (event) => { client_1.wire.debug('<<<<<<< WS Message Received'); const msg = event.toString(); // Store the last message received if (msg.trim()) { lastMessage = msg; } // Echo the remote terminal message to the user process.stdout.write(msg); if (!sentCommand) { sentCommand = true; const _cmd = cmd.join(' ') + '\n'; logger_1.logger.info('RUN - Sending initial command: ' + _cmd); if (isInteractiveSession) { const message = { type: 'data', buffer: Array.from(Buffer.from(_cmd)), width: process.stdout.columns, height: process.stdout.rows, }; client.send(JSON.stringify(message)); } else { client.send(Buffer.from(_cmd)); } } }); client.on('error', (event) => { client_1.wire.debug(`<<<<<<< WS Error. Message: ${event.message}`); // Stop playing ping pong if (pingInterval) { clearInterval(pingInterval); } process.stderr.write('error: ' + event.message); }); client.on('close', async () => { client_1.wire.debug('<<<<<<< WS Close Event info'); // Unsubscribe to resize event for connect if (isInteractiveSession) { process.stdout.off('resize', onProcessResize); } // Stop playing ping pong if (pingInterval) { clearInterval(pingInterval); } logger_1.logger.info('Run - Websocket closed'); // Delete workload after the socket connection has closed await this.tryDeleteWorkload(); if (lastMessage.includes('Failed to create terminal session: Internal error occurred:')) { process.exit(1); } process.exit(0); }); } catch (e) { logger_1.logger.info(`Run - Exception. Message: ${e.message}`); await this.tryDeleteWorkload(); this.session.abort({ error: e }); } } // ANCHOR - Private Methods async createWorkload(gvcSelfLink, locationSelfLink, args) { var _a, _b, _c; let workload = {}; if (args.clone) { logger_1.logger.info('Run - With Clone: ' + args.clone); const workloadLink = this.resolve.resourceLink(args.clone, this.session.context); const _workload = await this.client.get(workloadLink); delete _workload.links; delete _workload.status; this.workloadName = (0, names_1.nextName)(`${_workload.name}-`); _workload.name = this.workloadName; workload = _workload; } else { logger_1.logger.info('Run - Without Clone'); this.workloadName = (0, names_1.nextName)(`run-`); workload = { name: this.workloadName, spec: getRunWorkloadSpec(), }; } // We know for sure that spec and containers are defined workload.spec = workload.spec; workload.spec.containers = workload.spec.containers; if (args.container) { const workloadHasContainer = workload.spec.containers.some((c) => c.name === args.container); if (!workloadHasContainer) { this.session.abort({ message: `Container "${args.container}" does not exist in the workload.` }); } } if (args.tag) { args.tag = _.uniq((0, objects_1.toArray)(args.tag)); workload.tags = { ...workload.tags, ...(0, tags_1.expressionToTags)(args.tag) }; } if (workload.spec.defaultOptions) { workload.spec.defaultOptions.capacityAI = false; workload.spec.defaultOptions.suspend = true; } else { workload.spec.defaultOptions = { capacityAI: false, suspend: true, }; } workload.spec.localOptions = [ { location: locationSelfLink, suspend: false, capacityAI: false, autoscaling: { minScale: 1, maxScale: 1, }, }, ]; if (args.image) { workload.spec.containers[0].image = args.ima