hecks 2.6.0 → 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 (82) 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/bluebook/dsl/bootstrap_table.rb +2 -0
  6. data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
  7. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
  8. data/lib/hecks/bluebook/dsl/world_builder.rb +42 -3
  9. data/lib/hecks/bluebook/hexagon.rb +15 -7
  10. data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
  11. data/lib/hecks/bluebook/model_check.rb +1 -1
  12. data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
  13. data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
  14. data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
  15. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
  16. data/lib/hecks/embryonaut_bluebook.rb +29 -3
  17. data/lib/hecks/forms/value_object_shape.rb +1 -1
  18. data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
  19. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  20. data/lib/hecks/fuzzing/isolated_boot.rb +59 -5
  21. data/lib/hecks/language/world/world.bluebook +32 -2
  22. data/lib/hecks/ports/key_vault.rb +1 -1
  23. data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
  24. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +2 -1
  25. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
  26. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
  27. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +1 -1
  28. data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
  29. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
  30. data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
  31. data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
  32. data/lib/hecks/ports/persistence.rb +4 -0
  33. data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
  34. data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
  35. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
  36. data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
  37. data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
  38. data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
  39. data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
  40. data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
  41. data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
  42. data/lib/hecks/projections/deploy/fargate.rb +187 -138
  43. data/lib/hecks/projections/deploy/lambda.rb +5 -5
  44. data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
  45. data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
  46. data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
  47. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
  48. data/lib/hecks/projections/deploy/preview/script.rb +40 -0
  49. data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
  50. data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
  51. data/lib/hecks/projections/deploy/preview/template.rb +247 -0
  52. data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
  53. data/lib/hecks/projections/deploy/preview.rb +103 -0
  54. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
  55. data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
  56. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
  57. data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
  58. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
  59. data/lib/hecks/projections/deploy/scripts.rb +171 -0
  60. data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
  61. data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
  62. data/lib/hecks/projections/deploy/smoke.rb +159 -0
  63. data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
  64. data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
  65. data/lib/hecks/projections/deploy/template_diff.rb +173 -0
  66. data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
  67. data/lib/hecks/projections.rb +2 -0
  68. data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
  69. data/lib/hecks/release/gem_pin.rb +112 -0
  70. data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
  71. data/lib/hecks/runtime/loader.rb +26 -2
  72. data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
  73. data/lib/hecks/runtime/registry/verification.rb +4 -2
  74. data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
  75. data/lib/hecks/runtime/registry.rb +14 -9
  76. data/lib/hecks/runtime/tenant_check.rb +1 -1
  77. data/lib/hecks/vendoring/git_environment.rb +35 -0
  78. data/lib/hecks/vendoring/git_source.rb +102 -0
  79. data/lib/hecks/vendoring.rb +102 -0
  80. data/lib/hecks/version.rb +1 -1
  81. data/lib/hecks.rb +2 -1
  82. metadata +45 -1
