hecks 2.5.1 → 2.7.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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
  3. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  4. data/lib/hecks/adapters/driven/prism.rb +1 -1
  5. data/lib/hecks/bench/cli.rb +74 -0
  6. data/lib/hecks/bench/environment.rb +100 -0
  7. data/lib/hecks/bench/postgres_probe.rb +41 -0
  8. data/lib/hecks/bench/report.rb +116 -0
  9. data/lib/hecks/bench/ruby_runner.rb +151 -0
  10. data/lib/hecks/bench/run.rb +55 -0
  11. data/lib/hecks/bench/rust_runner.rb +165 -0
  12. data/lib/hecks/bench/stats.rb +75 -0
  13. data/lib/hecks/bench/suite.rb +172 -0
  14. data/lib/hecks/bench/workload.rb +137 -0
  15. data/lib/hecks/bench.rb +28 -0
  16. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  17. data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
  18. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
  19. data/lib/hecks/bluebook/dsl/world_builder.rb +42 -3
  20. data/lib/hecks/bluebook/hexagon.rb +15 -7
  21. data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
  22. data/lib/hecks/bluebook/model_check/client_profile.rb +153 -0
  23. data/lib/hecks/bluebook/model_check.rb +21 -2
  24. data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
  25. data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
  26. data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
  27. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
  28. data/lib/hecks/embryonaut_bluebook.rb +29 -3
  29. data/lib/hecks/forms/value_object_shape.rb +1 -1
  30. data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
  31. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  32. data/lib/hecks/fuzzing/isolated_boot.rb +136 -69
  33. data/lib/hecks/language/world/world.bluebook +32 -2
  34. data/lib/hecks/mcp_stdio_guard.rb +160 -0
  35. data/lib/hecks/ports/key_vault.rb +1 -1
  36. data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
  37. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +2 -1
  38. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
  39. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
  40. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +1 -1
  41. data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
  42. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
  43. data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
  44. data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
  45. data/lib/hecks/ports/persistence.rb +4 -0
  46. data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
  47. data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
  48. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
  49. data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
  50. data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
  51. data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
  52. data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
  53. data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
  54. data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
  55. data/lib/hecks/projections/deploy/fargate.rb +187 -138
  56. data/lib/hecks/projections/deploy/lambda.rb +5 -5
  57. data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
  58. data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
  59. data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
  60. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
  61. data/lib/hecks/projections/deploy/preview/script.rb +40 -0
  62. data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
  63. data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
  64. data/lib/hecks/projections/deploy/preview/template.rb +247 -0
  65. data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
  66. data/lib/hecks/projections/deploy/preview.rb +103 -0
  67. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
  68. data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
  69. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
  70. data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
  71. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
  72. data/lib/hecks/projections/deploy/scripts.rb +171 -0
  73. data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
  74. data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
  75. data/lib/hecks/projections/deploy/smoke.rb +159 -0
  76. data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
  77. data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
  78. data/lib/hecks/projections/deploy/template_diff.rb +173 -0
  79. data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
  80. data/lib/hecks/projections.rb +2 -0
  81. data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
  82. data/lib/hecks/release/gem_pin.rb +112 -0
  83. data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
  84. data/lib/hecks/runtime/loader.rb +26 -2
  85. data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
  86. data/lib/hecks/runtime/registry/verification.rb +4 -2
  87. data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
  88. data/lib/hecks/runtime/registry.rb +14 -9
  89. data/lib/hecks/runtime/tenant_check.rb +1 -1
  90. data/lib/hecks/vendoring/git_environment.rb +35 -0
  91. data/lib/hecks/vendoring/git_source.rb +102 -0
  92. data/lib/hecks/vendoring.rb +102 -0
  93. data/lib/hecks/version.rb +1 -1
  94. data/lib/hecks.rb +3 -1
  95. metadata +59 -2
