@constructive-io/bucket-provisioner 0.30.0 → 0.30.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.
- package/esm/provisioner.d.ts +16 -15
- package/esm/provisioner.js +64 -76
- package/package.json +2 -2
- package/provisioner.d.ts +16 -15
- package/provisioner.js +63 -75
package/esm/provisioner.d.ts
CHANGED
|
@@ -91,17 +91,16 @@ export declare class BucketProvisioner {
|
|
|
91
91
|
/**
|
|
92
92
|
* Configure S3 Block Public Access settings.
|
|
93
93
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* not all providers implement this AWS-specific API.
|
|
94
|
+
* Skips only when the S3-compatible backend (e.g. MinIO) answers that it
|
|
95
|
+
* does not implement PutPublicAccessBlock; every other failure throws.
|
|
97
96
|
*/
|
|
98
97
|
setPublicAccessBlock(bucketName: string, config: PublicAccessBlockConfig): Promise<void>;
|
|
99
98
|
/**
|
|
100
99
|
* Apply an S3 bucket policy.
|
|
101
100
|
*
|
|
102
|
-
*
|
|
103
|
-
* PutBucketPolicy
|
|
104
|
-
*
|
|
101
|
+
* Skips only when the S3-compatible backend answers that it does not
|
|
102
|
+
* implement PutBucketPolicy; every other failure (AccessDenied, a blocked
|
|
103
|
+
* public policy, a malformed document) throws.
|
|
105
104
|
*/
|
|
106
105
|
setBucketPolicy(bucketName: string, policy: BucketPolicyDocument): Promise<void>;
|
|
107
106
|
/**
|
|
@@ -114,25 +113,27 @@ export declare class BucketProvisioner {
|
|
|
114
113
|
/**
|
|
115
114
|
* Set CORS configuration on an S3 bucket.
|
|
116
115
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* implement this API via the same endpoint path.
|
|
116
|
+
* Skips only when the S3-compatible backend (e.g. older MinIO) answers
|
|
117
|
+
* that it does not implement PutBucketCors; every other failure throws.
|
|
120
118
|
*/
|
|
121
119
|
setCors(bucketName: string, rules: CorsRule[]): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Remove the CORS configuration from an S3 bucket, so no browser origin may
|
|
122
|
+
* reach it. A bucket that has none already is the converged state.
|
|
123
|
+
*/
|
|
124
|
+
clearCors(bucketName: string): Promise<void>;
|
|
122
125
|
/**
|
|
123
126
|
* Enable versioning on an S3 bucket.
|
|
124
127
|
*
|
|
125
|
-
*
|
|
126
|
-
* does not
|
|
127
|
-
* since not all providers implement this API.
|
|
128
|
+
* Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
|
|
129
|
+
* answers that it does not implement PutBucketVersioning.
|
|
128
130
|
*/
|
|
129
131
|
enableVersioning(bucketName: string): Promise<void>;
|
|
130
132
|
/**
|
|
131
133
|
* Set lifecycle rules on an S3 bucket.
|
|
132
134
|
*
|
|
133
|
-
*
|
|
134
|
-
* does not
|
|
135
|
-
* are best-effort since not all providers implement this API.
|
|
135
|
+
* Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
|
|
136
|
+
* answers that it does not implement PutBucketLifecycleConfiguration.
|
|
136
137
|
*/
|
|
137
138
|
setLifecycleRules(bucketName: string, rules: LifecycleRule[]): Promise<void>;
|
|
138
139
|
/**
|
package/esm/provisioner.js
CHANGED
|
@@ -9,12 +9,27 @@
|
|
|
9
9
|
* - Private/temp buckets: Block All Public Access, no bucket policy, presigned URLs only
|
|
10
10
|
* - Public buckets: Block Public Access partially relaxed, public-read bucket policy applied
|
|
11
11
|
*/
|
|
12
|
-
import { CreateBucketCommand, DeleteBucketPolicyCommand, GetBucketCorsCommand, GetBucketLifecycleConfigurationCommand, GetBucketPolicyCommand, GetBucketVersioningCommand, GetPublicAccessBlockCommand, HeadBucketCommand, PutBucketCorsCommand, PutBucketLifecycleConfigurationCommand, PutBucketPolicyCommand, PutBucketVersioningCommand, PutPublicAccessBlockCommand, } from '@aws-sdk/client-s3';
|
|
12
|
+
import { CreateBucketCommand, DeleteBucketCorsCommand, DeleteBucketPolicyCommand, GetBucketCorsCommand, GetBucketLifecycleConfigurationCommand, GetBucketPolicyCommand, GetBucketVersioningCommand, GetPublicAccessBlockCommand, HeadBucketCommand, PutBucketCorsCommand, PutBucketLifecycleConfigurationCommand, PutBucketPolicyCommand, PutBucketVersioningCommand, PutPublicAccessBlockCommand, } from '@aws-sdk/client-s3';
|
|
13
13
|
import { createS3Client } from './client';
|
|
14
14
|
import { buildPrivateCorsRules, buildUploadCorsRules } from './cors';
|
|
15
15
|
import { buildTempCleanupRule } from './lifecycle';
|
|
16
16
|
import { buildPublicReadPolicy, getPublicAccessBlock } from './policies';
|
|
17
17
|
import { ProvisionerError } from './types';
|
|
18
|
+
/**
|
|
19
|
+
* Error codes an S3-compatible backend answers with when it does not implement
|
|
20
|
+
* a bucket-configuration API at all (older MinIO, some RustFS builds). Matched
|
|
21
|
+
* on the error's code only — never on message text, which AWS fills with the
|
|
22
|
+
* operation and policy names of real failures (an AccessDenied for
|
|
23
|
+
* `s3:PutBucketPolicy` reads "...because no identity-based policy allows...").
|
|
24
|
+
*/
|
|
25
|
+
const UNSUPPORTED_OPERATION_CODES = ['XmlParseException', 'NotImplemented'];
|
|
26
|
+
function errorCode(err) {
|
|
27
|
+
return err?.Code ?? err?.name;
|
|
28
|
+
}
|
|
29
|
+
function isUnsupportedOperation(err, extraCodes = []) {
|
|
30
|
+
const code = errorCode(err);
|
|
31
|
+
return code !== undefined && [...UNSUPPORTED_OPERATION_CODES, ...extraCodes].includes(code);
|
|
32
|
+
}
|
|
18
33
|
/**
|
|
19
34
|
* The BucketProvisioner handles creating and configuring S3-compatible
|
|
20
35
|
* buckets with the correct privacy settings, CORS rules, and policies
|
|
@@ -91,6 +106,8 @@ export class BucketProvisioner {
|
|
|
91
106
|
// A bucket no browser origin may reach gets no rule set: cross-origin
|
|
92
107
|
// access is the bucket's own recorded data, and "none recorded" is a
|
|
93
108
|
// legitimate answer, not a missing setting to invent an origin for.
|
|
109
|
+
// Provisioning converges, so "none" also clears a rule set an earlier
|
|
110
|
+
// provision wrote for origins the bucket no longer records.
|
|
94
111
|
const effectiveOrigins = options.allowedOrigins ?? this.allowedOrigins;
|
|
95
112
|
const corsRules = effectiveOrigins.length === 0
|
|
96
113
|
? []
|
|
@@ -100,6 +117,9 @@ export class BucketProvisioner {
|
|
|
100
117
|
if (corsRules.length > 0) {
|
|
101
118
|
await this.setCors(bucketName, corsRules);
|
|
102
119
|
}
|
|
120
|
+
else {
|
|
121
|
+
await this.clearCors(bucketName);
|
|
122
|
+
}
|
|
103
123
|
// 5. Versioning
|
|
104
124
|
if (versioning) {
|
|
105
125
|
await this.enableVersioning(bucketName);
|
|
@@ -173,9 +193,8 @@ export class BucketProvisioner {
|
|
|
173
193
|
/**
|
|
174
194
|
* Configure S3 Block Public Access settings.
|
|
175
195
|
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* not all providers implement this AWS-specific API.
|
|
196
|
+
* Skips only when the S3-compatible backend (e.g. MinIO) answers that it
|
|
197
|
+
* does not implement PutPublicAccessBlock; every other failure throws.
|
|
179
198
|
*/
|
|
180
199
|
async setPublicAccessBlock(bucketName, config) {
|
|
181
200
|
try {
|
|
@@ -185,16 +204,9 @@ export class BucketProvisioner {
|
|
|
185
204
|
}));
|
|
186
205
|
}
|
|
187
206
|
catch (err) {
|
|
188
|
-
// Some S3-compatible backends (e.g. older MinIO) don't
|
|
189
|
-
// PutPublicAccessBlock
|
|
190
|
-
|
|
191
|
-
if (err.Code === 'XmlParseException' ||
|
|
192
|
-
err.name === 'XmlParseException' ||
|
|
193
|
-
err.Code === 'NotImplemented' ||
|
|
194
|
-
err.name === 'NotImplemented' ||
|
|
195
|
-
err.message?.includes('not well-formed') ||
|
|
196
|
-
err.message?.includes('not implemented') ||
|
|
197
|
-
err.message?.includes('PublicAccessBlockConfiguration')) {
|
|
207
|
+
// Some S3-compatible backends (e.g. older MinIO) don't implement
|
|
208
|
+
// PutPublicAccessBlock; the bucket is still usable without it.
|
|
209
|
+
if (isUnsupportedOperation(err)) {
|
|
198
210
|
return;
|
|
199
211
|
}
|
|
200
212
|
throw new ProvisionerError('POLICY_FAILED', `Failed to set public access block on '${bucketName}': ${err.message}`, err);
|
|
@@ -203,9 +215,9 @@ export class BucketProvisioner {
|
|
|
203
215
|
/**
|
|
204
216
|
* Apply an S3 bucket policy.
|
|
205
217
|
*
|
|
206
|
-
*
|
|
207
|
-
* PutBucketPolicy
|
|
208
|
-
*
|
|
218
|
+
* Skips only when the S3-compatible backend answers that it does not
|
|
219
|
+
* implement PutBucketPolicy; every other failure (AccessDenied, a blocked
|
|
220
|
+
* public policy, a malformed document) throws.
|
|
209
221
|
*/
|
|
210
222
|
async setBucketPolicy(bucketName, policy) {
|
|
211
223
|
try {
|
|
@@ -215,15 +227,8 @@ export class BucketProvisioner {
|
|
|
215
227
|
}));
|
|
216
228
|
}
|
|
217
229
|
catch (err) {
|
|
218
|
-
// Some S3-compatible backends don't
|
|
219
|
-
|
|
220
|
-
if (err.Code === 'XmlParseException' ||
|
|
221
|
-
err.name === 'XmlParseException' ||
|
|
222
|
-
err.Code === 'NotImplemented' ||
|
|
223
|
-
err.name === 'NotImplemented' ||
|
|
224
|
-
err.message?.includes('not well-formed') ||
|
|
225
|
-
err.message?.includes('not implemented') ||
|
|
226
|
-
err.message?.includes('policy')) {
|
|
230
|
+
// Some S3-compatible backends don't implement bucket policies.
|
|
231
|
+
if (isUnsupportedOperation(err)) {
|
|
227
232
|
return;
|
|
228
233
|
}
|
|
229
234
|
throw new ProvisionerError('POLICY_FAILED', `Failed to set bucket policy on '${bucketName}': ${err.message}`, err);
|
|
@@ -241,14 +246,9 @@ export class BucketProvisioner {
|
|
|
241
246
|
}
|
|
242
247
|
catch (err) {
|
|
243
248
|
// No policy to delete — that's fine
|
|
244
|
-
if (err
|
|
249
|
+
if (errorCode(err) === 'NoSuchBucketPolicy' ||
|
|
245
250
|
err.$metadata?.httpStatusCode === 404 ||
|
|
246
|
-
err
|
|
247
|
-
err.name === 'XmlParseException' ||
|
|
248
|
-
err.Code === 'NotImplemented' ||
|
|
249
|
-
err.name === 'NotImplemented' ||
|
|
250
|
-
err.message?.includes('not well-formed') ||
|
|
251
|
-
err.message?.includes('not implemented')) {
|
|
251
|
+
isUnsupportedOperation(err)) {
|
|
252
252
|
return;
|
|
253
253
|
}
|
|
254
254
|
throw new ProvisionerError('POLICY_FAILED', `Failed to delete bucket policy on '${bucketName}': ${err.message}`, err);
|
|
@@ -257,9 +257,8 @@ export class BucketProvisioner {
|
|
|
257
257
|
/**
|
|
258
258
|
* Set CORS configuration on an S3 bucket.
|
|
259
259
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* implement this API via the same endpoint path.
|
|
260
|
+
* Skips only when the S3-compatible backend (e.g. older MinIO) answers
|
|
261
|
+
* that it does not implement PutBucketCors; every other failure throws.
|
|
263
262
|
*/
|
|
264
263
|
async setCors(bucketName, rules) {
|
|
265
264
|
try {
|
|
@@ -277,27 +276,34 @@ export class BucketProvisioner {
|
|
|
277
276
|
}));
|
|
278
277
|
}
|
|
279
278
|
catch (err) {
|
|
280
|
-
// Some S3-compatible backends (e.g. older MinIO) don't
|
|
281
|
-
// PutBucketCors
|
|
282
|
-
|
|
283
|
-
if (err.Code === 'XmlParseException' ||
|
|
284
|
-
err.name === 'XmlParseException' ||
|
|
285
|
-
err.Code === 'NotImplemented' ||
|
|
286
|
-
err.name === 'NotImplemented' ||
|
|
287
|
-
err.message?.includes('not well-formed') ||
|
|
288
|
-
err.message?.includes('not implemented') ||
|
|
289
|
-
err.message?.includes('CORSConfiguration')) {
|
|
279
|
+
// Some S3-compatible backends (e.g. older MinIO) don't implement
|
|
280
|
+
// PutBucketCors.
|
|
281
|
+
if (isUnsupportedOperation(err)) {
|
|
290
282
|
return;
|
|
291
283
|
}
|
|
292
284
|
throw new ProvisionerError('CORS_FAILED', `Failed to set CORS on '${bucketName}': ${err.message}`, err);
|
|
293
285
|
}
|
|
294
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Remove the CORS configuration from an S3 bucket, so no browser origin may
|
|
289
|
+
* reach it. A bucket that has none already is the converged state.
|
|
290
|
+
*/
|
|
291
|
+
async clearCors(bucketName) {
|
|
292
|
+
try {
|
|
293
|
+
await this.client.send(new DeleteBucketCorsCommand({ Bucket: bucketName }));
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
if (errorCode(err) === 'NoSuchCORSConfiguration' || isUnsupportedOperation(err)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
throw new ProvisionerError('CORS_FAILED', `Failed to clear CORS on '${bucketName}': ${err.message}`, err);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
295
302
|
/**
|
|
296
303
|
* Enable versioning on an S3 bucket.
|
|
297
304
|
*
|
|
298
|
-
*
|
|
299
|
-
* does not
|
|
300
|
-
* since not all providers implement this API.
|
|
305
|
+
* Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
|
|
306
|
+
* answers that it does not implement PutBucketVersioning.
|
|
301
307
|
*/
|
|
302
308
|
async enableVersioning(bucketName) {
|
|
303
309
|
try {
|
|
@@ -307,16 +313,9 @@ export class BucketProvisioner {
|
|
|
307
313
|
}));
|
|
308
314
|
}
|
|
309
315
|
catch (err) {
|
|
310
|
-
// Some S3-compatible backends (e.g. MinIO edge-cicd) don't
|
|
311
|
-
// PutBucketVersioning.
|
|
312
|
-
|
|
313
|
-
if (err.Code === 'XmlParseException' ||
|
|
314
|
-
err.name === 'XmlParseException' ||
|
|
315
|
-
err.Code === 'NotImplemented' ||
|
|
316
|
-
err.name === 'NotImplemented' ||
|
|
317
|
-
err.message?.includes('not well-formed') ||
|
|
318
|
-
err.message?.includes('not implemented') ||
|
|
319
|
-
err.message?.includes('VersioningConfiguration')) {
|
|
316
|
+
// Some S3-compatible backends (e.g. MinIO edge-cicd) don't implement
|
|
317
|
+
// PutBucketVersioning.
|
|
318
|
+
if (isUnsupportedOperation(err)) {
|
|
320
319
|
return;
|
|
321
320
|
}
|
|
322
321
|
throw new ProvisionerError('VERSIONING_FAILED', `Failed to enable versioning on '${bucketName}': ${err.message}`, err);
|
|
@@ -325,9 +324,8 @@ export class BucketProvisioner {
|
|
|
325
324
|
/**
|
|
326
325
|
* Set lifecycle rules on an S3 bucket.
|
|
327
326
|
*
|
|
328
|
-
*
|
|
329
|
-
* does not
|
|
330
|
-
* are best-effort since not all providers implement this API.
|
|
327
|
+
* Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
|
|
328
|
+
* answers that it does not implement PutBucketLifecycleConfiguration.
|
|
331
329
|
*/
|
|
332
330
|
async setLifecycleRules(bucketName, rules) {
|
|
333
331
|
try {
|
|
@@ -344,20 +342,10 @@ export class BucketProvisioner {
|
|
|
344
342
|
}));
|
|
345
343
|
}
|
|
346
344
|
catch (err) {
|
|
347
|
-
// Some S3-compatible backends (e.g. MinIO edge-cicd) don't
|
|
348
|
-
// PutBucketLifecycleConfiguration
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
if (err.Code === 'XmlParseException' ||
|
|
352
|
-
err.name === 'XmlParseException' ||
|
|
353
|
-
err.Code === 'NotImplemented' ||
|
|
354
|
-
err.name === 'NotImplemented' ||
|
|
355
|
-
err.Code === 'MissingContentMD5' ||
|
|
356
|
-
err.name === 'MissingContentMD5' ||
|
|
357
|
-
err.message?.includes('not well-formed') ||
|
|
358
|
-
err.message?.includes('not implemented') ||
|
|
359
|
-
err.message?.includes('Content-Md5') ||
|
|
360
|
-
err.message?.includes('LifecycleConfiguration')) {
|
|
345
|
+
// Some S3-compatible backends (e.g. MinIO edge-cicd) don't implement
|
|
346
|
+
// PutBucketLifecycleConfiguration, or require a Content-MD5 header the
|
|
347
|
+
// SDK doesn't send.
|
|
348
|
+
if (isUnsupportedOperation(err, ['MissingContentMD5'])) {
|
|
361
349
|
return;
|
|
362
350
|
}
|
|
363
351
|
throw new ProvisionerError('LIFECYCLE_FAILED', `Failed to set lifecycle rules on '${bucketName}': ${err.message}`, err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/bucket-provisioner",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "S3-compatible bucket provisioning library — create buckets, configure privacy policies, CORS, and access controls",
|
|
6
6
|
"main": "index.js",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"storage",
|
|
45
45
|
"constructive"
|
|
46
46
|
],
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "8227c782c98281f7185d257a7f716fbffdcc45c2"
|
|
48
48
|
}
|
package/provisioner.d.ts
CHANGED
|
@@ -91,17 +91,16 @@ export declare class BucketProvisioner {
|
|
|
91
91
|
/**
|
|
92
92
|
* Configure S3 Block Public Access settings.
|
|
93
93
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* not all providers implement this AWS-specific API.
|
|
94
|
+
* Skips only when the S3-compatible backend (e.g. MinIO) answers that it
|
|
95
|
+
* does not implement PutPublicAccessBlock; every other failure throws.
|
|
97
96
|
*/
|
|
98
97
|
setPublicAccessBlock(bucketName: string, config: PublicAccessBlockConfig): Promise<void>;
|
|
99
98
|
/**
|
|
100
99
|
* Apply an S3 bucket policy.
|
|
101
100
|
*
|
|
102
|
-
*
|
|
103
|
-
* PutBucketPolicy
|
|
104
|
-
*
|
|
101
|
+
* Skips only when the S3-compatible backend answers that it does not
|
|
102
|
+
* implement PutBucketPolicy; every other failure (AccessDenied, a blocked
|
|
103
|
+
* public policy, a malformed document) throws.
|
|
105
104
|
*/
|
|
106
105
|
setBucketPolicy(bucketName: string, policy: BucketPolicyDocument): Promise<void>;
|
|
107
106
|
/**
|
|
@@ -114,25 +113,27 @@ export declare class BucketProvisioner {
|
|
|
114
113
|
/**
|
|
115
114
|
* Set CORS configuration on an S3 bucket.
|
|
116
115
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* implement this API via the same endpoint path.
|
|
116
|
+
* Skips only when the S3-compatible backend (e.g. older MinIO) answers
|
|
117
|
+
* that it does not implement PutBucketCors; every other failure throws.
|
|
120
118
|
*/
|
|
121
119
|
setCors(bucketName: string, rules: CorsRule[]): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Remove the CORS configuration from an S3 bucket, so no browser origin may
|
|
122
|
+
* reach it. A bucket that has none already is the converged state.
|
|
123
|
+
*/
|
|
124
|
+
clearCors(bucketName: string): Promise<void>;
|
|
122
125
|
/**
|
|
123
126
|
* Enable versioning on an S3 bucket.
|
|
124
127
|
*
|
|
125
|
-
*
|
|
126
|
-
* does not
|
|
127
|
-
* since not all providers implement this API.
|
|
128
|
+
* Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
|
|
129
|
+
* answers that it does not implement PutBucketVersioning.
|
|
128
130
|
*/
|
|
129
131
|
enableVersioning(bucketName: string): Promise<void>;
|
|
130
132
|
/**
|
|
131
133
|
* Set lifecycle rules on an S3 bucket.
|
|
132
134
|
*
|
|
133
|
-
*
|
|
134
|
-
* does not
|
|
135
|
-
* are best-effort since not all providers implement this API.
|
|
135
|
+
* Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
|
|
136
|
+
* answers that it does not implement PutBucketLifecycleConfiguration.
|
|
136
137
|
*/
|
|
137
138
|
setLifecycleRules(bucketName: string, rules: LifecycleRule[]): Promise<void>;
|
|
138
139
|
/**
|
package/provisioner.js
CHANGED
|
@@ -18,6 +18,21 @@ const cors_1 = require("./cors");
|
|
|
18
18
|
const lifecycle_1 = require("./lifecycle");
|
|
19
19
|
const policies_1 = require("./policies");
|
|
20
20
|
const types_1 = require("./types");
|
|
21
|
+
/**
|
|
22
|
+
* Error codes an S3-compatible backend answers with when it does not implement
|
|
23
|
+
* a bucket-configuration API at all (older MinIO, some RustFS builds). Matched
|
|
24
|
+
* on the error's code only — never on message text, which AWS fills with the
|
|
25
|
+
* operation and policy names of real failures (an AccessDenied for
|
|
26
|
+
* `s3:PutBucketPolicy` reads "...because no identity-based policy allows...").
|
|
27
|
+
*/
|
|
28
|
+
const UNSUPPORTED_OPERATION_CODES = ['XmlParseException', 'NotImplemented'];
|
|
29
|
+
function errorCode(err) {
|
|
30
|
+
return err?.Code ?? err?.name;
|
|
31
|
+
}
|
|
32
|
+
function isUnsupportedOperation(err, extraCodes = []) {
|
|
33
|
+
const code = errorCode(err);
|
|
34
|
+
return code !== undefined && [...UNSUPPORTED_OPERATION_CODES, ...extraCodes].includes(code);
|
|
35
|
+
}
|
|
21
36
|
/**
|
|
22
37
|
* The BucketProvisioner handles creating and configuring S3-compatible
|
|
23
38
|
* buckets with the correct privacy settings, CORS rules, and policies
|
|
@@ -94,6 +109,8 @@ class BucketProvisioner {
|
|
|
94
109
|
// A bucket no browser origin may reach gets no rule set: cross-origin
|
|
95
110
|
// access is the bucket's own recorded data, and "none recorded" is a
|
|
96
111
|
// legitimate answer, not a missing setting to invent an origin for.
|
|
112
|
+
// Provisioning converges, so "none" also clears a rule set an earlier
|
|
113
|
+
// provision wrote for origins the bucket no longer records.
|
|
97
114
|
const effectiveOrigins = options.allowedOrigins ?? this.allowedOrigins;
|
|
98
115
|
const corsRules = effectiveOrigins.length === 0
|
|
99
116
|
? []
|
|
@@ -103,6 +120,9 @@ class BucketProvisioner {
|
|
|
103
120
|
if (corsRules.length > 0) {
|
|
104
121
|
await this.setCors(bucketName, corsRules);
|
|
105
122
|
}
|
|
123
|
+
else {
|
|
124
|
+
await this.clearCors(bucketName);
|
|
125
|
+
}
|
|
106
126
|
// 5. Versioning
|
|
107
127
|
if (versioning) {
|
|
108
128
|
await this.enableVersioning(bucketName);
|
|
@@ -176,9 +196,8 @@ class BucketProvisioner {
|
|
|
176
196
|
/**
|
|
177
197
|
* Configure S3 Block Public Access settings.
|
|
178
198
|
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* not all providers implement this AWS-specific API.
|
|
199
|
+
* Skips only when the S3-compatible backend (e.g. MinIO) answers that it
|
|
200
|
+
* does not implement PutPublicAccessBlock; every other failure throws.
|
|
182
201
|
*/
|
|
183
202
|
async setPublicAccessBlock(bucketName, config) {
|
|
184
203
|
try {
|
|
@@ -188,16 +207,9 @@ class BucketProvisioner {
|
|
|
188
207
|
}));
|
|
189
208
|
}
|
|
190
209
|
catch (err) {
|
|
191
|
-
// Some S3-compatible backends (e.g. older MinIO) don't
|
|
192
|
-
// PutPublicAccessBlock
|
|
193
|
-
|
|
194
|
-
if (err.Code === 'XmlParseException' ||
|
|
195
|
-
err.name === 'XmlParseException' ||
|
|
196
|
-
err.Code === 'NotImplemented' ||
|
|
197
|
-
err.name === 'NotImplemented' ||
|
|
198
|
-
err.message?.includes('not well-formed') ||
|
|
199
|
-
err.message?.includes('not implemented') ||
|
|
200
|
-
err.message?.includes('PublicAccessBlockConfiguration')) {
|
|
210
|
+
// Some S3-compatible backends (e.g. older MinIO) don't implement
|
|
211
|
+
// PutPublicAccessBlock; the bucket is still usable without it.
|
|
212
|
+
if (isUnsupportedOperation(err)) {
|
|
201
213
|
return;
|
|
202
214
|
}
|
|
203
215
|
throw new types_1.ProvisionerError('POLICY_FAILED', `Failed to set public access block on '${bucketName}': ${err.message}`, err);
|
|
@@ -206,9 +218,9 @@ class BucketProvisioner {
|
|
|
206
218
|
/**
|
|
207
219
|
* Apply an S3 bucket policy.
|
|
208
220
|
*
|
|
209
|
-
*
|
|
210
|
-
* PutBucketPolicy
|
|
211
|
-
*
|
|
221
|
+
* Skips only when the S3-compatible backend answers that it does not
|
|
222
|
+
* implement PutBucketPolicy; every other failure (AccessDenied, a blocked
|
|
223
|
+
* public policy, a malformed document) throws.
|
|
212
224
|
*/
|
|
213
225
|
async setBucketPolicy(bucketName, policy) {
|
|
214
226
|
try {
|
|
@@ -218,15 +230,8 @@ class BucketProvisioner {
|
|
|
218
230
|
}));
|
|
219
231
|
}
|
|
220
232
|
catch (err) {
|
|
221
|
-
// Some S3-compatible backends don't
|
|
222
|
-
|
|
223
|
-
if (err.Code === 'XmlParseException' ||
|
|
224
|
-
err.name === 'XmlParseException' ||
|
|
225
|
-
err.Code === 'NotImplemented' ||
|
|
226
|
-
err.name === 'NotImplemented' ||
|
|
227
|
-
err.message?.includes('not well-formed') ||
|
|
228
|
-
err.message?.includes('not implemented') ||
|
|
229
|
-
err.message?.includes('policy')) {
|
|
233
|
+
// Some S3-compatible backends don't implement bucket policies.
|
|
234
|
+
if (isUnsupportedOperation(err)) {
|
|
230
235
|
return;
|
|
231
236
|
}
|
|
232
237
|
throw new types_1.ProvisionerError('POLICY_FAILED', `Failed to set bucket policy on '${bucketName}': ${err.message}`, err);
|
|
@@ -244,14 +249,9 @@ class BucketProvisioner {
|
|
|
244
249
|
}
|
|
245
250
|
catch (err) {
|
|
246
251
|
// No policy to delete — that's fine
|
|
247
|
-
if (err
|
|
252
|
+
if (errorCode(err) === 'NoSuchBucketPolicy' ||
|
|
248
253
|
err.$metadata?.httpStatusCode === 404 ||
|
|
249
|
-
err
|
|
250
|
-
err.name === 'XmlParseException' ||
|
|
251
|
-
err.Code === 'NotImplemented' ||
|
|
252
|
-
err.name === 'NotImplemented' ||
|
|
253
|
-
err.message?.includes('not well-formed') ||
|
|
254
|
-
err.message?.includes('not implemented')) {
|
|
254
|
+
isUnsupportedOperation(err)) {
|
|
255
255
|
return;
|
|
256
256
|
}
|
|
257
257
|
throw new types_1.ProvisionerError('POLICY_FAILED', `Failed to delete bucket policy on '${bucketName}': ${err.message}`, err);
|
|
@@ -260,9 +260,8 @@ class BucketProvisioner {
|
|
|
260
260
|
/**
|
|
261
261
|
* Set CORS configuration on an S3 bucket.
|
|
262
262
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
* implement this API via the same endpoint path.
|
|
263
|
+
* Skips only when the S3-compatible backend (e.g. older MinIO) answers
|
|
264
|
+
* that it does not implement PutBucketCors; every other failure throws.
|
|
266
265
|
*/
|
|
267
266
|
async setCors(bucketName, rules) {
|
|
268
267
|
try {
|
|
@@ -280,27 +279,34 @@ class BucketProvisioner {
|
|
|
280
279
|
}));
|
|
281
280
|
}
|
|
282
281
|
catch (err) {
|
|
283
|
-
// Some S3-compatible backends (e.g. older MinIO) don't
|
|
284
|
-
// PutBucketCors
|
|
285
|
-
|
|
286
|
-
if (err.Code === 'XmlParseException' ||
|
|
287
|
-
err.name === 'XmlParseException' ||
|
|
288
|
-
err.Code === 'NotImplemented' ||
|
|
289
|
-
err.name === 'NotImplemented' ||
|
|
290
|
-
err.message?.includes('not well-formed') ||
|
|
291
|
-
err.message?.includes('not implemented') ||
|
|
292
|
-
err.message?.includes('CORSConfiguration')) {
|
|
282
|
+
// Some S3-compatible backends (e.g. older MinIO) don't implement
|
|
283
|
+
// PutBucketCors.
|
|
284
|
+
if (isUnsupportedOperation(err)) {
|
|
293
285
|
return;
|
|
294
286
|
}
|
|
295
287
|
throw new types_1.ProvisionerError('CORS_FAILED', `Failed to set CORS on '${bucketName}': ${err.message}`, err);
|
|
296
288
|
}
|
|
297
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Remove the CORS configuration from an S3 bucket, so no browser origin may
|
|
292
|
+
* reach it. A bucket that has none already is the converged state.
|
|
293
|
+
*/
|
|
294
|
+
async clearCors(bucketName) {
|
|
295
|
+
try {
|
|
296
|
+
await this.client.send(new client_s3_1.DeleteBucketCorsCommand({ Bucket: bucketName }));
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
if (errorCode(err) === 'NoSuchCORSConfiguration' || isUnsupportedOperation(err)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
throw new types_1.ProvisionerError('CORS_FAILED', `Failed to clear CORS on '${bucketName}': ${err.message}`, err);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
298
305
|
/**
|
|
299
306
|
* Enable versioning on an S3 bucket.
|
|
300
307
|
*
|
|
301
|
-
*
|
|
302
|
-
* does not
|
|
303
|
-
* since not all providers implement this API.
|
|
308
|
+
* Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
|
|
309
|
+
* answers that it does not implement PutBucketVersioning.
|
|
304
310
|
*/
|
|
305
311
|
async enableVersioning(bucketName) {
|
|
306
312
|
try {
|
|
@@ -310,16 +316,9 @@ class BucketProvisioner {
|
|
|
310
316
|
}));
|
|
311
317
|
}
|
|
312
318
|
catch (err) {
|
|
313
|
-
// Some S3-compatible backends (e.g. MinIO edge-cicd) don't
|
|
314
|
-
// PutBucketVersioning.
|
|
315
|
-
|
|
316
|
-
if (err.Code === 'XmlParseException' ||
|
|
317
|
-
err.name === 'XmlParseException' ||
|
|
318
|
-
err.Code === 'NotImplemented' ||
|
|
319
|
-
err.name === 'NotImplemented' ||
|
|
320
|
-
err.message?.includes('not well-formed') ||
|
|
321
|
-
err.message?.includes('not implemented') ||
|
|
322
|
-
err.message?.includes('VersioningConfiguration')) {
|
|
319
|
+
// Some S3-compatible backends (e.g. MinIO edge-cicd) don't implement
|
|
320
|
+
// PutBucketVersioning.
|
|
321
|
+
if (isUnsupportedOperation(err)) {
|
|
323
322
|
return;
|
|
324
323
|
}
|
|
325
324
|
throw new types_1.ProvisionerError('VERSIONING_FAILED', `Failed to enable versioning on '${bucketName}': ${err.message}`, err);
|
|
@@ -328,9 +327,8 @@ class BucketProvisioner {
|
|
|
328
327
|
/**
|
|
329
328
|
* Set lifecycle rules on an S3 bucket.
|
|
330
329
|
*
|
|
331
|
-
*
|
|
332
|
-
* does not
|
|
333
|
-
* are best-effort since not all providers implement this API.
|
|
330
|
+
* Skips only when the S3-compatible backend (e.g. MinIO edge-cicd)
|
|
331
|
+
* answers that it does not implement PutBucketLifecycleConfiguration.
|
|
334
332
|
*/
|
|
335
333
|
async setLifecycleRules(bucketName, rules) {
|
|
336
334
|
try {
|
|
@@ -347,20 +345,10 @@ class BucketProvisioner {
|
|
|
347
345
|
}));
|
|
348
346
|
}
|
|
349
347
|
catch (err) {
|
|
350
|
-
// Some S3-compatible backends (e.g. MinIO edge-cicd) don't
|
|
351
|
-
// PutBucketLifecycleConfiguration
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
if (err.Code === 'XmlParseException' ||
|
|
355
|
-
err.name === 'XmlParseException' ||
|
|
356
|
-
err.Code === 'NotImplemented' ||
|
|
357
|
-
err.name === 'NotImplemented' ||
|
|
358
|
-
err.Code === 'MissingContentMD5' ||
|
|
359
|
-
err.name === 'MissingContentMD5' ||
|
|
360
|
-
err.message?.includes('not well-formed') ||
|
|
361
|
-
err.message?.includes('not implemented') ||
|
|
362
|
-
err.message?.includes('Content-Md5') ||
|
|
363
|
-
err.message?.includes('LifecycleConfiguration')) {
|
|
348
|
+
// Some S3-compatible backends (e.g. MinIO edge-cicd) don't implement
|
|
349
|
+
// PutBucketLifecycleConfiguration, or require a Content-MD5 header the
|
|
350
|
+
// SDK doesn't send.
|
|
351
|
+
if (isUnsupportedOperation(err, ['MissingContentMD5'])) {
|
|
364
352
|
return;
|
|
365
353
|
}
|
|
366
354
|
throw new types_1.ProvisionerError('LIFECYCLE_FAILED', `Failed to set lifecycle rules on '${bucketName}': ${err.message}`, err);
|