hecks 3.6.0 → 3.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/bluebook/meta_validator/syntax_boot/disk_cache.rb +28 -3
- data/lib/hecks/bluebook/meta_validator/verdict_cache/prebuilt.rb +44 -0
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +3 -17
- data/lib/hecks/cache_dir.rb +10 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +109 -0
- data/lib/hecks/deploy/oidc.json +10 -0
- data/lib/hecks/doors/usage_cache.rb +19 -9
- data/lib/hecks/fuzzing/coverage_campaign.rb +42 -17
- data/lib/hecks/fuzzing/failure_triage.rb +80 -0
- data/lib/hecks/fuzzing/form_census/chapter_forms.rb +38 -0
- data/lib/hecks/fuzzing/form_census/reference_forms.rb +49 -0
- data/lib/hecks/fuzzing/form_census.rb +28 -52
- data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +22 -7
- data/lib/hecks/fuzzing/isolated_boot/scratch_schema.rb +27 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +6 -2
- data/lib/hecks/fuzzing/mutation/checks.rb +76 -0
- data/lib/hecks/fuzzing/mutation/finders.rb +136 -0
- data/lib/hecks/fuzzing/mutation/judge.rb +94 -0
- data/lib/hecks/fuzzing/mutation/observation.rb +38 -0
- data/lib/hecks/fuzzing/mutation/operators.rb +52 -0
- data/lib/hecks/fuzzing/mutation/report.rb +78 -0
- data/lib/hecks/fuzzing/mutation/run.rb +99 -0
- data/lib/hecks/fuzzing/mutation/selection.rb +32 -0
- data/lib/hecks/fuzzing/mutation/site.rb +21 -0
- data/lib/hecks/fuzzing/mutation/source_map.rb +59 -0
- data/lib/hecks/fuzzing/mutation.rb +12 -0
- data/lib/hecks/fuzzing/properties/catalog.rb +10 -59
- data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +63 -0
- data/lib/hecks/fuzzing/properties/read_model_names.rb +50 -0
- data/lib/hecks/fuzzing/properties/read_models.rb +138 -0
- data/lib/hecks/fuzzing/properties/role_checks.rb +50 -0
- data/lib/hecks/fuzzing/properties.rb +7 -0
- data/lib/hecks/fuzzing/replay/observation.rb +1 -1
- data/lib/hecks/fuzzing/replay/role_check.rb +49 -0
- data/lib/hecks/fuzzing/replay/session.rb +22 -6
- data/lib/hecks/fuzzing/replay.rb +1 -0
- data/lib/hecks/fuzzing/runtime_coverage.rb +70 -0
- data/lib/hecks/fuzzing/shrinker/value_simplification.rb +64 -0
- data/lib/hecks/fuzzing/shrinker.rb +3 -0
- data/lib/hecks/fuzzing.rb +3 -0
- data/lib/hecks/gate/stages.yml +1 -1
- data/lib/hecks/hecks/adapters/deploy_toolchain/answers.rb +2 -1
- data/lib/hecks/hecks/adapters/github_rulesets.rb +7 -5
- data/lib/hecks/hecks/adapters/process_pool.rb +26 -4
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +37 -0
- data/lib/hecks/hecks/hecks.hecksagon +6 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +3 -0
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +6 -4
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +4 -0
- data/lib/hecks/projections/deploy/box/settings/types.rb +7 -3
- data/lib/hecks/projections/deploy/box/settings.rb +18 -0
- data/lib/hecks/projections/deploy/box/shared_database.rb +69 -0
- data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +6 -7
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/box/templates/provision-database.sh.tmpl +97 -0
- data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +4 -2
- data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +2 -1
- data/lib/hecks/projections/deploy/box.rb +14 -2
- data/lib/hecks/projections/deploy/shared_database_instance/templates/Makefile.tmpl +11 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/README.md.tmpl +25 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/rds.yaml.tmpl +180 -0
- data/lib/hecks/projections/deploy/shared_database_instance.rb +106 -0
- data/lib/hecks/projections/deploy/vercel/settings.rb +83 -0
- data/lib/hecks/projections/deploy/vercel.rb +134 -0
- data/lib/hecks/projections/site/site_host/templates.rb +12 -4
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +9 -2
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +1 -0
- data/lib/hecks/quality_control/cli/qa_sweep/seed_loop.rb +10 -2
- data/lib/hecks/rust_build/native_build.rb +29 -1
- data/lib/hecks/rust_build/wasm.rb +16 -3
- data/lib/hecks/tools/deploy_recipe/generation.rb +59 -0
- data/lib/hecks/tools/deploy_recipe/targets.rb +56 -2
- data/lib/hecks/tools/deploy_recipe/worlds.rb +2 -1
- data/lib/hecks/tools/deploy_recipe.rb +20 -10
- data/lib/hecks/tools/fuzz_sweep/options.rb +21 -10
- data/lib/hecks/tools/fuzz_sweep/reporting.rb +25 -3
- data/lib/hecks/tools/fuzz_sweep.rb +26 -9
- data/lib/hecks/tools/lanes/files.rb +2 -4
- data/lib/hecks/tools/lanes/live.rb +3 -4
- data/lib/hecks/tools/lanes/ruleset.rb +23 -1
- data/lib/hecks/tools/lanes.rb +12 -1
- data/lib/hecks/tools/mutation_run.rb +73 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -0
- data/prebuilt/945481eefbdb584649d45f256307053558966a868b9c38dee7ee88f8169610f8.marshal +0 -0
- data/prebuilt/a0818e2de67364dc1ab22868b6cbf869c62da6f8d9b924c778fa5cce2e76ee94.marshal +0 -0
- data/prebuilt/verdicts-be73f0df9eee0abe2f4aadc968321c4b919d19d424a3ccfe1777a71dabf7d9e7.json +1 -0
- data/qa/settings.yml +1 -0
- data/rust/host/Cargo.toml +2 -2
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +28 -5
- data/rust/host/src/auth.rs +11 -1
- data/rust/host/src/boundary_fuzz/api.rs +260 -0
- data/rust/host/src/boundary_fuzz/auth.rs +119 -0
- data/rust/host/src/boundary_fuzz/expr_json.rs +158 -0
- data/rust/host/src/boundary_fuzz/ir.rs +78 -0
- data/rust/host/src/boundary_fuzz/server.rs +227 -0
- data/rust/host/src/boundary_fuzz/web.rs +125 -0
- data/rust/host/src/fuzz_support.rs +165 -0
- data/rust/host/src/ir.rs +4 -0
- data/rust/host/src/main.rs +9 -4
- data/rust/host/src/secrets.rs +44 -0
- data/rust/host/src/server.rs +6 -1
- data/rust/host/src/web.rs +12 -2
- data/rust/parser/fuzz/.gitignore +4 -0
- data/rust/parser/fuzz/Cargo.lock +89 -0
- data/rust/parser/fuzz/Cargo.toml +27 -0
- data/rust/parser/fuzz/README.md +19 -0
- data/rust/parser/fuzz/fuzz_targets/chapter.rs +50 -0
- data/rust/parser/fuzz/fuzz_targets/resolve.rs +35 -0
- data/rust/parser/src/parse/command.rs +14 -2
- data/rust/parser/src/parse/mod.rs +25 -0
- data/rust/parser/src/ruby_value.rs +95 -6
- data/rust/src/kernel/json.rs +34 -3
- metadata +50 -1
|
@@ -51,10 +51,14 @@ module Hecks
|
|
|
51
51
|
:storage_gb, :backup_days, :snapshots_keep, :database_name, :engine_version,
|
|
52
52
|
:containers, :routes, :default_container, :origin_header, :origin_secret,
|
|
53
53
|
:secret_prefixes, :writable_secrets, :origin_env, :tunnel, :tunnel_service, :proxy_image,
|
|
54
|
-
:task_definition, :migration, :s3_buckets, :hosting, keyword_init: true
|
|
54
|
+
:task_definition, :migration, :s3_buckets, :hosting, :shared_database, keyword_init: true
|
|
55
55
|
) do
|
|
56
|
-
# @return [String] the CloudFormation stack that holds the database
|
|
57
|
-
|
|
56
|
+
# @return [String] the CloudFormation stack that holds the database: the shared
|
|
57
|
+
# instance's when the world names one, else this site's own
|
|
58
|
+
def rds_stack = shared_database || "#{stack_prefix}-#{infra_name}-rds"
|
|
59
|
+
|
|
60
|
+
# @return [Boolean] whether the database lives on an instance shared with other sites
|
|
61
|
+
def shared? = !shared_database.nil?
|
|
58
62
|
|
|
59
63
|
# @return [String] the CloudFormation stack that holds the box
|
|
60
64
|
def box_stack = "#{stack_prefix}-#{infra_name}-box"
|
|
@@ -47,6 +47,7 @@ module Hecks
|
|
|
47
47
|
def extra_fields(settings, task_definition, database_name)
|
|
48
48
|
{
|
|
49
49
|
task_definition: task_definition, migration: read_migration(settings[:migration], database_name),
|
|
50
|
+
shared_database: read_shared_database(settings),
|
|
50
51
|
s3_buckets: read_s3_access(settings.fetch(:s3_access, [])),
|
|
51
52
|
hosting: HostingSettings.read(settings, task_definition)
|
|
52
53
|
}
|
|
@@ -129,6 +130,23 @@ module Hecks
|
|
|
129
130
|
end.uniq(&:name)
|
|
130
131
|
end
|
|
131
132
|
|
|
133
|
+
# The stack of the RDS instance this site shares with others. The instance's size,
|
|
134
|
+
# storage, engine and backups are the instance's, so a world that sets them is refused.
|
|
135
|
+
#
|
|
136
|
+
# @param settings [Hash{Symbol => Object}] the world's `AwsBox` settings
|
|
137
|
+
# @return [String, nil] the shared stack's name, or nil for a database of its own
|
|
138
|
+
# @raise [ArgumentError] when the name is malformed or the world also sizes the instance
|
|
139
|
+
def read_shared_database(settings)
|
|
140
|
+
return nil unless settings.key?(:shared_database)
|
|
141
|
+
|
|
142
|
+
clash = SHARED_INSTANCE_SETTINGS & settings.keys
|
|
143
|
+
unless clash.empty?
|
|
144
|
+
raise ArgumentError, "shared_database: #{clash.join(", ")} size the instance, which belongs to the shared " \
|
|
145
|
+
"stack #{settings[:shared_database]}; drop #{clash.size == 1 ? "it" : "them"}"
|
|
146
|
+
end
|
|
147
|
+
check(:shared_database, settings[:shared_database], NAME)
|
|
148
|
+
end
|
|
149
|
+
|
|
132
150
|
def read_database_name(settings, infra_name)
|
|
133
151
|
check(:database_name, settings.fetch(:database_name, infra_name.gsub(/[^a-zA-Z0-9]/, "")), DB_NAME)
|
|
134
152
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Projections
|
|
3
|
+
module Deploy
|
|
4
|
+
module Box
|
|
5
|
+
# The parts of a box deploy that change when its database is one of several on a shared RDS
|
|
6
|
+
# instance, each client in its own database with its own login role (ADR 0092). Mixed into
|
|
7
|
+
# `Box`. Everything here returns the dedicated-instance text unchanged when the plan has no
|
|
8
|
+
# `shared_database`, so a site with its own instance generates the same files as before.
|
|
9
|
+
module SharedDatabase
|
|
10
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
11
|
+
# @return [Hash{String => String}] the Makefile markers that depend on where the database
|
|
12
|
+
# lives
|
|
13
|
+
def database_makefile_values(plan)
|
|
14
|
+
return dedicated_makefile_values unless plan.shared?
|
|
15
|
+
|
|
16
|
+
{ "WHAT" => "one app box and a database on the shared instance #{plan.rds_stack}",
|
|
17
|
+
"REHEARSAL_NOTE" => "# true gives the box no Elastic IP. It never deletes the shared instance\n" \
|
|
18
|
+
"# or this site's database.\n",
|
|
19
|
+
"RDS_DEPLOY" => "", "DB_SECRET_PARAM" => "\t\tDbSecretArn=$$(aws secretsmanager describe-secret --secret-id " \
|
|
20
|
+
"#{plan.infra_name}/database --query ARN --output text) \\",
|
|
21
|
+
"PHONY" => ".PHONY: stacks deploy provision", "PROVISION" => provision_target(plan) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
25
|
+
# @return [String] the line of `deploy-box.sh` that finds the secret the containers read
|
|
26
|
+
def database_secret_line(plan)
|
|
27
|
+
return 'DB_SECRET=$(out "$RDS_STACK" DbSecretArn)' unless plan.shared?
|
|
28
|
+
|
|
29
|
+
"DB_SECRET=$(aws secretsmanager describe-secret --secret-id #{plan.infra_name}/database --query ARN --output text)"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
33
|
+
# @return [String] `provision-database.sh`, which creates the site's database, role and
|
|
34
|
+
# secret
|
|
35
|
+
def provision_database_sh(plan)
|
|
36
|
+
template("provision-database.sh.tmpl", "STACK" => plan.infra_name, "RDS_STACK" => plan.rds_stack,
|
|
37
|
+
"DATABASE" => plan.database_name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def dedicated_makefile_values
|
|
43
|
+
{ "WHAT" => "one app box and one RDS instance",
|
|
44
|
+
"REHEARSAL_NOTE" => "# true makes a throwaway pair: the database is deleted with its stack and the box has no\n" \
|
|
45
|
+
"# Elastic IP. The default is a production pair, with deletion protection.\n",
|
|
46
|
+
"RDS_DEPLOY" => rds_deploy_lines, "DB_SECRET_PARAM" => dedicated_secret_param,
|
|
47
|
+
"PHONY" => ".PHONY: stacks deploy", "PROVISION" => "" }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def rds_deploy_lines
|
|
51
|
+
"\taws cloudformation deploy --template-file rds.yaml --stack-name $(RDS_STACK) --capabilities CAPABILITY_IAM \\\n" \
|
|
52
|
+
"\t\t--parameter-overrides VpcId=$(VPC) PrivateSubnetIds=$(PRIVATE_SUBNETS) Rehearsal=$(REHEARSAL)\n"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def dedicated_secret_param
|
|
56
|
+
"\t\tDbSecretArn=$$(aws cloudformation describe-stacks --stack-name $(RDS_STACK) " \
|
|
57
|
+
"--query \"Stacks[0].Outputs[?OutputKey=='DbSecretArn'].OutputValue\" --output text) \\"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def provision_target(plan)
|
|
61
|
+
"# Creates #{plan.infra_name}'s database, role and secret on #{plan.rds_stack}. Run it once; run it again with\n" \
|
|
62
|
+
"# ROTATE=--rotate to change the role's password. make provision BASTION=i-... [ROTATE=--rotate]\n" \
|
|
63
|
+
"provision:\n\tbash ./provision-database.sh $(BASTION) $(ROTATE)\n\n"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
# @@STACK@@:
|
|
1
|
+
# @@STACK@@: @@WHAT@@.
|
|
2
2
|
# make stacks VPC=vpc-... PRIVATE_SUBNETS=subnet-a,subnet-b PUBLIC_SUBNET=subnet-c [REHEARSAL=true]
|
|
3
3
|
# make deploy @@DEPLOY_HINT@@
|
|
4
4
|
RDS_STACK = @@RDS_STACK@@
|
|
5
5
|
BOX_STACK = @@BOX_STACK@@
|
|
6
|
-
|
|
7
|
-
# Elastic IP. The default is a production pair, with deletion protection.
|
|
6
|
+
@@REHEARSAL_NOTE@@
|
|
8
7
|
REHEARSAL ?= false
|
|
9
8
|
|
|
10
9
|
HECKS ?= hecks
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
@@PHONY@@
|
|
13
12
|
stacks:
|
|
14
|
-
|
|
15
|
-
--parameter-overrides VpcId=$(VPC) PrivateSubnetIds=$(PRIVATE_SUBNETS) Rehearsal=$(REHEARSAL)
|
|
13
|
+
@@RDS_DEPLOY@@
|
|
16
14
|
aws cloudformation deploy --template-file box.yaml --stack-name $(BOX_STACK) --capabilities CAPABILITY_IAM \
|
|
17
15
|
--parameter-overrides VpcId=$(VPC) SubnetId=$(PUBLIC_SUBNET) \
|
|
18
16
|
DbSecurityGroupId=$$(aws cloudformation describe-stacks --stack-name $(RDS_STACK) --query "Stacks[0].Outputs[?OutputKey=='DbSecurityGroupId'].OutputValue" --output text) \
|
|
19
|
-
|
|
17
|
+
@@DB_SECRET_PARAM@@
|
|
20
18
|
AlertTopicArn=$$(aws cloudformation describe-stacks --stack-name $(RDS_STACK) --query "Stacks[0].Outputs[?OutputKey=='AlertTopicArn'].OutputValue" --output text) \
|
|
21
19
|
Rehearsal=$(REHEARSAL)
|
|
22
20
|
|
|
21
|
+
@@PROVISION@@
|
|
23
22
|
deploy:
|
|
24
23
|
@@DEPLOY_RECIPE@@
|
|
@@ -26,7 +26,7 @@ out() { aws cloudformation describe-stacks --stack-name "$1" --query "Stacks[0].
|
|
|
26
26
|
|
|
27
27
|
BOX=$(out "$BOX_STACK" InstanceId)
|
|
28
28
|
DB_HOST=$(out "$RDS_STACK" DbEndpoint)
|
|
29
|
-
|
|
29
|
+
@@DB_SECRET_LINE@@
|
|
30
30
|
[ -n "$BOX" ] && [ "$BOX" != None ] || { echo "no instance in stack $BOX_STACK" >&2; exit 40; }
|
|
31
31
|
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
|
|
32
32
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Create @@STACK@@'s database and login role on the shared RDS instance (stack @@RDS_STACK@@), and the
|
|
3
|
+
# secret @@STACK@@/database that the box's containers read. Run once for a new site, from a machine with
|
|
4
|
+
# AWS credentials that may read the instance's master secret and start SSM sessions, through a bastion
|
|
5
|
+
# that can reach the instance.
|
|
6
|
+
#
|
|
7
|
+
# provision-database.sh <bastion-instance-id> [--rotate]
|
|
8
|
+
#
|
|
9
|
+
# The role owns only the database @@DATABASE@@, is not a superuser, and no other role may connect to that
|
|
10
|
+
# database. The secret holds {username, password, host, port, dbname}; the box's role reads that secret
|
|
11
|
+
# and never the instance's master secret.
|
|
12
|
+
#
|
|
13
|
+
# Safe to run again: a role and a database that exist are left alone, and a secret that exists keeps
|
|
14
|
+
# its password. --rotate gives the role a new password and writes it to the secret; roll the box after it.
|
|
15
|
+
#
|
|
16
|
+
# Ends 70 (the shared stack has no instance).
|
|
17
|
+
#
|
|
18
|
+
# Needs psql, jq, openssl, the SSM plugin and AWS credentials.
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
BASTION=${1:?bastion instance id}
|
|
22
|
+
ROTATE=${2:-}
|
|
23
|
+
[ -z "$ROTATE" ] || [ "$ROTATE" = --rotate ] || { echo "usage: provision-database.sh <bastion-instance-id> [--rotate]" >&2; exit 2; }
|
|
24
|
+
RDS_STACK=@@RDS_STACK@@
|
|
25
|
+
DB=@@DATABASE@@
|
|
26
|
+
ROLE=@@DATABASE@@
|
|
27
|
+
SECRET=@@STACK@@/database
|
|
28
|
+
PORT=15434
|
|
29
|
+
out() { aws cloudformation describe-stacks --stack-name "$1" --query "Stacks[0].Outputs[?OutputKey==\`$2\`].OutputValue" --output text; }
|
|
30
|
+
|
|
31
|
+
HOST=$(out "$RDS_STACK" DbEndpoint)
|
|
32
|
+
MASTER=$(out "$RDS_STACK" DbSecretArn)
|
|
33
|
+
[ -n "$HOST" ] && [ "$HOST" != None ] || { echo "no instance in stack $RDS_STACK" >&2; exit 70; }
|
|
34
|
+
|
|
35
|
+
WORK=$(mktemp -d)
|
|
36
|
+
PIDS=()
|
|
37
|
+
cleanup() { for p in "${PIDS[@]:-}"; do [ -n "$p" ] && kill "$p" 2>/dev/null || true; done; rm -rf "$WORK"; }
|
|
38
|
+
trap cleanup EXIT
|
|
39
|
+
|
|
40
|
+
master_field() { aws secretsmanager get-secret-value --secret-id "$MASTER" --query SecretString --output text | jq -r "$1"; }
|
|
41
|
+
MASTER_USER=$(master_field '.username // "postgres"')
|
|
42
|
+
MASTER_PW=$(master_field .password)
|
|
43
|
+
|
|
44
|
+
aws ssm start-session --target "$BASTION" --document-name AWS-StartPortForwardingSessionToRemoteHost \
|
|
45
|
+
--parameters "{\"host\":[\"$HOST\"],\"portNumber\":[\"5432\"],\"localPortNumber\":[\"$PORT\"]}" >"$WORK/tunnel.log" 2>&1 &
|
|
46
|
+
PIDS+=($!)
|
|
47
|
+
disown
|
|
48
|
+
for _ in $(seq 1 20); do grep -q "Waiting for connections" "$WORK/tunnel.log" 2>/dev/null && break; sleep 1; done
|
|
49
|
+
|
|
50
|
+
# One statement, one answer. The master connects to the maintenance database.
|
|
51
|
+
M() { PGPASSWORD=$MASTER_PW psql -h localhost -p $PORT -U "$MASTER_USER" -d postgres -v ON_ERROR_STOP=1 -Atc "$1"; }
|
|
52
|
+
# A script on stdin, with the new password passed as a variable so it is never in a command line.
|
|
53
|
+
MS() { PGPASSWORD=$MASTER_PW psql -h localhost -p $PORT -U "$MASTER_USER" -d postgres -v ON_ERROR_STOP=1 -v pw="$PW" -q -f -; }
|
|
54
|
+
|
|
55
|
+
HAVE_ROLE=$(M "select count(*) from pg_roles where rolname='$ROLE'")
|
|
56
|
+
HAVE_SECRET=no
|
|
57
|
+
aws secretsmanager describe-secret --secret-id "$SECRET" >/dev/null 2>&1 && HAVE_SECRET=yes
|
|
58
|
+
|
|
59
|
+
# Letters and digits only: the host puts the password in a URL without percent-encoding it.
|
|
60
|
+
PW=""
|
|
61
|
+
if [ "$HAVE_ROLE" = 0 ]; then
|
|
62
|
+
PW=$(openssl rand -hex 16)
|
|
63
|
+
MS <<SQL
|
|
64
|
+
create role "$ROLE" login password :'pw' nosuperuser nocreatedb nocreaterole noinherit;
|
|
65
|
+
SQL
|
|
66
|
+
echo "created role $ROLE"
|
|
67
|
+
elif [ "$ROTATE" = --rotate ] || [ "$HAVE_SECRET" = no ]; then
|
|
68
|
+
PW=$(openssl rand -hex 16)
|
|
69
|
+
MS <<SQL
|
|
70
|
+
alter role "$ROLE" password :'pw';
|
|
71
|
+
SQL
|
|
72
|
+
echo "set a new password for role $ROLE"
|
|
73
|
+
else
|
|
74
|
+
echo "role $ROLE exists and so does $SECRET; kept"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# RDS's master is not a true superuser: it may create a database for a role only as a member of that role.
|
|
78
|
+
if [ "$(M "select count(*) from pg_database where datname='$DB'")" = 0 ]; then
|
|
79
|
+
M "grant \"$ROLE\" to \"$MASTER_USER\"" >/dev/null
|
|
80
|
+
M "create database \"$DB\" owner \"$ROLE\"" >/dev/null
|
|
81
|
+
echo "created database $DB"
|
|
82
|
+
else
|
|
83
|
+
echo "database $DB exists"
|
|
84
|
+
fi
|
|
85
|
+
# Nobody but the owner (and the master) may connect to it.
|
|
86
|
+
M "revoke all on database \"$DB\" from public" >/dev/null
|
|
87
|
+
|
|
88
|
+
if [ -n "$PW" ]; then
|
|
89
|
+
BODY=$(jq -n --arg u "$ROLE" --arg p "$PW" --arg h "$HOST" --arg d "$DB" '{username: $u, password: $p, host: $h, port: 5432, dbname: $d}')
|
|
90
|
+
if [ "$HAVE_SECRET" = yes ]; then
|
|
91
|
+
aws secretsmanager put-secret-value --secret-id "$SECRET" --secret-string "$BODY" >/dev/null
|
|
92
|
+
else
|
|
93
|
+
aws secretsmanager create-secret --name "$SECRET" --description "@@STACK@@'s database login on @@RDS_STACK@@" --secret-string "$BODY" >/dev/null
|
|
94
|
+
fi
|
|
95
|
+
echo "wrote secret $SECRET"
|
|
96
|
+
fi
|
|
97
|
+
echo "ok $DB on $HOST, role $ROLE, secret $SECRET"
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
#
|
|
8
8
|
# FORCE=1 drop the target schemas first: a re-load before cutover, or a rollback copy in the other
|
|
9
9
|
# direction (swap the hosts and secrets, and SRC_DB and DST_DB)
|
|
10
|
+
# SCHEMAS the schemas to copy, space separated (default @@SCHEMAS@@); another database's schemas, such as
|
|
11
|
+
# an analytics database's `public`, are copied by naming them here with SRC_DB and DST_DB
|
|
10
12
|
# SRC_DB the source database (default @@SOURCE_DATABASE@@)
|
|
11
13
|
# DST_DB the target database (default @@DATABASE@@)
|
|
12
14
|
# VERIFY_BY_COMMAND=1 end after the copy and leave the comparison to `hecks deploy data_copy.restore`
|
|
@@ -28,7 +30,7 @@ HERE=$(cd "$(dirname "$0")" && pwd)
|
|
|
28
30
|
|
|
29
31
|
BASTION=${1:?bastion instance id}; SRC_HOST=${2:?source host}; SRC_SECRET=${3:?source secret arn}
|
|
30
32
|
DST_HOST=${4:?target host}; DST_SECRET=${5:?target secret arn}
|
|
31
|
-
SCHEMAS
|
|
33
|
+
SCHEMAS=${SCHEMAS:-"@@SCHEMAS@@"}
|
|
32
34
|
SRC_DB=${SRC_DB:-@@SOURCE_DATABASE@@}; DST_DB=${DST_DB:-@@DATABASE@@}
|
|
33
35
|
SRC_PORT=15432; DST_PORT=15433
|
|
34
36
|
|
|
@@ -93,4 +95,4 @@ done
|
|
|
93
95
|
|
|
94
96
|
# Same structure, same exact row counts in every table, nothing unpopulated.
|
|
95
97
|
[ -z "${VERIFY_BY_COMMAND:-}" ] || exit 0
|
|
96
|
-
A_DB=$SRC_DB B_DB=$DST_DB bash "$HERE/verify-copy.sh" "$BASTION" "$SRC_HOST" "$SRC_SECRET" "$DST_HOST" "$DST_SECRET"
|
|
98
|
+
A_DB=$SRC_DB B_DB=$DST_DB COMPARE_SCHEMAS=$(echo "$SCHEMAS" | tr ' ' ',') bash "$HERE/verify-copy.sh" "$BASTION" "$SRC_HOST" "$SRC_SECRET" "$DST_HOST" "$DST_SECRET"
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
# verify-copy.sh <bastion-instance-id> <host-a> <secret-arn-a> <host-b> <secret-arn-b>
|
|
7
7
|
#
|
|
8
8
|
# A_DB, B_DB the database on each side (default @@DATABASE@@)
|
|
9
|
+
# COMPARE_SCHEMAS the schemas to compare, comma separated (default @@SCHEMAS_CSV@@)
|
|
9
10
|
#
|
|
10
11
|
# Schemas compared: @@SCHEMAS@@
|
|
11
12
|
# Exit 0 only if both are identical and no materialized view is unpopulated in either; 50 when they differ.
|
|
12
13
|
set -euo pipefail
|
|
13
14
|
|
|
14
15
|
BASTION=${1:?bastion instance id}; A_HOST=${2:?}; A_SECRET=${3:?}; B_HOST=${4:?}; B_SECRET=${5:?}
|
|
15
|
-
SCHEMAS
|
|
16
|
+
SCHEMAS=${COMPARE_SCHEMAS:-"@@SCHEMAS_CSV@@"}
|
|
16
17
|
A_DB=${A_DB:-@@DATABASE@@}; B_DB=${B_DB:-@@DATABASE@@}
|
|
17
18
|
A_PORT=15434; B_PORT=15435
|
|
18
19
|
|
|
@@ -7,6 +7,7 @@ require_relative "box/caddy"
|
|
|
7
7
|
require_relative "box/access"
|
|
8
8
|
require_relative "box/stack_yaml"
|
|
9
9
|
require_relative "box/compose"
|
|
10
|
+
require_relative "box/shared_database"
|
|
10
11
|
require_relative "box/roll_scripts"
|
|
11
12
|
|
|
12
13
|
module Hecks
|
|
@@ -22,6 +23,7 @@ module Hecks
|
|
|
22
23
|
extend Access
|
|
23
24
|
extend StackYaml
|
|
24
25
|
extend Compose
|
|
26
|
+
extend SharedDatabase
|
|
25
27
|
extend RollScripts
|
|
26
28
|
|
|
27
29
|
projects_as :aws_box, needs_world: true, emits: :files
|
|
@@ -129,13 +131,23 @@ module Hecks
|
|
|
129
131
|
# @return [Hash{String => String}] every generated file
|
|
130
132
|
def render_all(plan, region)
|
|
131
133
|
{
|
|
132
|
-
"
|
|
134
|
+
"box.yaml" => box_yaml(plan, region),
|
|
133
135
|
"Caddyfile" => caddyfile(plan), "services.json" => services_json(plan),
|
|
134
136
|
"render-compose.sh" => render_compose_sh(plan, region),
|
|
135
137
|
"fetch-secrets.sh" => File.read(File.join(TEMPLATE_DIR, "fetch-secrets.sh")),
|
|
136
138
|
"deploy-box.sh" => deploy_box_sh(plan, region),
|
|
137
139
|
"Makefile" => makefile(plan)
|
|
138
|
-
}.merge(
|
|
140
|
+
}.merge(database_files(plan)).merge(migration_files(plan))
|
|
141
|
+
.then { |files| Hosting.extend_files(files, plan: plan, region: region) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @param plan [Settings::Plan] the resolved settings
|
|
145
|
+
# @return [Hash{String => String}] the database stack, or for a database on a shared
|
|
146
|
+
# instance the script that provisions it; the shared stack is not this site's to generate
|
|
147
|
+
def database_files(plan)
|
|
148
|
+
return { "rds.yaml" => rds_yaml(plan) } unless plan.shared?
|
|
149
|
+
|
|
150
|
+
{ "provision-database.sh" => provision_database_sh(plan) }
|
|
139
151
|
end
|
|
140
152
|
|
|
141
153
|
# Fills `@@NAME@@` markers. A marker alone on its line is replaced together with the line,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @@STACK@@: the shared RDS instance that each site's own box stack keeps a database on.
|
|
2
|
+
# make stack VPC=vpc-... PRIVATE_SUBNETS=subnet-a,subnet-b [BASTION_SG=sg-...] [ALERT_EMAIL=you@example.com] [REHEARSAL=true]
|
|
3
|
+
STACK = @@STACK@@
|
|
4
|
+
# true makes a throwaway copy: no deletion protection and no final snapshot. The default is the real instance.
|
|
5
|
+
REHEARSAL ?= false
|
|
6
|
+
|
|
7
|
+
.PHONY: stack
|
|
8
|
+
stack:
|
|
9
|
+
aws cloudformation deploy --template-file rds.yaml --stack-name $(STACK) --capabilities CAPABILITY_IAM \
|
|
10
|
+
--parameter-overrides VpcId=$(VPC) PrivateSubnetIds=$(PRIVATE_SUBNETS) Rehearsal=$(REHEARSAL) \
|
|
11
|
+
$(if $(BASTION_SG),BastionSecurityGroupId=$(BASTION_SG)) $(if $(ALERT_EMAIL),AlertEmail=$(ALERT_EMAIL))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @@STACK@@
|
|
2
|
+
|
|
3
|
+
The shared RDS Postgres instance (@@DATABASE_CLASS@@, @@STORAGE_GB@@ GB, Postgres @@ENGINE_VERSION@@, @@BACKUP_DAYS@@-day backups) on which several
|
|
4
|
+
sites each keep their own database and login role. Generated by `hecks deploy project`; do not edit `rds.yaml`.
|
|
5
|
+
|
|
6
|
+
## Create it
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
make stack VPC=vpc-... PRIVATE_SUBNETS=subnet-a,subnet-b BASTION_SG=sg-... ALERT_EMAIL=you@example.com
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`BASTION_SG` is the security group of the machine that tunnels to the instance over SSM (a site's own box works).
|
|
13
|
+
Outputs: `DbEndpoint`, `DbSecurityGroupId`, `DbSecretArn` (the master secret) and `AlertTopicArn`.
|
|
14
|
+
|
|
15
|
+
## Add a site
|
|
16
|
+
|
|
17
|
+
A site's world says `shared_database "@@STACK@@"`. Its generated `provision-database.sh` creates that site's role, its
|
|
18
|
+
database, and the secret `<site>/database` that its containers read; its box stack adds its own ingress rule to this
|
|
19
|
+
instance's security group. Nothing else needs to change here.
|
|
20
|
+
|
|
21
|
+
## Things to know
|
|
22
|
+
|
|
23
|
+
- The master secret is read only by the person who provisions a site, never by a box.
|
|
24
|
+
- This stack cannot be deleted while any site's box stack still references its security group.
|
|
25
|
+
- One maintenance window, restart or failure touches every site. Alarms (storage, CPU, memory) are for the instance.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
AWSTemplateFormatVersion: "2010-09-09"
|
|
2
|
+
Description: >
|
|
3
|
+
The shared RDS Postgres instance @@STACK@@, on which several sites each keep their own database and
|
|
4
|
+
login role. Private subnets only; each site's box stack admits itself on 5432, so nothing else can reach
|
|
5
|
+
it. The master password is managed by RDS in Secrets Manager ({username, password}); only the operator
|
|
6
|
+
who runs a site's provision-database.sh reads it, and no box does.
|
|
7
|
+
|
|
8
|
+
Parameters:
|
|
9
|
+
VpcId:
|
|
10
|
+
Type: AWS::EC2::VPC::Id
|
|
11
|
+
BastionSecurityGroupId:
|
|
12
|
+
Type: String
|
|
13
|
+
Default: ""
|
|
14
|
+
Description: >
|
|
15
|
+
A security group allowed to reach the instance on 5432, for the bastion that provision-database.sh and
|
|
16
|
+
restore-to-rds.sh tunnel through. Blank admits nothing but the sites' boxes.
|
|
17
|
+
PrivateSubnetIds:
|
|
18
|
+
Type: List<AWS::EC2::Subnet::Id>
|
|
19
|
+
Description: At least two subnets in different availability zones (an RDS subnet group needs them).
|
|
20
|
+
InstanceClass:
|
|
21
|
+
Type: String
|
|
22
|
+
Default: @@DB_CLASS@@
|
|
23
|
+
AllocatedStorageGb:
|
|
24
|
+
Type: Number
|
|
25
|
+
Default: @@STORAGE_GB@@
|
|
26
|
+
EngineVersion:
|
|
27
|
+
Type: String
|
|
28
|
+
Default: "@@ENGINE_VERSION@@"
|
|
29
|
+
Description: A major version ("16") lets RDS pick its current minor release.
|
|
30
|
+
BackupRetentionDays:
|
|
31
|
+
Type: Number
|
|
32
|
+
Default: @@BACKUP_DAYS@@
|
|
33
|
+
ExistingAlertTopicArn:
|
|
34
|
+
Type: String
|
|
35
|
+
Default: ""
|
|
36
|
+
Description: An SNS topic that already has subscribers. Blank creates a new topic.
|
|
37
|
+
AlertEmail:
|
|
38
|
+
Type: String
|
|
39
|
+
Default: ""
|
|
40
|
+
Description: Subscribed to a new topic (AWS sends one confirmation link). Ignored when ExistingAlertTopicArn is set.
|
|
41
|
+
Rehearsal:
|
|
42
|
+
Type: String
|
|
43
|
+
Default: "false"
|
|
44
|
+
AllowedValues: ["true", "false"]
|
|
45
|
+
Description: >
|
|
46
|
+
true is a throwaway copy (no deletion protection, no final snapshot). false is the real
|
|
47
|
+
database: deletion protection on and a snapshot taken on delete.
|
|
48
|
+
|
|
49
|
+
Conditions:
|
|
50
|
+
NewTopic: !Equals [!Ref ExistingAlertTopicArn, ""]
|
|
51
|
+
NewTopicWithEmail: !And [!Condition NewTopic, !Not [!Equals [!Ref AlertEmail, ""]]]
|
|
52
|
+
IsRehearsal: !Equals [!Ref Rehearsal, "true"]
|
|
53
|
+
HasBastion: !Not [!Equals [!Ref BastionSecurityGroupId, ""]]
|
|
54
|
+
|
|
55
|
+
Resources:
|
|
56
|
+
# No inline rules: every site's box stack adds its own ingress rule to this group, and CloudFormation can
|
|
57
|
+
# revoke an externally added rule when it updates an inline list. The bastion's rule is its own resource.
|
|
58
|
+
DbSecurityGroup:
|
|
59
|
+
Type: AWS::EC2::SecurityGroup
|
|
60
|
+
Properties:
|
|
61
|
+
GroupDescription: @@STACK@@ RDS, 5432 from each site's box (their stacks add the rules) and a bastion if given
|
|
62
|
+
VpcId: !Ref VpcId
|
|
63
|
+
|
|
64
|
+
BastionIngress:
|
|
65
|
+
Type: AWS::EC2::SecurityGroupIngress
|
|
66
|
+
Condition: HasBastion
|
|
67
|
+
Properties:
|
|
68
|
+
GroupId: !Ref DbSecurityGroup
|
|
69
|
+
IpProtocol: tcp
|
|
70
|
+
FromPort: 5432
|
|
71
|
+
ToPort: 5432
|
|
72
|
+
SourceSecurityGroupId: !Ref BastionSecurityGroupId
|
|
73
|
+
Description: Bastion for provision-database.sh and restore-to-rds.sh
|
|
74
|
+
|
|
75
|
+
DbSubnetGroup:
|
|
76
|
+
Type: AWS::RDS::DBSubnetGroup
|
|
77
|
+
Properties:
|
|
78
|
+
DBSubnetGroupDescription: @@STACK@@ RDS subnets
|
|
79
|
+
SubnetIds: !Ref PrivateSubnetIds
|
|
80
|
+
|
|
81
|
+
Database:
|
|
82
|
+
Type: AWS::RDS::DBInstance
|
|
83
|
+
DeletionPolicy: !If [IsRehearsal, Delete, Snapshot]
|
|
84
|
+
UpdateReplacePolicy: !If [IsRehearsal, Delete, Snapshot]
|
|
85
|
+
Properties:
|
|
86
|
+
Engine: postgres
|
|
87
|
+
EngineVersion: !Ref EngineVersion
|
|
88
|
+
DBInstanceClass: !Ref InstanceClass
|
|
89
|
+
AllocatedStorage: !Ref AllocatedStorageGb
|
|
90
|
+
StorageType: gp3
|
|
91
|
+
StorageEncrypted: true
|
|
92
|
+
MasterUsername: postgres
|
|
93
|
+
ManageMasterUserPassword: true
|
|
94
|
+
DBSubnetGroupName: !Ref DbSubnetGroup
|
|
95
|
+
VPCSecurityGroups: [!Ref DbSecurityGroup]
|
|
96
|
+
PubliclyAccessible: false
|
|
97
|
+
MultiAZ: false
|
|
98
|
+
BackupRetentionPeriod: !Ref BackupRetentionDays
|
|
99
|
+
DeletionProtection: !If [IsRehearsal, false, true]
|
|
100
|
+
AutoMinorVersionUpgrade: true
|
|
101
|
+
CopyTagsToSnapshot: true
|
|
102
|
+
Tags:
|
|
103
|
+
- Key: Name
|
|
104
|
+
Value: @@STACK@@-rds
|
|
105
|
+
|
|
106
|
+
AlertTopic:
|
|
107
|
+
Type: AWS::SNS::Topic
|
|
108
|
+
Condition: NewTopic
|
|
109
|
+
Properties:
|
|
110
|
+
DisplayName: @@STACK@@-alerts
|
|
111
|
+
|
|
112
|
+
AlertSubscription:
|
|
113
|
+
Type: AWS::SNS::Subscription
|
|
114
|
+
Condition: NewTopicWithEmail
|
|
115
|
+
Properties:
|
|
116
|
+
TopicArn: !Ref AlertTopic
|
|
117
|
+
Protocol: email
|
|
118
|
+
Endpoint: !Ref AlertEmail
|
|
119
|
+
|
|
120
|
+
# Every site's journal only grows, and RDS stops accepting writes when storage is full.
|
|
121
|
+
StorageLowAlarm:
|
|
122
|
+
Type: AWS::CloudWatch::Alarm
|
|
123
|
+
Properties:
|
|
124
|
+
AlarmDescription: RDS free storage under 3 GB
|
|
125
|
+
Namespace: AWS/RDS
|
|
126
|
+
MetricName: FreeStorageSpace
|
|
127
|
+
Dimensions: [{ Name: DBInstanceIdentifier, Value: !Ref Database }]
|
|
128
|
+
Statistic: Minimum
|
|
129
|
+
Period: 300
|
|
130
|
+
EvaluationPeriods: 3
|
|
131
|
+
Threshold: 3221225472
|
|
132
|
+
ComparisonOperator: LessThanThreshold
|
|
133
|
+
TreatMissingData: breaching
|
|
134
|
+
AlarmActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
135
|
+
OKActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
136
|
+
|
|
137
|
+
CpuHighAlarm:
|
|
138
|
+
Type: AWS::CloudWatch::Alarm
|
|
139
|
+
Properties:
|
|
140
|
+
AlarmDescription: "RDS CPU above 80% for 15 minutes (a burstable class may be out of credits)"
|
|
141
|
+
Namespace: AWS/RDS
|
|
142
|
+
MetricName: CPUUtilization
|
|
143
|
+
Dimensions: [{ Name: DBInstanceIdentifier, Value: !Ref Database }]
|
|
144
|
+
Statistic: Average
|
|
145
|
+
Period: 300
|
|
146
|
+
EvaluationPeriods: 3
|
|
147
|
+
Threshold: 80
|
|
148
|
+
ComparisonOperator: GreaterThanThreshold
|
|
149
|
+
TreatMissingData: notBreaching
|
|
150
|
+
AlarmActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
151
|
+
OKActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
152
|
+
|
|
153
|
+
MemoryLowAlarm:
|
|
154
|
+
Type: AWS::CloudWatch::Alarm
|
|
155
|
+
Properties:
|
|
156
|
+
AlarmDescription: RDS freeable memory under 100 MB for 15 minutes
|
|
157
|
+
Namespace: AWS/RDS
|
|
158
|
+
MetricName: FreeableMemory
|
|
159
|
+
Dimensions: [{ Name: DBInstanceIdentifier, Value: !Ref Database }]
|
|
160
|
+
Statistic: Minimum
|
|
161
|
+
Period: 300
|
|
162
|
+
EvaluationPeriods: 3
|
|
163
|
+
Threshold: 104857600
|
|
164
|
+
ComparisonOperator: LessThanThreshold
|
|
165
|
+
TreatMissingData: notBreaching
|
|
166
|
+
AlarmActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
167
|
+
OKActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
168
|
+
|
|
169
|
+
Outputs:
|
|
170
|
+
AlertTopicArn:
|
|
171
|
+
Description: Pass as AlertTopicArn to the box stack.
|
|
172
|
+
Value: !If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]
|
|
173
|
+
DbEndpoint:
|
|
174
|
+
Value: !GetAtt Database.Endpoint.Address
|
|
175
|
+
DbSecretArn:
|
|
176
|
+
Description: RDS-managed master secret ({username, password}).
|
|
177
|
+
Value: !GetAtt Database.MasterUserSecret.SecretArn
|
|
178
|
+
DbSecurityGroupId:
|
|
179
|
+
Description: Pass as DbSecurityGroupId to the box stack.
|
|
180
|
+
Value: !Ref DbSecurityGroup
|