UNPKG

47 kBJavaScriptView Raw
1require("source-map-support").install();
2
3import argparse from "minimist";
4import * as allIndexBundle from "./index.js"
5import {
6 rallyFunctions as funcs,
7 Preset, Rule, SupplyChain, Provider, Asset, User, Tag,
8 AbortError, UnconfiguredEnvError, Collection, APIError,
9} from "./index.js";
10
11import {version as packageVersion} from "../package.json";
12import {configFile, configObject, loadConfig, loadConfigFromArgs} from "./config.js";
13import {readFileSync, writeFileSync} from "fs";
14
15import {printOutLine, parseTrace, findLineInFile, getInfo as getTraceInfo} from "./trace.js";
16
17import {helpText, arg, param, usage, helpEntries, spawn} from "./decorators.js";
18
19import baseCode from "./baseCode.js";
20import {sep as pathSeperator} from "path";
21
22import moment from "moment";
23
24import * as configHelpers from "./config-create.js";
25const False = false; const True = true; const None = null;
26
27let argv = argparse(process.argv.slice(2), {
28 string: ["file", "env"],
29 //boolean: ["no-protect"],
30 boolean: ["anon"],
31 default: {protect: true},
32 alias: {
33 f: "file", e: "env",
34 }
35});
36
37//help menu helper
38function printHelp(help, short){
39 let helpText = chalk`
40{white ${help.name}}: ${help.text}
41 Usage: ${help.usage || "<unknown>"}
42`
43 //Trim newlines
44 helpText = helpText.substring(1, helpText.length-1);
45
46 if(!short){
47 for(let param of help.params || []){
48 helpText += chalk`\n {blue ${param.param}}: ${param.desc}`
49 }
50 for(let arg of help.args || []){
51 helpText += chalk`\n {blue ${arg.short}}, {blue ${arg.long}}: ${arg.desc}`
52 }
53 }
54
55 return helpText;
56}
57
58async function getFilesFromArgs(args){
59 let lastArg = args._.shift()
60 if(args.file){
61 let files = args.file;
62 if(typeof files === "string") files = [files];
63 return files;
64 }
65
66 if(lastArg == "-"){
67 log("Reading from stdin");
68 let getStdin = require("get-stdin");
69 let stdin = await getStdin();
70 let files = stdin.split("\n");
71 if(files[files.length - 1] === "") files.pop();
72 return files;
73 }else{
74 args._.push(lastArg);
75 }
76}
77
78let presetsub = {
79 async before(args){
80 this.env = args.env;
81 if(!this.env) throw new AbortError("No env supplied");
82
83 this.files = await getFilesFromArgs(args);
84 },
85 async $grab(args){
86 if(!this.files){
87 throw new AbortError("No files provided to grab (use --file argument)");
88 }
89
90 log(chalk`Grabbing {green ${this.files.length}} preset(s) metadata from {green ${this.env}}.`);
91
92 let presets = this.files.map(path => new Preset({path, remote: false}));
93 for(let preset of presets){
94 //TODO small refactor
95 await preset.grabMetadata(this.env);
96 await preset.saveLocalMetadata();
97
98 if(args.full){
99 let remo = await Preset.getByName(this.env, preset.name);
100 await remo.resolve();
101 await remo.downloadCode();
102 await remo.saveLocalFile();
103 }
104 }
105 },
106 async $create(args){
107 let provider, name, ext;
108 if(args.provider){
109 provider = {name: args.provider};
110 ext = args.ext
111 }else{
112 provider = await configHelpers.selectProvider(await Provider.getAll(this.env));
113 ext = (await provider.getEditorConfig()).fileExt;
114 }
115 if(args.name){
116 name = args.name;
117 }else{
118 name = await configHelpers.askInput("Preset Name", "What is the preset name?");
119 }
120
121 let preset = new Preset({subProject: configObject.project});
122
123 preset.providerType = {name: provider.name};
124 preset.isGeneric = true;
125 preset.name = name;
126 preset.ext = ext;
127 if(baseCode[provider.name]){
128 preset._code = baseCode[provider.name].replace("{name}", name);
129 }else{
130 preset._code = " ";
131 }
132
133 preset.saveLocalMetadata();
134 if(!args["only-metadata"]) preset.saveLocalFile();
135 },
136 async $list(args){
137 log("Loading...");
138 let presets = await Preset.getAll(this.env);
139 if(args.resolve){
140 Provider.getAll(this.env);
141 for(let preset of presets){
142 let resolve = await preset.resolve(this.env);
143 if(args.attach){
144 let {proType} = resolve;
145 proType.editorConfig.helpText = "";
146 preset.meta = {
147 ...preset.meta, proType
148 };
149 }
150 }
151 }
152 if(configObject.rawOutput) return presets;
153 log(chalk`{yellow ${presets.length}} presets on {green ${this.env}}.`);
154 presets.arr.sort((a, b) => {
155 return Number(a.attributes.updatedAt) - Number(b.attributes.updatedAt)
156 });
157 for(let preset of presets){
158 log(preset.chalkPrint());
159 }
160 },
161 async $upload(args){
162 if(!this.files){
163 throw new AbortError("No files provided to upload (use --file argument)");
164 }
165
166 log(chalk`Uploading {green ${this.files.length}} preset(s) to {green ${this.env}}.`);
167
168 let presets = this.files.map(path => new Preset({path, remote: false}));
169 await funcs.uploadPresets(this.env, presets);
170 },
171 async $deleteRemote(args){
172 let file = this.files[0];
173 if(!this.files){
174 throw new AbortError("No files provided to diff (use --file argument)");
175 }
176
177 let preset = new Preset({path: file, remote: false});
178 if(!preset.name){
179 throw new AbortError(chalk`No preset header found. Cannot get name.`);
180 }
181
182 let preset2 = await Preset.getByName(this.env, preset.name);
183 if(!preset2){
184 throw new AbortError(chalk`No preset found with name {red ${preset.name}} on {blue ${this.env}}`);
185 }
186
187 log(chalk`Deleting ${preset2.chalkPrint(true)}.`);
188
189 log(await preset2.delete());
190 },
191 async $diff(args){
192 let file = this.files[0];
193 if(!this.files){
194 throw new AbortError("No files provided to diff (use --file argument)");
195 }
196
197 let preset = new Preset({path: file, remote: false});
198 if(!preset.name){
199 throw new AbortError(chalk`No preset header found. Cannot get name.`);
200 }
201 let preset2 = await Preset.getByName(this.env, preset.name);
202 if(!preset2){
203 throw new AbortError(chalk`No preset found with name {red ${preset.name}} on {blue ${this.env}}`);
204 }
205 await preset2.downloadCode();
206
207 let tempfile = require("tempy").file;
208 let temp = tempfile({extension: `${this.env}.${preset.ext}`});
209 writeFileSync(temp, preset2.code);
210
211 let ptr = `${file},${temp}`;
212
213
214 //raw output returns "file1" "file2"
215 if(configObject.rawOutput){
216 if(args["only-new"]) return temp;
217 else return ptr;
218 }
219
220 //standard diff
221 argv.command = argv.command || "diff";
222 await spawn(argv.command, [file, temp], {stdio: "inherit"});
223 },
224 async $info(args){
225 if(!this.files){
226 throw new AbortError("No files provided to diff (use --file argument)");
227 }
228
229 let file = this.files[0];
230 let preset = new Preset({path: file, remote: false});
231 if(!preset.name){
232 throw new AbortError(chalk`No preset header found. Cannot get name.`);
233 }
234
235 await preset.getInfo(args.env);
236 },
237 async unknown(arg, args){
238 log(chalk`Unknown action {red ${arg}} try '{white rally help preset}'`);
239 },
240}
241
242let rulesub = {
243 async before(args){
244 this.env = args.env;
245 if(!this.env) throw new AbortError("No env supplied");
246 },
247 async $list(args){
248 log("Loading...");
249 let rules = await Rule.getAll(this.env);
250 if(configObject.rawOutput) return rules;
251
252 log(chalk`{yellow ${rules.length}} rules on {green ${this.env}}.`);
253 rules.arr.sort((a, b) => {
254 return Number(a.data.attributes.updatedAt) - Number(b.data.attributes.updatedAt)
255 });
256 for(let rule of rules) log(rule.chalkPrint());
257 },
258 async $create(args){
259 let preset = await configHelpers.selectPreset({canSelectNone: false});
260 let passNext = await configHelpers.selectRule({purpose: "'On Exit OK'"});
261 let errorNext = await configHelpers.selectRule({purpose: "'On Exit Error'"});
262 let name = await configHelpers.askInput("Rule Name", "What is the rule name?");
263 name = name.replace("@", preset.name);
264 let desc = await configHelpers.askInput("Description", "Enter a description.");
265
266 let dynamicNexts = [];
267 let next;
268 while(next = await configHelpers.selectRule({purpose: "dynamic next"})){
269 let name = await configHelpers.askInput("Key", "Key name for dynamic next");
270 dynamicNexts.push({
271 meta: {
272 transition: name,
273 },
274 type: "workflowRules",
275 name: next.name,
276 });
277 }
278
279 let rule = new Rule({subProject: configObject.project});
280 rule.name = name;
281 rule.description = desc;
282 rule.relationships.preset = {data: {name: preset.name, type: "presets"}}
283 if(errorNext) rule.relationships.errorNext = {data: {name: errorNext.name, type: "workflowRules"}}
284 if(passNext) rule.relationships.passNext = {data: {name: passNext.name, type: "workflowRules"}}
285 if(dynamicNexts[0]){
286 rule.relationships.dynamicNexts = {
287 data: dynamicNexts
288 };
289 }
290
291 rule.saveB()
292 },
293 async unknown(arg, args){
294 log(chalk`Unknown action {red ${arg}} try '{white rally help rule}'`);
295 },
296}
297
298let jupytersub = {
299 async before(args){
300 this.input = args._.shift() || "main.ipynb";
301 this.output = args._.shift() || "main.py";
302 },
303 async $build(args){
304 let cmd = `jupyter nbconvert --to python ${this.input} --TagRemovePreprocessor.remove_cell_tags={\"remove_cell\"} --output ${this.output} --TemplateExporter.exclude_markdown=True --TemplateExporter.exclude_input_prompt=True --TemplateExporter.exclude_output_prompt=True`.split(" ");
305 log(chalk`Compiling GCR file {green ${this.input}} into {green ${this.output}} using jupyter...`);
306
307 try{
308 let {timestr} = await spawn(cmd[0], cmd.slice(1));
309 log(chalk`Complete in ~{green.bold ${timestr}}.`);
310 }catch(e){
311 if(e.code !== "ENOENT") throw e;
312 log(chalk`Cannot run the build command. Make sure that you have jupyter notebook installed.\n{green pip install jupyter}`);
313 return;
314 }
315 },
316}
317
318async function categorizeString(str, defaultSubproject=undefined){
319 str = str.trim();
320 if(str.startsWith('"')){
321 str = str.slice(1, -1);
322 }
323 let match
324 if(match = /^(\w)-(\w{1,10})-(\d{1,10}):/.exec(str)){
325 if(match[1] === "P"){
326 let ret = await Preset.getById(match[2], match[3]);
327 //TODO modify for subproject a bit
328 return ret;
329 }else if(match[1] === "R"){
330 return await Rule.getById(match[2], match[3]);
331 }else{
332 return null;
333 }
334 }else if(match = /^([\w \/\\\-_]*)[\/\\]?silo\-(\w+)[\/\\]/.exec(str)){
335 try{
336 switch(match[2]){
337 case "presets": return new Preset({path: str, subProject: match[1]});
338 case "rules": return new Rule({path: str, subProject: match[1]});
339 case "metadata": return await Preset.fromMetadata(str, match[1]);
340 }
341 }catch(e){
342 log(e);
343 }
344 }else{
345 return null;
346 }
347}
348
349let tagsub = {
350 async before(args){
351 this.env = args.env;
352 if(!this.env) throw new AbortError("No env supplied");
353 },
354 async $list(args){
355 log("Loading...");
356 let tags = await Tag.getAll(this.env);
357 if(configObject.rawOutput) return tags;
358
359 log(chalk`{yellow ${tags.length}} tags on {green ${this.env}}.`);
360 tags.arr.sort((a, b) => {
361 return Number(a.data.attributes.updatedAt) - Number(b.data.attributes.updatedAt)
362 });
363 for(let tag of tags) log(tag.chalkPrint());
364 },
365 async $create(args){
366 return Tag.create(this.env, args._.shift());
367 }
368};
369
370let supplysub = {
371 async before(args){
372 this.env = args.env;
373 if(!this.env) throw new AbortError("No env supplied");
374 this.files = await getFilesFromArgs(args);
375 },
376
377 //Calculate a supply chain based on a starting rule at the top of the stack
378 async $calc(args){
379 let name = args._.shift();
380 let stopName = args._.shift();
381 if(!name){
382 throw new AbortError("No starting rule or @ supplied");
383 }
384
385 if(name === "@"){
386 log(chalk`Silo clone started`);
387 this.chain = new SupplyChain();
388 this.chain.remote = args.env;
389 }else{
390 let rules = await Rule.getAll(this.env);
391 let stop, start;
392 start = rules.findByNameContains(name);
393 if(stopName) stop = rules.findByNameContains(stopName);
394
395 if(!start){
396 throw new AbortError(chalk`No starting rule found by name {blue ${name}}`);
397 }
398 log(chalk`Analzying supply chain: ${start.chalkPrint(false)} - ${stop ? stop.chalkPrint(false) : "(open)"}`);
399 this.chain = new SupplyChain(start, stop);
400 }
401
402 await this.chain.calculate();
403 return await this.postAction(args);
404 },
405 async postAction(args){
406 //Now that we ahve a supply chain object, do something with it
407 if(args["to"]){
408 this.chain.log();
409 if(this.chain.presets.arr[0]){
410 await this.chain.downloadPresetCode(this.chain.presets);
411 log("Done");
412 }
413
414 if(Array.isArray(args["to"])){
415 for(let to of args["to"]){
416 await this.chain.syncTo(to);
417 }
418 }else{
419 await this.chain.syncTo(args["to"]);
420 }
421
422 }else if(args["delete"]){
423 if(Array.isArray(args["delete"])){
424 for(let to of args["delete"]){
425 await this.chain.deleteTo(to);
426 }
427 }else{
428 await this.chain.deleteTo(args["delete"]);
429 }
430 }else if(args["diff"]){
431 //Very basic diff
432 let env = args["diff"];
433 await Promise.all(this.chain.presets.arr.map(obj => obj.downloadCode()));
434 await Promise.all(this.chain.presets.arr.map(obj => obj.resolve()));
435
436 let otherPresets = await Promise.all(this.chain.presets.arr.map(obj => Preset.getByName(env, obj.name)));
437 otherPresets = new Collection(otherPresets.filter(x => x));
438 await Promise.all(otherPresets.arr.map(obj => obj.downloadCode()));
439 await Promise.all(otherPresets.arr.map(obj => obj.resolve()));
440
441 const printPresets = (preset, otherPreset) => {
442 log(preset.chalkPrint(true));
443 if(otherPreset.name){
444 log(otherPreset.chalkPrint(true));
445 }else{
446 log(chalk`{red (None)}`);
447 }
448 }
449
450 for(let preset of this.chain.presets){
451 let otherPreset = otherPresets.arr.find(x => x.name === preset.name) || {};
452
453 preset.code = preset.code.replace(/[\r\n ]/, "");
454 otherPreset.code = (otherPreset.code || "").replace(/[\r\n ]/, "");
455
456 if(preset.code === otherPreset.code){
457 if(!args["ignore-same"]){
458 printPresets(preset, otherPreset);
459 log("Code Same");
460 }
461 }else{
462 printPresets(preset, otherPreset);
463 if(args["ignore-same"]){
464 log("-------");
465 }else{
466 log("Code Different");
467 }
468 }
469 }
470
471 }else{
472 return await this.chain.log();
473 }
474
475 },
476 async $make(args){
477 let set = new Set();
478 let hints = args.hint ? (Array.isArray(args.hint) ? args.hint : [args.hint]) : []
479 //TODO modify for better hinting, and add this elsewhere
480 for(let hint of hints){
481 if(hint === "presets-uat"){
482 log("got hint");
483 await Preset.getAll("UAT");
484 }
485 }
486
487 for(let file of this.files){
488 set.add(await categorizeString(file));
489 }
490 let files = [...set];
491 files = files.filter(f => f && !f.missing);
492 this.chain = new SupplyChain();
493
494 this.chain.rules = new Collection(files.filter(f => f instanceof Rule));
495 this.chain.presets = new Collection(files.filter(f => f instanceof Preset));
496 this.chain.notifications = new Collection([]);
497
498 return await this.postAction(args);
499 },
500 async unknown(arg, args){
501 log(chalk`Unknown action {red ${arg}} try '{white rally help supply}'`);
502 },
503}
504
505function subCommand(object){
506 object = {
507 before(){}, after(){}, unknown(){},
508 ...object
509 };
510 return async function(args){
511 //Grab the next arg on the stack, find a function tied to it, and run
512 let arg = args._.shift();
513 let key = "$" + arg;
514 let ret;
515 if(object[key]){
516 await object.before(args);
517 ret = await object[key](args);
518 await object.after(args);
519 }else{
520 if(arg === undefined) arg = "(None)";
521 object.unknown(arg, args);
522 }
523 return ret;
524 }
525}
526
527let cli = {
528 @helpText(`Display the help menu`)
529 @usage(`rally help [subhelp]`)
530 @param("subhelp", "The name of the command to see help for")
531 async help(args){
532 let arg = args._.shift();
533 if(arg){
534 let help = helpEntries[arg];
535 if(!help){
536 log(chalk`No help found for '{red ${arg}}'`);
537 }else{
538 log(printHelp(helpEntries[arg]));
539 }
540 }else{
541 for(let helpArg in helpEntries){
542 log(printHelp(helpEntries[helpArg], true));
543 }
544 }
545 },
546
547 @helpText("Rally tools jupyter interface. Requires jupyter to be installed.")
548 @usage("rally jupyter build [in] [out]")
549 @param("in/out", "input and output file for jupyter. By default main.ipyrb and main.py")
550 async jupyter(args){
551 return subCommand(jupytersub)(args);
552 },
553
554 //@helpText(`Print input args, for debugging`)
555 async printArgs(args){
556 log(args);
557 },
558
559 @helpText(`Preset related actions`)
560 @usage(`rally preset [action] --env <enviornment> --file [file1] --file [file2] ...`)
561 @param("action", "The action to perform. Can be upload, diff, list, deleteRemote")
562 @arg("-e", "--env", "The enviornment you wish to perform the action on")
563 @arg("-f", "--file", "A file to act on")
564 @arg("~", "--command", "If the action is diff, this is the command to run instead of diff")
565 async preset(args){
566 return subCommand(presetsub)(args);
567 },
568
569 @helpText(`Rule related actions`)
570 @usage(`rally rule [action] --env [enviornment]`)
571 @param("action", "The action to perform. Only list is supported right now")
572 @arg("-e", "--env", "The enviornment you wish to perform the action on")
573 async rule(args){
574 return subCommand(rulesub)(args);
575 },
576
577 @helpText(`supply chain related actions`)
578 @usage(`rally supply [action] [identifier] --env [enviornment] [post actions]`)
579 @param("action", "The action to perform. Can be calc or make.")
580 @param("identifier", "If the action is calc, then this identifier should be the first rule in the chain. If this is make, then supply '-' to read from stdin")
581 @param("post actions", "The action to perform on the created supply chain. See commands below")
582 @arg("-e", "--env", "(calc only) environment to do the calculation on")
583 @arg("~", "--diff", "(post action) Use as `--diff [env]`. List all files with differences on the given env.")
584 @arg("~", "--to", "(post action) Use as `--to [env]`. Upload all objects.")
585 @arg("~", "--delete", "(post action) Use as `--delete [env]`. The reverse of uploading. Only presets are supported right now.")
586 async supply(args){
587 return subCommand(supplysub)(args);
588 },
589
590 @helpText(`tags stuff`)
591 @usage(`rally tags [action]`)
592 @param("action", "The action to perform. Can be list or create.")
593 @arg("-e", "--env", "The enviornment you wish to perform the action on")
594 async tag(args){
595 return subCommand(tagsub)(args);
596 },
597
598 @helpText(`print out some trace info`)
599 @usage(`rally trace -e [env] [jobid]`)
600 @param("jobid", "a job id like b86d7d90-f0a5-4622-8754-486ca8e9ecbd")
601 @arg("-e", "--env", "The enviornment you wish to perform the action on")
602 async trace(args){
603 let jobId = args._.shift();
604 if(!jobId) throw new AbortError("No job id");
605 if(!args.env) throw new AbortError("no env");
606 let ln = args._.shift();
607 if(!ln){
608 log("is trace");
609 let traceInfo = await parseTrace(args.env, jobId);
610
611 for(let line of traceInfo){
612 if(typeof(line) == "string"){
613 log(chalk.red(line));
614 }else{
615 printOutLine(line);
616 }
617 }
618 }else{
619 log("is ln");
620 let {renderedPreset} = await getTraceInfo(args.env, jobId);
621 return findLineInFile(renderedPreset, Number(ln));
622 }
623 },
624
625 @helpText(`List all available providers, or find one by name/id`)
626 @usage(`rally providers [identifier] --env [env] --raw`)
627 @param("identifier", "Either the name or id of the provider")
628 @arg("-e", "--env", "The enviornment you wish to perform the action on")
629 @arg("~", "--raw", "Raw output of command. If [identifier] is given, then print editorConfig too")
630 async providers(args){
631 let env = args.env;
632 if(!env) return errorLog("No env supplied.");
633 let ident = args._.shift();
634
635 let providers = await Provider.getAll(env);
636
637 if(ident){
638 let pro = providers.arr.find(x => x.id == ident || x.name.includes(ident));
639 if(!pro){
640 log(chalk`Couldn't find provider by {green ${ident}}`);
641 }else{
642 log(pro.chalkPrint(false));
643 let econfig = await pro.getEditorConfig();
644 if(args.raw){
645 return pro;
646 }else{
647 if(econfig.helpText.length > 100){
648 econfig.helpText = "<too long to display>";
649 }
650 if(econfig.completions.length > 5){
651 econfig.completions = "<too long to display>";
652 }
653 log(econfig);
654 }
655 }
656 }else{
657 if(args.raw) return providers;
658 for(let pro of providers) log(pro.chalkPrint());
659 }
660 },
661
662 @helpText(`Change config for rally tools`)
663 @usage("rally config [key] --set [value] --raw")
664 @param("key", chalk`Key you want to edit. For example, {green chalk} or {green api.DEV}`)
665 @arg("~", "--set", "If this value is given, no interactive prompt will launch and the config option will change.")
666 @arg("~", "--raw", "Raw output of json config")
667 async config(args){
668 let prop = args._.shift();
669 let propArray = prop && prop.split(".");
670
671 //if(!await configHelpers.askQuestion(`Would you like to create a new config file in ${configFile}`)) return;
672 let newConfigObject;
673
674 if(!prop){
675 if(configObject.rawOutput) return configObject;
676 log("Creating new config");
677 newConfigObject = {
678 ...configObject,
679 };
680 for(let helperName in configHelpers){
681 if(helperName.startsWith("$")){
682 newConfigObject = {
683 ...newConfigObject,
684 ...(await configHelpers[helperName](false))
685 }
686 }
687 }
688 }else{
689 log(chalk`Editing option {green ${prop}}`);
690 if(args.set){
691 newConfigObject = {
692 ...configObject,
693 [prop]: args.set,
694 };
695 }else{
696 let ident = "$" + propArray[0];
697
698 if(configHelpers[ident]){
699 newConfigObject = {
700 ...configObject,
701 ...(await configHelpers[ident](propArray))
702 };
703 }else{
704 log(chalk`No helper for {red ${ident}}`);
705 return;
706 }
707 }
708 }
709
710 newConfigObject.hasConfig = true;
711
712 //Create readable json and make sure the user is ok with it
713 let newConfig = JSON.stringify(newConfigObject, null, 4);
714 log(newConfig);
715
716 //-y or --set will make this not prompt
717 if(!args.y && !args.set && !await configHelpers.askQuestion("Write this config to disk?")) return;
718 writeFileSync(configFile, newConfig, {mode: 0o600});
719 log(chalk`Created file {green ${configFile}}.`);
720 },
721
722 @helpText(`create/modify asset`)
723 @usage("rally asset [action] [action...]")
724 @param("action", chalk`Options are create, delete, launch, addfile, metadata, show, patchMetadata, and launchEvalute. You can supply multiple actions to chain them`)
725 @arg(`-i`, `--id`, chalk`MOVIE_ID of asset to select`)
726 @arg(`-n`, `--name`, chalk`MOVIE_NAME of asset. with {white create}, '{white #}' will be replaced with a uuid. Default is '{white TEST_#}'`)
727 @arg(`~`, `--anon`, chalk`Supply this if no asset is needed (used to lauch anonymous workflows)`)
728 @arg(`-j`, `--job-name`, chalk`Job name to start (used with launch and launchEvalute)`)
729 @arg(`~`, `--init-data`, chalk`Init data to use when launching job. can be string, or {white @path/to/file} for a file`)
730 @arg(`~`, `--file-label`, chalk`File label (used with addfile)`)
731 @arg(`~`, `--file-uri`, chalk`File s3 uri. Can use multiple uri's for the same label (used with addfile)`)
732 @arg(`~`, `--metadata`, chalk`Metadata to use with patchMetadata. Can be string, or {white @path/to/file} for a file. Data must contain a top level key Metadata, or Workflow. Metadata will be pached into METADATA. Workflow will be patched into WORKFLOW_METADATA(not currently available)`)
733 @arg(`~`, `--priority`, chalk`set the priority of all launched jobs`)
734 @arg(`~`, `--new-name`, chalk`set the new name`)
735 async asset(args){
736 function uuid(args){
737 const digits = 16;
738 return String(Math.floor(Math.random() * Math.pow(10, digits))).padStart(digits, "0");
739 }
740
741 let name = args.name || `TEST_#`;
742 let env = args.env;
743
744 let asset;
745 let arg = args._.shift()
746 if(!arg){
747 throw new AbortError(chalk`Missing arguments: see {white 'rally help asset'}`);
748 }
749
750 if(args.anon){
751 args._.unshift(arg);
752 }else if(arg == "create"){
753 name = name.replace("#", uuid());
754 asset = await Asset.createNew(name, env);
755 }else{
756 args._.unshift(arg);
757 if(args.id){
758 asset = Asset.lite(args.id, env);
759 }else{
760 asset = await Asset.getByName(env, args.name);
761 }
762 }
763
764 if(!asset && !args.anon){
765 throw new AbortError("No asset found/created");
766 }
767 let launchArg = 0;
768 let fileArg = 0;
769
770 let arrayify = (obj, i) => Array.isArray(obj) ? obj[i] : (i == 0 ? obj : undefined);
771
772 while(arg = args._.shift()){
773
774 if(arg === "launch"){
775 let initData = arrayify(args["init-data"], launchArg);
776 if(initData && initData.startsWith("@")){
777 log(chalk`Reading init data from {white ${initData.slice(1)}}`);
778 initData = readFileSync(initData.slice(1), "utf-8");
779 }
780
781 let jobName = arrayify(args["job-name"], launchArg);
782 let p = await Rule.getByName(env, jobName);
783 if(!p){
784 throw new AbortError(`Cannot launch job ${jobName}, does not exist (?)`);
785 }else{
786 log(chalk`Launching ${p.chalkPrint(false)} on ${asset ? asset.chalkPrint(false) : "(None)"}`);
787 }
788
789 if(asset){
790 await asset.startWorkflow(jobName, {initData, priority: args.priority})
791 }else{
792 await Asset.startAnonWorkflow(env, jobName, {initData, priority: args.priority})
793 }
794 launchArg++;
795 }else if(arg === "launchEvaluate"){
796 let initData = arrayify(args["init-data"], launchArg);
797 if(initData && initData.startsWith("@")){
798 log(chalk`Reading init data from {white ${initData.slice(1)}}`);
799 initData = readFileSync(initData.slice(1), "utf-8");
800 }
801
802 let jobName = arrayify(args["job-name"], launchArg);
803 let jobData;
804 let ephemeralEval = false;
805 let p;
806 if(jobName.startsWith("@")){
807 ephemeralEval = true;
808 jobData = readFileSync(jobName.slice(1));
809 }else{
810 p = await Preset.getByName(env, jobName);
811 if(!p){
812 throw new AbortError(`Cannot launch preset ${jobName}, does not exist (?)`);
813 }else{
814 log(chalk`Launching ${p.chalkPrint(false)} on ${asset ? asset.chalkPrint(false) : "(None)"}`);
815 }
816 }
817
818
819 if(ephemeralEval){
820 await Asset.startEphemeralEvaluateIdeal(env, p.id, initData)
821 }else{
822 await asset.startEvaluate(p.id, initData)
823 }
824 launchArg++;
825 }else if(arg === "addfile"){
826 let label = arrayify(args["file-label"], fileArg)
827 let uri = arrayify(args["file-uri"], fileArg)
828 if(label === undefined || !uri){
829 throw new AbortError("Number of file-label and file-uri does not match");
830 }
831 await asset.addFile(label, uri);
832 log(chalk`Added file ${label}`);
833 fileArg++;
834 }else if(arg === "delete"){
835 await asset.delete();
836 }else if(arg === "create"){
837 throw new AbortError(`Cannot have more than 1 create/get per asset call`);
838 }else if(arg === "show" || arg == "load"){
839 if(asset.lite) asset = await Asset.getById(env, asset.id);
840 if(arg == "show") log(asset);
841 }else if(arg === "metadata" || arg === "md"){
842 log(await asset.getMetadata());
843 }else if(arg === "patchMetadata"){
844 let initData = arrayify(args["metadata"], launchArg);
845 if(initData && initData.startsWith("@")){
846 log(chalk`Reading data from {white ${initData.slice(1)}}`);
847 initData = readFileSync(initData.slice(1), "utf-8");
848 }
849
850 initData = JSON.parse(initData);
851
852 await asset.patchMetadata(initData);
853 }else if(arg === "rename"){
854 let newName = args["new-name"];
855 let oldName = asset.name;
856 await asset.rename(newName);
857 log(chalk`Rename: {green ${oldName}} -> {green ${newName}}`);
858 }
859 }
860 if(configObject.rawOutput) return asset;
861 },
862
863 async checkSegments(args){
864 let asset = args._.shift()
865 let res = await allIndexBundle.lib.makeAPIRequest({
866 env: args.env, path: `/movies/${asset}/metadata/Metadata`,
867 });
868 let segments = res.data.attributes.metadata.userMetaData.segments.segments;
869
870 let r = segments.reduce((lastSegment, val, ind) => {
871 let curSegment = val.startTime;
872 if(curSegment < lastSegment){
873 log("bad segment " + (ind + 1))
874 }
875 return val.endTime
876 }, "00:00:00:00");
877 },
878
879 async getJobs(args){
880 let req = await allIndexBundle.lib.indexPathFast({
881 env: args.env, path: "/jobs",
882 qs: {
883 filter: "presetName=DCTC Add Element US Checkin",
884 },
885 });
886
887 log(req.map(x => x.relationships.asset.data.id).join("\n"))
888 },
889
890 async listAssets(args, tag){
891 let req = await allIndexBundle.lib.indexPathFast({
892 env: args.env, path: "/assets",
893 qs: {
894 noRelationships: true,
895 sort: "id",
896 },
897 chunksize: 30,
898 });
899 for(let asset of req){
900 log(asset.id);
901 }
902
903 return req;
904 },
905
906 async listSegments(args){
907 let f = JSON.parse(readFileSync(args.file, "utf-8"));
908
909 for(let asset of f){
910 let r = await allIndexBundle.lib.makeAPIRequest({
911 env: args.env, path: `/movies/${asset.id}/metadata/Metadata`,
912 });
913
914 let segs = r.data.attributes.metadata.userMetaData?.segments?.segments;
915 if(segs && segs.length > 1){
916 log(asset.id);
917 log(asset.name);
918 }
919 }
920 },
921 async test4(args){
922 let things = await allIndexBundle.lib.indexPathFast({
923 env: args.env, path: "/assets",
924 qs: {
925 filter: `createdBefore=${Date.now() - 1000 * 160 * 24 * 60 * 60},createdSince=${Date.now() - 1000 * 170 * 24 * 60 * 60}`
926 }
927 });
928
929 log(JSON.stringify(things, null, 4));
930 },
931
932 async test5(args){
933 let things = await allIndexBundle.lib.indexPathFast({
934 env: args.env, path: "/jobs",
935 qs: {
936 filter: `state=Queued,presetName=E2 P4101 - DNE Compliance Edit - US Output Deal - Edit WorkOrder`
937 }
938 });
939
940 log(JSON.stringify(things, null, 4));
941 },
942 async test2(args){
943 let wfr = await allIndexBundle.lib.indexPath({
944 env: args.env, path: "/workflowRuleMetadata",
945 });
946 log(wfr);
947
948 for(let wfrm of wfr){
949 try{
950 wfrm.id = undefined;
951 wfrm.links = undefined;
952 log(wfrm);
953 let req = await allIndexBundle.lib.makeAPIRequest({
954 env: "DEV", path: "/workflowRuleMetadata",
955 method: "POST",
956 payload: {data: wfrm},
957 })
958 }catch(e){
959 log("caught");
960 }
961 //break;
962 }
963 },
964
965 async test3(args){
966 let wfr = await allIndexBundle.lib.indexPath({
967 env: args.env, path: "/workflowRuleMetadata",
968 });
969 log(wfr);
970
971 for(let wfrm of wfr){
972 try{
973 wfrm.id = undefined;
974 wfrm.links = undefined;
975 log(wfrm);
976 let req = await allIndexBundle.lib.makeAPIRequest({
977 env: "DEV", path: "/workflowRuleMetadata",
978 method: "POST",
979 payload: {data: wfrm},
980 })
981 }catch(e){
982 log("caught");
983 }
984 //break;
985 }
986 },
987
988 async deleteOmneons(args){
989 let id = args._.shift();
990
991 let asset;
992 if(Number(id)) {
993 asset = await Asset.getById("PROD", Number(id));
994 }else{
995 asset = await Asset.getByName("PROD", id);
996 }
997
998 log(asset);
999 let f = await asset.getFiles();
1000
1001 for(let file of f){
1002 if(file.label.includes("Omneon")){
1003 log(`removing ${file.label}`);
1004 await file.delete();
1005 }
1006 }
1007 },
1008
1009 async audit(args){
1010 let supportedAudits = ["presets", "rule", "other"];
1011 await configHelpers.addAutoCompletePrompt();
1012 let q = await configHelpers.inquirer.prompt([{
1013 type: "autocomplete", name: "obj",
1014 message: `What audit do you want?`,
1015 source: async (sofar, input) => {
1016 return supportedAudits.filter(x => input ? x.includes(input.toLowerCase()) : true);
1017 },
1018 }]);
1019 let choice = q.obj;
1020 let resourceId = undefined
1021 let filterFunc = _=>_;
1022 if(choice === "presets"){
1023 let preset = await configHelpers.selectPreset({canSelectNone: false});
1024 let remote = await Preset.getByName(args.env, preset.name);
1025 if(!remote) throw new AbortError("Could not find this item on remote env");
1026 filterFunc = ev => ev.resource == "Preset";
1027 resourceId = remote.id;
1028 }else if(choice === "rule"){
1029 let preset = await configHelpers.selectRule({canSelectNone: false});
1030 let remote = await Rule.getByName(args.env, preset.name);
1031 if(!remote) throw new AbortError("Could not find this item on remote env");
1032 filterFunc = ev => ev.resource == "Rule";
1033 resourceId = remote.id;
1034 }else{
1035 resourceId = await configHelpers.askInput(null, "What resourceID?");
1036 }
1037
1038 log(chalk`Resource ID on {blue ${args.env}} is {yellow ${resourceId}}`);
1039 log(`Loading audits (this might take a while)`);
1040 const numRows = 100;
1041 let r = await allIndexBundle.lib.makeAPIRequest({
1042 env: args.env,
1043 path: `/v1.0/audit?perPage=${numRows}&count=${numRows}&filter=%7B%22resourceId%22%3A%22${resourceId}%22%7D&autoload=false&pageNum=1&include=`,
1044 timeout: 180000,
1045 });
1046 r.data = r.data.filter(filterFunc);
1047
1048 log("Data recieved, parsing users");
1049
1050 for(let event of r.data){
1051 let uid = event?.correlation?.userId;
1052 if(!uid) continue;
1053 try{
1054 event.user = await User.getById(args.env, uid);
1055 }catch(e){
1056 event.user = {name: "????"};
1057 }
1058 }
1059
1060 if(args.raw) return r.data;
1061 let evCounter = 0;
1062 for(let event of r.data){
1063 let evtime = moment(event.createdAt);
1064 let date = evtime.format("ddd YYYY/MM/DD hh:mm:ssa");
1065 let timedist = evtime.fromNow();
1066 log(chalk`${date} {yellow ${timedist}} {green ${event.user?.name}} ${event.event}`);
1067
1068 if(++evCounter >= 30) break;
1069 }
1070 },
1071
1072 async audit2(args){
1073 const numRows = 1000
1074 let r = await allIndexBundle.lib.makeAPIRequest({
1075 env: args.env,
1076 //path: `/v1.0/audit?perPage=${numRows}&count=${numRows}&autoload=false&pageNum=1&include=`,
1077 path: `/v1.0/audit?perPage=${numRows}&count=${numRows}&filter=%7B%22correlation.userId%22%3A%5B%22164%22%5D%7D&autoload=false&pageNum=1&include=`,
1078 timeout: 60000,
1079 });
1080 for(let event of r.data){
1081 log(event.event);
1082 }
1083 },
1084
1085 async findIDs(args){
1086 let files = await getFilesFromArgs(args);
1087 for(let file of files){
1088 let preset = await Preset.getByName(args.env, file);
1089 await preset.resolve();
1090 log(`silo-presets/${file}.${preset.ext}`);
1091 }
1092 },
1093
1094 async getAssets(env, name){
1095 if(!this.callid) this.callid = 0;
1096 this.callid++;
1097 let callid = this.callid;
1098
1099 await allIndexBundle.sleep(500);
1100
1101 if(callid != this.callid) return this.lastResult || [];
1102
1103 let req = await allIndexBundle.lib.makeAPIRequest({
1104 env, path: `/assets`,
1105 qs: name ? {filter: `nameContains=${name}`} : undefined,
1106 })
1107 this.lastCall = Date.now();
1108
1109 return this.lastResult = req.data;
1110 },
1111
1112 async content(args){
1113 configHelpers.addAutoCompletePrompt();
1114 let q = await configHelpers.inquirer.prompt([{
1115 type: "autocomplete",
1116 name: "what",
1117 message: `What asset do you want?`,
1118 source: async (sofar, input) => {
1119 let assets = await this.getAssets(args.env, input);
1120 assets = assets.map(x => new Asset({data: x, remote: args.env}));
1121 return assets.map(x => ({
1122 name: x.chalkPrint(true) + ": " + x.data.links.self.replace("/api/v2/assets/", "/content/"),
1123 value: x,
1124 }));
1125 },
1126 }]);
1127 },
1128
1129 async ["@"](args){
1130 args._.unshift("-");
1131 args._.unshift("make");
1132 return this.supply(args);
1133 },
1134
1135 async test(args){
1136 let asset = await Asset.getByName("UAT", args.name);
1137 log(asset);
1138 },
1139
1140 //Used to test startup and teardown speed.
1141 noop(){
1142 return true;
1143 },
1144};
1145async function unknownCommand(cmd){
1146 log(chalk`Unknown command {red ${cmd}}.`);
1147}
1148
1149async function noCommand(){
1150 write(chalk`
1151Rally Tools {yellow v${packageVersion} (alpha)} CLI
1152by John Schmidt <John_Schmidt@discovery.com>
1153`);
1154
1155 //Prompt users to setup one time config.
1156 if(!configObject.hasConfig){
1157 write(chalk`
1158It looks like you haven't setup the config yet. Please run '{green rally config}'.
1159`);
1160 return;
1161 }
1162
1163 let envs = new Set(["LOCAL", "UAT", "DEV", "PROD", "QA", ...Object.keys(configObject.api)]);
1164
1165 let proms = [];
1166 for(let env of envs){
1167 proms.push({env, prom: funcs.testAccess(env)});
1168 }
1169
1170 //API Access tests
1171 for(let {env, prom} of proms){
1172 //Test access. Returns HTTP response code
1173 let resultStr;
1174 try{
1175
1176 let result = await prom;
1177
1178 //Create a colored display and response
1179 resultStr = chalk`{yellow ${result} <unknown>}`;
1180 if(result === 200) resultStr = chalk`{green 200 OK}`;
1181 else if(result === 401) resultStr = chalk`{red 401 No Access}`;
1182 else if(result >= 500) resultStr = chalk`{yellow ${result} API Down?}`;
1183 else if(result === true) resultStr = chalk`{green OK}`;
1184 else if(result === false) resultStr = chalk`{red BAD}`;
1185 }catch(e){
1186 if(e instanceof UnconfiguredEnvError){
1187 resultStr = chalk`{yellow Unconfigured}`;
1188 }else if(e instanceof APIError){
1189 if(!e.response.body){
1190 resultStr = chalk`{red Timeout (?)}`;
1191 }
1192 }else if(e.name == "RequestError"){
1193 resultStr = chalk`{red Low level error (check internet): ${e.error.errno}}`;
1194 }else{
1195 throw e;
1196 }
1197 }
1198
1199 log(chalk` ${env}: ${resultStr}`);
1200 }
1201}
1202
1203async function $main(){
1204 //Supply --config to load a different config file
1205 if(typeof(argv.config) === "string"){
1206 loadConfig(argv.config);
1207 }else if(typeof(argv.config) === "object") {
1208 loadConfigFromArgs(argv);
1209 }else{
1210 loadConfig();
1211 }
1212
1213 // First we need to decide if the user wants color or not. If they do want
1214 // color, we need to make sure we use the right mode
1215 chalk.enabled = configObject.hasConfig ? configObject.chalk : true;
1216 if(chalk.level === 0 || !chalk.enabled){
1217 let force = argv["force-color"];
1218 if(force){
1219 chalk.enabled = true;
1220 if(force === true && chalk.level === 0){
1221 chalk.level = 1;
1222 }else if(Number(force)){
1223 chalk.level = Number(force);
1224 }
1225 }
1226 }
1227
1228 //This flag being true allows you to modify UAT and PROD
1229 if(!argv["protect"]){
1230 configObject.dangerModify = true;
1231 }
1232
1233 //This enables raw output for some functions
1234 if(argv["raw"]){
1235 configObject.rawOutput = true;
1236 global.log = ()=>{};
1237 global.errorLog = ()=>{};
1238 global.write = ()=>{};
1239 }
1240
1241 if(argv["ignore-missing"]){
1242 configObject.ignoreMissing = true;
1243 }
1244
1245 if(argv["update-immutable"]){
1246 configObject.updateImmutable = true;
1247 }
1248
1249 if(argv["skip-header"]){
1250 configObject.skipHeader = true;
1251 }
1252
1253 configObject.globalProgress = !argv["hide-progress"];
1254
1255 //Default enviornment should normally be from config, but it can be
1256 //overridden by the -e/--env flag
1257 if(configObject.defaultEnv){
1258 argv.env = argv.env || configObject.defaultEnv;
1259 }
1260
1261 //Enable verbose logging in some places.
1262 if(argv["vverbose"]){
1263 configObject.verbose = argv["vverbose"];
1264 configObject.vverbose = true;
1265 }else if(argv["verbose"]){
1266 configObject.verbose = argv["verbose"]
1267 }else if(argv["vvverbose"]){
1268 configObject.verbose = true;
1269 configObject.vverbose = true;
1270 configObject.vvverbose = true;
1271 }
1272
1273 //copy argument array to new object to allow modification
1274 argv._old = argv._.slice();
1275
1276 //Take first argument after `node bundle.js`
1277 //If there is no argument, display the default version info and API access.
1278 let func = argv._.shift();
1279 if(func){
1280 if(!cli[func]) return await unknownCommand(func);
1281 try{
1282 //Call the cli function
1283 let ret = await cli[func](argv);
1284 if(ret){
1285 write(chalk.white("CLI returned: "));
1286 if(ret instanceof Collection) ret = ret.arr;
1287
1288 //Directly use console.log so that --raw works as intended.
1289 if(typeof ret === "object"){
1290 console.log(JSON.stringify(ret, null, 4));
1291 }else{
1292 console.log(ret);
1293 }
1294 }
1295 }catch(e){
1296 if(e instanceof AbortError){
1297 log(chalk`{red CLI Aborted}: ${e.message}`);
1298 }else{
1299 throw e;
1300 }
1301 }
1302 }else{
1303 await noCommand();
1304 }
1305}
1306
1307async function main(...args){
1308 //Catch all for errors to avoid ugly default node promise catcher
1309 try{
1310 await $main(...args);
1311 }catch(e){
1312 errorLog(e.stack);
1313 }
1314}
1315
1316// If this is an imported module, then we should exec the cli interface.
1317// Oterwise just export everything.
1318if(require.main === module){
1319 main();
1320}else{
1321 loadConfig();
1322 module.exports = allIndexBundle;
1323}