@reventlessdev/reventless-aws 3.0.0-alpha.239 → 3.0.0-alpha.241
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/CHANGELOG.md +14 -0
- package/package.json +7 -7
- package/src/Platform.res +292 -15
- package/src/Platform.res.mjs +362 -48
- package/src/adapter/Upload/Upload_Presign_S3.res +19 -3
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +9 -9
- package/src/capability/Capability_ObjectStore_S3.res +69 -7
- package/src/capability/Capability_ObjectStore_S3.res.mjs +32 -6
- package/src/plugin/stack/Plugin_Stack.res +209 -6
- package/src/plugin/stack/Plugin_Stack.res.mjs +122 -6
- package/src/util/Util_HostUiDomain.res +17 -0
- package/src/util/Util_HostUiDomain.res.mjs +8 -1
- package/src/util/Util_StoreLayout.res +136 -0
- package/src/util/Util_StoreLayout.res.mjs +55 -0
- package/tests/Util_StoreLayoutTest.res +155 -0
- package/tests/Util_StoreLayoutTest.res.mjs +87 -0
|
@@ -20,9 +20,20 @@
|
|
|
20
20
|
// both cost and delete semantics and belongs to a deployment's judgement rather
|
|
21
21
|
// than to a framework default.
|
|
22
22
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// A store's lifecycle now follows a *declaration*, which changes the risk. A
|
|
24
|
+
// hand-written bucket could only be destroyed by editing a line of Pulumi; a
|
|
25
|
+
// declared one can be destroyed by renaming a field, because the last
|
|
26
|
+
// `@storageRef("productImages")` disappearing removes the requirement. Hence
|
|
27
|
+
// `~protect`, on by default and turned off only for stacks that are routinely
|
|
28
|
+
// torn down — see `Util_StoreLayout.protectionFor`, which decides that from
|
|
29
|
+
// disposability rather than from the store's layout.
|
|
30
|
+
//
|
|
31
|
+
// Not to be confused with the framework's other buckets: a **store** is
|
|
32
|
+
// declared by a field's type and holds values that events reference, so its
|
|
33
|
+
// objects outlive any single deploy. A **task bucket** (`TaskBucket_S3`) is
|
|
34
|
+
// wired by a slice and holds transient input, and the plugin-bundle and
|
|
35
|
+
// host-ui-bundle buckets are hosting substrate. Neither has a `@storageRef`
|
|
36
|
+
// field pointing at it, and neither is provisioned from a declaration.
|
|
26
37
|
|
|
27
38
|
open PulumiAws
|
|
28
39
|
|
|
@@ -39,24 +50,62 @@ let defaultCorsRules: S3.Bucket.corsRules = [
|
|
|
39
50
|
]
|
|
40
51
|
|
|
41
52
|
/** Create an object store bucket with framework attribution tags and an
|
|
42
|
-
all-true public-access block.
|
|
53
|
+
all-true public-access block.
|
|
54
|
+
|
|
55
|
+
`~keyPrefix` is the path this store's object keys are rooted at; it travels
|
|
56
|
+
in the returned record so the mint side and the serve side read one value
|
|
57
|
+
instead of restating it.
|
|
58
|
+
|
|
59
|
+
`~plugin` attributes the store to the plugin whose field declared it. The
|
|
60
|
+
bucket is created by the platform deploy — that is where the serving CDN and
|
|
61
|
+
the presign services live, and where a shared bucket has a single owner — so
|
|
62
|
+
ownership is expressed in the tags rather than in which stack ran `make`.
|
|
63
|
+
|
|
64
|
+
`~protect` (default on) blocks `pulumi destroy` and accidental replacement.
|
|
65
|
+
`~forceDestroy` is its counterpart for disposable stacks: a protected bucket
|
|
66
|
+
cannot be torn down, so a PR stack would leak exactly the buckets that
|
|
67
|
+
sharing exists to save. */
|
|
43
68
|
let make = (
|
|
44
69
|
~name: string,
|
|
70
|
+
~keyPrefix: string=Upload_Presign_S3.defaultServedPrefix,
|
|
71
|
+
~plugin: option<string>=?,
|
|
72
|
+
~protect: bool=true,
|
|
73
|
+
~forceDestroy: bool=false,
|
|
45
74
|
~corsRules: S3.Bucket.corsRules=defaultCorsRules,
|
|
46
75
|
~opts: option<Pulumi.CustomResourceOptions.t>=?,
|
|
47
76
|
): ReventlessInfra.Platform.objectStore => {
|
|
77
|
+
// A declared store is plugin substrate; the legacy hand-written store, which
|
|
78
|
+
// names no plugin, stays platform-scoped so its tags are unchanged.
|
|
79
|
+
let (kind, scope) = switch plugin {
|
|
80
|
+
| Some(_) => (
|
|
81
|
+
ReventlessCore.ComponentType.Plugin,
|
|
82
|
+
ReventlessCore.ResourceAttribution.Scope.Plugin,
|
|
83
|
+
)
|
|
84
|
+
| None => (
|
|
85
|
+
ReventlessCore.ComponentType.Platform,
|
|
86
|
+
ReventlessCore.ResourceAttribution.Scope.Platform,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let opts: Pulumi.CustomResourceOptions.t = {
|
|
91
|
+
...opts->Option.getOr({}),
|
|
92
|
+
protect,
|
|
93
|
+
}
|
|
94
|
+
|
|
48
95
|
let bucket = S3.Bucket.make(
|
|
49
96
|
~name,
|
|
50
97
|
~args={
|
|
51
98
|
corsRules: corsRules->Pulumi.Input.make,
|
|
99
|
+
forceDestroy: forceDestroy->Pulumi.Input.make,
|
|
52
100
|
tags: AWS.Tags.make(
|
|
53
101
|
~name,
|
|
54
|
-
~kind
|
|
102
|
+
~kind,
|
|
55
103
|
~role=Other("ObjectStore"),
|
|
56
|
-
~scope
|
|
104
|
+
~scope,
|
|
105
|
+
~plugin?,
|
|
57
106
|
),
|
|
58
107
|
},
|
|
59
|
-
~opts
|
|
108
|
+
~opts,
|
|
60
109
|
)
|
|
61
110
|
|
|
62
111
|
// A served store is read through the UI's CDN via an origin access control,
|
|
@@ -80,5 +129,18 @@ let make = (
|
|
|
80
129
|
bucketId: bucket.id->Pulumi.Output.asInput,
|
|
81
130
|
bucketArn: bucket.arn->Pulumi.Output.asInput,
|
|
82
131
|
bucketRegionalDomainName: bucket.bucketRegionalDomainName->Pulumi.Output.asInput,
|
|
132
|
+
keyPrefix,
|
|
83
133
|
}
|
|
84
134
|
}
|
|
135
|
+
|
|
136
|
+
/** The same bucket, addressed under a different key prefix.
|
|
137
|
+
|
|
138
|
+
A shared-layout stack puts every declared store in one bucket, so the bucket
|
|
139
|
+
is created once and each store is that bucket seen through its own prefix.
|
|
140
|
+
Splitting this out keeps the memoisation of "one bucket per physical name"
|
|
141
|
+
separate from "one store per declaration", which are different cardinalities
|
|
142
|
+
the moment a layout shares. */
|
|
143
|
+
let underPrefix = (
|
|
144
|
+
store: ReventlessInfra.Platform.objectStore,
|
|
145
|
+
~keyPrefix: string,
|
|
146
|
+
): ReventlessInfra.Platform.objectStore => {...store, keyPrefix}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Aws from "@pulumi/aws";
|
|
4
|
-
import * as
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as AWS_Tags$ReventlessAws from "../adapter/AWS_Tags.res.mjs";
|
|
6
|
+
import * as Upload_Presign_S3$ReventlessAws from "../adapter/Upload/Upload_Presign_S3.res.mjs";
|
|
6
7
|
|
|
7
8
|
let defaultCorsRules = [{
|
|
8
9
|
allowedHeaders: ["*"],
|
|
@@ -17,15 +18,28 @@ let defaultCorsRules = [{
|
|
|
17
18
|
maxAgeSeconds: 3000
|
|
18
19
|
}];
|
|
19
20
|
|
|
20
|
-
function make(name, corsRulesOpt, opts) {
|
|
21
|
+
function make(name, keyPrefixOpt, plugin, protectOpt, forceDestroyOpt, corsRulesOpt, opts) {
|
|
22
|
+
let keyPrefix = keyPrefixOpt !== undefined ? keyPrefixOpt : Upload_Presign_S3$ReventlessAws.defaultServedPrefix;
|
|
23
|
+
let protect = protectOpt !== undefined ? protectOpt : true;
|
|
24
|
+
let forceDestroy = forceDestroyOpt !== undefined ? forceDestroyOpt : false;
|
|
21
25
|
let corsRules = corsRulesOpt !== undefined ? corsRulesOpt : defaultCorsRules;
|
|
26
|
+
let match = plugin !== undefined ? [
|
|
27
|
+
"Plugin",
|
|
28
|
+
"Plugin"
|
|
29
|
+
] : [
|
|
30
|
+
"Platform",
|
|
31
|
+
"Platform"
|
|
32
|
+
];
|
|
33
|
+
let newrecord = {...Stdlib_Option.getOr(opts, {})};
|
|
34
|
+
newrecord.protect = protect;
|
|
22
35
|
let bucket = new (Aws.s3.Bucket)(name, {
|
|
23
36
|
corsRules: corsRules,
|
|
24
|
-
|
|
37
|
+
forceDestroy: forceDestroy,
|
|
38
|
+
tags: AWS_Tags$ReventlessAws.make(name, match[0], {
|
|
25
39
|
TAG: "Other",
|
|
26
40
|
_0: "ObjectStore"
|
|
27
|
-
},
|
|
28
|
-
},
|
|
41
|
+
}, match[1], undefined, undefined, plugin, undefined)
|
|
42
|
+
}, newrecord);
|
|
29
43
|
new (Aws.s3.BucketPublicAccessBlock)(name + "-pab", {
|
|
30
44
|
bucket: bucket.id,
|
|
31
45
|
blockPublicAcls: true,
|
|
@@ -37,12 +51,24 @@ function make(name, corsRulesOpt, opts) {
|
|
|
37
51
|
bucketName: bucket.bucket,
|
|
38
52
|
bucketId: bucket.id,
|
|
39
53
|
bucketArn: bucket.arn,
|
|
40
|
-
bucketRegionalDomainName: bucket.bucketRegionalDomainName
|
|
54
|
+
bucketRegionalDomainName: bucket.bucketRegionalDomainName,
|
|
55
|
+
keyPrefix: keyPrefix
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function underPrefix(store, keyPrefix) {
|
|
60
|
+
return {
|
|
61
|
+
bucketName: store.bucketName,
|
|
62
|
+
bucketId: store.bucketId,
|
|
63
|
+
bucketArn: store.bucketArn,
|
|
64
|
+
bucketRegionalDomainName: store.bucketRegionalDomainName,
|
|
65
|
+
keyPrefix: keyPrefix
|
|
41
66
|
};
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
export {
|
|
45
70
|
defaultCorsRules,
|
|
46
71
|
make,
|
|
72
|
+
underPrefix,
|
|
47
73
|
}
|
|
48
74
|
/* @pulumi/aws Not a pure module */
|
|
@@ -225,17 +225,26 @@ let makeUiBundleDistribution = (
|
|
|
225
225
|
// CachingOptimized policy is safe. Prepended to `orderedCacheBehaviors` so a
|
|
226
226
|
// served path is matched before the SPA-serving default behavior and never
|
|
227
227
|
// routed to the bundle bucket.
|
|
228
|
-
let servedOriginId = (
|
|
229
|
-
let servedCacheBehavior = (
|
|
228
|
+
let servedOriginId = (id: string): string => "served-" ++ id
|
|
229
|
+
let servedCacheBehavior = (
|
|
230
|
+
~prefix: string,
|
|
231
|
+
~originId: string,
|
|
232
|
+
): PulumiAws.CloudFront.Distribution.orderedCacheBehavior => {
|
|
230
233
|
pathPattern: Pulumi.Input.make(prefix ++ "/*"),
|
|
231
|
-
targetOriginId: Pulumi.Input.make(
|
|
234
|
+
targetOriginId: Pulumi.Input.make(originId),
|
|
232
235
|
viewerProtocolPolicy: Pulumi.Input.make("redirect-to-https"),
|
|
233
236
|
allowedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
234
237
|
cachedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
235
238
|
cachePolicyId: Pulumi.Input.make(cachingOptimizedPolicyId),
|
|
236
239
|
}
|
|
237
240
|
let orderedCacheBehaviors = Array.concat(
|
|
238
|
-
|
|
241
|
+
// One behavior per served prefix; several prefixes may share one origin when
|
|
242
|
+
// they live in the same bucket.
|
|
243
|
+
servedBuckets->Array.flatMap(sb =>
|
|
244
|
+
sb.prefixes->Array.map(prefix =>
|
|
245
|
+
servedCacheBehavior(~prefix, ~originId=servedOriginId(sb.id))
|
|
246
|
+
)
|
|
247
|
+
),
|
|
239
248
|
Array.concat(
|
|
240
249
|
[noCacheBehavior("/remoteEntry.js")],
|
|
241
250
|
spaFallback ? [noCacheBehavior("/" ++ indexDocument), noCacheBehavior("/config.json")] : [],
|
|
@@ -336,7 +345,7 @@ let makeUiBundleDistribution = (
|
|
|
336
345
|
// per-bucket read grant is the served bucket's own BucketPolicy below).
|
|
337
346
|
servedBuckets->Array.map(sb => {
|
|
338
347
|
PulumiAws.CloudFront.Distribution.domainName: sb.bucketRegionalDomainName,
|
|
339
|
-
originId: Pulumi.Input.make(servedOriginId(sb.
|
|
348
|
+
originId: Pulumi.Input.make(servedOriginId(sb.id)),
|
|
340
349
|
originAccessControlId: Pulumi.Input.make(oacId),
|
|
341
350
|
}),
|
|
342
351
|
)
|
|
@@ -432,9 +441,13 @@ let makeUiBundleDistribution = (
|
|
|
432
441
|
// object is fetchable at `https://<ui-domain>/{prefix}/<key>` while the bucket
|
|
433
442
|
// stays private (direct S3 GET is 403). Mirrors the bundle bucket policy above;
|
|
434
443
|
// the served bucket keeps its own all-true BucketPublicAccessBlock (app-owned).
|
|
444
|
+
//
|
|
445
|
+
// Exactly one policy per bucket — S3 permits no more. Grouping prefixes into
|
|
446
|
+
// one `servedBucket` is what makes that structural rather than a rule to
|
|
447
|
+
// remember.
|
|
435
448
|
servedBuckets->Array.forEach(sb => {
|
|
436
449
|
let _ = PulumiAws.S3.BucketPolicy.make(
|
|
437
|
-
~name=name ++ "-served-" ++ sb.
|
|
450
|
+
~name=name ++ "-served-" ++ sb.id ++ "-policy",
|
|
438
451
|
~args={
|
|
439
452
|
bucket: sb.bucketId,
|
|
440
453
|
policy: (sb.bucketArn->Pulumi.Output.fromInput, distribution.arn)
|
|
@@ -525,3 +538,193 @@ let makeUiBundleDistribution = (
|
|
|
525
538
|
bucketName: bucket.bucket,
|
|
526
539
|
}
|
|
527
540
|
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
Front a set of served buckets with a CloudFront distribution of their own, with
|
|
544
|
+
no SPA bundle behind it.
|
|
545
|
+
|
|
546
|
+
`makeUiBundleDistribution` can already serve buckets, but only as a side car to
|
|
547
|
+
a UI bundle. A platform that deploys no shell — because its UI ships from its
|
|
548
|
+
own stack — still provisions stores and then has nothing to serve them from:
|
|
549
|
+
the bucket is created with an all-true public-access block and takes its read
|
|
550
|
+
grant solely from a distribution's `BucketPolicy`, so with no distribution the
|
|
551
|
+
objects are unreachable by anything.
|
|
552
|
+
|
|
553
|
+
Serving lives with the stack that owns the buckets, and that is not a style
|
|
554
|
+
preference. **S3 permits exactly one bucket policy per bucket.** If the read
|
|
555
|
+
grant were written by whichever stack happened to build a distribution, two
|
|
556
|
+
distributions fronting one store would silently overwrite each other's grant —
|
|
557
|
+
deploying green and 404ing the loser's objects. Keeping the policy next to the
|
|
558
|
+
bucket makes a second writer impossible rather than merely discouraged.
|
|
559
|
+
|
|
560
|
+
Returns the public base URL. A consumer needs that and the store's prefix to
|
|
561
|
+
address an object; it needs no bucket identity at all, which is both a smaller
|
|
562
|
+
contract and one that cannot collide.
|
|
563
|
+
*/
|
|
564
|
+
let makeServedBucketDistribution = (
|
|
565
|
+
~name: string,
|
|
566
|
+
~servedBuckets: array<ReventlessInfra.Platform.servedBucket>,
|
|
567
|
+
~customDomain: option<customDomain>=?,
|
|
568
|
+
): Pulumi.Output.t<string> => {
|
|
569
|
+
let oac = PulumiAws.CloudFront.OriginAccessControl.make(
|
|
570
|
+
~name=name ++ "-oac",
|
|
571
|
+
~args={
|
|
572
|
+
originAccessControlOriginType: Pulumi.Input.make("s3"),
|
|
573
|
+
signingBehavior: Pulumi.Input.make("always"),
|
|
574
|
+
signingProtocol: Pulumi.Input.make("sigv4"),
|
|
575
|
+
},
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
let originIdFor = (id: string): string => "served-" ++ id
|
|
579
|
+
|
|
580
|
+
// CloudFront requires a default behavior, and with no bundle origin there is
|
|
581
|
+
// nothing neutral to point it at. The first served bucket takes it; every
|
|
582
|
+
// real path is matched by its own `{prefix}/*` behavior first, so the default
|
|
583
|
+
// is only reached by a request for a path no store claims — which 404s at S3,
|
|
584
|
+
// the correct answer.
|
|
585
|
+
let defaultOriginId = switch servedBuckets->Array.get(0) {
|
|
586
|
+
| Some(sb) => originIdFor(sb.id)
|
|
587
|
+
| None =>
|
|
588
|
+
JsError.throwWithMessage(
|
|
589
|
+
"Plugin_Stack.makeServedBucketDistribution: no served buckets — call only when at least one store is declared",
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let viewerCertificate: PulumiAws.CloudFront.Distribution.viewerCertificate = switch customDomain {
|
|
594
|
+
| None => {
|
|
595
|
+
cloudfrontDefaultCertificate: Pulumi.Input.make(true),
|
|
596
|
+
}
|
|
597
|
+
| Some({fqdn, hostedZoneId: _}) =>
|
|
598
|
+
let usEast1 = _getUsEast1Provider()
|
|
599
|
+
let cert = PulumiAws.Acm.Certificate.make(
|
|
600
|
+
~name=name ++ "-cert",
|
|
601
|
+
~args={
|
|
602
|
+
domainName: Pulumi.Input.make(fqdn),
|
|
603
|
+
validationMethod: Pulumi.Input.make("DNS"),
|
|
604
|
+
},
|
|
605
|
+
~opts={provider: usEast1},
|
|
606
|
+
)
|
|
607
|
+
{
|
|
608
|
+
acmCertificateArn: cert.arn->Pulumi.Output.asInput,
|
|
609
|
+
sslSupportMethod: Pulumi.Input.make("sni-only"),
|
|
610
|
+
minimumProtocolVersion: Pulumi.Input.make("TLSv1.2_2021"),
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let distribution = PulumiAws.CloudFront.Distribution.make(
|
|
615
|
+
~name=name ++ "-cdn",
|
|
616
|
+
~args={
|
|
617
|
+
enabled: Pulumi.Input.make(true),
|
|
618
|
+
aliases: ?switch customDomain {
|
|
619
|
+
| None => None
|
|
620
|
+
| Some({fqdn}) => Some(Pulumi.Input.make([fqdn]))
|
|
621
|
+
},
|
|
622
|
+
origins: oac.id->Pulumi.Output.apply(oacId =>
|
|
623
|
+
servedBuckets->Array.map(sb => {
|
|
624
|
+
PulumiAws.CloudFront.Distribution.domainName: sb.bucketRegionalDomainName,
|
|
625
|
+
originId: Pulumi.Input.make(originIdFor(sb.id)),
|
|
626
|
+
originAccessControlId: Pulumi.Input.make(oacId),
|
|
627
|
+
})
|
|
628
|
+
)
|
|
629
|
+
->Pulumi.Output.asInput,
|
|
630
|
+
defaultCacheBehavior: Pulumi.Input.make(
|
|
631
|
+
(
|
|
632
|
+
{
|
|
633
|
+
targetOriginId: Pulumi.Input.make(defaultOriginId),
|
|
634
|
+
viewerProtocolPolicy: Pulumi.Input.make("redirect-to-https"),
|
|
635
|
+
allowedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
636
|
+
cachedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
637
|
+
cachePolicyId: Pulumi.Input.make(cachingOptimizedPolicyId),
|
|
638
|
+
}: PulumiAws.CloudFront.Distribution.defaultCacheBehavior
|
|
639
|
+
),
|
|
640
|
+
),
|
|
641
|
+
// One behavior per served prefix. Store objects have immutable uuid keys,
|
|
642
|
+
// so the long-TTL CachingOptimized policy is safe.
|
|
643
|
+
orderedCacheBehaviors: Pulumi.Input.make(
|
|
644
|
+
servedBuckets->Array.flatMap(sb =>
|
|
645
|
+
sb.prefixes->Array.map(prefix => {
|
|
646
|
+
PulumiAws.CloudFront.Distribution.pathPattern: Pulumi.Input.make(prefix ++ "/*"),
|
|
647
|
+
targetOriginId: Pulumi.Input.make(originIdFor(sb.id)),
|
|
648
|
+
viewerProtocolPolicy: Pulumi.Input.make("redirect-to-https"),
|
|
649
|
+
allowedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
650
|
+
cachedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
651
|
+
cachePolicyId: Pulumi.Input.make(cachingOptimizedPolicyId),
|
|
652
|
+
})
|
|
653
|
+
),
|
|
654
|
+
),
|
|
655
|
+
restrictions: Pulumi.Input.make({
|
|
656
|
+
PulumiAws.CloudFront.Distribution.geoRestriction: Pulumi.Input.make({
|
|
657
|
+
PulumiAws.CloudFront.Distribution.restrictionType: Pulumi.Input.make("none"),
|
|
658
|
+
}),
|
|
659
|
+
}),
|
|
660
|
+
viewerCertificate: Pulumi.Input.make(viewerCertificate),
|
|
661
|
+
comment: Pulumi.Input.make(name ++ " object store CDN"),
|
|
662
|
+
tags: AWS.Tags.make(
|
|
663
|
+
~name=name ++ "-cdn",
|
|
664
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
665
|
+
~role=Hosting,
|
|
666
|
+
~scope=Platform,
|
|
667
|
+
),
|
|
668
|
+
},
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
switch customDomain {
|
|
672
|
+
| None => ()
|
|
673
|
+
| Some({fqdn, hostedZoneId}) =>
|
|
674
|
+
let _ = PulumiAws.Route53.Record.make(
|
|
675
|
+
~name=name ++ "-domain-alias",
|
|
676
|
+
~args={
|
|
677
|
+
zoneId: Pulumi.Input.make(hostedZoneId),
|
|
678
|
+
name: Pulumi.Input.make(fqdn),
|
|
679
|
+
type_: Pulumi.Input.make("A"),
|
|
680
|
+
aliases: [
|
|
681
|
+
(
|
|
682
|
+
{
|
|
683
|
+
name: distribution.domainName->Pulumi.Output.asInput,
|
|
684
|
+
zoneId: Pulumi.Input.make(cloudFrontAliasZoneId),
|
|
685
|
+
evaluateTargetHealth: Pulumi.Input.make(false),
|
|
686
|
+
}: PulumiAws.Route53.Record.alias
|
|
687
|
+
),
|
|
688
|
+
]->Pulumi.Input.make,
|
|
689
|
+
},
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// One policy per bucket — see the module doc above for why this is here and
|
|
694
|
+
// not in the consuming stack.
|
|
695
|
+
servedBuckets->Array.forEach(sb => {
|
|
696
|
+
let _ = PulumiAws.S3.BucketPolicy.make(
|
|
697
|
+
~name=name ++ "-served-" ++ sb.id ++ "-policy",
|
|
698
|
+
~args={
|
|
699
|
+
bucket: sb.bucketId,
|
|
700
|
+
policy: (sb.bucketArn->Pulumi.Output.fromInput, distribution.arn)
|
|
701
|
+
->Pulumi.Output.all2
|
|
702
|
+
->Pulumi.Output.apply(((bucketArn, distributionArn)) =>
|
|
703
|
+
{
|
|
704
|
+
"Version": "2012-10-17",
|
|
705
|
+
"Statement": [
|
|
706
|
+
{
|
|
707
|
+
"Sid": "AllowCloudFrontServicePrincipal",
|
|
708
|
+
"Effect": "Allow",
|
|
709
|
+
"Principal": {"Service": "cloudfront.amazonaws.com"},
|
|
710
|
+
"Action": "s3:GetObject",
|
|
711
|
+
"Resource": bucketArn ++ "/*",
|
|
712
|
+
"Condition": {
|
|
713
|
+
"StringEquals": {"AWS:SourceArn": distributionArn},
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
}
|
|
718
|
+
->JSON.stringifyAny
|
|
719
|
+
->Option.getUnsafe
|
|
720
|
+
)
|
|
721
|
+
->Pulumi.Output.asInput,
|
|
722
|
+
},
|
|
723
|
+
)
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
switch customDomain {
|
|
727
|
+
| Some({fqdn}) => Pulumi.Output.make("https://" ++ fqdn)
|
|
728
|
+
| None => distribution.domainName->Pulumi.Output.apply(d => "https://" ++ d)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
@@ -119,11 +119,11 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
|
|
|
119
119
|
],
|
|
120
120
|
cachePolicyId: cachingDisabledPolicyId
|
|
121
121
|
});
|
|
122
|
-
let orderedCacheBehaviors = servedBuckets.
|
|
123
|
-
let
|
|
122
|
+
let orderedCacheBehaviors = servedBuckets.flatMap(sb => sb.prefixes.map(prefix => {
|
|
123
|
+
let originId = "served-" + sb.id;
|
|
124
124
|
return {
|
|
125
125
|
pathPattern: prefix + "/*",
|
|
126
|
-
targetOriginId:
|
|
126
|
+
targetOriginId: originId,
|
|
127
127
|
viewerProtocolPolicy: "redirect-to-https",
|
|
128
128
|
allowedMethods: [
|
|
129
129
|
"GET",
|
|
@@ -135,7 +135,7 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
|
|
|
135
135
|
],
|
|
136
136
|
cachePolicyId: cachingOptimizedPolicyId
|
|
137
137
|
};
|
|
138
|
-
}).concat([noCacheBehavior("/remoteEntry.js")].concat(spaFallback ? [
|
|
138
|
+
})).concat([noCacheBehavior("/remoteEntry.js")].concat(spaFallback ? [
|
|
139
139
|
noCacheBehavior("/" + indexDocument),
|
|
140
140
|
noCacheBehavior("/config.json")
|
|
141
141
|
] : []));
|
|
@@ -188,7 +188,7 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
|
|
|
188
188
|
originAccessControlId: oacId
|
|
189
189
|
}].concat(servedBuckets.map(sb => ({
|
|
190
190
|
domainName: sb.bucketRegionalDomainName,
|
|
191
|
-
originId: "served-" + sb.
|
|
191
|
+
originId: "served-" + sb.id,
|
|
192
192
|
originAccessControlId: oacId
|
|
193
193
|
})));
|
|
194
194
|
}),
|
|
@@ -253,7 +253,7 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
|
|
|
253
253
|
}))
|
|
254
254
|
});
|
|
255
255
|
servedBuckets.forEach(sb => {
|
|
256
|
-
new (Aws.s3.BucketPolicy)(name + "-served-" + sb.
|
|
256
|
+
new (Aws.s3.BucketPolicy)(name + "-served-" + sb.id + "-policy", {
|
|
257
257
|
bucket: sb.bucketId,
|
|
258
258
|
policy: Pulumi.all([
|
|
259
259
|
sb.bucketArn,
|
|
@@ -303,6 +303,121 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
|
|
|
303
303
|
};
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
function makeServedBucketDistribution(name, servedBuckets, customDomain) {
|
|
307
|
+
let oac = new (Aws.cloudfront.OriginAccessControl)(name + "-oac", {
|
|
308
|
+
originAccessControlOriginType: "s3",
|
|
309
|
+
signingBehavior: "always",
|
|
310
|
+
signingProtocol: "sigv4"
|
|
311
|
+
});
|
|
312
|
+
let sb = servedBuckets[0];
|
|
313
|
+
let defaultOriginId = sb !== undefined ? "served-" + sb.id : Stdlib_JsError.throwWithMessage("Plugin_Stack.makeServedBucketDistribution: no served buckets — call only when at least one store is declared");
|
|
314
|
+
let viewerCertificate;
|
|
315
|
+
if (customDomain !== undefined) {
|
|
316
|
+
let usEast1 = _getUsEast1Provider();
|
|
317
|
+
let cert = new (Aws.acm.Certificate)(name + "-cert", {
|
|
318
|
+
domainName: customDomain.fqdn,
|
|
319
|
+
validationMethod: "DNS"
|
|
320
|
+
}, {
|
|
321
|
+
provider: Primitive_option.some(usEast1)
|
|
322
|
+
});
|
|
323
|
+
viewerCertificate = {
|
|
324
|
+
acmCertificateArn: cert.arn,
|
|
325
|
+
sslSupportMethod: "sni-only",
|
|
326
|
+
minimumProtocolVersion: "TLSv1.2_2021"
|
|
327
|
+
};
|
|
328
|
+
} else {
|
|
329
|
+
viewerCertificate = {
|
|
330
|
+
cloudfrontDefaultCertificate: true
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
let distribution = new (Aws.cloudfront.Distribution)(name + "-cdn", {
|
|
334
|
+
enabled: true,
|
|
335
|
+
aliases: customDomain !== undefined ? [customDomain.fqdn] : undefined,
|
|
336
|
+
origins: oac.id.apply(oacId => servedBuckets.map(sb => ({
|
|
337
|
+
domainName: sb.bucketRegionalDomainName,
|
|
338
|
+
originId: "served-" + sb.id,
|
|
339
|
+
originAccessControlId: oacId
|
|
340
|
+
}))),
|
|
341
|
+
defaultCacheBehavior: {
|
|
342
|
+
targetOriginId: defaultOriginId,
|
|
343
|
+
viewerProtocolPolicy: "redirect-to-https",
|
|
344
|
+
allowedMethods: [
|
|
345
|
+
"GET",
|
|
346
|
+
"HEAD"
|
|
347
|
+
],
|
|
348
|
+
cachedMethods: [
|
|
349
|
+
"GET",
|
|
350
|
+
"HEAD"
|
|
351
|
+
],
|
|
352
|
+
cachePolicyId: cachingOptimizedPolicyId
|
|
353
|
+
},
|
|
354
|
+
orderedCacheBehaviors: servedBuckets.flatMap(sb => sb.prefixes.map(prefix => ({
|
|
355
|
+
pathPattern: prefix + "/*",
|
|
356
|
+
targetOriginId: "served-" + sb.id,
|
|
357
|
+
viewerProtocolPolicy: "redirect-to-https",
|
|
358
|
+
allowedMethods: [
|
|
359
|
+
"GET",
|
|
360
|
+
"HEAD"
|
|
361
|
+
],
|
|
362
|
+
cachedMethods: [
|
|
363
|
+
"GET",
|
|
364
|
+
"HEAD"
|
|
365
|
+
],
|
|
366
|
+
cachePolicyId: cachingOptimizedPolicyId
|
|
367
|
+
}))),
|
|
368
|
+
restrictions: {
|
|
369
|
+
geoRestriction: {
|
|
370
|
+
restrictionType: "none"
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
viewerCertificate: viewerCertificate,
|
|
374
|
+
comment: name + " object store CDN",
|
|
375
|
+
tags: AWS_Tags$ReventlessAws.make(name + "-cdn", "Platform", "Hosting", "Platform", undefined, undefined, undefined, undefined)
|
|
376
|
+
});
|
|
377
|
+
if (customDomain !== undefined) {
|
|
378
|
+
new (Aws.route53.Record)(name + "-domain-alias", {
|
|
379
|
+
zoneId: customDomain.hostedZoneId,
|
|
380
|
+
name: customDomain.fqdn,
|
|
381
|
+
type: "A",
|
|
382
|
+
aliases: [{
|
|
383
|
+
name: distribution.domainName,
|
|
384
|
+
zoneId: cloudFrontAliasZoneId,
|
|
385
|
+
evaluateTargetHealth: false
|
|
386
|
+
}]
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
servedBuckets.forEach(sb => {
|
|
390
|
+
new (Aws.s3.BucketPolicy)(name + "-served-" + sb.id + "-policy", {
|
|
391
|
+
bucket: sb.bucketId,
|
|
392
|
+
policy: Pulumi.all([
|
|
393
|
+
sb.bucketArn,
|
|
394
|
+
distribution.arn
|
|
395
|
+
]).apply(param => JSON.stringify({
|
|
396
|
+
Version: "2012-10-17",
|
|
397
|
+
Statement: [{
|
|
398
|
+
Sid: "AllowCloudFrontServicePrincipal",
|
|
399
|
+
Effect: "Allow",
|
|
400
|
+
Principal: {
|
|
401
|
+
Service: "cloudfront.amazonaws.com"
|
|
402
|
+
},
|
|
403
|
+
Action: "s3:GetObject",
|
|
404
|
+
Resource: param[0] + "/*",
|
|
405
|
+
Condition: {
|
|
406
|
+
StringEquals: {
|
|
407
|
+
"AWS:SourceArn": param[1]
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}]
|
|
411
|
+
}))
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
if (customDomain !== undefined) {
|
|
415
|
+
return Pulumi.output("https://" + customDomain.fqdn);
|
|
416
|
+
} else {
|
|
417
|
+
return distribution.domainName.apply(d => "https://" + d);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
306
421
|
export {
|
|
307
422
|
log,
|
|
308
423
|
cachingOptimizedPolicyId,
|
|
@@ -313,5 +428,6 @@ export {
|
|
|
313
428
|
cacheControlFor,
|
|
314
429
|
invalidateDistribution,
|
|
315
430
|
makeUiBundleDistribution,
|
|
431
|
+
makeServedBucketDistribution,
|
|
316
432
|
}
|
|
317
433
|
/* log Not a pure module */
|
|
@@ -26,3 +26,20 @@ let parseProdStacks = (csv: string): array<string> =>
|
|
|
26
26
|
->String.split(",")
|
|
27
27
|
->Array.map(String.trim)
|
|
28
28
|
->Array.filter(s => s !== "")
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
The stacks this deployment considers production, resolved from config.
|
|
32
|
+
|
|
33
|
+
The one notion of "is this stack production", deliberately shared rather than
|
|
34
|
+
answered twice. Domain naming asked it first; object-store layout asks it too,
|
|
35
|
+
and a second key would be two flags that can disagree — the state where a stack
|
|
36
|
+
gets a production domain and non-production storage, or the reverse, with
|
|
37
|
+
nothing to flag the mismatch.
|
|
38
|
+
|
|
39
|
+
Impure by necessity (it reads config); the derivations that consume it stay
|
|
40
|
+
pure and unit-tested.
|
|
41
|
+
*/
|
|
42
|
+
let resolveProdStacks = (): array<string> =>
|
|
43
|
+
Util_LocalConfig.get("hostUiProdStacks")
|
|
44
|
+
->Option.map(parseProdStacks)
|
|
45
|
+
->Option.getOr(defaultProdStacks)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as Util_LocalConfig$ReventlessAws from "./Util_LocalConfig.res.mjs";
|
|
3
5
|
|
|
4
6
|
let defaultProdStacks = [
|
|
5
7
|
"prod",
|
|
@@ -18,9 +20,14 @@ function parseProdStacks(csv) {
|
|
|
18
20
|
return csv.split(",").map(prim => prim.trim()).filter(s => s !== "");
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
function resolveProdStacks() {
|
|
24
|
+
return Stdlib_Option.getOr(Stdlib_Option.map(Util_LocalConfig$ReventlessAws.get("hostUiProdStacks"), parseProdStacks), defaultProdStacks);
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
export {
|
|
22
28
|
defaultProdStacks,
|
|
23
29
|
deriveFqdn,
|
|
24
30
|
parseProdStacks,
|
|
31
|
+
resolveProdStacks,
|
|
25
32
|
}
|
|
26
|
-
/*
|
|
33
|
+
/* Util_LocalConfig-ReventlessAws Not a pure module */
|