UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

2,395 lines 100 kB
"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