hecks 3.0.5 → 3.1.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.
- checksums.yaml +4 -4
- data/exe/hecks +24 -12
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
- data/lib/hecks/bluebook/hexagon.rb +45 -25
- data/lib/hecks/bluebook/model_check.rb +7 -7
- data/lib/hecks/chapters.rb +31 -3
- data/lib/hecks/cli/check_era.rb +1 -1
- data/lib/hecks/cli/deploy_template_diff.rb +1 -1
- data/lib/hecks/cli/doc_coverage.rb +1 -1
- data/lib/hecks/cli/domain_stub.rb +128 -0
- data/lib/hecks/cli/interview_draft.rb +246 -0
- data/lib/hecks/cli/model_check.rb +1 -1
- data/lib/hecks/cli/project_cli.rb +51 -29
- data/lib/hecks/cli/reference.rb +2 -1
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
- data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
- data/lib/hecks/cli/spec_example.rb +1 -1
- data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
- data/lib/hecks/corpus.rb +11 -8
- data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +4 -3
- data/lib/hecks/doors/cli_door.rb +13 -13
- data/lib/hecks/doors/cli_runner.rb +52 -32
- data/lib/hecks/doors/launcher_options.rb +5 -5
- data/lib/hecks/doors/mcp_door.rb +40 -18
- data/lib/hecks/doors/mcp_door_scope.rb +244 -24
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -2
- data/lib/hecks/forms/banking_presentation.rb +1 -1
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
- data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
- data/lib/hecks/framework.rb +1 -1
- data/lib/hecks/fuzzing/combination_miner.rb +1 -1
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
- data/lib/hecks/fuzzing/differential.rb +4 -2
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/form_census.rb +2 -2
- data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
- data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
- data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
- data/lib/hecks/gate/stages.yml +5 -5
- data/lib/hecks/grammar/grammar.hecksagon +2 -2
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
- data/lib/hecks/hecks/adapters/host_http.rb +4 -3
- data/lib/hecks/hecks/adapters/local_files.rb +59 -0
- data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
- data/lib/hecks/hecks/codebase.bluebook +4 -3
- data/lib/hecks/hecks/context_map.hecksagon +2 -2
- data/lib/hecks/hecks/custodian.bluebook +45 -3
- data/lib/hecks/hecks/hecks.hecksagon +10 -4
- data/lib/hecks/hecks/hecks.world +6 -6
- data/lib/hecks/hecks/site.behaviors +17 -0
- data/lib/hecks/hecks/site.world +10 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
- data/lib/hecks/projections/deploy/box/settings.rb +248 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +267 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +70 -0
- data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
- data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +165 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
- data/lib/hecks/projections/deploy/box.rb +387 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +11 -10
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
- data/lib/hecks/projections/diagrams.rb +2 -2
- data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
- data/lib/hecks/projections/site/edge/checks.rb +137 -0
- data/lib/hecks/projections/site/edge/pattern.rb +50 -0
- data/lib/hecks/projections/site/edge/rules.rb +128 -0
- data/lib/hecks/projections/site/edge.rb +136 -0
- data/lib/hecks/projections/site/regions.rb +44 -0
- data/lib/hecks/projections/site/routes_ts.rb +284 -0
- data/lib/hecks/projections/site/site_cdn.rb +78 -0
- data/lib/hecks/projections/site/table/checks.rb +124 -0
- data/lib/hecks/projections/site/table/row_builder.rb +155 -0
- data/lib/hecks/projections/site/table.rb +98 -0
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/cli_projector.rb +122 -104
- data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
- data/lib/hecks/quality_control/adapters/agent.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
- data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
- data/lib/hecks/quality_control/quality_control.bluebook +18 -18
- data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
- data/lib/hecks/runtime/registry/verification.rb +15 -29
- data/lib/hecks/runtime/registry.rb +14 -20
- data/lib/hecks/rust_build/append_optionals.rb +79 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
- data/lib/hecks/rust_build/domain_name.rb +26 -0
- data/lib/hecks/rust_build/kernel_input.rb +58 -0
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
- data/lib/hecks/rust_build/project_rust.rb +57 -24
- data/lib/hecks/rust_build/rust_literal.rb +33 -0
- data/lib/hecks/rust_build/write_if_changed.rb +86 -0
- data/lib/hecks/site/bluebook/site.bluebook +205 -0
- data/lib/hecks/site/bluebook/site.hecksagon +5 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
- data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
- data/lib/hecks/storehouse.rb +37 -7
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
- data/lib/hecks/three_zero/forms.yml +88 -88
- data/lib/hecks/tools/ci_gates.rb +1 -1
- data/lib/hecks/tools/deploy_recipe.rb +14 -6
- data/lib/hecks/tools/oidc_manifests.rb +2 -1
- data/lib/hecks/tools/sequence_script.rb +2 -2
- data/lib/hecks/tools/site_routes.rb +152 -0
- data/lib/hecks/tools/tenant_provisioning.rb +1 -1
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/rust/build/src/cargo_sync.rs +1 -1
- data/rust/build/src/pipeline.rs +23 -16
- data/rust/build/src/resolve.rs +39 -6
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +87 -7
- data/rust/host/src/needs.rs +111 -0
- data/rust/parser/src/keywords.rs +7 -6
- data/rust/parser/src/main.rs +4 -3
- data/rust/parser/src/parse/chapter.rs +1 -1
- data/rust/parser/src/parse/hecksagon.rs +25 -10
- data/rust/project/mutations.rb +4 -30
- data/rust/project/naming.rb +8 -28
- data/rust/project/write_if_changed.rb +4 -80
- data/rust/project_rust_pipeline.rb +16 -12
- data/rust/src/kernel/cli.rs +2 -0
- data/rust/src/kernel/needs.rs +74 -1
- metadata +37 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
AWSTemplateFormatVersion: "2010-09-09"
|
|
2
|
+
Description: >
|
|
3
|
+
Plain RDS Postgres for @@STACK@@, read by the app box (box.yaml). Private subnets only; the box's own
|
|
4
|
+
stack admits itself on 5432, so nothing else can reach it. The master password is managed by RDS in
|
|
5
|
+
Secrets Manager ({username, password}), which the box reads at deploy time.
|
|
6
|
+
|
|
7
|
+
Parameters:
|
|
8
|
+
VpcId:
|
|
9
|
+
Type: AWS::EC2::VPC::Id
|
|
10
|
+
PrivateSubnetIds:
|
|
11
|
+
Type: List<AWS::EC2::Subnet::Id>
|
|
12
|
+
Description: At least two subnets in different availability zones (an RDS subnet group needs them).
|
|
13
|
+
InstanceClass:
|
|
14
|
+
Type: String
|
|
15
|
+
Default: @@DB_CLASS@@
|
|
16
|
+
AllocatedStorageGb:
|
|
17
|
+
Type: Number
|
|
18
|
+
Default: @@STORAGE_GB@@
|
|
19
|
+
DatabaseName:
|
|
20
|
+
Type: String
|
|
21
|
+
Default: @@DB_NAME@@
|
|
22
|
+
EngineVersion:
|
|
23
|
+
Type: String
|
|
24
|
+
Default: "@@ENGINE_VERSION@@"
|
|
25
|
+
Description: A major version ("16") lets RDS pick its current minor release.
|
|
26
|
+
BackupRetentionDays:
|
|
27
|
+
Type: Number
|
|
28
|
+
Default: @@BACKUP_DAYS@@
|
|
29
|
+
ExistingAlertTopicArn:
|
|
30
|
+
Type: String
|
|
31
|
+
Default: ""
|
|
32
|
+
Description: An SNS topic that already has subscribers. Blank creates a new topic.
|
|
33
|
+
AlertEmail:
|
|
34
|
+
Type: String
|
|
35
|
+
Default: ""
|
|
36
|
+
Description: Subscribed to a new topic (AWS sends one confirmation link). Ignored when ExistingAlertTopicArn is set.
|
|
37
|
+
Rehearsal:
|
|
38
|
+
Type: String
|
|
39
|
+
Default: "false"
|
|
40
|
+
AllowedValues: ["true", "false"]
|
|
41
|
+
Description: >
|
|
42
|
+
true is a throwaway copy (no deletion protection, no final snapshot). false is the real
|
|
43
|
+
database: deletion protection on and a snapshot taken on delete.
|
|
44
|
+
|
|
45
|
+
Conditions:
|
|
46
|
+
NewTopic: !Equals [!Ref ExistingAlertTopicArn, ""]
|
|
47
|
+
NewTopicWithEmail: !And [!Condition NewTopic, !Not [!Equals [!Ref AlertEmail, ""]]]
|
|
48
|
+
IsRehearsal: !Equals [!Ref Rehearsal, "true"]
|
|
49
|
+
|
|
50
|
+
Resources:
|
|
51
|
+
DbSecurityGroup:
|
|
52
|
+
Type: AWS::EC2::SecurityGroup
|
|
53
|
+
Properties:
|
|
54
|
+
GroupDescription: @@STACK@@ RDS, 5432 from the app box only (the box stack adds the rule)
|
|
55
|
+
VpcId: !Ref VpcId
|
|
56
|
+
|
|
57
|
+
DbSubnetGroup:
|
|
58
|
+
Type: AWS::RDS::DBSubnetGroup
|
|
59
|
+
Properties:
|
|
60
|
+
DBSubnetGroupDescription: @@STACK@@ RDS subnets
|
|
61
|
+
SubnetIds: !Ref PrivateSubnetIds
|
|
62
|
+
|
|
63
|
+
Database:
|
|
64
|
+
Type: AWS::RDS::DBInstance
|
|
65
|
+
DeletionPolicy: !If [IsRehearsal, Delete, Snapshot]
|
|
66
|
+
UpdateReplacePolicy: !If [IsRehearsal, Delete, Snapshot]
|
|
67
|
+
Properties:
|
|
68
|
+
Engine: postgres
|
|
69
|
+
EngineVersion: !Ref EngineVersion
|
|
70
|
+
DBInstanceClass: !Ref InstanceClass
|
|
71
|
+
AllocatedStorage: !Ref AllocatedStorageGb
|
|
72
|
+
StorageType: gp3
|
|
73
|
+
StorageEncrypted: true
|
|
74
|
+
DBName: !Ref DatabaseName
|
|
75
|
+
MasterUsername: postgres
|
|
76
|
+
ManageMasterUserPassword: true
|
|
77
|
+
DBSubnetGroupName: !Ref DbSubnetGroup
|
|
78
|
+
VPCSecurityGroups: [!Ref DbSecurityGroup]
|
|
79
|
+
PubliclyAccessible: false
|
|
80
|
+
MultiAZ: false
|
|
81
|
+
BackupRetentionPeriod: !Ref BackupRetentionDays
|
|
82
|
+
DeletionProtection: !If [IsRehearsal, false, true]
|
|
83
|
+
AutoMinorVersionUpgrade: true
|
|
84
|
+
CopyTagsToSnapshot: true
|
|
85
|
+
Tags:
|
|
86
|
+
- Key: Name
|
|
87
|
+
Value: @@STACK@@-rds
|
|
88
|
+
|
|
89
|
+
AlertTopic:
|
|
90
|
+
Type: AWS::SNS::Topic
|
|
91
|
+
Condition: NewTopic
|
|
92
|
+
Properties:
|
|
93
|
+
DisplayName: @@STACK@@-alerts
|
|
94
|
+
|
|
95
|
+
AlertSubscription:
|
|
96
|
+
Type: AWS::SNS::Subscription
|
|
97
|
+
Condition: NewTopicWithEmail
|
|
98
|
+
Properties:
|
|
99
|
+
TopicArn: !Ref AlertTopic
|
|
100
|
+
Protocol: email
|
|
101
|
+
Endpoint: !Ref AlertEmail
|
|
102
|
+
|
|
103
|
+
# The journal only grows, and RDS stops accepting writes when storage is full.
|
|
104
|
+
StorageLowAlarm:
|
|
105
|
+
Type: AWS::CloudWatch::Alarm
|
|
106
|
+
Properties:
|
|
107
|
+
AlarmDescription: RDS free storage under 3 GB
|
|
108
|
+
Namespace: AWS/RDS
|
|
109
|
+
MetricName: FreeStorageSpace
|
|
110
|
+
Dimensions: [{ Name: DBInstanceIdentifier, Value: !Ref Database }]
|
|
111
|
+
Statistic: Minimum
|
|
112
|
+
Period: 300
|
|
113
|
+
EvaluationPeriods: 3
|
|
114
|
+
Threshold: 3221225472
|
|
115
|
+
ComparisonOperator: LessThanThreshold
|
|
116
|
+
TreatMissingData: breaching
|
|
117
|
+
AlarmActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
118
|
+
OKActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
119
|
+
|
|
120
|
+
CpuHighAlarm:
|
|
121
|
+
Type: AWS::CloudWatch::Alarm
|
|
122
|
+
Properties:
|
|
123
|
+
AlarmDescription: "RDS CPU above 80% for 15 minutes (a burstable class may be out of credits)"
|
|
124
|
+
Namespace: AWS/RDS
|
|
125
|
+
MetricName: CPUUtilization
|
|
126
|
+
Dimensions: [{ Name: DBInstanceIdentifier, Value: !Ref Database }]
|
|
127
|
+
Statistic: Average
|
|
128
|
+
Period: 300
|
|
129
|
+
EvaluationPeriods: 3
|
|
130
|
+
Threshold: 80
|
|
131
|
+
ComparisonOperator: GreaterThanThreshold
|
|
132
|
+
TreatMissingData: notBreaching
|
|
133
|
+
AlarmActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
134
|
+
OKActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
135
|
+
|
|
136
|
+
MemoryLowAlarm:
|
|
137
|
+
Type: AWS::CloudWatch::Alarm
|
|
138
|
+
Properties:
|
|
139
|
+
AlarmDescription: RDS freeable memory under 100 MB for 15 minutes
|
|
140
|
+
Namespace: AWS/RDS
|
|
141
|
+
MetricName: FreeableMemory
|
|
142
|
+
Dimensions: [{ Name: DBInstanceIdentifier, Value: !Ref Database }]
|
|
143
|
+
Statistic: Minimum
|
|
144
|
+
Period: 300
|
|
145
|
+
EvaluationPeriods: 3
|
|
146
|
+
Threshold: 104857600
|
|
147
|
+
ComparisonOperator: LessThanThreshold
|
|
148
|
+
TreatMissingData: notBreaching
|
|
149
|
+
AlarmActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
150
|
+
OKActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
151
|
+
|
|
152
|
+
Outputs:
|
|
153
|
+
AlertTopicArn:
|
|
154
|
+
Description: Pass as AlertTopicArn to the box stack.
|
|
155
|
+
Value: !If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]
|
|
156
|
+
DbEndpoint:
|
|
157
|
+
Value: !GetAtt Database.Endpoint.Address
|
|
158
|
+
DbName:
|
|
159
|
+
Value: !Ref DatabaseName
|
|
160
|
+
DbSecretArn:
|
|
161
|
+
Description: RDS-managed master secret ({username, password}).
|
|
162
|
+
Value: !GetAtt Database.MasterUserSecret.SecretArn
|
|
163
|
+
DbSecurityGroupId:
|
|
164
|
+
Description: Pass as DbSecurityGroupId to the box stack.
|
|
165
|
+
Value: !Ref DbSecurityGroup
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Render compose.json and secrets.json for @@STACK@@'s app box from services.json.
|
|
3
|
+
#
|
|
4
|
+
# render-compose.sh <db-host> <db-secret-arn> [name=tag ...]
|
|
5
|
+
#
|
|
6
|
+
# Tags default to "latest". Every container gets DB_HOST, DB_NAME, DB_SECRET_ARN and PORT, and runs on the
|
|
7
|
+
# box's own network, so the proxy reaches each one on 127.0.0.1:<port>. Secrets named in services.json
|
|
8
|
+
# are written to secrets.json for fetch-secrets.sh to resolve on the box.
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
11
|
+
DB_HOST=${1:?rds endpoint}
|
|
12
|
+
DB_SECRET=${2:?rds secret arn}
|
|
13
|
+
shift 2
|
|
14
|
+
|
|
15
|
+
REGION=@@REGION@@
|
|
16
|
+
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
|
|
17
|
+
ECR="$ACCOUNT.dkr.ecr.$REGION.amazonaws.com"
|
|
18
|
+
TAGS=$(printf '%s\n' "$@" | jq -R 'select(length > 0) | split("=") | {(.[0]): .[1]}' | jq -s 'add // {}')
|
|
19
|
+
|
|
20
|
+
jq --arg ecr "$ECR" --argjson tags "$TAGS" --arg host "$DB_HOST" --arg secret "$DB_SECRET" --arg db "@@DB_NAME@@" '
|
|
21
|
+
def log: {driver: "json-file", options: {"max-size": "10m", "max-file": "3"}};
|
|
22
|
+
. as $in
|
|
23
|
+
| {services: (
|
|
24
|
+
($in.services | with_entries(.value |= (
|
|
25
|
+
{image: ($ecr + "/" + .repository + ":" + ($tags[.name] // "latest")),
|
|
26
|
+
network_mode: "host", restart: "unless-stopped", logging: log,
|
|
27
|
+
environment: ({PORT: (.port | tostring), DB_HOST: $host, DB_NAME: $db, DB_SECRET_ARN: $secret} + .env)}
|
|
28
|
+
+ (if (.secrets | length) > 0 then {env_file: [(.name + ".secrets.env")]} else {} end))))
|
|
29
|
+
+ {caddy: ({image: "@@PROXY_IMAGE@@", network_mode: "host", restart: "unless-stopped",
|
|
30
|
+
volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro"], logging: log}
|
|
31
|
+
+ (if $in.origin then {env_file: ["caddy.secrets.env"]} else {} end))}
|
|
32
|
+
+ (if $in.tunnel then {cloudflared: {image: $in.tunnel.image, network_mode: "host", restart: "unless-stopped",
|
|
33
|
+
command: ["tunnel", "--no-autoupdate", "--url", $in.tunnel.url, "run"],
|
|
34
|
+
env_file: ["cloudflared.secrets.env"], logging: log}} else {} end))}' \
|
|
35
|
+
"$HERE/services.json" > compose.json
|
|
36
|
+
|
|
37
|
+
jq '. as $in
|
|
38
|
+
| [ ($in.services[] | . as $v | ($v.secrets | to_entries[]) | {service: $v.name, name: .key, valueFrom: .value}),
|
|
39
|
+
(if $in.origin then {service: "caddy", name: "ORIGIN_SECRET", valueFrom: $in.origin.secret} else empty end),
|
|
40
|
+
(if $in.tunnel then {service: "cloudflared", name: "TUNNEL_TOKEN", valueFrom: $in.tunnel.token_secret} else empty end) ]' \
|
|
41
|
+
"$HERE/services.json" > secrets.json
|
|
42
|
+
|
|
43
|
+
echo "rendered compose.json and secrets.json ($(jq '.services | length' compose.json) services)"
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "../../projector"
|
|
3
|
+
require_relative "box/settings"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Projections
|
|
7
|
+
module Deploy
|
|
8
|
+
# The AWS single-box deploy target for `deployed_to("AwsBox")`: one EC2 instance that runs the
|
|
9
|
+
# domain's containers with Docker Compose behind Caddy, reading a plain RDS Postgres instance.
|
|
10
|
+
# It renders two CloudFormation stacks (the database, the box), the proxy's `Caddyfile`, the
|
|
11
|
+
# `services.json` compose is rendered from, and the scripts that roll the box.
|
|
12
|
+
module Box
|
|
13
|
+
extend Projector::Target
|
|
14
|
+
|
|
15
|
+
projects_as :aws_box, needs_world: true, emits: :files
|
|
16
|
+
|
|
17
|
+
TEMPLATE_DIR = File.join(__dir__, "box", "templates").freeze
|
|
18
|
+
|
|
19
|
+
# Region suffixes of an Elastic address's public DNS name; us-east-1 is the only region with
|
|
20
|
+
# the legacy `compute-1` form.
|
|
21
|
+
LEGACY_COMPUTE_REGION = "us-east-1".freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# Generates `rds.yaml`, `box.yaml`, `Caddyfile`, `services.json`, `render-compose.sh`,
|
|
26
|
+
# `fetch-secrets.sh`, `deploy-box.sh` and a `Makefile`.
|
|
27
|
+
#
|
|
28
|
+
# @param bluebook [Bluebook::Behaviour::Chapter] the domain's own booted chapter
|
|
29
|
+
# @param options [Hash] generation options; same shape as `Fargate.call`'s
|
|
30
|
+
# @return [Hash{String => String}] the generated file contents, keyed by filename
|
|
31
|
+
# @raise [ArgumentError] if the domain's deploy settings are invalid or conflict
|
|
32
|
+
def call(bluebook:, options: {})
|
|
33
|
+
world, domain, world_file = options.values_at(:world, :domain_dir, :world_file)
|
|
34
|
+
tenant = options[:tenant] || {}
|
|
35
|
+
settings = tenant_settings(world.for_verb("deployed_to"), tenant, File.basename(domain))
|
|
36
|
+
infra_name = (settings[:stack_name] || File.basename(domain)).to_s.tr("_", "-").downcase
|
|
37
|
+
|
|
38
|
+
target = declare(world.domain, settings, world_file)
|
|
39
|
+
plan = Settings.resolve(deploy_settings: settings, target: target, infra_name: infra_name)
|
|
40
|
+
render_all(plan, target.state[:region].value)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Applies the `--tenant` suffix to the stack name, as `Fargate.call` does.
|
|
44
|
+
#
|
|
45
|
+
# @param settings [Hash{Symbol => Object}] the world's `deployed_to` settings
|
|
46
|
+
# @param tenant [Hash] the `--tenant` and `--schema` options
|
|
47
|
+
# @param domain_name [String] the domain directory's name
|
|
48
|
+
# @return [Hash{Symbol => Object}] the settings with the tenant's stack name
|
|
49
|
+
def tenant_settings(settings, tenant, domain_name)
|
|
50
|
+
return settings unless tenant[:tenant]
|
|
51
|
+
|
|
52
|
+
settings.merge(stack_name: "#{settings[:stack_name] || domain_name}-#{tenant[:tenant]}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Validates the target through the Deploy bluebook's own `BoxTarget.Declare`.
|
|
56
|
+
#
|
|
57
|
+
# @param domain [String] the world's domain name
|
|
58
|
+
# @param settings [Hash{Symbol => Object}] the world's `deployed_to` settings
|
|
59
|
+
# @param world_file [String] the `.world` file, named in a refusal
|
|
60
|
+
# @return [Object] the declared target aggregate
|
|
61
|
+
# @raise [ArgumentError] when the bluebook refuses a value
|
|
62
|
+
def declare(domain, settings, world_file)
|
|
63
|
+
dispatcher = Hecks.boot(File.expand_path("../../deploy", __dir__))
|
|
64
|
+
dispatcher.dispatch(
|
|
65
|
+
"Deploy::BoxTarget.Declare",
|
|
66
|
+
with: {
|
|
67
|
+
domain: { value: domain }, region: { value: settings[:region] },
|
|
68
|
+
instance_type: { value: settings.fetch(:instance_type, "t4g.medium") },
|
|
69
|
+
volume_gb: { value: settings.fetch(:volume_gb, 30) },
|
|
70
|
+
database_class: { value: settings.fetch(:database_class, "db.t4g.small") },
|
|
71
|
+
storage_gb: { value: settings.fetch(:storage_gb, 20) }
|
|
72
|
+
}
|
|
73
|
+
).instance
|
|
74
|
+
rescue *Hecks::Runtime::DOMAIN_REFUSALS => e
|
|
75
|
+
raise ArgumentError, "#{world_file}'s deployed_to(\"AwsBox\") is invalid: #{e.message}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
79
|
+
# @param region [String] the validated region
|
|
80
|
+
# @return [Hash{String => String}] every generated file
|
|
81
|
+
def render_all(plan, region)
|
|
82
|
+
{
|
|
83
|
+
"rds.yaml" => rds_yaml(plan), "box.yaml" => box_yaml(plan, region),
|
|
84
|
+
"Caddyfile" => caddyfile(plan), "services.json" => services_json(plan),
|
|
85
|
+
"render-compose.sh" => template("render-compose.sh.tmpl", "STACK" => plan.infra_name, "REGION" => region,
|
|
86
|
+
"DB_NAME" => plan.database_name,
|
|
87
|
+
"PROXY_IMAGE" => plan.proxy_image),
|
|
88
|
+
"fetch-secrets.sh" => File.read(File.join(TEMPLATE_DIR, "fetch-secrets.sh")),
|
|
89
|
+
"deploy-box.sh" => deploy_box_sh(plan, region),
|
|
90
|
+
"Makefile" => makefile(plan)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
95
|
+
# @return [String] the database stack
|
|
96
|
+
def rds_yaml(plan)
|
|
97
|
+
template("rds.yaml.tmpl", "STACK" => plan.infra_name, "DB_CLASS" => plan.database_class,
|
|
98
|
+
"STORAGE_GB" => plan.storage_gb.to_s, "DB_NAME" => plan.database_name,
|
|
99
|
+
"ENGINE_VERSION" => plan.engine_version, "BACKUP_DAYS" => plan.backup_days.to_s)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
103
|
+
# @param region [String] the validated region
|
|
104
|
+
# @return [String] the box stack
|
|
105
|
+
def box_yaml(plan, region)
|
|
106
|
+
template("box.yaml.tmpl",
|
|
107
|
+
"STACK" => plan.infra_name, "INSTANCE_TYPE" => plan.instance_type,
|
|
108
|
+
"VOLUME_GB" => plan.volume_gb.to_s, "SNAPSHOTS_KEEP" => plan.snapshots_keep.to_s,
|
|
109
|
+
"AMI_PARAMETER" => ami_parameter(plan.instance_type),
|
|
110
|
+
"COMPUTE_DOMAIN" => compute_domain(region),
|
|
111
|
+
"SECRET_RESOURCES" => secret_resources(plan), "TUNNEL_EGRESS" => tunnel_egress(plan),
|
|
112
|
+
"SWAP_COMMANDS" => swap_commands(plan), "ECR_REPOSITORIES" => ecr_repositories(plan),
|
|
113
|
+
"ECR_OUTPUTS" => ecr_outputs(plan))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Graviton families end their generation digit with `g` (`t4g`, `m7gd`, `c6gn`).
|
|
117
|
+
#
|
|
118
|
+
# @param instance_type [String] an EC2 instance type
|
|
119
|
+
# @return [String] the SSM parameter of the matching Amazon Linux image
|
|
120
|
+
def ami_parameter(instance_type)
|
|
121
|
+
arch = instance_type.split(".").first.match?(/\d+g[a-z]*\z/) ? "arm64" : "x86_64"
|
|
122
|
+
"/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-#{arch}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @param region [String] the AWS region
|
|
126
|
+
# @return [String] the DNS suffix an Elastic address's public name ends with
|
|
127
|
+
def compute_domain(region)
|
|
128
|
+
region == LEGACY_COMPUTE_REGION ? "compute-1.amazonaws.com" : "#{region}.compute.amazonaws.com"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# One Secrets Manager statement resource per prefix. A name without a trailing `*` gets `-*`
|
|
132
|
+
# for the random suffix AWS appends to every secret ARN.
|
|
133
|
+
#
|
|
134
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
135
|
+
# @return [String] YAML list items, one per prefix, ending in a newline
|
|
136
|
+
def secret_resources(plan)
|
|
137
|
+
named = [plan.origin_secret, plan.tunnel_service&.token_secret].compact
|
|
138
|
+
arns = (plan.secret_prefixes + named).map { |name| name.end_with?("*") ? name : "#{name}-*" }
|
|
139
|
+
arns.uniq.map do |pattern|
|
|
140
|
+
" - !Sub \"arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:#{pattern}\"\n"
|
|
141
|
+
end.join.chomp
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
145
|
+
# @return [String] the outbound rules a Cloudflare Tunnel sidecar needs, or nothing
|
|
146
|
+
def tunnel_egress(plan)
|
|
147
|
+
return "" unless plan.tunnel
|
|
148
|
+
|
|
149
|
+
<<~YAML
|
|
150
|
+
#{' ' * 8}# cloudflared dials out to the tunnel edge on 7844. Outbound only: nothing opens inbound.
|
|
151
|
+
#{' ' * 8}- IpProtocol: tcp
|
|
152
|
+
#{' ' * 8} FromPort: 7844
|
|
153
|
+
#{' ' * 8} ToPort: 7844
|
|
154
|
+
#{' ' * 8} CidrIp: 0.0.0.0/0
|
|
155
|
+
#{' ' * 8} Description: tunnel to the edge
|
|
156
|
+
#{' ' * 8}- IpProtocol: udp
|
|
157
|
+
#{' ' * 8} FromPort: 7844
|
|
158
|
+
#{' ' * 8} ToPort: 7844
|
|
159
|
+
#{' ' * 8} CidrIp: 0.0.0.0/0
|
|
160
|
+
#{' ' * 8} Description: tunnel to the edge (QUIC)
|
|
161
|
+
YAML
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
165
|
+
# @return [String] the user-data lines that add swap, or nothing when `swap_gb` is 0
|
|
166
|
+
def swap_commands(plan)
|
|
167
|
+
return "" if plan.swap_gb.zero?
|
|
168
|
+
|
|
169
|
+
pad = " " * 10
|
|
170
|
+
<<~SH.lines.map { |line| "#{pad}#{line}" }.join
|
|
171
|
+
# swap so a memory spike at boot cannot take the box down
|
|
172
|
+
fallocate -l #{plan.swap_gb}G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
|
|
173
|
+
echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
|
174
|
+
SH
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# One ECR repository per container, keeping the newest 30 images.
|
|
178
|
+
#
|
|
179
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
180
|
+
# @return [String] CloudFormation resources, each preceded by a blank line
|
|
181
|
+
def ecr_repositories(plan)
|
|
182
|
+
plan.containers.map do |container|
|
|
183
|
+
id = "#{logical(container.name)}Repository"
|
|
184
|
+
<<~YAML.lines.map { |line| line.strip.empty? ? line : " #{line}" }.join
|
|
185
|
+
|
|
186
|
+
#{id}:
|
|
187
|
+
Type: AWS::ECR::Repository
|
|
188
|
+
Properties:
|
|
189
|
+
RepositoryName: #{container.repository}
|
|
190
|
+
ImageScanningConfiguration:
|
|
191
|
+
ScanOnPush: true
|
|
192
|
+
LifecyclePolicy:
|
|
193
|
+
LifecyclePolicyText: '{"rules":[{"rulePriority":1,"description":"keep the newest 30","selection":{"tagStatus":"any","countType":"imageCountMoreThan","countNumber":30},"action":{"type":"expire"}}]}'
|
|
194
|
+
YAML
|
|
195
|
+
end.join.chomp
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
199
|
+
# @return [String] one repository URI output per container
|
|
200
|
+
def ecr_outputs(plan)
|
|
201
|
+
plan.containers.map do |container|
|
|
202
|
+
" #{logical(container.name)}RepositoryUri:\n Value: !GetAtt #{logical(container.name)}Repository.RepositoryUri\n"
|
|
203
|
+
end.join.chomp
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# @param name [String] a container name such as `web-app`
|
|
207
|
+
# @return [String] a CloudFormation logical id fragment, such as `WebApp`
|
|
208
|
+
def logical(name)
|
|
209
|
+
name.split("-").map(&:capitalize).join
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# The proxy's configuration: an optional origin-secret guard, one block per route and a
|
|
213
|
+
# default for everything else.
|
|
214
|
+
#
|
|
215
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
216
|
+
# @return [String] the Caddyfile
|
|
217
|
+
def caddyfile(plan)
|
|
218
|
+
guarded = !plan.origin_header.nil?
|
|
219
|
+
site =
|
|
220
|
+
if guarded
|
|
221
|
+
"\t@origin header #{plan.origin_header} {$ORIGIN_SECRET}\n\n\thandle @origin {\n" \
|
|
222
|
+
"#{indent(indent(route_blocks(plan)))}\t}\n\n\thandle {\n\t\trespond \"Forbidden\" 403\n\t}\n"
|
|
223
|
+
else
|
|
224
|
+
indent(route_blocks(plan))
|
|
225
|
+
end
|
|
226
|
+
"#{caddy_header(plan)}#{caddy_global(guarded)}\n:80 {\n#{site}}\n"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
230
|
+
# @return [String] the `handle` blocks for each route, then the default
|
|
231
|
+
def route_blocks(plan)
|
|
232
|
+
routes = plan.routes.each_with_index.map do |route, i|
|
|
233
|
+
port = plan.containers.find { |c| c.name == route.container }.port
|
|
234
|
+
"@r#{i + 1} path #{route.paths.join(' ')}\nhandle @r#{i + 1} {\n\treverse_proxy 127.0.0.1:#{port}\n}\n\n"
|
|
235
|
+
end
|
|
236
|
+
"#{routes.join}handle {\n\treverse_proxy 127.0.0.1:#{plan.default.port}\n}\n"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
240
|
+
# @return [String] the comment that opens the Caddyfile
|
|
241
|
+
def caddy_header(plan)
|
|
242
|
+
origin =
|
|
243
|
+
if plan.origin_header
|
|
244
|
+
"Only requests that carry #{plan.origin_header} with the origin secret are proxied;\n" \
|
|
245
|
+
"# everything else gets a flat 403.\n"
|
|
246
|
+
else
|
|
247
|
+
"Every request is proxied.\n"
|
|
248
|
+
end
|
|
249
|
+
"# #{plan.infra_name}'s proxy. #{origin}"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# With a guard, the proxy trusts every source: a request that reaches a `handle` block has
|
|
253
|
+
# already proved it came through the CDN by presenting the secret, and everything else is
|
|
254
|
+
# refused. Caddy then keeps the client address the CDN put in X-Forwarded-For.
|
|
255
|
+
#
|
|
256
|
+
# @param guarded [Boolean] whether an origin secret guards the site
|
|
257
|
+
# @return [String] the global options block
|
|
258
|
+
def caddy_global(guarded)
|
|
259
|
+
return "{\n\tauto_https off\n\tadmin off\n}\n" unless guarded
|
|
260
|
+
|
|
261
|
+
"{\n\tauto_https off\n\tadmin off\n\tservers {\n\t\ttrusted_proxies static 0.0.0.0/0 ::/0\n\t}\n}\n"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# @param text [String] lines to indent one level with a tab
|
|
265
|
+
# @return [String] the text, each non-blank line prefixed with a tab
|
|
266
|
+
def indent(text)
|
|
267
|
+
text.lines.map { |line| line.strip.empty? ? line : "\t#{line}" }.join
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# The compose source: one entry per container, plus the origin secret the proxy checks.
|
|
271
|
+
#
|
|
272
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
273
|
+
# @return [String] `services.json`
|
|
274
|
+
def services_json(plan)
|
|
275
|
+
services = plan.containers.to_h do |c|
|
|
276
|
+
[c.name, { "name" => c.name, "repository" => c.repository, "port" => c.port,
|
|
277
|
+
"env" => c.env, "secrets" => c.secrets }]
|
|
278
|
+
end
|
|
279
|
+
origin = plan.origin_secret ? { "header" => plan.origin_header, "secret" => plan.origin_secret } : nil
|
|
280
|
+
document = { "services" => services, "origin" => origin }
|
|
281
|
+
document["tunnel"] = tunnel_entry(plan.tunnel_service) if plan.tunnel_service
|
|
282
|
+
json = JSON.pretty_generate(document)
|
|
283
|
+
# An empty object prints as `{}` or `{` newline `}`, depending on the json gem.
|
|
284
|
+
"#{json.gsub(/\{\s*\}/, '{}')}\n"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# @param tunnel [Settings::Tunnel] the declared tunnel service
|
|
288
|
+
# @return [Hash{String => Object}] its entry in `services.json`
|
|
289
|
+
def tunnel_entry(tunnel)
|
|
290
|
+
{ "url" => "http://127.0.0.1:#{tunnel.port}", "token_secret" => tunnel.token_secret, "image" => tunnel.image }
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
294
|
+
# @param region [String] the validated region
|
|
295
|
+
# @return [String] `deploy-box.sh`
|
|
296
|
+
def deploy_box_sh(plan, region)
|
|
297
|
+
template("deploy-box.sh.tmpl", "STACK" => plan.infra_name, "BOX_STACK" => plan.box_stack,
|
|
298
|
+
"RDS_STACK" => plan.rds_stack, "REGION" => region,
|
|
299
|
+
"DIR" => "/opt/#{plan.infra_name}", "HEALTH_CHECKS" => health_checks(plan))
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# The probes the post-roll check runs on the box, against the proxy on localhost.
|
|
303
|
+
#
|
|
304
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
305
|
+
# @return [String] bash lines that set `BAD=1` on a failed probe
|
|
306
|
+
def health_checks(plan)
|
|
307
|
+
write_out = '-w "%{http_code}"' # rubocop:disable Style/FormatStringToken -- curl's own token
|
|
308
|
+
probe = lambda do |label, args, wanted|
|
|
309
|
+
<<~SH.chomp
|
|
310
|
+
code=$(curl -s -o /dev/null #{write_out} --max-time 40 localhost/#{args})
|
|
311
|
+
#{wanted} && echo "ok #{label} -> $code" || { echo "FAIL #{label} -> $code"; BAD=1; }
|
|
312
|
+
SH
|
|
313
|
+
end
|
|
314
|
+
probes =
|
|
315
|
+
if plan.origin_header
|
|
316
|
+
[
|
|
317
|
+
"S=$(grep ^ORIGIN_SECRET= caddy.secrets.env | cut -d= -f2-)",
|
|
318
|
+
probe.call("a request without the origin secret is refused", "", '[ "$code" = 403 ]'),
|
|
319
|
+
probe.call("a request with the origin secret is served", %( -H "#{plan.origin_header}: $S"),
|
|
320
|
+
'[ "${code#5}" = "$code" ]')
|
|
321
|
+
]
|
|
322
|
+
else
|
|
323
|
+
[probe.call("the proxy serves /", "", '[ "${code#5}" = "$code" ]')]
|
|
324
|
+
end
|
|
325
|
+
(probes + tunnel_probe(plan)).join("\n")
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
329
|
+
# @return [Array<String>] the probe that waits for the tunnel to register, when one runs
|
|
330
|
+
def tunnel_probe(plan)
|
|
331
|
+
return [] unless plan.tunnel_service
|
|
332
|
+
|
|
333
|
+
[<<~SH.chomp]
|
|
334
|
+
for _ in 1 2 3 4 5 6; do
|
|
335
|
+
n=$(docker compose -f compose.json logs --no-color cloudflared 2>&1 | grep -ci "registered tunnel connection" || true)
|
|
336
|
+
[ "$n" -gt 0 ] && break
|
|
337
|
+
sleep 5
|
|
338
|
+
done
|
|
339
|
+
[ "$n" -gt 0 ] && echo "ok the tunnel registered $n connections" || { echo "FAIL the tunnel has no connection"; BAD=1; }
|
|
340
|
+
SH
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
344
|
+
# @return [String] a Makefile whose targets create the two stacks and roll the box
|
|
345
|
+
def makefile(plan)
|
|
346
|
+
<<~MAKE
|
|
347
|
+
# #{plan.infra_name}: one app box and one RDS instance.
|
|
348
|
+
# make stacks VPC=vpc-... PRIVATE_SUBNETS=subnet-a,subnet-b PUBLIC_SUBNET=subnet-c
|
|
349
|
+
# make deploy [TAGS="web=20260101 worker=20260101"]
|
|
350
|
+
RDS_STACK = #{plan.rds_stack}
|
|
351
|
+
BOX_STACK = #{plan.box_stack}
|
|
352
|
+
|
|
353
|
+
.PHONY: stacks deploy
|
|
354
|
+
stacks:
|
|
355
|
+
\taws cloudformation deploy --template-file rds.yaml --stack-name $(RDS_STACK) --capabilities CAPABILITY_IAM \\
|
|
356
|
+
\t\t--parameter-overrides VpcId=$(VPC) PrivateSubnetIds=$(PRIVATE_SUBNETS)
|
|
357
|
+
\taws cloudformation deploy --template-file box.yaml --stack-name $(BOX_STACK) --capabilities CAPABILITY_IAM \\
|
|
358
|
+
\t\t--parameter-overrides VpcId=$(VPC) SubnetId=$(PUBLIC_SUBNET) \\
|
|
359
|
+
\t\tDbSecurityGroupId=$$(aws cloudformation describe-stacks --stack-name $(RDS_STACK) --query "Stacks[0].Outputs[?OutputKey=='DbSecurityGroupId'].OutputValue" --output text) \\
|
|
360
|
+
\t\tDbSecretArn=$$(aws cloudformation describe-stacks --stack-name $(RDS_STACK) --query "Stacks[0].Outputs[?OutputKey=='DbSecretArn'].OutputValue" --output text) \\
|
|
361
|
+
\t\tAlertTopicArn=$$(aws cloudformation describe-stacks --stack-name $(RDS_STACK) --query "Stacks[0].Outputs[?OutputKey=='AlertTopicArn'].OutputValue" --output text)
|
|
362
|
+
|
|
363
|
+
deploy:
|
|
364
|
+
\tbash ./deploy-box.sh $(TAGS)
|
|
365
|
+
MAKE
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Fills `@@NAME@@` markers. A marker alone on its line is replaced together with the line,
|
|
369
|
+
# so an empty value leaves no blank line behind; one inside a line is replaced in place.
|
|
370
|
+
#
|
|
371
|
+
# @param file [String] the template file's name in `TEMPLATE_DIR`
|
|
372
|
+
# @param values [Hash{String => String}] marker name => text
|
|
373
|
+
# @return [String] the rendered file
|
|
374
|
+
def template(file, values)
|
|
375
|
+
values.reduce(File.read(File.join(TEMPLATE_DIR, file))) do |text, (marker, value)|
|
|
376
|
+
own_line = /^@@#{marker}@@\n/
|
|
377
|
+
if text.match?(own_line)
|
|
378
|
+
text.sub(own_line) { value.empty? || value.end_with?("\n") ? value : "#{value}\n" }
|
|
379
|
+
else
|
|
380
|
+
text.gsub("@@#{marker}@@") { value }
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
@@ -241,12 +241,21 @@ module Hecks
|
|
|
241
241
|
end
|
|
242
242
|
private_class_method :s3_origin_lines
|
|
243
243
|
|
|
244
|
+
# Renders one cache behaviour: the default one under `key`, or a list item led by its
|
|
245
|
+
# path.
|
|
246
|
+
#
|
|
247
|
+
# @param key [String, nil] `"DefaultCacheBehavior"`, or nil for a `CacheBehaviors` item
|
|
248
|
+
# @param entry [Hash{Symbol => Object}] the behaviour as `normalize` checks it (`:origin`,
|
|
249
|
+
# `:path` for an item, `:viewer_protocol`, `:methods`, `:compress`, `:cache_policy`,
|
|
250
|
+
# `:origin_request_policy`), and optionally `:response_headers_policy`, each policy as
|
|
251
|
+
# `[id_or_intrinsic, comment_or_nil]`
|
|
252
|
+
# @param base [String] the indentation of `key`, or of the list item's dash
|
|
253
|
+
# @return [Array<String>] the lines
|
|
244
254
|
def behavior_lines(key, entry, base)
|
|
245
255
|
pad = "#{base} "
|
|
246
256
|
head = key ? "#{base}#{key}:" : "#{base}- PathPattern: #{Yaml.string(entry[:path])}"
|
|
247
257
|
[head, "#{pad}TargetOriginId: #{entry[:origin]}", *behavior_property_lines(entry, pad)]
|
|
248
258
|
end
|
|
249
|
-
private_class_method :behavior_lines
|
|
250
259
|
|
|
251
260
|
def behavior_property_lines(entry, pad)
|
|
252
261
|
lines = ["#{pad}ViewerProtocolPolicy: #{entry[:viewer_protocol]}",
|
|
@@ -255,6 +264,9 @@ module Hecks
|
|
|
255
264
|
lines << "#{pad}Compress: #{entry[:compress]}" if entry.key?(:compress)
|
|
256
265
|
lines << policy_line(pad, "CachePolicyId", entry[:cache_policy])
|
|
257
266
|
lines << policy_line(pad, "OriginRequestPolicyId", entry[:origin_request_policy]) if entry[:origin_request_policy]
|
|
267
|
+
if entry[:response_headers_policy]
|
|
268
|
+
lines << policy_line(pad, "ResponseHeadersPolicyId", entry[:response_headers_policy])
|
|
269
|
+
end
|
|
258
270
|
lines
|
|
259
271
|
end
|
|
260
272
|
private_class_method :behavior_property_lines
|
|
@@ -634,7 +634,7 @@ module Hecks
|
|
|
634
634
|
# generator the Lambda deploy path already uses to produce
|
|
635
635
|
# rust/dist/#{domain_name}.wasm/.ir.json from this domain's own
|
|
636
636
|
# .bluebook.
|
|
637
|
-
\tcd $(ROOT) && HECKS_ENVIRONMENT=memory ruby exe/hecks build_wasm domain=$(DOMAIN) --wait
|
|
637
|
+
\tcd $(ROOT) && HECKS_ENVIRONMENT=memory ruby exe/hecks build.build_wasm domain=$(DOMAIN) --wait
|
|
638
638
|
\tcd $(ROOT)/rust/host && CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc rustup run stable cargo build --release --target aarch64-unknown-linux-gnu --bin bootstrap
|
|
639
639
|
\tcp $(ROOT)/rust/host/target/aarch64-unknown-linux-gnu/release/bootstrap #{domain_name}-host
|
|
640
640
|
\tcp $(ROOT)/rust/dist/#{domain_name}.wasm #{domain_name}.wasm
|