@controlplane/cli
Version:
Control Plane Corporation CLI
1,261 lines • 66.8 kB
JavaScript
"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