@@ -0,0 +1,268 @@
1
+ require_relative "check"
2
+ require_relative "yaml"
3
+
4
+ module Hecks
5
+ module Projections
6
+ module Deploy
7
+ module Fargate
8
+ # The extra resources, parameters, outputs and IAM grants a stack can
9
+ # declare beyond the ones the generator always makes.
10
+ #
11
+ # ## Settings
12
+ #
13
+ # - `buckets`: S3 buckets, each `{id:, name_prefix:, public_read:, cors_origins:}`. The
14
+ # bucket is named `<name_prefix>-<account id>`. `public_read` opens the bucket to
15
+ # anonymous `GetObject`. `cors_origins` allows those origins to `GET` from a browser.
16
+ # - `generated_secrets`: Secrets Manager secrets with a generated value, each
17
+ # `{id:, name:, description:, key:, length:}`. `key` is the JSON field the value is
18
+ # stored under (`secret`).
19
+ # - `session_secret`: `{name:, description:}` names the always-created session secret
20
+ # and describes it.
21
+ # - `task_policies` and `execution_policies`: extra IAM policies for the task role (what
22
+ # the containers may do) and the execution role (what `ECS` may do to start them).
23
+ # Each is `{name:, statements: [{effect:, actions:, resources:}]}`; `effect` is `Allow`
24
+ # by default and resources may be intrinsics.
25
+ # - `parameters`: extra template parameters, a map of name to
26
+ # `{type:, default:, description:, no_echo:}`.
27
+ # - `outputs`: extra template outputs, a map of name to a value or intrinsic.
28
+ module Extras
29
+ module_function
30
+
31
+ BUCKET_KEYS = [:id, :name_prefix, :public_read, :cors_origins].freeze
32
+ SECRET_KEYS = [:id, :name, :description, :key, :length].freeze
33
+ POLICY_KEYS = [:name, :statements].freeze
34
+ STATEMENT_KEYS = [:effect, :actions, :resources].freeze
35
+ PARAMETER_KEYS = [:type, :default, :description, :no_echo].freeze
36
+ PARAMETER_TYPE = /\A(String|Number|CommaDelimitedList|List<Number>|AWS::[A-Za-z0-9:]+|AWS::SSM::Parameter::Value<[^>]+>)\z/
37
+ EXEC_ACTIONS = %w[ssmmessages:CreateControlChannel ssmmessages:CreateDataChannel
38
+ ssmmessages:OpenControlChannel ssmmessages:OpenDataChannel].freeze
39
+
40
+ # Reads and checks the resource-declaring settings.
41
+ #
42
+ # @param settings [Hash{Symbol => Object}] the world's `deployed_to("AwsFargate")`
43
+ # settings
44
+ # @return [Hash{Symbol => Object}] `:buckets`, `:secrets`, `:session_secret`,
45
+ # `:task_policies`,
46
+ # `:execution_policies`, `:parameters` and `:outputs`, each checked and defaulted to
47
+ # empty
48
+ # @raise [ArgumentError] if any of those settings is malformed
49
+ def normalize(settings)
50
+ {
51
+ buckets: buckets(settings.fetch(:buckets, [])),
52
+ secrets: secrets(settings.fetch(:generated_secrets, [])),
53
+ session_secret: session_secret(settings[:session_secret]),
54
+ task_policies: policies(settings.fetch(:task_policies, []), "task_policies"),
55
+ execution_policies: policies(settings.fetch(:execution_policies, []), "execution_policies"),
56
+ parameters: parameters(settings.fetch(:parameters, {})),
57
+ outputs: outputs(settings.fetch(:outputs, {}))
58
+ }
59
+ end
60
+
61
+ # Renders the buckets and generated secrets.
62
+ #
63
+ # @param extras [Hash] the checked settings from `normalize`
64
+ # @return [String] flush-left resources separated by blank lines, or empty
65
+ def resources_yaml(extras)
66
+ (extras[:buckets].flat_map { |bucket| bucket_blocks(bucket) } + extras[:secrets].map do |secret|
67
+ secret_yaml(secret)
68
+ end).join("\n\n")
69
+ end
70
+
71
+ # Renders extra policies as entries of a role's `Policies` list.
72
+ #
73
+ # @param policies [Array<Hash>] the checked policies
74
+ # @return [String] flush-left list entries, or empty
75
+ def policies_yaml(policies)
76
+ policies.map { |policy| policy_yaml(policy) }.join
77
+ end
78
+
79
+ # Renders the policy that lets `aws ecs execute-command` open a session in a container.
80
+ #
81
+ # @return [String] one flush-left `Policies` list entry
82
+ def execute_command_policy_yaml
83
+ policy_yaml(
84
+ name: "EcsExecDebug",
85
+ statements: [{ effect: "Allow", actions: EXEC_ACTIONS, resources: ["*"] }]
86
+ )
87
+ end
88
+
89
+ # Renders the extra template parameters.
90
+ #
91
+ # @param parameters [Hash{String => Hash}] the checked parameters
92
+ # @return [String] flush-left parameters, or empty
93
+ def parameters_yaml(parameters)
94
+ parameters.map do |name, given|
95
+ lines = ["#{name}:", " Type: #{given[:type]}"]
96
+ lines << " Default: #{Yaml.scalar(given[:default])}" if given.key?(:default)
97
+ lines << " Description: #{Yaml.string(given[:description])}" if given[:description]
98
+ lines << " NoEcho: true" if given[:no_echo]
99
+ "#{lines.join("\n")}\n"
100
+ end.join
101
+ end
102
+
103
+ # Renders the extra template outputs.
104
+ #
105
+ # @param outputs [Hash{String => Object}] the checked outputs
106
+ # @return [String] flush-left outputs, or empty
107
+ def outputs_yaml(outputs)
108
+ outputs.map { |name, value| "#{name}:\n Value: #{Yaml.string(value)}\n" }.join
109
+ end
110
+
111
+ def buckets(entries)
112
+ Check.hashes!(entries, "buckets", allowed: BUCKET_KEYS, required: [:id, :name_prefix]).map do |entry|
113
+ {
114
+ id: Check.logical_id!(entry[:id], "buckets id"),
115
+ name_prefix: Check.resource_name!(entry[:name_prefix], "buckets name_prefix").downcase,
116
+ public_read: Check.boolean!(entry.fetch(:public_read, false), "buckets public_read"),
117
+ cors_origins: Check.strings!(entry.fetch(:cors_origins, []), "buckets cors_origins")
118
+ }
119
+ end
120
+ end
121
+ private_class_method :buckets
122
+
123
+ def secrets(entries)
124
+ Check.hashes!(entries, "generated_secrets", allowed: SECRET_KEYS, required: [:id, :name]).map do |entry|
125
+ {
126
+ id: Check.logical_id!(entry[:id], "generated_secrets id"),
127
+ name: Check.resource_name!(entry[:name], "generated_secrets name"),
128
+ description: entry[:description]&.to_s,
129
+ key: Check.resource_name!(entry.fetch(:key, "secret"), "generated_secrets key"),
130
+ length: Check.integer!(entry.fetch(:length, 64), "generated_secrets length", range: 8..512)
131
+ }
132
+ end
133
+ end
134
+ private_class_method :secrets
135
+
136
+ def session_secret(value)
137
+ return {} if value.nil?
138
+
139
+ given = Check.hash!(value, "session_secret", allowed: [:name, :description])
140
+ { name: given[:name] && Check.resource_name!(given[:name], "session_secret name"),
141
+ description: given[:description]&.to_s }.compact
142
+ end
143
+ private_class_method :session_secret
144
+
145
+ def policies(entries, where)
146
+ Check.hashes!(entries, where, allowed: POLICY_KEYS, required: [:name, :statements]).map do |entry|
147
+ statements = Check.hashes!(entry[:statements], "#{where}[#{entry[:name]}] statements",
148
+ allowed: STATEMENT_KEYS, required: [:actions, :resources])
149
+ {
150
+ name: Check.logical_id!(entry[:name], "#{where} name"),
151
+ statements: statements.map do |statement|
152
+ {
153
+ effect: Check.one_of!(statement.fetch(:effect, "Allow"), "#{where} effect", %w[Allow Deny]),
154
+ actions: Check.strings!(statement[:actions], "#{where} actions", min: 1),
155
+ resources: Check.strings!(statement[:resources], "#{where} resources", min: 1)
156
+ }
157
+ end
158
+ }
159
+ end
160
+ end
161
+ private_class_method :policies
162
+
163
+ def parameters(map)
164
+ raise ArgumentError, "parameters must be a hash of name to settings, got #{map.inspect}" unless map.is_a?(Hash)
165
+
166
+ map.to_h do |name, entry|
167
+ key = Check.logical_id!(name, "parameters name")
168
+ given = Check.hash!(entry, "parameters.#{key}", allowed: PARAMETER_KEYS, required: [:type])
169
+ unless PARAMETER_TYPE.match?(given[:type].to_s)
170
+ raise ArgumentError,
171
+ "parameters.#{key} type #{given[:type].inspect} is not a CloudFormation parameter type"
172
+ end
173
+
174
+ [key, given.merge(type: given[:type].to_s, no_echo: given[:no_echo] ? true : false)]
175
+ end
176
+ end
177
+ private_class_method :parameters
178
+
179
+ def outputs(map)
180
+ raise ArgumentError, "outputs must be a hash of name to value, got #{map.inspect}" unless map.is_a?(Hash)
181
+
182
+ map.to_h do |name, value|
183
+ raise ArgumentError, "outputs.#{name} needs a value" if value.nil?
184
+
185
+ [Check.logical_id!(name, "outputs name"), value]
186
+ end
187
+ end
188
+ private_class_method :outputs
189
+
190
+ def bucket_blocks(bucket)
191
+ blocks = [bucket_yaml(bucket)]
192
+ blocks << bucket_policy_yaml(bucket) if bucket[:public_read]
193
+ blocks
194
+ end
195
+ private_class_method :bucket_blocks
196
+
197
+ def bucket_yaml(bucket)
198
+ lines = ["#{bucket[:id]}:", " Type: AWS::S3::Bucket", " Properties:",
199
+ " BucketName: !Sub \"#{bucket[:name_prefix]}-${AWS::AccountId}\""]
200
+ lines.concat(public_access_lines) if bucket[:public_read]
201
+ lines.concat(cors_lines(bucket[:cors_origins])) unless bucket[:cors_origins].empty?
202
+ lines.join("\n")
203
+ end
204
+ private_class_method :bucket_yaml
205
+
206
+ def public_access_lines
207
+ [" PublicAccessBlockConfiguration:", " BlockPublicAcls: false", " BlockPublicPolicy: false",
208
+ " IgnorePublicAcls: false", " RestrictPublicBuckets: false"]
209
+ end
210
+ private_class_method :public_access_lines
211
+
212
+ def cors_lines(origins)
213
+ [" CorsConfiguration:", " CorsRules:", " - AllowedOrigins: #{Yaml.flow_list(origins)}",
214
+ " AllowedMethods: [GET]", " AllowedHeaders: [\"*\"]"]
215
+ end
216
+ private_class_method :cors_lines
217
+
218
+ def bucket_policy_yaml(bucket)
219
+ <<~POLICY.rstrip
220
+ #{bucket[:id]}Policy:
221
+ Type: AWS::S3::BucketPolicy
222
+ Properties:
223
+ Bucket: !Ref #{bucket[:id]}
224
+ PolicyDocument:
225
+ Version: "2012-10-17"
226
+ Statement:
227
+ - Effect: Allow
228
+ Principal: "*"
229
+ Action: s3:GetObject
230
+ Resource: !Sub "${#{bucket[:id]}.Arn}/*"
231
+ POLICY
232
+ end
233
+ private_class_method :bucket_policy_yaml
234
+
235
+ def secret_yaml(secret)
236
+ lines = ["#{secret[:id]}:", " Type: AWS::SecretsManager::Secret", " Properties:", " Name: #{secret[:name]}"]
237
+ lines << " Description: #{Yaml.string(secret[:description])}" if secret[:description]
238
+ lines.push(" GenerateSecretString:", " SecretStringTemplate: '{}'",
239
+ " GenerateStringKey: #{secret[:key]}", " PasswordLength: #{secret[:length]}",
240
+ " ExcludePunctuation: true")
241
+ lines.join("\n")
242
+ end
243
+ private_class_method :secret_yaml
244
+
245
+ def policy_yaml(policy)
246
+ lines = ["- PolicyName: #{policy[:name]}", " PolicyDocument:", " Version: '2012-10-17'", " Statement:"]
247
+ policy[:statements].each do |statement|
248
+ lines << " - Effect: #{statement[:effect]}"
249
+ lines.concat(statement_list("Action", statement[:actions]))
250
+ lines.concat(statement_list("Resource", statement[:resources].map { |resource| Yaml.string(resource) }))
251
+ end
252
+ "#{lines.join("\n")}\n"
253
+ end
254
+ private_class_method :policy_yaml
255
+
256
+ # One entry is written as a scalar, the form IAM policies are usually written in; more are
257
+ # a list.
258
+ def statement_list(key, values)
259
+ return [" #{key}: #{values.first}"] if values.one?
260
+
261
+ [" #{key}:"] + values.map { |value| " - #{value}" }
262
+ end
263
+ private_class_method :statement_list
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end