hecks 3.6.0 → 3.9.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/modes.rb +14 -0
- 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/bluebook/meta_validator.rb +4 -2
- data/lib/hecks/cache_dir.rb +10 -0
- data/lib/hecks/corpus/routes.rb +1 -1
- 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 +12 -2
- 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/adapters/site_toolchain/prober.rb +64 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +41 -5
- 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 +4 -1
- 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/probe.rb +131 -0
- data/lib/hecks/projections/site/site_host/dockerfile.rb +1 -1
- 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/runtime/value/validation.rb +34 -3
- data/lib/hecks/rust_build/native_build.rb +29 -1
- data/lib/hecks/rust_build/wasm.rb +16 -3
- data/lib/hecks/site/bluebook/site.bluebook +31 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- 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 +3 -1
- data/prebuilt/4e6c3cd1f578a38732ea5454dae52562d03d8e9886216077a5c34cbf589f3463.marshal +0 -0
- data/prebuilt/911cdcc56b5a094ed26583a4c76401dba6455b1f434722ed22b850bc1b1bdd28.marshal +0 -0
- data/prebuilt/verdicts-3230a251570aa03666708d16f92e646feab6a3f8cc2a3540c017d748e41d41a1.json +1 -0
- data/qa/settings.yml +1 -0
- data/rust/codegen/src/fielded.rs +3 -3
- data/rust/codegen/src/json_codec.rs +2 -2
- data/rust/codegen/src/naming.rs +20 -0
- data/rust/host/Cargo.toml +6 -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 +79 -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/commerce_ir.rs +459 -0
- data/rust/host/src/extension.rs +100 -0
- data/rust/host/src/fuzz_support.rs +165 -0
- data/rust/host/src/ir.rs +4 -448
- data/rust/host/src/journal.rs +1 -1
- data/rust/host/src/lib.rs +39 -0
- data/rust/host/src/main.rs +10 -44
- data/rust/host/src/mint.rs +1 -1
- data/rust/host/src/payments/tests.rs +8 -8
- data/rust/host/src/payments.rs +10 -10
- data/rust/host/src/rate_limit.rs +27 -41
- data/rust/host/src/secrets.rs +44 -0
- data/rust/host/src/server.rs +6 -1
- data/rust/host/src/web/commerce.rs +113 -0
- data/rust/host/src/web/newsletter.rs +2 -1
- data/rust/host/src/web/newsletter_send.rs +1 -1
- data/rust/host/src/web/registration_receipt.rs +2 -2
- data/rust/host/src/web/registrations.rs +49 -49
- data/rust/host/src/web/resend_webhook.rs +299 -0
- data/rust/host/src/web.rs +55 -49
- 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 +58 -2
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require_relative "../../../projections/site/probe"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
class SiteToolchain
|
|
10
|
+
# Sends a route table's probe requests to a running site and reports each answer.
|
|
11
|
+
#
|
|
12
|
+
# Every request is anonymous and follows no redirect, so a probe changes nothing on the site.
|
|
13
|
+
class Prober
|
|
14
|
+
# Seconds to wait to connect to the site, and again for its answer.
|
|
15
|
+
TIMEOUT = 10
|
|
16
|
+
|
|
17
|
+
# The failures of the network that fail one check rather than the whole run.
|
|
18
|
+
NETWORK = [SystemCallError, Timeout::Error, SocketError, OpenSSL::SSL::SSLError].freeze
|
|
19
|
+
|
|
20
|
+
# @param rows [Array<Projections::Site::Table::Row>] the checked rows of the route table
|
|
21
|
+
# @param run [String] the run's name
|
|
22
|
+
# @param base [String] the site's scheme, host and port
|
|
23
|
+
def initialize(rows, run:, base:)
|
|
24
|
+
@probe = Projections::Site::Probe.new(rows, run: run)
|
|
25
|
+
@base = base
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Array<(String, Integer)>] the report, one line per check ending in the count,
|
|
29
|
+
# and how many checks failed
|
|
30
|
+
def call
|
|
31
|
+
lines = @probe.checks.map { |check| judged(check) }
|
|
32
|
+
failed = lines.count { |_, wrong| wrong }
|
|
33
|
+
[lines.map(&:first).append("#{lines.size} checks, #{failed} failed").join("\n"), failed]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# One check's line of the report: its label and `ok`, or the reason it failed.
|
|
39
|
+
def judged(check)
|
|
40
|
+
why = @probe.verdict(check, answer_to(check))
|
|
41
|
+
[" #{check.label}... #{why ? "FAILED: #{why}" : "ok"}", !why.nil?]
|
|
42
|
+
rescue *NETWORK => e
|
|
43
|
+
[" #{check.label}... FAILED: #{e.class}: #{e.message}", true]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def answer_to(check)
|
|
47
|
+
uri = URI.join(@base, check.path)
|
|
48
|
+
request = Net::HTTP.const_get(check.verb.capitalize).new(uri)
|
|
49
|
+
request["Origin"] = @base
|
|
50
|
+
response = connection(uri).request(request)
|
|
51
|
+
Projections::Site::Probe::Response.new(status: response.code.to_i, body: response.body,
|
|
52
|
+
headers: response.to_hash.transform_values(&:first))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def connection(uri)
|
|
56
|
+
Net::HTTP.new(uri.host, uri.port).tap do |http|
|
|
57
|
+
http.use_ssl = uri.scheme == "https"
|
|
58
|
+
http.open_timeout = http.read_timeout = TIMEOUT
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -8,6 +8,7 @@ require_relative "codebase/ruby_child"
|
|
|
8
8
|
require "hecks/tools"
|
|
9
9
|
require "hecks/tools/site_routes"
|
|
10
10
|
require "hecks/projections/site/live_edge"
|
|
11
|
+
require_relative "site_toolchain/prober"
|
|
11
12
|
|
|
12
13
|
module Hecks
|
|
13
14
|
module Adapters
|
|
@@ -66,8 +67,35 @@ module Hecks
|
|
|
66
67
|
{ output: { value: comparison.to_s } }
|
|
67
68
|
end
|
|
68
69
|
|
|
70
|
+
# Asks a running site what its route table says it must answer, and judges each answer. It
|
|
71
|
+
# sends anonymous requests only, follows no redirect, and changes nothing on the site: an
|
|
72
|
+
# admin route is asked without a session and must refuse.
|
|
73
|
+
#
|
|
74
|
+
# @param held [Hash] the `SiteProjection` record: `domain`, `run` and `url`, the site's base
|
|
75
|
+
# address
|
|
76
|
+
# @return [Hash{Symbol => Hash}] `output:` one line per check, ending in the count
|
|
77
|
+
# @raise [ConsoleCapture::Failure] when the address is not http(s) or an answer is wrong; the
|
|
78
|
+
# message is the report
|
|
79
|
+
def probe(**held)
|
|
80
|
+
rows = table_of(located(:domain, held[:domain])).rows
|
|
81
|
+
report, failed = Prober.new(rows, run: plain(held[:run]).to_s, base: base_url(plain(held[:url]))).call
|
|
82
|
+
raise ConsoleCapture::Failure, report if failed.positive?
|
|
83
|
+
|
|
84
|
+
{ output: { value: report } }
|
|
85
|
+
end
|
|
86
|
+
|
|
69
87
|
private
|
|
70
88
|
|
|
89
|
+
# The scheme, host and port of an http(s) address.
|
|
90
|
+
def base_url(url)
|
|
91
|
+
uri = URI.parse(url.to_s)
|
|
92
|
+
raise URI::InvalidURIError unless uri.is_a?(URI::HTTP) && uri.host
|
|
93
|
+
|
|
94
|
+
"#{uri.scheme}://#{uri.host}#{":#{uri.port}" unless uri.port == uri.default_port}"
|
|
95
|
+
rescue URI::InvalidURIError
|
|
96
|
+
raise ConsoleCapture::Failure, "url must be an http(s) address, got #{url.inspect}"
|
|
97
|
+
end
|
|
98
|
+
|
|
71
99
|
# The flags `project_site` is run with: each the record names, and `--check` when asked.
|
|
72
100
|
def project_flags(held)
|
|
73
101
|
flags = FLAGS.filter_map { |flag, key| "--#{flag}=#{located(key, held[key])}" unless plain(held[key]).nil? }
|
|
@@ -84,15 +112,23 @@ module Hecks
|
|
|
84
112
|
|
|
85
113
|
# The project's checked edge, read the way `project_site` reads it.
|
|
86
114
|
def edge_of(root, template = nil)
|
|
115
|
+
registry, chapter, table = read_table(root)
|
|
116
|
+
site = Projections::Site
|
|
117
|
+
site::Edge.read(chapter, table: table, template: template, vocabulary: site::Table.vocabulary(registry)) ||
|
|
118
|
+
raise(ConsoleCapture::Failure, "#{root} declares no Edge rows")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The project's checked route table.
|
|
122
|
+
def table_of(root) = read_table(root).last
|
|
123
|
+
|
|
124
|
+
# The registry, chapter and checked table of the project at `root`.
|
|
125
|
+
def read_table(root)
|
|
87
126
|
registry = nil
|
|
88
127
|
outcome = ConsoleCapture.capture { registry = Tools::SiteRoutes.registry_for(root) }
|
|
89
128
|
raise ConsoleCapture::Failure, outcome.output.strip unless outcome.ok?
|
|
90
129
|
|
|
91
|
-
|
|
92
|
-
chapter
|
|
93
|
-
table = site::Table.read(chapter, registry: registry)
|
|
94
|
-
site::Edge.read(chapter, table: table, template: template, vocabulary: site::Table.vocabulary(registry)) ||
|
|
95
|
-
raise(ConsoleCapture::Failure, "#{root} declares no Edge rows")
|
|
130
|
+
chapter = Projections::Site::Table.chapter(registry)
|
|
131
|
+
[registry, chapter, Projections::Site::Table.read(chapter, registry: registry)]
|
|
96
132
|
rescue Projections::Site::Table::Invalid => e
|
|
97
133
|
raise ConsoleCapture::Failure, e.message
|
|
98
134
|
end
|
|
@@ -147,6 +147,12 @@ Hecks.behaviors "Custodian" do
|
|
|
147
147
|
expect refused: "a count is positive"
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
test "Mutate refuses a budget that is not positive" do
|
|
151
|
+
tests "Mutate", on: "FuzzRun"
|
|
152
|
+
input run: { value: "mutants" }, domain: { value: "examples/pizzas" }, budget: { value: 0 }
|
|
153
|
+
expect refused: "a count is positive"
|
|
154
|
+
end
|
|
155
|
+
|
|
150
156
|
test "Fuzz refuses a persistence it does not know" do
|
|
151
157
|
tests "Fuzz", on: "FuzzRun"
|
|
152
158
|
input run: { value: "sweep" }, adapter: { value: "cassandra" }
|
|
@@ -1963,7 +1963,9 @@ policy "RecordTheEra" do
|
|
|
1963
1963
|
attribute :seeds, Count, optional: true
|
|
1964
1964
|
attribute :steps, Count, optional: true
|
|
1965
1965
|
attribute :workers, Count, optional: true
|
|
1966
|
+
attribute :budget, Count, optional: true
|
|
1966
1967
|
attribute :adapter, Persistence, optional: true
|
|
1968
|
+
attribute :persist_regressions, RegressionSwitch, optional: true
|
|
1967
1969
|
attribute :domains, NameList, optional: true
|
|
1968
1970
|
attribute :targets, NameList, optional: true
|
|
1969
1971
|
attribute :iterations, Count, optional: true
|
|
@@ -2006,6 +2008,11 @@ policy "RecordTheEra" do
|
|
|
2006
2008
|
attribute :value, String, pattern: '^(memory|sqlite|postgres)$'
|
|
2007
2009
|
end
|
|
2008
2010
|
|
|
2011
|
+
# Whether each distinct finding's minimized repro is also kept under `spec/corpus/regressions/`.
|
|
2012
|
+
value_object "RegressionSwitch" do
|
|
2013
|
+
attribute :value, Boolean
|
|
2014
|
+
end
|
|
2015
|
+
|
|
2009
2016
|
# Names, comma separated: benchmark domains, or the targets to measure.
|
|
2010
2017
|
value_object "NameList" do
|
|
2011
2018
|
attribute :value, String, pattern: '^[^ \t\n\r,]+(,[^ \t\n\r,]+)*$'
|
|
@@ -2047,6 +2054,7 @@ policy "RecordTheEra" do
|
|
|
2047
2054
|
attribute :steps, Count, optional: true
|
|
2048
2055
|
attribute :workers, Count, optional: true
|
|
2049
2056
|
attribute :adapter, Persistence, optional: true
|
|
2057
|
+
attribute :persist_regressions, RegressionSwitch, optional: true
|
|
2050
2058
|
|
|
2051
2059
|
sets :domain
|
|
2052
2060
|
sets :run
|
|
@@ -2054,10 +2062,34 @@ policy "RecordTheEra" do
|
|
|
2054
2062
|
sets :steps
|
|
2055
2063
|
sets :workers
|
|
2056
2064
|
sets :adapter
|
|
2065
|
+
sets :persist_regressions
|
|
2057
2066
|
|
|
2058
2067
|
emits FuzzRequested
|
|
2059
2068
|
end
|
|
2060
2069
|
|
|
2070
|
+
# Mutation-tests a domain's own checks: changes its bluebooks in small ways and keeps which
|
|
2071
|
+
# changes the fuzz properties, the corpus script and the domain's behaviors tests let through.
|
|
2072
|
+
# A survivor is a hole in those checks. `budget` caps how many mutants are tried. The run is a
|
|
2073
|
+
# child process started through the ProcessPool adapter, as a sweep is.
|
|
2074
|
+
command "Mutate" do
|
|
2075
|
+
role "Operator"
|
|
2076
|
+
goal "Mutate a domain's rules and keep whether its checks noticed each change"
|
|
2077
|
+
|
|
2078
|
+
attribute :domain, DomainPath
|
|
2079
|
+
attribute :run, FuzzKey
|
|
2080
|
+
attribute :seeds, Count, optional: true
|
|
2081
|
+
attribute :steps, Count, optional: true
|
|
2082
|
+
attribute :budget, Count, optional: true
|
|
2083
|
+
|
|
2084
|
+
sets :domain
|
|
2085
|
+
sets :run
|
|
2086
|
+
sets :seeds
|
|
2087
|
+
sets :steps
|
|
2088
|
+
sets :budget
|
|
2089
|
+
|
|
2090
|
+
emits MutateRequested
|
|
2091
|
+
end
|
|
2092
|
+
|
|
2061
2093
|
# Measures each persistence adapter, and the Rust binary when there is one to build or use.
|
|
2062
2094
|
# The report is kept in this record, which the Hecks domain binds to Memory: keeping a
|
|
2063
2095
|
# domain's benchmark history beside it, in Sqlite under the project's `.hecks/`, is decided
|
|
@@ -2148,6 +2180,11 @@ policy "RecordTheEra" do
|
|
|
2148
2180
|
trigger FuzzRun::ProcessPool::Sweep, with: { run: :run }
|
|
2149
2181
|
end
|
|
2150
2182
|
|
|
2183
|
+
policy "MutateWhenRequested" do
|
|
2184
|
+
on "FuzzRun.MutateRequested"
|
|
2185
|
+
trigger FuzzRun::ProcessPool::Probe, with: { run: :run }
|
|
2186
|
+
end
|
|
2187
|
+
|
|
2151
2188
|
policy "MeasureWhenRequested" do
|
|
2152
2189
|
on "FuzzRun.BenchRequested"
|
|
2153
2190
|
trigger FuzzRun::RustToolchain::Measure, with: { run: :run }
|
|
@@ -188,6 +188,12 @@ Hecks.hecksagon "Hecks" do
|
|
|
188
188
|
answers "FuzzAnswered"
|
|
189
189
|
refuses "FuzzRefused"
|
|
190
190
|
end
|
|
191
|
+
|
|
192
|
+
asks "Probe", to: FuzzRun do
|
|
193
|
+
attribute :run, FuzzKey
|
|
194
|
+
answers "FuzzAnswered"
|
|
195
|
+
refuses "FuzzRefused"
|
|
196
|
+
end
|
|
191
197
|
end
|
|
192
198
|
|
|
193
199
|
Hecks::FuzzRun.port "RustToolchain" do
|
|
@@ -626,7 +626,7 @@ Hecks.bluebook "Bluebook" do
|
|
|
626
626
|
label: "does this change touch lib/hecks/runtime/**?"
|
|
627
627
|
member name: "postgres_io_relevant_changed", workflow: "ci-postgres-io-parallel.yml", mode: "skips_unless",
|
|
628
628
|
pattern: '^(docs/|editors/|release/|deploy/|\.claude/|\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests)/|\.rubocop\.yml$|\.rubocop_todo\.yml$|\.mcp\.json$|\.rspec-local\.example$|README\.md$|CHANGELOG\.md$|CONTRIBUTING\.md$|SECURITY\.md$|LICENSE$|\.gitignore$)',
|
|
629
|
-
push: "
|
|
629
|
+
push: "skip",
|
|
630
630
|
label: "does this change touch anything rspec_postgres_io_parallel covers?"
|
|
631
631
|
end
|
|
632
632
|
|
|
@@ -706,6 +706,8 @@ Hecks.bluebook "Bluebook" do
|
|
|
706
706
|
meaning: "Fraction (0.0..1.0) of seeds that start from a prefix of an earlier seed's new coverage. Off by default: measured, it trades breadth for depth (banking reached 1.41x the deep-state tuples, chess lost 0.12x of all tuples), while favoring rare verbs alone never lost."
|
|
707
707
|
member name: "favor_rare_verbs", type: "Integer",
|
|
708
708
|
meaning: "How many of the least-hit verbs the next seed weights up; 0 turns the lever off."
|
|
709
|
+
member name: "runtime_coverage_feedback", type: "Boolean",
|
|
710
|
+
meaning: "Whether guided generation also admits a seed to the corpus for reaching a new line or branch of the Ruby runtime (`Hecks::Fuzzing::RuntimeCoverage`), not only a new (verb, kind, state, mutation, outcome) tuple. Off by default: tracing every line slows each seed."
|
|
709
711
|
member name: "self_consistency_checks", type: "Boolean",
|
|
710
712
|
meaning: "Whether `hecks quality_control query sweep.run` also runs the self-consistency pass (cold rehydration, replay idempotency, value-object JSON round trip) on the same generated sequence."
|
|
711
713
|
member name: "shrink_budget", type: "Integer",
|
|
@@ -802,6 +804,7 @@ Hecks.bluebook "Bluebook" do
|
|
|
802
804
|
member script: "fuzz", section: "Custodian", aggregate: "FuzzRun", verb: "fuzz"
|
|
803
805
|
member script: "generate", section: "Custodian", aggregate: "FuzzRun", verb: "generate_sequence"
|
|
804
806
|
member script: "bench", section: "Custodian", aggregate: "FuzzRun", verb: "bench"
|
|
807
|
+
member script: "(new)", section: "Custodian", aggregate: "FuzzRun", verb: "mutate"
|
|
805
808
|
member script: "(new)", section: "Custodian", aggregate: "Era", verb: "hold_first"
|
|
806
809
|
member script: "project_deploy", section: "Deploy", aggregate: "Recipe", verb: "project"
|
|
807
810
|
member script: "lint_deploy_recipes", section: "Deploy", aggregate: "MakefileCheck", verb: "lint"
|
|
@@ -44,7 +44,8 @@ module Hecks
|
|
|
44
44
|
template("deploy-box.sh.tmpl", "STACK" => plan.infra_name, "BOX_STACK" => plan.box_stack,
|
|
45
45
|
"RDS_STACK" => plan.rds_stack, "REGION" => region,
|
|
46
46
|
"DIR" => "/opt/#{plan.infra_name}", "HEALTH_CHECKS" => health_checks(plan),
|
|
47
|
-
"SMOKE_HEADER" => smoke_header(plan), "USAGE" => deploy_usage(plan)
|
|
47
|
+
"SMOKE_HEADER" => smoke_header(plan), "USAGE" => deploy_usage(plan),
|
|
48
|
+
"DB_SECRET_LINE" => database_secret_line(plan))
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
# @param plan [Settings::Plan] the resolved settings
|
|
@@ -96,9 +97,10 @@ module Hecks
|
|
|
96
97
|
# @return [String] a Makefile whose targets create the two stacks and roll the box
|
|
97
98
|
def makefile(plan)
|
|
98
99
|
hint = plan.task_definition ? "[TASKDEF=family:revision]" : '[TAGS="web=20260101 worker=20260101"]'
|
|
99
|
-
base = template("Makefile.tmpl", "STACK" => plan.infra_name, "DEPLOY_HINT" => hint,
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
base = template("Makefile.tmpl", { "STACK" => plan.infra_name, "DEPLOY_HINT" => hint,
|
|
101
|
+
"RDS_STACK" => plan.rds_stack, "BOX_STACK" => plan.box_stack,
|
|
102
|
+
"DEPLOY_RECIPE" => RollRecipe.deploy(plan) }
|
|
103
|
+
.merge(database_makefile_values(plan)))
|
|
102
104
|
plan.hosting ? "#{base}\ninclude hosting.mk\n" : base
|
|
103
105
|
end
|
|
104
106
|
|
|
@@ -23,6 +23,10 @@ module Hecks
|
|
|
23
23
|
S3_BUCKET = /\A[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]\z/
|
|
24
24
|
S3_SHAPE = "s3_access: a list of `{ bucket: \"name\", write: true }` hashes (write is optional)".freeze
|
|
25
25
|
FROM_TASKDEF = %i[env secrets repository].freeze
|
|
26
|
+
|
|
27
|
+
# The settings that size an RDS instance: with `shared_database` they are the shared
|
|
28
|
+
# instance's.
|
|
29
|
+
SHARED_INSTANCE_SETTINGS = %i[database_class storage_gb engine_version backup_days].freeze
|
|
26
30
|
# Default images, each a version tag plus the digest of its multi-architecture index,
|
|
27
31
|
# so a rebuilt box pulls the same bytes.
|
|
28
32
|
TUNNEL_IMAGE = "cloudflare/cloudflared:2026.9.3" \
|
|
@@ -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,
|