@adobe/helix-deploy 4.13.0 → 4.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ const chalk = require('chalk');
17
17
  const BaseBundler = require('./BaseBundler.js');
18
18
 
19
19
  /**
20
- * Creates the action bundle
20
+ * Webpack based bundler
21
21
  */
22
22
  module.exports = class WebpackBundler extends BaseBundler {
23
23
  async init() {
@@ -32,12 +32,13 @@ module.exports = class WebpackBundler extends BaseBundler {
32
32
  target: 'node',
33
33
  mode: 'development',
34
34
  // the universal adapter is the entry point
35
- entry: cfg.adapterFile || path.resolve(__dirname, 'template', 'index.js'),
35
+ entry: cfg.adapterFile || path.resolve(__dirname, '..', 'template', 'node-index.js'),
36
36
  output: {
37
37
  path: cfg.cwd,
38
38
  filename: path.relative(cfg.cwd, cfg.bundle),
39
39
  library: 'main',
40
40
  libraryTarget: 'umd',
41
+ globalObject: 'globalThis',
41
42
  },
42
43
  devtool: false,
43
44
  externals: [
@@ -88,17 +89,14 @@ module.exports = class WebpackBundler extends BaseBundler {
88
89
  return opts;
89
90
  }
90
91
 
91
- async createBundle() {
92
+ async createWebpackBundle(arch) {
92
93
  const { cfg } = this;
93
- if (!cfg.bundle) {
94
- throw Error('bundle path is undefined');
95
- }
96
94
  if (!cfg.depFile) {
97
95
  throw Error('dependencies info path is undefined');
98
96
  }
99
97
  const m = cfg.minify ? 'minified ' : '';
100
98
  if (!cfg.progressHandler) {
101
- cfg.log.info(`--: creating ${m}bundle using webpack ...`);
99
+ cfg.log.info(`--: creating ${arch} ${m}bundle using webpack ...`);
102
100
  }
103
101
  const config = await this.getWebpackConfig();
104
102
  const compiler = webpack(config);
@@ -120,11 +118,18 @@ module.exports = class WebpackBundler extends BaseBundler {
120
118
  // write dependencies info file
121
119
  await fse.writeJson(cfg.depFile, cfg.dependencies, { spaces: 2 });
122
120
  if (!cfg.progressHandler) {
123
- cfg.log.info(chalk`{green ok:} created bundle {yellow ${config.output.filename}}`);
121
+ cfg.log.info(chalk`{green ok:} created ${arch} bundle {yellow ${config.output.filename}}`);
124
122
  }
125
123
  return stats;
126
124
  }
127
125
 
126
+ async createBundle() {
127
+ if (!this.cfg.bundle) {
128
+ throw Error('bundle path is undefined');
129
+ }
130
+ return this.createWebpackBundle('node');
131
+ }
132
+
128
133
  /**
129
134
  * Resolves the dependencies by chunk. eg:
130
135
  *
package/src/cli.js CHANGED
@@ -20,6 +20,8 @@ const OpenWhiskDeployer = require('./deploy/OpenWhiskDeployer');
20
20
  const AWSDeployer = require('./deploy/AWSDeployer');
21
21
  const AzureDeployer = require('./deploy/AzureDeployer');
22
22
  const GoogleDeployer = require('./deploy/GoogleDeployer');
23
+ const CloudflareDeployer = require('./deploy/CloudflareDeployer');
24
+ const ComputeAtEdgeDeployer = require('./deploy/ComputeAtEdgeDeployer');
23
25
  const FastlyGateway = require('./gateway/FastlyGateway');
24
26
 
25
27
  const PLUGINS = [
@@ -27,6 +29,8 @@ const PLUGINS = [
27
29
  AWSDeployer,
28
30
  AzureDeployer,
29
31
  GoogleDeployer,
32
+ CloudflareDeployer,
33
+ ComputeAtEdgeDeployer,
30
34
  FastlyGateway,
31
35
  ];
32
36
 
@@ -24,6 +24,11 @@ class BaseDeployer {
24
24
  return this.cfg.log;
25
25
  }
26
26
 
27
+ // eslint-disable-next-line class-methods-use-this
28
+ async init() {
29
+ // nothing to do
30
+ }
31
+
27
32
  ready() {
28
33
  return this.cfg && false;
29
34
  }
@@ -0,0 +1,71 @@
1
+ /*
2
+ * Copyright 2021 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
+ class CloudflareConfig {
13
+ constructor() {
14
+ Object.assign(this, {});
15
+ }
16
+
17
+ configure(argv) {
18
+ return this
19
+ .withEmail(argv.cloudflareEmail)
20
+ .withAuth(argv.cloudflareAuth)
21
+ .withTestDomain(argv.cloudflareTestDomain)
22
+ .withAccountID(argv.cloudflareAccountId);
23
+ }
24
+
25
+ withAccountID(value) {
26
+ this.accountID = value;
27
+ return this;
28
+ }
29
+
30
+ withEmail(value) {
31
+ this.email = value;
32
+ return this;
33
+ }
34
+
35
+ withTestDomain(value) {
36
+ this.testDomain = value;
37
+ return this;
38
+ }
39
+
40
+ withAuth(value) {
41
+ this.auth = value;
42
+ return this;
43
+ }
44
+
45
+ static yarg(yargs) {
46
+ return yargs
47
+ .group(['cloudflare-account-id', 'cloudflare-auth', 'cloudflare-email', 'cloudflare-test-domain'], 'Cloudflare Workers Deployment Options')
48
+ .option('cloudflare-account-id', {
49
+ description: 'the Cloudflare account ID to deploy to',
50
+ type: 'string',
51
+ default: '',
52
+ })
53
+ .option('cloudflare-email', {
54
+ description: 'the Cloudflare email address belonging to the authentication token',
55
+ type: 'string',
56
+ default: '',
57
+ })
58
+ .option('cloudflare-test-domain', {
59
+ description: 'the *.workers.dev subdomain to use for testing deployed scripts',
60
+ type: 'string',
61
+ default: '',
62
+ })
63
+ .option('cloudflare-auth', {
64
+ description: 'the Cloudflare API token from https://dash.cloudflare.com/profile/api-tokens',
65
+ type: 'string',
66
+ default: '',
67
+ });
68
+ }
69
+ }
70
+
71
+ module.exports = CloudflareConfig;
@@ -0,0 +1,145 @@
1
+ /*
2
+ * Copyright 2021 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
+ const path = require('path');
13
+ const fs = require('fs');
14
+ const FormData = require('form-data');
15
+ const BaseDeployer = require('./BaseDeployer');
16
+ const CloudflareConfig = require('./CloudflareConfig');
17
+
18
+ class CloudflareDeployer extends BaseDeployer {
19
+ constructor(baseConfig, config) {
20
+ super(baseConfig);
21
+ Object.assign(this, {
22
+ id: 'cloudflare',
23
+ name: 'Cloudflare',
24
+ _cfg: config,
25
+ noGatewayBackend: true,
26
+ });
27
+ }
28
+
29
+ ready() {
30
+ return !!this._cfg.auth && !!this._cfg.accountID && !!this.cfg.edgeBundle;
31
+ }
32
+
33
+ validate() {
34
+ if (!this.ready()) {
35
+ throw new Error('Cloudflare target needs email, token, and account ID');
36
+ }
37
+ }
38
+
39
+ get fullFunctionName() {
40
+ return `${this.cfg.packageName}--${this.cfg.name}`
41
+ .replace(/\./g, '_')
42
+ .replace('@', '_');
43
+ }
44
+
45
+ async deploy() {
46
+ const body = fs.readFileSync(path.relative(this.cfg.cwd, this.cfg.edgeBundle));
47
+ const { id } = await this.createKVNamespace(`${this.cfg.packageName}--secrets`);
48
+
49
+ const metadata = {
50
+ body_part: 'script',
51
+ bindings: [
52
+ ...Object.entries(this.cfg.params).map(([key, value]) => ({
53
+ name: key,
54
+ type: 'secret_text',
55
+ text: value,
56
+ })),
57
+ {
58
+ name: 'PACKAGE',
59
+ namespace_id: id,
60
+ type: 'kv_namespace',
61
+ },
62
+ ],
63
+ };
64
+
65
+ // what https://api.cloudflare.com/#worker-script-upload-worker won't tell you:
66
+ // you can use multipart/formdata to set metadata according to
67
+ // https://community.cloudflare.com/t/bind-kv-and-workers-via-api/221391
68
+ const form = new FormData();
69
+ form.append('script', body, {
70
+ contentType: 'application/javascript',
71
+ });
72
+ form.append('metadata', JSON.stringify(metadata), {
73
+ contentType: 'application/json',
74
+ });
75
+
76
+ const res = await this.fetch(`https://api.cloudflare.com/client/v4/accounts/${this._cfg.accountID}/workers/scripts/${this.fullFunctionName}`, {
77
+ method: 'PUT',
78
+ headers: form.getHeaders({
79
+ Authorization: `Bearer ${this._cfg.auth}`,
80
+ }),
81
+ body: form.getBuffer(),
82
+ });
83
+
84
+ if (!res.ok) {
85
+ const { errors } = await res.json();
86
+ throw new Error(`Unable to upload worker to Cloudflare: ${errors[0].message}`);
87
+ }
88
+
89
+ await this.updatePackageParams(id, this.cfg.packageParams);
90
+ }
91
+
92
+ async updatePackageParams(id, params) {
93
+ const kvlist = Object.entries(params).map(([key, value]) => ({
94
+ key, value,
95
+ }));
96
+
97
+ const res = await this.fetch(`https://api.cloudflare.com/client/v4/accounts/${this._cfg.accountID}/storage/kv/namespaces/${id}/bulk`, {
98
+ method: 'PUT',
99
+ headers: {
100
+ Authorization: `Bearer ${this._cfg.auth}`,
101
+ 'content-type': 'application/json',
102
+ },
103
+ body: JSON.stringify(kvlist),
104
+ });
105
+ return res.ok;
106
+ }
107
+
108
+ async createKVNamespace(name) {
109
+ const postres = await this.fetch(`https://api.cloudflare.com/client/v4/accounts/${this._cfg.accountID}/storage/kv/namespaces`, {
110
+ method: 'POST',
111
+ headers: {
112
+ Authorization: `Bearer ${this._cfg.auth}`,
113
+ 'content-type': 'application/json',
114
+ },
115
+ body: {
116
+ title: name,
117
+ },
118
+ });
119
+ let { result } = await postres.json();
120
+ if (!result) {
121
+ const listres = await this.fetch(`https://api.cloudflare.com/client/v4/accounts/${this._cfg.accountID}/storage/kv/namespaces`, {
122
+ method: 'GET',
123
+ headers: {
124
+ Authorization: `Bearer ${this._cfg.auth}`,
125
+ },
126
+ });
127
+ const { result: results } = await listres.json();
128
+ result = results.find((r) => r.title === name);
129
+ }
130
+ return result;
131
+ }
132
+
133
+ async test() {
134
+ return this._cfg.testDomain
135
+ ? this.testRequest({
136
+ url: `https://${this.fullFunctionName}.${this._cfg.testDomain}.workers.dev`,
137
+ idHeader: 'CF-RAY',
138
+ retry404: 0,
139
+ })
140
+ : undefined;
141
+ }
142
+ }
143
+
144
+ CloudflareDeployer.Config = CloudflareConfig;
145
+ module.exports = CloudflareDeployer;
@@ -0,0 +1,93 @@
1
+ /*
2
+ * Copyright 2021 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
+ class ComputeAtEdgeConfig {
13
+ constructor() {
14
+ Object.assign(this, {});
15
+ }
16
+
17
+ configure(argv) {
18
+ return this
19
+ .withServiceID(argv.computeServiceId)
20
+ .withAuth(argv.fastlyAuth)
21
+ .withCoralogixToken(argv.coralogixToken)
22
+ .withFastlyGateway(argv.fastlyGateway)
23
+ .withComputeDomain(argv.computeTestDomain)
24
+ .withCoralogixApp(argv.computeCoralogixApp);
25
+ }
26
+
27
+ withServiceID(value) {
28
+ this.service = value;
29
+ return this;
30
+ }
31
+
32
+ withAuth(value) {
33
+ this.auth = value;
34
+ return this;
35
+ }
36
+
37
+ withCoralogixToken(value) {
38
+ this.coralogixToken = value;
39
+ return this;
40
+ }
41
+
42
+ withCoralogixApp(value) {
43
+ this.coralogixApp = value;
44
+ return this;
45
+ }
46
+
47
+ withFastlyGateway(value) {
48
+ this.fastlyGateway = value;
49
+ return this;
50
+ }
51
+
52
+ withComputeDomain(value) {
53
+ this.testDomain = value;
54
+ return this;
55
+ }
56
+
57
+ static yarg(yargs) {
58
+ return yargs
59
+ .group(['compute-service-id', 'compute-domain', 'fastly-auth', 'coralogix-token', 'compute-coralogix-app'], 'Fastly Compute@Edge Options')
60
+ .option('compute-service-id', {
61
+ description: 'the Fastly Service to deploy the action to',
62
+ type: 'string',
63
+ default: '',
64
+ })
65
+ .option('compute-test-domain', {
66
+ description: 'the domain name of the Compute@Edge service (used for testing)',
67
+ type: 'string',
68
+ default: '',
69
+ })
70
+ .option('fastly-auth', {
71
+ description: 'the Fastly token',
72
+ type: 'string',
73
+ default: '',
74
+ })
75
+ .option('coralogix-token', {
76
+ description: 'the Coralogix token (to enable logging)',
77
+ type: 'string',
78
+ default: '',
79
+ })
80
+ .option('fastly-gateway', {
81
+ description: 'the hostname of the Fastly gateway for package params',
82
+ type: 'string',
83
+ default: '',
84
+ })
85
+ .option('compute-coralogix-app', {
86
+ description: 'the Application name',
87
+ type: 'string',
88
+ default: 'fastly-compute',
89
+ });
90
+ }
91
+ }
92
+
93
+ module.exports = ComputeAtEdgeConfig;
@@ -0,0 +1,190 @@
1
+ /*
2
+ * Copyright 2021 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
+ const { fork } = require('child_process');
13
+ const path = require('path');
14
+ const fs = require('fs/promises');
15
+ const tar = require('tar');
16
+ const getStream = require('get-stream');
17
+ const Fastly = require('@adobe/fastly-native-promises');
18
+ const BaseDeployer = require('./BaseDeployer');
19
+ const ComputeAtEdgeConfig = require('./ComputeAtEdgeConfig');
20
+
21
+ /**
22
+ * The class ComputeAtEdgeDeployer deploys to Fastly's Compute(at)Edge (WASM) runtime.
23
+ * It should be seen as a functional equivalent to the CloudflareDeployer
24
+ * and not confused with the FastlyGateway (which only routes requests, but
25
+ * does not handle them.)
26
+ */
27
+ class ComputeAtEdgeDeployer extends BaseDeployer {
28
+ constructor(baseConfig, config) {
29
+ super(baseConfig);
30
+ Object.assign(this, {
31
+ id: 'c@e',
32
+ name: 'Fastly Compute@Edge',
33
+ _cfg: config,
34
+ _fastly: null,
35
+ noGatewayBackend: true,
36
+ });
37
+ }
38
+
39
+ ready() {
40
+ return !!this._cfg.service && !!this._cfg.auth && !!this.cfg.edgeBundle;
41
+ }
42
+
43
+ validate() {
44
+ if (!this.ready()) {
45
+ throw new Error('Compute@Edge target needs token and service ID');
46
+ }
47
+ }
48
+
49
+ init() {
50
+ if (this.ready() && !this._fastly) {
51
+ this._fastly = Fastly(this._cfg.auth, this._cfg.service, 60000);
52
+ }
53
+ }
54
+
55
+ get log() {
56
+ return this.cfg.log;
57
+ }
58
+
59
+ /**
60
+ *
61
+ * @returns
62
+ */
63
+ async bundle() {
64
+ const bundleDir = path.dirname(this.cfg.edgeBundle);
65
+ this.log.debug(`Creating fastly.toml in ${bundleDir}`);
66
+ fs.writeFile(path.resolve(bundleDir, 'fastly.toml'), `
67
+ # This file describes a Fastly Compute@Edge package. To learn more visit:
68
+ # https://developer.fastly.com/reference/fastly-toml/
69
+
70
+ authors = ["Helix Deploy"]
71
+ description = "Test Project"
72
+ language = "javascript"
73
+ manifest_version = 2
74
+ name = "Test"
75
+ service_id = ""
76
+ `);
77
+
78
+ return new Promise((resolve, reject) => {
79
+ const child = fork(
80
+ path.resolve(
81
+ __dirname,
82
+ '..',
83
+ '..',
84
+ 'node_modules',
85
+ '@fastly',
86
+ 'js-compute',
87
+ 'js-compute-runtime-cli.js',
88
+ ),
89
+ [this.cfg.edgeBundle, 'bin/main.wasm'],
90
+ {
91
+ cwd: bundleDir,
92
+ },
93
+ );
94
+ child.on('data', (data) => resolve(data));
95
+ child.on('error', (err) => reject(err));
96
+ child.on('close', (err) => {
97
+ if (err) {
98
+ // non-zero status code
99
+ reject(err);
100
+ } else {
101
+ this.log.debug(`Created WASM bundle of script and interpreter in ${bundleDir}/bin/main.wasm`);
102
+ const stream = tar.c({
103
+ gzip: true,
104
+ // sync: true,
105
+ cwd: bundleDir,
106
+ prefix: 'Test',
107
+ // file: path.resolve(bundleDir, 'fastly-bundle.tar.gz')
108
+ }, ['bin/main.wasm', 'fastly.toml']);
109
+ // this.log.debug(`Created tar file in ${bundleDir}/fastly-bundle.tar.gz`);
110
+ resolve(getStream.buffer(stream));
111
+ }
112
+ });
113
+ });
114
+ }
115
+
116
+ async deploy() {
117
+ const buf = await this.bundle();
118
+
119
+ this.init();
120
+
121
+ await this._fastly.transact(async (version) => {
122
+ await this._fastly.writePackage(version, buf);
123
+
124
+ await this._fastly.writeDictionary(version, 'secrets', {
125
+ name: 'secrets',
126
+ write_only: 'true',
127
+ });
128
+
129
+ const host = this._cfg.fastlyGateway;
130
+ console.log('Host', host);
131
+ const backend = {
132
+ hostname: host,
133
+ ssl_cert_hostname: host,
134
+ ssl_sni_hostname: host,
135
+ address: host,
136
+ override_host: host,
137
+ name: 'gateway',
138
+ error_threshold: 0,
139
+ first_byte_timeout: 60000,
140
+ weight: 100,
141
+ connect_timeout: 5000,
142
+ port: 443,
143
+ between_bytes_timeout: 10000,
144
+ shield: '', // 'bwi-va-us',
145
+ max_conn: 200,
146
+ use_ssl: true,
147
+ };
148
+ await this._fastly.writeBackend(version, 'gateway', backend);
149
+ }, true);
150
+ }
151
+
152
+ async updatePackage() {
153
+ this.log.info('--: updating app (gateway) config ...');
154
+
155
+ this.init();
156
+
157
+ const functionparams = Object
158
+ .entries(this.cfg.params)
159
+ .map(([key, value]) => ({
160
+ item_key: key,
161
+ item_value: value,
162
+ op: 'update',
163
+ }));
164
+
165
+ await this._fastly.bulkUpdateDictItems(undefined, 'secrets', ...functionparams);
166
+ await this._fastly.updateDictItem(undefined, 'secrets', '_token', this.cfg.packageToken);
167
+ console.log('package', `https://${this._cfg.fastlyGateway}/${this.cfg.packageName}/`);
168
+ await this._fastly.updateDictItem(undefined, 'secrets', '_package', `https://${this._cfg.fastlyGateway}/${this.cfg.packageName}/`);
169
+
170
+ this._fastly.discard();
171
+ }
172
+
173
+ get fullFunctionName() {
174
+ return `${this.cfg.packageName}--${this.cfg.name}`
175
+ .replace(/\./g, '_')
176
+ .replace('@', '_');
177
+ }
178
+
179
+ async test() {
180
+ return this._cfg.testDomain
181
+ ? this.testRequest({
182
+ url: `https://${this._cfg.testDomain}.edgecompute.app`,
183
+ retry404: 0,
184
+ })
185
+ : undefined;
186
+ }
187
+ }
188
+
189
+ ComputeAtEdgeDeployer.Config = ComputeAtEdgeConfig;
190
+ module.exports = ComputeAtEdgeDeployer;