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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Hecks.hecksagon "Hecks" do
|
|
2
|
-
|
|
2
|
+
attaches "Governance"
|
|
3
3
|
|
|
4
4
|
# The chapters the gem carries: the language declared in itself (Paging comes with Bluebook),
|
|
5
|
-
# Expression, Tenancy, Deploy and QualityControl (
|
|
6
|
-
%w[Bluebook Hecksagon World Adapter Port Translation Expression Tenancy Deploy QualityControl]
|
|
5
|
+
# Expression, Tenancy, Deploy, Site, Tickets and QualityControl (the last three bring their own ports).
|
|
6
|
+
%w[Bluebook Hecksagon World Adapter Port Translation Expression Tenancy Deploy Site Tickets QualityControl]
|
|
7
7
|
.each { |chapter| attaches chapter }
|
|
8
8
|
# Persistence is not bound here: hecks.world's `default_adapter` binds every aggregate, so
|
|
9
9
|
# environments/memory.world can swap the whole domain without a double bind.
|
|
@@ -95,6 +95,12 @@ Hecks.hecksagon "Hecks" do
|
|
|
95
95
|
answers "DoorAnswered"
|
|
96
96
|
refuses "DoorRefused"
|
|
97
97
|
end
|
|
98
|
+
|
|
99
|
+
asks "Scaffold", to: Door do
|
|
100
|
+
attribute :run, DoorKey
|
|
101
|
+
answers "DoorAnswered"
|
|
102
|
+
refuses "DoorRefused"
|
|
103
|
+
end
|
|
98
104
|
end
|
|
99
105
|
|
|
100
106
|
Hecks::Door.port "Terminal" do
|
|
@@ -103,6 +109,12 @@ Hecks.hecksagon "Hecks" do
|
|
|
103
109
|
answers "DoorAnswered"
|
|
104
110
|
refuses "DoorRefused"
|
|
105
111
|
end
|
|
112
|
+
|
|
113
|
+
asks "Converse", to: Door do
|
|
114
|
+
attribute :run, DoorKey
|
|
115
|
+
answers "DoorAnswered"
|
|
116
|
+
refuses "DoorRefused"
|
|
117
|
+
end
|
|
106
118
|
end
|
|
107
119
|
|
|
108
120
|
Hecks::Build.port "RustToolchain" do
|
|
@@ -394,7 +406,7 @@ Hecks.hecksagon "Hecks" do
|
|
|
394
406
|
end
|
|
395
407
|
|
|
396
408
|
# The one seam where Deploy's "Tenant" (a validated request) becomes Tenancy's "Tenant" (a fact),
|
|
397
|
-
# as tenancy.hecksagon declares it for the chapters booted on their own: `hecks deploy provision`
|
|
409
|
+
# as tenancy.hecksagon declares it for the chapters booted on their own: `hecks deploy tenant.provision`
|
|
398
410
|
# registers the tenant it provisioned. It sits here, after `attaches` has loaded both chapters.
|
|
399
411
|
Hecks.hecksagon "Tenancy" do
|
|
400
412
|
translates "RegisterProvisionedTenant" do
|
data/lib/hecks/hecks/hecks.world
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# Postgres (a `postgres://` URL, or a bare database name); otherwise a local database named for
|
|
16
16
|
# the domain. It connects as the ordinary role `hecks`, never a superuser: the era write-fence
|
|
17
17
|
# (row-level security) is bypassed by one, so boot refuses it. Create the role once with
|
|
18
|
-
# `hecks quality_control create_ledger_role hecks` and the database with `createdb -O hecks hecks`.
|
|
18
|
+
# `hecks quality_control sweep.create_ledger_role hecks` and the database with `createdb -O hecks hecks`.
|
|
19
19
|
#
|
|
20
20
|
# PostgresEra versions the storage shape. Once a database holds an era, a later change to this
|
|
21
21
|
# domain's shape needs a translation edge, kept in `translations/` beside the bluebook; an
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
# `failure_states` are the lifecycle states `--wait` reports as exit 1 (every failure state the
|
|
27
27
|
# chapter's lifecycles declare), `names` gives a command the launcher name clients type,
|
|
28
28
|
# `executable` is where `hecks project_cli` writes this chapter's launcher (the gem's
|
|
29
|
-
# `exe/hecks`), `streams` lists the questions `--stream` may tail (`Follow`), `
|
|
30
|
-
# the
|
|
31
|
-
# `hecks console` needs no database), and `legacy` names the
|
|
29
|
+
# `exe/hecks`), `streams` lists the questions `--stream` may tail (`Follow`), `memory_commands` names
|
|
30
|
+
# the commands that launcher runs on the Memory environment unless `HECKS_ENVIRONMENT` is set (so
|
|
31
|
+
# `hecks console` needs no database), and `legacy` names the commands it hands to `Hecks::CLI` in the
|
|
32
32
|
# positional forms ADR 0066 shipped.
|
|
33
33
|
Hecks.world "Hecks" do
|
|
34
34
|
realm "Hecks"
|
|
@@ -37,10 +37,11 @@ Hecks.world "Hecks" do
|
|
|
37
37
|
launcher "Launcher", run_keys: true,
|
|
38
38
|
failure_states: %w[flagged failed drifted unreachable refused faulted halted stopped
|
|
39
39
|
abandoned red needs_fix],
|
|
40
|
-
names: { "mcp" => "serve_mcp", "console" => "open_console"
|
|
40
|
+
names: { "mcp" => "door.serve_mcp", "console" => "operation.open_console", "init" => "door.init",
|
|
41
|
+
"interview" => "door.interview" },
|
|
41
42
|
streams: %w[Follow],
|
|
42
43
|
executable: "exe/hecks",
|
|
43
|
-
|
|
44
|
+
memory_commands: %w[console init interview],
|
|
44
45
|
legacy: %w[run docs narrate ir stores model_check smoke_test
|
|
45
46
|
project_diagrams project_cli mcp]
|
|
46
47
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Hecks.behaviors "Site" do
|
|
2
|
+
vision "A projection of a site's route table is recorded as requested before anything outside is asked."
|
|
3
|
+
|
|
4
|
+
loads "hecks.bluebook", "custodian.bluebook", "codebase.bluebook", "context_map.hecksagon", "hecks.hecksagon", "site.world", "environments/memory.world"
|
|
5
|
+
|
|
6
|
+
test "ProjectSite keeps a project that has no bluebook as a faulted projection" do
|
|
7
|
+
tests "ProjectSite", on: "SiteProjection"
|
|
8
|
+
input run: { value: "routes" }, domain: { value: "/tmp/hecks-behaviors-absent/project" }
|
|
9
|
+
expect emits: %w[SiteProjectionRequested SiteRefused SiteFaulted]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
test "ProjectSite refuses a run that is not named" do
|
|
13
|
+
tests "ProjectSite", on: "SiteProjection"
|
|
14
|
+
input run: { value: "" }, domain: { value: "/tmp/hecks-behaviors-absent/project" }
|
|
15
|
+
expect refused: "must match"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# The Site chapter's launcher when the Hecks domain attaches it: `hecks site <verb>` reads the
|
|
2
|
+
# world named for the chapter, as `deploy.world` does for the Deploy chapter. Its store is not set
|
|
3
|
+
# here: hecks.world's `default_adapter` fills the gap, so the memory overlay swaps it with the rest.
|
|
4
|
+
#
|
|
5
|
+
# `run_keys` mints the `run` key of a projection, and `--wait` reports a faulted projection (a
|
|
6
|
+
# routes.ts out of date under `--check`, or a route table the projector refused) as exit 1.
|
|
7
|
+
Hecks.world "Site" do
|
|
8
|
+
launcher "Launcher", run_keys: true,
|
|
9
|
+
failure_states: %w[faulted refused halted stopped abandoned red needs_fix]
|
|
10
|
+
end
|
|
@@ -5,9 +5,7 @@ Hecks.bluebook "Hecksagon" do
|
|
|
5
5
|
identified_by HecksagonName, as: :domain
|
|
6
6
|
|
|
7
7
|
attribute :subscriptions, list_of(Subscription)
|
|
8
|
-
attribute :
|
|
9
|
-
attribute :vendored_bluebooks, list_of(VendoredBluebook)
|
|
10
|
-
attribute :attached_chapters, list_of(AttachedChapter)
|
|
8
|
+
attribute :attachments, list_of(Attachment)
|
|
11
9
|
|
|
12
10
|
value_object "HecksagonName" do
|
|
13
11
|
attribute :value, String
|
|
@@ -19,22 +17,20 @@ Hecks.bluebook "Hecksagon" do
|
|
|
19
17
|
invariant("a subscription names an event") { !value.to_s.empty? }
|
|
20
18
|
end
|
|
21
19
|
|
|
22
|
-
# A
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
attribute :value, String
|
|
31
|
-
invariant("a vendored bluebook is named") { !value.to_s.empty? }
|
|
20
|
+
# A chapter this hecksagon attaches: `value` is its name, `source` is `gem` for a chapter the
|
|
21
|
+
# gem carries (a framework member such as Governance, or a language chapter such as Deploy)
|
|
22
|
+
# or `vendor` for a package vendored into the consuming project, not shipped in hecks's lib/.
|
|
23
|
+
value_object "Attachment" do
|
|
24
|
+
attribute :value, String
|
|
25
|
+
attribute :source, String, default: "gem"
|
|
26
|
+
invariant("an attachment is named") { !value.to_s.empty? }
|
|
27
|
+
invariant("an attachment comes from the gem or from vendor") { source == "gem" || source == "vendor" }
|
|
32
28
|
end
|
|
33
29
|
|
|
34
|
-
#
|
|
35
|
-
value_object "
|
|
30
|
+
# The name an attachment is asked for by, before its source is known.
|
|
31
|
+
value_object "ChapterName" do
|
|
36
32
|
attribute :value, String
|
|
37
|
-
invariant("
|
|
33
|
+
invariant("a chapter is named") { !value.to_s.empty? }
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
command "Declare" do
|
|
@@ -59,35 +55,14 @@ Hecks.bluebook "Hecksagon" do
|
|
|
59
55
|
emits "SubscriptionAdded"
|
|
60
56
|
end
|
|
61
57
|
|
|
62
|
-
command "UsesFramework" do
|
|
63
|
-
role "Deployment"
|
|
64
|
-
goal "Attach a framework/bluebook/ member to this hecksagon"
|
|
65
|
-
|
|
66
|
-
attribute :member, FrameworkMember
|
|
67
|
-
|
|
68
|
-
sets :framework_members, append: { value: :member }
|
|
69
|
-
|
|
70
|
-
emits "FrameworkMemberAttached"
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
command "UsesEmbryonautBluebook" do
|
|
74
|
-
role "Deployment"
|
|
75
|
-
goal "Attach a vendored, external bluebook to this hecksagon"
|
|
76
|
-
|
|
77
|
-
attribute :package, VendoredBluebook
|
|
78
|
-
|
|
79
|
-
sets :vendored_bluebooks, append: { value: :package }
|
|
80
|
-
|
|
81
|
-
emits "VendoredBluebookAttached"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
58
|
command "Attaches" do
|
|
85
59
|
role "Deployment"
|
|
86
|
-
goal "Attach a chapter
|
|
60
|
+
goal "Attach a chapter to this hecksagon, from the gem or from vendor"
|
|
87
61
|
|
|
88
|
-
attribute :chapter,
|
|
62
|
+
attribute :chapter, ChapterName
|
|
63
|
+
attribute :source, String, default: "gem"
|
|
89
64
|
|
|
90
|
-
sets :
|
|
65
|
+
sets :attachments, append: { value: :chapter, source: :source }
|
|
91
66
|
|
|
92
67
|
emits "ChapterAttached"
|
|
93
68
|
end
|
|
@@ -107,9 +82,10 @@ Hecks.bluebook "Hecksagon" do
|
|
|
107
82
|
|
|
108
83
|
member word: "hecksagon", context: "File", body: "keywords", inner: "Hecksagon", opens: "Hecksagon", fills: ""
|
|
109
84
|
member word: "subscribe", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "subscriptions"
|
|
110
|
-
member word: "
|
|
111
|
-
|
|
112
|
-
member word: "
|
|
85
|
+
member word: "attaches", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments"
|
|
86
|
+
# Deprecated spellings of `attaches`; they warn and are removed in 3.2.0.
|
|
87
|
+
member word: "uses_framework", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated"
|
|
88
|
+
member word: "uses_embryonaut_bluebook", context: "Hecksagon", body: "none", inner: "", opens: "", fills: "attachments", status: "deprecated"
|
|
113
89
|
member word: "port", context: "Hecksagon", body: "keywords", inner: "DomainPort", opens: "DomainPort", fills: "", calls: "port_impl"
|
|
114
90
|
# A cross-domain reaction wired here rather than in the bluebook; reuses the Policy context.
|
|
115
91
|
# No `calls:` — HecksagonBuilder#translates is an ordinary method, found before `word_gate`.
|
|
@@ -138,9 +114,10 @@ Hecks.bluebook "Hecksagon" do
|
|
|
138
114
|
|
|
139
115
|
member keyword: "hecksagon", context: "File", at: "1", named: "", kind: "text", required: "true", fills: "domain"
|
|
140
116
|
member keyword: "subscribe", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "subscriptions"
|
|
141
|
-
member keyword: "
|
|
142
|
-
member keyword: "
|
|
143
|
-
member keyword: "
|
|
117
|
+
member keyword: "attaches", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments"
|
|
118
|
+
member keyword: "attaches", context: "Hecksagon", at: "", named: "from", kind: "symbol", required: "false", fills: "source"
|
|
119
|
+
member keyword: "uses_framework", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments"
|
|
120
|
+
member keyword: "uses_embryonaut_bluebook", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "attachments"
|
|
144
121
|
member keyword: "port", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name"
|
|
145
122
|
member keyword: "translates", context: "Hecksagon", at: "1", named: "", kind: "text", required: "true", fills: "name"
|
|
146
123
|
|
|
@@ -16,7 +16,7 @@ module Hecks
|
|
|
16
16
|
|
|
17
17
|
# Runs both halves of the era gate over every bluebook in a registry.
|
|
18
18
|
#
|
|
19
|
-
# Looks up each bluebook's own source separately: `
|
|
19
|
+
# Looks up each bluebook's own source separately: `attaches` can share a boot between
|
|
20
20
|
# the domain's own bluebook and a differently-sourced one, so one file read once and reused
|
|
21
21
|
# for every bluebook would attribute the wrong text.
|
|
22
22
|
#
|
|
@@ -84,7 +84,7 @@ module Hecks
|
|
|
84
84
|
# Reads the source text one bluebook was declared in, wherever that source lives.
|
|
85
85
|
#
|
|
86
86
|
# Matched by declared name, not position: a domain directory using
|
|
87
|
-
# `
|
|
87
|
+
# `attaches` may hold more than one file, and a framework
|
|
88
88
|
# member or vendored bluebook is excluded from the single-file
|
|
89
89
|
# fallback so its own real source is read, not a sibling's.
|
|
90
90
|
#
|
|
@@ -150,11 +150,11 @@ module Hecks
|
|
|
150
150
|
#
|
|
151
151
|
# @param registry [Runtime::Registry] the registry whose hecksagons are searched
|
|
152
152
|
# @param bluebook_name [String] the bluebook's declared Pascal-case name
|
|
153
|
-
# @return [String, nil] the package name as written in `
|
|
153
|
+
# @return [String, nil] the package name as written in `attaches ... from: :vendor`; nil
|
|
154
154
|
# when no hecksagon vendored a package whose name Pascal-cases to `bluebook_name`
|
|
155
155
|
def vendored_bluebook_name_for(registry, bluebook_name)
|
|
156
156
|
registry.hecksagons.each_value do |hecksagon|
|
|
157
|
-
match = hecksagon.
|
|
157
|
+
match = hecksagon.vendored_packages.find { |name| Naming.pascal(name) == bluebook_name }
|
|
158
158
|
return match if match
|
|
159
159
|
end
|
|
160
160
|
nil
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Projections
|
|
3
|
+
module Deploy
|
|
4
|
+
module Box
|
|
5
|
+
# Resolves a world's `deployed_to("AwsBox")` settings into the checked plan the box
|
|
6
|
+
# generator renders from.
|
|
7
|
+
#
|
|
8
|
+
# Every string that reaches a template or a script is matched against a conservative
|
|
9
|
+
# pattern first, so a world file cannot splice shell, YAML or Caddy syntax into the output.
|
|
10
|
+
# A setting the world leaves out resolves to the default the golden stack was built with.
|
|
11
|
+
module Settings
|
|
12
|
+
NAME = /\A[a-z][a-z0-9-]{0,40}\z/
|
|
13
|
+
REPOSITORY = %r{\A[a-z0-9][a-z0-9._/-]{1,100}\z}
|
|
14
|
+
URL_PATH = %r{\A/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*\z}
|
|
15
|
+
SECRET_NAME = %r{\A[A-Za-z0-9/_+=.@-]{1,256}\*?\z}
|
|
16
|
+
ENV_KEY = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
17
|
+
ENV_VALUE = /\A[^\x00-\x1f\x7f]*\z/
|
|
18
|
+
HEADER = /\A[A-Za-z][A-Za-z0-9-]{0,63}\z/
|
|
19
|
+
DB_NAME = /\A[a-zA-Z][a-zA-Z0-9]{0,62}\z/
|
|
20
|
+
ENGINE = /\A\d{2}(\.\d{1,2})?\z/
|
|
21
|
+
PREFIX = /\A[a-z][a-z0-9-]{0,20}\z/
|
|
22
|
+
IMAGE = %r{\A[a-z0-9][a-z0-9._/:@-]{1,200}\z}
|
|
23
|
+
TASKDEF = /\A[a-zA-Z0-9_-]{1,255}\z/
|
|
24
|
+
SCHEMA = /\A[a-z][a-z0-9_]{0,62}\z/
|
|
25
|
+
MIGRATION_SHAPE = "migration: a hash needs `schemas`, a list of the schema names to copy".freeze
|
|
26
|
+
S3_BUCKET = /\A[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]\z/
|
|
27
|
+
S3_SHAPE = "s3_access: a list of `{ bucket: \"name\", write: true }` hashes (write is optional)".freeze
|
|
28
|
+
FROM_TASKDEF = %i[env secrets repository].freeze
|
|
29
|
+
# Default images, each a version tag plus the digest of its multi-architecture index,
|
|
30
|
+
# so a rebuilt box pulls the same bytes.
|
|
31
|
+
TUNNEL_IMAGE = "cloudflare/cloudflared:2026.9.3" \
|
|
32
|
+
"@sha256:072c067d25ccbe61d46e18f0d0723255f2bb5304f7317caa95b27031520ff92c".freeze
|
|
33
|
+
PROXY_IMAGE = "public.ecr.aws/docker/library/caddy:2.8" \
|
|
34
|
+
"@sha256:226d1f059b75399fe19182893c7184591c07b97afc8dfcf44eeb80c9a77a530f".freeze
|
|
35
|
+
TUNNEL_SHAPE = "tunnel: a hash needs `to` (the container it forwards to) and `token_secret` " \
|
|
36
|
+
"(the secret holding the tunnel token)".freeze
|
|
37
|
+
ORIGIN_PAIR = "origin_header and origin_secret go together: the header a CDN sends, " \
|
|
38
|
+
"and the secret that holds its value".freeze
|
|
39
|
+
|
|
40
|
+
# One container the box runs: its image repository, its port and its settings.
|
|
41
|
+
#
|
|
42
|
+
# @!attribute [r] name [String] the compose service name
|
|
43
|
+
# @!attribute [r] repository [String] the ECR repository holding its images
|
|
44
|
+
# @!attribute [r] port [Integer] the port it listens on, on the box's own network
|
|
45
|
+
# @!attribute [r] env [Hash{String => String}] plain environment variables
|
|
46
|
+
# @!attribute [r] secrets [Hash{String => String}] environment variable => secret name,
|
|
47
|
+
# resolved on the box at deploy time and never written into a template
|
|
48
|
+
Container = Struct.new(:name, :repository, :port, :env, :secrets, keyword_init: true)
|
|
49
|
+
|
|
50
|
+
# A set of URL paths one container serves.
|
|
51
|
+
#
|
|
52
|
+
# @!attribute [r] container [String] the container's name
|
|
53
|
+
# @!attribute [r] paths [Array<String>] Caddy path patterns, such as "/cms/*"
|
|
54
|
+
Route = Struct.new(:container, :paths, keyword_init: true)
|
|
55
|
+
|
|
56
|
+
# A Cloudflare tunnel the box runs as a service, pointed at one container.
|
|
57
|
+
#
|
|
58
|
+
# @!attribute [r] container [String] the container the tunnel forwards to
|
|
59
|
+
# @!attribute [r] port [Integer] that container's port
|
|
60
|
+
# @!attribute [r] token_secret [String] the secret holding the tunnel token
|
|
61
|
+
# @!attribute [r] image [String] the cloudflared image
|
|
62
|
+
Tunnel = Struct.new(:container, :port, :token_secret, :image, keyword_init: true)
|
|
63
|
+
|
|
64
|
+
# The data a project moves from its old database into the new RDS instance.
|
|
65
|
+
#
|
|
66
|
+
# @!attribute [r] schemas [Array<String>] the schemas to copy
|
|
67
|
+
# @!attribute [r] database [String] the database holding them on the RDS instance
|
|
68
|
+
# @!attribute [r] source_database [String] the database holding them on the old server
|
|
69
|
+
Migration = Struct.new(:schemas, :database, :source_database, keyword_init: true)
|
|
70
|
+
|
|
71
|
+
# An S3 bucket the box's role may read, and in production write.
|
|
72
|
+
#
|
|
73
|
+
# @!attribute [r] name [String] the bucket's name
|
|
74
|
+
# @!attribute [r] write [Boolean] whether a production box may also write and delete
|
|
75
|
+
Bucket = Struct.new(:name, :write, keyword_init: true)
|
|
76
|
+
|
|
77
|
+
# Everything the generator reads, checked.
|
|
78
|
+
Plan = Struct.new(
|
|
79
|
+
:infra_name, :stack_prefix, :instance_type, :volume_gb, :swap_gb, :database_class,
|
|
80
|
+
:storage_gb, :backup_days, :snapshots_keep, :database_name, :engine_version,
|
|
81
|
+
:containers, :routes, :default_container, :origin_header, :origin_secret,
|
|
82
|
+
:secret_prefixes, :tunnel, :tunnel_service, :proxy_image, :task_definition,
|
|
83
|
+
:migration, :s3_buckets, keyword_init: true
|
|
84
|
+
) do
|
|
85
|
+
# @return [String] the CloudFormation stack that holds the database
|
|
86
|
+
def rds_stack = "#{stack_prefix}-#{infra_name}-rds"
|
|
87
|
+
|
|
88
|
+
# @return [String] the CloudFormation stack that holds the box
|
|
89
|
+
def box_stack = "#{stack_prefix}-#{infra_name}-box"
|
|
90
|
+
|
|
91
|
+
# @return [Container] the container that answers every path no route claims
|
|
92
|
+
def default
|
|
93
|
+
containers.find { |c| c.name == default_container }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
module_function
|
|
98
|
+
|
|
99
|
+
# @param deploy_settings [Hash{Symbol => Object}] the world's `AwsBox` settings
|
|
100
|
+
# @param target [Object] the validated `BoxTarget` (its instance type, volume, database
|
|
101
|
+
# class and storage, already checked by `Declare`)
|
|
102
|
+
# @param infra_name [String] the stack name the world or the domain gives
|
|
103
|
+
# @return [Plan] the resolved plan
|
|
104
|
+
# @raise [ArgumentError] when a setting is missing, malformed or inconsistent
|
|
105
|
+
def resolve(deploy_settings:, target:, infra_name:)
|
|
106
|
+
s = deploy_settings
|
|
107
|
+
check(:stack_name, infra_name, NAME)
|
|
108
|
+
database_name = read_database_name(s, infra_name)
|
|
109
|
+
listed = s.fetch(:containers) { raise ArgumentError, missing_containers }
|
|
110
|
+
task_definition = read_task_definition(s[:task_definition], listed)
|
|
111
|
+
containers = read_containers(listed, infra_name)
|
|
112
|
+
header, secret = read_origin(s)
|
|
113
|
+
tunnel, tunnel_service = read_tunnel(s.fetch(:tunnel, false), containers)
|
|
114
|
+
|
|
115
|
+
Plan.new(
|
|
116
|
+
infra_name: infra_name, stack_prefix: check(:stack_prefix, s.fetch(:stack_prefix, "hecks"), PREFIX),
|
|
117
|
+
**declared_sizes(target), **read_sizes(s), database_name: database_name,
|
|
118
|
+
engine_version: check(:engine_version, s.fetch(:engine_version, "16").to_s, ENGINE),
|
|
119
|
+
containers: containers, routes: read_routes(s.fetch(:routes, []), containers),
|
|
120
|
+
default_container: read_default(s[:default_container], containers), origin_header: header,
|
|
121
|
+
origin_secret: secret, secret_prefixes: read_prefixes(s, infra_name),
|
|
122
|
+
tunnel: tunnel, tunnel_service: tunnel_service,
|
|
123
|
+
proxy_image: check(:proxy_image, s.fetch(:proxy_image, PROXY_IMAGE), IMAGE),
|
|
124
|
+
task_definition: task_definition, migration: read_migration(s[:migration], database_name),
|
|
125
|
+
s3_buckets: read_s3_access(s.fetch(:s3_access, []))
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param target [Object] the declared `BoxTarget`
|
|
130
|
+
# @return [Hash{Symbol => Object}] the sizes `Declare` has already validated
|
|
131
|
+
def declared_sizes(target)
|
|
132
|
+
%i[instance_type volume_gb database_class storage_gb].to_h { |key| [key, target.state[key].value] }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @param settings [Hash{Symbol => Object}] the world's settings
|
|
136
|
+
# @return [Hash{Symbol => Integer}] swap, backup retention and snapshot retention, bounded
|
|
137
|
+
def read_sizes(settings)
|
|
138
|
+
{
|
|
139
|
+
swap_gb: integer(:swap_gb, settings.fetch(:swap_gb, 2), 0, 64),
|
|
140
|
+
backup_days: integer(:backup_days, settings.fetch(:backup_days, 7), 1, 35),
|
|
141
|
+
snapshots_keep: integer(:snapshots_keep, settings.fetch(:snapshots_keep, 7), 1, 1000)
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# @param value [Hash{Symbol => Object}, nil] the world's `migration` setting
|
|
146
|
+
# @param database_name [String] the RDS database, which the schemas move into by default
|
|
147
|
+
# @return [Migration, nil] the data to move, or nil when the world declares none
|
|
148
|
+
# @raise [ArgumentError] when the setting is not a hash with a non-empty list of schemas
|
|
149
|
+
def read_migration(value, database_name)
|
|
150
|
+
return nil if value.nil?
|
|
151
|
+
raise ArgumentError, MIGRATION_SHAPE unless value.is_a?(Hash)
|
|
152
|
+
|
|
153
|
+
listed = value.fetch(:schemas) { raise ArgumentError, MIGRATION_SHAPE }
|
|
154
|
+
raise ArgumentError, MIGRATION_SHAPE unless listed.is_a?(Array) && !listed.empty?
|
|
155
|
+
|
|
156
|
+
database = check(:migration_database, value.fetch(:database, database_name), DB_NAME)
|
|
157
|
+
Migration.new(schemas: listed.map { |name| check(:migration_schemas, name, SCHEMA) }.uniq, database: database,
|
|
158
|
+
source_database: check(:migration_source_database, value.fetch(:source_database, database), DB_NAME))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @param list [Array<Hash>] the world's `s3_access` setting
|
|
162
|
+
# @return [Array<Bucket>] the buckets, each readable and optionally writable in production
|
|
163
|
+
# @raise [ArgumentError] when the setting is not a list of bucket hashes
|
|
164
|
+
def read_s3_access(list)
|
|
165
|
+
raise ArgumentError, S3_SHAPE unless list.is_a?(Array)
|
|
166
|
+
|
|
167
|
+
list.map do |spec|
|
|
168
|
+
raise ArgumentError, S3_SHAPE unless spec.is_a?(Hash)
|
|
169
|
+
|
|
170
|
+
name = check(:s3_bucket, spec.fetch(:bucket) { raise ArgumentError, S3_SHAPE }, S3_BUCKET)
|
|
171
|
+
Bucket.new(name: name, write: boolean(:s3_write, spec.fetch(:write, false)))
|
|
172
|
+
end.uniq(&:name)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def read_database_name(settings, infra_name)
|
|
176
|
+
check(:database_name, settings.fetch(:database_name, infra_name.gsub(/[^a-zA-Z0-9]/, "")), DB_NAME)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# With a task definition, the images, environment and secrets are read from it at deploy
|
|
180
|
+
# time, so a container that also sets them is ambiguous and refused.
|
|
181
|
+
#
|
|
182
|
+
# @param family [String, nil] the ECS task definition family the world names
|
|
183
|
+
# @param listed [Array<Hash>] the containers as the world wrote them
|
|
184
|
+
# @return [String, nil] the checked family, or nil when the world does not use one
|
|
185
|
+
# @raise [ArgumentError] when the family is malformed or a container sets what it supplies
|
|
186
|
+
def read_task_definition(family, listed)
|
|
187
|
+
return nil if family.nil?
|
|
188
|
+
|
|
189
|
+
check(:task_definition, family, TASKDEF)
|
|
190
|
+
Array(listed).each do |spec|
|
|
191
|
+
clash = spec.is_a?(Hash) ? FROM_TASKDEF & spec.keys : []
|
|
192
|
+
next if clash.empty?
|
|
193
|
+
|
|
194
|
+
raise ArgumentError, "containers: #{spec[:name]} sets #{clash.join(', ')}, which the task definition " \
|
|
195
|
+
"#{family} supplies; drop #{clash.size == 1 ? 'it' : 'them'} or drop task_definition"
|
|
196
|
+
end
|
|
197
|
+
family
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def read_containers(list, infra_name)
|
|
201
|
+
raise ArgumentError, missing_containers unless list.is_a?(Array) && !list.empty?
|
|
202
|
+
|
|
203
|
+
containers = list.map { |c| read_container(c, infra_name) }
|
|
204
|
+
names = containers.map(&:name)
|
|
205
|
+
dup = names.find { |n| names.count(n) > 1 }
|
|
206
|
+
raise ArgumentError, "containers: two containers are named #{dup.inspect}" if dup
|
|
207
|
+
|
|
208
|
+
ports = containers.map(&:port)
|
|
209
|
+
clash = ports.find { |p| ports.count(p) > 1 }
|
|
210
|
+
raise ArgumentError, "containers: two containers listen on port #{clash}; they share the box's network" if clash
|
|
211
|
+
|
|
212
|
+
containers
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def read_container(spec, infra_name)
|
|
216
|
+
raise ArgumentError, "containers: each container is a hash, got #{spec.inspect}" unless spec.is_a?(Hash)
|
|
217
|
+
|
|
218
|
+
name = check(:container_name, spec.fetch(:name) { raise ArgumentError, "containers: a container has no name" }, NAME)
|
|
219
|
+
Container.new(
|
|
220
|
+
name: name, repository: check(:repository, spec.fetch(:repository, "#{infra_name}-#{name}"), REPOSITORY),
|
|
221
|
+
port: integer(:port, spec.fetch(:port) { raise ArgumentError, "containers: #{name} has no port" }, 1, 65_535),
|
|
222
|
+
env: string_map(:env, spec.fetch(:env, {}), ENV_KEY, ENV_VALUE),
|
|
223
|
+
secrets: string_map(:secrets, spec.fetch(:secrets, {}), ENV_KEY, SECRET_NAME)
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def read_routes(list, containers)
|
|
228
|
+
raise ArgumentError, "routes: expected a list" unless list.is_a?(Array)
|
|
229
|
+
|
|
230
|
+
list.map { |spec| read_route(spec, containers) }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def read_route(spec, containers)
|
|
234
|
+
container = spec.fetch(:container) { raise ArgumentError, "routes: a route has no container" }
|
|
235
|
+
name = check(:route_container, container, NAME)
|
|
236
|
+
known_container!(:routes, name, containers)
|
|
237
|
+
paths = Array(spec.fetch(:paths) { raise ArgumentError, "routes: the route for #{name} has no paths" })
|
|
238
|
+
raise ArgumentError, "routes: the route for #{name} has no paths" if paths.empty?
|
|
239
|
+
|
|
240
|
+
Route.new(container: name, paths: paths.map { |p| check(:path, p, URL_PATH) })
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def read_default(name, containers)
|
|
244
|
+
return containers.first.name if name.nil? && containers.size == 1
|
|
245
|
+
raise ArgumentError, "default_container: name the container that answers every other path" if name.nil?
|
|
246
|
+
|
|
247
|
+
known_container!(:default_container, check(:default_container, name, NAME), containers)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def known_container!(key, name, containers)
|
|
251
|
+
return name if containers.any? { |c| c.name == name }
|
|
252
|
+
|
|
253
|
+
raise ArgumentError, "#{key}: #{name.inspect} is not a declared container"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def read_origin(settings)
|
|
257
|
+
header = settings[:origin_header]
|
|
258
|
+
secret = settings[:origin_secret]
|
|
259
|
+
return [nil, nil] if header.nil? && secret.nil?
|
|
260
|
+
raise ArgumentError, ORIGIN_PAIR if header.nil? || secret.nil?
|
|
261
|
+
|
|
262
|
+
[check(:origin_header, header, HEADER), check(:origin_secret, secret, SECRET_NAME)]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# `tunnel true` only opens the outbound port; a hash also runs cloudflared as a service.
|
|
266
|
+
#
|
|
267
|
+
# @param value [Boolean, Hash{Symbol => Object}] the world's `tunnel` setting
|
|
268
|
+
# @param containers [Array<Container>] the declared containers
|
|
269
|
+
# @return [Array(Boolean, Tunnel)] whether the egress opens, and the service if one runs
|
|
270
|
+
def read_tunnel(value, containers)
|
|
271
|
+
return [boolean(:tunnel, value), nil] unless value.is_a?(Hash)
|
|
272
|
+
|
|
273
|
+
to = value.fetch(:to) { raise ArgumentError, TUNNEL_SHAPE }
|
|
274
|
+
target = known_container!(:tunnel, check(:tunnel_to, to, NAME), containers)
|
|
275
|
+
token = check(:tunnel_token_secret, value.fetch(:token_secret) { raise ArgumentError, TUNNEL_SHAPE }, SECRET_NAME)
|
|
276
|
+
image = check(:tunnel_image, value.fetch(:image, TUNNEL_IMAGE), IMAGE)
|
|
277
|
+
port = containers.find { |c| c.name == target }.port
|
|
278
|
+
[true, Tunnel.new(container: target, port: port, token_secret: token, image: image)]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def read_prefixes(settings, infra_name)
|
|
282
|
+
list = Array(settings.fetch(:secret_prefixes, ["#{infra_name}/*"]))
|
|
283
|
+
list.map { |p| check(:secret_prefixes, p, SECRET_NAME) }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def string_map(key, value, key_pattern, value_pattern)
|
|
287
|
+
raise ArgumentError, "#{key}: expected a hash" unless value.is_a?(Hash)
|
|
288
|
+
|
|
289
|
+
value.to_h { |k, v| [check(key, k.to_s, key_pattern), check(key, v.to_s, value_pattern)] }
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def check(key, value, pattern)
|
|
293
|
+
return value if value.is_a?(String) && value.match?(pattern)
|
|
294
|
+
|
|
295
|
+
raise ArgumentError, "#{key}: #{value.inspect} does not match #{pattern.inspect}"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def integer(key, value, low, high)
|
|
299
|
+
return value if value.is_a?(Integer) && value.between?(low, high)
|
|
300
|
+
|
|
301
|
+
raise ArgumentError, "#{key}: #{value.inspect} must be an integer from #{low} to #{high}"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def boolean(key, value)
|
|
305
|
+
return value if [true, false].include?(value)
|
|
306
|
+
|
|
307
|
+
raise ArgumentError, "#{key}: #{value.inspect} must be true or false"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def missing_containers
|
|
311
|
+
<<~MSG
|
|
312
|
+
deployed_to("AwsBox") needs at least one container. Add one, e.g.:
|
|
313
|
+
|
|
314
|
+
deployed_to("AwsBox") do
|
|
315
|
+
region "us-east-1"
|
|
316
|
+
containers [{ name: "web", port: 8080 }]
|
|
317
|
+
end
|
|
318
|
+
MSG
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|