hecks 2.6.0 → 2.8.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 +13 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +2 -2
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +13 -14
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +188 -139
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +27 -3
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +15 -13
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +22 -13
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +8 -2
- metadata +72 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
|
@@ -148,7 +148,7 @@ module Hecks
|
|
|
148
148
|
# `stack_name` at all. They diverge on purpose for a domain whose
|
|
149
149
|
# declared identity (`Hecks.bluebook`, hence `world.domain`, hence
|
|
150
150
|
# `.world`'s own filename — `domain_name` above, needed just to find
|
|
151
|
-
# that file)
|
|
151
|
+
# that file) does not match its live AWS stack's own name: a
|
|
152
152
|
# CloudFormation stack cannot be renamed in place, and this domain's
|
|
153
153
|
# logical ids are already load-bearing on a real RDS instance other
|
|
154
154
|
# domains borrow from in Shared mode (see `owner_stack_name` below) —
|
|
@@ -1039,10 +1039,10 @@ module Hecks
|
|
|
1039
1039
|
# matching this stack's own name), a question `infra_name`
|
|
1040
1040
|
# answers and a domain's own business identity does not.
|
|
1041
1041
|
# Real, live, and confirmed the hard way: this was still
|
|
1042
|
-
# wired to `declared_domain_name` when
|
|
1042
|
+
# wired to `declared_domain_name` when a client app
|
|
1043
1043
|
# first deployed, and WebFunction immediately threw
|
|
1044
1044
|
# `ResourceNotFoundException: Function not found:
|
|
1045
|
-
# hecks
|
|
1045
|
+
# hecks-<domain>` on every read AND on the
|
|
1046
1046
|
# Google OAuth callback itself — a real signed-in user hit
|
|
1047
1047
|
# this within minutes of the deploy that introduced it.
|
|
1048
1048
|
DOMAIN_NAME: #{infra_name}
|
|
@@ -1246,7 +1246,7 @@ module Hecks
|
|
|
1246
1246
|
template_yaml = template_yaml.sub(
|
|
1247
1247
|
/^ - LambdaInvokePolicy:\n FunctionName: !Ref #{Regexp.escape(logical_id)}\n/, ""
|
|
1248
1248
|
)
|
|
1249
|
-
# Outputs — no `#{logical_id}Url` exists
|
|
1249
|
+
# Outputs — no `#{logical_id}Url` exists for `FunctionUrl` to
|
|
1250
1250
|
# `!GetAtt`; WebFunction's own URL becomes the stack's one and only
|
|
1251
1251
|
# "FunctionUrl" (never "WebFunctionUrl" — that name is reserved for
|
|
1252
1252
|
# the two-URL case, a domain that also has a #{logical_id} of its
|
|
@@ -1317,7 +1317,7 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
|
|
|
1317
1317
|
# aggregates with no snapshot table at all -- it only provisions the
|
|
1318
1318
|
# ONE bluebook it's called with (era_resolver.rb's own `bluebook.
|
|
1319
1319
|
# aggregates.each`), never the whole registry. Found live minting
|
|
1320
|
-
#
|
|
1320
|
+
# a client site's own era: `Payments::Payment.Initiate` refused with
|
|
1321
1321
|
# "relation \\"payment_head_snapshot_1\\" does not exist" the first
|
|
1322
1322
|
# time this domain's own checkout route ran for real. The automated
|
|
1323
1323
|
# (non-Shared) recipe below now does this correctly for every OTHER
|
|
@@ -1490,7 +1490,7 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
|
|
|
1490
1490
|
# `Hecks::Bluebook::Behaviour::World#for_binding` nor
|
|
1491
1491
|
# `PostgresEra.connect_for` ever reads `ENV["DATABASE_URL"]` or
|
|
1492
1492
|
# `ENV["HECKS_SCHEMA"]` — both scripts call
|
|
1493
|
-
# `registry.
|
|
1493
|
+
# `registry.binding_settings(...)`, a pure hash lookup
|
|
1494
1494
|
# against whatever literal `database "..."` string #{domain}'s own
|
|
1495
1495
|
# `.world` file declares. Without `db_env_blind:` below, this whole
|
|
1496
1496
|
# recipe would stand up a real bastion, punch a live 5432 ingress rule
|
|
@@ -1722,7 +1722,7 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
|
|
|
1722
1722
|
# `shared` are collected together here rather than spelled as an
|
|
1723
1723
|
# if/elsif/else, the same shape `Parameters:` above holds to: the
|
|
1724
1724
|
# two are independent facts about a domain, not alternatives, and
|
|
1725
|
-
#
|
|
1725
|
+
# a client site can be both — an elsif shape would silently drop the
|
|
1726
1726
|
# Owning* overrides whenever OAuth is also present. Caught live
|
|
1727
1727
|
# the first time a Shared-mode domain with real Google OAuth
|
|
1728
1728
|
# actually ran `make deploy`: `sam deploy` refused
|
|
@@ -2287,7 +2287,7 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
|
|
|
2287
2287
|
reindent = ->(text) { text.each_line.map { |line| line.strip.empty? ? line : " #{line}" }.join }
|
|
2288
2288
|
|
|
2289
2289
|
# Not pre-reindented (unlike the return value as a whole, below) — each
|
|
2290
|
-
# is spliced back into the still-being-dedented RESOURCES heredoc via
|
|
2290
|
+
# is spliced back into the still-being-dedented `RESOURCES` heredoc via
|
|
2291
2291
|
# `#{...}`, which the outer `reindent.call` already shifts by 2
|
|
2292
2292
|
# spaces once; reindenting here too would double it.
|
|
2293
2293
|
origin_access_control = use_oac ? <<~OAC : ""
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require_relative "yaml_text"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Projections
|
|
5
|
+
module Deploy
|
|
6
|
+
module Preview
|
|
7
|
+
# The secrets and IAM roles of a preview stack.
|
|
8
|
+
#
|
|
9
|
+
# The task role reads the shared database secret and this preview's own
|
|
10
|
+
# secrets and nothing owned by the main stack. The execution role also reads
|
|
11
|
+
# the database secret, only so the one-shot task can receive the credentials
|
|
12
|
+
# as container secrets.
|
|
13
|
+
module AccessYaml
|
|
14
|
+
extend YamlText
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Renders the secrets a preview generates: its session secret and the ones
|
|
19
|
+
# containers declare.
|
|
20
|
+
#
|
|
21
|
+
# @param settings [Settings] the resolved preview settings
|
|
22
|
+
# @return [Array<String>] one YAML resource block per secret
|
|
23
|
+
def secrets(settings)
|
|
24
|
+
[session_secret, *settings.containers.flat_map { |c| container_secrets(c) }]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Renders the per-branch session secret the host reads at start.
|
|
28
|
+
#
|
|
29
|
+
# @return [String] the secret's resource block
|
|
30
|
+
def session_secret
|
|
31
|
+
<<~YAML.chomp
|
|
32
|
+
# No explicit Name: Secrets Manager keeps a deleted secret through its recovery window,
|
|
33
|
+
# and a fixed name would make destroy followed by deploy on one branch fail.
|
|
34
|
+
SessionSecret:
|
|
35
|
+
Type: AWS::SecretsManager::Secret
|
|
36
|
+
Properties:
|
|
37
|
+
Description: !Sub "Session secret of the ${EnvName} preview."
|
|
38
|
+
GenerateSecretString:
|
|
39
|
+
SecretStringTemplate: '{}'
|
|
40
|
+
GenerateStringKey: session_secret
|
|
41
|
+
PasswordLength: 64
|
|
42
|
+
ExcludePunctuation: true
|
|
43
|
+
YAML
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Renders one generated secret per name a container declares.
|
|
47
|
+
#
|
|
48
|
+
# @param container [Containers::Entry] the container
|
|
49
|
+
# @return [Array<String>] one resource block per declared secret name
|
|
50
|
+
def container_secrets(container)
|
|
51
|
+
container.secrets.map do |name|
|
|
52
|
+
<<~YAML.chomp
|
|
53
|
+
#{secret_id(container, name)}:
|
|
54
|
+
Type: AWS::SecretsManager::Secret
|
|
55
|
+
Properties:
|
|
56
|
+
Description: !Sub "#{name} of the ${EnvName} preview's #{container.name} container."
|
|
57
|
+
GenerateSecretString:
|
|
58
|
+
SecretStringTemplate: '{}'
|
|
59
|
+
GenerateStringKey: secret
|
|
60
|
+
PasswordLength: 64
|
|
61
|
+
ExcludePunctuation: true
|
|
62
|
+
YAML
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Renders the task execution role and the task role.
|
|
67
|
+
#
|
|
68
|
+
# @param settings [Settings] the resolved preview settings
|
|
69
|
+
# @return [Array<String>] the two role resource blocks
|
|
70
|
+
def roles(settings)
|
|
71
|
+
assume = <<~YAML.chomp
|
|
72
|
+
AssumeRolePolicyDocument:
|
|
73
|
+
Version: '2012-10-17'
|
|
74
|
+
Statement:
|
|
75
|
+
- Effect: Allow
|
|
76
|
+
Principal: { Service: ecs-tasks.amazonaws.com }
|
|
77
|
+
Action: sts:AssumeRole
|
|
78
|
+
YAML
|
|
79
|
+
[execution_role(assume), task_role(assume, settings)]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Renders the execution role, which pulls images and hands the one-shot task
|
|
83
|
+
# its credentials.
|
|
84
|
+
#
|
|
85
|
+
# @param assume [String] the shared trust policy block
|
|
86
|
+
# @return [String] the role's resource block
|
|
87
|
+
def execution_role(assume)
|
|
88
|
+
<<~YAML.chomp
|
|
89
|
+
# The execution role reads the database secret only so the one-shot task can be given
|
|
90
|
+
# its credentials as container secrets.
|
|
91
|
+
ExecutionRole:
|
|
92
|
+
Type: AWS::IAM::Role
|
|
93
|
+
Properties:
|
|
94
|
+
#{indent(assume, 4)}
|
|
95
|
+
ManagedPolicyArns:
|
|
96
|
+
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
|
|
97
|
+
Policies:
|
|
98
|
+
- PolicyName: SharedDatabaseSecretRead
|
|
99
|
+
PolicyDocument:
|
|
100
|
+
Version: '2012-10-17'
|
|
101
|
+
Statement:
|
|
102
|
+
- Effect: Allow
|
|
103
|
+
Action: secretsmanager:GetSecretValue
|
|
104
|
+
Resource: !Ref OwningDatabaseSecretArn
|
|
105
|
+
YAML
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Renders the task role: read access to the shared database secret and this
|
|
109
|
+
# preview's own secrets.
|
|
110
|
+
#
|
|
111
|
+
# @param assume [String] the shared trust policy block
|
|
112
|
+
# @param settings [Settings] the resolved preview settings
|
|
113
|
+
# @return [String] the role's resource block
|
|
114
|
+
def task_role(assume, settings)
|
|
115
|
+
readable = ["SessionSecret", *settings.containers.flat_map { |c| c.secrets.map { |n| secret_id(c, n) } }]
|
|
116
|
+
<<~YAML.chomp
|
|
117
|
+
# Reads the shared database secret (it connects with it and creates its database) and
|
|
118
|
+
# this preview's own secrets; nothing owned by the main stack.
|
|
119
|
+
TaskRole:
|
|
120
|
+
Type: AWS::IAM::Role
|
|
121
|
+
Properties:
|
|
122
|
+
#{indent(assume, 4)}
|
|
123
|
+
Policies:
|
|
124
|
+
- PolicyName: SharedDatabaseSecretRead
|
|
125
|
+
PolicyDocument:
|
|
126
|
+
Version: '2012-10-17'
|
|
127
|
+
Statement:
|
|
128
|
+
- Effect: Allow
|
|
129
|
+
Action: secretsmanager:GetSecretValue
|
|
130
|
+
Resource: !Ref OwningDatabaseSecretArn
|
|
131
|
+
- PolicyName: PreviewSecretRead
|
|
132
|
+
PolicyDocument:
|
|
133
|
+
Version: '2012-10-17'
|
|
134
|
+
Statement:
|
|
135
|
+
- Effect: Allow
|
|
136
|
+
Action: secretsmanager:GetSecretValue
|
|
137
|
+
Resource:
|
|
138
|
+
#{indent(readable.map { |id| "- !Ref #{id}" }.join("\n"), 16)}
|
|
139
|
+
YAML
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Projections
|
|
3
|
+
module Deploy
|
|
4
|
+
module Preview
|
|
5
|
+
# The containers one preview task runs, in one normalized shape.
|
|
6
|
+
#
|
|
7
|
+
# A preview task runs the same containers as the stack it previews.
|
|
8
|
+
# This module is the single place that decides which containers those
|
|
9
|
+
# are, so the rest of `Preview` never asks where the list came from.
|
|
10
|
+
#
|
|
11
|
+
# ## One adapter to the shared setting
|
|
12
|
+
#
|
|
13
|
+
# The main Fargate stack is growing a multi-container setting.
|
|
14
|
+
# `shared_setting` is the only method that reads it; every other
|
|
15
|
+
# method here works on the normalized entries described below. When
|
|
16
|
+
# that setting lands with a different shape, translate it inside
|
|
17
|
+
# `shared_setting` and nothing else changes.
|
|
18
|
+
#
|
|
19
|
+
# ## Entry keys
|
|
20
|
+
#
|
|
21
|
+
# Each entry is a Hash with Symbol keys:
|
|
22
|
+
#
|
|
23
|
+
# - `name` (required): lowercase letters, digits and hyphens; also the
|
|
24
|
+
# container name and the suffix of its ECR repository.
|
|
25
|
+
# - `port` (required): the port the container listens on. Unique
|
|
26
|
+
# across the task, since all containers share one network namespace.
|
|
27
|
+
# - `health_check_path`: target group health check path, default `"/"`.
|
|
28
|
+
# - `paths`: load balancer path patterns forwarded to this container.
|
|
29
|
+
# - `default`: `true` sends every unmatched request here. When no
|
|
30
|
+
# entry says so, the `host` entry is the default, else the first.
|
|
31
|
+
# - `host`: `true` marks the Hecks host container. It gets the domain,
|
|
32
|
+
# session and database environment, and the signup path.
|
|
33
|
+
# - `database`: `true` gives a non-host container the database
|
|
34
|
+
# environment as well. The host always has it.
|
|
35
|
+
# - `environment`: extra environment, name to value. `{{preview_url}}`
|
|
36
|
+
# inside a value becomes the preview's public https URL.
|
|
37
|
+
# - `secrets`: environment variable names. Each becomes a per-branch
|
|
38
|
+
# generated secret whose ARN is that variable's value.
|
|
39
|
+
# - `image`: the local docker image `preview.sh` pushes for this
|
|
40
|
+
# container, default `"<name>:latest"`.
|
|
41
|
+
module Containers
|
|
42
|
+
# One normalized container of the preview task.
|
|
43
|
+
Entry = Data.define(:name, :port, :health_check_path, :paths, :default, :host, :database,
|
|
44
|
+
:environment, :secrets, :image) do
|
|
45
|
+
# Answers the CloudFormation logical-id fragment for this container.
|
|
46
|
+
#
|
|
47
|
+
# @return [String] `name` in CamelCase, such as `"MySite"` for `"my-site"`
|
|
48
|
+
def logical = name.split("-").map(&:capitalize).join
|
|
49
|
+
|
|
50
|
+
# Answers whether the container is reachable through the load balancer.
|
|
51
|
+
#
|
|
52
|
+
# @return [Boolean] true when it is the default or owns any path
|
|
53
|
+
def routed? = default || !paths.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
NAME_PATTERN = /\A[a-z][a-z0-9-]{0,29}\z/
|
|
57
|
+
PATH_PATTERN = %r{\A/[A-Za-z0-9._~/*?%-]*\z}
|
|
58
|
+
IMAGE_PATTERN = %r{\A[A-Za-z0-9._/:@-]+\z}
|
|
59
|
+
ENV_NAME_PATTERN = /\A[A-Z_][A-Z0-9_]*\z/
|
|
60
|
+
|
|
61
|
+
module_function
|
|
62
|
+
|
|
63
|
+
# Answers the preview task's containers, normalized and validated.
|
|
64
|
+
#
|
|
65
|
+
# @param preview [Hash{Symbol => Object}] the preview settings; `:containers` wins
|
|
66
|
+
# @param deploy_settings [Hash{Symbol => Object}] the `deployed_to` settings, read
|
|
67
|
+
# by `shared_setting`
|
|
68
|
+
# @param main [Hash{Symbol => Object}] the main stack's own facts; `:name`, `:port`,
|
|
69
|
+
# `:image` describe the single container used when neither list is given
|
|
70
|
+
# @param signup_path [String, nil] path forwarded to the host container, or nil
|
|
71
|
+
# @return [Array<Entry>] at least one entry, exactly one of them the default
|
|
72
|
+
# @raise [ArgumentError] if an entry is malformed, a name or port repeats, or two
|
|
73
|
+
# entries claim to be the default or the host
|
|
74
|
+
def resolve(preview:, deploy_settings:, main:, signup_path: nil)
|
|
75
|
+
raw = preview[:containers] || shared_setting(deploy_settings) || [single(main)]
|
|
76
|
+
entries = Array(raw).map { |item| normalize(item) }
|
|
77
|
+
check!(entries)
|
|
78
|
+
with_default(with_signup(entries, signup_path))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Reads the main stack's own container list, when it declares one.
|
|
82
|
+
#
|
|
83
|
+
# This is the one hook to the multi-container setting shared with the
|
|
84
|
+
# main template; it answers entries in the shape documented above.
|
|
85
|
+
#
|
|
86
|
+
# @param deploy_settings [Hash{Symbol => Object}] the `deployed_to` settings
|
|
87
|
+
# @return [Array<Hash>, nil] the list, or nil when the stack runs one container
|
|
88
|
+
def shared_setting(deploy_settings)
|
|
89
|
+
deploy_settings[:containers]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Builds the single-container entry a one-container stack previews as.
|
|
93
|
+
#
|
|
94
|
+
# @param main [Hash{Symbol => Object}] the main stack's facts, see `resolve`
|
|
95
|
+
# @return [Hash{Symbol => Object}] the entry, always the host and the default
|
|
96
|
+
def single(main)
|
|
97
|
+
name = main.fetch(:name).to_s.downcase.gsub(/[^a-z0-9]+/, "-").sub(/\A[^a-z]+/, "")[0, 30].sub(/-+\z/, "")
|
|
98
|
+
{ name: name, port: main.fetch(:port), host: true, default: true, image: main[:image] }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Turns one raw entry into an `Entry`, filling every default.
|
|
102
|
+
#
|
|
103
|
+
# @param item [Hash{Symbol => Object}] the raw entry, string keys accepted
|
|
104
|
+
# @return [Entry] the normalized entry
|
|
105
|
+
# @raise [ArgumentError] if the entry is not a Hash or lacks a valid `name` or `port`
|
|
106
|
+
def normalize(item)
|
|
107
|
+
raise ArgumentError, "preview container #{item.inspect} must be a Hash" unless item.is_a?(Hash)
|
|
108
|
+
|
|
109
|
+
item = item.transform_keys(&:to_sym)
|
|
110
|
+
name = matching(item[:name], NAME_PATTERN, "preview container name")
|
|
111
|
+
Entry.new(
|
|
112
|
+
name: name, port: port_of(item, name), health_check_path: path_of(item[:health_check_path] || "/"),
|
|
113
|
+
paths: Array(item[:paths]).map { |path| path_of(path) }, default: item[:default] == true,
|
|
114
|
+
host: item[:host] == true, database: item[:database] == true,
|
|
115
|
+
environment: environment_of(item[:environment]), secrets: Array(item[:secrets]).map(&:to_s),
|
|
116
|
+
image: matching(item[:image] || "#{name}:latest", IMAGE_PATTERN, "preview container #{name} image")
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Answers a value as a String, refusing one that does not match a pattern.
|
|
121
|
+
#
|
|
122
|
+
# @param value [Object] the raw value
|
|
123
|
+
# @param pattern [Regexp] what the string must match
|
|
124
|
+
# @param label [String] what the value is, for the message
|
|
125
|
+
# @return [String] the value as a String
|
|
126
|
+
# @raise [ArgumentError] if the string does not match
|
|
127
|
+
def matching(value, pattern, label)
|
|
128
|
+
text = value.to_s
|
|
129
|
+
raise ArgumentError, "#{label} #{text.inspect} must match #{pattern.inspect}" unless text.match?(pattern)
|
|
130
|
+
|
|
131
|
+
text
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Answers one path pattern, refusing one the template could not quote safely.
|
|
135
|
+
#
|
|
136
|
+
# @param path [Object] the raw path
|
|
137
|
+
# @return [String] the path
|
|
138
|
+
# @raise [ArgumentError] if it does not start with `/` or holds an unexpected character
|
|
139
|
+
def path_of(path) = matching(path, PATH_PATTERN, "preview container path")
|
|
140
|
+
|
|
141
|
+
# Reads and range-checks one entry's port.
|
|
142
|
+
#
|
|
143
|
+
# @param item [Hash{Symbol => Object}] the raw entry
|
|
144
|
+
# @param name [String] the entry's name, for the message
|
|
145
|
+
# @return [Integer] the port
|
|
146
|
+
# @raise [ArgumentError] if the port is missing or outside 1..65535
|
|
147
|
+
def port_of(item, name)
|
|
148
|
+
port = Integer(item[:port], exception: false)
|
|
149
|
+
raise ArgumentError, "preview container #{name} needs a port between 1 and 65535" unless port&.between?(1, 65_535)
|
|
150
|
+
|
|
151
|
+
port
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Stringifies an entry's environment, keeping declaration order.
|
|
155
|
+
#
|
|
156
|
+
# @param environment [Hash{Symbol, String => Object}, nil] the raw environment
|
|
157
|
+
# @return [Hash{String => String}] name to value
|
|
158
|
+
def environment_of(environment)
|
|
159
|
+
(environment || {}).to_h { |name, value| [name.to_s, value.to_s] }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Refuses a list that cannot be one task.
|
|
163
|
+
#
|
|
164
|
+
# @param entries [Array<Entry>] the normalized entries
|
|
165
|
+
# @return [void]
|
|
166
|
+
# @raise [ArgumentError] on an empty list, a repeated name or port, more than one
|
|
167
|
+
# default or host, or a secret name that is not an environment variable name
|
|
168
|
+
def check!(entries)
|
|
169
|
+
raise ArgumentError, "preview containers must not be empty" if entries.empty?
|
|
170
|
+
|
|
171
|
+
%i[name port].each do |field|
|
|
172
|
+
dup = entries.map(&field).tally.find { |_value, count| count > 1 }
|
|
173
|
+
raise ArgumentError, "preview containers repeat the #{field} #{dup.first.inspect}" if dup
|
|
174
|
+
end
|
|
175
|
+
check_roles!(entries)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Refuses a second default or host, and a secret that is not an environment variable name.
|
|
179
|
+
#
|
|
180
|
+
# @param entries [Array<Entry>] the normalized entries
|
|
181
|
+
# @return [void]
|
|
182
|
+
# @raise [ArgumentError] on a second default or host, or a malformed secret name
|
|
183
|
+
def check_roles!(entries)
|
|
184
|
+
raise ArgumentError, "at most one preview container may be the default" if entries.count(&:default) > 1
|
|
185
|
+
raise ArgumentError, "at most one preview container may be the host" if entries.count(&:host) > 1
|
|
186
|
+
|
|
187
|
+
bad = entries.flat_map(&:secrets).find { |secret| !secret.match?(ENV_NAME_PATTERN) }
|
|
188
|
+
raise ArgumentError, "preview secret name #{bad.inspect} must be an environment variable name" if bad
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Adds the signup path to the host container's own paths.
|
|
192
|
+
#
|
|
193
|
+
# @param entries [Array<Entry>] the normalized entries
|
|
194
|
+
# @param signup_path [String, nil] the path, or nil for none
|
|
195
|
+
# @return [Array<Entry>] the entries, the host's paths extended when it needs a rule
|
|
196
|
+
def with_signup(entries, signup_path)
|
|
197
|
+
return entries unless signup_path
|
|
198
|
+
|
|
199
|
+
entries.map do |entry|
|
|
200
|
+
entry.host && !entry.default ? entry.with(paths: (entry.paths + [signup_path]).uniq) : entry
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Marks the default container when no entry does.
|
|
205
|
+
#
|
|
206
|
+
# @param entries [Array<Entry>] the normalized entries
|
|
207
|
+
# @return [Array<Entry>] the entries with exactly one default
|
|
208
|
+
def with_default(entries)
|
|
209
|
+
return entries if entries.any?(&:default)
|
|
210
|
+
|
|
211
|
+
chosen = entries.find(&:host) || entries.first
|
|
212
|
+
entries.map { |entry| entry.equal?(chosen) ? entry.with(default: true) : entry }
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|