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,446 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# GENERATED by bin/project_deploy - re-run it to refresh this file rather than
|
|
3
|
+
# hand-editing. Source: the `preview` block of the domain's deployed_to("AwsFargate").
|
|
4
|
+
#
|
|
5
|
+
# Feature-branch previews of <%= s.infra_name %>: one isolated stack per git branch,
|
|
6
|
+
# named from the branch, no flags to remember.
|
|
7
|
+
#
|
|
8
|
+
# preview.sh deploy create or update THIS branch's preview
|
|
9
|
+
# preview.sh destroy tear it down again (PREVIEW_DROP_DATABASE=1 also drops its database)
|
|
10
|
+
# preview.sh list every preview that exists
|
|
11
|
+
# preview.sh url this branch's URL
|
|
12
|
+
# preview.sh name what this branch's preview is called (no AWS calls)
|
|
13
|
+
# preview.sh ensure-database create this branch's database if it is missing
|
|
14
|
+
<% if s.first_admin -%>
|
|
15
|
+
# preview.sh login a browser-console line that signs you in as its first admin
|
|
16
|
+
<% end -%>
|
|
17
|
+
#
|
|
18
|
+
# `deploy` pushes images that are already built locally (<%= image_hint %>), so it deploys
|
|
19
|
+
# your WORKING TREE, not the last commit; the Commit stack tag says which, with -dirty
|
|
20
|
+
# when there are uncommitted changes. Build them first with `make docker-build`.
|
|
21
|
+
#
|
|
22
|
+
# It cannot touch the live stack: it only ever operates on stacks named
|
|
23
|
+
# <%= s.prefix %>-*, it refuses <%= s.protected_branches.join(', ') %>, and preview.yaml is a
|
|
24
|
+
# separate template from template.yaml.
|
|
25
|
+
#
|
|
26
|
+
# Needs: aws, docker, git, curl<% if s.first_admin %>, python3 (for the signup and login tokens)<% end %>.
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
cd "$(dirname "${BASH_SOURCE[0]}")"
|
|
29
|
+
|
|
30
|
+
REGION="<%= s.region %>"
|
|
31
|
+
OWNER_STACK="<%= s.owner_stack %>"
|
|
32
|
+
DATABASE_STACK="<%= s.database_stack %>"
|
|
33
|
+
DATABASE_ENDPOINT_OUTPUT="<%= s.database_endpoint_output %>"
|
|
34
|
+
DATABASE_SECRET_OUTPUT="<%= s.database_secret_output %>"
|
|
35
|
+
PREFIX="<%= s.prefix %>"
|
|
36
|
+
DB_PREFIX="<%= s.db_prefix %>"
|
|
37
|
+
SERVICES="<%= s.containers.map(&:name).join(' ') %>"
|
|
38
|
+
PROTECTED_BRANCHES="<%= s.protected_branches.join(' ') %>"
|
|
39
|
+
SIGNUP_PATH="<%= s.signup_path %>"
|
|
40
|
+
LANDING_PATH="<%= s.landing_path %>"
|
|
41
|
+
SESSION_COOKIE="<%= s.session_cookie || 'hecks_session' %>"
|
|
42
|
+
|
|
43
|
+
die() { echo "error: $*" >&2; exit 1; }
|
|
44
|
+
warn() { echo "warning: $*" >&2; }
|
|
45
|
+
|
|
46
|
+
# The local docker image `deploy` pushes for one container.
|
|
47
|
+
local_image() {
|
|
48
|
+
case "$1" in
|
|
49
|
+
<% s.containers.each do |c| -%>
|
|
50
|
+
<%= c.name %>) echo "<%= c.image %>" ;;
|
|
51
|
+
<% end -%>
|
|
52
|
+
*) die "no local image is declared for '$1'" ;;
|
|
53
|
+
esac
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# The CloudFormation image-tag parameter of one container.
|
|
57
|
+
tag_parameter() {
|
|
58
|
+
case "$1" in
|
|
59
|
+
<% s.containers.each do |c| -%>
|
|
60
|
+
<%= c.name %>) echo "<%= c.logical %>ImageTag" ;;
|
|
61
|
+
<% end -%>
|
|
62
|
+
esac
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# ---- naming: everything derives from the branch ---------------------------
|
|
66
|
+
|
|
67
|
+
# The 5-character hash that keeps two branches from sharing a stack.
|
|
68
|
+
hash5() {
|
|
69
|
+
printf '%s' "$1" | { shasum 2>/dev/null || sha1sum; } | cut -c1-5
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Lowercase, non-alphanumerics to hyphens. Anything that changed on the way (a
|
|
73
|
+
# slash, an underscore, truncation to the 20 characters the ALB name allows)
|
|
74
|
+
# gets a 5-character hash of the real name, so `feat/x` and `feat-x` cannot land
|
|
75
|
+
# on the same stack. An already-clean short name is used as is.
|
|
76
|
+
derive_env() {
|
|
77
|
+
local lower slug
|
|
78
|
+
lower=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
|
|
79
|
+
slug=$(printf '%s' "$lower" | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')
|
|
80
|
+
[ -n "$slug" ] || die "branch name '$1' has no usable characters"
|
|
81
|
+
if [ "$slug" != "$lower" ] || [ ${#slug} -gt 20 ]; then
|
|
82
|
+
slug=$(printf '%s' "${slug:0:14}" | sed -E 's/-+$//')-$(hash5 "$1")
|
|
83
|
+
fi
|
|
84
|
+
printf '%s' "$slug"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Sets BRANCH and every name derived from it. Called by the commands that act on
|
|
88
|
+
# one branch's preview (not by `list`, which is branch-independent).
|
|
89
|
+
init_branch() {
|
|
90
|
+
BRANCH="${BRANCH:-$(git rev-parse --abbrev-ref HEAD)}"
|
|
91
|
+
[ "$BRANCH" != "HEAD" ] || die "detached HEAD - check out a branch, or pass BRANCH=<name>"
|
|
92
|
+
local protected
|
|
93
|
+
for protected in $PROTECTED_BRANCHES; do
|
|
94
|
+
[ "$BRANCH" != "$protected" ] || die "refusing to make a preview of $BRANCH - the live stack deploys through its own targets"
|
|
95
|
+
done
|
|
96
|
+
ENV_NAME=$(derive_env "$BRANCH")
|
|
97
|
+
STACK="$PREFIX-$ENV_NAME"
|
|
98
|
+
DB_NAME="${DB_PREFIX}_pv_$(printf '%s' "$ENV_NAME" | tr '-' '_')"
|
|
99
|
+
CLUSTER="$PREFIX-$ENV_NAME"
|
|
100
|
+
LOG_GROUP="/ecs/$PREFIX-$ENV_NAME"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# ---- helpers ---------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
stack_status() {
|
|
106
|
+
aws cloudformation describe-stacks --stack-name "$STACK" --region "$REGION" \
|
|
107
|
+
--query 'Stacks[0].StackStatus' --output text 2>/dev/null || true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
stack_output() { # <stack> <OutputKey>
|
|
111
|
+
aws cloudformation describe-stacks --stack-name "$1" --region "$REGION" \
|
|
112
|
+
--query "Stacks[0].Outputs[?OutputKey=='$2'].OutputValue" --output text
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# A value the owner or database stack must provide.
|
|
116
|
+
owner_output() { # <stack> <OutputKey>
|
|
117
|
+
local value
|
|
118
|
+
value=$(stack_output "$1" "$2")
|
|
119
|
+
{ [ -n "$value" ] && [ "$value" != "None" ]; } || die "$1 has no $2 output"
|
|
120
|
+
printf '%s' "$value"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
resolve_owner() {
|
|
124
|
+
VPC=$(owner_output "$OWNER_STACK" VpcId)
|
|
125
|
+
SUBNET_A=$(owner_output "$OWNER_STACK" PrivateSubnetAId)
|
|
126
|
+
SUBNET_B=$(owner_output "$OWNER_STACK" PrivateSubnetBId)
|
|
127
|
+
PUBLIC_A=$(owner_output "$OWNER_STACK" PublicSubnetId)
|
|
128
|
+
PUBLIC_B=$(owner_output "$OWNER_STACK" BastionSubnetId)
|
|
129
|
+
SG=$(owner_output "$OWNER_STACK" FunctionSecurityGroupId)
|
|
130
|
+
DB_HOST=$(owner_output "$DATABASE_STACK" "$DATABASE_ENDPOINT_OUTPUT")
|
|
131
|
+
DB_SECRET=$(owner_output "$DATABASE_STACK" "$DATABASE_SECRET_OUTPUT")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# The DesiredCount the stack currently has (0 if there is no stack yet). Kept as
|
|
135
|
+
# is while new images roll in, so a re-run after a failed deploy cannot start
|
|
136
|
+
# tasks before the database exists.
|
|
137
|
+
current_count() {
|
|
138
|
+
aws cloudformation describe-stacks --stack-name "$STACK" --region "$REGION" \
|
|
139
|
+
--query "Stacks[0].Parameters[?ParameterKey=='DesiredCount'].ParameterValue" --output text 2>/dev/null \
|
|
140
|
+
| grep -E '^[0-9]+$' || echo 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
guard_status() {
|
|
144
|
+
case "$(stack_status)" in
|
|
145
|
+
ROLLBACK_COMPLETE|CREATE_FAILED|DELETE_FAILED)
|
|
146
|
+
die "$STACK is in a failed state ($(stack_status)); run 'preview.sh destroy' first, then deploy again" ;;
|
|
147
|
+
esac
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Only ever operate on preview stacks.
|
|
151
|
+
guard_stack_name() {
|
|
152
|
+
case "$STACK" in
|
|
153
|
+
"$PREFIX"-*) ;;
|
|
154
|
+
*) die "refusing to operate on $STACK: not a preview stack" ;;
|
|
155
|
+
esac
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# stack_deploy <desired-count> <image-tag>
|
|
159
|
+
stack_deploy() {
|
|
160
|
+
local commit dirty=""
|
|
161
|
+
commit=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
|
162
|
+
git diff --quiet HEAD -- 2>/dev/null || dirty="-dirty"
|
|
163
|
+
local overrides="" svc
|
|
164
|
+
for svc in $SERVICES; do
|
|
165
|
+
overrides="$overrides $(tag_parameter "$svc")=$2"
|
|
166
|
+
done
|
|
167
|
+
# shellcheck disable=SC2086
|
|
168
|
+
aws cloudformation deploy --template-file preview.yaml --stack-name "$STACK" --region "$REGION" \
|
|
169
|
+
--capabilities CAPABILITY_IAM --no-fail-on-empty-changeset \
|
|
170
|
+
--tags "Project=$PREFIX" "ManagedBy=preview.sh" "Branch=$BRANCH" "Commit=$commit$dirty" \
|
|
171
|
+
--parameter-overrides \
|
|
172
|
+
EnvName="$ENV_NAME" DbName="$DB_NAME" \
|
|
173
|
+
OwningVpcId="$VPC" OwningSubnetAId="$SUBNET_A" OwningSubnetBId="$SUBNET_B" \
|
|
174
|
+
OwningPublicSubnetAId="$PUBLIC_A" OwningPublicSubnetBId="$PUBLIC_B" \
|
|
175
|
+
OwningSecurityGroupId="$SG" OwningDatabaseEndpoint="$DB_HOST" OwningDatabaseSecretArn="$DB_SECRET" \
|
|
176
|
+
DesiredCount="$1" $overrides
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# ---- the database ----------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
# The database instance is reachable only from inside the VPC, so the database is
|
|
182
|
+
# created (or dropped) by a one-shot task from preview.yaml, not from this machine.
|
|
183
|
+
# run_database_task <create|drop>
|
|
184
|
+
run_database_task() {
|
|
185
|
+
local action="$1" taskdef task code
|
|
186
|
+
taskdef=$(stack_output "$STACK" DbInitTaskDefinition)
|
|
187
|
+
task=$(aws ecs run-task --cluster "$CLUSTER" --launch-type FARGATE --task-definition "$taskdef" --region "$REGION" \
|
|
188
|
+
--overrides "{\"containerOverrides\":[{\"name\":\"dbinit\",\"environment\":[{\"name\":\"ACTION\",\"value\":\"$action\"}]}]}" \
|
|
189
|
+
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET_A,$SUBNET_B],securityGroups=[$SG],assignPublicIp=DISABLED}" \
|
|
190
|
+
--query 'tasks[0].taskArn' --output text)
|
|
191
|
+
{ [ -n "$task" ] && [ "$task" != "None" ]; } || die "could not start the database task"
|
|
192
|
+
aws ecs wait tasks-stopped --cluster "$CLUSTER" --tasks "$task" --region "$REGION"
|
|
193
|
+
code=$(aws ecs describe-tasks --cluster "$CLUSTER" --tasks "$task" --region "$REGION" \
|
|
194
|
+
--query 'tasks[0].containers[0].exitCode' --output text)
|
|
195
|
+
if [ "$code" != "0" ]; then
|
|
196
|
+
aws logs tail "$LOG_GROUP" --log-stream-name-prefix dbinit --since 15m --region "$REGION" >&2 || true
|
|
197
|
+
die "database task exited with code $code (log above)"
|
|
198
|
+
fi
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
ensure_database() {
|
|
202
|
+
echo "==> creating database $DB_NAME if it does not exist (one-shot task)"
|
|
203
|
+
run_database_task create
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
cmd_ensure_database() {
|
|
207
|
+
[ -n "$(stack_status)" ] || die "no preview stack $STACK - run 'preview.sh deploy' first"
|
|
208
|
+
resolve_owner
|
|
209
|
+
ensure_database
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# ---- deploy ----------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
cmd_deploy() {
|
|
215
|
+
local svc
|
|
216
|
+
for svc in $SERVICES; do
|
|
217
|
+
docker image inspect "$(local_image "$svc")" >/dev/null 2>&1 \
|
|
218
|
+
|| die "image $(local_image "$svc") is not built - build the images first (make docker-build)"
|
|
219
|
+
done
|
|
220
|
+
guard_stack_name
|
|
221
|
+
guard_status
|
|
222
|
+
resolve_owner
|
|
223
|
+
|
|
224
|
+
local existed=0 tag account ecr
|
|
225
|
+
[ -n "$(stack_status)" ] && existed=1
|
|
226
|
+
|
|
227
|
+
echo "==> preview '$ENV_NAME' for branch '$BRANCH' (stack $STACK, database $DB_NAME)"
|
|
228
|
+
if [ "$existed" = 0 ]; then
|
|
229
|
+
echo "==> creating the stack with no tasks running (the ECR repositories must exist before images can be pushed)"
|
|
230
|
+
stack_deploy 0 bootstrap
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
tag="$ENV_NAME-$(date -u +%Y%m%d%H%M%S)"
|
|
234
|
+
account=$(aws sts get-caller-identity --query Account --output text)
|
|
235
|
+
ecr="$account.dkr.ecr.$REGION.amazonaws.com"
|
|
236
|
+
|
|
237
|
+
echo "==> ecr login"
|
|
238
|
+
# An isolated docker config: Docker Desktop's keychain helper can hang in a
|
|
239
|
+
# headless shell.
|
|
240
|
+
local docker_config=.docker-config-no-keychain
|
|
241
|
+
mkdir -p "$docker_config"
|
|
242
|
+
echo '{}' > "$docker_config/config.json"
|
|
243
|
+
aws ecr get-login-password --region "$REGION" | docker --config "$docker_config" login --username AWS --password-stdin "$ecr"
|
|
244
|
+
|
|
245
|
+
for svc in $SERVICES; do
|
|
246
|
+
echo "==> pushing $svc as $tag"
|
|
247
|
+
docker tag "$(local_image "$svc")" "$ecr/$PREFIX-$ENV_NAME-$svc:$tag"
|
|
248
|
+
docker --config "$docker_config" push "$ecr/$PREFIX-$ENV_NAME-$svc:$tag"
|
|
249
|
+
done
|
|
250
|
+
|
|
251
|
+
echo "==> pointing the stack at the new images"
|
|
252
|
+
stack_deploy "$(current_count)" "$tag"
|
|
253
|
+
|
|
254
|
+
ensure_database
|
|
255
|
+
|
|
256
|
+
echo "==> starting the service"
|
|
257
|
+
stack_deploy 1 "$tag"
|
|
258
|
+
echo "==> waiting for the service to settle (a first boot can take several minutes)"
|
|
259
|
+
aws ecs wait services-stable --cluster "$CLUSTER" --services "$PREFIX-$ENV_NAME" --region "$REGION" \
|
|
260
|
+
|| die "service did not settle in time - check: aws logs tail $LOG_GROUP --follow --region $REGION"
|
|
261
|
+
<% if s.first_admin -%>
|
|
262
|
+
|
|
263
|
+
signup_admin
|
|
264
|
+
<% end -%>
|
|
265
|
+
|
|
266
|
+
echo
|
|
267
|
+
echo "Preview is up: $(stack_output "$STACK" PreviewUrl)"
|
|
268
|
+
<% if s.first_admin -%>
|
|
269
|
+
echo " sign in: preview.sh login (a one-line session for $ADMIN_EMAIL)"
|
|
270
|
+
<% end -%>
|
|
271
|
+
echo " logs: aws logs tail $LOG_GROUP --follow --region $REGION"
|
|
272
|
+
echo " tear down: preview.sh destroy"
|
|
273
|
+
}
|
|
274
|
+
<% if s.first_admin -%>
|
|
275
|
+
|
|
276
|
+
# ---- first admin: sign the deployer up -------------------------------------
|
|
277
|
+
#
|
|
278
|
+
# A brand-new preview has nobody in it, and sign-in checks membership. The way in
|
|
279
|
+
# is the host's own signup: POST <%= s.signup_path %> dispatches the domain's SignUp command,
|
|
280
|
+
# and the domain's translations turn the signup into the first admin in the
|
|
281
|
+
# preview's own database. The credential is a purpose-bound token minted from the
|
|
282
|
+
# preview's session secret, so only someone with AWS access to the account can
|
|
283
|
+
# sign anyone up. It needs a domain image whose host serves the signup route and
|
|
284
|
+
# a domain that attaches the signups chapter; the warnings below say which is missing.
|
|
285
|
+
|
|
286
|
+
# Who becomes the preview's first admin: the person deploying it. PREVIEW_ADMIN_EMAIL
|
|
287
|
+
# and PREVIEW_ADMIN_NAME override; the default is your git identity.
|
|
288
|
+
admin_identity() {
|
|
289
|
+
ADMIN_EMAIL="${PREVIEW_ADMIN_EMAIL:-$(git config user.email || true)}"
|
|
290
|
+
[ -n "$ADMIN_EMAIL" ] || die "no email to sign up as the preview's first admin - set PREVIEW_ADMIN_EMAIL or git config user.email"
|
|
291
|
+
ADMIN_NAME="${PREVIEW_ADMIN_NAME:-$(git config user.name || true)}"
|
|
292
|
+
[ -n "$ADMIN_NAME" ] || ADMIN_NAME="${ADMIN_EMAIL%%@*}"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# The preview's own session secret (the stack generates it). Never printed.
|
|
296
|
+
session_secret() {
|
|
297
|
+
aws secretsmanager get-secret-value --secret-id "$(stack_output "$STACK" SessionSecretArn)" --region "$REGION" \
|
|
298
|
+
--query SecretString --output text \
|
|
299
|
+
| python3 -c 'import json,sys; print(json.load(sys.stdin)["session_secret"])'
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
# The two tokens the host verifies: base64url(payload) + "." + hex HMAC-SHA256.
|
|
303
|
+
# A signup token is bound to its purpose (key "signup:<secret>", good for 10
|
|
304
|
+
# minutes), so a session can never sign anyone up. A session token is the plain
|
|
305
|
+
# account token, keyed with the bare secret (good for 8 hours).
|
|
306
|
+
mint_token() { # <signup|session> <secret> [email]
|
|
307
|
+
KIND="$1" SECRET="$2" EMAIL="${3:-}" python3 - <<'PY'
|
|
308
|
+
import base64, hashlib, hmac, json, os, time
|
|
309
|
+
|
|
310
|
+
kind, secret = os.environ["KIND"], os.environ["SECRET"]
|
|
311
|
+
now = int(time.time())
|
|
312
|
+
if kind == "signup":
|
|
313
|
+
payload, key = {"purpose": "signup", "exp": now + 600}, "signup:" + secret
|
|
314
|
+
else:
|
|
315
|
+
payload, key = {"email": os.environ["EMAIL"], "exp": now + 8 * 3600}, secret
|
|
316
|
+
encoded = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode()).rstrip(b"=").decode()
|
|
317
|
+
print(encoded + "." + hmac.new(key.encode(), encoded.encode(), hashlib.sha256).hexdigest())
|
|
318
|
+
PY
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Signs the deployer up. Never fatal: a preview without an admin is still worth
|
|
322
|
+
# having, so a problem is a warning that says what is missing. A new distribution
|
|
323
|
+
# and ALB rule can take a few minutes to answer, so connection errors, 403s and
|
|
324
|
+
# 5xxs are retried (up to ~10 minutes); anything else is an answer.
|
|
325
|
+
signup_admin() {
|
|
326
|
+
admin_identity
|
|
327
|
+
local url secret body response status attempt=0
|
|
328
|
+
url=$(stack_output "$STACK" PreviewUrl)
|
|
329
|
+
secret=$(session_secret)
|
|
330
|
+
response=$(mktemp)
|
|
331
|
+
echo "==> signing $ADMIN_EMAIL up as the preview's first admin (POST $url$SIGNUP_PATH)"
|
|
332
|
+
while :; do
|
|
333
|
+
body=$(TOKEN="$(mint_token signup "$secret")" EMAIL="$ADMIN_EMAIL" NAME="$ADMIN_NAME" python3 -c \
|
|
334
|
+
'import json, os; print(json.dumps({"token": os.environ["TOKEN"], "email": os.environ["EMAIL"], "name": os.environ["NAME"]}))')
|
|
335
|
+
status=$(curl -s -o "$response" -w '%{http_code}' --max-time 30 -X POST -H 'Content-Type: application/json' -d "$body" "$url$SIGNUP_PATH" || true)
|
|
336
|
+
status="${status:-000}"
|
|
337
|
+
case "$status" in
|
|
338
|
+
201)
|
|
339
|
+
echo " signed up"; rm -f "$response"; return 0 ;;
|
|
340
|
+
422)
|
|
341
|
+
if grep -q '"AlreadyExists"' "$response"; then
|
|
342
|
+
echo " already signed up"
|
|
343
|
+
else
|
|
344
|
+
warn "the domain refused the signup: $(cat "$response")"
|
|
345
|
+
warn "does this domain attach the signups chapter?"
|
|
346
|
+
fi
|
|
347
|
+
rm -f "$response"; return 0 ;;
|
|
348
|
+
302|404)
|
|
349
|
+
warn "this domain has no POST $SIGNUP_PATH route (HTTP $status) - build the domain image from a host that serves it and redeploy"
|
|
350
|
+
rm -f "$response"; return 0 ;;
|
|
351
|
+
400|401)
|
|
352
|
+
warn "the domain refused the signup (HTTP $status): $(cat "$response")"
|
|
353
|
+
rm -f "$response"; return 0 ;;
|
|
354
|
+
*)
|
|
355
|
+
attempt=$((attempt + 1))
|
|
356
|
+
if [ "$attempt" -ge 40 ]; then
|
|
357
|
+
warn "the preview never answered POST $SIGNUP_PATH (last HTTP $status) - run 'preview.sh deploy' again once it is up"
|
|
358
|
+
rm -f "$response"; return 0
|
|
359
|
+
fi
|
|
360
|
+
sleep "${PREVIEW_SIGNUP_RETRY_SECONDS:-15}" ;;
|
|
361
|
+
esac
|
|
362
|
+
done
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
# Prints what to paste into the browser console to sign in as the preview's first
|
|
366
|
+
# admin: a session for that email signed with THIS preview's session secret (the
|
|
367
|
+
# secret itself is never printed). Anyone with AWS access to the account could
|
|
368
|
+
# mint the same, which is the point: it is the deployer's door, not a public one.
|
|
369
|
+
cmd_login() {
|
|
370
|
+
[ -n "$(stack_status)" ] || die "no preview for branch '$BRANCH' - run 'preview.sh deploy'"
|
|
371
|
+
admin_identity
|
|
372
|
+
local url token
|
|
373
|
+
url=$(stack_output "$STACK" PreviewUrl)
|
|
374
|
+
token=$(mint_token session "$(session_secret)" "$ADMIN_EMAIL")
|
|
375
|
+
echo "Signing in to $url as $ADMIN_EMAIL (good for 8 hours)"
|
|
376
|
+
echo
|
|
377
|
+
echo "1. Open $url$LANDING_PATH"
|
|
378
|
+
echo "2. In the browser console (developer tools), run:"
|
|
379
|
+
echo
|
|
380
|
+
echo " document.cookie = \"$SESSION_COOKIE=$token; path=/; Secure; SameSite=Lax\""
|
|
381
|
+
echo
|
|
382
|
+
echo "3. Reload the page"
|
|
383
|
+
}
|
|
384
|
+
<% end -%>
|
|
385
|
+
|
|
386
|
+
# ---- destroy ---------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
cmd_destroy() {
|
|
389
|
+
guard_stack_name
|
|
390
|
+
[ -n "$(stack_status)" ] || die "no preview stack $STACK (nothing to destroy)"
|
|
391
|
+
if [ "${PREVIEW_DROP_DATABASE:-}" = "1" ]; then
|
|
392
|
+
resolve_owner
|
|
393
|
+
echo "==> dropping database $DB_NAME (one-shot task; PREVIEW_DROP_DATABASE=1)"
|
|
394
|
+
run_database_task drop
|
|
395
|
+
fi
|
|
396
|
+
echo "==> deleting $STACK"
|
|
397
|
+
aws cloudformation delete-stack --stack-name "$STACK" --region "$REGION"
|
|
398
|
+
aws cloudformation wait stack-delete-complete --stack-name "$STACK" --region "$REGION"
|
|
399
|
+
if [ "${PREVIEW_DROP_DATABASE:-}" = "1" ]; then
|
|
400
|
+
echo "Deleted, and the database $DB_NAME is dropped."
|
|
401
|
+
else
|
|
402
|
+
echo "Deleted. The database $DB_NAME is left on the shared instance on purpose, so a later"
|
|
403
|
+
echo "deploy of this branch picks its data back up. Set PREVIEW_DROP_DATABASE=1 to drop it."
|
|
404
|
+
fi
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
# ---- list / url / name -----------------------------------------------------
|
|
408
|
+
|
|
409
|
+
cmd_list() {
|
|
410
|
+
aws cloudformation describe-stacks --region "$REGION" \
|
|
411
|
+
--query "Stacks[?starts_with(StackName, '$PREFIX-')].[StackName, StackStatus, Tags[?Key=='Branch']|[0].Value, Tags[?Key=='Commit']|[0].Value, Outputs[?OutputKey=='PreviewUrl']|[0].OutputValue]" \
|
|
412
|
+
--output table
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
cmd_url() {
|
|
416
|
+
[ -n "$(stack_status)" ] || die "no preview for branch '$BRANCH' - run 'preview.sh deploy'"
|
|
417
|
+
stack_output "$STACK" PreviewUrl
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# What this branch's preview is (or would be) called - no AWS calls.
|
|
421
|
+
cmd_name() {
|
|
422
|
+
echo "branch: $BRANCH"
|
|
423
|
+
echo "env: $ENV_NAME"
|
|
424
|
+
echo "stack: $STACK"
|
|
425
|
+
echo "database: $DB_NAME"
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
main() {
|
|
429
|
+
case "${1:-}" in
|
|
430
|
+
deploy) init_branch; cmd_deploy ;;
|
|
431
|
+
destroy) init_branch; cmd_destroy ;;
|
|
432
|
+
ensure-database) init_branch; cmd_ensure_database ;;
|
|
433
|
+
list) cmd_list ;;
|
|
434
|
+
url) init_branch; cmd_url ;;
|
|
435
|
+
name) init_branch; cmd_name ;;
|
|
436
|
+
<% if s.first_admin -%>
|
|
437
|
+
login) init_branch; cmd_login ;;
|
|
438
|
+
<% end -%>
|
|
439
|
+
*) die "usage: preview.sh <deploy|destroy|ensure-database|list|url|name<%= '|login' if s.first_admin %>>" ;;
|
|
440
|
+
esac
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# Sourcing this file (to reuse its functions) does not run a command.
|
|
444
|
+
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
445
|
+
main "$@"
|
|
446
|
+
fi
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "yaml_text"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module Projections
|
|
6
|
+
module Deploy
|
|
7
|
+
module Preview
|
|
8
|
+
# The network edge of a preview stack: security groups, target groups, the
|
|
9
|
+
# load balancer, its listener rules and the service that registers the containers.
|
|
10
|
+
#
|
|
11
|
+
# A container is reachable when it is the default or owns a path. Paths are
|
|
12
|
+
# split into rules of at most five, the most a load balancer condition holds.
|
|
13
|
+
module RoutingYaml
|
|
14
|
+
CLOUDFRONT_PREFIX_LIST = "pl-3b927c52".freeze
|
|
15
|
+
PATHS_PER_RULE = 5
|
|
16
|
+
INGRESS_NOTE = <<~NOTE.chomp.freeze
|
|
17
|
+
# One ingress rule per routed container on the shared owner security group, removed
|
|
18
|
+
# with the stack. The group has a rules-per-group quota, so delete stale previews.
|
|
19
|
+
NOTE
|
|
20
|
+
|
|
21
|
+
extend YamlText
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# Renders the load balancer's security group and one ingress rule per routed container.
|
|
26
|
+
#
|
|
27
|
+
# @param settings [Settings] the resolved preview settings
|
|
28
|
+
# @return [Array<String>] the resource blocks
|
|
29
|
+
def networking(settings)
|
|
30
|
+
ingress = settings.containers.select(&:routed?).map do |c|
|
|
31
|
+
<<~YAML.chomp
|
|
32
|
+
ComputeIngressFromAlb#{c.logical}:
|
|
33
|
+
Type: AWS::EC2::SecurityGroupIngress
|
|
34
|
+
Properties:
|
|
35
|
+
GroupId: !Ref OwningSecurityGroupId
|
|
36
|
+
IpProtocol: tcp
|
|
37
|
+
FromPort: #{c.port}
|
|
38
|
+
ToPort: #{c.port}
|
|
39
|
+
SourceSecurityGroupId: !Ref AlbSecurityGroup
|
|
40
|
+
YAML
|
|
41
|
+
end
|
|
42
|
+
[alb_security_group(settings), "#{INGRESS_NOTE}\n#{ingress.join("\n\n")}"]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Renders the load balancer's security group, open to CloudFront's prefix list only.
|
|
46
|
+
#
|
|
47
|
+
# @param settings [Settings] the resolved preview settings
|
|
48
|
+
# @return [String] the resource block
|
|
49
|
+
def alb_security_group(settings)
|
|
50
|
+
<<~YAML.chomp
|
|
51
|
+
AlbSecurityGroup:
|
|
52
|
+
Type: AWS::EC2::SecurityGroup
|
|
53
|
+
Properties:
|
|
54
|
+
VpcId: !Ref OwningVpcId
|
|
55
|
+
GroupDescription: !Sub "#{settings.infra_name} preview ${EnvName} ALB - HTTP ingress from CloudFront only"
|
|
56
|
+
SecurityGroupIngress:
|
|
57
|
+
# The AWS-managed com.amazonaws.global.cloudfront.origin-facing prefix list.
|
|
58
|
+
- IpProtocol: tcp
|
|
59
|
+
FromPort: 80
|
|
60
|
+
ToPort: 80
|
|
61
|
+
SourcePrefixListId: #{CLOUDFRONT_PREFIX_LIST}
|
|
62
|
+
YAML
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Renders the target groups, the load balancer, its listener and the path rules.
|
|
66
|
+
#
|
|
67
|
+
# @param settings [Settings] the resolved preview settings
|
|
68
|
+
# @return [Array<String>] the resource blocks
|
|
69
|
+
def load_balancing(settings)
|
|
70
|
+
routed = settings.containers.select(&:routed?)
|
|
71
|
+
[*routed.map { |c| target_group(c) }, alb(settings), listener(settings), *listener_rules(settings)]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Renders the target group of one routed container.
|
|
75
|
+
#
|
|
76
|
+
# @param container [Containers::Entry] the container
|
|
77
|
+
# @return [String] the resource block
|
|
78
|
+
def target_group(container)
|
|
79
|
+
<<~YAML.chomp
|
|
80
|
+
#{container.logical}TargetGroup:
|
|
81
|
+
Type: AWS::ElasticLoadBalancingV2::TargetGroup
|
|
82
|
+
Properties:
|
|
83
|
+
TargetType: ip
|
|
84
|
+
Port: #{container.port}
|
|
85
|
+
Protocol: HTTP
|
|
86
|
+
VpcId: !Ref OwningVpcId
|
|
87
|
+
HealthCheckPath: #{container.health_check_path}
|
|
88
|
+
HealthCheckPort: "#{container.port}"
|
|
89
|
+
YAML
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Renders the internet-facing application load balancer.
|
|
93
|
+
#
|
|
94
|
+
# @param settings [Settings] the resolved preview settings
|
|
95
|
+
# @return [String] the resource block
|
|
96
|
+
def alb(settings)
|
|
97
|
+
<<~YAML.chomp
|
|
98
|
+
Alb:
|
|
99
|
+
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
|
|
100
|
+
Properties:
|
|
101
|
+
Name: !Sub "#{settings.alb_prefix}-${EnvName}"
|
|
102
|
+
Scheme: internet-facing
|
|
103
|
+
Type: application
|
|
104
|
+
SecurityGroups: [!Ref AlbSecurityGroup]
|
|
105
|
+
Subnets: [!Ref OwningPublicSubnetAId, !Ref OwningPublicSubnetBId]
|
|
106
|
+
YAML
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Renders the port 80 listener, whose default action goes to the default container.
|
|
110
|
+
#
|
|
111
|
+
# @param settings [Settings] the resolved preview settings
|
|
112
|
+
# @return [String] the resource block
|
|
113
|
+
def listener(settings)
|
|
114
|
+
<<~YAML.chomp
|
|
115
|
+
Listener:
|
|
116
|
+
Type: AWS::ElasticLoadBalancingV2::Listener
|
|
117
|
+
Properties:
|
|
118
|
+
LoadBalancerArn: !Ref Alb
|
|
119
|
+
Port: 80
|
|
120
|
+
Protocol: HTTP
|
|
121
|
+
DefaultActions:
|
|
122
|
+
- Type: forward
|
|
123
|
+
TargetGroupArn: !Ref #{settings.default_container.logical}TargetGroup
|
|
124
|
+
YAML
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Lists the listener rules to render: a non-default container's paths, sliced to
|
|
128
|
+
# fit one condition.
|
|
129
|
+
#
|
|
130
|
+
# @param settings [Settings] the resolved preview settings
|
|
131
|
+
# @return [Array<Hash{Symbol => Object}>] entries with `:container`, `:paths` and `:id`
|
|
132
|
+
def rule_specs(settings)
|
|
133
|
+
settings.containers.reject(&:default).select(&:routed?).flat_map do |c|
|
|
134
|
+
c.paths.each_slice(PATHS_PER_RULE).with_index.map do |slice, n|
|
|
135
|
+
{ container: c, paths: slice, id: rule_id(c, n) }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Renders every listener rule, with priorities counting up in tens.
|
|
141
|
+
#
|
|
142
|
+
# @param settings [Settings] the resolved preview settings
|
|
143
|
+
# @return [Array<String>] the resource blocks
|
|
144
|
+
def listener_rules(settings)
|
|
145
|
+
rule_specs(settings).each_with_index.map { |spec, i| listener_rule(spec, (i + 1) * 10) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Renders one listener rule.
|
|
149
|
+
#
|
|
150
|
+
# @param spec [Hash{Symbol => Object}] one entry of `rule_specs`
|
|
151
|
+
# @param priority [Integer] the rule's listener priority
|
|
152
|
+
# @return [String] the resource block
|
|
153
|
+
def listener_rule(spec, priority)
|
|
154
|
+
<<~YAML.chomp
|
|
155
|
+
#{spec[:id]}:
|
|
156
|
+
Type: AWS::ElasticLoadBalancingV2::ListenerRule
|
|
157
|
+
Properties:
|
|
158
|
+
ListenerArn: !Ref Listener
|
|
159
|
+
Priority: #{priority}
|
|
160
|
+
Conditions:
|
|
161
|
+
- Field: path-pattern
|
|
162
|
+
Values: [#{spec[:paths].map { |p| JSON.generate(p) }.join(', ')}]
|
|
163
|
+
Actions:
|
|
164
|
+
- Type: forward
|
|
165
|
+
TargetGroupArn: !Ref #{spec[:container].logical}TargetGroup
|
|
166
|
+
YAML
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Names one listener rule's logical id.
|
|
170
|
+
#
|
|
171
|
+
# @param container [Containers::Entry] the container the rule forwards to
|
|
172
|
+
# @param slice [Integer] the zero-based index of the rule among that container's rules
|
|
173
|
+
# @return [String] the logical id
|
|
174
|
+
def rule_id(container, slice) = "ListenerRule#{container.logical}#{slice + 1}"
|
|
175
|
+
|
|
176
|
+
# Renders the service, which registers every routed container with its target group.
|
|
177
|
+
#
|
|
178
|
+
# @param settings [Settings] the resolved preview settings
|
|
179
|
+
# @return [String] the resource block
|
|
180
|
+
def service(settings)
|
|
181
|
+
rule_ids = rule_specs(settings).map { |spec| spec[:id] }
|
|
182
|
+
<<~YAML.chomp
|
|
183
|
+
Service:
|
|
184
|
+
Type: AWS::ECS::Service
|
|
185
|
+
DependsOn: [#{(['Listener'] + rule_ids).join(', ')}]
|
|
186
|
+
Properties:
|
|
187
|
+
ServiceName: !Sub "#{settings.prefix}-${EnvName}"
|
|
188
|
+
Cluster: !Ref Cluster
|
|
189
|
+
TaskDefinition: !Ref TaskDefinition
|
|
190
|
+
DesiredCount: !Ref DesiredCount
|
|
191
|
+
LaunchType: FARGATE
|
|
192
|
+
EnableExecuteCommand: true
|
|
193
|
+
NetworkConfiguration:
|
|
194
|
+
AwsvpcConfiguration:
|
|
195
|
+
AssignPublicIp: DISABLED
|
|
196
|
+
Subnets: [!Ref OwningSubnetAId, !Ref OwningSubnetBId]
|
|
197
|
+
SecurityGroups: [!Ref OwningSecurityGroupId]
|
|
198
|
+
LoadBalancers:
|
|
199
|
+
#{indent(service_load_balancers(settings), 6)}
|
|
200
|
+
YAML
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Renders the service's load balancer registrations.
|
|
204
|
+
#
|
|
205
|
+
# @param settings [Settings] the resolved preview settings
|
|
206
|
+
# @return [String] one list entry per routed container
|
|
207
|
+
def service_load_balancers(settings)
|
|
208
|
+
settings.containers.select(&:routed?).map do |c|
|
|
209
|
+
"- ContainerName: #{c.name}\n ContainerPort: #{c.port}\n TargetGroupArn: !Ref #{c.logical}TargetGroup"
|
|
210
|
+
end.join("\n")
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|