hecks 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
  3. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  4. data/lib/hecks/adapters/driven/prism.rb +1 -1
  5. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  6. data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
  7. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
  8. data/lib/hecks/bluebook/dsl/world_builder.rb +42 -3
  9. data/lib/hecks/bluebook/hexagon.rb +15 -7
  10. data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
  11. data/lib/hecks/bluebook/model_check.rb +1 -1
  12. data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
  13. data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
  14. data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
  15. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
  16. data/lib/hecks/embryonaut_bluebook.rb +29 -3
  17. data/lib/hecks/forms/value_object_shape.rb +1 -1
  18. data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
  19. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  20. data/lib/hecks/fuzzing/isolated_boot.rb +59 -5
  21. data/lib/hecks/language/world/world.bluebook +32 -2
  22. data/lib/hecks/ports/key_vault.rb +1 -1
  23. data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
  24. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +2 -1
  25. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
  26. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
  27. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +1 -1
  28. data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
  29. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
  30. data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
  31. data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
  32. data/lib/hecks/ports/persistence.rb +4 -0
  33. data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
  34. data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
  35. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
  36. data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
  37. data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
  38. data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
  39. data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
  40. data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
  41. data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
  42. data/lib/hecks/projections/deploy/fargate.rb +187 -138
  43. data/lib/hecks/projections/deploy/lambda.rb +5 -5
  44. data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
  45. data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
  46. data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
  47. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
  48. data/lib/hecks/projections/deploy/preview/script.rb +40 -0
  49. data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
  50. data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
  51. data/lib/hecks/projections/deploy/preview/template.rb +247 -0
  52. data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
  53. data/lib/hecks/projections/deploy/preview.rb +103 -0
  54. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
  55. data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
  56. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
  57. data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
  58. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
  59. data/lib/hecks/projections/deploy/scripts.rb +171 -0
  60. data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
  61. data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
  62. data/lib/hecks/projections/deploy/smoke.rb +159 -0
  63. data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
  64. data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
  65. data/lib/hecks/projections/deploy/template_diff.rb +173 -0
  66. data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
  67. data/lib/hecks/projections.rb +2 -0
  68. data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
  69. data/lib/hecks/release/gem_pin.rb +112 -0
  70. data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
  71. data/lib/hecks/runtime/loader.rb +26 -2
  72. data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
  73. data/lib/hecks/runtime/registry/verification.rb +4 -2
  74. data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
  75. data/lib/hecks/runtime/registry.rb +14 -9
  76. data/lib/hecks/runtime/tenant_check.rb +1 -1
  77. data/lib/hecks/vendoring/git_environment.rb +35 -0
  78. data/lib/hecks/vendoring/git_source.rb +102 -0
  79. data/lib/hecks/vendoring.rb +102 -0
  80. data/lib/hecks/version.rb +1 -1
  81. data/lib/hecks.rb +2 -1
  82. metadata +45 -1
