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,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
|