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.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +1 -1
- data/lib/hecks/bench/cli.rb +74 -0
- data/lib/hecks/bench/environment.rb +100 -0
- data/lib/hecks/bench/postgres_probe.rb +41 -0
- data/lib/hecks/bench/report.rb +116 -0
- data/lib/hecks/bench/ruby_runner.rb +151 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +165 -0
- data/lib/hecks/bench/stats.rb +75 -0
- data/lib/hecks/bench/suite.rb +172 -0
- data/lib/hecks/bench/workload.rb +137 -0
- data/lib/hecks/bench.rb +28 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +42 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +153 -0
- data/lib/hecks/bluebook/model_check.rb +21 -2
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +136 -69
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_stdio_guard.rb +160 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +2 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +187 -138
- data/lib/hecks/projections/deploy/lambda.rb +5 -5
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/loader.rb +26 -2
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +4 -2
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +14 -9
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +3 -1
- metadata +59 -2
|
@@ -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
|