@@ -0,0 +1,145 @@
1
+ require_relative "yaml_text"
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Deploy
6
+ module Preview
7
+ # The secrets and IAM roles of a preview stack.
8
+ #
9
+ # The task role reads the shared database secret and this preview's own
10
+ # secrets and nothing owned by the main stack. The execution role also reads
11
+ # the database secret, only so the one-shot task can receive the credentials
12
+ # as container secrets.
13
+ module AccessYaml
14
+ extend YamlText
15
+
16
+ module_function
17
+
18
+ # Renders the secrets a preview generates: its session secret and the ones
19
+ # containers declare.
20
+ #
21
+ # @param settings [Settings] the resolved preview settings
22
+ # @return [Array<String>] one YAML resource block per secret
23
+ def secrets(settings)
24
+ [session_secret, *settings.containers.flat_map { |c| container_secrets(c) }]
25
+ end
26
+
27
+ # Renders the per-branch session secret the host reads at start.
28
+ #
29
+ # @return [String] the secret's resource block
30
+ def session_secret
31
+ <<~YAML.chomp
32
+ # No explicit Name: Secrets Manager keeps a deleted secret through its recovery window,
33
+ # and a fixed name would make destroy followed by deploy on one branch fail.
34
+ SessionSecret:
35
+ Type: AWS::SecretsManager::Secret
36
+ Properties:
37
+ Description: !Sub "Session secret of the ${EnvName} preview."
38
+ GenerateSecretString:
39
+ SecretStringTemplate: '{}'
40
+ GenerateStringKey: session_secret
41
+ PasswordLength: 64
42
+ ExcludePunctuation: true
43
+ YAML
44
+ end
45
+
46
+ # Renders one generated secret per name a container declares.
47
+ #
48
+ # @param container [Containers::Entry] the container
49
+ # @return [Array<String>] one resource block per declared secret name
50
+ def container_secrets(container)
51
+ container.secrets.map do |name|
52
+ <<~YAML.chomp
53
+ #{secret_id(container, name)}:
54
+ Type: AWS::SecretsManager::Secret
55
+ Properties:
56
+ Description: !Sub "#{name} of the ${EnvName} preview's #{container.name} container."
57
+ GenerateSecretString:
58
+ SecretStringTemplate: '{}'
59
+ GenerateStringKey: secret
60
+ PasswordLength: 64
61
+ ExcludePunctuation: true
62
+ YAML
63
+ end
64
+ end
65
+
66
+ # Renders the task execution role and the task role.
67
+ #
68
+ # @param settings [Settings] the resolved preview settings
69
+ # @return [Array<String>] the two role resource blocks
70
+ def roles(settings)
71
+ assume = <<~YAML.chomp
72
+ AssumeRolePolicyDocument:
73
+ Version: '2012-10-17'
74
+ Statement:
75
+ - Effect: Allow
76
+ Principal: { Service: ecs-tasks.amazonaws.com }
77
+ Action: sts:AssumeRole
78
+ YAML
79
+ [execution_role(assume), task_role(assume, settings)]
80
+ end
81
+
82
+ # Renders the execution role, which pulls images and hands the one-shot task
83
+ # its credentials.
84
+ #
85
+ # @param assume [String] the shared trust policy block
86
+ # @return [String] the role's resource block
87
+ def execution_role(assume)
88
+ <<~YAML.chomp
89
+ # The execution role reads the database secret only so the one-shot task can be given
90
+ # its credentials as container secrets.
91
+ ExecutionRole:
92
+ Type: AWS::IAM::Role
93
+ Properties:
94
+ #{indent(assume, 4)}
95
+ ManagedPolicyArns:
96
+ - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
97
+ Policies:
98
+ - PolicyName: SharedDatabaseSecretRead
99
+ PolicyDocument:
100
+ Version: '2012-10-17'
101
+ Statement:
102
+ - Effect: Allow
103
+ Action: secretsmanager:GetSecretValue
104
+ Resource: !Ref OwningDatabaseSecretArn
105
+ YAML
106
+ end
107
+
108
+ # Renders the task role: read access to the shared database secret and this
109
+ # preview's own secrets.
110
+ #
111
+ # @param assume [String] the shared trust policy block
112
+ # @param settings [Settings] the resolved preview settings
113
+ # @return [String] the role's resource block
114
+ def task_role(assume, settings)
115
+ readable = ["SessionSecret", *settings.containers.flat_map { |c| c.secrets.map { |n| secret_id(c, n) } }]
116
+ <<~YAML.chomp
117
+ # Reads the shared database secret (it connects with it and creates its database) and
118
+ # this preview's own secrets; nothing owned by the main stack.
119
+ TaskRole:
120
+ Type: AWS::IAM::Role
121
+ Properties:
122
+ #{indent(assume, 4)}
123
+ Policies:
124
+ - PolicyName: SharedDatabaseSecretRead
125
+ PolicyDocument:
126
+ Version: '2012-10-17'
127
+ Statement:
128
+ - Effect: Allow
129
+ Action: secretsmanager:GetSecretValue
130
+ Resource: !Ref OwningDatabaseSecretArn
131
+ - PolicyName: PreviewSecretRead
132
+ PolicyDocument:
133
+ Version: '2012-10-17'
134
+ Statement:
135
+ - Effect: Allow
136
+ Action: secretsmanager:GetSecretValue
137
+ Resource:
138
+ #{indent(readable.map { |id| "- !Ref #{id}" }.join("\n"), 16)}
139
+ YAML
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,218 @@
1
+ module Hecks
2
+ module Projections
3
+ module Deploy
4
+ module Preview
5
+ # The containers one preview task runs, in one normalized shape.
6
+ #
7
+ # A preview task runs the same containers as the stack it previews.
8
+ # This module is the single place that decides which containers those
9
+ # are, so the rest of `Preview` never asks where the list came from.
10
+ #
11
+ # ## One adapter to the shared setting
12
+ #
13
+ # The main Fargate stack is growing a multi-container setting.
14
+ # `shared_setting` is the only method that reads it; every other
15
+ # method here works on the normalized entries described below. When
16
+ # that setting lands with a different shape, translate it inside
17
+ # `shared_setting` and nothing else changes.
18
+ #
19
+ # ## Entry keys
20
+ #
21
+ # Each entry is a Hash with Symbol keys:
22
+ #
23
+ # - `name` (required): lowercase letters, digits and hyphens; also the
24
+ # container name and the suffix of its ECR repository.
25
+ # - `port` (required): the port the container listens on. Unique
26
+ # across the task, since all containers share one network namespace.
27
+ # - `health_check_path`: target group health check path, default `"/"`.
28
+ # - `paths`: load balancer path patterns forwarded to this container.
29
+ # - `default`: `true` sends every unmatched request here. When no
30
+ # entry says so, the `host` entry is the default, else the first.
31
+ # - `host`: `true` marks the Hecks host container. It gets the domain,
32
+ # session and database environment, and the signup path.
33
+ # - `database`: `true` gives a non-host container the database
34
+ # environment as well. The host always has it.
35
+ # - `environment`: extra environment, name to value. `{{preview_url}}`
36
+ # inside a value becomes the preview's public https URL.
37
+ # - `secrets`: environment variable names. Each becomes a per-branch
38
+ # generated secret whose ARN is that variable's value.
39
+ # - `image`: the local docker image `preview.sh` pushes for this
40
+ # container, default `"<name>:latest"`.
41
+ module Containers
42
+ # One normalized container of the preview task.
43
+ Entry = Data.define(:name, :port, :health_check_path, :paths, :default, :host, :database,
44
+ :environment, :secrets, :image) do
45
+ # Answers the CloudFormation logical-id fragment for this container.
46
+ #
47
+ # @return [String] `name` in CamelCase, such as `"MySite"` for `"my-site"`
48
+ def logical = name.split("-").map(&:capitalize).join
49
+
50
+ # Answers whether the container is reachable through the load balancer.
51
+ #
52
+ # @return [Boolean] true when it is the default or owns any path
53
+ def routed? = default || !paths.empty?
54
+ end
55
+
56
+ NAME_PATTERN = /\A[a-z][a-z0-9-]{0,29}\z/
57
+ PATH_PATTERN = %r{\A/[A-Za-z0-9._~/*?%-]*\z}
58
+ IMAGE_PATTERN = %r{\A[A-Za-z0-9._/:@-]+\z}
59
+ ENV_NAME_PATTERN = /\A[A-Z_][A-Z0-9_]*\z/
60
+
61
+ module_function
62
+
63
+ # Answers the preview task's containers, normalized and validated.
64
+ #
65
+ # @param preview [Hash{Symbol => Object}] the preview settings; `:containers` wins
66
+ # @param deploy_settings [Hash{Symbol => Object}] the `deployed_to` settings, read
67
+ # by `shared_setting`
68
+ # @param main [Hash{Symbol => Object}] the main stack's own facts; `:name`, `:port`,
69
+ # `:image` describe the single container used when neither list is given
70
+ # @param signup_path [String, nil] path forwarded to the host container, or nil
71
+ # @return [Array<Entry>] at least one entry, exactly one of them the default
72
+ # @raise [ArgumentError] if an entry is malformed, a name or port repeats, or two
73
+ # entries claim to be the default or the host
74
+ def resolve(preview:, deploy_settings:, main:, signup_path: nil)
75
+ raw = preview[:containers] || shared_setting(deploy_settings) || [single(main)]
76
+ entries = Array(raw).map { |item| normalize(item) }
77
+ check!(entries)
78
+ with_default(with_signup(entries, signup_path))
79
+ end
80
+
81
+ # Reads the main stack's own container list, when it declares one.
82
+ #
83
+ # This is the one hook to the multi-container setting shared with the
84
+ # main template; it answers entries in the shape documented above.
85
+ #
86
+ # @param deploy_settings [Hash{Symbol => Object}] the `deployed_to` settings
87
+ # @return [Array<Hash>, nil] the list, or nil when the stack runs one container
88
+ def shared_setting(deploy_settings)
89
+ deploy_settings[:containers]
90
+ end
91
+
92
+ # Builds the single-container entry a one-container stack previews as.
93
+ #
94
+ # @param main [Hash{Symbol => Object}] the main stack's facts, see `resolve`
95
+ # @return [Hash{Symbol => Object}] the entry, always the host and the default
96
+ def single(main)
97
+ name = main.fetch(:name).to_s.downcase.gsub(/[^a-z0-9]+/, "-").sub(/\A[^a-z]+/, "")[0, 30].sub(/-+\z/, "")
98
+ { name: name, port: main.fetch(:port), host: true, default: true, image: main[:image] }
99
+ end
100
+
101
+ # Turns one raw entry into an `Entry`, filling every default.
102
+ #
103
+ # @param item [Hash{Symbol => Object}] the raw entry, string keys accepted
104
+ # @return [Entry] the normalized entry
105
+ # @raise [ArgumentError] if the entry is not a Hash or lacks a valid `name` or `port`
106
+ def normalize(item)
107
+ raise ArgumentError, "preview container #{item.inspect} must be a Hash" unless item.is_a?(Hash)
108
+
109
+ item = item.transform_keys(&:to_sym)
110
+ name = matching(item[:name], NAME_PATTERN, "preview container name")
111
+ Entry.new(
112
+ name: name, port: port_of(item, name), health_check_path: path_of(item[:health_check_path] || "/"),
113
+ paths: Array(item[:paths]).map { |path| path_of(path) }, default: item[:default] == true,
114
+ host: item[:host] == true, database: item[:database] == true,
115
+ environment: environment_of(item[:environment]), secrets: Array(item[:secrets]).map(&:to_s),
116
+ image: matching(item[:image] || "#{name}:latest", IMAGE_PATTERN, "preview container #{name} image")
117
+ )
118
+ end
119
+
120
+ # Answers a value as a String, refusing one that does not match a pattern.
121
+ #
122
+ # @param value [Object] the raw value
123
+ # @param pattern [Regexp] what the string must match
124
+ # @param label [String] what the value is, for the message
125
+ # @return [String] the value as a String
126
+ # @raise [ArgumentError] if the string does not match
127
+ def matching(value, pattern, label)
128
+ text = value.to_s
129
+ raise ArgumentError, "#{label} #{text.inspect} must match #{pattern.inspect}" unless text.match?(pattern)
130
+
131
+ text
132
+ end
133
+
134
+ # Answers one path pattern, refusing one the template could not quote safely.
135
+ #
136
+ # @param path [Object] the raw path
137
+ # @return [String] the path
138
+ # @raise [ArgumentError] if it does not start with `/` or holds an unexpected character
139
+ def path_of(path) = matching(path, PATH_PATTERN, "preview container path")
140
+
141
+ # Reads and range-checks one entry's port.
142
+ #
143
+ # @param item [Hash{Symbol => Object}] the raw entry
144
+ # @param name [String] the entry's name, for the message
145
+ # @return [Integer] the port
146
+ # @raise [ArgumentError] if the port is missing or outside 1..65535
147
+ def port_of(item, name)
148
+ port = Integer(item[:port], exception: false)
149
+ raise ArgumentError, "preview container #{name} needs a port between 1 and 65535" unless port&.between?(1, 65_535)
150
+
151
+ port
152
+ end
153
+
154
+ # Stringifies an entry's environment, keeping declaration order.
155
+ #
156
+ # @param environment [Hash{Symbol, String => Object}, nil] the raw environment
157
+ # @return [Hash{String => String}] name to value
158
+ def environment_of(environment)
159
+ (environment || {}).to_h { |name, value| [name.to_s, value.to_s] }
160
+ end
161
+
162
+ # Refuses a list that cannot be one task.
163
+ #
164
+ # @param entries [Array<Entry>] the normalized entries
165
+ # @return [void]
166
+ # @raise [ArgumentError] on an empty list, a repeated name or port, more than one
167
+ # default or host, or a secret name that is not an environment variable name
168
+ def check!(entries)
169
+ raise ArgumentError, "preview containers must not be empty" if entries.empty?
170
+
171
+ %i[name port].each do |field|
172
+ dup = entries.map(&field).tally.find { |_value, count| count > 1 }
173
+ raise ArgumentError, "preview containers repeat the #{field} #{dup.first.inspect}" if dup
174
+ end
175
+ check_roles!(entries)
176
+ end
177
+
178
+ # Refuses a second default or host, and a secret that is not an environment variable name.
179
+ #
180
+ # @param entries [Array<Entry>] the normalized entries
181
+ # @return [void]
182
+ # @raise [ArgumentError] on a second default or host, or a malformed secret name
183
+ def check_roles!(entries)
184
+ raise ArgumentError, "at most one preview container may be the default" if entries.count(&:default) > 1
185
+ raise ArgumentError, "at most one preview container may be the host" if entries.count(&:host) > 1
186
+
187
+ bad = entries.flat_map(&:secrets).find { |secret| !secret.match?(ENV_NAME_PATTERN) }
188
+ raise ArgumentError, "preview secret name #{bad.inspect} must be an environment variable name" if bad
189
+ end
190
+
191
+ # Adds the signup path to the host container's own paths.
192
+ #
193
+ # @param entries [Array<Entry>] the normalized entries
194
+ # @param signup_path [String, nil] the path, or nil for none
195
+ # @return [Array<Entry>] the entries, the host's paths extended when it needs a rule
196
+ def with_signup(entries, signup_path)
197
+ return entries unless signup_path
198
+
199
+ entries.map do |entry|
200
+ entry.host && !entry.default ? entry.with(paths: (entry.paths + [signup_path]).uniq) : entry
201
+ end
202
+ end
203
+
204
+ # Marks the default container when no entry does.
205
+ #
206
+ # @param entries [Array<Entry>] the normalized entries
207
+ # @return [Array<Entry>] the entries with exactly one default
208
+ def with_default(entries)
209
+ return entries if entries.any?(&:default)
210
+
211
+ chosen = entries.find(&:host) || entries.first
212
+ entries.map { |entry| entry.equal?(chosen) ? entry.with(default: true) : entry }
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end