@adobe/helix-deploy 4.12.2 → 4.15.0

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.
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
 
@@ -223,7 +223,7 @@ class AWSDeployer extends BaseDeployer {
223
223
  Environment: {
224
224
  Variables: cfg.params,
225
225
  },
226
- Handler: 'index.lambda',
226
+ Handler: cfg.esm ? 'esm-adapter/index.handler' : 'index.lambda',
227
227
  };
228
228
 
229
229
  this.log.info(`--: using lambda role "${this._cfg.role}"`);
@@ -705,7 +705,9 @@ class AWSDeployer extends BaseDeployer {
705
705
  return;
706
706
  }
707
707
  // eslint-disable-next-line no-await-in-loop
708
- await new Promise((resolve) => setTimeout(resolve, 1500));
708
+ await new Promise((resolve) => {
709
+ setTimeout(resolve, 1500);
710
+ });
709
711
  } catch (e) {
710
712
  this.log.error(chalk`{red error}: error checking function state`);
711
713
  throw e;
@@ -178,19 +178,23 @@ class AzureDeployer extends BaseDeployer {
178
178
  const { cfg } = this;
179
179
  this.log.info('--: updating app (package) parameters ...');
180
180
 
181
- const result = await this._client.webApps.listApplicationSettings(this._app.resourceGroup,
182
- this._cfg.appName);
181
+ const result = await this._client.webApps.listApplicationSettings(
182
+ this._app.resourceGroup,
183
+ this._cfg.appName,
184
+ );
183
185
 
184
186
  const update = {
185
187
  ...cfg.packageParams,
186
188
  ...result.properties,
187
189
  };
188
190
 
189
- await this._client.webApps.updateApplicationSettings(this._app.resourceGroup,
191
+ await this._client.webApps.updateApplicationSettings(
192
+ this._app.resourceGroup,
190
193
  this._cfg.appName,
191
194
  {
192
195
  properties: update,
193
- });
196
+ },
197
+ );
194
198
 
195
199
  this.log.info(`${Object.keys(update).length} package parameters have been updated.`);
196
200
  }
@@ -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
  }
@@ -37,9 +42,7 @@ class BaseDeployer {
37
42
  getOrCreateFetchContext() {
38
43
  if (!this._fetchContext) {
39
44
  this._fetchContext = process.env.HELIX_FETCH_FORCE_HTTP1
40
- ? fetchAPI.context({
41
- alpnProtocols: [fetchAPI.ALPN_HTTP1_1],
42
- })
45
+ ? fetchAPI.h1()
43
46
  : fetchAPI.context();
44
47
  }
45
48
  return this._fetchContext;
@@ -109,6 +112,8 @@ class BaseDeployer {
109
112
  if (ret.ok) {
110
113
  this.log.info(`id: ${chalk.grey(id)}`);
111
114
  this.log.info(`${chalk.green('ok:')} ${ret.status}`);
115
+ this.log.debug(chalk.grey(JSON.stringify(ret.headers.plain(), null, 2)));
116
+ this.log.debug('');
112
117
  this.log.debug(chalk.grey(body));
113
118
  return;
114
119
  }
@@ -123,7 +128,9 @@ class BaseDeployer {
123
128
  // eslint-disable-next-line no-param-reassign
124
129
  retry404 -= 1;
125
130
  // eslint-disable-next-line no-await-in-loop
126
- await new Promise((resolve) => setTimeout(resolve, 1500));
131
+ await new Promise((resolve) => {
132
+ setTimeout(resolve, 1500);
133
+ });
127
134
  } else {
128
135
  // this.log.info(`${chalk.red('error:')} test failed: ${ret.status} ${body}`);
129
136
  throw new Error(`test failed: ${ret.status} ${body}`);
@@ -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;