1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | const path = require('path');
|
14 | const fs = require('fs');
|
15 | const fse = require('fs-extra');
|
16 | const archiver = require('archiver');
|
17 | const webpack = require('webpack');
|
18 | const chalk = require('chalk');
|
19 | const dotenv = require('dotenv');
|
20 | const os = require('os');
|
21 | const ow = require('openwhisk');
|
22 | const semver = require('semver');
|
23 | const fetchAPI = require('@adobe/helix-fetch');
|
24 | const git = require('isomorphic-git');
|
25 | const { version } = require('../package.json');
|
26 |
|
27 | require('dotenv').config();
|
28 |
|
29 | const { fetch } = process.env.HELIX_FETCH_FORCE_HTTP1
|
30 | ? fetchAPI.context({
|
31 | httpProtocol: 'http1',
|
32 | httpsProtocols: ['http1'],
|
33 | })
|
34 | : fetchAPI;
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | async function getOrigin(dir) {
|
43 | try {
|
44 | const rmt = (await git.listRemotes({ fs, dir })).find((entry) => entry.remote === 'origin');
|
45 | return typeof rmt === 'object' ? rmt.url : '';
|
46 | } catch (e) {
|
47 |
|
48 | return '';
|
49 | }
|
50 | }
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 | async function getCurrentRevision(dir) {
|
59 | try {
|
60 | return await git.resolveRef({ fs, dir, ref: 'HEAD' });
|
61 | } catch (e) {
|
62 |
|
63 | return '';
|
64 | }
|
65 | }
|
66 |
|
67 | module.exports = class ActionBuilder {
|
68 | |
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 | decodeParams(params, isFile, warnError) {
|
76 | let content = params;
|
77 | let cwd = this._cwd;
|
78 | if (isFile) {
|
79 | if (!fse.existsSync(params)) {
|
80 | if (warnError) {
|
81 | this.log.info(chalk`{yellow warn:} specified param file does not exist: ${params}`);
|
82 | return {};
|
83 | }
|
84 | throw Error(`Specified param file does not exist: ${params}`);
|
85 | }
|
86 | content = fse.readFileSync(params, 'utf-8');
|
87 | cwd = path.dirname(params);
|
88 | }
|
89 | let data;
|
90 | if (typeof params === 'object') {
|
91 | data = content;
|
92 | } else {
|
93 |
|
94 | try {
|
95 | data = JSON.parse(content);
|
96 | } catch (e) {
|
97 |
|
98 | data = dotenv.parse(content);
|
99 | }
|
100 | }
|
101 |
|
102 | const resolve = (obj) => {
|
103 |
|
104 | Object.keys(obj).forEach((key) => {
|
105 | const value = obj[key];
|
106 | if (typeof value === 'object') {
|
107 | resolve(value);
|
108 | } else {
|
109 | const param = String(value);
|
110 | if (param.startsWith('@') && !param.startsWith('@@')) {
|
111 | const filePath = path.resolve(cwd, param.substring(1));
|
112 |
|
113 | obj[key] = `@${filePath}`;
|
114 | }
|
115 | }
|
116 | });
|
117 | };
|
118 | resolve(data);
|
119 | return data;
|
120 | }
|
121 |
|
122 | |
123 |
|
124 |
|
125 |
|
126 |
|
127 | static async resolveParams(params) {
|
128 | const tasks = [];
|
129 | const resolve = async (obj, key, file) => {
|
130 |
|
131 | obj[key] = await fse.readFile(file, 'utf-8');
|
132 | };
|
133 |
|
134 | const resolver = (obj) => {
|
135 | Object.keys(obj).forEach((key) => {
|
136 | const param = obj[key];
|
137 | if (typeof param === 'object') {
|
138 | resolver(param);
|
139 | } else {
|
140 | const value = String(param);
|
141 | if (value.startsWith('@@')) {
|
142 |
|
143 | obj[key] = value.substring(1);
|
144 | } else if (value.startsWith('@')) {
|
145 | tasks.push(resolve(obj, key, value.substring(1)));
|
146 | } else {
|
147 |
|
148 | obj[key] = value;
|
149 | }
|
150 | }
|
151 | });
|
152 | };
|
153 | resolver(params);
|
154 | await Promise.all(tasks);
|
155 | return params;
|
156 | }
|
157 |
|
158 | |
159 |
|
160 |
|
161 |
|
162 |
|
163 | static toEnv(obj) {
|
164 | let str = '';
|
165 | Object.keys(obj).forEach((k) => {
|
166 | str += `${k}=${JSON.stringify(obj[k])}\n`;
|
167 | });
|
168 | return str;
|
169 | }
|
170 |
|
171 | constructor() {
|
172 | Object.assign(this, {
|
173 | _cwd: process.cwd(),
|
174 | _distDir: null,
|
175 | _name: null,
|
176 | _namespace: null,
|
177 | _version: null,
|
178 | _file: null,
|
179 | _zipFile: null,
|
180 | _bundle: null,
|
181 | _env: null,
|
182 | _wskNamespace: null,
|
183 | _wskAuth: null,
|
184 | _wskApiHost: null,
|
185 | _verbose: false,
|
186 | _externals: [],
|
187 | _docker: null,
|
188 | _kind: null,
|
189 | _deploy: false,
|
190 | _test: null,
|
191 | _test_params: {},
|
192 | _statics: [],
|
193 | _params: {},
|
194 | _webAction: true,
|
195 | _webSecure: false,
|
196 | _rawHttp: false,
|
197 | _showHints: false,
|
198 | _modules: [],
|
199 | _build: true,
|
200 | _delete: false,
|
201 | _updatePackage: false,
|
202 | _actionName: '',
|
203 | _packageName: '',
|
204 | _packageShared: false,
|
205 | _packageParams: {},
|
206 | _timeout: 60000,
|
207 | _concurrency: null,
|
208 | _memory: null,
|
209 | _links: [],
|
210 | _linksPackage: null,
|
211 | _dependencies: {},
|
212 | _gitUrl: '',
|
213 | _gitOrigin: '',
|
214 | _gitRef: '',
|
215 | _updatedAt: null,
|
216 | _updatedBy: null,
|
217 | });
|
218 | }
|
219 |
|
220 | get log() {
|
221 | if (!this._logger) {
|
222 |
|
223 |
|
224 | this._logger = {
|
225 | debug: (...args) => { if (this._verbose) { console.error(...args); } },
|
226 | info: console.error,
|
227 | warn: console.error,
|
228 | error: console.error,
|
229 | };
|
230 |
|
231 | }
|
232 | return this._logger;
|
233 | }
|
234 |
|
235 | verbose(enable) {
|
236 | this._verbose = enable;
|
237 | return this;
|
238 | }
|
239 |
|
240 | withDirectory(value) {
|
241 | this._cwd = value === '.' ? process.cwd() : value;
|
242 | return this;
|
243 | }
|
244 |
|
245 | withDeploy(enable) {
|
246 | this._deploy = enable;
|
247 | return this;
|
248 | }
|
249 |
|
250 | withBuild(enable) {
|
251 | this._build = enable;
|
252 | return this;
|
253 | }
|
254 |
|
255 | withDelete(enable) {
|
256 | this._delete = enable;
|
257 | return this;
|
258 | }
|
259 |
|
260 | withUpdatePackage(enable) {
|
261 | this._updatePackage = enable;
|
262 | return this;
|
263 | }
|
264 |
|
265 | withTest(enable) {
|
266 | this._test = enable;
|
267 | return this;
|
268 | }
|
269 |
|
270 | withTestParams(params) {
|
271 | if (!params) {
|
272 | return this;
|
273 | }
|
274 | if (Array.isArray(params)) {
|
275 | params.forEach((v) => {
|
276 | this._test_params = Object.assign(this._test_params, this.decodeParams(v, false));
|
277 | });
|
278 | } else {
|
279 | this._test_params = Object.assign(this._test_params, this.decodeParams(params, false));
|
280 | }
|
281 | return this;
|
282 | }
|
283 |
|
284 | withHints(showHints) {
|
285 | this._showHints = showHints;
|
286 | return this;
|
287 | }
|
288 |
|
289 | withModules(value) {
|
290 | this._modules = value;
|
291 | return this;
|
292 | }
|
293 |
|
294 | withWebExport(value) {
|
295 | this._webAction = value;
|
296 | return this;
|
297 | }
|
298 |
|
299 | withWebSecure(value) {
|
300 | this._webSecure = value;
|
301 | return this;
|
302 | }
|
303 |
|
304 | withRawHttp(value) {
|
305 | this._rawHttp = value;
|
306 | return this;
|
307 | }
|
308 |
|
309 | withExternals(value) {
|
310 | this._externals = (Array.isArray(value) ? value : [value]).map((e) => {
|
311 | if (typeof e === 'string' && e.startsWith('/') && e.endsWith('/')) {
|
312 | return new RegExp(e.substring(1, e.length - 1));
|
313 | }
|
314 | return e;
|
315 | });
|
316 | return this;
|
317 | }
|
318 |
|
319 | withStatic(srcPath, dstRelPath) {
|
320 | if (!srcPath) {
|
321 | return this;
|
322 | }
|
323 |
|
324 | if (Array.isArray(srcPath)) {
|
325 | srcPath.forEach((v) => {
|
326 | if (Array.isArray(v)) {
|
327 | this._statics.push(v);
|
328 | } else {
|
329 | this._statics.push([v, v]);
|
330 | }
|
331 | });
|
332 | } else {
|
333 | this._statics.push([srcPath, dstRelPath]);
|
334 | }
|
335 | return this;
|
336 | }
|
337 |
|
338 | withParams(params, forceFile) {
|
339 | if (!params) {
|
340 | return this;
|
341 | }
|
342 | if (Array.isArray(params)) {
|
343 | params.forEach((v) => {
|
344 | this._params = Object.assign(this._params, this.decodeParams(v, forceFile));
|
345 | });
|
346 | } else {
|
347 | this._params = Object.assign(this._params, this.decodeParams(params, forceFile));
|
348 | }
|
349 | return this;
|
350 | }
|
351 |
|
352 | withPackageParams(params, forceFile) {
|
353 | if (!params) {
|
354 | return this;
|
355 | }
|
356 | const warnError = !this._updatePackage;
|
357 | if (Array.isArray(params)) {
|
358 | params.forEach((v) => {
|
359 |
|
360 | this._packageParams = Object.assign(this._packageParams, this.decodeParams(v, forceFile, warnError));
|
361 | });
|
362 | } else {
|
363 |
|
364 | this._packageParams = Object.assign(this._packageParams, this.decodeParams(params, forceFile, warnError));
|
365 | }
|
366 | return this;
|
367 | }
|
368 |
|
369 | withParamsFile(params) {
|
370 | return this.withParams(params, true);
|
371 | }
|
372 |
|
373 | withPackageParamsFile(params) {
|
374 | return this.withPackageParams(params, true);
|
375 | }
|
376 |
|
377 | withName(value) {
|
378 | this._name = value;
|
379 | return this;
|
380 | }
|
381 |
|
382 | withNamespace(value) {
|
383 | this._namespace = value;
|
384 | return this;
|
385 | }
|
386 |
|
387 | withVersion(value) {
|
388 | this._version = value;
|
389 | return this;
|
390 | }
|
391 |
|
392 | withKind(value) {
|
393 | this._kind = value;
|
394 | return this;
|
395 | }
|
396 |
|
397 | withDocker(value) {
|
398 | this._docker = value;
|
399 | return this;
|
400 | }
|
401 |
|
402 | withEntryFile(value) {
|
403 | this._file = value;
|
404 | return this;
|
405 | }
|
406 |
|
407 | withPackageShared(value) {
|
408 | this._packageShared = value;
|
409 | return this;
|
410 | }
|
411 |
|
412 | withPackageName(value) {
|
413 | this._packageName = value;
|
414 | return this;
|
415 | }
|
416 |
|
417 | withTimeout(value) {
|
418 | this._timeout = value;
|
419 | return this;
|
420 | }
|
421 |
|
422 | withConcurrency(value) {
|
423 | this._concurrency = value;
|
424 | return this;
|
425 | }
|
426 |
|
427 | withMemory(value) {
|
428 | this._memory = value;
|
429 | return this;
|
430 | }
|
431 |
|
432 | withLinks(value) {
|
433 | this._links = value || [];
|
434 | return this;
|
435 | }
|
436 |
|
437 | withLinksPackage(value) {
|
438 | this._linksPackage = value;
|
439 | return this;
|
440 | }
|
441 |
|
442 | withUpdatedBy(value) {
|
443 | this._updatedBy = value;
|
444 | return this;
|
445 | }
|
446 |
|
447 | withUpdatedAt(value) {
|
448 | this._updatedAt = value;
|
449 | return this;
|
450 | }
|
451 |
|
452 | async initWskProps() {
|
453 | const wskPropsFile = process.env.WSK_CONFIG_FILE || path.resolve(os.homedir(), '.wskprops');
|
454 | let wskProps = {};
|
455 | if (await fse.pathExists(wskPropsFile)) {
|
456 | wskProps = dotenv.parse(await fse.readFile(wskPropsFile));
|
457 | }
|
458 | this._wskNamespace = this._wskNamespace || process.env.WSK_NAMESPACE || wskProps.NAMESPACE;
|
459 | this._wskAuth = this._wskAuth || process.env.WSK_AUTH || wskProps.AUTH;
|
460 | this._wskApiHost = this._wskApiHost || process.env.WSK_APIHOST || wskProps.APIHOST || 'https://adobeioruntime.net';
|
461 | }
|
462 |
|
463 | async validate() {
|
464 | try {
|
465 | this._pkgJson = await fse.readJson(path.resolve(this._cwd, 'package.json'));
|
466 | } catch (e) {
|
467 | this._pkgJson = {};
|
468 | }
|
469 | this._file = path.resolve(this._cwd, this._file || 'index.js');
|
470 | if (!this._env) {
|
471 | this._env = path.resolve(this._cwd, '.env');
|
472 | }
|
473 | if (!this._distDir) {
|
474 | this._distDir = path.resolve(this._cwd, 'dist');
|
475 | }
|
476 | if (!this._name) {
|
477 | this._name = this._pkgJson.name || path.basename(this._cwd);
|
478 | }
|
479 | if (!this._version) {
|
480 | this._version = this._pkgJson.version || '0.0.0';
|
481 | }
|
482 |
|
483 |
|
484 | this._name = this._name.replace('${version}', this._version);
|
485 |
|
486 | const segs = this._name.split('/');
|
487 | this._name = segs.pop();
|
488 | if (segs.length > 0 && !this._packageName) {
|
489 | this._packageName = segs.pop();
|
490 | }
|
491 | this._actionName = `${this._packageName}/${this._name}`;
|
492 | if (!this._packageName) {
|
493 | this._packageName = 'default';
|
494 | this._actionName = this._name;
|
495 | }
|
496 | if (!this._linksPackage) {
|
497 | this._linksPackage = this._packageName;
|
498 | }
|
499 |
|
500 | if (!this._zipFile) {
|
501 | this._zipFile = path.resolve(this._distDir, this._packageName, `${this._name}.zip`);
|
502 | }
|
503 | if (!this._bundle) {
|
504 | this._bundle = path.resolve(this._distDir, this._packageName, `${this._name}-bundle.js`);
|
505 | }
|
506 |
|
507 |
|
508 | await fse.ensureDir(this._distDir);
|
509 |
|
510 |
|
511 | await this.initWskProps();
|
512 |
|
513 | if (this._rawHttp && !this._webAction) {
|
514 | throw new Error('raw-http requires web-export');
|
515 | }
|
516 | this._params = await ActionBuilder.resolveParams(this._params);
|
517 | this._packageParams = await ActionBuilder.resolveParams(this._packageParams);
|
518 | this._fqn = `/${this._wskNamespace}/${this._packageName}/${this._name}`;
|
519 |
|
520 |
|
521 | this._gitUrl = (this._pkgJson.repository || {}).url || '';
|
522 | this._gitRef = await getCurrentRevision(this._cwd);
|
523 | this._gitOrigin = await getOrigin(this._cwd);
|
524 |
|
525 |
|
526 | if (!this._updatedAt) {
|
527 | this._updatedAt = new Date().getTime();
|
528 | }
|
529 | if (this._delete) {
|
530 | this._deploy = false;
|
531 | this._build = false;
|
532 | this._showHints = false;
|
533 | this._links = [];
|
534 | }
|
535 | }
|
536 |
|
537 | async createArchive() {
|
538 | return new Promise((resolve, reject) => {
|
539 |
|
540 | const output = fse.createWriteStream(this._zipFile);
|
541 | const archive = archiver('zip');
|
542 | this.log.info('--: creating zip file ...');
|
543 |
|
544 | let hadErrors = false;
|
545 | output.on('close', () => {
|
546 | if (!hadErrors) {
|
547 | this.log.debug(' %d total bytes', archive.pointer());
|
548 | resolve();
|
549 | }
|
550 | });
|
551 | archive.on('entry', (data) => {
|
552 | this.log.debug(' - %s', data.name);
|
553 | });
|
554 | archive.on('warning', (err) => {
|
555 | hadErrors = true;
|
556 | reject(err);
|
557 | });
|
558 | archive.on('error', (err) => {
|
559 | hadErrors = true;
|
560 | reject(err);
|
561 | });
|
562 |
|
563 | const packageJson = {
|
564 | name: this._actionName,
|
565 | version: this._version,
|
566 | description: `OpenWhisk Action of ${this._name}`,
|
567 | main: 'main.js',
|
568 | license: 'Apache-2.0',
|
569 | };
|
570 |
|
571 | archive.pipe(output);
|
572 | this.updateArchive(archive, packageJson).then(() => {
|
573 | archive.finalize();
|
574 | });
|
575 | });
|
576 | }
|
577 |
|
578 | async updateArchive(archive, packageJson) {
|
579 | archive.file(this._bundle, { name: 'main.js' });
|
580 | this._statics.forEach(([src, name]) => {
|
581 | if (fse.lstatSync(src).isDirectory()) {
|
582 | archive.directory(src, name);
|
583 | } else {
|
584 | archive.file(src, { name });
|
585 | }
|
586 | });
|
587 | this._modules.forEach((mod) => {
|
588 | archive.directory(path.resolve(this._cwd, `node_modules/${mod}`), `node_modules/${mod}`);
|
589 | });
|
590 | archive.append(JSON.stringify(packageJson, null, ' '), { name: 'package.json' });
|
591 | }
|
592 |
|
593 | async getWebpackConfig() {
|
594 | return {
|
595 | target: 'node',
|
596 | mode: 'development',
|
597 | entry: this._file,
|
598 | output: {
|
599 | path: this._cwd,
|
600 | filename: path.relative(this._cwd, this._bundle),
|
601 | library: 'main',
|
602 | libraryTarget: 'umd',
|
603 | },
|
604 | devtool: false,
|
605 | externals: this._externals,
|
606 | module: {
|
607 | rules: [{
|
608 | test: /\.mjs$/,
|
609 | type: 'javascript/auto',
|
610 | }],
|
611 | },
|
612 | resolve: {
|
613 | mainFields: ['main', 'module'],
|
614 | extensions: ['.wasm', '.js', '.mjs', '.json'],
|
615 | },
|
616 | node: {
|
617 | __dirname: true,
|
618 | },
|
619 | };
|
620 | }
|
621 |
|
622 | async createPackage() {
|
623 | this.log.info('--: creating bundle ...');
|
624 | const config = await this.getWebpackConfig();
|
625 | const compiler = webpack(config);
|
626 | const stats = await new Promise((resolve, reject) => {
|
627 | compiler.run((err, s) => {
|
628 | if (err) {
|
629 | reject(err);
|
630 | } else {
|
631 | resolve(s);
|
632 | }
|
633 | });
|
634 | });
|
635 | this.log.debug(stats.toString({
|
636 | chunks: false,
|
637 | colors: true,
|
638 | }));
|
639 |
|
640 | await this.resolveDependencyInfos(stats);
|
641 | this.log.info(chalk`{green ok:} created bundle {yellow ${config.output.filename}}`);
|
642 | }
|
643 |
|
644 | |
645 |
|
646 |
|
647 |
|
648 |
|
649 |
|
650 |
|
651 |
|
652 |
|
653 |
|
654 |
|
655 |
|
656 | async resolveDependencyInfos(stats) {
|
657 |
|
658 | const depsByFile = {};
|
659 | const resolved = {};
|
660 |
|
661 | const jsonStats = stats.toJson({
|
662 | chunks: true,
|
663 | chunkModules: true,
|
664 | });
|
665 |
|
666 | await Promise.all(jsonStats.chunks
|
667 | .map(async (chunk) => {
|
668 | const chunkName = chunk.names[0];
|
669 | const deps = {};
|
670 | depsByFile[chunkName] = deps;
|
671 |
|
672 | await Promise.all(chunk.modules.map(async (mod) => {
|
673 | const segs = mod.identifier.split('/');
|
674 | let idx = segs.lastIndexOf('node_modules');
|
675 | if (idx >= 0) {
|
676 | idx += 1;
|
677 | if (segs[idx].charAt(0) === '@') {
|
678 | idx += 1;
|
679 | }
|
680 | segs.splice(idx + 1);
|
681 | const dir = path.resolve('/', ...segs);
|
682 |
|
683 | try {
|
684 | if (!resolved[dir]) {
|
685 | const pkgJson = await fse.readJson(path.resolve(dir, 'package.json'));
|
686 | const id = `${pkgJson.name}:${pkgJson.version}`;
|
687 | resolved[dir] = {
|
688 | id,
|
689 | name: pkgJson.name,
|
690 | version: pkgJson.version,
|
691 | };
|
692 | }
|
693 | const dep = resolved[dir];
|
694 | deps[dep.id] = dep;
|
695 | } catch (e) {
|
696 |
|
697 | }
|
698 | }
|
699 | }));
|
700 | }));
|
701 |
|
702 |
|
703 | Object.entries(depsByFile)
|
704 | .forEach(([scriptFile, deps]) => {
|
705 | this._dependencies[scriptFile] = Object.values(deps)
|
706 | .sort((d0, d1) => d0.name.localeCompare(d1.name));
|
707 | });
|
708 | }
|
709 |
|
710 | async validateBundle() {
|
711 | this.log.info('--: validating bundle ...');
|
712 | let module;
|
713 | try {
|
714 |
|
715 | module = require(this._bundle);
|
716 | } catch (e) {
|
717 | this.log.error(chalk`{red error:}`, e);
|
718 | throw Error(`Validation failed: ${e}`);
|
719 | }
|
720 | if (!module.main && typeof module.main !== 'function') {
|
721 | throw Error('Validation failed: Action has no main() function.');
|
722 | }
|
723 | this.log.info(chalk`{green ok:} bundle can be loaded and has a {gray main()} function.`);
|
724 | }
|
725 |
|
726 | getOpenwhiskClient() {
|
727 | if (!this._wskApiHost || !this._wskAuth || !this._wskNamespace) {
|
728 | throw Error(chalk`\nMissing OpenWhisk credentials. Make sure you have a {grey .wskprops} in your home directory.\nYou can also set {grey WSK_NAMESPACE}, {gray WSK_AUTH} and {gray WSK_API_HOST} environment variables.`);
|
729 | }
|
730 | if (this._namespace && this._namespace !== this._wskNamespace) {
|
731 | throw Error(chalk`Openhwhisk namespace {grey '${this._wskNamespace}'} doesn't match configured namespace {grey '${this._namespace}'}.\nThis is a security measure to prevent accidental deployment dues to wrong .wskprops.`);
|
732 | }
|
733 | return ow({
|
734 | apihost: this._wskApiHost,
|
735 | api_key: this._wskAuth,
|
736 | namespace: this._wskNamespace,
|
737 | });
|
738 | }
|
739 |
|
740 | async deploy() {
|
741 | const openwhisk = this.getOpenwhiskClient();
|
742 | const relZip = path.relative(process.cwd(), this._zipFile);
|
743 | this.log.info(`--: deploying ${relZip} as ${this._actionName} ...`);
|
744 | const actionoptions = {
|
745 | name: this._actionName,
|
746 | action: await fse.readFile(this._zipFile),
|
747 | kind: this._docker ? 'blackbox' : this._kind,
|
748 | annotations: {
|
749 | 'web-export': this._webAction,
|
750 | 'raw-http': this._rawHttp,
|
751 | description: this._pkgJson.description,
|
752 | pkgVersion: this._version,
|
753 | dependencies: this._dependencies.main.map((dep) => `${dep.name}:${dep.version}`).join(','),
|
754 | repository: this._gitUrl,
|
755 | git: `${this._gitOrigin}#${this._gitRef}`,
|
756 | updated: this._updatedAt,
|
757 | },
|
758 | params: this._params,
|
759 | limits: {
|
760 | timeout: this._timeout,
|
761 | },
|
762 | };
|
763 | if (this._docker) {
|
764 | actionoptions.exec = {
|
765 | image: this._docker,
|
766 | };
|
767 | }
|
768 | if (this._webSecure) {
|
769 | actionoptions.annotations['require-whisk-auth'] = this._webSecure;
|
770 | }
|
771 | if (this._updatedBy) {
|
772 | actionoptions.annotations.updatedBy = this._updatedBy;
|
773 | }
|
774 | if (this._memory) {
|
775 | actionoptions.limits.memory = this._memory;
|
776 | }
|
777 | if (this._concurrency) {
|
778 | actionoptions.limits.concurrency = this._concurrency;
|
779 | }
|
780 |
|
781 | await openwhisk.actions.update(actionoptions);
|
782 | this.log.info(chalk`{green ok:} updated action {yellow ${`/${this._wskNamespace}/${this._packageName}/${this._name}`}}`);
|
783 | if (this._showHints) {
|
784 | this.log.info('\nYou can verify the action with:');
|
785 | if (this._webAction) {
|
786 | let opts = '';
|
787 | if (this._webSecure === true) {
|
788 | opts = ' -u "$WSK_AUTH"';
|
789 | } else if (this._webSecure) {
|
790 | opts = ` -H "x-require-whisk-auth: ${this._webSecure}"`;
|
791 | }
|
792 | this.log.info(chalk`{grey $ curl${opts} "${this._wskApiHost}/api/v1/web${this._fqn}"}`);
|
793 | } else {
|
794 | this.log.info(chalk`{grey $ wsk action invoke -r ${this._fqn}}`);
|
795 | }
|
796 | }
|
797 | }
|
798 |
|
799 | async delete() {
|
800 | const openwhisk = this.getOpenwhiskClient();
|
801 | this.log.info('--: deleting action ...');
|
802 | await openwhisk.actions.delete(this._actionName);
|
803 | this.log.info(chalk`{green ok:} deleted action {yellow ${`/${this._wskNamespace}/${this._packageName}/${this._name}`}}`);
|
804 | }
|
805 |
|
806 | async updatePackage() {
|
807 | const openwhisk = this.getOpenwhiskClient();
|
808 | let fn = openwhisk.packages.update.bind(openwhisk.packages);
|
809 | let verb = 'updated';
|
810 | try {
|
811 | await openwhisk.packages.get(this._packageName);
|
812 | } catch (e) {
|
813 | if (e.statusCode === 404) {
|
814 | fn = openwhisk.packages.create.bind(openwhisk.packages);
|
815 | verb = 'created';
|
816 | } else {
|
817 | this.log.error(`${chalk.red('error: ')} ${e.message}`);
|
818 | }
|
819 | }
|
820 |
|
821 | try {
|
822 | const parameters = Object.keys(this._packageParams).map((key) => {
|
823 | const value = this._packageParams[key];
|
824 | return { key, value };
|
825 | });
|
826 | const result = await fn({
|
827 | name: this._packageName,
|
828 | package: {
|
829 | publish: this._packageShared,
|
830 | parameters,
|
831 | },
|
832 | });
|
833 | this.log.info(`${chalk.green('ok:')} ${verb} package ${chalk.whiteBright(`/${result.namespace}/${result.name}`)}`);
|
834 | } catch (e) {
|
835 | this.log.error(`${chalk.red('error: failed processing package: ')} ${e.message}`);
|
836 | throw Error('abort.');
|
837 | }
|
838 | }
|
839 |
|
840 | async test() {
|
841 | if (this._webAction) {
|
842 | return this.testRequest();
|
843 | }
|
844 | return this.testInvoke();
|
845 | }
|
846 |
|
847 | async testRequest() {
|
848 | const url = `${this._wskApiHost}/api/v1/web${this._fqn}${this._test || ''}`;
|
849 | this.log.info(`--: requesting: ${chalk.blueBright(url)} ...`);
|
850 | const headers = {};
|
851 | if (this._webSecure === true) {
|
852 | headers.authorization = `Basic ${Buffer.from(this._wskAuth).toString('base64')}`;
|
853 | } else if (this._webSecure) {
|
854 | headers['x-require-whisk-auth'] = this._webSecure;
|
855 | }
|
856 | const ret = await fetch(url, {
|
857 | headers,
|
858 | });
|
859 | const body = await ret.text();
|
860 | const id = ret.headers.get('x-openwhisk-activation-id');
|
861 | if (ret.ok) {
|
862 | this.log.info(`id: ${chalk.grey(id)}`);
|
863 | this.log.info(`${chalk.green('ok:')} ${ret.status}`);
|
864 | this.log.debug(chalk.grey(body));
|
865 | } else {
|
866 | this.log.info(`id: ${chalk.grey(id)}`);
|
867 | if (ret.status === 302 || ret.status === 301) {
|
868 | this.log.info(`${chalk.green('ok:')} ${ret.status}`);
|
869 | this.log.debug(chalk.grey(`Location: ${ret.headers.get('location')}`));
|
870 | } else {
|
871 | throw new Error(`test failed: ${ret.status} ${body}`);
|
872 | }
|
873 | }
|
874 | }
|
875 |
|
876 | async testInvoke() {
|
877 | const openwhisk = this.getOpenwhiskClient();
|
878 |
|
879 | const params = Object.entries(this._test_params).reduce((s, [key, value]) => (`${s} -p ${key} ${value}`), '');
|
880 | this.log.info(chalk`--: invoking: {blueBright ${this._actionName}${params}} ...`);
|
881 | try {
|
882 | const ret = await openwhisk.actions.invoke({
|
883 | name: this._actionName,
|
884 | blocking: true,
|
885 | result: true,
|
886 | params: this._test_params,
|
887 | });
|
888 | this.log.info(`${chalk.green('ok:')} 200`);
|
889 | this.log.debug(chalk.grey(JSON.stringify(ret, null, ' ')));
|
890 | } catch (e) {
|
891 | throw new Error(`test failed: ${e.message}`);
|
892 | }
|
893 | }
|
894 |
|
895 | async showDeployHints() {
|
896 | const relZip = path.relative(process.cwd(), this._zipFile);
|
897 | this.log.info('Deploy to openwhisk the following command or specify --deploy on the commandline:');
|
898 | if (this._docker) {
|
899 | this.log.info(chalk.grey(`$ wsk action update ${this._actionName} --docker ${this._docker} --web raw ${relZip}`));
|
900 | } else {
|
901 | this.log.info(chalk.grey(`$ wsk action update ${this._actionName} --kind ${this._kind} --web raw ${relZip}`));
|
902 | }
|
903 | }
|
904 |
|
905 | async updateLinks() {
|
906 | if (this._links.length === 0) {
|
907 | return;
|
908 | }
|
909 | const idx = this._name.lastIndexOf('@');
|
910 | if (idx < 0) {
|
911 | this.log.warn(`${chalk.yellow('warn:')} unable to create version sequence. unsupported action name format. should be: "name@version"`);
|
912 | return;
|
913 | }
|
914 |
|
915 | const pkgPrefix = this._linksPackage === 'default' ? '' : `${this._linksPackage}/`;
|
916 | const prefix = `${pkgPrefix}${this._name.substring(0, idx + 1)}`;
|
917 | const s = semver.parse(this._version);
|
918 | const pkgName = this._packageName === 'default' ? '' : `${this._packageName}/`;
|
919 | const fqn = `/${this._wskNamespace}/${pkgName}${this._name}`;
|
920 | const sfx = [];
|
921 | this._links.forEach((link) => {
|
922 | if (link === 'major' || link === 'minor') {
|
923 | if (!s) {
|
924 | this.log.warn(`${chalk.yellow('warn:')} unable to create version sequences. error while parsing version: ${this._version}`);
|
925 | return;
|
926 | }
|
927 | if (link === 'major') {
|
928 | sfx.push(`v${s.major}`);
|
929 | } else {
|
930 | sfx.push(`v${s.major}.${s.minor}`);
|
931 | }
|
932 | } else {
|
933 | sfx.push(link);
|
934 | }
|
935 | });
|
936 |
|
937 | const openwhisk = this.getOpenwhiskClient();
|
938 |
|
939 | const annotations = [
|
940 | { key: 'exec', value: 'sequence' },
|
941 | { key: 'web-export', value: this._webAction },
|
942 | { key: 'raw-http', value: this._rawHttp },
|
943 | { key: 'final', value: true },
|
944 | { key: 'updated', value: this._updatedAt },
|
945 | ];
|
946 | if (this._webSecure) {
|
947 | annotations.push({ key: 'require-whisk-auth', value: this._webSecure });
|
948 | }
|
949 | if (this._updatedBy) {
|
950 | annotations.push({ key: 'updatedBy', value: this._updatedBy });
|
951 | }
|
952 |
|
953 | let hasErrors = false;
|
954 | await Promise.all(sfx.map(async (sf) => {
|
955 | const options = {
|
956 | name: `${prefix}${sf}`,
|
957 | action: {
|
958 | namespace: this._wskNamespace,
|
959 | name: `${prefix}${sf}`,
|
960 | exec: {
|
961 | kind: 'sequence',
|
962 | components: [fqn],
|
963 | },
|
964 | annotations,
|
965 | },
|
966 | annotations,
|
967 | };
|
968 |
|
969 | try {
|
970 | this.log.debug(`creating sequence: ${options.name} -> ${options.action.exec.components[0]}`);
|
971 | const result = await openwhisk.actions.update(options);
|
972 | this.log.info(`${chalk.green('ok:')} created sequence ${chalk.whiteBright(`/${result.namespace}/${result.name}`)} -> ${chalk.whiteBright(fqn)}`);
|
973 | } catch (e) {
|
974 | hasErrors = true;
|
975 | this.log.error(`${chalk.red('error:')} failed creating sequence: ${e.message}`);
|
976 | }
|
977 | }));
|
978 | if (hasErrors) {
|
979 | throw new Error('Aborting due to errors during sequence updates.');
|
980 | }
|
981 | }
|
982 |
|
983 | async run() {
|
984 | this.log.info(chalk`{grey openwhisk-action-builder v${version}}`);
|
985 | await this.validate();
|
986 | if (this._build) {
|
987 | await this.createPackage();
|
988 | await this.createArchive();
|
989 | const relZip = path.relative(process.cwd(), this._zipFile);
|
990 | this.log.info(chalk`{green ok:} created action: {yellow ${relZip}}.`);
|
991 | await this.validateBundle();
|
992 | }
|
993 |
|
994 | if (this._updatePackage) {
|
995 | await this.updatePackage();
|
996 | }
|
997 | if (this._deploy) {
|
998 | await this.deploy();
|
999 | } else if (this._showHints) {
|
1000 | await this.showDeployHints();
|
1001 | }
|
1002 |
|
1003 | if (this._delete) {
|
1004 | await this.delete();
|
1005 | }
|
1006 |
|
1007 | if (typeof this._test === 'string' || Object.keys(this._test_params).length) {
|
1008 | await this.test();
|
1009 | }
|
1010 |
|
1011 | await this.updateLinks();
|
1012 |
|
1013 | return {
|
1014 | name: `openwhisk;host=${this._wskApiHost}`,
|
1015 | url: this._fqn,
|
1016 | };
|
1017 | }
|
1018 | };
|