@controlplane/cli
Version:
Control Plane Corporation CLI
2,395 lines • 100 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CronRun = exports.GetCron = exports.StopCron = exports.StartCron = exports.Cron = exports.Run = exports.StartStop = exports.ForceRedeployment = exports.GetWorkload = exports.Create = exports.Open = exports.ConnectExec = exports.WorkloadCmd = void 0;
exports.withReplicaNameOption = withReplicaNameOption;
const WebSocket = require("ws");
const _ = require("lodash");
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 browser_1 = require("../util/browser");
const functions_1 = require("../util/functions");
const objects_1 = require("../util/objects");
const client_1 = require("../session/client");
const logger_1 = require("../util/logger");
const format_1 = require("../util/format");
const names_1 = require("../util/names");
const workload_1 = require("../util/workload");
const time_1 = require("../util/time");
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.Audit(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>.env.<name>',
},
{
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',
},
// Request Retry Policy
{
path: 'spec.requestRetryPolicy.attempts',
type: 'number',
},
{
path: 'spec.requestRetryPolicy.retryOn',
array: true,
},
// Extras (BYOK Kubernetes customizations)
{
path: 'spec.extras',
json: true,
},
{
path: 'spec.extras.affinity',
json: true,
},
{
path: 'spec.extras.tolerations',
json: true,
},
{
path: 'spec.extras.topologySpreadConstraints',
json: true,
},
], ...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',
},
});
};
}
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);
replicaList.items = (0, resultFetcher_1.applyMaxAndWarn)(this.session, replicaList.items);
await 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({
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',
},
'-': {
requiresArg: true,
description: 'Command to execute on replica',
},
});
}
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) {
// 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 the request 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;
}
// Run the session via the shared terminal server and capture its exit code
const code = await terminalServer.exec(terminalConfig);
// Exit the process with the session's exit code, falling back to 1 when it is not a valid code
try {
process.exit(code);
}
catch (e) {
process.exit(1);
}
}
}
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) {
await (0, browser_1.openInBrowser)(endpoint);
}
}
}
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: 'standard',
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.withDryRunOption, 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, body);
for (let item of body.items) {
item = await this.insertReadyState(item);
}
await 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 healthObject = (0, workload_1.getWorkloadHealth)(body.health);
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.withDryRunOption, 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.withDryRunOption, 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
.parserConfiguration({
'sort-commands': true,
'strip-aliased': true,
'parse-numbers': false,
'populate--': true,
'nargs-eats-options': true,
})
.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,
nargs: 1,
},
arg: {
alias: 'a',
describe: 'Container args',
requiresArg: true,
multiple: true,
nargs: 1,
},
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, _c;
let isInteractiveSession = false;
let sentCommand = false;
let lastMessage = '';
let hadTransportError = false;
let terminateTimeout;
let pingInterval;
// Determine where user wants to delet workload after use / error
this.isWorkloadDeleteRequested = args.remove || false;
// Validate org and gvc before resolving any link so the helpful messages are reached
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.' });
}
// Resolve GVC link
const gvcSelfLink = (0, resolver_1.resolveToLink)('gvc', this.session.context.gvc, this.session.context);
// 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 = '';
const gvcLocationLinks = (_c = (_b = (_a = gvc.spec) === null || _a === void 0 ? void 0 : _a.staticPlacement) === null || _b === void 0 ? void 0 : _b.locationLinks) !== null && _c !== void 0 ? _c : [];
if (args.location) {
// Ensure the requested location is one the GVC actually deploys to, otherwise the workload
// would deploy nowhere and the readiness wait would hang until it times out
locationSelfLink = (0, resolver_1.resolveToLink)('location', args.location, this.session.context);
if (!gvcLocationLinks.includes(locationSelfLink)) {
const available = gvcLocationLinks.map(links_1.getLastPartOfLink).join(', ') || '(none)';
this.session.abort({
message: `ERROR: Location '${args.location}' is not part of GVC '${this.session.context.gvc}'. Available locations: ${available}.`,
});
}
}
else if (gvcLocationLinks.length > 0) {
locationSelfLink = gvcLocationLinks[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: [...Buffer.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}`);
// A transport-level failure must not be reported as a successful run
hadTransportError = true;
// 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();
// Exit with the session's code, treating a transport error as a failure
process.exit(terminal_server_1.TerminalServer.exitCodeForClose(lastMessage, hadTransportError));
});
}
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.image;
}
else if (!args.clone) {
// not cloned and not given an image, so it uses ubuntu we need the sleep cmd
workload.spec.containers[0].command = 'sleep';
workload.spec.containers[0].args = ['999d'];
}
if (args.cpu) {
workload.spec.containers[0].cpu = args.cpu;
}
if (args.memory) {
workload.spec.containers[0].memory = args.memory;
}
if (!workload.spec.containers[0].env) {
workload.spec.containers[0].env = [];
}
// Record who ran the workload, but only when an email is known (service accounts may have none)
const runByEmail = (_a = this.session.profile.authInfo) === null || _a === void 0 ? void 0 : _a.email;
if (runByEmail) {
workload.spec.containers[0].env.push({ name: 'XCPLN_RUN_BY', value: runByEmail });
}
const envArr = (0, objects_1.toEnv)((_b = (0, objects_1.toArray)(args.env)) !== null && _b !== void 0 ? _b : []);
for (let env of envArr) {
workload.spec.containers[0].env.push(env);
}
if (args.command) {
workload.spec.containers[0].command = args.command;
workload.spec.containers[0].args = (_c = (0, objects_1.toArray)(args.arg)) !== null && _c !== void 0 ? _c : [];
}
logger_1.logger.info('Run - Workload Body: ' + JSON.stringify(workload, null, 2));
// Create workload
await this.client.create(gvcSelfLink + '/workload', workload);
this.isWorkloadCreated = true;
this.workloadSelfLink = this.resolve.resourceLink(this.workloadName, this.session.context);
logger_1.logger.info('Run - Workload Link: ' + this.workloadSelfLink);
// Fetch the created workload
return await this.client.get(this.workloadSelfLink);
}
async createTerminalConfig(workload, locationSelfLink, container) {
let pod = '';
let remoteHost = '';
// Determine location name
const locationName = locationSelfLink.split('/location/')[1];
// Determine container name
let containerName = container || workload.spec.containers[0].name;
logger_1.logger.info('Run - Container Name: ' + containerName);
logger_1.logger.info('Workload instance name is: ' + this.workloadName);
// The platform's most recent account of the deployment
let lastStatusSummary;
// Wait for the workload to become ready
await (0, functions_1.retryFn)(async (repeatIndex) => {
logger_1.logger.info('RUN - Waiting for workload to be ready ' + repeatIndex);
// Fetch workload deployment at the specific location
const deployment = await this.client.get(this.workloadSelfLink + '/deployment/' + locationName);
// Describe what the platform reports about the deployment's expected version
const report = (0, workload_1.describeDeploymentStatus)(deployment, containerName);
// Remember the remote endpoint as soon as the platform assigns one, for the replica lookup below
if (report.remote) {
remoteHost = report.remote;
}
// Log the account only when it changes, so a long wait reports each state once
if (report.summary !== lastStatusSummary) {
logger_1.logger.info('RUN - Deployment status: ' + report.summary);
}
lastStatusSummary = report.summary;
// The target container is ready to accept the command
if (report.isContainerReady) {
return;
}
throw new Error('Workload is not ready yet');
}, {
repeatTimes: 200,
onFailure: async () => {
await this.tryDeleteWorkload();
this.session.abort({ message: this.workloadNotReadyMessage(lastStatusSummary) });
},
});
// Find replica
await (0, functions_1.retryFn)(async (repeatIndex) => {
const replicaListLink = `${remoteHost}/replicas${this.workloadSelfLink}`;
// Fetch all workload replias
const replicaList = await this.client.get(replicaListLink);
if (replicaList.items.length < 1) {
logger_1.logger.info(`Run - Replica List is empty, retrying... - Attempt ${repeatIndex}`);
throw new Error('Replica list is empty');
}
else {
// TODO REQUEST get info from backend about which one is the latest healthy one, if there are multiple replicas
pod = replicaList.items[0];
}
}, {
repeatTimes: 20,
onFailure: async () => {
await this.tryDeleteWorkload();
this.session.abort({ message: 'No replica is present.' });
},
});
logger_1.logger.info('RUN - Workload is ready');
// Initialize a new terminal server instance
const terminalServer = new terminal_server_1.TerminalServer(this.session, this.client);
return await terminalServer.createConfig(workload.name, locationName, pod, containerName);
}
/**
* Builds the error message shown when a workload never became ready within the retry budget.
*
* @param {string | undefined} summary - (Optional) The platform's most recent account of the deployment.
* @returns {string} A message stating the command was not run, with the platform's account when known.
*/
workloadNotReadyMessage(summary) {
const base = 'ERROR: After 200 tries, the workload is still not ready, so the command was not run.';
if (summary) {
return `${base}\n\nThe platform last reported: ${summary}`;
}
return base;
}
async tryDeleteWorkload() {
if (this.isWorkloadDeleteRequested && this.isWorkloadCreated) {
logger_1.logger.info('\nDeleting workload instance...');
const manualDeleteCommand = `cpln workload delete ${this.workloadName} --org ${this.session.context.org} --gvc ${this.session.context.gvc}`;
await (0, functions_1.retryFn)(async () => {
try {
await this.client.delete(this.workloadSelfLink);
}
catch (e) {
// Print error
logger_1.logger.info(e);
throw e;
}
}, {
repeatTimes: 10,
intervalSec: 5,
onSuccess: () => logger_1.logger.info('Deleted workload'),
onFailure: () => {
this.session.out(`Unable to delete workload. Run the following command to delete workload manually: ${manualDeleteCommand}`);
},
});
}
}
}
exports.Run = Run;
/**
* Creates the default workload spec for a run workload.
*
* @returns {WorkloadSpec} The default workload spec for a run workload.
*/
function getRunWorkloadSpec() {
const container = {
name: 'main',
image: 'ubuntu:latest',
};
return {
type: 'standard',
containers: [container],
firewallConfig: {
external: {
inboundAllowCIDR: ['0.0.0.0/0'],
outboundAllowCIDR: ['0.0.0.0/0'],
},
internal: {
inboundAllowType: 'same-org',
},
},
defaultOptions: {
autoscaling: {
metric: 'cpu',
minScale: 1,
maxScale: 1,
},
},
};
}
class Cron extends command_1.Command {
constructor(resolve) {
super();
this.resolve = resolve;
this.command = 'cron';
this.describe = 'Manage cron workloads';
}
builder(yargs) {
return (yargs
//
.demandCommand()
.version(false)
.help()
// specific
.command(new StartCron(this.resolve).toYargs())
.command(new StopCron(this.resolve).toYargs())
.command(new GetCron(this.resolve).toYargs())
.command(new CronRun(this.resolve).toYargs()));
}
handle() { }
}
exports.Cron = Cron;
function withStartCronOptions(yargs) {
return yargs
.parserConfiguration({
'sort-commands': true,
'strip-aliased': true,
'parse-numbers': false,
'populate--': true,
'nargs-eats-options': true,
})
.options({
'container-name': {
type: 'string',
describe: 'Name of the container',
},
env: {
type: 'string',
multiple: true,
describe: 'One or more environment variables in KEY=VALUE format',
},
command: {
type: 'string',
nargs: 1,
describe: 'Override the entrypoint',
},
arg: {
type: 'string',
multiple: true,
nargs: 1,
describe: 'One or more custom command line argument that will be sent to the container',
},
'active-deadline-seconds': {
type: 'number',
describe: 'Maximum duration in seconds for the cron job to complete. Used by the deadline reconciler to detect orphaned commands.',
},
});
}
class StartCron extends command_1.Command {
constructor(resolve) {
super();
this.resolve = resolve;
this.command = 'start <ref>';
this.describe = 'Start a cron job';
}
builder(yargs) {
return (0, functions_1.pipe)(
//
generic_1.withSingleRef, generic_1.withMultipleLocationsOption, withStartCronOptions, generic_1.withOptionalFileInputOptions, options_1.withAllOptions)(yargs);
}
async handle(args) {
var _a, _b, _c, _d;
// Initialize
let containerOverrides = [];
const workloadSelfLink = this.resolve.resourceLink(args.ref, this.session.context);
const failures = [];
const accumulator = {
kind: 'list',
itemKind: 'workloadCron',
items: [],
links: [],
};
if (args.containerName || args.env || args.command || args.arg) {
if (args.file) {
this.session.abort({
message: 'ERROR: You can either provide files containing container overrides or provide container overrides through arguments.',
});
}
let containerName;
if (args.containerName) {
containerName = args.containerName;
}
else {
// Fetch workload and get the first container name
const workload = await this.client.get(workloadSelfLink);
containerName = workload.spec.containers[0].name;
}
const containerOverride = {
name: containerName,
};
if (args.env) {
containerOverride.env = (0, objects_1.toEnv)((0, objects_1.toArray)(args.env));
}
if (args.command) {
containerOverride.command = args.command;
}
if (args.arg) {
containerOverride.args = (0, objects_1.toArray)(args.arg);
}
containerOverrides.push(containerOverride);
}
// Determine locations
if (args.location) {
args.location = (0, objects_1.toArray)(args.location);
}
else {
// Fetch GVC locations
const gvcSelfLink = (0, resolver_1.resolveToLink)('gvc', this.session.context.gvc, this.session.context);
const gvc = await this.client.get(gvcSelfLink);
// Get location names from GVC
args.location = (_d = (_c = (_b = (_a = gvc.spec) === null || _a === void 0 ? void 0 : _a.staticPlacement) === null || _b === void 0 ? void 0 : _b.locationLinks) === null || _c === void 0 ? void 0 : _c.map((l) => (0, links_1.getLastPartOfLink)(l))) !== null && _d !== void 0 ? _d : [];
if (args.location.length === 0) {
this.session.abort({ message: 'ERROR: GVC has no locations' });
}
}
// Determine container overrides
const filePaths = (0, io_1.readFileInputsOption)((0, objects_1.toArray)(args.file));
for (const filePath of filePaths) {
containerOverrides = [...containerOverrides, ...(0, io_1.loadAllObject)(await (0, io_1.readTextFile)(filePath))];
}
// Start executing runCronWorkload command against each location specified
for (const location of args.location) {
try {
const command = {
type: 'runCronWorkload',
spec: {
location,
containerOverrides,
},
};
if (args.activeDeadlineSeconds) {
command.spec.activeDeadlineSeconds = args.activeDeadlineSeconds;
}
const response = await this.client.axios.post(`${workloadSelfLink}/-command`, command);
if (response.status === 201) {
const workloadCronResource = {
workloadName: args.ref,
location,
id: (0, links_1.getLastPartOfLink)(response.headers.location),
status: 'active',
startTime: new Date(response.headers.date),
};
if (args.activeDeadlineSeconds) {
workloadCronResource.activeDeadlineSeconds = args.activeDeadlineSeconds;
}
accumulator.items.push(workloadCronResource);
}
else {
throw new Error(`Unexpected status code ${response.status} while starting a cron job in location ${location}`);
}
}
catch (e) {
failures.push(e);
}
}
await this.session.outFormat(accumulator);
if (failures.length > 0) {
this.session.abort({ error: failures });
}
}
}
exports.StartCron = StartCron;
class StopCron extends StopReplica {
constructor() {
super(...arguments);
this.command = 'stop <ref>';
this.describe = 'Stop a cron job';
}
}
exports.StopCron = StopCron;
class GetCron extends command_1.Command {
constructor(resolve) {
super();
this.resolve = resolve;
this.command = 'get <ref>';
this.describe = 'Get a list of job executions for the referenced workload';
}
builder(yargs) {
return (0, functions_1.pipe)(
//
generic_1.withSingleRef, generic_1.withMultipleLocationsOption, options_1.withAllOptions)(yargs);
}
async handle(args) {
var _a, _b, _c, _d;
// Initialize
const ignoreKeywords = ['command-', 'run-for-command-'];
const workloadSelfLink = this.resolve.resourceLink(args.ref, this.session.context);
const accumulator = {
kind: 'list',
itemKind: 'workloadCron',
items: [],
links: [],
};
// Fetch data
const commands = await this.client.get(`${workloadSelfLink}/-command`);
const deployments = await this.client.get(`${workloadSelfLink}/deployment`);
// Fetch all items
await (0, resultFetcher_1.fetchPages)(this.client, this.session, commands);
// Fetch all items
await (0, resultFetcher_1.fetchPages)(this.client, this.session, deployments);
// Determine specified locations
if (args.location) {
args.location = (0, objects_1.toArray)(args.location);
}
for (const _deployment of deployments.items) {
const deployment = _deployment;
// Ignore deployments that are not in the specified locations
if (args.location && !args.location.includes(deployment.name)) {
continue;
}
for (const jobExecution of (_b = (_a = deployment.status) === null || _a === void 0 ? void 0 : _a.jobExecutions) !== null && _b !== void 0 ? _b : []) {
let skip = false;
for (const ignoreKeyword of ignoreKeywords) {
if (jobExecution.name.startsWith(ignoreKeyword)) {
skip = true;
break;
}
}
// Skip job executions that has a name that starts with the ignore keywords
if (skip) {
continue;
}
const workloadCronResource = {
location: deployment.name,
workloadName: args.ref,
id: jobExecution.name,
status: jobExecution.status,
startTime: new Date(jobExecution.startTime),
completionTime: jobExecution.completionTime ? new Date(jobExecution.completionTime) : undefined,
};
accumulator.items.push(workloadCronResource);
}
}
for (const _command of commands.items) {
const command = _command;
if (command.spec && args.location && !args.location.includes(command.spec.location)) {
continue;
}
let status = command.lifecycleStage;
switch (status) {
case 'running':
status = 'active';
break;
case 'completed':
status = 'successful';
break;
}
const workloadCronResource = {
location: (_d = (_c = command.spec) === null || _c === void 0 ? void 0 : _c.location) !== null && _d !== void 0 ? _d : '',
workloadName: args.ref,
id: command.id,
status: status,
startTime: new Date(command.created),
completionTime: command.lifecycleStage === 'completed' ? new Date(command.lastModified) : undefined,
};
accumulator.items.push(workloadCronResource);
}
// Sort by location and start time descending
if (accumulator.items.length > 0) {
accumulator.items = accumulator.items.sort((a, b) => {
if (a.location !== b.location) {
return a.location.localeCompare(b.location);
}
if (a.startTime < b.startTime) {
return 1;
}
if (a.startTime > b.startTime) {
return -1;
}
return 0;
});
}
accumulator.items = (0, resultFetcher_1.applyMaxAndWarn)(this.session, accumulator.items);
await this.session.outFormat(accumulator);
}
}
exports.GetCron = GetCron;
// ANCHOR - Cron Run Options
function withCronRunOptions(yargs) {
return yargs.options({
name: {
describe: 'Target (or create) a cron workload with this exact name; without it a generated cpln-run-* runner is used',
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',
},
background: {
describe: 'Run in background without waiting for completion (fire and forget)',
boolean: true,
default: false,
alias: 'b',
},
timeout: {
describe: 'Maximum time to wait for job completion in seconds',
number: true,
default: 600,
alias: 't',
},
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,
},
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',
requiresArg: true,
},
identity: {
describe: 'Attach the named identity to the runner workload',
requiresArg: true,
},
'-': {
requiresArg: true,
description: 'Command to execute. Must be at the end of the command (e.g., -- echo hello world)',
},
});
}
// ANCHOR - Cron Run Command
class CronRun extends command_1.Command {
constructor(resolve) {
super();
this.resolve = resolve;
this.command = 'run';
this.describe = 'Run a command with a cron workload instance. Use -- at the end to specify the command (e.g., cpln workload cron run --gvc my-gvc -- echo hello)';
}
builder(yargs) {
return (0, functions_1.pipe)(
//
withCronRunOptions, options_1.withAllOptions)(yargs);
}
async handle(args) {
var _a, _b;
// Validate org and gvc
this.requireFullContext();
// Set instance properties
this.orgSelfLink = (0, resolver_1.resolveToLink)('org', undefined, this.session.context);
this.gvcSelfLink = (0, resolver_1.resolveToLink)('gvc', this.session.context.gvc, this.session.context);
this.containerName = args.container || CronRun.RUNNER_CONTAINER_NAME;
logger_1.logger.info(`CronRun - Org Link: ${this.orgSelfLink}, GVC Link: ${this.gvcSelfLink}`);
// Read command from args
const cmd = args['--'];
// Require command
if (!cmd || !Array.isArray(cmd) || cmd.length < 1) {
logger_1.logger.error('CronRun - Command was not provided');
this.session.abort({ message: 'ERROR: A command and args are required' });
}
// Interactive and background modes are mutually exclusive
if (args.interactive && args.background) {
this.session.abort({ message: 'ERROR: --interactive and --background options are mutually exclusive' });
}
// Reject a malformed --name up front (including an empty one) so it fails clearly rather than as an opaque API error
if (args.name !== undefined && (!(0, format_1.isNameValid)(args.name) || args.name.length > CronRun.MAX_WORKLOAD_NAME_LENGTH)) {
this.session.abort({
message: `ERROR: Invalid runner workload name '${args.name}'. A name must be lowercase, start with a letter, ` +
`end with a letter or digit, contain only letters, digits, and hyphens, and be 2 to ${CronRun.MAX_WORKLOAD_NAME_LENGTH} characters.`,
});
}
try {
// Determine location
if (args.location) {
this.locationName = args.location;
}
else {
// Fetch GVC to get the first location
const gvc = await this.client.get(this.gvcSelfLink);
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) {
this.locationName = (0, links_1.getLastPartOfLink)(gvc.spec.staticPlacement.locationLinks[0]);
}
else {
return this.session.abort({ message: 'ERROR: GVC has no locations' });
}
}
logger_1.logger.info('CronRun - Location: ' + this.locationName);
// Extract and validate identity if provided
if (args.identity) {
this.identityName = this.extractIdentityName(args.identity);
logger_1.logger.info('CronRun - Identity: ' + this.identityName);
await this.validateIdentityExists(this.identityName);
}
// Ensure runner workload exists and set workload properties
const runnerWorkload = await this.ensureRunnerWorkload(args, this.identityName);
this.workloadName = runnerWorkload.name;
this.workloadSelfLink = (0, resolver_1.resolveToLink)('workload', this.workloadName, this.session.context);
// Build container overrides from args
const containerOverrides = this.buildContainerOverrides(args, cmd);
// Build tags for the command
const commandTags = args.tag ? (0, tags_1.expressionToTags)((0, objects_1.toArray)(args.tag)) : undefined;
// Start job execution
this.commandId = await this.startJobExecution(this.locationName, containerOverrides, commandTags);
logger_1.logger.info(`CronRun - Job started with command ID: ${this.commandId}`);
if (args.interactive) {
// Interactive mode: wait for job to be running, then connect via WebSocket
await this.handleInteractiveMode(args.shell);
}
else if (args.background) {
// Background mode: output job ID and exit immediately (fire and forget)
this.outputJobResult('started');
}
else {
// Default: wait for job completion
await this.handleWaitMode(args.timeout);
}
}
catch (e) {
logger_1.logger.info(`CronRun - Exception. Message: ${e.message}`);
this.session.abort({ error: e });
}
}
// ANCHOR - Private Methods
/**
* Extracts the identity name from a plain name, shorthand link, or full self-link.
*
* @param {string} input - The identity reference (e.g., 'my-identity', '//identity/my-identity', or '/org/.../identity/my-identity').
* @returns {string} The extracted identity name.
*/
extractIdentityName(input) {
// If the input contains a slash, extract the last segment
if (input.includes('/')) {
return (0, links_1.getLastPartOfLink)(input);
}
// Otherwise, return the input as-is
return input;
}
/**
* Validates that the specified identity exists in the current GVC.
*
* @param {string} identityName - The name of the identity to validate.
* @returns {Promise<void>} Resolves if the identity exists, aborts if not found.
*/
async validateIdentityExists(identityName) {
const identityLink = (0, resolver_1.resolveToLink)('identity', identityName, this.session.context);
try {
await this.client.get(identityLink);
}
catch (e) {
this.session.abort({ message: `ERROR: Identity '${identityName}' not found in GVC '${this.session.context.gvc}'` });
}
}
/**
* Output the job result in a consistent format.
*
* @param {string} status - The status of the job.
* @param {string | undefined} message - (Optional) An additional message to include.
*/
outputJobResult(status, message) {
const result = {
workload: this.workloadName,
workloadLink: `//gvc/${this.session.context.gvc}/workload/${this.workloadName}`,
commandId: this.commandId,
location: this.locationName,
status,
};
// Include identity in output if present
if (this.identityName) {
result.identity = this.identityName;
}
if (message) {
result.message = message;
}
this.session.outFormat(result);
}
/**
* Find or create the runner workload for this run.
*
* Without --name the default runner is reused via the cpln/run tag and created with a generated name when
* absent (the original behavior). With --name the runner is addressed by that exact name, so it can be
* pre-referenced elsewhere — e.g. a called service's internal firewall allowlist.
*
* @param {Arguments<CronRunOptions>} args - The command arguments.
* @param {string | undefined} identityName - (Optional) The identity name to scope the runner workload to.
* @returns {Promise<Workload>} The runner workload.
*/
async ensureRunnerWorkload(args, identityName) {
// An explicit --name addresses a single, stable workload by that exact name
if (args.name) {
return await this.ensureNamedRunner(args.name, args, identityName);
}
// Default: reuse any tagged runner, otherwise create a fresh one with a generated name
const existing = await this.findRunnerWorkload(identityName);
if (existing) {
logger_1.logger.info('CronRun - Reusing existing runner workload ' + existing.name);
return existing;
}
return await this.createRunnerWorkload((0, names_1.nextName)(CronRun.RUNNER_WORKLOAD_NAME_PREFIX), args, identityName);
}
/**
* Find the runner workload by querying for the cpln/run tag.
*
* @param {string | undefined} identityName - (Optional) The identity name to match against the runner workload.
* @returns {Promise<Workload | null>} The runner workload, or null if not found.
*/
async findRunnerWorkload(identityName) {
const workloadQueryLink = `${this.orgSelfLink}/workload/-query`;
// Build query terms
const terms = [
{
op: '=',
tag: CronRun.RUNNER_TAG_KEY,
value: 'true',
},
{
op: '=',
rel: 'gvc',
value: this.session.context.gvc,
},
];
// Add identity-specific query term
if (identityName) {
terms.push({
op: '=',
tag: CronRun.RUNNER_IDENTITY_TAG_KEY,
value: identityName,
});
}
else {
// Ensure default runs don't match identity-tagged workloads
terms.push({
op: '!exists',
tag: CronRun.RUNNER_IDENTITY_TAG_KEY,
});
}
const queryResult = await this.client.post(workloadQueryLink, {
kind: 'workload',
spec: {
match: 'all',
terms,
},
});
// Return the first matching workload
if (queryResult.items && queryResult.items.length > 0) {
return queryResult.items[0];
}
return null;
}
/**
* Find or create the runner workload addressed by an explicit name.
*
* Reuses the workload of that exact name when it is a runner this run may use, and creates it when absent,
* tolerating a concurrent run that wins the create race.
*
* @param {string} name - The explicit runner workload name.
* @param {Arguments<CronRunOptions>} args - The command arguments.
* @param {string | undefined} identityName - (Optional) The identity name to scope the runner workload to.
* @returns {Promise<Workload>} The runner workload.
*/
async ensureNamedRunner(name, args, identityName) {
var _a;
const workloadSelfLink = (0, resolver_1.resolveToLink)('workload', name, this.session.context);
// Look for an existing workload under that exact name
let existing = null;
try {
existing = await this.client.get(workloadSelfLink);
}
catch (e) {
// Anything other than "not found" is a real error
if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.status) !== 404) {
throw e;
}
}
// Reuse it when present, after confirming it is a runner we may reuse
if (existing && existing.name === name) {
logger_1.logger.info('CronRun - Reusing existing runner workload ' + name);
this.assertReusableRunner(existing, name, identityName);
return existing;
}
// Otherwise create it under the requested name
return await this.createRunnerWorkload(name, args, identityName);
}
/**
* Assert that an existing workload can serve as the runner for this run.
*
* Refuses a workload that is not a cron workload or that lacks the targeted container, and — only when
* --identity is given — one whose identity differs from it (the identity link is fixed and cannot change per run).
*
* @param {Workload} workload - The existing workload found under the runner name.
* @param {string} workloadName - The runner workload name, used in error messages.
* @param {string | undefined} identityName - (Optional) The identity requested for this run.
*/
assertReusableRunner(workload, workloadName, identityName) {
var _a, _b, _c, _d, _e;
// cron run executes a cron job, so the target must be a cron workload
if (((_a = workload.spec) === null || _a === void 0 ? void 0 : _a.type) !== 'cron') {
this.session.abort({
message: `ERROR: Workload '${workloadName}' is a '${(_b = workload.spec) === null || _b === void 0 ? void 0 : _b.type}' workload; cron run can only target a cron workload.`,
});
}
// The command overrides a container by name, so that container must exist on the target
const hasTargetContainer = !!((_d = (_c = workload.spec) === null || _c === void 0 ? void 0 : _c.containers) === null || _d === void 0 ? void 0 : _d.some((container) => container.name === this.containerName));
if (!hasTargetContainer) {
this.session.abort({
message: `ERROR: Container '${this.containerName}' does not exist in workload '${workloadName}'. Use --container to target an existing container.`,
});
}
// Only enforce identity when this run explicitly requested one; otherwise run under whatever the workload has.
// Read the identity from the workload's own link, normalizing the API's canonical form to a plain name.
if (identityName !== undefined) {
const identityLink = (_e = workload.spec) === null || _e === void 0 ? void 0 : _e.identityLink;
const runnerIdentity = identityLink ? (0, links_1.getLastPartOfLink)(identityLink) : undefined;
if (runnerIdentity !== identityName) {
this.session.abort({
message: `ERROR: Workload '${workloadName}' is bound to identity '${runnerIdentity !== null && runnerIdentity !== void 0 ? runnerIdentity : '(none)'}', ` +
`but this run requested '${identityName}'. Its identity is fixed; re-run requesting the same identity, or use --name to target a different workload.`,
});
}
}
}
/**
* Create the runner workload as a suspended cron workload.
*
* @param {string} workloadName - The runner workload name (generated for a default runner, or the explicit --name).
* @param {Arguments<CronRunOptions>} args - The command arguments.
* @param {string | undefined} identityName - (Optional) The identity name to attach to the runner workload.
* @returns {Promise<Workload>} The created (or concurrently-created) runner workload.
*/
async createRunnerWorkload(workloadName, args, identityName) {
var _a;
logger_1.logger.info('CronRun - Creating runner workload ' + workloadName);
// Build workload tags
const tags = {
[CronRun.RUNNER_TAG_KEY]: 'true',
};
// Tag with identity name if provided
if (identityName) {
tags[CronRun.RUNNER_IDENTITY_TAG_KEY] = identityName;
}
const workload = {
name: workloadName,
tags,
spec: {
type: 'cron',
containers: [
{
name: CronRun.RUNNER_CONTAINER_NAME,
image: args.image || CronRun.RUNNER_DEFAULT_IMAGE,
cpu: args.cpu || '0.25',
memory: args.memory || '256Mi',
},
],
job: {
schedule: '* * * * *', // Required but unused (workload is suspended)
concurrencyPolicy: 'Allow',
historyLimit: 5,
restartPolicy: 'Never',
},
defaultOptions: {
capacityAI: false,
suspend: true, // On-demand only
},
firewallConfig: {
external: {
inboundAllowCIDR: ['0.0.0.0/0'],
outboundAllowCIDR: ['0.0.0.0/0'],
},
internal: {
inboundAllowType: 'same-org',
},
},
},
};
// Set identity link if provided
if (identityName) {
workload.spec.identityLink = '//identity/' + identityName;
}
logger_1.logger.info('CronRun - Runner Workload Body: ' + JSON.stringify(workload, null, 2));
const workloadSelfLink = (0, resolver_1.resolveToLink)('workload', workloadName, this.session.context);
try {
await this.client.create(`${this.gvcSelfLink}/workload`, workload);
}
catch (e) {
// A concurrent run created the same runner first; adopt theirs instead of failing
if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.status) === 409) {
logger_1.logger.info('CronRun - Runner workload created concurrently, reusing ' + workloadName);
const concurrent = await this.client.get(workloadSelfLink);
this.assertReusableRunner(concurrent, workloadName, identityName);
return concurrent;
}
throw e;
}
// Fetch and return the created workload
return await this.client.get(workloadSelfLink);
}
/**
* Build container overrides from command args.
*
* @param {Arguments<CronRunOptions>} args - The command arguments.
* @param {string[]} cmd - The command to execute.
* @returns {ContainerOverride[]} The container overrides array.
*/
buildContainerOverrides(args, cmd) {
const containerName = args.container || CronRun.RUNNER_CONTAINER_NAME;
const override = {
name: containerName,
};
// Image override
if (args.image) {
override.image = args.image;
}
// CPU override
if (args.cpu) {
override.cpu = args.cpu;
}
// Memory override
if (args.memory) {
override.memory = args.memory;
}
// Environment variables
if (args.env) {
override.env = (0, objects_1.toEnv)((0, objects_1.toArray)(args.env));
}
// Command override
override.command = cmd[0];
if (cmd.length > 1) {
override.args = cmd.slice(1);
}
return [override];
}
/**
* Start a job execution via the runCronWorkload command.
*
* @param {string} location - The location to run the job in.
* @param {ContainerOverride[]} containerOverrides - The container overrides.
* @param {Tags | undefined} tags - (Optional) Tags for the command.
* @returns {Promise<string>} The command ID.
*/
async startJobExecution(location, containerOverrides, tags) {
var _a, _b;
const command = {
type: 'runCronWorkload',
tags,
spec: {
location,
containerOverrides,
},
};
// Trigger a job execution
const response = await this.client.axios.post(`${this.workloadSelfLink}/-command`, command);
if (response.status !== 201) {
const errorDetail = ((_a = response.data) === null || _a === void 0 ? void 0 : _a.message) || ((_b = response.data) === null || _b === void 0 ? void 0 : _b.error) || response.statusText || 'Unknown error';
this.session.abort({ message: `Failed to start job: ${response.status} - ${errorDetail}` });
}
return (0, links_1.getLastPartOfLink)(response.headers.location);
}
/**
* Poll until job reaches a terminal state and output status.
*
* @param {number} timeoutSeconds - Maximum time to wait in seconds.
*/
async handleWaitMode(timeoutSeconds) {
logger_1.logger.info(`CronRun - Waiting for job completion (timeout: ${timeoutSeconds}s)`);
let finalStatus = 'unknown';
let exitCode = 0;
let message;
await (0, functions_1.retryFn)(async (repeatIndex) => {
const command = await this.client.get(`${this.workloadSelfLink}/-command/${this.commandId}`);
const stage = command.lifecycleStage;
if (stage) {
logger_1.logger.info(`CronRun - Poll ${repeatIndex}: lifecycleStage=${stage}`);
finalStatus = stage;
if (stage === 'completed') {
return;
}
if (stage === 'failed' || stage === 'cancelled') {
exitCode = 1;
return;
}
}
// Still pending or running
throw new Error('Job not yet complete');
}, {
// --timeout is a deadline in real seconds, not a number of attempts
timeoutMs: timeoutSeconds * 1000,
repeatTimes: timeoutSeconds,
intervalSec: 1,
onFailure: async () => {
// A job that was never placed on a node has no container to report on, only this
const reason = await this.getJobExecutionMessage();
this.session.abort({
message: `Job did not complete within ${timeoutSeconds} seconds. Use --timeout to adjust the wait time.` +
(reason ? `\n\nThe platform reports: ${reason}` : ''),
});
},
});
if (finalStatus === 'failed') {
message = await this.getJobExecutionContainerMessage();
}
this.outputJobResult(finalStatus, message);
process.exitCode = exitCode;
}
/**
* Handle interactive mode: wait for job to be running, get replica, connect via WebSocket.
*
* @param {string | undefined} shell - (Optional) The shell to use.
*/
async handleInteractiveMode(shell) {
let lastMessage = '';
let hadTransportError = false;
let terminateTimeout;
let pingInterval;
logger_1.logger.info('CronRun - Interactive mode: waiting for job to be running');
// Wait for job to be running and get replica name from remote endpoint
const replica = await this.waitForJobRunningAndGetReplica();
logger_1.logger.info(`CronRun - Job running, replica: ${replica}`);
// Get terminal config
const terminalServer = new terminal_server_1.TerminalServer(this.session, this.client);
const terminalConfig = await terminalServer.createConfig(this.workloadName, this.locationName, replica, this.containerName);
// Configure for interactive session
terminalConfig.request.shell = shell || "sh -c 'bash -i || zsh -i || sh -i'";
terminalConfig.request.stdin = true;
terminalConfig.request.tty = true;
terminalConfig.request.quiet = true;
terminalConfig.request.width = process.stdout.columns;
terminalConfig.request.height = process.stdout.rows;
// Enable raw mode
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
// Initialize WebSocket connection
client_1.wire.debug('>>>>>>> Creating WS');
const client = new WebSocket(terminalConfig.remoteSocket, { origin: 'http://localhost' });
client_1.wire.debug('>>>>>>> WS Created');
// Handle terminal resize
const onProcessResize = (0, helpers_1.terminalResizeDebounce)(client);
// Setup ping/pong for interactive session
pingInterval = setInterval(() => {
client_1.wire.debug('>>>>>>> WS Ping Sending');
client.ping();
client_1.wire.debug('>>>>>>> WS Ping Sent');
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');
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));
// Subscribe to resize event
process.stdout.on('resize', onProcessResize);
// Subscribe to data event for user input
process.stdin.on('data', (chunk) => {
const message = {
type: 'data',
buffer: [...Buffer.from(chunk)],
width: process.stdout.columns,
height: process.stdout.rows,
};
client.send(JSON.stringify(message));
});
});
client.on('message', (event) => {
client_1.wire.debug('<<<<<<< WS Message Received');
const msg = event.toString();
if (msg.trim()) {
lastMessage = msg;
}
process.stdout.write(msg);
});
client.on('error', (event) => {
// A transport-level failure must not be reported as a successful session
hadTransportError = true;
if (pingInterval) {
clearInterval(pingInterval);
}
process.stderr.write('error: ' + event.message);
});
client.on('close', () => {
client_1.wire.debug('<<<<<<< WS Close Event info');
process.stdout.off('resize', onProcessResize);
if (pingInterval) {
clearInterval(pingInterval);
}
// Exit with the session's code, treating a transport error as a failure
process.exit(terminal_server_1.TerminalServer.exitCodeForClose(lastMessage, hadTransportError));
});
}
/**
* Wait for the job replica to be available and return its name.
* Fetches replicas from the remote endpoint and finds the one matching the command ID.
*
* @returns {Promise<string>} The replica name.
*/
async waitForJobRunningAndGetReplica() {
let replica = '';
await (0, functions_1.retryFn)(async (repeatIndex) => {
var _a;
// Check command status - fail fast if job failed or was cancelled
const command = await this.client.get(`${this.workloadSelfLink}/-command/${this.commandId}`);
const stage = command.lifecycleStage;
logger_1.logger.info(`CronRun - Poll ${repeatIndex}: lifecycleStage=${stage}`);
if (stage === 'failed' || stage === 'cancelled') {
this.session.abort({ message: `Job ${stage}` });
}
// Get deployment to get the remote endpoint
const deployment = await this.client.get(`${this.workloadSelfLink}/deployment/${this.locationName}`);
if (!((_a = deployment.status) === null || _a === void 0 ? void 0 : _a.remote)) {
throw new Error('Deployment remote endpoint not available');
}
// Fetch replicas from the remote endpoint
const remoteEndpoint = deployment.status.remote;
const replicaListLink = `${remoteEndpoint}/replicas${this.workloadSelfLink}`;
logger_1.logger.info(`CronRun - Poll ${repeatIndex}: Fetching replicas from ${replicaListLink}`);
const replicaList = await this.client.get(replicaListLink);
// Find replica whose name includes the command ID
// The items are returned as strings (replica names)
const matchingReplica = replicaList.items.find((item) => item.includes(this.commandId));
if (matchingReplica) {
replica = matchingReplica;
return;
}
logger_1.logger.info(`CronRun - Poll ${repeatIndex}: Looking for replica matching command ${this.commandId}`);
throw new Error('Replica not found');
}, {
repeatTimes: 60,
intervalSec: 1,
onFailure: () => {
this.session.abort({ message: 'Could not find running replica for job' });
},
});
return replica;
}
/**
* Extract the message from the deployment jobExecutions for a failed job.
*
* @returns {Promise<string | undefined>} The container message, or undefined if not found.
*/
/**
* Reads the platform's account of why the job execution is in the state it is in.
*
* @returns {Promise<string | undefined>} The job execution's status message, or undefined when there is none.
*/
async getJobExecutionMessage() {
var _a, _b;
try {
const deployment = await this.client.get(`${this.workloadSelfLink}/deployment/${this.locationName}`);
const jobExecution = (_b = (_a = deployment.status) === null || _a === void 0 ? void 0 : _a.jobExecutions) === null || _b === void 0 ? void 0 : _b.find((je) => je.name === `command-${this.commandId}`);
return (jobExecution === null || jobExecution === void 0 ? void 0 : jobExecution.message) || undefined;
}
catch (e) {
logger_1.logger.info(`CronRun - Failed to fetch job execution: ${e.message}`);
return undefined;
}
}
async getJobExecutionContainerMessage() {
var _a, _b, _c;
try {
const deployment = await this.client.get(`${this.workloadSelfLink}/deployment/${this.locationName}`);
const jobExecution = (_b = (_a = deployment.status) === null || _a === void 0 ? void 0 : _a.jobExecutions) === null || _b === void 0 ? void 0 : _b.find((je) => je.name === `command-${this.commandId}`);
if (!jobExecution) {
logger_1.logger.info(`CronRun - No jobExecution found for command-${this.commandId}`);
return undefined;
}
const containerStatus = (_c = jobExecution.containers) === null || _c === void 0 ? void 0 : _c[this.containerName];
const message = containerStatus === null || containerStatus === void 0 ? void 0 : containerStatus.message;
if (message) {
logger_1.logger.info(`CronRun - Found container message: ${message}`);
return message;
}
return undefined;
}
catch (e) {
logger_1.logger.info(`CronRun - Failed to fetch job execution: ${e.message}`);
return undefined;
}
}
}
exports.CronRun = CronRun;
// Constants
CronRun.RUNNER_WORKLOAD_NAME_PREFIX = 'cpln-run-';
CronRun.RUNNER_TAG_KEY = 'cpln/run';
CronRun.RUNNER_IDENTITY_TAG_KEY = 'cpln/run-identity';
CronRun.RUNNER_CONTAINER_NAME = 'main';
CronRun.RUNNER_DEFAULT_IMAGE = 'ubuntu:latest';
CronRun.MAX_WORKLOAD_NAME_LENGTH = 63;
//# sourceMappingURL=workload.js.map