hecks 1.4.0 → 1.5.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/lib/hecks/adapters/driven/claude_code.rb +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/deploy/fargate.rb +666 -0
- data/lib/hecks/projections/deploy/lambda.rb +2423 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projections.rb +3 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +47 -10
- data/lib/hecks/projector.rb +101 -19
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +14 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
- data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Projections
|
|
3
|
+
module Deploy
|
|
4
|
+
# Plumbing `Lambda` and `Fargate` both need — VPC/subnet/security-group
|
|
5
|
+
# resources, RDS/Aurora, `bastion.yaml`, and a cross-domain policy's
|
|
6
|
+
# own least-privilege invoke grant are the same problem (get a
|
|
7
|
+
# domain's own Postgres instance stood up and reachable for the one
|
|
8
|
+
# boot that mints era 1, and let a domain call another domain's
|
|
9
|
+
# Lambda) whichever compute target ships the domain's own code.
|
|
10
|
+
#
|
|
11
|
+
# Plain module functions, not a registered `Projector::Target` — this
|
|
12
|
+
# has no `deployed_to(...)` block of its own to generate from, only
|
|
13
|
+
# helpers the two real targets call with the facts they have already
|
|
14
|
+
# resolved (`db_id`, `aurora`, `google_oauth_present`, and so on).
|
|
15
|
+
#
|
|
16
|
+
# Every `*_table` method here answers the same `key`/`var`/`name`
|
|
17
|
+
# strings regardless of which target built them — `stack_outputs`'
|
|
18
|
+
# own `"FunctionSecurityGroupId"` key, in particular, is what a
|
|
19
|
+
# Shared-mode domain looks up from its owner's stack (`bin/project_deploy`'s
|
|
20
|
+
# own Shared-mode branch), and that lookup has to succeed the same
|
|
21
|
+
# way whether the owner is a `Lambda` or a `Fargate` deploy.
|
|
22
|
+
module Shared
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# The stack↔bastion contract — one shared table each of
|
|
26
|
+
# `template.yaml`'s Outputs, `bastion.yaml`'s Parameters, and a
|
|
27
|
+
# Makefile's own eval/`--parameter-overrides` lines reads from,
|
|
28
|
+
# instead of three to four independent string literals per fact
|
|
29
|
+
# with nothing checking they agree. `var` is the Make variable
|
|
30
|
+
# each output becomes.
|
|
31
|
+
#
|
|
32
|
+
# Empty when `shared` — a Shared-mode domain provisions no
|
|
33
|
+
# VPC/RDS of its own to expose at all (`bastion_parameters`,
|
|
34
|
+
# `bastion_yaml`, and era-minting are all skipped for the same
|
|
35
|
+
# reason: there's nothing here for a bastion to reach). A
|
|
36
|
+
# Shared-mode domain's own stack is never an owner, so nothing
|
|
37
|
+
# downstream ever needs to look these up from it either.
|
|
38
|
+
#
|
|
39
|
+
# @param shared [Boolean] whether this domain borrows another domain's
|
|
40
|
+
# RDS instance (`database "Shared"`) rather than provisioning its own
|
|
41
|
+
# @param db_id [String] the RDS/VPC resources' own shared logical-id prefix
|
|
42
|
+
# @param db_ref_id [String] the logical id `.Endpoint`/`.MasterUserSecret` resolve
|
|
43
|
+
# against — `db_id` itself for plain RDS, `"#{db_id}Cluster"` for Aurora
|
|
44
|
+
# @param secret_intrinsic [String] the rendered `!Ref`/`!GetAtt` expression for
|
|
45
|
+
# this domain's own database secret
|
|
46
|
+
# @param compute_security_group_ref [String] the `!Ref` expression for the
|
|
47
|
+
# compute resource's own security group (a Lambda function's, an `ECS`
|
|
48
|
+
# service's) — reused by a Shared-mode domain elsewhere to attach its own
|
|
49
|
+
# compute to this domain's VPC without minting a security group of its own
|
|
50
|
+
# @param google_oauth_present [Boolean] whether this domain's own NAT
|
|
51
|
+
# Gateway/public-subnet resources exist, and so whether `PublicSubnetId`/
|
|
52
|
+
# `BastionSubnetId` are real outputs to expose
|
|
53
|
+
# @return [Array<Hash{Symbol => String}>] frozen; each entry a `{key:, var:,
|
|
54
|
+
# ref:}` triple
|
|
55
|
+
def stack_outputs(shared:, db_id:, db_ref_id:, secret_intrinsic:, compute_security_group_ref:, google_oauth_present:)
|
|
56
|
+
return [].freeze if shared
|
|
57
|
+
|
|
58
|
+
[
|
|
59
|
+
{ key: "VpcId", var: "VPC_ID", ref: "!Ref #{db_id}Vpc" },
|
|
60
|
+
{ key: "DbSecurityGroupId", var: "DB_SG_ID", ref: "!Ref #{db_id}SecurityGroup" },
|
|
61
|
+
{ key: "DatabaseEndpoint", var: "DB_HOST", ref: "!GetAtt #{db_ref_id}.Endpoint.Address" },
|
|
62
|
+
{ key: "DatabaseSecretArn", var: "DB_SECRET_ARN", ref: secret_intrinsic },
|
|
63
|
+
# Not consumed by anything in this domain's own generated output —
|
|
64
|
+
# a "Shared"-mode domain elsewhere looks these two up live to
|
|
65
|
+
# attach its own compute to this domain's VPC/security group
|
|
66
|
+
# without minting either itself.
|
|
67
|
+
{ key: "FunctionSecurityGroupId", var: "FN_SG_ID", ref: compute_security_group_ref },
|
|
68
|
+
{ key: "PrivateSubnetAId", var: "PRIVATE_SUBNET_A_ID", ref: "!Ref #{db_id}SubnetA" },
|
|
69
|
+
{ key: "PrivateSubnetBId", var: "PRIVATE_SUBNET_B_ID", ref: "!Ref #{db_id}SubnetB" },
|
|
70
|
+
# Only when google_oauth_present — #{db_id}PublicSubnet (and the
|
|
71
|
+
# Internet Gateway it's attached through) only exist as resources
|
|
72
|
+
# at all when the NAT Gateway block does. `bastion.yaml` needs
|
|
73
|
+
# this: a VPC accepts only one attached Internet Gateway, and one
|
|
74
|
+
# already exists once this is true — its own temporary IGW+subnet
|
|
75
|
+
# would collide (a real, live "Resource.AlreadyAssociated").
|
|
76
|
+
*(google_oauth_present ? [{ key: "PublicSubnetId", var: "PUBLIC_SUBNET_ID", ref: "!Ref #{db_id}PublicSubnet" }] : []),
|
|
77
|
+
# A separate subnet from PublicSubnetId, same shared route
|
|
78
|
+
# table/Internet Gateway -- #{db_id}PublicSubnet always lands in
|
|
79
|
+
# AZ index 0 (`!Select [0, !GetAZs '']`), and a real, live
|
|
80
|
+
# CREATE_FAILED caught this account unable to launch any EC2
|
|
81
|
+
# instance there at all ("Your requested instance type ... is
|
|
82
|
+
# not supported in your requested Availability Zone
|
|
83
|
+
# (us-east-1a)") -- NatGateway and RDS aren't EC2 instances and
|
|
84
|
+
# never hit this, only BastionInstance does. AZ index 1 instead,
|
|
85
|
+
# matching #{db_id}SubnetB's own choice.
|
|
86
|
+
*(google_oauth_present ? [{ key: "BastionSubnetId", var: "BASTION_SUBNET_ID", ref: "!Ref #{db_id}BastionPublicSubnet" }] : []),
|
|
87
|
+
].freeze
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# `bastion.yaml`'s own Parameters — deliberately allowed to rename
|
|
91
|
+
# (`RdsSecurityGroupId` reads better inside `bastion.yaml` than the
|
|
92
|
+
# stack output's own `DbSecurityGroupId`), which is exactly the kind
|
|
93
|
+
# of rename that can silently drift between the two files without
|
|
94
|
+
# this shared table.
|
|
95
|
+
#
|
|
96
|
+
# @param shared [Boolean] whether this domain borrows another domain's
|
|
97
|
+
# RDS instance — see `stack_outputs`' own comment
|
|
98
|
+
# @param google_oauth_present [Boolean] see `stack_outputs`
|
|
99
|
+
# @return [Array<Hash{Symbol => String}>] frozen; each entry a `{name:,
|
|
100
|
+
# from_output:, type:}` triple
|
|
101
|
+
def bastion_parameters(shared:, google_oauth_present:)
|
|
102
|
+
return [].freeze if shared
|
|
103
|
+
|
|
104
|
+
[
|
|
105
|
+
{ name: "VpcId", from_output: "VpcId", type: "AWS::EC2::VPC::Id" },
|
|
106
|
+
{ name: "RdsSecurityGroupId", from_output: "DbSecurityGroupId", type: "AWS::EC2::SecurityGroup::Id" },
|
|
107
|
+
*(google_oauth_present ? [{ name: "PublicSubnetId", from_output: "PublicSubnetId", type: "AWS::EC2::Subnet::Id" }] : []),
|
|
108
|
+
*(google_oauth_present ? [{ name: "BastionSubnetId", from_output: "BastionSubnetId", type: "AWS::EC2::Subnet::Id" }] : []),
|
|
109
|
+
].freeze
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# A generation-time assertion, not a bluebook refusal — this is the
|
|
113
|
+
# generator catching its own bug immediately, before writing a
|
|
114
|
+
# single file, instead of producing a `bastion.yaml` whose Parameter
|
|
115
|
+
# nothing could ever fill.
|
|
116
|
+
#
|
|
117
|
+
# @param bastion_parameters [Array<Hash{Symbol => String}>] as `bastion_parameters` builds
|
|
118
|
+
# @param stack_outputs [Array<Hash{Symbol => String}>] as `stack_outputs` builds
|
|
119
|
+
# @return [void]
|
|
120
|
+
# @raise [RuntimeError] if any parameter names a `from_output` absent from
|
|
121
|
+
# `stack_outputs`
|
|
122
|
+
def check_bastion_parameters!(bastion_parameters, stack_outputs)
|
|
123
|
+
bastion_parameters.each do |param|
|
|
124
|
+
stack_outputs.any? { |o| o[:key] == param[:from_output] } or
|
|
125
|
+
raise "bastion parameter #{param[:name]} references undeclared stack output #{param[:from_output]}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Renders the least-privilege IAM grant a cross-domain policy's own
|
|
130
|
+
# invoke needs — one `lambda:InvokeFunction` statement per declared
|
|
131
|
+
# `across:` target, appended to the compute role's existing
|
|
132
|
+
# `Policies:` list. `rust/host`'s own `lambda_client.rs`
|
|
133
|
+
# (`AwsLambdaInvoker`) is the one piece of a cross-domain dispatch
|
|
134
|
+
# path that needs this at all, regardless of which compute target
|
|
135
|
+
# ships the calling domain's own code — a Fargate task and a Lambda
|
|
136
|
+
# function both reach another domain's dispatch Lambda the same way.
|
|
137
|
+
#
|
|
138
|
+
# Declared, not deployed: an ARN naming a function that does not
|
|
139
|
+
# exist yet is still valid IAM policy — the call simply fails with
|
|
140
|
+
# `ResourceNotFoundException` instead of `AccessDeniedException`
|
|
141
|
+
# until the target domain is deployed for real.
|
|
142
|
+
#
|
|
143
|
+
# @param targets [Array<String>] the domain names this stack's `across:` targets
|
|
144
|
+
# declare
|
|
145
|
+
# @param base [String] the marker line's own rendered indentation whitespace;
|
|
146
|
+
# everything below is built relative to it
|
|
147
|
+
# @return [String] the rendered comment plus one `Statement`-shaped IAM policy
|
|
148
|
+
# entry granting invoke on each target's function ARN, ending in exactly one
|
|
149
|
+
# trailing newline; an empty string if `targets` is empty
|
|
150
|
+
def cross_domain_invoke_policy_yaml(targets, base)
|
|
151
|
+
return "" if targets.empty?
|
|
152
|
+
|
|
153
|
+
comment = [
|
|
154
|
+
"least-privilege, one ARN per declared `across:` target -- the same",
|
|
155
|
+
"shape this compute's own DB-secret grant already takes, extended to",
|
|
156
|
+
"a target this stack does not own (so no `!Ref` to reach for -- the",
|
|
157
|
+
"target's own function name is `lambda_client.rs`'s own computed",
|
|
158
|
+
'"hecks-#{domain}" convention, read directly, matching every other',
|
|
159
|
+
"function-name computation in this whole project). Targets: #{targets.join(', ')}.",
|
|
160
|
+
].map { |line| "#{base}# #{line}" }.join("\n")
|
|
161
|
+
|
|
162
|
+
resources = targets.map { |target|
|
|
163
|
+
"#{base} - !Sub \"arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:hecks-#{target.downcase}\""
|
|
164
|
+
}.join("\n")
|
|
165
|
+
|
|
166
|
+
[
|
|
167
|
+
comment,
|
|
168
|
+
"#{base}- Statement:",
|
|
169
|
+
"#{base} - Effect: Allow",
|
|
170
|
+
"#{base} Action: lambda:InvokeFunction",
|
|
171
|
+
"#{base} Resource:",
|
|
172
|
+
resources,
|
|
173
|
+
].join("\n") + "\n"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Renders one domain's own private VPC, subnets, RDS/Aurora Postgres
|
|
177
|
+
# instance, and the two security groups pairing it with its compute
|
|
178
|
+
# — everything `Lambda` and `Fargate` both need to give a
|
|
179
|
+
# non-`"Shared"`-mode domain a real, reachable Postgres endpoint.
|
|
180
|
+
# Dedented flush-left, the same shape a caller's own enclosing
|
|
181
|
+
# `<<~` heredoc expects to reindent as it splices this in.
|
|
182
|
+
#
|
|
183
|
+
# `compute_logical_id`'s own security group carries no inbound rule
|
|
184
|
+
# at all — neither a Lambda function nor a Fargate task/`ALB` target
|
|
185
|
+
# group receives traffic over this VPC-attached `ENI` directly, only
|
|
186
|
+
# egress to the database (and, when `google_oauth_present`, to the
|
|
187
|
+
# internet for a real OAuth token exchange).
|
|
188
|
+
#
|
|
189
|
+
# @param db_id [String] the RDS/VPC resources' own shared logical-id prefix
|
|
190
|
+
# @param db_name [String] the database identifier RDS/Aurora provisions
|
|
191
|
+
# @param infra_name [String] the domain's own AWS-facing name, quoted in the
|
|
192
|
+
# `DBSubnetGroupDescription`
|
|
193
|
+
# @param aurora [Boolean] Aurora Serverless v2 when true, plain RDS otherwise
|
|
194
|
+
# @param google_oauth_present [Boolean] whether a NAT Gateway, public subnet, and
|
|
195
|
+
# the compute security group's own internet egress rule are needed
|
|
196
|
+
# @param compute_logical_id [String] the compute resource's own logical id,
|
|
197
|
+
# naming its security group and egress rules
|
|
198
|
+
# @param compute_description [String] the compute security group's own
|
|
199
|
+
# `GroupDescription` text — what actually terminates the `ENI` this ingress
|
|
200
|
+
# pairs with
|
|
201
|
+
# @return [String] the rendered CloudFormation Resources, flush-left
|
|
202
|
+
def vpc_and_database_yaml(db_id:, db_name:, infra_name:, aurora:, google_oauth_present:, compute_logical_id:, compute_description:)
|
|
203
|
+
<<~OWNDB.rstrip
|
|
204
|
+
#{db_id}Vpc:
|
|
205
|
+
Type: AWS::EC2::VPC
|
|
206
|
+
Properties:
|
|
207
|
+
CidrBlock: 10.0.0.0/16
|
|
208
|
+
EnableDnsSupport: true
|
|
209
|
+
EnableDnsHostnames: true
|
|
210
|
+
|
|
211
|
+
#{db_id}SubnetA:
|
|
212
|
+
Type: AWS::EC2::Subnet
|
|
213
|
+
Properties:
|
|
214
|
+
VpcId: !Ref #{db_id}Vpc
|
|
215
|
+
CidrBlock: 10.0.1.0/24
|
|
216
|
+
AvailabilityZone: !Select [0, !GetAZs '']
|
|
217
|
+
|
|
218
|
+
#{db_id}SubnetB:
|
|
219
|
+
Type: AWS::EC2::Subnet
|
|
220
|
+
Properties:
|
|
221
|
+
VpcId: !Ref #{db_id}Vpc
|
|
222
|
+
CidrBlock: 10.0.2.0/24
|
|
223
|
+
AvailabilityZone: !Select [1, !GetAZs '']
|
|
224
|
+
|
|
225
|
+
#{google_oauth_present ? <<~NATGW.rstrip : ""}
|
|
226
|
+
# Real Google OAuth token exchange needs real internet access --
|
|
227
|
+
# see the enclosing template's own "NO NAT GATEWAY" comment for
|
|
228
|
+
# why this is opt-in. One NAT Gateway, one public subnet, wired
|
|
229
|
+
# only when google_oauth_present -- nothing else in this stack
|
|
230
|
+
# pays for it.
|
|
231
|
+
#{db_id}PublicSubnet:
|
|
232
|
+
Type: AWS::EC2::Subnet
|
|
233
|
+
Properties:
|
|
234
|
+
VpcId: !Ref #{db_id}Vpc
|
|
235
|
+
CidrBlock: 10.0.3.0/24
|
|
236
|
+
AvailabilityZone: !Select [0, !GetAZs '']
|
|
237
|
+
MapPublicIpOnLaunch: true
|
|
238
|
+
|
|
239
|
+
#{db_id}InternetGateway:
|
|
240
|
+
Type: AWS::EC2::InternetGateway
|
|
241
|
+
|
|
242
|
+
#{db_id}InternetGatewayAttachment:
|
|
243
|
+
Type: AWS::EC2::VPCGatewayAttachment
|
|
244
|
+
Properties:
|
|
245
|
+
VpcId: !Ref #{db_id}Vpc
|
|
246
|
+
InternetGatewayId: !Ref #{db_id}InternetGateway
|
|
247
|
+
|
|
248
|
+
#{db_id}PublicRouteTable:
|
|
249
|
+
Type: AWS::EC2::RouteTable
|
|
250
|
+
Properties:
|
|
251
|
+
VpcId: !Ref #{db_id}Vpc
|
|
252
|
+
|
|
253
|
+
#{db_id}PublicRoute:
|
|
254
|
+
Type: AWS::EC2::Route
|
|
255
|
+
DependsOn: #{db_id}InternetGatewayAttachment
|
|
256
|
+
Properties:
|
|
257
|
+
RouteTableId: !Ref #{db_id}PublicRouteTable
|
|
258
|
+
DestinationCidrBlock: 0.0.0.0/0
|
|
259
|
+
GatewayId: !Ref #{db_id}InternetGateway
|
|
260
|
+
|
|
261
|
+
#{db_id}PublicSubnetRouteTableAssociation:
|
|
262
|
+
Type: AWS::EC2::SubnetRouteTableAssociation
|
|
263
|
+
Properties:
|
|
264
|
+
SubnetId: !Ref #{db_id}PublicSubnet
|
|
265
|
+
RouteTableId: !Ref #{db_id}PublicRouteTable
|
|
266
|
+
|
|
267
|
+
# A second public subnet, AZ index 1 -- not where NatGateway
|
|
268
|
+
# lives (that's still #{db_id}PublicSubnet, AZ index 0). Only
|
|
269
|
+
# for bastion.yaml's own BastionInstance, a real EC2 instance
|
|
270
|
+
# that a real, live CREATE_FAILED proved this account can't
|
|
271
|
+
# launch in AZ index 0 at all. Same shared
|
|
272
|
+
# #{db_id}PublicRouteTable/#{db_id}InternetGateway as
|
|
273
|
+
# #{db_id}PublicSubnet, not a second IGW -- a VPC accepts only one.
|
|
274
|
+
#{db_id}BastionPublicSubnet:
|
|
275
|
+
Type: AWS::EC2::Subnet
|
|
276
|
+
Properties:
|
|
277
|
+
VpcId: !Ref #{db_id}Vpc
|
|
278
|
+
CidrBlock: 10.0.4.0/24
|
|
279
|
+
AvailabilityZone: !Select [1, !GetAZs '']
|
|
280
|
+
MapPublicIpOnLaunch: true
|
|
281
|
+
|
|
282
|
+
#{db_id}BastionPublicSubnetRouteTableAssociation:
|
|
283
|
+
Type: AWS::EC2::SubnetRouteTableAssociation
|
|
284
|
+
Properties:
|
|
285
|
+
SubnetId: !Ref #{db_id}BastionPublicSubnet
|
|
286
|
+
RouteTableId: !Ref #{db_id}PublicRouteTable
|
|
287
|
+
|
|
288
|
+
#{db_id}NatEip:
|
|
289
|
+
Type: AWS::EC2::EIP
|
|
290
|
+
Properties:
|
|
291
|
+
Domain: vpc
|
|
292
|
+
|
|
293
|
+
#{db_id}NatGateway:
|
|
294
|
+
Type: AWS::EC2::NatGateway
|
|
295
|
+
Properties:
|
|
296
|
+
SubnetId: !Ref #{db_id}PublicSubnet
|
|
297
|
+
AllocationId: !GetAtt #{db_id}NatEip.AllocationId
|
|
298
|
+
|
|
299
|
+
# #{db_id}SubnetA/B (private -- the compute and RDS live here)
|
|
300
|
+
# have no route to the internet at all without this: routes
|
|
301
|
+
# outbound through #{db_id}NatGateway instead of an Internet
|
|
302
|
+
# Gateway directly -- the whole reason a NAT (not just an IGW)
|
|
303
|
+
# exists: outbound-only, no inbound path back into these subnets.
|
|
304
|
+
#{db_id}PrivateRouteTable:
|
|
305
|
+
Type: AWS::EC2::RouteTable
|
|
306
|
+
Properties:
|
|
307
|
+
VpcId: !Ref #{db_id}Vpc
|
|
308
|
+
|
|
309
|
+
#{db_id}PrivateRoute:
|
|
310
|
+
Type: AWS::EC2::Route
|
|
311
|
+
Properties:
|
|
312
|
+
RouteTableId: !Ref #{db_id}PrivateRouteTable
|
|
313
|
+
DestinationCidrBlock: 0.0.0.0/0
|
|
314
|
+
NatGatewayId: !Ref #{db_id}NatGateway
|
|
315
|
+
|
|
316
|
+
#{db_id}SubnetARouteTableAssociation:
|
|
317
|
+
Type: AWS::EC2::SubnetRouteTableAssociation
|
|
318
|
+
Properties:
|
|
319
|
+
SubnetId: !Ref #{db_id}SubnetA
|
|
320
|
+
RouteTableId: !Ref #{db_id}PrivateRouteTable
|
|
321
|
+
|
|
322
|
+
#{db_id}SubnetBRouteTableAssociation:
|
|
323
|
+
Type: AWS::EC2::SubnetRouteTableAssociation
|
|
324
|
+
Properties:
|
|
325
|
+
SubnetId: !Ref #{db_id}SubnetB
|
|
326
|
+
RouteTableId: !Ref #{db_id}PrivateRouteTable
|
|
327
|
+
|
|
328
|
+
NATGW
|
|
329
|
+
#{db_id}SubnetGroup:
|
|
330
|
+
Type: AWS::RDS::DBSubnetGroup
|
|
331
|
+
Properties:
|
|
332
|
+
DBSubnetGroupDescription: !Sub "${AWS::StackName} - private subnets for #{infra_name}'s own RDS instance"
|
|
333
|
+
SubnetIds: [!Ref #{db_id}SubnetA, !Ref #{db_id}SubnetB]
|
|
334
|
+
|
|
335
|
+
# No inline SecurityGroupEgress/Ingress on either group -- each
|
|
336
|
+
# would need to reference the other's id from inside its own
|
|
337
|
+
# Properties, a genuine circular create-order dependency
|
|
338
|
+
# CloudFormation refuses outright (cfn-lint E3004). Standard
|
|
339
|
+
# fix: create both groups empty, then wire the cross-reference
|
|
340
|
+
# through two standalone rule resources below, each of which
|
|
341
|
+
# can legitimately depend on both groups already existing.
|
|
342
|
+
#{compute_logical_id}SecurityGroup:
|
|
343
|
+
Type: AWS::EC2::SecurityGroup
|
|
344
|
+
Properties:
|
|
345
|
+
VpcId: !Ref #{db_id}Vpc
|
|
346
|
+
GroupDescription: #{compute_description}
|
|
347
|
+
|
|
348
|
+
#{db_id}SecurityGroup:
|
|
349
|
+
Type: AWS::EC2::SecurityGroup
|
|
350
|
+
Properties:
|
|
351
|
+
VpcId: !Ref #{db_id}Vpc
|
|
352
|
+
GroupDescription: #{db_id} - ingress rule attached separately below, from #{compute_logical_id} only
|
|
353
|
+
|
|
354
|
+
#{compute_logical_id}EgressToDb:
|
|
355
|
+
Type: AWS::EC2::SecurityGroupEgress
|
|
356
|
+
Properties:
|
|
357
|
+
GroupId: !Ref #{compute_logical_id}SecurityGroup
|
|
358
|
+
IpProtocol: tcp
|
|
359
|
+
FromPort: 5432
|
|
360
|
+
ToPort: 5432
|
|
361
|
+
DestinationSecurityGroupId: !Ref #{db_id}SecurityGroup
|
|
362
|
+
|
|
363
|
+
#{google_oauth_present ? <<~OAUTHEGRESS.rstrip : ""}
|
|
364
|
+
# A standalone AWS::EC2::SecurityGroupEgress resource for a
|
|
365
|
+
# group replaces that group's own default allow-all-outbound
|
|
366
|
+
# rule with only what's explicitly declared here --
|
|
367
|
+
# #{compute_logical_id}EgressToDb's own existence already means
|
|
368
|
+
# #{compute_logical_id}SecurityGroup has no other egress at
|
|
369
|
+
# all. 443 to 0.0.0.0/0, not a narrower destination -- Google
|
|
370
|
+
# publishes no fixed IP range for oauth2.googleapis.com an AWS
|
|
371
|
+
# security group could reference instead.
|
|
372
|
+
#{compute_logical_id}EgressToInternet:
|
|
373
|
+
Type: AWS::EC2::SecurityGroupEgress
|
|
374
|
+
Properties:
|
|
375
|
+
GroupId: !Ref #{compute_logical_id}SecurityGroup
|
|
376
|
+
IpProtocol: tcp
|
|
377
|
+
FromPort: 443
|
|
378
|
+
ToPort: 443
|
|
379
|
+
CidrIp: 0.0.0.0/0
|
|
380
|
+
|
|
381
|
+
OAUTHEGRESS
|
|
382
|
+
#{db_id}IngressFromFunction:
|
|
383
|
+
Type: AWS::EC2::SecurityGroupIngress
|
|
384
|
+
Properties:
|
|
385
|
+
GroupId: !Ref #{db_id}SecurityGroup
|
|
386
|
+
IpProtocol: tcp
|
|
387
|
+
FromPort: 5432
|
|
388
|
+
ToPort: 5432
|
|
389
|
+
SourceSecurityGroupId: !Ref #{compute_logical_id}SecurityGroup
|
|
390
|
+
|
|
391
|
+
#{if aurora
|
|
392
|
+
<<~AURORADB.rstrip
|
|
393
|
+
# Aurora Serverless v2, Postgres-compatible -- the split
|
|
394
|
+
# shape Aurora needs (a DBCluster carrying the managed
|
|
395
|
+
# password/endpoint/subnet-group/security-group, plus at
|
|
396
|
+
# least one DBInstance inside it) is the only thing
|
|
397
|
+
# different from the plain-RDS path below;
|
|
398
|
+
# Adapters::PostgresEra/tokio_postgres speak the identical
|
|
399
|
+
# wire protocol against either endpoint.
|
|
400
|
+
# Self-managed, not ManageMasterUserPassword: true --
|
|
401
|
+
# CloudFormation's `{{resolve:secretsmanager:...}}` only
|
|
402
|
+
# resolves an auto-rotating managed secret fresh at the
|
|
403
|
+
# compute resource's own creation, and RDS's managed-
|
|
404
|
+
# password feature rotates the password itself shortly
|
|
405
|
+
# after standing up the cluster -- every subsequent
|
|
406
|
+
# invocation would keep authenticating with the now-stale
|
|
407
|
+
# original value. A self-managed secret (generated once,
|
|
408
|
+
# here, never auto-rotated) has nothing to go stale.
|
|
409
|
+
#{db_id}Secret:
|
|
410
|
+
Type: AWS::SecretsManager::Secret
|
|
411
|
+
Properties:
|
|
412
|
+
GenerateSecretString:
|
|
413
|
+
SecretStringTemplate: '{"username":"postgres"}'
|
|
414
|
+
GenerateStringKey: password
|
|
415
|
+
PasswordLength: 32
|
|
416
|
+
ExcludeCharacters: '"@/'
|
|
417
|
+
|
|
418
|
+
#{db_id}Cluster:
|
|
419
|
+
Type: AWS::RDS::DBCluster
|
|
420
|
+
DeletionPolicy: Snapshot
|
|
421
|
+
UpdateReplacePolicy: Snapshot
|
|
422
|
+
DependsOn: #{db_id}Secret
|
|
423
|
+
Properties:
|
|
424
|
+
Engine: aurora-postgresql
|
|
425
|
+
EngineVersion: "16.14"
|
|
426
|
+
DatabaseName: #{db_name}
|
|
427
|
+
MasterUsername: postgres
|
|
428
|
+
MasterUserPassword: !Sub "{{resolve:secretsmanager:${#{db_id}Secret}:SecretString:password}}"
|
|
429
|
+
StorageEncrypted: true
|
|
430
|
+
DBSubnetGroupName: !Ref #{db_id}SubnetGroup
|
|
431
|
+
VpcSecurityGroupIds: [!Ref #{db_id}SecurityGroup]
|
|
432
|
+
ServerlessV2ScalingConfiguration:
|
|
433
|
+
MinCapacity: 0.5
|
|
434
|
+
MaxCapacity: 1
|
|
435
|
+
|
|
436
|
+
#{db_id}Instance:
|
|
437
|
+
Type: AWS::RDS::DBInstance
|
|
438
|
+
DeletionPolicy: Snapshot
|
|
439
|
+
UpdateReplacePolicy: Snapshot
|
|
440
|
+
Properties:
|
|
441
|
+
Engine: aurora-postgresql
|
|
442
|
+
DBInstanceClass: db.serverless
|
|
443
|
+
DBClusterIdentifier: !Ref #{db_id}Cluster
|
|
444
|
+
PubliclyAccessible: false
|
|
445
|
+
AURORADB
|
|
446
|
+
else
|
|
447
|
+
<<~PLAINDB.rstrip
|
|
448
|
+
#{db_id}:
|
|
449
|
+
Type: AWS::RDS::DBInstance
|
|
450
|
+
DeletionPolicy: Snapshot
|
|
451
|
+
UpdateReplacePolicy: Snapshot
|
|
452
|
+
Properties:
|
|
453
|
+
Engine: postgres
|
|
454
|
+
EngineVersion: "16.14"
|
|
455
|
+
DBInstanceClass: db.t4g.micro
|
|
456
|
+
AllocatedStorage: "20"
|
|
457
|
+
StorageEncrypted: true
|
|
458
|
+
DBName: #{db_name}
|
|
459
|
+
MasterUsername: postgres
|
|
460
|
+
ManageMasterUserPassword: true
|
|
461
|
+
PubliclyAccessible: false
|
|
462
|
+
DBSubnetGroupName: !Ref #{db_id}SubnetGroup
|
|
463
|
+
VPCSecurityGroups: [!Ref #{db_id}SecurityGroup]
|
|
464
|
+
PLAINDB
|
|
465
|
+
end}
|
|
466
|
+
OWNDB
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Renders the temporary era-minting bastion — a standalone
|
|
470
|
+
# CloudFormation template, deployed and destroyed by `make
|
|
471
|
+
# mint-era`, never merged into the main stack. SSM Session Manager
|
|
472
|
+
# only, never SSH: no key pair, no inbound security group rule at
|
|
473
|
+
# all — the only way in is `aws ssm start-session`, itself gated by
|
|
474
|
+
# the caller's own IAM permissions.
|
|
475
|
+
#
|
|
476
|
+
# Callers skip this entirely when `shared` — a Shared-mode domain
|
|
477
|
+
# provisions no RDS/VPC of its own for a bastion to reach, and era-
|
|
478
|
+
# minting for it reuses its owner's own already-standing bastion
|
|
479
|
+
# path instead.
|
|
480
|
+
#
|
|
481
|
+
# @param domain [String] the domain directory's path, named in the header comment
|
|
482
|
+
# @param infra_name [String] the domain's own AWS-facing name
|
|
483
|
+
# @param stack_name [String] the main stack's own name, tagged onto the bastion instance
|
|
484
|
+
# @param db_id [String] the RDS/VPC resources' own shared logical-id prefix, as
|
|
485
|
+
# `vpc_and_database_yaml` names them
|
|
486
|
+
# @param google_oauth_present [Boolean] whether the main stack already has its own
|
|
487
|
+
# public subnet/Internet Gateway to reuse, rather than minting a second one
|
|
488
|
+
# @param bastion_parameters [Array<Hash{Symbol => String}>] as `bastion_parameters` builds
|
|
489
|
+
# @return [String] the rendered CloudFormation template
|
|
490
|
+
def bastion_yaml(domain:, infra_name:, stack_name:, db_id:, google_oauth_present:, bastion_parameters:)
|
|
491
|
+
<<~YAML
|
|
492
|
+
# GENERATED by bin/project_deploy #{domain} — re-run it to refresh
|
|
493
|
+
# this file rather than hand-editing. Deployed and destroyed by
|
|
494
|
+
# `make mint-era` (this directory's own Makefile) — never left
|
|
495
|
+
# running: it exists only long enough for one Ruby boot to reach
|
|
496
|
+
# #{infra_name}'s private RDS instance and mint era 1.
|
|
497
|
+
AWSTemplateFormatVersion: '2010-09-09'
|
|
498
|
+
Description: >
|
|
499
|
+
TEMPORARY — #{infra_name}'s era-minting bastion. Deployed and torn
|
|
500
|
+
down by `make mint-era`; not meant to run continuously.
|
|
501
|
+
|
|
502
|
+
# bastion_parameters (Shared.bastion_parameters) — the other end of
|
|
503
|
+
# the contract the main template's own Outputs comment describes;
|
|
504
|
+
# the Makefile's --parameter-overrides line fills these from the
|
|
505
|
+
# same table.
|
|
506
|
+
Parameters:
|
|
507
|
+
#{bastion_parameters.map { |p| "#{p[:name]}:\n Type: #{p[:type]}" }.join("\n ")}
|
|
508
|
+
|
|
509
|
+
Resources:
|
|
510
|
+
#{google_oauth_present ? <<~REUSEPUBLIC.each_line.with_index.map { |l, i| (i.zero? ? "" : " ") + l }.join.rstrip : <<~OWNPUBLIC.each_line.with_index.map { |l, i| (i.zero? ? "" : " ") + l }.join.rstrip}
|
|
511
|
+
# BastionSubnetId, not a subnet minted here — the main
|
|
512
|
+
# template's own #{db_id}BastionPublicSubnet (and the
|
|
513
|
+
# Internet Gateway it shares with #{db_id}PublicSubnet)
|
|
514
|
+
# already exist once google_oauth_present is true, and a VPC
|
|
515
|
+
# accepts only one attached Internet Gateway: this stack's
|
|
516
|
+
# own BastionInternetGateway (below, the no-oauth branch)
|
|
517
|
+
# would collide with it. Its own subnet, not PublicSubnetId
|
|
518
|
+
# directly, despite sharing that subnet's route table --
|
|
519
|
+
# PublicSubnetId's AZ can't launch EC2 instances at all on
|
|
520
|
+
# this account.
|
|
521
|
+
REUSEPUBLIC
|
|
522
|
+
# A new public subnet, not the RDS's own private ones --
|
|
523
|
+
# those have no internet route at all, and the SSM agent
|
|
524
|
+
# needs to reach AWS's own SSM endpoints over HTTPS.
|
|
525
|
+
# 10.0.99.0/24 is picked to sit outside the main template's
|
|
526
|
+
# own 10.0.1.0/24 and 10.0.2.0/24 ranges inside the same /16 VPC.
|
|
527
|
+
BastionSubnet:
|
|
528
|
+
Type: AWS::EC2::Subnet
|
|
529
|
+
Properties:
|
|
530
|
+
VpcId: !Ref VpcId
|
|
531
|
+
CidrBlock: 10.0.99.0/24
|
|
532
|
+
AvailabilityZone: !Select [1, !GetAZs '']
|
|
533
|
+
MapPublicIpOnLaunch: true
|
|
534
|
+
|
|
535
|
+
BastionInternetGateway:
|
|
536
|
+
Type: AWS::EC2::InternetGateway
|
|
537
|
+
|
|
538
|
+
BastionVpcGatewayAttachment:
|
|
539
|
+
Type: AWS::EC2::VPCGatewayAttachment
|
|
540
|
+
Properties:
|
|
541
|
+
VpcId: !Ref VpcId
|
|
542
|
+
InternetGatewayId: !Ref BastionInternetGateway
|
|
543
|
+
|
|
544
|
+
BastionRouteTable:
|
|
545
|
+
Type: AWS::EC2::RouteTable
|
|
546
|
+
Properties:
|
|
547
|
+
VpcId: !Ref VpcId
|
|
548
|
+
|
|
549
|
+
BastionInternetRoute:
|
|
550
|
+
Type: AWS::EC2::Route
|
|
551
|
+
DependsOn: BastionVpcGatewayAttachment
|
|
552
|
+
Properties:
|
|
553
|
+
RouteTableId: !Ref BastionRouteTable
|
|
554
|
+
DestinationCidrBlock: 0.0.0.0/0
|
|
555
|
+
GatewayId: !Ref BastionInternetGateway
|
|
556
|
+
|
|
557
|
+
BastionSubnetRouteTableAssociation:
|
|
558
|
+
Type: AWS::EC2::SubnetRouteTableAssociation
|
|
559
|
+
Properties:
|
|
560
|
+
SubnetId: !Ref BastionSubnet
|
|
561
|
+
RouteTableId: !Ref BastionRouteTable
|
|
562
|
+
|
|
563
|
+
OWNPUBLIC
|
|
564
|
+
# Zero Ingress rules — SSM Session Manager is outbound-only from
|
|
565
|
+
# the instance's side (it dials out to AWS's SSM service), so
|
|
566
|
+
# nothing needs to reach this instance over the network at all,
|
|
567
|
+
# despite it sitting in a public subnet with a public IP.
|
|
568
|
+
BastionSecurityGroup:
|
|
569
|
+
Type: AWS::EC2::SecurityGroup
|
|
570
|
+
Properties:
|
|
571
|
+
VpcId: !Ref VpcId
|
|
572
|
+
# A plain hyphen, not an em-dash - EC2's GroupDescription
|
|
573
|
+
# field rejects any non-ASCII character outright.
|
|
574
|
+
GroupDescription: "TEMPORARY era-minting bastion - SSM only, no inbound rules"
|
|
575
|
+
|
|
576
|
+
BastionToRds:
|
|
577
|
+
Type: AWS::EC2::SecurityGroupIngress
|
|
578
|
+
Properties:
|
|
579
|
+
GroupId: !Ref RdsSecurityGroupId
|
|
580
|
+
IpProtocol: tcp
|
|
581
|
+
FromPort: 5432
|
|
582
|
+
ToPort: 5432
|
|
583
|
+
SourceSecurityGroupId: !Ref BastionSecurityGroup
|
|
584
|
+
|
|
585
|
+
BastionRole:
|
|
586
|
+
Type: AWS::IAM::Role
|
|
587
|
+
Properties:
|
|
588
|
+
AssumeRolePolicyDocument:
|
|
589
|
+
Version: '2012-10-17'
|
|
590
|
+
Statement:
|
|
591
|
+
- Effect: Allow
|
|
592
|
+
Principal: { Service: ec2.amazonaws.com }
|
|
593
|
+
Action: sts:AssumeRole
|
|
594
|
+
ManagedPolicyArns:
|
|
595
|
+
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
|
|
596
|
+
|
|
597
|
+
BastionInstanceProfile:
|
|
598
|
+
Type: AWS::IAM::InstanceProfile
|
|
599
|
+
Properties:
|
|
600
|
+
Roles: [!Ref BastionRole]
|
|
601
|
+
|
|
602
|
+
BastionInstance:
|
|
603
|
+
Type: AWS::EC2::Instance
|
|
604
|
+
Properties:
|
|
605
|
+
# Amazon Linux 2023's own SSM parameter — always the current
|
|
606
|
+
# AL2023 AMI for whatever region this deploys into, never a
|
|
607
|
+
# hand-copied, region-specific, staleness-prone AMI id.
|
|
608
|
+
ImageId: "{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}"
|
|
609
|
+
InstanceType: t3.micro
|
|
610
|
+
SubnetId: !Ref #{google_oauth_present ? "BastionSubnetId" : "BastionSubnet"}
|
|
611
|
+
SecurityGroupIds: [!Ref BastionSecurityGroup]
|
|
612
|
+
IamInstanceProfile: !Ref BastionInstanceProfile
|
|
613
|
+
Tags:
|
|
614
|
+
- { Key: Name, Value: #{stack_name}-bastion }
|
|
615
|
+
|
|
616
|
+
Outputs:
|
|
617
|
+
InstanceId:
|
|
618
|
+
Value: !Ref BastionInstance
|
|
619
|
+
YAML
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
end
|