UNPKG

29 kBJavaScriptView Raw
1/*
2 * Copyright 2019 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13const path = require('path');
14const fs = require('fs');
15const fse = require('fs-extra');
16const archiver = require('archiver');
17const webpack = require('webpack');
18const chalk = require('chalk');
19const dotenv = require('dotenv');
20const os = require('os');
21const ow = require('openwhisk');
22const semver = require('semver');
23const fetchAPI = require('@adobe/helix-fetch');
24const git = require('isomorphic-git');
25const { version } = require('../package.json');
26
27require('dotenv').config();
28
29const { fetch } = process.env.HELIX_FETCH_FORCE_HTTP1
30 ? fetchAPI.context({
31 httpProtocol: 'http1',
32 httpsProtocols: ['http1'],
33 })
34 : fetchAPI;
35
36/**
37 * Returns the `origin` remote url or `''` if none is defined.
38 *
39 * @param {string} dir working tree directory path of the git repo
40 * @returns {Promise<string>} `origin` remote url
41 */
42async 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 // don't fail if directory is not a git repository
48 return '';
49 }
50}
51
52/**
53 * Returns the sha of the current (i.e. `HEAD`) commit.
54 *
55 * @param {string} dir working tree directory path of the git repo
56 * @returns {Promise<string>} sha of the current (i.e. `HEAD`) commit
57 */
58async function getCurrentRevision(dir) {
59 try {
60 return await git.resolveRef({ fs, dir, ref: 'HEAD' });
61 } catch (e) {
62 // ignore if no git repository
63 return '';
64 }
65}
66
67module.exports = class ActionBuilder {
68 /**
69 * Decoded the params string or file. First as JSON and if this fails, as ENV format.
70 * @param {string} params Params string or file name
71 * @param {boolean} isFile {@code true} to indicate a file.
72 * @param {boolean} warnError {@code true} to only issue warning instead of throwing error
73 * @returns {*} Decoded params object.
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 // first try JSON
94 try {
95 data = JSON.parse(content);
96 } catch (e) {
97 // then try env
98 data = dotenv.parse(content);
99 }
100 }
101
102 const resolve = (obj) => {
103 // resolve file references
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 // eslint-disable-next-line no-param-reassign
113 obj[key] = `@${filePath}`;
114 }
115 }
116 });
117 };
118 resolve(data);
119 return data;
120 }
121
122 /**
123 * Iterates the given params and resolves potential file references.
124 * @param {object} params the params
125 * @returns the resolved object.
126 */
127 static async resolveParams(params) {
128 const tasks = [];
129 const resolve = async (obj, key, file) => {
130 // eslint-disable-next-line no-param-reassign
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 // eslint-disable-next-line no-param-reassign
143 obj[key] = value.substring(1);
144 } else if (value.startsWith('@')) {
145 tasks.push(resolve(obj, key, value.substring(1)));
146 } else {
147 // eslint-disable-next-line no-param-reassign
148 obj[key] = value;
149 }
150 }
151 });
152 };
153 resolver(params);
154 await Promise.all(tasks);
155 return params;
156 }
157
158 /**
159 * Converts the given {@code obj} to ENV format.
160 * @param {Object} obj the object to convert.
161 * @returns {string} the formatted string.
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 // poor men's logging...
223 /* eslint-disable no-console */
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 /* eslint-enable no-console */
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 // eslint-disable-next-line max-len
360 this._packageParams = Object.assign(this._packageParams, this.decodeParams(v, forceFile, warnError));
361 });
362 } else {
363 // eslint-disable-next-line max-len
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 // do some very simple variable substitution
483 // eslint-disable-next-line no-template-curly-in-string
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 // create dist dir
508 await fse.ensureDir(this._distDir);
509
510 // init openwhisk props
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 // init git coordinates
521 this._gitUrl = (this._pkgJson.repository || {}).url || '';
522 this._gitRef = await getCurrentRevision(this._cwd);
523 this._gitOrigin = await getOrigin(this._cwd);
524
525 // init deploy time
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 // create zip file for package
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 * Resolves the dependencies by chunk. eg:
646 *
647 * {
648 * 'src/idx_json.bundle.js': [{
649 * id: '@adobe/helix-epsagon:1.2.0',
650 * name: '@adobe/helix-epsagon',
651 * version: '1.2.0' },
652 * ],
653 * ...
654 * }
655 */
656 async resolveDependencyInfos(stats) {
657 // get list of dependencies
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 // ignore
697 }
698 }
699 }));
700 }));
701
702 // sort the deps
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 // eslint-disable-next-line global-require,import/no-dynamic-require
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 // using `default` as package name doesn't work with sequences...
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};