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.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +13 -0
  3. data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/memory.rb +1 -1
  6. data/lib/hecks/adapters/driven/postgres.rb +1 -1
  7. data/lib/hecks/adapters/driven/prism.rb +2 -2
  8. data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
  9. data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
  10. data/lib/hecks/behaviors/expectations.rb +2 -2
  11. data/lib/hecks/bluebook/assembly/contract.rb +1 -1
  12. data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
  13. data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
  14. data/lib/hecks/bluebook/assembly.rb +1 -1
  15. data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
  16. data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
  17. data/lib/hecks/bluebook/capabilities.rb +2 -2
  18. data/lib/hecks/bluebook/chapter.rb +1 -1
  19. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
  20. data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
  21. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
  22. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  23. data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
  24. data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
  25. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
  26. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
  27. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
  28. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
  29. data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
  30. data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
  31. data/lib/hecks/bluebook/hexagon.rb +15 -7
  32. data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
  33. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
  34. data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
  35. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
  36. data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
  37. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
  38. data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
  39. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  40. data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
  41. data/lib/hecks/bluebook/model_check.rb +13 -14
  42. data/lib/hecks/cache_dir.rb +103 -0
  43. data/lib/hecks/cli/document.rb +104 -0
  44. data/lib/hecks/cli/ir.rb +47 -0
  45. data/lib/hecks/cli/mcp.rb +32 -0
  46. data/lib/hecks/cli/mcp_door.rb +461 -0
  47. data/lib/hecks/cli/model_check.rb +270 -0
  48. data/lib/hecks/cli/project_cli.rb +123 -0
  49. data/lib/hecks/cli/project_diagrams.rb +43 -0
  50. data/lib/hecks/cli/run.rb +219 -0
  51. data/lib/hecks/cli/smoke_test.rb +59 -0
  52. data/lib/hecks/cli/stores.rb +54 -0
  53. data/lib/hecks/cli.rb +166 -0
  54. data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
  55. data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
  56. data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
  57. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
  58. data/lib/hecks/embryonaut_bluebook.rb +29 -3
  59. data/lib/hecks/facade/handle.rb +2 -2
  60. data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
  61. data/lib/hecks/forms/value_object_shape.rb +1 -1
  62. data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
  63. data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
  64. data/lib/hecks/language/world/world.bluebook +32 -2
  65. data/lib/hecks/mcp_door_scope.rb +176 -0
  66. data/lib/hecks/ports/key_vault.rb +1 -1
  67. data/lib/hecks/ports/persistence/append_only.rb +1 -1
  68. data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
  69. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
  70. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
  71. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
  72. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
  73. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
  74. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
  75. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
  76. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
  77. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
  78. data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
  79. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
  80. data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
  81. data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
  82. data/lib/hecks/ports/persistence/state_codec.rb +1 -1
  83. data/lib/hecks/ports/persistence.rb +4 -0
  84. data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
  85. data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
  86. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
  87. data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
  88. data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
  89. data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
  90. data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
  91. data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
  92. data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
  93. data/lib/hecks/projections/deploy/fargate.rb +188 -139
  94. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  95. data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
  96. data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
  97. data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
  98. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
  99. data/lib/hecks/projections/deploy/preview/script.rb +40 -0
  100. data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
  101. data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
  102. data/lib/hecks/projections/deploy/preview/template.rb +247 -0
  103. data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
  104. data/lib/hecks/projections/deploy/preview.rb +103 -0
  105. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
  106. data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
  107. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
  108. data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
  109. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
  110. data/lib/hecks/projections/deploy/scripts.rb +171 -0
  111. data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
  112. data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
  113. data/lib/hecks/projections/deploy/smoke.rb +159 -0
  114. data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
  115. data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
  116. data/lib/hecks/projections/deploy/template_diff.rb +173 -0
  117. data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
  118. data/lib/hecks/projections/reference.rb +7 -1
  119. data/lib/hecks/projections.rb +2 -0
  120. data/lib/hecks/projector/exporter.rb +4 -4
  121. data/lib/hecks/projector/target.rb +1 -1
  122. data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
  123. data/lib/hecks/release/gem_pin.rb +112 -0
  124. data/lib/hecks/release/runner/ci_publisher.rb +96 -0
  125. data/lib/hecks/release/runner/commands.rb +68 -0
  126. data/lib/hecks/release/runner/console.rb +52 -0
  127. data/lib/hecks/release/runner/gem_publisher.rb +44 -0
  128. data/lib/hecks/release/runner/git.rb +48 -0
  129. data/lib/hecks/release/runner/npm_publisher.rb +89 -0
  130. data/lib/hecks/release/runner/preflight.rb +119 -0
  131. data/lib/hecks/release/runner/published.rb +55 -0
  132. data/lib/hecks/release/runner/tagger.rb +87 -0
  133. data/lib/hecks/release/runner.rb +244 -0
  134. data/lib/hecks/router/namespace_installer.rb +1 -1
  135. data/lib/hecks/runtime/command_interpreter.rb +3 -3
  136. data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
  137. data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
  138. data/lib/hecks/runtime/entity_element.rb +1 -1
  139. data/lib/hecks/runtime/entity_interpreter.rb +1 -1
  140. data/lib/hecks/runtime/invocation.rb +1 -1
  141. data/lib/hecks/runtime/loader.rb +27 -3
  142. data/lib/hecks/runtime/outbox.rb +1 -1
  143. data/lib/hecks/runtime/reaction_invocation.rb +3 -3
  144. data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
  145. data/lib/hecks/runtime/reference_hop.rb +4 -4
  146. data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
  147. data/lib/hecks/runtime/registry/verification.rb +15 -13
  148. data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
  149. data/lib/hecks/runtime/registry.rb +22 -13
  150. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  151. data/lib/hecks/runtime/tenant_check.rb +1 -1
  152. data/lib/hecks/runtime/value/coercion.rb +3 -3
  153. data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
  154. data/lib/hecks/storehouse.rb +18 -12
  155. data/lib/hecks/vendoring/git_environment.rb +35 -0
  156. data/lib/hecks/vendoring/git_source.rb +102 -0
  157. data/lib/hecks/vendoring.rb +102 -0
  158. data/lib/hecks/version.rb +1 -1
  159. data/lib/hecks/vocabulary.rb +5 -0
  160. data/lib/hecks.rb +8 -2
  161. metadata +72 -58
  162. data/lib/hecks/bench/cli.rb +0 -74
  163. data/lib/hecks/bench/environment.rb +0 -100
  164. data/lib/hecks/bench/postgres_probe.rb +0 -41
  165. data/lib/hecks/bench/report.rb +0 -116
  166. data/lib/hecks/bench/ruby_runner.rb +0 -151
  167. data/lib/hecks/bench/run.rb +0 -55
  168. data/lib/hecks/bench/rust_runner.rb +0 -165
  169. data/lib/hecks/bench/stats.rb +0 -75
  170. data/lib/hecks/bench/suite.rb +0 -172
  171. data/lib/hecks/bench/workload.rb +0 -137
  172. data/lib/hecks/bench.rb +0 -28
  173. data/lib/hecks/codemod.rb +0 -443
  174. data/lib/hecks/corpus.rb +0 -479
  175. data/lib/hecks/doc/reference.rb +0 -585
  176. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  177. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  178. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  179. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  180. data/lib/hecks/fuzzing/differential.rb +0 -192
  181. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  182. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  183. data/lib/hecks/fuzzing/form_census.rb +0 -285
  184. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  185. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  186. data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
  187. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  188. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  189. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  190. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  191. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  192. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  193. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  194. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  195. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  196. data/lib/hecks/fuzzing/properties.rb +0 -267
  197. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  198. data/lib/hecks/fuzzing/replay.rb +0 -930
  199. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  200. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  201. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  202. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  203. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  204. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  205. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  206. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  207. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  208. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  209. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  210. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  211. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  212. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  213. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  214. data/lib/hecks/fuzzing.rb +0 -23
  215. data/lib/hecks/grammar/evolve.rb +0 -524
  216. data/lib/hecks/query_ir.rb +0 -482
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env bash
2
+ # GENERATED by bin/project_deploy - re-run it to refresh this file rather than
3
+ # hand-editing. Source: the `preview` block of the domain's deployed_to("AwsFargate").
4
+ #
5
+ # Feature-branch previews of <%= s.infra_name %>: one isolated stack per git branch,
6
+ # named from the branch, no flags to remember.
7
+ #
8
+ # preview.sh deploy create or update THIS branch's preview
9
+ # preview.sh destroy tear it down again (PREVIEW_DROP_DATABASE=1 also drops its database)
10
+ # preview.sh list every preview that exists
11
+ # preview.sh url this branch's URL
12
+ # preview.sh name what this branch's preview is called (no AWS calls)
13
+ # preview.sh ensure-database create this branch's database if it is missing
14
+ <% if s.first_admin -%>
15
+ # preview.sh login a browser-console line that signs you in as its first admin
16
+ <% end -%>
17
+ #
18
+ # `deploy` pushes images that are already built locally (<%= image_hint %>), so it deploys
19
+ # your WORKING TREE, not the last commit; the Commit stack tag says which, with -dirty
20
+ # when there are uncommitted changes. Build them first with `make docker-build`.
21
+ #
22
+ # It cannot touch the live stack: it only ever operates on stacks named
23
+ # <%= s.prefix %>-*, it refuses <%= s.protected_branches.join(', ') %>, and preview.yaml is a
24
+ # separate template from template.yaml.
25
+ #
26
+ # Needs: aws, docker, git, curl<% if s.first_admin %>, python3 (for the signup and login tokens)<% end %>.
27
+ set -euo pipefail
28
+ cd "$(dirname "${BASH_SOURCE[0]}")"
29
+
30
+ REGION="<%= s.region %>"
31
+ OWNER_STACK="<%= s.owner_stack %>"
32
+ DATABASE_STACK="<%= s.database_stack %>"
33
+ DATABASE_ENDPOINT_OUTPUT="<%= s.database_endpoint_output %>"
34
+ DATABASE_SECRET_OUTPUT="<%= s.database_secret_output %>"
35
+ PREFIX="<%= s.prefix %>"
36
+ DB_PREFIX="<%= s.db_prefix %>"
37
+ SERVICES="<%= s.containers.map(&:name).join(' ') %>"
38
+ PROTECTED_BRANCHES="<%= s.protected_branches.join(' ') %>"
39
+ SIGNUP_PATH="<%= s.signup_path %>"
40
+ LANDING_PATH="<%= s.landing_path %>"
41
+ SESSION_COOKIE="<%= s.session_cookie || 'hecks_session' %>"
42
+
43
+ die() { echo "error: $*" >&2; exit 1; }
44
+ warn() { echo "warning: $*" >&2; }
45
+
46
+ # The local docker image `deploy` pushes for one container.
47
+ local_image() {
48
+ case "$1" in
49
+ <% s.containers.each do |c| -%>
50
+ <%= c.name %>) echo "<%= c.image %>" ;;
51
+ <% end -%>
52
+ *) die "no local image is declared for '$1'" ;;
53
+ esac
54
+ }
55
+
56
+ # The CloudFormation image-tag parameter of one container.
57
+ tag_parameter() {
58
+ case "$1" in
59
+ <% s.containers.each do |c| -%>
60
+ <%= c.name %>) echo "<%= c.logical %>ImageTag" ;;
61
+ <% end -%>
62
+ esac
63
+ }
64
+
65
+ # ---- naming: everything derives from the branch ---------------------------
66
+
67
+ # The 5-character hash that keeps two branches from sharing a stack.
68
+ hash5() {
69
+ printf '%s' "$1" | { shasum 2>/dev/null || sha1sum; } | cut -c1-5
70
+ }
71
+
72
+ # Lowercase, non-alphanumerics to hyphens. Anything that changed on the way (a
73
+ # slash, an underscore, truncation to the 20 characters the ALB name allows)
74
+ # gets a 5-character hash of the real name, so `feat/x` and `feat-x` cannot land
75
+ # on the same stack. An already-clean short name is used as is.
76
+ derive_env() {
77
+ local lower slug
78
+ lower=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
79
+ slug=$(printf '%s' "$lower" | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')
80
+ [ -n "$slug" ] || die "branch name '$1' has no usable characters"
81
+ if [ "$slug" != "$lower" ] || [ ${#slug} -gt 20 ]; then
82
+ slug=$(printf '%s' "${slug:0:14}" | sed -E 's/-+$//')-$(hash5 "$1")
83
+ fi
84
+ printf '%s' "$slug"
85
+ }
86
+
87
+ # Sets BRANCH and every name derived from it. Called by the commands that act on
88
+ # one branch's preview (not by `list`, which is branch-independent).
89
+ init_branch() {
90
+ BRANCH="${BRANCH:-$(git rev-parse --abbrev-ref HEAD)}"
91
+ [ "$BRANCH" != "HEAD" ] || die "detached HEAD - check out a branch, or pass BRANCH=<name>"
92
+ local protected
93
+ for protected in $PROTECTED_BRANCHES; do
94
+ [ "$BRANCH" != "$protected" ] || die "refusing to make a preview of $BRANCH - the live stack deploys through its own targets"
95
+ done
96
+ ENV_NAME=$(derive_env "$BRANCH")
97
+ STACK="$PREFIX-$ENV_NAME"
98
+ DB_NAME="${DB_PREFIX}_pv_$(printf '%s' "$ENV_NAME" | tr '-' '_')"
99
+ CLUSTER="$PREFIX-$ENV_NAME"
100
+ LOG_GROUP="/ecs/$PREFIX-$ENV_NAME"
101
+ }
102
+
103
+ # ---- helpers ---------------------------------------------------------------
104
+
105
+ stack_status() {
106
+ aws cloudformation describe-stacks --stack-name "$STACK" --region "$REGION" \
107
+ --query 'Stacks[0].StackStatus' --output text 2>/dev/null || true
108
+ }
109
+
110
+ stack_output() { # <stack> <OutputKey>
111
+ aws cloudformation describe-stacks --stack-name "$1" --region "$REGION" \
112
+ --query "Stacks[0].Outputs[?OutputKey=='$2'].OutputValue" --output text
113
+ }
114
+
115
+ # A value the owner or database stack must provide.
116
+ owner_output() { # <stack> <OutputKey>
117
+ local value
118
+ value=$(stack_output "$1" "$2")
119
+ { [ -n "$value" ] && [ "$value" != "None" ]; } || die "$1 has no $2 output"
120
+ printf '%s' "$value"
121
+ }
122
+
123
+ resolve_owner() {
124
+ VPC=$(owner_output "$OWNER_STACK" VpcId)
125
+ SUBNET_A=$(owner_output "$OWNER_STACK" PrivateSubnetAId)
126
+ SUBNET_B=$(owner_output "$OWNER_STACK" PrivateSubnetBId)
127
+ PUBLIC_A=$(owner_output "$OWNER_STACK" PublicSubnetId)
128
+ PUBLIC_B=$(owner_output "$OWNER_STACK" BastionSubnetId)
129
+ SG=$(owner_output "$OWNER_STACK" FunctionSecurityGroupId)
130
+ DB_HOST=$(owner_output "$DATABASE_STACK" "$DATABASE_ENDPOINT_OUTPUT")
131
+ DB_SECRET=$(owner_output "$DATABASE_STACK" "$DATABASE_SECRET_OUTPUT")
132
+ }
133
+
134
+ # The DesiredCount the stack currently has (0 if there is no stack yet). Kept as
135
+ # is while new images roll in, so a re-run after a failed deploy cannot start
136
+ # tasks before the database exists.
137
+ current_count() {
138
+ aws cloudformation describe-stacks --stack-name "$STACK" --region "$REGION" \
139
+ --query "Stacks[0].Parameters[?ParameterKey=='DesiredCount'].ParameterValue" --output text 2>/dev/null \
140
+ | grep -E '^[0-9]+$' || echo 0
141
+ }
142
+
143
+ guard_status() {
144
+ case "$(stack_status)" in
145
+ ROLLBACK_COMPLETE|CREATE_FAILED|DELETE_FAILED)
146
+ die "$STACK is in a failed state ($(stack_status)); run 'preview.sh destroy' first, then deploy again" ;;
147
+ esac
148
+ }
149
+
150
+ # Only ever operate on preview stacks.
151
+ guard_stack_name() {
152
+ case "$STACK" in
153
+ "$PREFIX"-*) ;;
154
+ *) die "refusing to operate on $STACK: not a preview stack" ;;
155
+ esac
156
+ }
157
+
158
+ # stack_deploy <desired-count> <image-tag>
159
+ stack_deploy() {
160
+ local commit dirty=""
161
+ commit=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
162
+ git diff --quiet HEAD -- 2>/dev/null || dirty="-dirty"
163
+ local overrides="" svc
164
+ for svc in $SERVICES; do
165
+ overrides="$overrides $(tag_parameter "$svc")=$2"
166
+ done
167
+ # shellcheck disable=SC2086
168
+ aws cloudformation deploy --template-file preview.yaml --stack-name "$STACK" --region "$REGION" \
169
+ --capabilities CAPABILITY_IAM --no-fail-on-empty-changeset \
170
+ --tags "Project=$PREFIX" "ManagedBy=preview.sh" "Branch=$BRANCH" "Commit=$commit$dirty" \
171
+ --parameter-overrides \
172
+ EnvName="$ENV_NAME" DbName="$DB_NAME" \
173
+ OwningVpcId="$VPC" OwningSubnetAId="$SUBNET_A" OwningSubnetBId="$SUBNET_B" \
174
+ OwningPublicSubnetAId="$PUBLIC_A" OwningPublicSubnetBId="$PUBLIC_B" \
175
+ OwningSecurityGroupId="$SG" OwningDatabaseEndpoint="$DB_HOST" OwningDatabaseSecretArn="$DB_SECRET" \
176
+ DesiredCount="$1" $overrides
177
+ }
178
+
179
+ # ---- the database ----------------------------------------------------------
180
+
181
+ # The database instance is reachable only from inside the VPC, so the database is
182
+ # created (or dropped) by a one-shot task from preview.yaml, not from this machine.
183
+ # run_database_task <create|drop>
184
+ run_database_task() {
185
+ local action="$1" taskdef task code
186
+ taskdef=$(stack_output "$STACK" DbInitTaskDefinition)
187
+ task=$(aws ecs run-task --cluster "$CLUSTER" --launch-type FARGATE --task-definition "$taskdef" --region "$REGION" \
188
+ --overrides "{\"containerOverrides\":[{\"name\":\"dbinit\",\"environment\":[{\"name\":\"ACTION\",\"value\":\"$action\"}]}]}" \
189
+ --network-configuration "awsvpcConfiguration={subnets=[$SUBNET_A,$SUBNET_B],securityGroups=[$SG],assignPublicIp=DISABLED}" \
190
+ --query 'tasks[0].taskArn' --output text)
191
+ { [ -n "$task" ] && [ "$task" != "None" ]; } || die "could not start the database task"
192
+ aws ecs wait tasks-stopped --cluster "$CLUSTER" --tasks "$task" --region "$REGION"
193
+ code=$(aws ecs describe-tasks --cluster "$CLUSTER" --tasks "$task" --region "$REGION" \
194
+ --query 'tasks[0].containers[0].exitCode' --output text)
195
+ if [ "$code" != "0" ]; then
196
+ aws logs tail "$LOG_GROUP" --log-stream-name-prefix dbinit --since 15m --region "$REGION" >&2 || true
197
+ die "database task exited with code $code (log above)"
198
+ fi
199
+ }
200
+
201
+ ensure_database() {
202
+ echo "==> creating database $DB_NAME if it does not exist (one-shot task)"
203
+ run_database_task create
204
+ }
205
+
206
+ cmd_ensure_database() {
207
+ [ -n "$(stack_status)" ] || die "no preview stack $STACK - run 'preview.sh deploy' first"
208
+ resolve_owner
209
+ ensure_database
210
+ }
211
+
212
+ # ---- deploy ----------------------------------------------------------------
213
+
214
+ cmd_deploy() {
215
+ local svc
216
+ for svc in $SERVICES; do
217
+ docker image inspect "$(local_image "$svc")" >/dev/null 2>&1 \
218
+ || die "image $(local_image "$svc") is not built - build the images first (make docker-build)"
219
+ done
220
+ guard_stack_name
221
+ guard_status
222
+ resolve_owner
223
+
224
+ local existed=0 tag account ecr
225
+ [ -n "$(stack_status)" ] && existed=1
226
+
227
+ echo "==> preview '$ENV_NAME' for branch '$BRANCH' (stack $STACK, database $DB_NAME)"
228
+ if [ "$existed" = 0 ]; then
229
+ echo "==> creating the stack with no tasks running (the ECR repositories must exist before images can be pushed)"
230
+ stack_deploy 0 bootstrap
231
+ fi
232
+
233
+ tag="$ENV_NAME-$(date -u +%Y%m%d%H%M%S)"
234
+ account=$(aws sts get-caller-identity --query Account --output text)
235
+ ecr="$account.dkr.ecr.$REGION.amazonaws.com"
236
+
237
+ echo "==> ecr login"
238
+ # An isolated docker config: Docker Desktop's keychain helper can hang in a
239
+ # headless shell.
240
+ local docker_config=.docker-config-no-keychain
241
+ mkdir -p "$docker_config"
242
+ echo '{}' > "$docker_config/config.json"
243
+ aws ecr get-login-password --region "$REGION" | docker --config "$docker_config" login --username AWS --password-stdin "$ecr"
244
+
245
+ for svc in $SERVICES; do
246
+ echo "==> pushing $svc as $tag"
247
+ docker tag "$(local_image "$svc")" "$ecr/$PREFIX-$ENV_NAME-$svc:$tag"
248
+ docker --config "$docker_config" push "$ecr/$PREFIX-$ENV_NAME-$svc:$tag"
249
+ done
250
+
251
+ echo "==> pointing the stack at the new images"
252
+ stack_deploy "$(current_count)" "$tag"
253
+
254
+ ensure_database
255
+
256
+ echo "==> starting the service"
257
+ stack_deploy 1 "$tag"
258
+ echo "==> waiting for the service to settle (a first boot can take several minutes)"
259
+ aws ecs wait services-stable --cluster "$CLUSTER" --services "$PREFIX-$ENV_NAME" --region "$REGION" \
260
+ || die "service did not settle in time - check: aws logs tail $LOG_GROUP --follow --region $REGION"
261
+ <% if s.first_admin -%>
262
+
263
+ signup_admin
264
+ <% end -%>
265
+
266
+ echo
267
+ echo "Preview is up: $(stack_output "$STACK" PreviewUrl)"
268
+ <% if s.first_admin -%>
269
+ echo " sign in: preview.sh login (a one-line session for $ADMIN_EMAIL)"
270
+ <% end -%>
271
+ echo " logs: aws logs tail $LOG_GROUP --follow --region $REGION"
272
+ echo " tear down: preview.sh destroy"
273
+ }
274
+ <% if s.first_admin -%>
275
+
276
+ # ---- first admin: sign the deployer up -------------------------------------
277
+ #
278
+ # A brand-new preview has nobody in it, and sign-in checks membership. The way in
279
+ # is the host's own signup: POST <%= s.signup_path %> dispatches the domain's SignUp command,
280
+ # and the domain's translations turn the signup into the first admin in the
281
+ # preview's own database. The credential is a purpose-bound token minted from the
282
+ # preview's session secret, so only someone with AWS access to the account can
283
+ # sign anyone up. It needs a domain image whose host serves the signup route and
284
+ # a domain that attaches the signups chapter; the warnings below say which is missing.
285
+
286
+ # Who becomes the preview's first admin: the person deploying it. PREVIEW_ADMIN_EMAIL
287
+ # and PREVIEW_ADMIN_NAME override; the default is your git identity.
288
+ admin_identity() {
289
+ ADMIN_EMAIL="${PREVIEW_ADMIN_EMAIL:-$(git config user.email || true)}"
290
+ [ -n "$ADMIN_EMAIL" ] || die "no email to sign up as the preview's first admin - set PREVIEW_ADMIN_EMAIL or git config user.email"
291
+ ADMIN_NAME="${PREVIEW_ADMIN_NAME:-$(git config user.name || true)}"
292
+ [ -n "$ADMIN_NAME" ] || ADMIN_NAME="${ADMIN_EMAIL%%@*}"
293
+ }
294
+
295
+ # The preview's own session secret (the stack generates it). Never printed.
296
+ session_secret() {
297
+ aws secretsmanager get-secret-value --secret-id "$(stack_output "$STACK" SessionSecretArn)" --region "$REGION" \
298
+ --query SecretString --output text \
299
+ | python3 -c 'import json,sys; print(json.load(sys.stdin)["session_secret"])'
300
+ }
301
+
302
+ # The two tokens the host verifies: base64url(payload) + "." + hex HMAC-SHA256.
303
+ # A signup token is bound to its purpose (key "signup:<secret>", good for 10
304
+ # minutes), so a session can never sign anyone up. A session token is the plain
305
+ # account token, keyed with the bare secret (good for 8 hours).
306
+ mint_token() { # <signup|session> <secret> [email]
307
+ KIND="$1" SECRET="$2" EMAIL="${3:-}" python3 - <<'PY'
308
+ import base64, hashlib, hmac, json, os, time
309
+
310
+ kind, secret = os.environ["KIND"], os.environ["SECRET"]
311
+ now = int(time.time())
312
+ if kind == "signup":
313
+ payload, key = {"purpose": "signup", "exp": now + 600}, "signup:" + secret
314
+ else:
315
+ payload, key = {"email": os.environ["EMAIL"], "exp": now + 8 * 3600}, secret
316
+ encoded = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode()).rstrip(b"=").decode()
317
+ print(encoded + "." + hmac.new(key.encode(), encoded.encode(), hashlib.sha256).hexdigest())
318
+ PY
319
+ }
320
+
321
+ # Signs the deployer up. Never fatal: a preview without an admin is still worth
322
+ # having, so a problem is a warning that says what is missing. A new distribution
323
+ # and ALB rule can take a few minutes to answer, so connection errors, 403s and
324
+ # 5xxs are retried (up to ~10 minutes); anything else is an answer.
325
+ signup_admin() {
326
+ admin_identity
327
+ local url secret body response status attempt=0
328
+ url=$(stack_output "$STACK" PreviewUrl)
329
+ secret=$(session_secret)
330
+ response=$(mktemp)
331
+ echo "==> signing $ADMIN_EMAIL up as the preview's first admin (POST $url$SIGNUP_PATH)"
332
+ while :; do
333
+ body=$(TOKEN="$(mint_token signup "$secret")" EMAIL="$ADMIN_EMAIL" NAME="$ADMIN_NAME" python3 -c \
334
+ 'import json, os; print(json.dumps({"token": os.environ["TOKEN"], "email": os.environ["EMAIL"], "name": os.environ["NAME"]}))')
335
+ status=$(curl -s -o "$response" -w '%{http_code}' --max-time 30 -X POST -H 'Content-Type: application/json' -d "$body" "$url$SIGNUP_PATH" || true)
336
+ status="${status:-000}"
337
+ case "$status" in
338
+ 201)
339
+ echo " signed up"; rm -f "$response"; return 0 ;;
340
+ 422)
341
+ if grep -q '"AlreadyExists"' "$response"; then
342
+ echo " already signed up"
343
+ else
344
+ warn "the domain refused the signup: $(cat "$response")"
345
+ warn "does this domain attach the signups chapter?"
346
+ fi
347
+ rm -f "$response"; return 0 ;;
348
+ 302|404)
349
+ warn "this domain has no POST $SIGNUP_PATH route (HTTP $status) - build the domain image from a host that serves it and redeploy"
350
+ rm -f "$response"; return 0 ;;
351
+ 400|401)
352
+ warn "the domain refused the signup (HTTP $status): $(cat "$response")"
353
+ rm -f "$response"; return 0 ;;
354
+ *)
355
+ attempt=$((attempt + 1))
356
+ if [ "$attempt" -ge 40 ]; then
357
+ warn "the preview never answered POST $SIGNUP_PATH (last HTTP $status) - run 'preview.sh deploy' again once it is up"
358
+ rm -f "$response"; return 0
359
+ fi
360
+ sleep "${PREVIEW_SIGNUP_RETRY_SECONDS:-15}" ;;
361
+ esac
362
+ done
363
+ }
364
+
365
+ # Prints what to paste into the browser console to sign in as the preview's first
366
+ # admin: a session for that email signed with THIS preview's session secret (the
367
+ # secret itself is never printed). Anyone with AWS access to the account could
368
+ # mint the same, which is the point: it is the deployer's door, not a public one.
369
+ cmd_login() {
370
+ [ -n "$(stack_status)" ] || die "no preview for branch '$BRANCH' - run 'preview.sh deploy'"
371
+ admin_identity
372
+ local url token
373
+ url=$(stack_output "$STACK" PreviewUrl)
374
+ token=$(mint_token session "$(session_secret)" "$ADMIN_EMAIL")
375
+ echo "Signing in to $url as $ADMIN_EMAIL (good for 8 hours)"
376
+ echo
377
+ echo "1. Open $url$LANDING_PATH"
378
+ echo "2. In the browser console (developer tools), run:"
379
+ echo
380
+ echo " document.cookie = \"$SESSION_COOKIE=$token; path=/; Secure; SameSite=Lax\""
381
+ echo
382
+ echo "3. Reload the page"
383
+ }
384
+ <% end -%>
385
+
386
+ # ---- destroy ---------------------------------------------------------------
387
+
388
+ cmd_destroy() {
389
+ guard_stack_name
390
+ [ -n "$(stack_status)" ] || die "no preview stack $STACK (nothing to destroy)"
391
+ if [ "${PREVIEW_DROP_DATABASE:-}" = "1" ]; then
392
+ resolve_owner
393
+ echo "==> dropping database $DB_NAME (one-shot task; PREVIEW_DROP_DATABASE=1)"
394
+ run_database_task drop
395
+ fi
396
+ echo "==> deleting $STACK"
397
+ aws cloudformation delete-stack --stack-name "$STACK" --region "$REGION"
398
+ aws cloudformation wait stack-delete-complete --stack-name "$STACK" --region "$REGION"
399
+ if [ "${PREVIEW_DROP_DATABASE:-}" = "1" ]; then
400
+ echo "Deleted, and the database $DB_NAME is dropped."
401
+ else
402
+ echo "Deleted. The database $DB_NAME is left on the shared instance on purpose, so a later"
403
+ echo "deploy of this branch picks its data back up. Set PREVIEW_DROP_DATABASE=1 to drop it."
404
+ fi
405
+ }
406
+
407
+ # ---- list / url / name -----------------------------------------------------
408
+
409
+ cmd_list() {
410
+ aws cloudformation describe-stacks --region "$REGION" \
411
+ --query "Stacks[?starts_with(StackName, '$PREFIX-')].[StackName, StackStatus, Tags[?Key=='Branch']|[0].Value, Tags[?Key=='Commit']|[0].Value, Outputs[?OutputKey=='PreviewUrl']|[0].OutputValue]" \
412
+ --output table
413
+ }
414
+
415
+ cmd_url() {
416
+ [ -n "$(stack_status)" ] || die "no preview for branch '$BRANCH' - run 'preview.sh deploy'"
417
+ stack_output "$STACK" PreviewUrl
418
+ }
419
+
420
+ # What this branch's preview is (or would be) called - no AWS calls.
421
+ cmd_name() {
422
+ echo "branch: $BRANCH"
423
+ echo "env: $ENV_NAME"
424
+ echo "stack: $STACK"
425
+ echo "database: $DB_NAME"
426
+ }
427
+
428
+ main() {
429
+ case "${1:-}" in
430
+ deploy) init_branch; cmd_deploy ;;
431
+ destroy) init_branch; cmd_destroy ;;
432
+ ensure-database) init_branch; cmd_ensure_database ;;
433
+ list) cmd_list ;;
434
+ url) init_branch; cmd_url ;;
435
+ name) init_branch; cmd_name ;;
436
+ <% if s.first_admin -%>
437
+ login) init_branch; cmd_login ;;
438
+ <% end -%>
439
+ *) die "usage: preview.sh <deploy|destroy|ensure-database|list|url|name<%= '|login' if s.first_admin %>>" ;;
440
+ esac
441
+ }
442
+
443
+ # Sourcing this file (to reuse its functions) does not run a command.
444
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
445
+ main "$@"
446
+ fi
@@ -0,0 +1,216 @@
1
+ require "json"
2
+ require_relative "yaml_text"
3
+
4
+ module Hecks
5
+ module Projections
6
+ module Deploy
7
+ module Preview
8
+ # The network edge of a preview stack: security groups, target groups, the
9
+ # load balancer, its listener rules and the service that registers the containers.
10
+ #
11
+ # A container is reachable when it is the default or owns a path. Paths are
12
+ # split into rules of at most five, the most a load balancer condition holds.
13
+ module RoutingYaml
14
+ CLOUDFRONT_PREFIX_LIST = "pl-3b927c52".freeze
15
+ PATHS_PER_RULE = 5
16
+ INGRESS_NOTE = <<~NOTE.chomp.freeze
17
+ # One ingress rule per routed container on the shared owner security group, removed
18
+ # with the stack. The group has a rules-per-group quota, so delete stale previews.
19
+ NOTE
20
+
21
+ extend YamlText
22
+
23
+ module_function
24
+
25
+ # Renders the load balancer's security group and one ingress rule per routed container.
26
+ #
27
+ # @param settings [Settings] the resolved preview settings
28
+ # @return [Array<String>] the resource blocks
29
+ def networking(settings)
30
+ ingress = settings.containers.select(&:routed?).map do |c|
31
+ <<~YAML.chomp
32
+ ComputeIngressFromAlb#{c.logical}:
33
+ Type: AWS::EC2::SecurityGroupIngress
34
+ Properties:
35
+ GroupId: !Ref OwningSecurityGroupId
36
+ IpProtocol: tcp
37
+ FromPort: #{c.port}
38
+ ToPort: #{c.port}
39
+ SourceSecurityGroupId: !Ref AlbSecurityGroup
40
+ YAML
41
+ end
42
+ [alb_security_group(settings), "#{INGRESS_NOTE}\n#{ingress.join("\n\n")}"]
43
+ end
44
+
45
+ # Renders the load balancer's security group, open to CloudFront's prefix list only.
46
+ #
47
+ # @param settings [Settings] the resolved preview settings
48
+ # @return [String] the resource block
49
+ def alb_security_group(settings)
50
+ <<~YAML.chomp
51
+ AlbSecurityGroup:
52
+ Type: AWS::EC2::SecurityGroup
53
+ Properties:
54
+ VpcId: !Ref OwningVpcId
55
+ GroupDescription: !Sub "#{settings.infra_name} preview ${EnvName} ALB - HTTP ingress from CloudFront only"
56
+ SecurityGroupIngress:
57
+ # The AWS-managed com.amazonaws.global.cloudfront.origin-facing prefix list.
58
+ - IpProtocol: tcp
59
+ FromPort: 80
60
+ ToPort: 80
61
+ SourcePrefixListId: #{CLOUDFRONT_PREFIX_LIST}
62
+ YAML
63
+ end
64
+
65
+ # Renders the target groups, the load balancer, its listener and the path rules.
66
+ #
67
+ # @param settings [Settings] the resolved preview settings
68
+ # @return [Array<String>] the resource blocks
69
+ def load_balancing(settings)
70
+ routed = settings.containers.select(&:routed?)
71
+ [*routed.map { |c| target_group(c) }, alb(settings), listener(settings), *listener_rules(settings)]
72
+ end
73
+
74
+ # Renders the target group of one routed container.
75
+ #
76
+ # @param container [Containers::Entry] the container
77
+ # @return [String] the resource block
78
+ def target_group(container)
79
+ <<~YAML.chomp
80
+ #{container.logical}TargetGroup:
81
+ Type: AWS::ElasticLoadBalancingV2::TargetGroup
82
+ Properties:
83
+ TargetType: ip
84
+ Port: #{container.port}
85
+ Protocol: HTTP
86
+ VpcId: !Ref OwningVpcId
87
+ HealthCheckPath: #{container.health_check_path}
88
+ HealthCheckPort: "#{container.port}"
89
+ YAML
90
+ end
91
+
92
+ # Renders the internet-facing application load balancer.
93
+ #
94
+ # @param settings [Settings] the resolved preview settings
95
+ # @return [String] the resource block
96
+ def alb(settings)
97
+ <<~YAML.chomp
98
+ Alb:
99
+ Type: AWS::ElasticLoadBalancingV2::LoadBalancer
100
+ Properties:
101
+ Name: !Sub "#{settings.alb_prefix}-${EnvName}"
102
+ Scheme: internet-facing
103
+ Type: application
104
+ SecurityGroups: [!Ref AlbSecurityGroup]
105
+ Subnets: [!Ref OwningPublicSubnetAId, !Ref OwningPublicSubnetBId]
106
+ YAML
107
+ end
108
+
109
+ # Renders the port 80 listener, whose default action goes to the default container.
110
+ #
111
+ # @param settings [Settings] the resolved preview settings
112
+ # @return [String] the resource block
113
+ def listener(settings)
114
+ <<~YAML.chomp
115
+ Listener:
116
+ Type: AWS::ElasticLoadBalancingV2::Listener
117
+ Properties:
118
+ LoadBalancerArn: !Ref Alb
119
+ Port: 80
120
+ Protocol: HTTP
121
+ DefaultActions:
122
+ - Type: forward
123
+ TargetGroupArn: !Ref #{settings.default_container.logical}TargetGroup
124
+ YAML
125
+ end
126
+
127
+ # Lists the listener rules to render: a non-default container's paths, sliced to
128
+ # fit one condition.
129
+ #
130
+ # @param settings [Settings] the resolved preview settings
131
+ # @return [Array<Hash{Symbol => Object}>] entries with `:container`, `:paths` and `:id`
132
+ def rule_specs(settings)
133
+ settings.containers.reject(&:default).select(&:routed?).flat_map do |c|
134
+ c.paths.each_slice(PATHS_PER_RULE).with_index.map do |slice, n|
135
+ { container: c, paths: slice, id: rule_id(c, n) }
136
+ end
137
+ end
138
+ end
139
+
140
+ # Renders every listener rule, with priorities counting up in tens.
141
+ #
142
+ # @param settings [Settings] the resolved preview settings
143
+ # @return [Array<String>] the resource blocks
144
+ def listener_rules(settings)
145
+ rule_specs(settings).each_with_index.map { |spec, i| listener_rule(spec, (i + 1) * 10) }
146
+ end
147
+
148
+ # Renders one listener rule.
149
+ #
150
+ # @param spec [Hash{Symbol => Object}] one entry of `rule_specs`
151
+ # @param priority [Integer] the rule's listener priority
152
+ # @return [String] the resource block
153
+ def listener_rule(spec, priority)
154
+ <<~YAML.chomp
155
+ #{spec[:id]}:
156
+ Type: AWS::ElasticLoadBalancingV2::ListenerRule
157
+ Properties:
158
+ ListenerArn: !Ref Listener
159
+ Priority: #{priority}
160
+ Conditions:
161
+ - Field: path-pattern
162
+ Values: [#{spec[:paths].map { |p| JSON.generate(p) }.join(', ')}]
163
+ Actions:
164
+ - Type: forward
165
+ TargetGroupArn: !Ref #{spec[:container].logical}TargetGroup
166
+ YAML
167
+ end
168
+
169
+ # Names one listener rule's logical id.
170
+ #
171
+ # @param container [Containers::Entry] the container the rule forwards to
172
+ # @param slice [Integer] the zero-based index of the rule among that container's rules
173
+ # @return [String] the logical id
174
+ def rule_id(container, slice) = "ListenerRule#{container.logical}#{slice + 1}"
175
+
176
+ # Renders the service, which registers every routed container with its target group.
177
+ #
178
+ # @param settings [Settings] the resolved preview settings
179
+ # @return [String] the resource block
180
+ def service(settings)
181
+ rule_ids = rule_specs(settings).map { |spec| spec[:id] }
182
+ <<~YAML.chomp
183
+ Service:
184
+ Type: AWS::ECS::Service
185
+ DependsOn: [#{(['Listener'] + rule_ids).join(', ')}]
186
+ Properties:
187
+ ServiceName: !Sub "#{settings.prefix}-${EnvName}"
188
+ Cluster: !Ref Cluster
189
+ TaskDefinition: !Ref TaskDefinition
190
+ DesiredCount: !Ref DesiredCount
191
+ LaunchType: FARGATE
192
+ EnableExecuteCommand: true
193
+ NetworkConfiguration:
194
+ AwsvpcConfiguration:
195
+ AssignPublicIp: DISABLED
196
+ Subnets: [!Ref OwningSubnetAId, !Ref OwningSubnetBId]
197
+ SecurityGroups: [!Ref OwningSecurityGroupId]
198
+ LoadBalancers:
199
+ #{indent(service_load_balancers(settings), 6)}
200
+ YAML
201
+ end
202
+
203
+ # Renders the service's load balancer registrations.
204
+ #
205
+ # @param settings [Settings] the resolved preview settings
206
+ # @return [String] one list entry per routed container
207
+ def service_load_balancers(settings)
208
+ settings.containers.select(&:routed?).map do |c|
209
+ "- ContainerName: #{c.name}\n ContainerPort: #{c.port}\n TargetGroupArn: !Ref #{c.logical}TargetGroup"
210
+ end.join("\n")
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end