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,235 @@
1
+ require_relative "check"
2
+ require_relative "yaml"
3
+ require_relative "containers"
4
+ require_relative "cdn"
5
+ require_relative "monitoring"
6
+ require_relative "extras"
7
+
8
+ module Hecks
9
+ module Projections
10
+ module Deploy
11
+ module Fargate
12
+ # Everything a `deployed_to("AwsFargate")` block can say beyond its
13
+ # required settings, read and checked into one `Plan`.
14
+ #
15
+ # A world that sets none of these keys resolves to a plan whose every
16
+ # value is the generator's own default: the derived logical ids and
17
+ # names, one container, no extra resources. That plan renders the same
18
+ # template the generator has always produced.
19
+ #
20
+ # ## Settings owned here
21
+ #
22
+ # - `logical_ids`: a map of role to logical id that replaces the id derived from the
23
+ # stack name, so an existing stack's ids can be reproduced. The roles are `service`,
24
+ # `ecr_repository`, `cluster`, `task_definition`, `execution_role`, `task_role`,
25
+ # `log_group`, `target_group`, `alb`, `alb_security_group`, `listener`, `distribution`,
26
+ # `session_secret`, `ingress_from_alb`, `database_prefix` and `compute_prefix`, plus
27
+ # the alerting roles `Monitoring` lists. `database_prefix` and `compute_prefix` are the
28
+ # prefixes of the VPC and database ids and of the compute security-group ids.
29
+ # - `names`: a map of role to the AWS name that replaces the derived one. The roles are
30
+ # `cluster`, `log_group`, `service`, `alb` and `family`; `db_secret_policy`, the name of
31
+ # the task role's database-secret policy (`DbSecretRead`); and
32
+ # `alb_security_group_description`, the load balancer's security group description
33
+ # (EC2 replaces the group when it changes).
34
+ # - `execution_role_database_grant`: false leaves the database secret out of the
35
+ # execution role. The host reads that secret itself with the task role, so the
36
+ # execution role needs it only when a container's `ECS` `Secrets` names it;
37
+ # `execution_policies` must then hold a policy.
38
+ # - `execute_command`: true lets `aws ecs execute-command` open a session in a container.
39
+ # - `health_check_grace_period`: seconds `ECS` ignores failing load-balancer health
40
+ # checks after a task starts.
41
+ # - `deregistration_delay`: seconds a target group drains a deregistered target.
42
+ # - `desired_count_parameter`: makes the service's desired count a template parameter
43
+ # with this name, defaulting to `desired_count`.
44
+ # - `domain_env`: extra environment variables for the domain container, a map of name to
45
+ # a value or intrinsic. A name the generator already sets replaces it; a `nil` value
46
+ # removes it.
47
+ # - `install_dir`: where the Dockerfile installs the host binary and its `.wasm` and
48
+ # `.ir.json` files, and where `HECKS_WASM_PATH` and `HECKS_IR_PATH` point
49
+ # (`/usr/local/bin`).
50
+ # - `build_context_dir`: a directory prefix for the Dockerfile's `COPY` sources.
51
+ # - `db_name_parameter`: for `database "Shared"`, a template parameter that names the
52
+ # shared database, defaulting to the owner's name.
53
+ module Settings
54
+ module_function
55
+
56
+ Plan = Struct.new(
57
+ :ids, :names, :layout, :cdn, :alerts, :extras, :execute_command, :health_check_grace_period,
58
+ :deregistration_delay, :desired_count_parameter, :domain_env, :install_dir, :build_context_dir,
59
+ :db_name_parameter, :execution_database_grant,
60
+ keyword_init: true
61
+ )
62
+
63
+ ID_ROLES = {
64
+ service: nil, ecr_repository: "Repository", cluster: "Cluster", task_definition: "TaskDefinition",
65
+ execution_role: "ExecutionRole", task_role: "TaskRole", log_group: "LogGroup", target_group: "TargetGroup",
66
+ alb: "Alb", alb_security_group: "AlbSecurityGroup", listener: "Listener", distribution: "Distribution",
67
+ session_secret: "SessionSecret", ingress_from_alb: "IngressFromAlb"
68
+ }.freeze
69
+ FIXED_IDS = {
70
+ alerts_topic: "AlertsTopic", alerts_subscription: "AlertsTopicEmailSubscription", alb_5xx_alarm: "AlbTarget5xxAlarm",
71
+ cloudfront_5xx_alarm: "CloudFront5xxAlarm", cloudfront_monitoring: "CloudFrontMonitoringSubscription",
72
+ synthetic_alarm: "SyntheticCheckFailuresAlarm", warmer_role: "WarmerRole", warmer_function: "WarmerFunction",
73
+ scheduler_role: "SchedulerInvokeRole", warmer_schedule: "WarmerSchedule"
74
+ }.freeze
75
+ NAME_ROLES = [:cluster, :log_group, :service, :alb, :family, :alb_security_group_description, :db_secret_policy].freeze
76
+ DEFAULT_INSTALL_DIR = "/usr/local/bin".freeze
77
+
78
+ # Reads and checks every optional setting into a `Plan`.
79
+ #
80
+ # @param settings [Hash{Symbol => Object}] the world's `deployed_to("AwsFargate")`
81
+ # settings
82
+ # @param base [Hash{Symbol => Object}] facts the generator has already resolved:
83
+ # `:infra_name`,
84
+ # `:logical_id`, `:db_id`, `:stack_name`, `:port`, `:desired_count` and `:shared`
85
+ # @return [Plan] the checked settings with every default applied
86
+ # @raise [ArgumentError] if any setting is malformed, or two settings contradict each
87
+ # other
88
+ def resolve(settings, base)
89
+ ids = logical_ids(settings[:logical_ids], base)
90
+ layout = Containers.normalize(settings, infra_name: base[:infra_name], port: base[:port], ids: ids)
91
+ cdn = Cdn.normalize(settings[:cdn], default_origin_id: "#{ids[:alb]}Origin")
92
+ alerts = Monitoring.normalize(settings[:alerts], containers: layout.balanced.map(&:name))
93
+ check_alerts!(alerts, cdn)
94
+ plan = Plan.new(ids: ids, names: names(settings[:names], base), layout: layout, cdn: cdn, alerts: alerts,
95
+ extras: Extras.normalize(settings))
96
+ tuning(plan, settings, base)
97
+ plan.freeze
98
+ end
99
+
100
+ # Lists every template parameter the plan declares beyond the generator's own.
101
+ #
102
+ # @param plan [Plan] the resolved plan
103
+ # @param default_count [Integer] the service's `desired_count`, the default of its
104
+ # parameter
105
+ # @param shared_db_name [String, nil] the owning database's name, when `database "Shared"`
106
+ # @return [String] flush-left parameter text separated by newlines, or empty
107
+ def parameters_yaml(plan, default_count:, shared_db_name:)
108
+ [
109
+ Containers.parameters_yaml(plan.layout), Cdn.parameters_yaml(plan.cdn),
110
+ count_parameter_yaml(plan, default_count), db_name_parameter_yaml(plan, shared_db_name),
111
+ Extras.parameters_yaml(plan.extras[:parameters])
112
+ ].reject(&:empty?).join
113
+ end
114
+
115
+ def logical_ids(overrides, base)
116
+ given = if overrides
117
+ Check.hash!(overrides, "logical_ids",
118
+ allowed: ID_ROLES.keys + FIXED_IDS.keys + [:database_prefix, :compute_prefix])
119
+ else
120
+ {}
121
+ end
122
+ derived = ID_ROLES.to_h { |role, suffix| [role, suffix ? "#{base[:logical_id]}#{suffix}" : base[:logical_id]] }
123
+ derived.merge!(FIXED_IDS)
124
+ derived[:database_prefix] = base[:db_id]
125
+ derived[:compute_prefix] = base[:logical_id]
126
+ given.each { |role, id| derived[role] = Check.logical_id!(id, "logical_ids #{role}") }
127
+ derived.freeze
128
+ end
129
+ private_class_method :logical_ids
130
+
131
+ def names(overrides, base)
132
+ given = overrides ? Check.hash!(overrides, "names", allowed: NAME_ROLES) : {}
133
+ stack = base[:stack_name]
134
+ derived = {
135
+ cluster: stack, log_group: "/ecs/#{stack}", service: stack, alb: "#{stack}-alb",
136
+ family: base[:infra_name], db_secret_policy: "DbSecretRead"
137
+ }
138
+ given.each { |role, name| derived[role] = named(role, name) }
139
+ derived.freeze
140
+ end
141
+ private_class_method :names
142
+
143
+ def named(role, value)
144
+ return Check.resource_name!(value, "names #{role}") unless role == :alb_security_group_description
145
+
146
+ text = value.to_s
147
+ return text if text.match?(/\A[\x20-\x7e]{1,255}\z/)
148
+
149
+ raise ArgumentError,
150
+ "names #{role} must be 1 to 255 printable ASCII characters (EC2 rejects anything else), got #{value.inspect}"
151
+ end
152
+ private_class_method :named
153
+
154
+ def check_alerts!(alerts, cdn)
155
+ return unless alerts
156
+ if Monitoring.cloudfront_metrics?(alerts) && cdn.nil?
157
+ raise ArgumentError, "alerts cloudfront_5xx needs cdn options to be set, " \
158
+ "so its metric subscription is declared with the distribution"
159
+ end
160
+ end
161
+ private_class_method :check_alerts!
162
+
163
+ def tuning(plan, settings, base)
164
+ plan.execute_command = Check.boolean!(settings.fetch(:execute_command, false), "execute_command")
165
+ plan.health_check_grace_period = optional_integer(settings, :health_check_grace_period, 0..2_147_483_647)
166
+ plan.deregistration_delay = optional_integer(settings, :deregistration_delay, 0..3600)
167
+ plan.desired_count_parameter = optional_id(settings, :desired_count_parameter)
168
+ plan.db_name_parameter = optional_id(settings, :db_name_parameter)
169
+ if plan.db_name_parameter && !base[:shared]
170
+ raise ArgumentError,
171
+ "db_name_parameter applies only to database \"Shared\""
172
+ end
173
+
174
+ plan.execution_database_grant = Check.boolean!(settings.fetch(:execution_role_database_grant, true),
175
+ "execution_role_database_grant")
176
+ if !plan.execution_database_grant && plan.extras[:execution_policies].empty?
177
+ raise ArgumentError, "execution_role_database_grant false leaves the execution role without a policy; " \
178
+ "add one to execution_policies"
179
+ end
180
+
181
+ plan.domain_env = Check.map!(settings.fetch(:domain_env, {}), "domain_env", nil_ok: true)
182
+ plan.install_dir = install_dir(settings.fetch(:install_dir, DEFAULT_INSTALL_DIR))
183
+ plan.build_context_dir = build_context_dir(settings.fetch(:build_context_dir, ""))
184
+ end
185
+ private_class_method :tuning
186
+
187
+ def optional_integer(settings, key, range)
188
+ settings.key?(key) ? Check.integer!(settings[key], key.to_s, range: range) : nil
189
+ end
190
+ private_class_method :optional_integer
191
+
192
+ def optional_id(settings, key)
193
+ settings.key?(key) ? Check.logical_id!(settings[key], key.to_s) : nil
194
+ end
195
+ private_class_method :optional_id
196
+
197
+ def install_dir(value)
198
+ text = value.to_s
199
+ unless text.match?(%r{\A/[A-Za-z0-9_./-]*[A-Za-z0-9_.-]\z})
200
+ raise ArgumentError,
201
+ "install_dir must be an absolute path without a trailing slash, got #{value.inspect}"
202
+ end
203
+
204
+ text
205
+ end
206
+ private_class_method :install_dir
207
+
208
+ def build_context_dir(value)
209
+ text = value.to_s
210
+ return text if text.empty? || text.match?(%r{\A[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*/\z})
211
+
212
+ raise ArgumentError,
213
+ "build_context_dir must be a relative directory ending in /, such as \"build/\", got #{value.inspect}"
214
+ end
215
+ private_class_method :build_context_dir
216
+
217
+ def count_parameter_yaml(plan, default_count)
218
+ return "" unless plan.desired_count_parameter
219
+
220
+ "#{plan.desired_count_parameter}:\n Type: Number\n Default: #{default_count}\n"
221
+ end
222
+ private_class_method :count_parameter_yaml
223
+
224
+ def db_name_parameter_yaml(plan, shared_db_name)
225
+ return "" unless plan.db_name_parameter
226
+
227
+ "#{plan.db_name_parameter}:\n Type: String\n Default: #{Yaml.string(shared_db_name)}\n " \
228
+ "Description: The Postgres database name on the shared instance.\n"
229
+ end
230
+ private_class_method :db_name_parameter_yaml
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,116 @@
1
+ module Hecks
2
+ module Projections
3
+ module Deploy
4
+ module Fargate
5
+ # Text helpers the Fargate template generator renders YAML with.
6
+ #
7
+ # The template is built as text, not as a data structure, so the
8
+ # comments that explain each resource survive into the generated file.
9
+ # These helpers cover the three things text rendering needs: writing a
10
+ # setting's value as a YAML scalar, indenting a flush-left block, and
11
+ # splicing a block into a marker line of the surrounding template.
12
+ module Yaml
13
+ module_function
14
+
15
+ # Writes one value as a YAML scalar.
16
+ #
17
+ # A string that starts with `!` is a CloudFormation intrinsic
18
+ # (`!Ref Name`, `!Sub "..."`, `!GetAtt Name.Arn`) and is written
19
+ # verbatim. Any other string is double-quoted, so a value such as
20
+ # `"1"` stays a string. Numbers and booleans are written bare.
21
+ #
22
+ # @param value [String, Integer, Float, true, false] the value to render
23
+ # @return [String] the YAML text for the value, on one line
24
+ def scalar(value)
25
+ case value
26
+ when true, false, Integer, Float then value.to_s
27
+ else
28
+ text = value.to_s
29
+ text.start_with?("!") ? text : text.to_json
30
+ end
31
+ end
32
+
33
+ # Writes a value as a scalar that must stay a string.
34
+ #
35
+ # Environment values and container settings such as a port are
36
+ # strings to `ECS` however they are typed in the world file, so a
37
+ # number is quoted here where `scalar` would leave it bare.
38
+ #
39
+ # @param value [Object] the value to render
40
+ # @return [String] the YAML text for the value, on one line
41
+ def string(value)
42
+ text = value.to_s
43
+ text.start_with?("!") ? text : text.to_json
44
+ end
45
+
46
+ # Writes a list as a YAML flow sequence.
47
+ #
48
+ # A string made of a word (an id, an HTTP method) is left bare; any
49
+ # other string is written with `scalar`, so a value YAML would read
50
+ # as a number, a boolean or null stays a string.
51
+ #
52
+ # @param values [Array<Object>] the items, each rendered with `scalar` unless it is a
53
+ # plain word
54
+ # @return [String] the `[a, b]` text
55
+ def flow_list(values)
56
+ "[#{values.map { |value| plain_word?(value) ? value : scalar(value) }.join(', ')}]"
57
+ end
58
+
59
+ def plain_word?(value)
60
+ value.is_a?(String) && value.match?(/\A[A-Za-z][A-Za-z0-9_.-]*\z/) && !%w[true false null yes no on
61
+ off].include?(value.downcase)
62
+ end
63
+ private_class_method :plain_word?
64
+
65
+ # Indents every non-blank line of a block.
66
+ #
67
+ # @param text [String] the block to indent
68
+ # @param base [String] the whitespace to put in front of each non-blank line
69
+ # @return [String] the block, indented, with blank lines left empty
70
+ def indent(text, base)
71
+ text.each_line.map { |line| line.strip.empty? ? line : "#{base}#{line}" }.join
72
+ end
73
+
74
+ # Replaces one `# TMPL:<marker>` line of a template with a block.
75
+ #
76
+ # The block is written flush-left and takes the marker line's own
77
+ # indentation. An empty block removes the marker line completely, so
78
+ # a template that uses none of the optional sections renders exactly
79
+ # as it did before those sections existed.
80
+ #
81
+ # @param template [String] the template holding the marker line
82
+ # @param marker [String] the marker name, without the `# TMPL:` prefix
83
+ # @param text [String] the flush-left block to splice in, ending in a newline or empty
84
+ # @return [String] the template with the marker line replaced
85
+ def splice(template, marker, text)
86
+ template.sub(/^([ \t]*)# TMPL:#{Regexp.escape(marker)}\n/) do
87
+ text.empty? ? "" : indent(text, Regexp.last_match(1))
88
+ end
89
+ end
90
+
91
+ # Turns a name such as `mock-payments` into a CloudFormation-safe
92
+ # logical id part: `MockPayments`.
93
+ #
94
+ # @param name [String] a name made of letters, digits, `_` and `-`
95
+ # @return [String] the name in camel case, letters and digits only
96
+ def camel(name)
97
+ name.to_s.split(/[^a-zA-Z0-9]+/).reject(&:empty?).map { |part| part[0].upcase + part[1..] }.join
98
+ end
99
+
100
+ # Finds keys declared more than once at one indentation in a section.
101
+ #
102
+ # YAML loaders keep the last of a repeated key without complaint, so
103
+ # two resources given the same logical id would silently drop one.
104
+ #
105
+ # @param section [String] the text of a `Resources:` or `Parameters:` section
106
+ # @param indent [Integer] the indentation of the keys to check
107
+ # @return [Array<String>] the keys that appear more than once
108
+ def duplicate_keys(section, indent: 2)
109
+ keys = section.scan(/^ {#{indent}}([A-Za-z0-9]+):(?:\s|$)/).flatten
110
+ keys.tally.select { |_key, count| count > 1 }.keys
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end