hecks 3.4.1 → 3.5.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/memory/shared_elements.rb +121 -17
- data/lib/hecks/deploy/bluebook/deploy.bluebook +1630 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +65 -0
- data/lib/hecks/deploy/oidc.json +240 -0
- data/lib/hecks/doors/cli_runner.rb +13 -4
- data/lib/hecks/doors/launcher_options.rb +14 -2
- data/lib/hecks/hecks/adapters/bluebook_report.rb +91 -0
- data/lib/hecks/hecks/adapters/deploy_plans.rb +49 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +379 -8
- data/lib/hecks/hecks/adapters/first_admin.rb +8 -1
- data/lib/hecks/hecks/adapters/git.rb +29 -0
- data/lib/hecks/hecks/adapters/in_process_operations.rb +6 -2
- data/lib/hecks/hecks/adapters/site_toolchain.rb +8 -8
- data/lib/hecks/hecks/custodian.bluebook +23 -2
- data/lib/hecks/hecks/hecks.hecksagon +66 -0
- data/lib/hecks/hecks/hecks.world +6 -1
- data/lib/hecks/hecks/site.world +4 -2
- data/lib/hecks/projections/deploy/box/hosting.rb +13 -3
- data/lib/hecks/projections/deploy/box/roll_recipe.rb +62 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +10 -3
- data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +2 -2
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +16 -4
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +6 -6
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +1 -1
- data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +11 -5
- data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +8 -3
- data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +2 -2
- data/lib/hecks/projections/deploy/box.rb +26 -13
- data/lib/hecks/projections/site/admin.rb +140 -0
- data/lib/hecks/projections/site/admin_cms/templates.rb +388 -0
- data/lib/hecks/projections/site/admin_cms.rb +72 -0
- data/lib/hecks/projections/site/payload/fields.rb +234 -0
- data/lib/hecks/projections/site/payload/lifecycle.rb +191 -0
- data/lib/hecks/projections/site/payload/model.rb +128 -0
- data/lib/hecks/projections/site/payload/specs.rb +179 -0
- data/lib/hecks/projections/site/payload_driver.rb +93 -0
- data/lib/hecks/projections/site/root_rows.rb +59 -0
- data/lib/hecks/projections/site/site_admin.rb +292 -0
- data/lib/hecks/projections/site/site_host/templates.rb +165 -0
- data/lib/hecks/projections/site/site_host.rb +58 -0
- data/lib/hecks/projections/site/site_root.rb +179 -0
- data/lib/hecks/runtime/value.rb +7 -0
- data/lib/hecks/site/bluebook/site.bluebook +13 -2
- data/lib/hecks/tools/site_routes.rb +78 -8
- data/lib/hecks/version.rb +1 -1
- data/rust/host/HECKS_RELEASE +1 -1
- metadata +18 -2
|
@@ -105,6 +105,7 @@ Hecks.hecksagon "Hecks" do
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
answers_query "Check"
|
|
108
|
+
answers_query "Digest"
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
Hecks::Door.port "Workspace" do
|
|
@@ -400,6 +401,71 @@ Hecks.hecksagon "Hecks" do
|
|
|
400
401
|
end
|
|
401
402
|
end
|
|
402
403
|
|
|
404
|
+
Deploy::SmokeRun.port "DeployToolchain" do
|
|
405
|
+
asks "Smoke", to: SmokeRun do
|
|
406
|
+
attribute :run, RunKey
|
|
407
|
+
attribute :after, Origin, default: { value: "alone" }
|
|
408
|
+
answers "SmokeAnswered"
|
|
409
|
+
refuses "SmokeRefused"
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
Deploy::ServiceRoll.port "DeployToolchain" do
|
|
414
|
+
asks "RollService", to: ServiceRoll do
|
|
415
|
+
attribute :run, RunKey
|
|
416
|
+
answers "ServiceRollAnswered"
|
|
417
|
+
refuses "ServiceRollRefused"
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
Deploy::BoxRoll.port "DeployToolchain" do
|
|
422
|
+
asks "RollBox", to: BoxRoll do
|
|
423
|
+
attribute :run, RunKey
|
|
424
|
+
answers "BoxRollAnswered"
|
|
425
|
+
refuses "BoxRollRefused"
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
Deploy::DataCopy.port "DeployToolchain" do
|
|
430
|
+
asks "CopyData", to: DataCopy do
|
|
431
|
+
attribute :run, RunKey
|
|
432
|
+
answers "DataCopyAnswered"
|
|
433
|
+
refuses "DataCopyRefused"
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
Deploy::CopyVerification.port "DeployToolchain" do
|
|
438
|
+
asks "CompareCopy", to: CopyVerification do
|
|
439
|
+
attribute :run, RunKey
|
|
440
|
+
answers "CopyVerificationAnswered"
|
|
441
|
+
refuses "CopyVerificationRefused"
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
Deploy::BluebookDiff.port "DeployToolchain" do
|
|
446
|
+
asks "CompareBluebooks", to: BluebookDiff do
|
|
447
|
+
attribute :run, RunKey
|
|
448
|
+
answers "BluebookDiffAnswered"
|
|
449
|
+
refuses "BluebookDiffRefused"
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
Deploy::PreviewRun.port "DeployToolchain" do
|
|
454
|
+
asks "RunPreview", to: PreviewRun do
|
|
455
|
+
attribute :run, RunKey
|
|
456
|
+
answers "PreviewRunAnswered"
|
|
457
|
+
refuses "PreviewRunRefused"
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
Deploy::CompanionRoll.port "DeployToolchain" do
|
|
462
|
+
asks "RollCompanion", to: CompanionRoll do
|
|
463
|
+
attribute :run, RunKey
|
|
464
|
+
answers "CompanionRollAnswered"
|
|
465
|
+
refuses "CompanionRollRefused"
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
403
469
|
Deploy::TemplateComparison.port "DeployToolchain" do
|
|
404
470
|
asks "Compare", to: TemplateComparison do
|
|
405
471
|
attribute :run, RunKey
|
data/lib/hecks/hecks/hecks.world
CHANGED
|
@@ -40,9 +40,14 @@ Hecks.world "Hecks" do
|
|
|
40
40
|
failure_states: %w[flagged failed drifted unreachable refused faulted halted stopped
|
|
41
41
|
abandoned red needs_fix],
|
|
42
42
|
settled: %w[package.vendor package.revendor package.release
|
|
43
|
-
operation.bootstrap_admin
|
|
43
|
+
operation.bootstrap_admin smoke_run.run
|
|
44
|
+
service_roll.run box_roll.run data_copy.restore
|
|
45
|
+
data_copy.verify bluebook_diff.run preview_run.name
|
|
46
|
+
preview_run.url preview_run.list preview_run.deploy
|
|
47
|
+
preview_run.destroy preview_run.login companion_roll.run],
|
|
44
48
|
names: { "mcp" => "door.serve_mcp", "console" => "operation.open_console", "init" => "door.init",
|
|
45
49
|
"interview" => "door.interview", "package.check" => "registry.check",
|
|
50
|
+
"package.digest" => "registry.digest",
|
|
46
51
|
"package.release" => "registry.release" },
|
|
47
52
|
streams: %w[Follow],
|
|
48
53
|
executable: "exe/hecks",
|
data/lib/hecks/hecks/site.world
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
# `run_keys` mints the `run` key of a projection, and `--wait` reports a faulted projection (a
|
|
6
6
|
# routes.ts out of date under `--check`, or a route table the projector refused) as exit 1, and
|
|
7
7
|
# `settled` makes `site_projection.check_live` wait for its reactions without the flag, so a live
|
|
8
|
-
# distribution that differs from the project is an exit status.
|
|
8
|
+
# distribution that differs from the project is an exit status. `report` prints that command's
|
|
9
|
+
# answer as the report alone: the differences, or the line saying the distribution matches.
|
|
9
10
|
Hecks.world "Site" do
|
|
10
11
|
launcher "Launcher", run_keys: true,
|
|
11
12
|
failure_states: %w[faulted refused halted stopped abandoned red needs_fix],
|
|
12
|
-
settled: %w[site_projection.check_live]
|
|
13
|
+
settled: %w[site_projection.check_live],
|
|
14
|
+
report: %w[site_projection.check_live]
|
|
13
15
|
end
|
|
@@ -33,8 +33,10 @@ module Hecks
|
|
|
33
33
|
# @param plan [Settings::Plan] the resolved settings
|
|
34
34
|
# @return [String] the Makefile fragment the box `Makefile` includes
|
|
35
35
|
def hosting_mk(plan)
|
|
36
|
-
Box.template("hosting.mk.tmpl", "SERVICE"
|
|
37
|
-
"URL"
|
|
36
|
+
Box.template("hosting.mk.tmpl", "SERVICE" => plan.containers.first.name,
|
|
37
|
+
"URL" => plan.hosting.public_url.to_s,
|
|
38
|
+
"SMOKE_RECIPE" => Box::SMOKE_RECIPE,
|
|
39
|
+
"SERVICE_RECIPE" => service_recipe)
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
# @param plan [Settings::Plan] the resolved settings
|
|
@@ -49,6 +51,14 @@ module Hecks
|
|
|
49
51
|
"ROLL_STEPS" => part(roll))
|
|
50
52
|
end
|
|
51
53
|
|
|
54
|
+
# @return [String] the `deploy-service` recipe: the `service_roll.run` command
|
|
55
|
+
def service_recipe
|
|
56
|
+
args = "service=$(SERVICE) $(if $(EXISTING_TAG),existing_tag=$(EXISTING_TAG)) " \
|
|
57
|
+
"$(if $(LOCAL_IMAGE),local_image=$(LOCAL_IMAGE))"
|
|
58
|
+
direct = "bash ./deploy-service.sh $(SERVICE)"
|
|
59
|
+
Box::RollRecipe.call(command: "service_roll.run", args: args, direct: direct)
|
|
60
|
+
end
|
|
61
|
+
|
|
52
62
|
# @param plan [Settings::Plan] the resolved settings
|
|
53
63
|
# @param region [String] the validated AWS region
|
|
54
64
|
# @return [String] `smoke-after-deploy.sh`
|
|
@@ -147,7 +157,7 @@ module Hecks
|
|
|
147
157
|
resolve_service() {
|
|
148
158
|
case "$1" in
|
|
149
159
|
#{arms.join("\n")}
|
|
150
|
-
*) echo "unknown service '$1', must be one of: ${SERVICES}" >&2; exit
|
|
160
|
+
*) echo "unknown service '$1', must be one of: ${SERVICES}" >&2; exit 2 ;;
|
|
151
161
|
esac
|
|
152
162
|
}
|
|
153
163
|
BASH
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Projections
|
|
3
|
+
module Deploy
|
|
4
|
+
module Box
|
|
5
|
+
# The Makefile recipes that run a roll as `box_roll.run` or `service_roll.run`.
|
|
6
|
+
#
|
|
7
|
+
# The command rolls with the generated script and, through its policy, requests the
|
|
8
|
+
# smoke, so one deploy leaves a `BoxRoll` or `ServiceRoll` (which also records the smoke's
|
|
9
|
+
# outcome) and a `SmokeRun` in the Hecks database. A failed roll or smoke is the command's
|
|
10
|
+
# exit 1, with the script's status in its message. With no database the command never
|
|
11
|
+
# starts, so the recipe runs the script itself, states the database error apart from the
|
|
12
|
+
# deploy's result, and fails (`Error 24` if all passed).
|
|
13
|
+
module RollRecipe
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
17
|
+
# @return [String] the `deploy` target's recipe, tab-indented: the `box_roll.run` command
|
|
18
|
+
def deploy(plan)
|
|
19
|
+
variable = plan.task_definition ? "TASKDEF" : "TAGS"
|
|
20
|
+
args = plan.task_definition ? "$(if $(TASKDEF),taskdef=$(TASKDEF))" : "$(if $(TAGS),tags=\"$(TAGS)\")"
|
|
21
|
+
direct = "bash ./deploy-box.sh $(#{variable})"
|
|
22
|
+
direct += " || exit $$?; TASKDEF=\"$(TASKDEF)\" bash ./smoke-after-deploy.sh" if plan.hosting
|
|
23
|
+
"\t#{call(command: 'box_roll.run', args: args, direct: direct)}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param command [String] the Deploy command, such as `box_roll.run`
|
|
27
|
+
# @param args [String] the Makefile text that turns variables into the arguments
|
|
28
|
+
# @param direct [String] the shell that runs the script when there is no database
|
|
29
|
+
# @return [String] the recipe lines, joined for a Makefile rule
|
|
30
|
+
def call(command:, args:, direct:)
|
|
31
|
+
[*run_lines(command, args), *no_database_lines(direct), *outcome_lines].join("\n\t").freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def run_lines(command, args)
|
|
35
|
+
["@err=$$(mktemp); run=deploy-$$(date -u +%Y%m%d%H%M%S); \\",
|
|
36
|
+
"$(HECKS) deploy #{command} project=\"$(CURDIR)\" run=\"$$run\" #{args} \\",
|
|
37
|
+
' $(if $(SKIP_POST_DEPLOY_SMOKE),skip_smoke=true) --wait 2>"$$err"; rc=$$?; \\']
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def no_database_lines(direct)
|
|
41
|
+
['if [ $$rc -ne 0 ] && grep -q \'^cannot open Hecks\' "$$err"; then \\',
|
|
42
|
+
' echo "==> the deploy record was NOT written: $$(grep -m1 \'^cannot open Hecks\' "$$err")" >&2; \\',
|
|
43
|
+
' echo " one-time setup on this machine: a database AND a non-superuser role that owns it" >&2; \\',
|
|
44
|
+
' echo " (a superuser skips the era write-fence, so createdb alone fails), then set" >&2; \\',
|
|
45
|
+
' echo " HECKS_DATABASE=postgres://<role>@localhost/<db>" >&2; \\',
|
|
46
|
+
' echo " deploying anyway so the deploy is not lost" >&2; \\',
|
|
47
|
+
" rm -f \"$$err\"; #{direct}; rc=$$?; \\",
|
|
48
|
+
' echo "==> deploy exit $$rc. The deploy record was NOT written: the database is unavailable (see above)." >&2; \\',
|
|
49
|
+
" [ $$rc -ne 0 ] || rc=24; exit $$rc; \\",
|
|
50
|
+
"fi; \\"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def outcome_lines
|
|
54
|
+
['cat "$$err" >&2; rm -f "$$err"; exit $$rc']
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private_class_method :run_lines, :no_database_lines, :outcome_lines
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
# Roll @@STACK@@'s app box: render the compose files, push them over SSM, start the containers and check
|
|
3
3
|
# them. Exits non-zero if the roll fails or the box does not answer as expected.
|
|
4
4
|
#
|
|
5
|
+
# Exit codes (the ones `hecks deploy box_roll.run` names; a step that fails under `set -e` ends with its
|
|
6
|
+
# own status, 1 from bash and 254/255 from the aws CLI):
|
|
7
|
+
# 0 the roll succeeded and the box answered healthy
|
|
8
|
+
# 40 the box stack has no instance
|
|
9
|
+
# 41 the roll did not succeed on the box
|
|
10
|
+
# 42 the roll succeeded but the box is not healthy after it
|
|
11
|
+
#
|
|
5
12
|
@@USAGE@@
|
|
6
13
|
set -euo pipefail
|
|
7
14
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
@@ -14,7 +21,7 @@ out() { aws cloudformation describe-stacks --stack-name "$1" --query "Stacks[0].
|
|
|
14
21
|
BOX=$(out "$BOX_STACK" InstanceId)
|
|
15
22
|
DB_HOST=$(out "$RDS_STACK" DbEndpoint)
|
|
16
23
|
DB_SECRET=$(out "$RDS_STACK" DbSecretArn)
|
|
17
|
-
[ -n "$BOX" ] && [ "$BOX" != None ] || { echo "no instance in stack $BOX_STACK" >&2; exit
|
|
24
|
+
[ -n "$BOX" ] && [ "$BOX" != None ] || { echo "no instance in stack $BOX_STACK" >&2; exit 40; }
|
|
18
25
|
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
|
|
19
26
|
|
|
20
27
|
WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT
|
|
@@ -68,7 +75,7 @@ ROLL=$(jq -n --arg compose "$(B64 "$WORK/compose.json")" --arg secrets "$(B64 "$
|
|
|
68
75
|
executionTimeout: ["900"]}')
|
|
69
76
|
|
|
70
77
|
echo "==> rolling $BOX_STACK ($BOX) at $(date -u +%H:%M:%SZ)"
|
|
71
|
-
run_on_box "$ROLL" || { echo "==> the roll did not succeed" >&2; exit
|
|
78
|
+
run_on_box "$ROLL" || { echo "==> the roll did not succeed" >&2; exit 41; }
|
|
72
79
|
|
|
73
80
|
echo "==> checking the box"
|
|
74
81
|
CHECK=$(cat <<'CHECK_EOF'
|
|
@@ -81,5 +88,5 @@ if [ -z "$DOWN" ]; then echo "ok every container is up"; else echo "FAIL conta
|
|
|
81
88
|
CHECK_EOF
|
|
82
89
|
)
|
|
83
90
|
CHECK_JSON=$(jq -n --arg c "$(printf '%s' "$CHECK" | base64 | tr -d '\n')" '{commands: ["echo \($c) | base64 -d | bash"]}')
|
|
84
|
-
run_on_box "$CHECK_JSON" || { echo "==> the box is NOT healthy after the roll" >&2; exit
|
|
91
|
+
run_on_box "$CHECK_JSON" || { echo "==> the box is NOT healthy after the roll" >&2; exit 42; }
|
|
85
92
|
echo "==> box roll done ($(date -u +%H:%M:%SZ))"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# container runs now, read from the box's own Compose file, so rolling one service leaves the rest alone.
|
|
3
3
|
BOX_ID="$(aws cloudformation describe-stacks --stack-name "$BOX_STACK" --region "$REGION" \
|
|
4
4
|
--query "Stacks[0].Outputs[?OutputKey=='InstanceId'].OutputValue" --output text)"
|
|
5
|
-
[ -n "$BOX_ID" ] && [ "$BOX_ID" != None ] || { echo "no instance in stack ${BOX_STACK}" >&2; exit
|
|
5
|
+
[ -n "$BOX_ID" ] && [ "$BOX_ID" != None ] || { echo "no instance in stack ${BOX_STACK}" >&2; exit 33; }
|
|
6
6
|
|
|
7
7
|
echo "==> reading the tags the box runs now"
|
|
8
8
|
READ_JSON="$(jq -n --arg c "cat ${BOX_DIR}/compose.json 2>/dev/null || echo '{}'" '{commands: [$c]}')"
|
|
@@ -14,7 +14,7 @@ for _ in $(seq 1 30); do
|
|
|
14
14
|
--query Status --output text 2>/dev/null || echo Pending)"
|
|
15
15
|
[ "$state" != InProgress ] && [ "$state" != Pending ] && break
|
|
16
16
|
done
|
|
17
|
-
[ "$state" = Success ] || { echo "could not read the box's Compose file (${state}); nothing was rolled" >&2; exit
|
|
17
|
+
[ "$state" = Success ] || { echo "could not read the box's Compose file (${state}); nothing was rolled" >&2; exit 32; }
|
|
18
18
|
aws ssm get-command-invocation --command-id "$COMMAND_ID" --instance-id "$BOX_ID" --region "$REGION" \
|
|
19
19
|
--query StandardOutputContent --output text > "${SCRATCH_DIR}/compose.json"
|
|
20
20
|
|
|
@@ -14,13 +14,24 @@
|
|
|
14
14
|
@@MODE_SUMMARY@@
|
|
15
15
|
#
|
|
16
16
|
# It ends with smoke-after-deploy.sh, whose own exit codes (20-23) are distinct from a failed deploy
|
|
17
|
-
# step's. SKIP_POST_DEPLOY_SMOKE=1 skips it.
|
|
17
|
+
# step's. SKIP_POST_DEPLOY_SMOKE=1 skips it. `hecks deploy service_roll.run` runs this script with
|
|
18
|
+
# SMOKE_BY_COMMAND=1, which ends after the roll and leaves the smoke to the smoke_run.run its policy requests.
|
|
19
|
+
#
|
|
20
|
+
# Exit codes (the ones `service_roll.run` names; a step that fails under `set -e` ends with its own
|
|
21
|
+
# status, 1 from bash and 254/255 from the aws CLI):
|
|
22
|
+
# 2 unknown service
|
|
23
|
+
# 30 EXISTING_TAG is not in ECR 31 the fresh tag is already in ECR
|
|
24
|
+
# 32 the box's Compose file is unreadable 33 the box stack has no instance
|
|
25
|
+
# 34 the stack has no parameter for the container 35 the stack update failed or did not settle
|
|
26
|
+
# 36 a parameter other than the container's changed 37 the task definition lacks the pushed image
|
|
27
|
+
# 38 the task definition has no such container 40-42 deploy-box.sh's own
|
|
18
28
|
#
|
|
19
29
|
# Environment:
|
|
20
30
|
# LOCAL_IMAGE local image to push, default <ecr repository>:latest
|
|
21
31
|
# EXISTING_TAG redeploy this tag from ECR instead of building and pushing
|
|
22
32
|
# SETTLE_TIMEOUT_SECS default 1200, how long to wait for a stack update
|
|
23
33
|
# SKIP_POST_DEPLOY_SMOKE=1 passed through to smoke-after-deploy.sh
|
|
34
|
+
# SMOKE_BY_COMMAND=1 end after the roll and print `rolled taskdef=<td> tag=<tag>`; the caller smokes
|
|
24
35
|
set -euo pipefail
|
|
25
36
|
|
|
26
37
|
@@SETTINGS@@
|
|
@@ -49,12 +60,12 @@ if [ -n "${EXISTING_TAG:-}" ]; then
|
|
|
49
60
|
echo "==> reusing ${IMAGE_URI} (no push)"
|
|
50
61
|
aws ecr describe-images --repository-name "$ECR_REPOSITORY" --region "$REGION" \
|
|
51
62
|
--image-ids "imageTag=${TAG}" --query 'imageDetails[0].imageTags' --output text > /dev/null \
|
|
52
|
-
|| { echo "tag ${TAG} is not in ECR" >&2; exit
|
|
63
|
+
|| { echo "tag ${TAG} is not in ECR" >&2; exit 30; }
|
|
53
64
|
else
|
|
54
65
|
if aws ecr describe-images --repository-name "$ECR_REPOSITORY" --region "$REGION" \
|
|
55
66
|
--image-ids "imageTag=${TAG}" > /dev/null 2>&1; then
|
|
56
67
|
echo "tag ${TAG} is already in ECR; a tag is never reused. Run again." >&2
|
|
57
|
-
exit
|
|
68
|
+
exit 31
|
|
58
69
|
fi
|
|
59
70
|
echo "==> tagging ${LOCAL_IMAGE} as ${IMAGE_URI}"
|
|
60
71
|
docker tag "$LOCAL_IMAGE" "$IMAGE_URI"
|
|
@@ -74,6 +85,7 @@ fi
|
|
|
74
85
|
|
|
75
86
|
@@ROLL_STEPS@@
|
|
76
87
|
|
|
77
|
-
# The deploy itself is done. End with the smoke, which reports separately.
|
|
88
|
+
# The deploy itself is done. End with the smoke, which reports separately, unless the caller runs it.
|
|
78
89
|
echo "==> deploy succeeded"
|
|
90
|
+
[ -z "${SMOKE_BY_COMMAND:-}" ] || { echo "==> rolled taskdef=${TASKDEF:-} tag=${TAG}"; exit 0; }
|
|
79
91
|
bash ./smoke-after-deploy.sh
|
|
@@ -17,7 +17,7 @@ for key in $ALL_PARAM_KEYS; do
|
|
|
17
17
|
done
|
|
18
18
|
if [ "$found" != "1" ]; then
|
|
19
19
|
echo "stack ${IMAGE_STACK} has no parameter named ${CFN_PARAM_KEY}; the image was pushed but nothing was rolled" >&2
|
|
20
|
-
exit
|
|
20
|
+
exit 34
|
|
21
21
|
fi
|
|
22
22
|
|
|
23
23
|
if UPDATE_OUT="$(aws cloudformation update-stack --stack-name "$IMAGE_STACK" --region "$REGION" \
|
|
@@ -32,11 +32,11 @@ if UPDATE_OUT="$(aws cloudformation update-stack --stack-name "$IMAGE_STACK" --r
|
|
|
32
32
|
UPDATE_COMPLETE) echo " -> ${stack_status}"; break ;;
|
|
33
33
|
*ROLLBACK*|*FAILED*)
|
|
34
34
|
echo " -> ${stack_status}: the stack update failed, investigate before rolling anything" >&2
|
|
35
|
-
exit
|
|
35
|
+
exit 35 ;;
|
|
36
36
|
esac
|
|
37
37
|
if timed_out "$started"; then
|
|
38
38
|
echo "==> the stack update did not settle within ${SETTLE_TIMEOUT_SECS}s (last: ${stack_status})" >&2
|
|
39
|
-
exit
|
|
39
|
+
exit 35
|
|
40
40
|
fi
|
|
41
41
|
sleep 10
|
|
42
42
|
done
|
|
@@ -44,7 +44,7 @@ elif echo "$UPDATE_OUT" | grep -q "No updates are to be performed"; then
|
|
|
44
44
|
echo " -> the stack already has ${TAG}; nothing to update"
|
|
45
45
|
else
|
|
46
46
|
echo "$UPDATE_OUT" >&2
|
|
47
|
-
exit
|
|
47
|
+
exit 35
|
|
48
48
|
fi
|
|
49
49
|
|
|
50
50
|
echo "==> checking that only ${CFN_PARAM_KEY} changed"
|
|
@@ -56,7 +56,7 @@ CHANGED="$(jq -rn --slurpfile before "${SCRATCH_DIR}/params-before.json" --slurp
|
|
|
56
56
|
NOW="$(jq -r --arg k "$CFN_PARAM_KEY" '.[] | select(.k == $k) | .v' "${SCRATCH_DIR}/params-after.json")"
|
|
57
57
|
if [ "$NOW" != "$TAG" ] || { [ -n "$CHANGED" ] && [ "$CHANGED" != "$CFN_PARAM_KEY" ]; }; then
|
|
58
58
|
echo "expected only ${CFN_PARAM_KEY} to change to ${TAG}; it is ${NOW} and changed: ${CHANGED:-nothing}" >&2
|
|
59
|
-
exit
|
|
59
|
+
exit 36
|
|
60
60
|
fi
|
|
61
61
|
echo " -> ${CFN_PARAM_KEY} is ${TAG}; no other parameter changed"
|
|
62
62
|
|
|
@@ -69,7 +69,7 @@ RUNNING_IMAGE="$(aws ecs describe-task-definition --task-definition "$TD" --regi
|
|
|
69
69
|
--query "taskDefinition.containerDefinitions[?name=='${SERVICE_NAME}'].image | [0]" --output text)"
|
|
70
70
|
if [ "$RUNNING_IMAGE" != "$IMAGE_URI" ]; then
|
|
71
71
|
echo "task definition ${TD} has ${RUNNING_IMAGE}, not ${IMAGE_URI}; refusing to roll" >&2
|
|
72
|
-
exit
|
|
72
|
+
exit 37
|
|
73
73
|
fi
|
|
74
74
|
echo "==> task definition ${TD} carries ${SERVICE_NAME}=${TAG}"
|
|
75
75
|
|
|
@@ -6,7 +6,7 @@ active_image() {
|
|
|
6
6
|
}
|
|
7
7
|
CURRENT_IMAGE="$(active_image)"
|
|
8
8
|
case "$CURRENT_IMAGE" in
|
|
9
|
-
None|"") echo "task definition family ${FAMILY} has no container named ${SERVICE_NAME}" >&2; exit
|
|
9
|
+
None|"") echo "task definition family ${FAMILY} has no container named ${SERVICE_NAME}" >&2; exit 38 ;;
|
|
10
10
|
esac
|
|
11
11
|
ECR_REPOSITORY="${CURRENT_IMAGE#*/}"
|
|
12
12
|
ECR_REPOSITORY="${ECR_REPOSITORY%%[:@]*}"
|
|
@@ -8,15 +8,21 @@ URL ?= @@URL@@
|
|
|
8
8
|
|
|
9
9
|
.PHONY: deploy-service smoke-after-deploy check-era
|
|
10
10
|
|
|
11
|
-
# Pushes one container's already-built local image under a fresh tag and rolls it onto the box
|
|
12
|
-
#
|
|
11
|
+
# Pushes one container's already-built local image under a fresh tag and rolls it onto the box, as the
|
|
12
|
+
# service_roll.run command: it runs deploy-service.sh, records the roll in the Hecks database
|
|
13
|
+
# (HECKS_DATABASE, default postgres://hecks@localhost/hecks) and requests the smoke. Build the image
|
|
14
|
+
# first; see deploy-service.sh. EXISTING_TAG=<tag> redeploys a tag already in ECR. Without the
|
|
15
|
+
# database the script runs by itself and the target exits non-zero (24 if it all passed).
|
|
13
16
|
deploy-service:
|
|
14
|
-
|
|
17
|
+
@@SERVICE_RECIPE@@
|
|
15
18
|
|
|
16
|
-
# Waits for the last roll to settle, then dispatches and follows the smoke.
|
|
19
|
+
# Waits for the last roll to settle, then dispatches and follows the smoke, as the smoke_run.run
|
|
20
|
+
# command, which runs smoke-after-deploy.sh and records how it ended in the Hecks database
|
|
21
|
+
# (HECKS_DATABASE, default postgres://hecks@localhost/hecks). Without that database the smoke still
|
|
22
|
+
# runs and its result is printed, and the target exits non-zero (24 if the smoke passed).
|
|
17
23
|
# SKIP_POST_DEPLOY_SMOKE=1 opts out, DRY_RUN=1 dispatches nothing.
|
|
18
24
|
smoke-after-deploy:
|
|
19
|
-
|
|
25
|
+
@@SMOKE_RECIPE@@
|
|
20
26
|
|
|
21
27
|
# Compares the era a live host reports at GET /version with expected-era.
|
|
22
28
|
check-era:
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
# direction (swap the hosts and secrets, and SRC_DB and DST_DB)
|
|
10
10
|
# SRC_DB the source database (default @@SOURCE_DATABASE@@)
|
|
11
11
|
# DST_DB the target database (default @@DATABASE@@)
|
|
12
|
+
# VERIFY_BY_COMMAND=1 end after the copy and leave the comparison to `hecks deploy data_copy.restore`
|
|
13
|
+
#
|
|
14
|
+
# Ends 60 (a client older than 16), 61 (the target already has a schema), 62 (unexpected restore
|
|
15
|
+
# errors), or, run by hand, verify-copy.sh's 50 (the databases differ).
|
|
12
16
|
#
|
|
13
17
|
# Schemas copied: @@SCHEMAS@@
|
|
14
18
|
#
|
|
@@ -34,7 +38,7 @@ if [ "$(pg_major pg_dump || echo 0)" -lt 16 ] && [ -x /opt/homebrew/opt/postgres
|
|
|
34
38
|
export PATH=/opt/homebrew/opt/postgresql@16/bin:$PATH
|
|
35
39
|
fi
|
|
36
40
|
for t in pg_dump pg_restore psql; do
|
|
37
|
-
[ "$(pg_major $t || echo 0)" -ge 16 ] || { echo "need $t 16 or newer on the PATH" >&2; exit
|
|
41
|
+
[ "$(pg_major $t || echo 0)" -ge 16 ] || { echo "need $t 16 or newer on the PATH" >&2; exit 60; }
|
|
38
42
|
done
|
|
39
43
|
|
|
40
44
|
WORK=$(mktemp -d)
|
|
@@ -66,7 +70,7 @@ echo "source $(SRC 'show server_version'), target $(DST 'show server_version')"
|
|
|
66
70
|
for S in $SCHEMAS; do
|
|
67
71
|
if [ "$(DST "select count(*) from pg_namespace where nspname='$S'")" != 0 ]; then
|
|
68
72
|
if [ "${FORCE:-}" = 1 ]; then DST "drop schema \"$S\" cascade" >/dev/null; echo "dropped target schema $S"
|
|
69
|
-
else echo "target already has schema $S; re-run with FORCE=1 to replace it" >&2; exit
|
|
73
|
+
else echo "target already has schema $S; re-run with FORCE=1 to replace it" >&2; exit 61; fi
|
|
70
74
|
fi
|
|
71
75
|
done
|
|
72
76
|
|
|
@@ -77,7 +81,7 @@ for S in $SCHEMAS; do
|
|
|
77
81
|
TOTAL=$(grep -c 'error:' "$WORK/err_$S.txt" || true)
|
|
78
82
|
UNEXPECTED=$(grep 'error:' "$WORK/err_$S.txt" | grep -vc 'hecks_tr_extract' || true)
|
|
79
83
|
echo " restore errors: $TOTAL (unexpected: $UNEXPECTED)"
|
|
80
|
-
if [ "$UNEXPECTED" != 0 ]; then grep 'error:' "$WORK/err_$S.txt" | grep -v 'hecks_tr_extract' | head -5 >&2; exit
|
|
84
|
+
if [ "$UNEXPECTED" != 0 ]; then grep 'error:' "$WORK/err_$S.txt" | grep -v 'hecks_tr_extract' | head -5 >&2; exit 62; fi
|
|
81
85
|
done
|
|
82
86
|
|
|
83
87
|
# Refresh what pg_restore could not (with the schema on the search_path).
|
|
@@ -88,4 +92,5 @@ for S in $SCHEMAS; do
|
|
|
88
92
|
done
|
|
89
93
|
|
|
90
94
|
# Same structure, same exact row counts in every table, nothing unpopulated.
|
|
95
|
+
[ -z "${VERIFY_BY_COMMAND:-}" ] || exit 0
|
|
91
96
|
A_DB=$SRC_DB B_DB=$DST_DB bash "$HERE/verify-copy.sh" "$BASTION" "$SRC_HOST" "$SRC_SECRET" "$DST_HOST" "$DST_SECRET"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# A_DB, B_DB the database on each side (default @@DATABASE@@)
|
|
9
9
|
#
|
|
10
10
|
# Schemas compared: @@SCHEMAS@@
|
|
11
|
-
# Exit 0 only if both are identical and no materialized view is unpopulated in either.
|
|
11
|
+
# Exit 0 only if both are identical and no materialized view is unpopulated in either; 50 when they differ.
|
|
12
12
|
set -euo pipefail
|
|
13
13
|
|
|
14
14
|
BASTION=${1:?bastion instance id}; A_HOST=${2:?}; A_SECRET=${3:?}; B_HOST=${4:?}; B_SECRET=${5:?}
|
|
@@ -54,5 +54,5 @@ A "$COUNTS" >"$WORK/counts_a"; B "$COUNTS" >"$WORK/counts_b"
|
|
|
54
54
|
[ "$(A "$UNPOP")" = 0 ] && [ "$(B "$UNPOP")" = 0 ] || { echo "FAIL: unpopulated materialized views" >&2; BAD=1; }
|
|
55
55
|
diff "$WORK/shape_a" "$WORK/shape_b" >/dev/null || { echo "FAIL: structure differs" >&2; diff "$WORK/shape_a" "$WORK/shape_b" >&2 || true; BAD=1; }
|
|
56
56
|
diff "$WORK/counts_a" "$WORK/counts_b" >/dev/null || { echo "FAIL: row counts differ" >&2; diff "$WORK/counts_a" "$WORK/counts_b" >&2 || true; BAD=1; }
|
|
57
|
-
[ "$BAD" = 0 ] || exit
|
|
57
|
+
[ "$BAD" = 0 ] || exit 50
|
|
58
58
|
echo "OK: $(wc -l <"$WORK/counts_a" | tr -d ' ') tables, identical row counts and structure"
|
|
@@ -2,6 +2,7 @@ require "json"
|
|
|
2
2
|
require_relative "../../projector"
|
|
3
3
|
require_relative "box/settings"
|
|
4
4
|
require_relative "box/hosting"
|
|
5
|
+
require_relative "box/roll_recipe"
|
|
5
6
|
|
|
6
7
|
module Hecks
|
|
7
8
|
module Projections
|
|
@@ -28,6 +29,27 @@ module Hecks
|
|
|
28
29
|
"# after adding one: the admin API is off, so a reload cannot reach it.\n" \
|
|
29
30
|
"import /etc/caddy/extra/*\n".freeze
|
|
30
31
|
|
|
32
|
+
# The `smoke-after-deploy` recipe: the smoke runs as `smoke_run.run`, which finds
|
|
33
|
+
# `smoke-after-deploy.sh` beside the Makefile and records how it ended in the Hecks
|
|
34
|
+
# database. When that database cannot be opened the command never starts, so the recipe
|
|
35
|
+
# runs the script itself, states the database error apart from the smoke's result, and
|
|
36
|
+
# exits non-zero (24 when the smoke passed).
|
|
37
|
+
SMOKE_RECIPE = [
|
|
38
|
+
"@err=$$(mktemp); \\",
|
|
39
|
+
'$(HECKS) deploy smoke_run.run project="$(CURDIR)" --wait $(if $(TASKDEF),taskdef=$(TASKDEF)) 2>"$$err"; rc=$$?; \\',
|
|
40
|
+
'if [ $$rc -ne 0 ] && grep -q \'^cannot open Hecks\' "$$err"; then \\',
|
|
41
|
+
' echo "==> the deploy record was NOT written: $$(grep -m1 \'^cannot open Hecks\' "$$err")" >&2; \\',
|
|
42
|
+
' echo " one-time setup on this machine: a database AND a non-superuser role that owns it" >&2; \\',
|
|
43
|
+
' echo " (a superuser skips the era write-fence, so createdb alone fails), then set" >&2; \\',
|
|
44
|
+
' echo " HECKS_DATABASE=postgres://<role>@localhost/<db>" >&2; \\',
|
|
45
|
+
' echo " running the smoke anyway so its result is not lost" >&2; \\',
|
|
46
|
+
' rm -f "$$err"; TASKDEF="$(TASKDEF)" bash ./smoke-after-deploy.sh; rc=$$?; \\',
|
|
47
|
+
' echo "==> smoke exit $$rc. The deploy record was NOT written: the database is unavailable (see above)." >&2; \\',
|
|
48
|
+
" [ $$rc -ne 0 ] || rc=24; exit $$rc; \\",
|
|
49
|
+
"fi; \\",
|
|
50
|
+
'cat "$$err" >&2; rm -f "$$err"; exit $$rc'
|
|
51
|
+
].join("\n\t").freeze
|
|
52
|
+
|
|
31
53
|
module_function
|
|
32
54
|
|
|
33
55
|
# Generates `rds.yaml`, `box.yaml`, `Caddyfile`, `services.json`, `render-compose.sh`,
|
|
@@ -557,6 +579,8 @@ module Hecks
|
|
|
557
579
|
# Elastic IP. The default is a production pair, with deletion protection.
|
|
558
580
|
REHEARSAL ?= false
|
|
559
581
|
|
|
582
|
+
HECKS ?= hecks
|
|
583
|
+
|
|
560
584
|
.PHONY: stacks deploy
|
|
561
585
|
stacks:
|
|
562
586
|
\taws cloudformation deploy --template-file rds.yaml --stack-name $(RDS_STACK) --capabilities CAPABILITY_IAM \\
|
|
@@ -569,20 +593,9 @@ module Hecks
|
|
|
569
593
|
\t\tRehearsal=$(REHEARSAL)
|
|
570
594
|
|
|
571
595
|
deploy:
|
|
572
|
-
|
|
596
|
+
#{RollRecipe.deploy(plan)}
|
|
573
597
|
MAKE
|
|
574
|
-
"#{base}
|
|
575
|
-
end
|
|
576
|
-
|
|
577
|
-
# With hosting scripts, a deploy ends with the post-deploy smoke and the Makefile includes
|
|
578
|
-
# the fragment that adds the hosting targets.
|
|
579
|
-
#
|
|
580
|
-
# @param plan [Settings::Plan] the resolved settings
|
|
581
|
-
# @return [String] the Makefile lines that follow the `deploy` recipe, or nothing
|
|
582
|
-
def hosting_makefile_tail(plan)
|
|
583
|
-
return "" unless plan.hosting
|
|
584
|
-
|
|
585
|
-
"\t#{'TASKDEF="$(TASKDEF)" ' if plan.task_definition}bash ./smoke-after-deploy.sh\n\ninclude hosting.mk\n"
|
|
598
|
+
plan.hosting ? "#{base}\ninclude hosting.mk\n" : base
|
|
586
599
|
end
|
|
587
600
|
|
|
588
601
|
# Fills `@@NAME@@` markers. A marker alone on its line is replaced together with the line,
|