hecks 3.0.5 → 3.1.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.
- 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 +32 -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/domain_writer.rb +39 -0
- data/lib/hecks/cli/interview_agent.rb +59 -0
- data/lib/hecks/cli/interview_draft.rb +246 -0
- data/lib/hecks/cli/interview_run.rb +87 -0
- data/lib/hecks/cli/interview_session.rb +299 -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 +12 -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 +53 -33
- 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/usage_cache.rb +123 -0
- 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/finding_github.adapter +7 -0
- data/lib/hecks/hecks/adapters/finding_github.rb +114 -0
- 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/adapters/terminal.rb +15 -0
- data/lib/hecks/hecks/codebase.bluebook +4 -3
- data/lib/hecks/hecks/context_map.hecksagon +2 -2
- data/lib/hecks/hecks/custodian.bluebook +81 -3
- data/lib/hecks/hecks/hecks.bluebook +18 -0
- data/lib/hecks/hecks/hecks.hecksagon +16 -4
- data/lib/hecks/hecks/hecks.world +7 -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 +324 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +268 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +68 -0
- data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
- data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +181 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +49 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
- data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +91 -0
- data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +58 -0
- data/lib/hecks/projections/deploy/box.rb +553 -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 +153 -111
- 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/loader.rb +31 -8
- 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/tickets/bluebook/tickets.bluebook +331 -0
- data/lib/hecks/tickets/bluebook/tickets.hecksagon +11 -0
- data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +26 -0
- 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/lib/hecks.rb +1 -0
- 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/codegen/src/json_codec.rs +3 -0
- data/rust/codegen/src/naming.rs +4 -0
- data/rust/codegen/src/types.rs +45 -0
- 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 +51 -2
|
@@ -0,0 +1,553 @@
|
|
|
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
|
+
# The end of every Caddyfile: sites a rehearsal mounts under `caddy-extra`, such as a
|
|
24
|
+
# loopback listener that adds the origin secret so a smoke test runs without the CDN. A
|
|
25
|
+
# glob that matches nothing is not an error, so production, which mounts none, is unchanged.
|
|
26
|
+
CADDY_EXTRA = "# Rehearsal-only sites are mounted here; nothing matches in production. Restart the proxy\n" \
|
|
27
|
+
"# after adding one: the admin API is off, so a reload cannot reach it.\n" \
|
|
28
|
+
"import /etc/caddy/extra/*\n".freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# Generates `rds.yaml`, `box.yaml`, `Caddyfile`, `services.json`, `render-compose.sh`,
|
|
33
|
+
# `fetch-secrets.sh`, `deploy-box.sh` and a `Makefile`, and for a world that declares a
|
|
34
|
+
# `migration`, `restore-to-rds.sh`, `verify-copy.sh` and `MIGRATION.md`.
|
|
35
|
+
#
|
|
36
|
+
# @param bluebook [Bluebook::Behaviour::Chapter] the domain's own booted chapter
|
|
37
|
+
# @param options [Hash] generation options; same shape as `Fargate.call`'s
|
|
38
|
+
# @return [Hash{String => String}] the generated file contents, keyed by filename
|
|
39
|
+
# @raise [ArgumentError] if the domain's deploy settings are invalid or conflict
|
|
40
|
+
def call(bluebook:, options: {})
|
|
41
|
+
world, domain, world_file = options.values_at(:world, :domain_dir, :world_file)
|
|
42
|
+
tenant = options[:tenant] || {}
|
|
43
|
+
settings = tenant_settings(world.for_verb("deployed_to"), tenant, File.basename(domain))
|
|
44
|
+
infra_name = (settings[:stack_name] || File.basename(domain)).to_s.tr("_", "-").downcase
|
|
45
|
+
|
|
46
|
+
target = declare(world.domain, settings, world_file)
|
|
47
|
+
plan = Settings.resolve(deploy_settings: settings, target: target, infra_name: infra_name)
|
|
48
|
+
render_all(plan, target.state[:region].value)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Applies the `--tenant` suffix to the stack name, as `Fargate.call` does.
|
|
52
|
+
#
|
|
53
|
+
# @param settings [Hash{Symbol => Object}] the world's `deployed_to` settings
|
|
54
|
+
# @param tenant [Hash] the `--tenant` and `--schema` options
|
|
55
|
+
# @param domain_name [String] the domain directory's name
|
|
56
|
+
# @return [Hash{Symbol => Object}] the settings with the tenant's stack name
|
|
57
|
+
def tenant_settings(settings, tenant, domain_name)
|
|
58
|
+
return settings unless tenant[:tenant]
|
|
59
|
+
|
|
60
|
+
settings.merge(stack_name: "#{settings[:stack_name] || domain_name}-#{tenant[:tenant]}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Validates the target through the Deploy bluebook's own `BoxTarget.Declare`.
|
|
64
|
+
#
|
|
65
|
+
# @param domain [String] the world's domain name
|
|
66
|
+
# @param settings [Hash{Symbol => Object}] the world's `deployed_to` settings
|
|
67
|
+
# @param world_file [String] the `.world` file, named in a refusal
|
|
68
|
+
# @return [Object] the declared target aggregate
|
|
69
|
+
# @raise [ArgumentError] when the bluebook refuses a value
|
|
70
|
+
def declare(domain, settings, world_file)
|
|
71
|
+
dispatcher = Hecks.boot(File.expand_path("../../deploy", __dir__))
|
|
72
|
+
dispatcher.dispatch(
|
|
73
|
+
"Deploy::BoxTarget.Declare",
|
|
74
|
+
with: {
|
|
75
|
+
domain: { value: domain }, region: { value: settings[:region] },
|
|
76
|
+
instance_type: { value: settings.fetch(:instance_type, "t4g.medium") },
|
|
77
|
+
volume_gb: { value: settings.fetch(:volume_gb, 30) },
|
|
78
|
+
database_class: { value: settings.fetch(:database_class, "db.t4g.small") },
|
|
79
|
+
storage_gb: { value: settings.fetch(:storage_gb, 20) }
|
|
80
|
+
}
|
|
81
|
+
).instance
|
|
82
|
+
rescue *Hecks::Runtime::DOMAIN_REFUSALS => e
|
|
83
|
+
raise ArgumentError, "#{world_file}'s deployed_to(\"AwsBox\") is invalid: #{e.message}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
87
|
+
# @param region [String] the validated region
|
|
88
|
+
# @return [Hash{String => String}] every generated file
|
|
89
|
+
def render_all(plan, region)
|
|
90
|
+
{
|
|
91
|
+
"rds.yaml" => rds_yaml(plan), "box.yaml" => box_yaml(plan, region),
|
|
92
|
+
"Caddyfile" => caddyfile(plan), "services.json" => services_json(plan),
|
|
93
|
+
"render-compose.sh" => render_compose_sh(plan, region),
|
|
94
|
+
"fetch-secrets.sh" => File.read(File.join(TEMPLATE_DIR, "fetch-secrets.sh")),
|
|
95
|
+
"deploy-box.sh" => deploy_box_sh(plan, region),
|
|
96
|
+
"Makefile" => makefile(plan)
|
|
97
|
+
}.merge(migration_files(plan))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The tooling that moves a project's data from its old database into the RDS instance, for
|
|
101
|
+
# a world that declares a `migration`.
|
|
102
|
+
#
|
|
103
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
104
|
+
# @return [Hash{String => String}] `restore-to-rds.sh`, `verify-copy.sh` and `MIGRATION.md`,
|
|
105
|
+
# or nothing
|
|
106
|
+
def migration_files(plan)
|
|
107
|
+
migration = plan.migration
|
|
108
|
+
return {} unless migration
|
|
109
|
+
|
|
110
|
+
values = { "STACK" => plan.infra_name, "SCHEMAS" => migration.schemas.join(" "),
|
|
111
|
+
"SCHEMAS_CSV" => migration.schemas.join(","), "DATABASE" => migration.database,
|
|
112
|
+
"SOURCE_DATABASE" => migration.source_database }
|
|
113
|
+
{
|
|
114
|
+
"restore-to-rds.sh" => template("restore-to-rds.sh.tmpl", values),
|
|
115
|
+
"verify-copy.sh" => template("verify-copy.sh.tmpl", values),
|
|
116
|
+
"MIGRATION.md" => migration_md(plan)
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
121
|
+
# @return [String] the runbook for moving onto the box, in the order the steps are run
|
|
122
|
+
def migration_md(plan)
|
|
123
|
+
migration = plan.migration
|
|
124
|
+
deploy = plan.task_definition ? "make deploy TASKDEF=#{plan.task_definition}:<revision>" : "make deploy"
|
|
125
|
+
<<~MD
|
|
126
|
+
# Moving #{plan.infra_name} onto the box and RDS
|
|
127
|
+
|
|
128
|
+
Generated from the world's `migration` setting. Nothing here has been run for you.
|
|
129
|
+
|
|
130
|
+
Schemas to copy: #{migration.schemas.map { |name| "`#{name}`" }.join(', ')}, in database
|
|
131
|
+
`#{migration.source_database}` on the old server and `#{migration.database}` on RDS.
|
|
132
|
+
|
|
133
|
+
## Before cutover
|
|
134
|
+
|
|
135
|
+
1. Create the stacks: `make stacks VPC=... PRIVATE_SUBNETS=... PUBLIC_SUBNET=...`. To rehearse first,
|
|
136
|
+
deploy `rds.yaml` and `box.yaml` under other stack names with `Rehearsal=true`: the database is then
|
|
137
|
+
deleted with its stack and the box gets no stable public address. Delete the rehearsal stacks after.
|
|
138
|
+
2. Copy the data. The bastion is any instance that can reach both databases.
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
RDS_HOST=$(aws cloudformation describe-stacks --stack-name #{plan.rds_stack} --query "Stacks[0].Outputs[?OutputKey=='DbEndpoint'].OutputValue" --output text)
|
|
142
|
+
RDS_SECRET=$(aws cloudformation describe-stacks --stack-name #{plan.rds_stack} --query "Stacks[0].Outputs[?OutputKey=='DbSecretArn'].OutputValue" --output text)
|
|
143
|
+
bash restore-to-rds.sh <bastion-instance-id> <old-host> <old-secret-arn> "$RDS_HOST" "$RDS_SECRET"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It copies each schema, refreshes the materialized views a plain `pg_restore` cannot, then runs
|
|
147
|
+
`verify-copy.sh`, which compares structure and the exact row count of every table and prints `OK`
|
|
148
|
+
only if both match. Re-run with `FORCE=1` to reload.
|
|
149
|
+
3. Deploy the app onto the box: `#{deploy}`. The deploy ends with health checks on the box.
|
|
150
|
+
4. Run the project's own smoke test against the box before any traffic moves.
|
|
151
|
+
|
|
152
|
+
## Cutover
|
|
153
|
+
|
|
154
|
+
1. Stop writes on the old stack, then run `restore-to-rds.sh` again with `FORCE=1` for a final copy and
|
|
155
|
+
wait for `OK`.
|
|
156
|
+
2. Point the CDN's origin at the box stack's `AppOriginDomain` output.
|
|
157
|
+
3. Keep the old database untouched for several days, and take a final snapshot before deleting it.
|
|
158
|
+
|
|
159
|
+
## Rollback
|
|
160
|
+
|
|
161
|
+
Until the first write lands on RDS, point the origin back. After that, writes taken by both databases
|
|
162
|
+
cannot be merged: choose one side and copy it over the other (swap the hosts and secrets, and set
|
|
163
|
+
`SRC_DB` and `DST_DB`, with `FORCE=1`). Do not change the domain's era in the same window, so a rollback
|
|
164
|
+
only has to move data.
|
|
165
|
+
MD
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
169
|
+
# @param region [String] the validated region
|
|
170
|
+
# @return [String] the script that renders the Compose file, from `services.json` or from
|
|
171
|
+
# an ECS task definition when the world names one
|
|
172
|
+
def render_compose_sh(plan, region)
|
|
173
|
+
if plan.task_definition
|
|
174
|
+
template("render-compose-taskdef.sh.tmpl", "STACK" => plan.infra_name, "FAMILY" => plan.task_definition,
|
|
175
|
+
"PROXY_IMAGE" => plan.proxy_image)
|
|
176
|
+
else
|
|
177
|
+
template("render-compose.sh.tmpl", "STACK" => plan.infra_name, "REGION" => region,
|
|
178
|
+
"DB_NAME" => plan.database_name, "PROXY_IMAGE" => plan.proxy_image)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
183
|
+
# @return [String] the database stack
|
|
184
|
+
def rds_yaml(plan)
|
|
185
|
+
template("rds.yaml.tmpl", "STACK" => plan.infra_name, "DB_CLASS" => plan.database_class,
|
|
186
|
+
"STORAGE_GB" => plan.storage_gb.to_s, "DB_NAME" => plan.database_name,
|
|
187
|
+
"ENGINE_VERSION" => plan.engine_version, "BACKUP_DAYS" => plan.backup_days.to_s)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
191
|
+
# @param region [String] the validated region
|
|
192
|
+
# @return [String] the box stack
|
|
193
|
+
def box_yaml(plan, region)
|
|
194
|
+
template("box.yaml.tmpl",
|
|
195
|
+
"STACK" => plan.infra_name, "INSTANCE_TYPE" => plan.instance_type,
|
|
196
|
+
"VOLUME_GB" => plan.volume_gb.to_s, "SNAPSHOTS_KEEP" => plan.snapshots_keep.to_s,
|
|
197
|
+
"AMI_PARAMETER" => ami_parameter(plan.instance_type),
|
|
198
|
+
"COMPUTE_DOMAIN" => compute_domain(region),
|
|
199
|
+
"SECRET_RESOURCES" => secret_resources(plan), "TUNNEL_EGRESS" => tunnel_egress(plan),
|
|
200
|
+
"SWAP_COMMANDS" => swap_commands(plan), "ECR_REPOSITORIES" => ecr_repositories(plan),
|
|
201
|
+
"ECR_OUTPUTS" => ecr_outputs(plan), "S3_POLICY" => s3_policy(plan))
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# The role's S3 policy: every declared bucket readable, and writable only on a production
|
|
205
|
+
# box, so a rehearsal never changes the real objects.
|
|
206
|
+
#
|
|
207
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
208
|
+
# @return [String] a policy list item, indented into the role's `Policies`, or nothing
|
|
209
|
+
def s3_policy(plan)
|
|
210
|
+
return "" if plan.s3_buckets.empty?
|
|
211
|
+
|
|
212
|
+
arn = ->(path) { "arn:${AWS::Partition}:s3:::#{path}" }
|
|
213
|
+
reads = plan.s3_buckets.flat_map { |b| [arn.call(b.name), arn.call("#{b.name}/*")] }
|
|
214
|
+
writes = plan.s3_buckets.select(&:write).map { |b| arn.call("#{b.name}/*") }
|
|
215
|
+
rows = s3_read_rows(reads)
|
|
216
|
+
rows += s3_write_rows(writes) unless writes.empty?
|
|
217
|
+
"#{rows.map { |row| " #{row}" }.join("\n")}\n"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @param resources [Array<String>] the bucket and object ARNs a box may read
|
|
221
|
+
# @return [Array<String>] the policy's head and its read statement, relative to `Policies`
|
|
222
|
+
def s3_read_rows(resources)
|
|
223
|
+
["- PolicyName: s3-access", " PolicyDocument:", " Version: \"2012-10-17\"", " Statement:",
|
|
224
|
+
" - Effect: Allow", " Action: [s3:GetObject, s3:ListBucket]", " Resource:"] +
|
|
225
|
+
resources.map { |r| " - !Sub \"#{r}\"" }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# @param resources [Array<String>] the object ARNs a production box may write
|
|
229
|
+
# @return [Array<String>] the production-only write statement, relative to `Policies`
|
|
230
|
+
def s3_write_rows(resources)
|
|
231
|
+
[" - !If", " - IsProduction", " - Effect: Allow",
|
|
232
|
+
" Action: [s3:PutObject, s3:DeleteObject]", " Resource:"] +
|
|
233
|
+
resources.map { |r| " - !Sub \"#{r}\"" } + [" - !Ref AWS::NoValue"]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Graviton families end their generation digit with `g` (`t4g`, `m7gd`, `c6gn`).
|
|
237
|
+
#
|
|
238
|
+
# @param instance_type [String] an EC2 instance type
|
|
239
|
+
# @return [String] the SSM parameter of the matching Amazon Linux image
|
|
240
|
+
def ami_parameter(instance_type)
|
|
241
|
+
arch = instance_type.split(".").first.match?(/\d+g[a-z]*\z/) ? "arm64" : "x86_64"
|
|
242
|
+
"/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-#{arch}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# @param region [String] the AWS region
|
|
246
|
+
# @return [String] the DNS suffix an Elastic address's public name ends with
|
|
247
|
+
def compute_domain(region)
|
|
248
|
+
region == LEGACY_COMPUTE_REGION ? "compute-1.amazonaws.com" : "#{region}.compute.amazonaws.com"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# One Secrets Manager statement resource per prefix. A name without a trailing `*` gets `-*`
|
|
252
|
+
# for the random suffix AWS appends to every secret ARN.
|
|
253
|
+
#
|
|
254
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
255
|
+
# @return [String] YAML list items, one per prefix, ending in a newline
|
|
256
|
+
def secret_resources(plan)
|
|
257
|
+
named = [plan.origin_secret, plan.tunnel_service&.token_secret].compact
|
|
258
|
+
arns = (plan.secret_prefixes + named).map { |name| name.end_with?("*") ? name : "#{name}-*" }
|
|
259
|
+
arns.uniq.map do |pattern|
|
|
260
|
+
" - !Sub \"arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:#{pattern}\"\n"
|
|
261
|
+
end.join.chomp
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
265
|
+
# @return [String] the outbound rules a Cloudflare Tunnel sidecar needs, or nothing
|
|
266
|
+
def tunnel_egress(plan)
|
|
267
|
+
return "" unless plan.tunnel
|
|
268
|
+
|
|
269
|
+
<<~YAML
|
|
270
|
+
#{' ' * 8}# cloudflared dials out to the tunnel edge on 7844. Outbound only: nothing opens inbound.
|
|
271
|
+
#{' ' * 8}- IpProtocol: tcp
|
|
272
|
+
#{' ' * 8} FromPort: 7844
|
|
273
|
+
#{' ' * 8} ToPort: 7844
|
|
274
|
+
#{' ' * 8} CidrIp: 0.0.0.0/0
|
|
275
|
+
#{' ' * 8} Description: tunnel to the edge
|
|
276
|
+
#{' ' * 8}- IpProtocol: udp
|
|
277
|
+
#{' ' * 8} FromPort: 7844
|
|
278
|
+
#{' ' * 8} ToPort: 7844
|
|
279
|
+
#{' ' * 8} CidrIp: 0.0.0.0/0
|
|
280
|
+
#{' ' * 8} Description: tunnel to the edge (QUIC)
|
|
281
|
+
YAML
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
285
|
+
# @return [String] the user-data lines that add swap, or nothing when `swap_gb` is 0
|
|
286
|
+
def swap_commands(plan)
|
|
287
|
+
return "" if plan.swap_gb.zero?
|
|
288
|
+
|
|
289
|
+
pad = " " * 10
|
|
290
|
+
<<~SH.lines.map { |line| "#{pad}#{line}" }.join
|
|
291
|
+
# swap so a memory spike at boot cannot take the box down
|
|
292
|
+
fallocate -l #{plan.swap_gb}G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
|
|
293
|
+
echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
|
294
|
+
SH
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# One ECR repository per container, keeping the newest 30 images. A task definition names
|
|
298
|
+
# images that already have repositories, so none are made.
|
|
299
|
+
#
|
|
300
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
301
|
+
# @return [String] CloudFormation resources, each preceded by a blank line
|
|
302
|
+
def ecr_repositories(plan)
|
|
303
|
+
return "" if plan.task_definition
|
|
304
|
+
|
|
305
|
+
plan.containers.map do |container|
|
|
306
|
+
id = "#{logical(container.name)}Repository"
|
|
307
|
+
<<~YAML.lines.map { |line| line.strip.empty? ? line : " #{line}" }.join
|
|
308
|
+
|
|
309
|
+
#{id}:
|
|
310
|
+
Type: AWS::ECR::Repository
|
|
311
|
+
Properties:
|
|
312
|
+
RepositoryName: #{container.repository}
|
|
313
|
+
ImageScanningConfiguration:
|
|
314
|
+
ScanOnPush: true
|
|
315
|
+
LifecyclePolicy:
|
|
316
|
+
LifecyclePolicyText: '{"rules":[{"rulePriority":1,"description":"keep the newest 30","selection":{"tagStatus":"any","countType":"imageCountMoreThan","countNumber":30},"action":{"type":"expire"}}]}'
|
|
317
|
+
YAML
|
|
318
|
+
end.join.chomp
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
322
|
+
# @return [String] one repository URI output per container
|
|
323
|
+
def ecr_outputs(plan)
|
|
324
|
+
return "" if plan.task_definition
|
|
325
|
+
|
|
326
|
+
plan.containers.map do |container|
|
|
327
|
+
" #{logical(container.name)}RepositoryUri:\n Value: !GetAtt #{logical(container.name)}Repository.RepositoryUri\n"
|
|
328
|
+
end.join.chomp
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# @param name [String] a container name such as `web-app`
|
|
332
|
+
# @return [String] a CloudFormation logical id fragment, such as `WebApp`
|
|
333
|
+
def logical(name)
|
|
334
|
+
name.split("-").map(&:capitalize).join
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# The proxy's configuration: an optional origin-secret guard, one block per route and a
|
|
338
|
+
# default for everything else.
|
|
339
|
+
#
|
|
340
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
341
|
+
# @return [String] the Caddyfile
|
|
342
|
+
def caddyfile(plan)
|
|
343
|
+
guarded = !plan.origin_header.nil?
|
|
344
|
+
site =
|
|
345
|
+
if guarded
|
|
346
|
+
"\t@origin header #{plan.origin_header} {$ORIGIN_SECRET}\n\n\thandle @origin {\n" \
|
|
347
|
+
"#{indent(indent(route_blocks(plan)))}\t}\n\n\thandle {\n\t\trespond \"Forbidden\" 403\n\t}\n"
|
|
348
|
+
else
|
|
349
|
+
indent(route_blocks(plan))
|
|
350
|
+
end
|
|
351
|
+
"#{caddy_header(plan)}#{caddy_global(guarded)}\n:80 {\n#{site}}\n\n#{CADDY_EXTRA}"
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
355
|
+
# @return [String] the `handle` blocks for each route, then the default
|
|
356
|
+
def route_blocks(plan)
|
|
357
|
+
routes = plan.routes.each_with_index.map do |route, i|
|
|
358
|
+
port = plan.containers.find { |c| c.name == route.container }.port
|
|
359
|
+
"@r#{i + 1} path #{route.paths.join(' ')}\nhandle @r#{i + 1} {\n\treverse_proxy 127.0.0.1:#{port}\n}\n\n"
|
|
360
|
+
end
|
|
361
|
+
"#{routes.join}handle {\n\treverse_proxy 127.0.0.1:#{plan.default.port}\n}\n"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
365
|
+
# @return [String] the comment that opens the Caddyfile
|
|
366
|
+
def caddy_header(plan)
|
|
367
|
+
origin =
|
|
368
|
+
if plan.origin_header
|
|
369
|
+
"Only requests that carry #{plan.origin_header} with the origin secret are proxied;\n" \
|
|
370
|
+
"# everything else gets a flat 403.\n"
|
|
371
|
+
else
|
|
372
|
+
"Every request is proxied.\n"
|
|
373
|
+
end
|
|
374
|
+
"# #{plan.infra_name}'s proxy. #{origin}"
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# With a guard, the proxy trusts every source: a request that reaches a `handle` block has
|
|
378
|
+
# already proved it came through the CDN by presenting the secret, and everything else is
|
|
379
|
+
# refused. Caddy then keeps the client address the CDN put in X-Forwarded-For.
|
|
380
|
+
#
|
|
381
|
+
# @param guarded [Boolean] whether an origin secret guards the site
|
|
382
|
+
# @return [String] the global options block
|
|
383
|
+
def caddy_global(guarded)
|
|
384
|
+
options = "\t# No certificate for the :80 site. disable_redirects, not off, so a listener a rehearsal\n" \
|
|
385
|
+
"\t# mounts under caddy-extra can still ask for `tls internal`.\n" \
|
|
386
|
+
"\tauto_https disable_redirects\n\tadmin off\n"
|
|
387
|
+
return "{\n#{options}}\n" unless guarded
|
|
388
|
+
|
|
389
|
+
"{\n#{options}\tservers {\n\t\ttrusted_proxies static 0.0.0.0/0 ::/0\n\t}\n}\n"
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# @param text [String] lines to indent one level with a tab
|
|
393
|
+
# @return [String] the text, each non-blank line prefixed with a tab
|
|
394
|
+
def indent(text)
|
|
395
|
+
text.lines.map { |line| line.strip.empty? ? line : "\t#{line}" }.join
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# The compose source: one entry per container, plus the origin secret the proxy checks.
|
|
399
|
+
#
|
|
400
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
401
|
+
# @return [String] `services.json`
|
|
402
|
+
def services_json(plan)
|
|
403
|
+
services = plan.containers.to_h { |c| [c.name, service_entry(plan, c)] }
|
|
404
|
+
origin = plan.origin_secret ? { "header" => plan.origin_header, "secret" => plan.origin_secret } : nil
|
|
405
|
+
document = { "services" => services, "origin" => origin }
|
|
406
|
+
document["task_definition"] = plan.task_definition if plan.task_definition
|
|
407
|
+
document["tunnel"] = tunnel_entry(plan.tunnel_service) if plan.tunnel_service
|
|
408
|
+
json = JSON.pretty_generate(document)
|
|
409
|
+
# An empty object prints as `{}` or `{` newline `}`, depending on the json gem.
|
|
410
|
+
"#{json.gsub(/\{\s*\}/, '{}')}\n"
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# A container's entry. With a task definition the image, environment and secrets are read
|
|
414
|
+
# from it at deploy time, so only the name and port are written here.
|
|
415
|
+
#
|
|
416
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
417
|
+
# @param container [Settings::Container] the container
|
|
418
|
+
# @return [Hash{String => Object}] its entry in `services.json`
|
|
419
|
+
def service_entry(plan, container)
|
|
420
|
+
entry = { "name" => container.name, "port" => container.port }
|
|
421
|
+
return entry if plan.task_definition
|
|
422
|
+
|
|
423
|
+
{ "name" => container.name, "repository" => container.repository, "port" => container.port,
|
|
424
|
+
"env" => container.env, "secrets" => container.secrets }
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# @param tunnel [Settings::Tunnel] the declared tunnel service
|
|
428
|
+
# @return [Hash{String => Object}] its entry in `services.json`
|
|
429
|
+
def tunnel_entry(tunnel)
|
|
430
|
+
{ "url" => "http://127.0.0.1:#{tunnel.port}", "token_secret" => tunnel.token_secret, "image" => tunnel.image }
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
434
|
+
# @param region [String] the validated region
|
|
435
|
+
# @return [String] `deploy-box.sh`
|
|
436
|
+
def deploy_box_sh(plan, region)
|
|
437
|
+
template("deploy-box.sh.tmpl", "STACK" => plan.infra_name, "BOX_STACK" => plan.box_stack,
|
|
438
|
+
"RDS_STACK" => plan.rds_stack, "REGION" => region,
|
|
439
|
+
"DIR" => "/opt/#{plan.infra_name}", "HEALTH_CHECKS" => health_checks(plan),
|
|
440
|
+
"USAGE" => deploy_usage(plan))
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
444
|
+
# @return [String] the comment lines that say how to call `deploy-box.sh`
|
|
445
|
+
def deploy_usage(plan)
|
|
446
|
+
if plan.task_definition
|
|
447
|
+
<<~USAGE
|
|
448
|
+
# deploy-box.sh [task-definition]
|
|
449
|
+
#
|
|
450
|
+
# The task definition (a family or family:revision) defaults to the latest active revision of
|
|
451
|
+
# #{plan.task_definition}. Secret values are resolved on the box by fetch-secrets.sh and never
|
|
452
|
+
# pass through the SSM command.
|
|
453
|
+
USAGE
|
|
454
|
+
else
|
|
455
|
+
<<~USAGE
|
|
456
|
+
# deploy-box.sh [name=tag ...]
|
|
457
|
+
#
|
|
458
|
+
# A container named without a tag runs its "latest" image. Secret values are resolved on the box by
|
|
459
|
+
# fetch-secrets.sh and never pass through the SSM command.
|
|
460
|
+
USAGE
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# The probes the post-roll check runs on the box, against the proxy on localhost.
|
|
465
|
+
#
|
|
466
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
467
|
+
# @return [String] bash lines that set `BAD=1` on a failed probe
|
|
468
|
+
def health_checks(plan)
|
|
469
|
+
write_out = '-w "%{http_code}"' # rubocop:disable Style/FormatStringToken -- curl's own token
|
|
470
|
+
probe = lambda do |label, args, wanted|
|
|
471
|
+
<<~SH.chomp
|
|
472
|
+
code=$(curl -s -o /dev/null #{write_out} --max-time 40 localhost/#{args})
|
|
473
|
+
#{wanted} && echo "ok #{label} -> $code" || { echo "FAIL #{label} -> $code"; BAD=1; }
|
|
474
|
+
SH
|
|
475
|
+
end
|
|
476
|
+
probes =
|
|
477
|
+
if plan.origin_header
|
|
478
|
+
[
|
|
479
|
+
"S=$(grep ^ORIGIN_SECRET= caddy.secrets.env | cut -d= -f2-)",
|
|
480
|
+
probe.call("a request without the origin secret is refused", "", '[ "$code" = 403 ]'),
|
|
481
|
+
probe.call("a request with the origin secret is served", %( -H "#{plan.origin_header}: $S"),
|
|
482
|
+
'[ "${code#5}" = "$code" ]')
|
|
483
|
+
]
|
|
484
|
+
else
|
|
485
|
+
[probe.call("the proxy serves /", "", '[ "${code#5}" = "$code" ]')]
|
|
486
|
+
end
|
|
487
|
+
(probes + tunnel_probe(plan)).join("\n")
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
491
|
+
# @return [Array<String>] the probe that waits for the tunnel to register, when one runs
|
|
492
|
+
def tunnel_probe(plan)
|
|
493
|
+
return [] unless plan.tunnel_service
|
|
494
|
+
|
|
495
|
+
[<<~SH.chomp]
|
|
496
|
+
for _ in 1 2 3 4 5 6; do
|
|
497
|
+
n=$(docker compose -f compose.json logs --no-color cloudflared 2>&1 | grep -ci "registered tunnel connection" || true)
|
|
498
|
+
[ "$n" -gt 0 ] && break
|
|
499
|
+
sleep 5
|
|
500
|
+
done
|
|
501
|
+
[ "$n" -gt 0 ] && echo "ok the tunnel registered $n connections" || { echo "FAIL the tunnel has no connection"; BAD=1; }
|
|
502
|
+
SH
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
506
|
+
# @return [String] a Makefile whose targets create the two stacks and roll the box
|
|
507
|
+
def makefile(plan)
|
|
508
|
+
<<~MAKE
|
|
509
|
+
# #{plan.infra_name}: one app box and one RDS instance.
|
|
510
|
+
# make stacks VPC=vpc-... PRIVATE_SUBNETS=subnet-a,subnet-b PUBLIC_SUBNET=subnet-c [REHEARSAL=true]
|
|
511
|
+
# make deploy #{plan.task_definition ? '[TASKDEF=family:revision]' : '[TAGS="web=20260101 worker=20260101"]'}
|
|
512
|
+
RDS_STACK = #{plan.rds_stack}
|
|
513
|
+
BOX_STACK = #{plan.box_stack}
|
|
514
|
+
# true makes a throwaway pair: the database is deleted with its stack and the box has no
|
|
515
|
+
# Elastic IP. The default is a production pair, with deletion protection.
|
|
516
|
+
REHEARSAL ?= false
|
|
517
|
+
|
|
518
|
+
.PHONY: stacks deploy
|
|
519
|
+
stacks:
|
|
520
|
+
\taws cloudformation deploy --template-file rds.yaml --stack-name $(RDS_STACK) --capabilities CAPABILITY_IAM \\
|
|
521
|
+
\t\t--parameter-overrides VpcId=$(VPC) PrivateSubnetIds=$(PRIVATE_SUBNETS) Rehearsal=$(REHEARSAL)
|
|
522
|
+
\taws cloudformation deploy --template-file box.yaml --stack-name $(BOX_STACK) --capabilities CAPABILITY_IAM \\
|
|
523
|
+
\t\t--parameter-overrides VpcId=$(VPC) SubnetId=$(PUBLIC_SUBNET) \\
|
|
524
|
+
\t\tDbSecurityGroupId=$$(aws cloudformation describe-stacks --stack-name $(RDS_STACK) --query "Stacks[0].Outputs[?OutputKey=='DbSecurityGroupId'].OutputValue" --output text) \\
|
|
525
|
+
\t\tDbSecretArn=$$(aws cloudformation describe-stacks --stack-name $(RDS_STACK) --query "Stacks[0].Outputs[?OutputKey=='DbSecretArn'].OutputValue" --output text) \\
|
|
526
|
+
\t\tAlertTopicArn=$$(aws cloudformation describe-stacks --stack-name $(RDS_STACK) --query "Stacks[0].Outputs[?OutputKey=='AlertTopicArn'].OutputValue" --output text) \\
|
|
527
|
+
\t\tRehearsal=$(REHEARSAL)
|
|
528
|
+
|
|
529
|
+
deploy:
|
|
530
|
+
\tbash ./deploy-box.sh $(#{plan.task_definition ? 'TASKDEF' : 'TAGS'})
|
|
531
|
+
MAKE
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Fills `@@NAME@@` markers. A marker alone on its line is replaced together with the line,
|
|
535
|
+
# so an empty value leaves no blank line behind; one inside a line is replaced in place.
|
|
536
|
+
#
|
|
537
|
+
# @param file [String] the template file's name in `TEMPLATE_DIR`
|
|
538
|
+
# @param values [Hash{String => String}] marker name => text
|
|
539
|
+
# @return [String] the rendered file
|
|
540
|
+
def template(file, values)
|
|
541
|
+
values.reduce(File.read(File.join(TEMPLATE_DIR, file))) do |text, (marker, value)|
|
|
542
|
+
own_line = /^@@#{marker}@@\n/
|
|
543
|
+
if text.match?(own_line)
|
|
544
|
+
text.sub(own_line) { value.empty? || value.end_with?("\n") ? value : "#{value}\n" }
|
|
545
|
+
else
|
|
546
|
+
text.gsub("@@#{marker}@@") { value }
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
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
|