@@ -0,0 +1,48 @@
1
+ require "json"
2
+
3
+ module Hecks
4
+ module Projections
5
+ module Deploy
6
+ module Preview
7
+ # Text helpers the preview template's sections share: indentation and the
8
+ # rendering of a value as a YAML scalar or CloudFormation intrinsic.
9
+ module YamlText
10
+ PREVIEW_URL_TOKEN = "{{preview_url}}".freeze
11
+ PREVIEW_URL_SUB = "https://${PreviewDistribution.DomainName}".freeze
12
+
13
+ module_function
14
+
15
+ # Indents every non-empty line of a block.
16
+ #
17
+ # @param text [String] the block
18
+ # @param width [Integer] spaces to add
19
+ # @return [String] the indented block
20
+ def indent(text, width)
21
+ pad = " " * width
22
+ text.each_line.map { |line| line.strip.empty? ? line : "#{pad}#{line}" }.join
23
+ end
24
+
25
+ # Renders one environment or property value as a YAML scalar or intrinsic.
26
+ #
27
+ # @param value [String] a literal, optionally holding the `{{preview_url}}` token
28
+ # @return [String] a double-quoted scalar, or a `!Sub` when the token is present
29
+ def scalar(value)
30
+ return JSON.generate(value) unless value.include?(PREVIEW_URL_TOKEN)
31
+
32
+ "!Sub #{JSON.generate(value.gsub(PREVIEW_URL_TOKEN, PREVIEW_URL_SUB))}"
33
+ end
34
+
35
+ # Names the logical id of a container's generated secret.
36
+ #
37
+ # @param container [Containers::Entry] the container that declares the secret
38
+ # @param name [String] the environment variable name
39
+ # @return [String] the logical id, ending in `Secret`
40
+ def secret_id(container, name)
41
+ stem = "#{container.logical}#{name.sub(/_ARN\z/, '').split('_').map(&:capitalize).join}"
42
+ stem.end_with?("Secret") ? stem : "#{stem}Secret"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,103 @@
1
+ require_relative "preview/settings"
2
+ require_relative "preview/template"
3
+ require_relative "preview/script"
4
+
5
+ module Hecks
6
+ module Projections
7
+ module Deploy
8
+ # Per-branch preview stacks for a `deployed_to("AwsFargate")` domain: one
9
+ # isolated copy of the stack per git branch, created and torn down by a
10
+ # generated script.
11
+ #
12
+ # Opt-in. A domain with no `preview` setting gets exactly the files it
13
+ # got before; `Fargate.call` asks `requested?` and merges in `call`'s
14
+ # files only when the answer is yes.
15
+ #
16
+ # ## What is generated
17
+ #
18
+ # - `preview.yaml`: a separate CloudFormation template, so a preview never
19
+ # appears in a change set against the live stack (see `Template`).
20
+ # - `preview.sh`: `deploy` (create or update), `destroy`, `list`, `url`,
21
+ # `name`, `ensure-database` and `login` (see `Script`).
22
+ # - The database step is a one-shot task declared in `preview.yaml`
23
+ # and run by `preview.sh`; there is no separate file, because the
24
+ # instance is reachable only from inside the VPC.
25
+ #
26
+ # ## Settings
27
+ #
28
+ # ```ruby
29
+ # deployed_to("AwsFargate") do
30
+ # region "us-east-1"
31
+ # preview do
32
+ # containers [{ name: "web", port: 8080, host: true, default: true }]
33
+ # end
34
+ # end
35
+ # ```
36
+ #
37
+ # `preview true` and a block with no settings accept every default. Keys:
38
+ #
39
+ # - `prefix`: stack, cluster, service and image repository name prefix.
40
+ # Default `<stack_prefix>-<stack>-preview`.
41
+ # - `alb_prefix`: load balancer name prefix, at most 11 characters.
42
+ # Default the first 8 letters of the stack name plus `pv`.
43
+ # - `owner_stack`: the stack whose VPC, subnets and compute security group
44
+ # the preview attaches to. Default the `owner_stack` of a Shared main
45
+ # stack, else the main stack.
46
+ # - `database_stack`: the stack whose outputs name the shared database
47
+ # instance. Default `owner_stack`.
48
+ # - `database_endpoint_output`, `database_secret_output`: the output keys
49
+ # holding the instance host and the master secret ARN. Defaults
50
+ # `DatabaseEndpoint` and `DatabaseSecretArn`.
51
+ # - `db_prefix`: a preview's database is `<db_prefix>_pv_<env>`. Default
52
+ # the stack name, lowercased, at most 30 characters.
53
+ # - `protected_databases`: names the database task refuses, added to the
54
+ # main database, `postgres` and the two template databases. Default `[]`.
55
+ # - `protected_branches`: branches `preview.sh` refuses. Default
56
+ # `["main", "master"]`.
57
+ # - `cpu`, `memory`: task size. Default the main stack's.
58
+ # - `log_retention_days`: a value CloudWatch accepts. Default `7`.
59
+ # - `session_cookie`: cookie name; sets `HECKS_SESSION_COOKIE` on the host
60
+ # and is what `login` writes. Default the host's own default.
61
+ # - `first_admin`: whether `preview.sh` signs the deployer up and offers
62
+ # `login`. Default `true`.
63
+ # - `signup_path`: the host route the signup posts to. Default `/signups`.
64
+ # - `landing_path`: the page `login` points the browser at. Default `/`.
65
+ # - `db_init_image`: image of the database task; needs `sh` and `psql`.
66
+ # Default the public Postgres 16 alpine image.
67
+ # - `containers`: the containers the preview task runs. Default the main
68
+ # stack's container list; see `Containers` for the entry keys.
69
+ #
70
+ # ## Why a preview mints no era and needs no bastion
71
+ #
72
+ # The host mints era 1 itself on an empty database, and the database
73
+ # task runs inside the VPC, so neither the main stack's bastion nor a
74
+ # tunnel is involved.
75
+ module Preview
76
+ module_function
77
+
78
+ # Answers whether a domain opted in to previews.
79
+ #
80
+ # @param deploy_settings [Hash{Symbol => Object}] the `deployed_to` settings
81
+ # @return [Boolean] true when a `preview` setting is present and not `false`
82
+ def requested?(deploy_settings)
83
+ value = deploy_settings[:preview]
84
+ !value.nil? && value != false
85
+ end
86
+
87
+ # Generates the preview files for one domain.
88
+ #
89
+ # @param deploy_settings [Hash{Symbol => Object}] the `deployed_to` settings
90
+ # @param main [Hash{Symbol => Object}] the main stack's facts; see `Settings.build`
91
+ # @return [Hash{String => String}] `"preview.yaml"` and `"preview.sh"`, or `{}` when
92
+ # the domain did not opt in
93
+ # @raise [ArgumentError] if a preview setting is unknown or malformed
94
+ def call(deploy_settings:, main:)
95
+ return {} unless requested?(deploy_settings)
96
+
97
+ settings = Settings.build(deploy_settings[:preview], deploy_settings: deploy_settings, main: main)
98
+ { "preview.yaml" => Template.render(settings), "preview.sh" => Script.render(settings) }
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env bash
2
+ # GENERATED by bin/project_deploy -- re-run it to refresh this file rather
3
+ # than hand-editing. Settings come from the domain's deployed_to("AwsFargate")
4
+ # block; see Hecks::Projections::Deploy::Scripts for each key.
5
+ #
6
+ # Deploys ONE container's freshly built local image to the live ECS service,
7
+ # under a fresh, unique ECR tag.
8
+ #
9
+ # bash deploy-service.sh <service>
10
+ #
11
+ # Why it exists: `docker push` of a floating tag plus
12
+ # `aws ecs update-service --force-new-deployment` looks like a deploy, but the
13
+ # task definition still names the old tag, so the new image never runs. This
14
+ # script reads the CURRENTLY ACTIVE task definition (the real source of truth,
15
+ # not the template's parameters), pushes the local image under a never-reused
16
+ # tag (<service>-<UTC timestamp>), swaps only that one container's image, and
17
+ # registers and deploys the result. Every other container keeps its image.
18
+ #
19
+ # It then syncs the CloudFormation stack's matching *ImageTag parameter, with
20
+ # --use-previous-template and UsePreviousValue for every other parameter, and
21
+ # checks that only that one parameter changed. The template stays the source of
22
+ # truth for what is live instead of drifting from it.
23
+ #
24
+ # Environment:
25
+ # LOCAL_IMAGE local image to push, default <ecr repository>:latest
26
+ # SETTLE_TIMEOUT_SECS default 1200, how long to wait for a rollout or stack update
27
+ # SKIP_POST_DEPLOY_SMOKE=1 passed through to smoke-after-deploy.sh
28
+ set -euo pipefail
29
+
30
+ #@@SETTINGS@@
31
+
32
+ #@@SERVICES@@
33
+
34
+ SERVICE_NAME="${1:?usage: deploy-service.sh <service> (one of: ${SERVICES})}"
35
+ resolve_service "$SERVICE_NAME"
36
+
37
+ SETTLE_TIMEOUT_SECS="${SETTLE_TIMEOUT_SECS:-1200}"
38
+ LOCAL_IMAGE="${LOCAL_IMAGE:-${ECR_REPOSITORY}:latest}"
39
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
40
+
41
+ ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
42
+ ECR_HOST="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com"
43
+ TAG="${SERVICE_NAME}-$(date -u +%Y%m%d%H%M%S)"
44
+ IMAGE_URI="${ECR_HOST}/${ECR_REPOSITORY}:${TAG}"
45
+ SCRATCH_DIR="$(mktemp -d)"
46
+ trap 'rm -rf "$SCRATCH_DIR"' EXIT
47
+
48
+ # Seconds elapsed since $1 (a value of $SECONDS) exceeds the settle timeout.
49
+ timed_out() { [ $((SECONDS - $1)) -gt "$SETTLE_TIMEOUT_SECS" ]; }
50
+
51
+ echo "==> tagging ${LOCAL_IMAGE} as ${IMAGE_URI}"
52
+ docker tag "$LOCAL_IMAGE" "$IMAGE_URI"
53
+
54
+ # A private docker config with no credential helper: a helper that blocks on a
55
+ # keychain prompt hangs forever in any unattended shell.
56
+ echo "==> ecr login"
57
+ DOCKER_CONFIG_DIR="${SCRATCH_DIR}/docker-config"
58
+ mkdir -p "$DOCKER_CONFIG_DIR"
59
+ echo '{}' > "${DOCKER_CONFIG_DIR}/config.json"
60
+ aws ecr get-login-password --region "$REGION" \
61
+ | docker --config "$DOCKER_CONFIG_DIR" login --username AWS --password-stdin "$ECR_HOST"
62
+
63
+ echo "==> pushing ${IMAGE_URI}"
64
+ docker --config "$DOCKER_CONFIG_DIR" push "$IMAGE_URI"
65
+
66
+ echo "==> reading the ACTIVE task definition (the real source of truth)"
67
+ CURRENT_TASKDEF="$(aws ecs describe-services --cluster "$CLUSTER" --services "$ECS_SERVICE" \
68
+ --region "$REGION" --query 'services[0].taskDefinition' --output text)"
69
+ aws ecs describe-task-definition --task-definition "$CURRENT_TASKDEF" --region "$REGION" \
70
+ --query 'taskDefinition' --output json > "${SCRATCH_DIR}/current.json"
71
+
72
+ SERVICE_NAME="$SERVICE_NAME" IMAGE_URI="$IMAGE_URI" python3 - "${SCRATCH_DIR}/current.json" "${SCRATCH_DIR}/new.json" <<'PYEOF'
73
+ import json, os, sys
74
+
75
+ service_name = os.environ["SERVICE_NAME"]
76
+ image_uri = os.environ["IMAGE_URI"]
77
+ with open(sys.argv[1]) as f:
78
+ td = json.load(f)
79
+ found = False
80
+ for c in td["containerDefinitions"]:
81
+ if c["name"] == service_name:
82
+ c["image"] = image_uri
83
+ found = True
84
+ if not found:
85
+ raise SystemExit("no container named %r in the active task definition" % service_name)
86
+ for key in ["taskDefinitionArn", "revision", "status", "requiresAttributes",
87
+ "compatibilities", "registeredAt", "registeredBy"]:
88
+ td.pop(key, None)
89
+ with open(sys.argv[2], "w") as f:
90
+ json.dump(td, f)
91
+ PYEOF
92
+
93
+ echo "==> registering a new task definition revision (only the ${SERVICE_NAME} image changes)"
94
+ NEW_TASKDEF_ARN="$(aws ecs register-task-definition --region "$REGION" \
95
+ --cli-input-json "file://${SCRATCH_DIR}/new.json" \
96
+ --query 'taskDefinition.taskDefinitionArn' --output text)"
97
+ echo " -> ${NEW_TASKDEF_ARN}"
98
+
99
+ echo "==> updating the service"
100
+ aws ecs update-service --cluster "$CLUSTER" --service "$ECS_SERVICE" --region "$REGION" \
101
+ --task-definition "$NEW_TASKDEF_ARN" --query 'service.taskDefinition' --output text
102
+
103
+ echo "==> waiting for the rollout to settle to a single deployment"
104
+ started=$SECONDS
105
+ while true; do
106
+ count="$(aws ecs describe-services --cluster "$CLUSTER" --services "$ECS_SERVICE" \
107
+ --region "$REGION" --query 'length(services[0].deployments)' --output text)"
108
+ [ "$count" = "1" ] && break
109
+ if timed_out "$started"; then
110
+ echo "==> the rollout did not settle within ${SETTLE_TIMEOUT_SECS}s; the service is still deploying" >&2
111
+ exit 1
112
+ fi
113
+ sleep 10
114
+ done
115
+
116
+ echo "==> done. now running:"
117
+ aws ecs describe-task-definition --task-definition "$NEW_TASKDEF_ARN" --region "$REGION" \
118
+ --query "taskDefinition.containerDefinitions[?name=='${SERVICE_NAME}'].image" --output text
119
+
120
+ echo "==> syncing CloudFormation's ${CFN_PARAM_KEY} so the stack stops drifting from reality"
121
+ stack_parameters() {
122
+ aws cloudformation describe-stacks --stack-name "$STACK" --region "$REGION" \
123
+ --query 'Stacks[0].Parameters[].[ParameterKey,ParameterValue]' --output json
124
+ }
125
+ stack_parameters > "${SCRATCH_DIR}/params-before.json"
126
+ ALL_PARAM_KEYS="$(aws cloudformation describe-stacks --stack-name "$STACK" --region "$REGION" \
127
+ --query 'Stacks[0].Parameters[].ParameterKey' --output text)"
128
+ read -r -a PARAM_KEYS <<< "$ALL_PARAM_KEYS"
129
+ CFN_ARGS=()
130
+ found=0
131
+ for key in "${PARAM_KEYS[@]}"; do
132
+ if [ "$key" = "$CFN_PARAM_KEY" ]; then
133
+ CFN_ARGS+=("ParameterKey=${key},ParameterValue=${TAG}")
134
+ found=1
135
+ else
136
+ CFN_ARGS+=("ParameterKey=${key},UsePreviousValue=true")
137
+ fi
138
+ done
139
+ if [ "$found" != "1" ]; then
140
+ echo "stack ${STACK} has no parameter named ${CFN_PARAM_KEY}; the service is deployed but the stack was not updated" >&2
141
+ exit 1
142
+ fi
143
+ aws cloudformation update-stack --stack-name "$STACK" --region "$REGION" \
144
+ --use-previous-template --capabilities CAPABILITY_IAM --parameters "${CFN_ARGS[@]}" \
145
+ --query 'StackId' --output text
146
+
147
+ echo "==> waiting for the CloudFormation stack update to settle"
148
+ started=$SECONDS
149
+ while true; do
150
+ stack_status="$(aws cloudformation describe-stacks --stack-name "$STACK" --region "$REGION" \
151
+ --query 'Stacks[0].StackStatus' --output text)"
152
+ case "$stack_status" in
153
+ UPDATE_COMPLETE) echo " -> ${stack_status}"; break ;;
154
+ *ROLLBACK*|*FAILED*)
155
+ echo " -> ${stack_status}: the CloudFormation update failed, investigate before relying on it" >&2
156
+ exit 1 ;;
157
+ esac
158
+ if timed_out "$started"; then
159
+ echo "==> the stack update did not settle within ${SETTLE_TIMEOUT_SECS}s (last: ${stack_status})" >&2
160
+ exit 1
161
+ fi
162
+ sleep 10
163
+ done
164
+
165
+ echo "==> checking that only ${CFN_PARAM_KEY} changed"
166
+ stack_parameters > "${SCRATCH_DIR}/params-after.json"
167
+ python3 - "${SCRATCH_DIR}/params-before.json" "${SCRATCH_DIR}/params-after.json" "$CFN_PARAM_KEY" "$TAG" <<'PYEOF'
168
+ import json, sys
169
+
170
+ before = dict(json.load(open(sys.argv[1])))
171
+ after = dict(json.load(open(sys.argv[2])))
172
+ key, tag = sys.argv[3], sys.argv[4]
173
+ changed = sorted(k for k in set(before) | set(after) if before.get(k) != after.get(k))
174
+ if changed != [key] or after.get(key) != tag:
175
+ raise SystemExit("expected only %s to change to %s, but changed: %s" % (key, tag, changed))
176
+ print(" -> %s is now %s; no other parameter changed" % (key, tag))
177
+ PYEOF
178
+
179
+ # The deploy itself is done. Finish with the smoke, whose own exit codes (20-23)
180
+ # are distinct from a failed deploy step's. SKIP_POST_DEPLOY_SMOKE=1 skips it.
181
+ echo "==> deploy succeeded"
182
+ bash "${SCRIPT_DIR}/smoke-after-deploy.sh"
@@ -0,0 +1,17 @@
1
+ # The domain eras the post-deploy check accepts as "the era that should be
2
+ # running", one era id per line. Blank lines and lines starting with # are
3
+ # ignored. While no era is listed the check only verifies that the host reports
4
+ # one, and does not compare it.
5
+ #
6
+ # Changing the era on a roll:
7
+ # 1. Add the NEW era id below and merge that BEFORE the roll, so a scheduled
8
+ # check does not fail the moment the new era boots.
9
+ # 2. Roll. The check then passes on the new era.
10
+ # 3. Once the roll has settled, delete the OLD era id, so a rollback to it, or
11
+ # a fork of the journal back onto it, fails the check instead of passing.
12
+ #
13
+ # The id is what the host reports at GET /version, the same id the era mint
14
+ # prints. Compare a live host with this file:
15
+ #
16
+ # bin/check_era https://host.example expected-era
17
+ # make check-era URL=https://host.example
@@ -0,0 +1,54 @@
1
+ # GENERATED by bin/project_deploy -- re-run it to refresh this file rather
2
+ # than hand-editing. Included by the Makefile beside it; settings come from the
3
+ # domain's deployed_to("AwsFargate") block (see
4
+ # Hecks::Projections::Deploy::Scripts for each key).
5
+ #
6
+ # HECKS_ROOT is a checkout of one pinned Hecks release, fetched once into a
7
+ # cache directory, so the domain image is built from the same Hecks on every
8
+ # machine. It replaces any path to a working copy: ROOT below points the
9
+ # Makefile's build and mint-era steps at it. Override HECKS_VERSION to build
10
+ # from another release, or HECKS_ROOT to use a checkout you manage yourself.
11
+
12
+ HECKS_VERSION ?= @@RELEASE@@
13
+ HECKS_SOURCE ?= @@SOURCE@@
14
+ HECKS_CACHE_DIR ?= @@CACHE_DIR@@
15
+ HECKS_ROOT ?= $(HECKS_CACHE_DIR)/hecks-$(HECKS_VERSION)
16
+ ROOT := $(HECKS_ROOT)
17
+ SERVICE ?= @@SERVICE@@
18
+ URL ?= @@URL@@
19
+
20
+ .PHONY: hecks-release deploy-service smoke-after-deploy check-era
21
+
22
+ build: hecks-release
23
+
24
+ # Fetches the pinned release's tag into HECKS_ROOT unless it is already there,
25
+ # and refuses a checkout that is not exactly that tag.
26
+ hecks-release:
27
+ @if [ -f "$(HECKS_ROOT)/bin/project_wasm" ]; then \
28
+ echo "hecks v$(HECKS_VERSION) is already at $(HECKS_ROOT)"; \
29
+ else \
30
+ echo "Fetching hecks v$(HECKS_VERSION) from $(HECKS_SOURCE) into $(HECKS_ROOT)..."; \
31
+ mkdir -p "$(HECKS_CACHE_DIR)" && \
32
+ git clone --quiet --depth 1 --branch "v$(HECKS_VERSION)" "$(HECKS_SOURCE)" "$(HECKS_ROOT)"; \
33
+ fi
34
+ @tag="$$(git -C "$(HECKS_ROOT)" describe --tags --exact-match 2>/dev/null)"; \
35
+ if [ "$$tag" != "v$(HECKS_VERSION)" ]; then \
36
+ echo "$(HECKS_ROOT) is at '$$tag', not v$(HECKS_VERSION). Remove it and re-run, or set HECKS_ROOT."; \
37
+ exit 1; \
38
+ fi
39
+
40
+ # Pushes one container's already-built local image under a fresh tag and rolls
41
+ # it out. Build the image first (docker-build); see deploy-service.sh.
42
+ deploy-service:
43
+ @echo "Deploying $(SERVICE) with deploy-service.sh (pushes its local image, rolls the ECS service)..."
44
+ bash ./deploy-service.sh $(SERVICE)
45
+
46
+ # Waits for the last roll to settle, then dispatches and follows the smoke.
47
+ # SKIP_POST_DEPLOY_SMOKE=1 opts out, DRY_RUN=1 dispatches nothing.
48
+ smoke-after-deploy:
49
+ bash ./smoke-after-deploy.sh
50
+
51
+ # Compares the era a live host reports at GET /version with expected-era.
52
+ check-era:
53
+ @test -n "$(URL)" || { echo "usage: make check-era URL=https://host.example"; exit 1; }
54
+ ruby "$(HECKS_ROOT)/bin/check_era" "$(URL)" expected-era
@@ -0,0 +1,114 @@
1
+ module Hecks
2
+ module Projections
3
+ module Deploy
4
+ module Scripts
5
+ # The `deployed_to("AwsFargate")` settings the hosting scripts read,
6
+ # with their defaults and the check each value must pass.
7
+ #
8
+ # Every value ends up inside a shell script or a Makefile, so each one
9
+ # is refused unless it matches a conservative pattern for what it names
10
+ # (a region, a cluster, a GitHub repository). That check is what lets
11
+ # the renderers write values without per-context escaping.
12
+ #
13
+ # The containers, their ECR repositories and image-tag parameters, and
14
+ # the default cluster and service names are not settings here. They come
15
+ # from the template generator's own resolved plan
16
+ # (`Fargate::Settings::Plan`), so a script can only name what the
17
+ # generated `template.yaml` defines.
18
+ #
19
+ # The keys are documented on `Scripts`.
20
+ class Settings
21
+ RESOURCE = /\A[a-zA-Z0-9][a-zA-Z0-9_-]*\z/
22
+ REGION = /\A[a-z]{2}(-[a-z]+)+-\d\z/
23
+ GITHUB_REPO = %r{\A[\w.-]+/[\w.-]+\z}
24
+ WORKFLOW = /\A[\w.-]+\.ya?ml\z/
25
+ REF = %r{\A[\w./-]+\z}
26
+ ERA = /\A[0-9a-zA-Z._-]+\z/
27
+ RELEASE = /\Av?\d+\.\d+\.\d+([.-][0-9A-Za-z.-]+)?\z/
28
+ SOURCE = %r{\A[\w.@:/~-]+\z}
29
+ MAKE_PATH = /\A[^\s#]+\z/
30
+
31
+ DEFAULT_SOURCE = "https://github.com/heckslabs/hecks.git".freeze
32
+ DEFAULT_CACHE_DIR = "$(HOME)/.cache/hecks".freeze
33
+
34
+ attr_reader :region, :stack, :cluster, :service, :containers, :hecks_release, :hecks_source,
35
+ :hecks_cache_dir, :smoke_repo, :smoke_workflow, :smoke_ref, :expected_eras, :public_url
36
+
37
+ # Reads and checks every hosting setting.
38
+ #
39
+ # @param deploy_settings [Hash{Symbol => Object}] the world's
40
+ # `deployed_to("AwsFargate")` settings
41
+ # @param plan [Fargate::Settings::Plan] the template generator's resolved settings:
42
+ # its `layout` holds the containers, its `names` the cluster and service
43
+ # @param stack_name [String] the CloudFormation stack
44
+ # @param region [String] the validated AWS region
45
+ # @raise [ArgumentError] if `hecks_release` is missing, or a value
46
+ # does not match the pattern for what it names
47
+ def initialize(deploy_settings:, plan:, stack_name:, region:)
48
+ @raw = deploy_settings
49
+ @region = check(:region, region, REGION)
50
+ @stack = check(:stack_name, stack_name, RESOURCE)
51
+ @cluster = check(:ecs_cluster, fetch(:ecs_cluster, plan.names.fetch(:cluster)), RESOURCE)
52
+ @service = check(:ecs_service, fetch(:ecs_service, plan.names.fetch(:service)), RESOURCE)
53
+ @containers = plan.layout.all
54
+ @hecks_release = read_release
55
+ @hecks_source = check(:hecks_source, fetch(:hecks_source, DEFAULT_SOURCE), SOURCE)
56
+ @hecks_cache_dir = check(:hecks_cache_dir, fetch(:hecks_cache_dir, DEFAULT_CACHE_DIR), MAKE_PATH)
57
+ read_smoke
58
+ @expected_eras = list(:expected_eras, []).each { |era| check(:expected_eras, era, ERA) }
59
+ @public_url = optional(:public_url, %r{\Ahttps?://[^\s'"$`\\]+\z})
60
+ end
61
+
62
+ private
63
+
64
+ def read_release
65
+ release = fetch(:hecks_release, nil)
66
+ raise ArgumentError, missing_release_message unless release
67
+
68
+ check(:hecks_release, release.to_s, RELEASE).delete_prefix("v")
69
+ end
70
+
71
+ def read_smoke
72
+ @smoke_repo = optional(:smoke_repo, GITHUB_REPO)
73
+ @smoke_workflow = optional(:smoke_workflow, WORKFLOW)
74
+ @smoke_ref = check(:smoke_ref, fetch(:smoke_ref, "main"), REF)
75
+ end
76
+
77
+ def missing_release_message
78
+ <<~MSG
79
+ deployed_to("AwsFargate") enables hosting_scripts but names no hecks_release. Pin one, e.g.:
80
+
81
+ deployed_to("AwsFargate") do
82
+ ...
83
+ hosting_scripts true
84
+ hecks_release "2.5.1"
85
+ end
86
+
87
+ so the domain image is built from a fixed Hecks release, not from a checkout on one machine.
88
+ MSG
89
+ end
90
+
91
+ def fetch(key, default)
92
+ @raw.key?(key) ? @raw[key] : default
93
+ end
94
+
95
+ def optional(key, pattern)
96
+ value = fetch(key, nil)
97
+ value.nil? ? nil : check(key, value.to_s, pattern)
98
+ end
99
+
100
+ def list(key, default)
101
+ Array(fetch(key, default)).map(&:to_s)
102
+ end
103
+
104
+ def check(key, value, pattern)
105
+ return value if value.is_a?(String) && value.match?(pattern)
106
+
107
+ raise ArgumentError,
108
+ "deployed_to(\"AwsFargate\") #{key} #{value.inspect} is not a valid value (expected #{pattern.inspect})"
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end