UNPKG

rally-tools

Version:
1,997 lines (1,713 loc) 149 kB
#!/usr/bin/env node /*---------------------------------------------- * Generated by rollup. Written by John Schmidt. * Rally Tools CLI v3.4.1 *--------------------------------------------*/ const importLazy = require("import-lazy")(require); 'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var os = require('os'); var fs = require('fs'); var fs__default = _interopDefault(fs); var child_process = require('child_process'); var perf_hooks = require('perf_hooks'); var chalk$1 = _interopDefault(require('chalk')); var rp = _interopDefault(require('request-promise')); var path = require('path'); var path__default = _interopDefault(path); var moment = _interopDefault(require('moment')); var argparse = _interopDefault(require('minimist')); function _asyncIterator(iterable) { var method; if (typeof Symbol === "function") { if (Symbol.asyncIterator) { method = iterable[Symbol.asyncIterator]; if (method != null) return method.call(iterable); } if (Symbol.iterator) { method = iterable[Symbol.iterator]; if (method != null) return method.call(iterable); } } throw new TypeError("Object is not async iterable"); } function _AwaitValue(value) { this.wrapped = value; } function _AsyncGenerator(gen) { var front, back; function send(key, arg) { return new Promise(function (resolve, reject) { var request = { key: key, arg: arg, resolve: resolve, reject: reject, next: null }; if (back) { back = back.next = request; } else { front = back = request; resume(key, arg); } }); } function resume(key, arg) { try { var result = gen[key](arg); var value = result.value; var wrappedAwait = value instanceof _AwaitValue; Promise.resolve(wrappedAwait ? value.wrapped : value).then(function (arg) { if (wrappedAwait) { resume("next", arg); return; } settle(result.done ? "return" : "normal", arg); }, function (err) { resume("throw", err); }); } catch (err) { settle("throw", err); } } function settle(type, value) { switch (type) { case "return": front.resolve({ value: value, done: true }); break; case "throw": front.reject(value); break; default: front.resolve({ value: value, done: false }); break; } front = front.next; if (front) { resume(front.key, front.arg); } else { back = null; } } this._invoke = send; if (typeof gen.return !== "function") { this.return = undefined; } } if (typeof Symbol === "function" && Symbol.asyncIterator) { _AsyncGenerator.prototype[Symbol.asyncIterator] = function () { return this; }; } _AsyncGenerator.prototype.next = function (arg) { return this._invoke("next", arg); }; _AsyncGenerator.prototype.throw = function (arg) { return this._invoke("throw", arg); }; _AsyncGenerator.prototype.return = function (arg) { return this._invoke("return", arg); }; function _wrapAsyncGenerator(fn) { return function () { return new _AsyncGenerator(fn.apply(this, arguments)); }; } function _awaitAsyncGenerator(value) { return new _AwaitValue(value); } function _asyncGeneratorDelegate(inner, awaitWrap) { var iter = {}, waiting = false; function pump(key, value) { waiting = true; value = new Promise(function (resolve) { resolve(inner[key](value)); }); return { done: false, value: awaitWrap(value) }; } if (typeof Symbol === "function" && Symbol.iterator) { iter[Symbol.iterator] = function () { return this; }; } iter.next = function (value) { if (waiting) { waiting = false; return value; } return pump("next", value); }; if (typeof inner.throw === "function") { iter.throw = function (value) { if (waiting) { waiting = false; throw value; } return pump("throw", value); }; } if (typeof inner.return === "function") { iter.return = function (value) { return pump("return", value); }; } return iter; } function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object['ke' + 'ys'](descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object['define' + 'Property'](target, property, desc); desc = null; } return desc; } let configFile = null; if (os.homedir) { configFile = os.homedir() + "/.rallyconfig"; } let configObject; function loadConfig(file) { if (file) configFile = file; if (!configFile) return; configObject = { hasConfig: true }; try { let json = fs.readFileSync(configFile); configObject = JSON.parse(json); configObject.hasConfig = true; } catch (e) { if (e.code == "ENOENT") { configObject.hasConfig = false; //ok, they should probably make a config } else { throw e; } } } function loadConfigFromArgs(args) { let tempConfig = { hasConfig: true, ...args.config }; configObject = tempConfig; } function setConfig(obj) { configObject = obj; } //these are the help entries for each command let helpEntries = {}; let helpEntry = name => helpEntries[name] ? helpEntries[name] : helpEntries[name] = { name }; //short description function helpText(text) { return function (func, name) { helpEntry(name).text = text; return func; }; } //flag type argument like -f or --file function arg(long, short, desc) { return function (func, name) { let args = helpEntry(name).args = helpEntry(name).args || []; args.unshift({ long, short, desc }); return func; }; } //normal argument function param(param, desc) { return function (func, name) { let params = helpEntry(name).params = helpEntry(name).params || []; params.unshift({ param, desc }); return func; }; } //usage string function usage(usage) { return function (func, name) { usage = usage.replace(/[\[<](\w+)[\]>]/g, chalk`[{blue $1}]`); helpEntry(name).usage = usage; return func; }; } //function retuns obj.a.b.c function deepAccess(obj, path$$1) { let o = obj; for (let key of path$$1) { if (!o) return []; o = o[key]; } return o; } //This takes a class as the first argument, then adds a getter/setter pair that //corresponds to an object in this.data function defineAssoc(classname, shortname, path$$1) { path$$1 = path$$1.split("."); let lastKey = path$$1.pop(); Object.defineProperty(classname.prototype, shortname, { get() { return deepAccess(this, path$$1)[lastKey]; }, set(val) { deepAccess(this, path$$1)[lastKey] = val; } }); } function spawn(options, ...args) { if (typeof options !== "object") { args.unshift(options); options = {}; } //todo options return new Promise((resolve, reject) => { let start = perf_hooks.performance.now(); let stdout = ""; let stderr = ""; let cp = child_process.spawn(...args); let write = global.write; if (options.noecho) { write = () => {}; } if (cp.stdout) cp.stdout.on("data", chunk => { stdout += chunk; write(chunk); }); if (cp.stderr) cp.stderr.on("data", chunk => { stderr += chunk; write(chunk); }); if (options.stdin) { options.stdin(cp.stdin); } cp.on("error", reject); cp.on("close", code => { let end = perf_hooks.performance.now(); let time = end - start; let timestr = time > 1000 ? (time / 100 | 0) / 10 + "s" : (time | 0) + "ms"; resolve({ stdout, stderr, exitCode: code, time, timestr }); }); }); } global.chalk = chalk$1; global.log = (...text) => console.log(...text); global.write = (...text) => process.stdout.write(...text); global.elog = (...text) => console.error(...text); global.ewrite = (...text) => process.stderr.write(...text); global.errorLog = (...text) => log(...text.map(chalk$1.red)); class lib { //This function takes 2 required arguemnts: // env: the enviornment you wish to use // and either: // 'path', the short path to the resource. ex '/presets/' // 'path_full', the full path to the resource like 'https://discovery-dev.sdvi.com/presets' // // If the method is anything but GET, either payload or body should be set. // payload should be a javascript object to be turned into json as the request body // body should be a string that is passed as the body. for example: the python code of a preset. // // qs are the querystring parameters, in a key: value object. // {filter: "name=test name"} becomes something like 'filter=name=test+name' // // headers are the headers of the request. "Content-Type" is already set if // payload is given as a parameter // // fullResponse should be true if you want to receive the request object, // not just the returned data. static async makeAPIRequest({ env, path: path$$1, path_full, fullPath, payload, body, method = "GET", qs, headers = {}, fullResponse = false, timeout = configObject.timeout || 20000 }) { var _configObject$api; //backwards compatability from ruby script if (fullPath) path_full = fullPath; //Keys are defined in enviornment variables let config = configObject === null || configObject === void 0 ? void 0 : (_configObject$api = configObject.api) === null || _configObject$api === void 0 ? void 0 : _configObject$api[env]; if (!config) { throw new UnconfiguredEnvError(env); } if (method !== "GET" && !configObject.dangerModify) { if (env === "UAT" && configObject.restrictUAT || env === "PROD") { throw new ProtectedEnvError(env); } } let rally_api_key = config.key; let rally_api = config.url; if (path$$1 && path$$1.startsWith("/v1.0/")) { rally_api = rally_api.replace("/api/v2", "/api"); } path$$1 = path_full || rally_api + path$$1; if (payload) { body = JSON.stringify(payload, null, 4); } if (payload) { headers["Content-Type"] = "application/vnd.api+json"; } let fullHeaders = { //SDVI ignores this header sometimes. Accept: "application/vnd.api+json", "X-SDVI-Client-Application": "Discovery-rtlib-" + (configObject.appName || "commandline"), ...headers }; if (configObject.vvverbose) { log(`${method} @ ${path$$1}`); log(JSON.stringify(fullHeaders, null, 4)); if (body) { log(body); } else { log("(No body"); } } let requestOptions = { method, body, qs, uri: path$$1, timeout, auth: { bearer: rally_api_key }, headers: fullHeaders, simple: false, resolveWithFullResponse: true }; let response; try { response = await rp(requestOptions); if (configObject.vverbose || configObject.vvverbose) { log(chalk$1`${method} @ ${response.request.uri.href}`); } } catch (e) { if ((e === null || e === void 0 ? void 0 : e.cause.code) === "ESOCKETTIMEDOUT") { throw new APIError(response || {}, requestOptions, body); } else { throw e; } } //Throw an error for any 5xx or 4xx if (!fullResponse && ![200, 201, 202, 203, 204].includes(response.statusCode)) { throw new APIError(response, requestOptions, body); } let contentType = response.headers["content-type"]; let isJSONResponse = contentType === "application/vnd.api+json" || contentType === "application/json"; if (configObject.vvverbose) { log(response.body); } if (fullResponse) { return response; } else if (isJSONResponse) { var _response, _response$body; if ([200, 201, 202, 203, 204].includes(response.statusCode) && !((_response = response) === null || _response === void 0 ? void 0 : (_response$body = _response.body) === null || _response$body === void 0 ? void 0 : _response$body.trim())) return {}; try { return JSON.parse(response.body); } catch (e) { log(response.body); throw new AbortError("Body is not valid json: "); } } else { return response.body; } } //Index a json endpoint that returns a {links} field. //This function returns the merged data objects as an array // //Additonal options (besides makeAPIRequest options): // - Observe: function to be called for each set of data from the api static async indexPath(env, path$$1) { let opts = typeof env === "string" ? { env, path: path$$1 } : env; opts.maxParallelRequests = 1; let index = new IndexObject(opts); return await index.fullResults(); } static clearProgress(size = 30) { if (!configObject.globalProgress) return; process.stderr.write(`\r${" ".repeat(size + 15)}\r`); } static async drawProgress(i, max, size = process.stdout.columns - 15 || 15) { if (!configObject.globalProgress) return; if (size > 45) size = 45; let pct = Number(i) / Number(max); //clamp between 0 and 1 pct = pct < 0 ? 0 : pct > 1 ? 1 : pct; let numFilled = Math.floor(pct * size); let numEmpty = size - numFilled; this.clearProgress(size); process.stderr.write(`[${"*".repeat(numFilled)}${" ".repeat(numEmpty)}] ${i} / ${max}`); } static async keepalive(funcs) { for (let f of funcs) { await f(); } } //Index a json endpoint that returns a {links} field. // //This function is faster than indexPath because it can guess the pages it //needs to retreive so that it can request all assets at once. // //This function assumes that the content from the inital request is the //first page, so starting on another page may cause issues. Consider //indexPath for that. // //Additional opts, besides default indexPath opts: // - chunksize[10]: How often to break apart concurrent requests static async indexPathFast(env, path$$1) { let opts = typeof env === "string" ? { env, path: path$$1 } : env; let index = new IndexObject(opts); return await index.fullResults(); } static isLocalEnv(env) { return !env || env === "LOCAL" || env === "LOC"; } static envName(env) { if (this.isLocalEnv(env)) return "LOCAL"; return env; } } class AbortError extends Error { constructor(message) { super(message); Error.captureStackTrace(this, this.constructor); this.name = "AbortError"; } } class APIError extends Error { constructor(response, opts, body) { super(chalk$1` {reset Request returned} {yellow ${response === null || response === void 0 ? void 0 : response.statusCode}}{ {green ${JSON.stringify(opts, null, 4)}} {green ${body}} {reset ${response.body}} =============================== {red ${response.body ? "Request timed out" : "Bad response from API"}} =============================== `); this.response = response; this.opts = opts; this.body = body; Error.captureStackTrace(this, this.constructor); this.name = "ApiError"; } } class UnconfiguredEnvError extends AbortError { constructor(env) { super("Unconfigured enviornment: " + env); this.name = "Unconfigured Env Error"; } } class ProtectedEnvError extends AbortError { constructor(env) { super("Protected enviornment: " + env); this.name = "Protected Env Error"; } } class FileTooLargeError extends Error { constructor(file) { super(`File ${file.parentAsset ? file.parentAsset.name : "(unknown)"}/${file.name} size is: ${file.sizeGB}g (> ~.2G)`); this.name = "File too large error"; } } class Collection { constructor(arr) { this.arr = arr; } [Symbol.iterator]() { return this.arr[Symbol.iterator](); } findById(id) { return this.arr.find(x => x.id == id); } findByName(name) { return this.arr.find(x => x.name == name); } findByNameContains(name) { return this.arr.find(x => x.name.includes(name)); } log() { for (let d of this) { if (d) { log(d.chalkPrint(true)); } else { log(chalk$1`{red (None)}`); } } } get length() { return this.arr.length; } } class RallyBase { static handleCaching() { if (!this.cache) this.cache = []; } static isLoaded(env) { if (!this.hasLoadedAll) return; return this.hasLoadedAll[env]; } static async getById(env, id, qs) { this.handleCaching(); for (let item of this.cache) { if (item.id == id && item.remote === env || `${env}-${id}` === item.metastring) return item; } let data = await lib.makeAPIRequest({ env, path: `/${this.endpoint}/${id}`, qs }); if (data.data) { let o = new this({ data: data.data, remote: env, included: data.included }); this.cache.push(o); return o; } } static async getByName(env, name, qs) { this.handleCaching(); for (let item of this.cache) { if (item.name === name && item.remote === env) return item; } let data = await lib.makeAPIRequest({ env, path: `/${this.endpoint}`, qs: { ...qs, filter: `name=${name}` + (qs ? qs.filter : "") } }); //TODO included might not wokr correctly here if (data.data[0]) { let o = new this({ data: data.data[0], remote: env, included: data.included }); this.cache.push(o); return o; } } static async getAllPreCollect(d) { return d; } static async getAll(env) { this.handleCaching(); let datas = await lib.indexPathFast({ env, path: `/${this.endpoint}`, pageSize: "50", qs: { sort: "id" } }); datas = await this.getAllPreCollect(datas); let all = new Collection(datas.map(data => new this({ data, remote: env }))); this.cache = [...this.cache, ...all.arr]; return all; } static async removeCache(env) { this.handleCaching(); this.cache = this.cache.filter(x => x.remote !== env); } //Specific turns name into id based on env //Generic turns ids into names async resolveApply(type, dataObj, direction) { let obj; if (direction == "generic") { obj = await type.getById(this.remote, dataObj.id); if (obj) { dataObj.name = obj.name; } } else if (direction == "specific") { obj = await type.getByName(this.remote, dataObj.name); if (obj) { dataObj.id = obj.id; } } return obj; } //Type is the baseclass you are looking for (should extend RallyBase) //name is the name of the field //isArray is true if it has multiple cardinailty, false if it is single //direction gets passed directly to resolveApply async resolveField(type, name, isArray = false, direction = "generic") { // ignore empty fields let field = this.relationships[name]; if (!(field === null || field === void 0 ? void 0 : field.data)) return; if (isArray) { return await Promise.all(field.data.map(o => this.resolveApply(type, o, direction))); } else { return await this.resolveApply(type, field.data, direction); } } cleanup() { for (let [key, val] of Object.entries(this.relationships)) { //Remove ids from data if (val.data) { if (val.data.id) { delete val.data.id; } else if (val.data[0]) { for (let x of val.data) delete x.id; } } delete val.links; } // organization is unused (?) delete this.relationships.organization; // id is specific to envs // but save source inside meta string in case we need it this.metastring = this.remote + "-" + this.data.id; delete this.data.id; // links too delete this.data.links; } } function sleep(time = 1000) { return new Promise(resolve => setTimeout(resolve, time)); } function* zip(...items) { let iters = items.map(x => x[Symbol.iterator]()); for (;;) { let r = []; for (let i of iters) { let next = i.next(); if (next.done) return; r.push(next.value); } yield r; } } function unordered(_x) { return _unordered.apply(this, arguments); } function _unordered() { _unordered = _wrapAsyncGenerator(function* (proms) { let encapsulatedPromises = proms.map(async (x, i) => [i, await x]); while (encapsulatedPromises.length > 0) { let [ind, result] = yield _awaitAsyncGenerator(Promise.race(encapsulatedPromises.filter(x => x))); yield result; encapsulatedPromises[ind] = undefined; } }); return _unordered.apply(this, arguments); } function* range(start, end) { if (end === undefined) { end = start; start = 0; } while (start < end) yield start++; } class IndexObject { //normal opts from any makeAPIRequest //Note that full_response and pages won't work. // //if you want to start from another page, use `opts.start` //opts.observe: async function(jsonData) => jsonData. Transform the data from the api //opts.maxParallelRequests: number of max api requests to do at once //opts.noCollect: return [] instead of the full data constructor(opts) { this.opts = opts; } linkToPage(page) { return this.baselink.replace(`page=1p`, `page=${page}p`); } async initializeFirstRequest() { //Create a copy of the options in case we need to have a special first request this.start = this.opts.start || 1; let initOpts = { ...this.opts }; if (this.opts.pageSize) { initOpts.qs = { ...this.opts.qs }; initOpts.qs.page = `${this.start}p${this.opts.pageSize}`; } this.allResults = []; //we make 1 non-parallel request to the first page so we know how to //format the next requests let json = await lib.makeAPIRequest(initOpts); if (this.opts.observe) json = await this.opts.observe(json); if (!this.opts.noCollect) this.allResults.push(json); this.baselink = json.links.first; this.currentPageRequest = this.start; this.hasHit404 = false; } getNextRequestLink() { this.currentPageRequest++; return [this.currentPageRequest, this.linkToPage(this.currentPageRequest)]; } ///promiseID is the id in `currentPromises`, so that it can be marked as ///done inside the promise array. promiseID is a number from 0 to ///maxparallel-1 async getNextRequestPromise(promiseID) { let [page, path_full] = this.getNextRequestLink(); return [promiseID, page, await lib.makeAPIRequest({ ...this.opts, path_full, fullResponse: true })]; } cancel() { this.willCancel = true; } async fullResults() { await this.initializeFirstRequest(); let maxParallelRequests = this.opts.maxParallelRequests || this.opts.chunksize || 20; let currentPromises = []; //generate the first set of requests. Everything after this will re-use these i promiseIDs for (let i = 0; i < maxParallelRequests; i++) { currentPromises.push(this.getNextRequestPromise(currentPromises.length)); } for (;;) { let [promiseID, page, requestResult] = await Promise.race(currentPromises.filter(x => x)); if (this.willCancel) { return null; } if (requestResult.statusCode === 404) { this.hasHit404 = true; } else if (requestResult.statusCode === 200) { let json = JSON.parse(requestResult.body); if (this.opts.observe) json = await this.opts.observe(json); if (!this.opts.noCollect) this.allResults.push(json); if (json.data.length === 0) this.hasHit404 = true; } else { throw new APIError(requestResult, `(unknown args) page ${page}`, null); } if (this.hasHit404) { currentPromises[promiseID] = null; } else { currentPromises[promiseID] = this.getNextRequestPromise(promiseID); } if (currentPromises.filter(x => x).length === 0) break; } let all = []; for (let result of this.allResults) { for (let item of result.data) { all.push(item); } } return all; } } const inquirer = importLazy("inquirer"); const readdir = importLazy("recursive-readdir"); let hasAutoCompletePrompt = false; function addAutoCompletePrompt() { if (hasAutoCompletePrompt) return; hasAutoCompletePrompt = true; inquirer.registerPrompt("autocomplete", require("inquirer-autocomplete-prompt")); } async function $api(propArray) { let q; q = await inquirer.prompt([{ type: "input", name: "company", message: `What is your company?`, default: `discovery` }]); let company = q.company; const defaults = { DEV: `https://${company}-dev.sdvi.com/api/v2`, UAT: `https://${company}-uat.sdvi.com/api/v2`, QA: `https://${company}-qa.sdvi.com/api/v2`, PROD: `https://${company}.sdvi.com/api/v2` }; if (propArray && propArray[1]) { q = { envs: [propArray[1]] }; } else { //Create a checkbox prompt to choose enviornments q = await inquirer.prompt([{ type: "checkbox", name: "envs", message: `What enviornments would you like to configure?`, choices: Object.keys(defaults).map(name => ({ name, checked: true })) }]); } //Each env should ask 2 for two things: The url and the key. let questions = q.envs.map(env => { let defaultKey = process.env[`rally_api_key_${env}`]; if (configObject && configObject.api && configObject.api[env]) { defaultKey = configObject.api[env].key; } return [{ type: "input", name: `api.${env}.url`, message: `What is the api endpoint for ${env}?`, default: defaults[env] }, { type: "input", name: `api.${env}.key`, message: `What is your api key for ${env}?`, default: defaultKey }]; }); //flatten and ask questions = [].concat(...questions); q = await inquirer.prompt(questions); if (propArray) { q.api = { ...configObject.api, ...q.api }; } return q; } async function $chalk(propArray) { return { chalk: await askQuestion("Would you like chalk enabled (Adds coloring)?") }; } async function $restrictUAT(propArray) { return { restrictUAT: await askQuestion("Would you like to protect UAT?") }; } async function $repodir(propArray) { return await inquirer.prompt([{ type: "input", name: `repodir`, message: `Where is your rally repository (empty for N/A)?`, default: process.env["rally_repo_path"] }]); } async function $appName(propArray) { let defaultAppName = "cmdline-" + (process.env.USERNAME || process.env.LOGNAME); let project = await askInput("Application name?", defaultAppName); if (project === "none" || project === "-" || project === "" || !project) { project = null; } return { appName: project }; } async function $project(propArray) { let project = await askInput("Subproject directory?"); if (project === "none" || project === "-" || project === "" || !project) { project = null; } return { project }; } async function $defaultEnv(propArray) { return await inquirer.prompt([{ type: "input", name: `defaultEnv`, message: `Default enviornment?`, default: "DEV" }]); } //Internal usage/testing async function selectProvider(providers, autoDefault = false) { addAutoCompletePrompt(); let defaultProvider = providers.findByName("SdviEvaluate"); if (autoDefault) { return defaultProvider; } else { let choices = providers.arr.map(x => ({ name: x.chalkPrint(true), value: x })); let q = await inquirer.prompt([{ type: "autocomplete", name: "provider", default: defaultProvider, source: async (sofar, input) => { return choices.filter(x => input ? x.value.name.toLowerCase().includes(input.toLowerCase()) : true); } }]); return q.provider; } } async function loadLocals(path$$1, Class) { let basePath = configObject.repodir; let objs = (await readdir(basePath)).filter(name => name.includes(path$$1)).filter(name => !path.basename(name).startsWith(".")).map(name => new Class({ path: name })); return objs; } async function selectLocal(path$$1, typeName, Class, canSelectNone = true) { addAutoCompletePrompt(); let objs = await loadLocals(path$$1, Class); let objsMap = objs.map(x => ({ name: x.chalkPrint(true), value: x })); let none = { name: chalk` {red None}: {red None}`, value: null }; if (canSelectNone) objsMap.unshift(none); let q = await inquirer.prompt([{ type: "autocomplete", name: "obj", message: `What ${typeName} do you want?`, source: async (sofar, input) => { return objsMap.filter(x => input ? x.name.toLowerCase().includes(input.toLowerCase()) : true); } }]); return q.obj; } async function selectPreset({ purpose = "preset", canSelectNone }) { return selectLocal("silo-presets", purpose, Preset, canSelectNone); } async function selectRule({ purpose = "rule", canSelectNone }) { return selectLocal("silo-rules", purpose, Rule, canSelectNone); } async function askInput(question, def) { return (await inquirer.prompt([{ type: "input", name: "ok", message: question, default: def }])).ok; } async function askQuestion(question) { return (await inquirer.prompt([{ type: "confirm", name: "ok", message: question }])).ok; } async function saveConfig(newConfigObject, { ask = true, print = true } = {}) { //Create readable json and make sure the user is ok with it let newConfig = JSON.stringify(newConfigObject, null, 4); if (print) log(newConfig); //-y or --set will make this not prompt if (ask && !(await askQuestion("Write config to disk?"))) return; fs.writeFileSync(configFile, newConfig, { mode: 0o600 }); log(chalk`Created file {green ${configFile}}.`); } var configHelpers = /*#__PURE__*/Object.freeze({ inquirer: inquirer, addAutoCompletePrompt: addAutoCompletePrompt, $api: $api, $chalk: $chalk, $restrictUAT: $restrictUAT, $repodir: $repodir, $appName: $appName, $project: $project, $defaultEnv: $defaultEnv, selectProvider: selectProvider, loadLocals: loadLocals, selectLocal: selectLocal, selectPreset: selectPreset, selectRule: selectRule, askInput: askInput, askQuestion: askQuestion, saveConfig: saveConfig }); class Provider extends RallyBase { constructor({ data, remote }) { super(); this.data = data; this.meta = {}; this.remote = remote; } //cached async getEditorConfig() { if (this.editorConfig) return this.editorConfig; this.editorConfig = await lib.makeAPIRequest({ env: this.remote, path_full: this.data.links.editorConfig }); this.editorConfig.fileExt = await this.getFileExtension(); return this.editorConfig; } static async getAllPreCollect(providers) { return providers.sort((a, b) => { return a.attributes.category.localeCompare(b.attributes.category) || a.attributes.name.localeCompare(b.attributes.name); }); } async getFileExtension() { let config = await this.getEditorConfig(); let map = { python: "py", text: "txt", getmap(key) { if (this.name === "Aurora") return "zip"; if (this[key]) return this[key]; return key; } }; return map.getmap(config.lang); } chalkPrint(pad = true) { let id = String(this.id); if (pad) id = id.padStart(4); return chalk`{green ${id}}: {blue ${this.category}} - {green ${this.name}}`; } } defineAssoc(Provider, "id", "data.id"); defineAssoc(Provider, "name", "data.attributes.name"); defineAssoc(Provider, "category", "data.attributes.category"); defineAssoc(Provider, "remote", "meta.remote"); defineAssoc(Provider, "editorConfig", "meta.editorConfig"); Provider.endpoint = "providerTypes"; class File extends RallyBase { constructor({ data, remote, included, parent }) { super(); this.data = data; this.meta = {}; this.remote = remote; this.parentAsset = parent; } chalkPrint(pad = false) { let id = String("F-" + (this.remote && this.remote + "-" + this.id || "LOCAL")); if (pad) id = id.padStart(15); return chalk`{green ${id}}: {blue ${this.data.attributes ? this.name : "(lite file)"}} {red ${this.sizeHR}}`; } canBeDownloaded() { return this.sizeGB <= .2; } async getContent(force = false) { if (!this.canBeDownloaded() && !force) { throw new FileTooLargeError(this); } return lib.makeAPIRequest({ env: this.remote, fullPath: this.contentLink }); } async delete(remove = true) { return lib.makeAPIRequest({ env: this.remote, fullPath: this.selfLink, method: "DELETE" }); } get size() { return Object.values(this.data.attributes.instances)[0].size; } get sizeGB() { return Math.round(this.size / 1024 / 1024 / 1024 * 10) / 10; } get sizeHR() { let units = ["B", "K", "M", "G", "T"]; let unitIdx = 0; let size = this.size; while (size > 1000) { size /= 1024; unitIdx++; } if (size > 100) { size = Math.round(size); } else { size = Math.round(size * 10) / 10; } return size + units[unitIdx]; } get instancesList() { let instances = []; for (let [key, val] of Object.entries(this.instances)) { let n = { id: key }; Object.assign(n, val); instances.push(n); } return instances; } static rslURL(instance) { return `rsl://${instance.storageLocationName}/${instance.name}`; } } defineAssoc(File, "id", "data.id"); defineAssoc(File, "name", "data.attributes.label"); defineAssoc(File, "contentLink", "data.links.content"); defineAssoc(File, "selfLink", "data.links.self"); defineAssoc(File, "label", "data.attributes.label"); defineAssoc(File, "md5", "data.attributes.md5"); defineAssoc(File, "sha512", "data.attributes.sha512"); defineAssoc(File, "tags", "data.attributes.tagList"); defineAssoc(File, "instances", "data.attributes.instances"); File.endpoint = null; async function findLineInFile(renderedPreset, lineNumber) { let trueFileLine = lineNumber; let linedRenderedPreset = renderedPreset.split("\n").slice(2, -2); renderedPreset = renderedPreset.split("\n").slice(2, -2).join("\n"); let includeLocation = renderedPreset.split("\n").filter(x => x.includes("@include")); let endIncludeNumber = -1, addTabDepth = 2; let lineBeforeIncludeStatement = ''; let withinInclude = true; if (lineNumber > linedRenderedPreset.indexOf(includeLocation[includeLocation.length - 1])) { addTabDepth = 0; withinInclude = false; } for (let index = includeLocation.length - 1; index >= 0; index--) { let currIncludeIndex = linedRenderedPreset.indexOf(includeLocation[index]); let tabDepth = includeLocation[index].split(" ").length; if (lineNumber > currIncludeIndex) { if (includeLocation[index].split(" ").filter(Boolean)[1] != "ERROR:") { if (lineBeforeIncludeStatement.split(" ").length == tabDepth && withinInclude) { trueFileLine = trueFileLine - currIncludeIndex; break; } else if (lineBeforeIncludeStatement.split(" ").length + addTabDepth == tabDepth && endIncludeNumber == -1) { endIncludeNumber = currIncludeIndex; } else if (lineBeforeIncludeStatement.split(" ").length + addTabDepth == tabDepth) { trueFileLine = trueFileLine - (endIncludeNumber - currIncludeIndex); endIncludeNumber = -1; } } } else { lineBeforeIncludeStatement = includeLocation[index]; } } let funcLine = ""; for (let line of linedRenderedPreset.slice(0, lineNumber).reverse()) { let match = /def (\w+)/.exec(line); if (match) { funcLine = match[1]; break; } } let includeFilename; if (lineBeforeIncludeStatement != "") { includeFilename = lineBeforeIncludeStatement.slice(1).trim().slice(14, -1); } else { includeFilename = null; } if (includeLocation.length !== 0) { trueFileLine -= 1; lineNumber -= 1; } return { lineNumber: trueFileLine, includeFilename, line: linedRenderedPreset[lineNumber], funcLine }; } function printOutLine(eLine) { return log(chalk`{blue ${eLine.includeFilename || "Main"}}:{green ${eLine.lineNumber}} in ${eLine.funcLine} ${eLine.line}`); } async function getArtifact(env, artifact, jobid) { let path$$1 = `/jobs/${jobid}/artifacts/${artifact}`; let art = lib.makeAPIRequest({ env, path: path$$1 }).catch(_ => null); return await art; } async function getInfo(env, jobid) { let trace = getArtifact(env, "trace", jobid); let renderedPreset = getArtifact(env, "preset", jobid); let result = getArtifact(env, "result", jobid); let error = getArtifact(env, "error", jobid); let output = getArtifact(env, "output", jobid); [trace, renderedPreset, result, output, error] = await Promise.all([trace, renderedPreset, result, output, error]); return { trace, renderedPreset, result, output, error }; } const tracelineRegex = /^(?:[\d.]+) ([\w ]+):(\d+): (.+)/; function parseTraceLine(line) { let info = tracelineRegex.exec(line); if (!info) { return { full: line, parsed: false, content: line }; } return { absoluteTime: info[0], presetName: info[1], lineNumber: info[2], text: info[3], content: info[3], full: line, parsed: true }; } async function parseTrace(env, jobid) { let { trace, renderedPreset } = await getInfo(env, jobid); let errorLines = []; let shouldBreak = 0; for (let tr of trace.split("\n\n").reverse()) { errorLines.push(tr); shouldBreak--; if (tr.includes("Exception")) shouldBreak = 1; if (tr.includes("raised")) shouldBreak = 1; if (!shouldBreak) break; } let errorList = []; for (let errLine of errorLines) { let info = parseTraceLine(errLine); if (!info.parsed) { errorList.push((await findLineInFile(renderedPreset, info.lineNumber))); } else { errorList.push(errLine); } } return errorList; } const Trace = { parseTrace, printOutLine, getInfo, findLineInFile, getArtifact }; class Asset extends RallyBase { constructor({ data, remote, included, lite }) { super(); this.data = data; this.meta = {}; this.remote = remote; if (included) { this.meta.metadata = Asset.normalizeMetadata(included); } this.lite = !!lite; } static normalizeMetadata(payload) { let newMetadata = {}; for (let md of payload) { if (md.type !== "metadata") continue; newMetadata[md.attributes.usage] = md.attributes.metadata; } return newMetadata; } async getMetadata(forceRefresh = false) { if (this.meta.metadata && !forceRefresh) return this.meta.metadata; let req = await lib.makeAPIRequest({ env: this.remote, path: `/movies/${this.id}/metadata?page=1p100` }); return this.meta.metadata = Asset.normalizeMetadata(req.data); } async patchMetadata(metadata) { if (metadata.Workflow) { //FIXME //Currently, WORKFLOW_METADATA cannot be patched via api: we need to //start a ephemeral eval to upload it let md = JSON.stringify(JSON.stringify(metadata.Workflow)); let fakePreset = { code: `WORKFLOW_METADATA.update(json.loads(${md}))` }; await this.startEphemeralEvaluateIdeal(fakePreset); log("WFMD Patched using ephemeralEval"); } if (metadata.Metadata) { let req = await lib.makeAPIRequest({ env: this.remote, path: `/movies/${this.id}/metadata/Metadata`, method: "PATCH", payload: { "data": { "type": "metadata", "attributes": { "metadata": metadata.Metadata } } } }); log("MD Patched"); } } static lite(id, remote) { return new this({ data: { id }, remote, lite: true }); } chalkPrint(pad = false) { let id = String("A-" + (this.remote && this.remote + "-" + this.id || "LOCAL")); if (pad) id = id.padStart(15); return chalk`{green ${id}}: {blue ${this.data.attributes ? this.name : "(lite asset)"}}`; } static async createNew(name, env) { let req = await lib.makeAPIRequest({ env, path: "/assets", method: "POST", payload: { data: { attributes: { name }, type: "assets" } } }); return new this({ data: req.data, remote: env }); } async delete() { let req = await lib.makeAPIRequest({ env: this.remote, path: "/assets/" + this.id, method: "DELETE" }); } async getFiles(refresh = false) { if (this._files && !refresh) return this._files; let req = await lib.indexPathFast({ env: this.remote, path: `/assets/${this.id}/files`, method: "GET" }); //return req; return this._files = new Collection(req.map(x => new File({ data: x, remote: this.remote, parent: this }))); } async addFile(label, fileuris) { if (!Array.isArray(fileuris)) fileuris = [fileuris]; let instances = {}; for (let i = 0; i < fileuris.length; i++) { instances[String(i + 1)] = { uri: fileuris[i] }; } let req = await lib.makeAPIRequest({ env: this.remote, path: "/files", method: "POST", payload: { "data": { "attributes": { label, instances }, "relationships": { "asset": { "data": { id: this.id, "type": "assets" } } }, "type": "files" } } }); return req; } async startWorkflow(jobName, { initData, priority } = {}) { let attributes = {}; if (initData) { //Convert init data to string initData = typeof initData === "string" ? initData : JSON.stringify(initData); attributes.initData = initData; } if (priority) { attributes.priority = priority; } let req = await lib.makeAPIRequest({ env: this.remote, path: "/workflows", method: "POST", payload: { "data": { "type": "workflows", attributes, "relationships": { "movie": { "data": { id: this.id, "type": "movies" } }, "rule": { "data": { "attributes": { "name": jobName }, "type": "rules" } } } } } }); return req; } static async startAnonWorkflow(env, jobName, { initData, priority } = {}) { let attributes = {}; if (initData) { //Convert init data to string initData = typeof initData === "string" ? initData : JSON.stringify(initData); attributes.initData = initData; } if (priority) { attributes.priority = priority; } let req = await lib.makeAPIRequest({ env, path: "/workflows", method: "POST", payload: { "data": { "type": "workflows", attributes, "relationships": { "rule": { "data": { "attributes": { "name": jobName }, "type": "rules" } } } } } }); return req; } async startEphemeralEvaluateIdeal(preset, dynamicPresetData, isBinary = false) { let res; const env = this.remote; let provider = await Provider.getByName(this.remote, "SdviEvaluate"); write(chalk`Starting ephemeral evaluate on ${this.chalkPrint(false)}...`); // Fire and forget. let evalInfo = await lib.makeAPIRequest({ env: this.remote, path: "/jobs", method: "POST", payload: { data: { attributes: { category: provider.category, providerTypeName: provider.name, rallyConfiguration: {}, //we need to strip invalid utf8 characters from the //buffer before we encode it or the sdvi backend dies providerData: Buffer.from(preset.code, isBinary && "binary" || "utf8").toString("base64"), dynamicPresetData }, type: "jobs", relationships: { movie: { data: { id: this.id, type: "movies" } } } } } }); write(" Waiting for finish...\n"); let dots = 0; for (;;) { res = await lib.makeAPIRequest({ env, path_full: evalInfo.data.links.self }); write(`\r${res.data.attributes.state}${".".repeat(dots++)} `); if (dots === 5) { dots = 1; } if (res.data.attributes.state == "Complete") { write(chalk`{green Done}...\n`); break; } await sleep(500); } return; } async startEvaluate(presetid, dynamicPresetData) { // Fire and forget. let data = await lib.makeAPIRequest({ env: this.remote, path: "/jobs", method: "POST", payload: { data: { type: "jobs", attributes: { dynamicPresetData }, relationships: { movie: { data: { id: this.id, type: "movies" } }, preset: { data: { id: presetid, type: "presets" } } } } } }); return data; } async rename(newName) { let req = await lib.makeAPIRequest({ env: this.remote, path: `/assets/${this.id}`, method: "PATCH", payload: { data: { attributes: { name: newName }, type: "assets" } } }); this.name = newName; return req; } async migrate(targetEnv) { configObject.globalProgress = false; log(`Creating paired file in ${targetEnv}`); //Fetch metadata in parallel, we await it later let _mdPromise = this.getMetadata(); let targetAsset = await Asset.getByName(targetEnv, this.name); if (targetAsset) { log(`Asset already exists ${targetAsset.chalkPrint()}`); //if(configObject.script) process.exit(10); } else { targetAsset = await Asset.createNew(this.name, targetEnv); log(`Asset created ${targetAsset.chalkPrint()}`); } //wait for metadata to be ready before patching await _mdPromise; log("Adding asset metadata"); await targetAsset.patchMetadata(this.md); let fileCreations = []; for (let file of await this.getFiles()) { let possibleInstances = {}; //Check for any valid copy-able instances for (let inst of file.instancesList) { //We need to skip internal files if (inst.storageLocationName === "Rally Platform Bucket") continue; log(`Adding file: ${file.chalkPrint()}`); possibleInstances[inst.storageLocationName] = () => targetAsset.addFileInstance(file, inst); } if (Object.values(possibleInstances).length > 1) { //prioritize archive is possible if (possibleInstances["Archive"]) { log("Hit archive prioritizer"); fileCreations.push(possibleInstances["Archive"]); } else { fileCreations.push(...Object.values(possibleInstances)); } } else { fileCreations.push(...Object.values(possibleInstances)); } } await Promise.all(fileCreations.map(x => x())); } async addFileInstance(file, inst, tagList = []) { let newInst = { uri: File.rslURL(inst), name: inst.name, size: inst.size, lastModified: inst.lastModified, storageLocationName: inst.storageLocationName }; let instances = {}; instances[String(Math.floor(Math.random() * 100000 + 1))] = newInst; let request = lib.makeAPIRequest({ env: this.remote, path: `/files`, method: "POST", payload: { data: { type: "files", attributes: { label: file.label, tagList, instances }, relationships: { asset: { data: { id: this.id, type: "assets" } } } } } }); try { let fileData = await request; let newFile = new File({ data: fileData.data, remote: this.remote, parent: this }); if (configObject.scrip