Merge origin/main (132cf996, #275) into zdd-owed-items
The census-ruling-owed overlay now loads after census-ruling-275, which is envelope-only and leaves the 292 seal standing; the 292 seal is still checked before the owed overlay, and the owed seal 79db70c4..dfbb is unchanged on the merged tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 files changed,
+371
−13
.amadan/ci.d/20-cross@5+51 −0README.md+2 −0internal/alerting/alerting.go+12 −1internal/awsinv/clients.go+198 −7internal/edge/route53dns.go+29 −1internal/host/memwatch.go+11 −1internal/provision/plan14_root_reads_test.go+7 −0internal/provision/testdata/plan14-root-reads-census-ruling-275.tsv+6 −0internal/provision/testdata/plan14-root-reads-census-ruling-owed.tsv+1 −1internal/steering/route53.go+54 −2
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.011s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 279.377s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.616s
ok github.com/carlosframework/platform/cmd/moveprobe 93.242s
ok github.com/carlosframework/platform/cmd/zddprobe 0.706s
ok github.com/carlosframework/platform/internal/activator 239.152s
ok github.com/carlosframework/platform/internal/agentsock 0.016s
ok github.com/carlosframework/platform/internal/alerting 0.003s
ok github.com/carlosframework/platform/internal/analytics 0.018s
ok github.com/carlosframework/platform/internal/appconfig 2.320s
ok github.com/carlosframework/platform/internal/apperr 0.071s
ok github.com/carlosframework/platform/internal/applog 0.128s
ok github.com/carlosframework/platform/internal/awsinv 0.003s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 0.800s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.008s
ok github.com/carlosframework/platform/internal/caps 0.012s
ok github.com/carlosframework/platform/internal/certwarm 0.005s
ok github.com/carlosframework/platform/internal/cgroup 0.698s
ok github.com/carlosframework/platform/internal/changelog 0.308s
ok github.com/carlosframework/platform/internal/clamd 0.054s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 362.971s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 5.934s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.008s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.003s
ok github.com/carlosframework/platform/internal/docsite 0.004s
ok github.com/carlosframework/platform/internal/domains 2.177s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.003s
ok github.com/carlosframework/platform/internal/edge 26.012s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.007s
ok github.com/carlosframework/platform/internal/fleetapi 0.121s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.014s
ok github.com/carlosframework/platform/internal/geo 0.016s
ok github.com/carlosframework/platform/internal/geoip 0.030s
ok github.com/carlosframework/platform/internal/host 128.233s
ok github.com/carlosframework/platform/internal/hostactors 0.367s
ok github.com/carlosframework/platform/internal/hostbox 2.092s
ok github.com/carlosframework/platform/internal/hostclaim 0.481s
ok github.com/carlosframework/platform/internal/hostevidence 0.015s
ok github.com/carlosframework/platform/internal/hostexec 4.758s
ok github.com/carlosframework/platform/internal/hostfence 0.035s
ok github.com/carlosframework/platform/internal/hostfiles 10.495s
ok github.com/carlosframework/platform/internal/hostfloor 0.015s
ok github.com/carlosframework/platform/internal/hostguard 1.004s
ok github.com/carlosframework/platform/internal/hostops 1.410s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.015s
ok github.com/carlosframework/platform/internal/hostquarantine 0.015s
ok github.com/carlosframework/platform/internal/hostrelocate 0.426s
ok github.com/carlosframework/platform/internal/hostresolve 0.133s
ok github.com/carlosframework/platform/internal/hostwrite 0.015s
ok github.com/carlosframework/platform/internal/infraview 0.016s
ok github.com/carlosframework/platform/internal/instancectl 3.497s
ok github.com/carlosframework/platform/internal/instanceexec 2.722s
ok github.com/carlosframework/platform/internal/instancefs 0.018s
ok github.com/carlosframework/platform/internal/instancekey 0.016s
ok github.com/carlosframework/platform/internal/instanceupdates 0.022s
ok github.com/carlosframework/platform/internal/layout 1.429s
ok github.com/carlosframework/platform/internal/lease 9.795s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.021s
ok github.com/carlosframework/platform/internal/ledger 0.011s
ok github.com/carlosframework/platform/internal/mail 0.159s
ok github.com/carlosframework/platform/internal/maintenance 0.080s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.497s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.084s
ok github.com/carlosframework/platform/internal/metrics 0.007s
ok github.com/carlosframework/platform/internal/moves 20.947s
ok github.com/carlosframework/platform/internal/moves/archive 0.742s
ok github.com/carlosframework/platform/internal/objstore 0.172s
ok github.com/carlosframework/platform/internal/ops 0.159s
ok github.com/carlosframework/platform/internal/park 0.199s
ok github.com/carlosframework/platform/internal/privatefile 0.009s
ok github.com/carlosframework/platform/internal/provision 12.334s
ok github.com/carlosframework/platform/internal/publicrelease 0.006s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.050s
ok github.com/carlosframework/platform/internal/relay 7.222s
ok github.com/carlosframework/platform/internal/release 19.176s
ok github.com/carlosframework/platform/internal/rollout 3.661s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.006s
ok github.com/carlosframework/platform/internal/rootparticipation 0.007s
ok github.com/carlosframework/platform/internal/rootstate 0.025s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.003s
ok github.com/carlosframework/platform/internal/schedule 2.268s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.061s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.008s
ok github.com/carlosframework/platform/internal/status 0.038s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.206s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.002s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.005s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.536s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 45809826 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
minio: downloaded, digest verified
mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
mc: downloaded, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.021s
=== RUN TestT4StorageProbeMinio
2026/10/14 23:21:15 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE88C3B527A128, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.00s)
PASS
ok github.com/carlosframework/platform/internal/host 0.020s
minio contract: PASS
diff --git a/.amadan/ci.d/20-cross@5 b/.amadan/ci.d/20-cross@5| index 9dec4b177..60fe5870a 100755 |
| --- a/.amadan/ci.d/20-cross@5 |
| +++ b/.amadan/ci.d/20-cross@5 |
| @@ -53,3 +53,54 @@ echo "cross-compiling $GOOS/$GOARCH" |
| # here so that adding a cgo dependency fails loudly rather than quietly |
| # changing what this covers. The tree is pure Go today. |
| GOOS="$GOOS" GOARCH="$GOARCH" CGO_ENABLED=0 go build ./... |
| + |
| +# The size ceiling, on the one shard that builds the shipping target. |
| +# |
| +# The platform binary is what every box downloads and what the update gate |
| +# waits on, so its size is a cost to the fleet. On 2026-10-13 it doubled from |
| +# about 45 MB to 77.7 MB linux/arm64 with no one deciding that (discussion |
| +# #275). Cause: html/template, which the edge and console pages use, puts the |
| +# Go linker in a conservative mode that keeps EVERY exported method of any |
| +# concrete type stored in an interface value. internal/awsinv's factoryFor put |
| +# *ec2.Client (~2,400 methods, 14 MB) and six more SDK clients into narrow |
| +# interfaces, and the whole lot came along. The fix is to store METHOD VALUES |
| +# (a struct or func type bound to the few calls actually made) and not the |
| +# client; wrapping the client in a struct does not help. |
| +# |
| +# The ceiling is the measured size after that fix (45,678,754 bytes, 45.7 MB) |
| +# plus 6 MB of headroom for ordinary growth, rounded up to a whole MB: |
| +# 52 MB (decimal, 52000000 bytes). It is a ceiling and not an equality on |
| +# purpose. If you are here because it fired, look for an SDK client stored in |
| +# an interface, which html/template's method retention then drags in whole: |
| +# |
| +# go build -ldflags=-dumpdep -o /dev/null . 2>&1 | |
| +# grep 'go:itab.*aws-sdk-go-v2/service' |
| +# |
| +# names the conversions. Two are inherent and are not the cause: s3.New and |
| +# config.assumeWebIdentity convert their own clients inside the SDK, so S3 |
| +# and STS stay whole whatever this repo does. Raise the number only for |
| +# growth you meant, and say why in the commit. |
| +if [ "$GOOS/$GOARCH" = "linux/arm64" ]; then |
| + max_bytes=52000000 |
| + size_dir=$(mktemp -d) |
| + trap 'rm -rf "$size_dir"' EXIT |
| + GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags "-s -w" -o "$size_dir/carlos" . |
| + # No pipeline here: `wc | tr` would report tr's exit status, an unreadable |
| + # binary would leave size empty, and the comparison below would error |
| + # inside `if` and let the step pass. Fail on the read, then on anything |
| + # that is not a number (BSD wc pads with spaces, which $(( )) accepts). |
| + size=$(wc -c <"$size_dir/carlos") || { echo "cannot read the built binary $size_dir/carlos"; exit 1; } |
| + case $size in |
| + '' | *[!0-9' ']*) echo "could not measure the linux/arm64 binary (wc said '$size')"; exit 1 ;; |
| + esac |
| + size=$((size)) |
| + echo "linux/arm64 carlos binary: $size bytes (ceiling $max_bytes)" |
| + if [ "$size" -gt "$max_bytes" ]; then |
| + echo "the linux/arm64 binary is $size bytes, over the $max_bytes ceiling." |
| + echo "look for an SDK client stored in an interface under html/template's" |
| + echo "method retention (every exported method of the client is kept):" |
| + echo "store method values instead. See discussion #275 and the comment in" |
| + echo ".amadan/ci.d/20-cross@5." |
| + exit 1 |
| + fi |
| +fi |
diff --git a/README.md b/README.md| index 941764166..b2d8c1a51 100644 |
| --- a/README.md |
| +++ b/README.md |
| @@ -295,6 +295,8 @@ A release-specific note goes here when a release changes something an |
| operator needs to check or act on before rolling it. Read the note for |
| the release you are about to roll. |
| +**Binary size (#275).** The linux/arm64 platform binary doubled to 77.7 MB in earlier builds because SDK clients were kept whole by the linker. This release is 45.7 MB again, so boxes download about 32 MB less. CI now fails a build over 52 MB. |
| + |
| **Pointer digests (#253): writers before readers.** Once a box has verified a channel pointer that carries manifest digests, it refuses pointers for that channel without them. So digests are turned on in order. First upgrade every writer: every console, and every CLI or deploy script that promotes, rolls back or fast-tracks. Then run `carlos channels digests on`. Then watch `carlos channels digests`: a channel marked as written by an old writer means one is still in use. Rolling a writer back below this release stops it writing pointers boxes accept; `carlos channels digests off` suspends digests while it is fixed, but does not clear the boxes that have already seen them. When `carlos channels digests` lists nothing, set `CARLOS_REQUIRE_POINTER_DIGEST=on` in every box's host.env, so a new box refuses old pointers too. |
| **Instance update settings: the roll order.** Roll every box to the release that applies update settings before anyone turns on manual updates, a major hold or a pin: the console refuses those until every box reports it can protect the builds they name from pruning, and says how many boxes it is waiting on. Before rolling any box back below that release, turn retention off for the deployment (`carlos releases retention --off` for each app that has one), because an older housekeeper prunes builds a pin or a held instance still needs. After such a rollback the console shows *settings not enforced* for instances on the older boxes: they follow their channel until the box is rolled forward again. |
diff --git a/internal/alerting/alerting.go b/internal/alerting/alerting.go| index 22ceb2df0..ec088e105 100644 |
| --- a/internal/alerting/alerting.go |
| +++ b/internal/alerting/alerting.go |
| @@ -274,5 +274,16 @@ func NewSNSClient(ctx context.Context, region string) (Publisher, error) { |
| if err != nil { |
| return nil, err |
| } |
| - return sns.NewFromConfig(cfg), nil |
| + return publishFunc(sns.NewFromConfig(cfg).Publish), nil |
| +} |
| + |
| +// publishFunc is the Publisher over one bound method value. It is not |
| +// `return sns.NewFromConfig(cfg), nil` on purpose: an *sns.Client stored in |
| +// an interface makes the linker keep every exported SNS method once |
| +// html/template is linked in (discussion #275, ~0.75 MB here and ~15 MB |
| +// across the SDK clients). A method value keeps Publish alone. |
| +type publishFunc func(context.Context, *sns.PublishInput, ...func(*sns.Options)) (*sns.PublishOutput, error) |
| + |
| +func (f publishFunc) Publish(ctx context.Context, in *sns.PublishInput, opts ...func(*sns.Options)) (*sns.PublishOutput, error) { |
| + return f(ctx, in, opts...) |
| } |
diff --git a/internal/awsinv/clients.go b/internal/awsinv/clients.go| index 094e91c56..0a299b02b 100644 |
| --- a/internal/awsinv/clients.go |
| +++ b/internal/awsinv/clients.go |
| @@ -67,6 +67,189 @@ type TaggingAPI interface { |
| GetResources(context.Context, *resourcegroupstaggingapi.GetResourcesInput, ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) |
| } |
| +// ec2Methods satisfies EC2API from method values of one ec2 client. |
| +type ec2Methods struct { |
| + describeInstances func(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) |
| + describeVolumes func(context.Context, *ec2.DescribeVolumesInput, ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error) |
| + describeAddresses func(context.Context, *ec2.DescribeAddressesInput, ...func(*ec2.Options)) (*ec2.DescribeAddressesOutput, error) |
| + describeSecurityGroups func(context.Context, *ec2.DescribeSecurityGroupsInput, ...func(*ec2.Options)) (*ec2.DescribeSecurityGroupsOutput, error) |
| + describeVpcs func(context.Context, *ec2.DescribeVpcsInput, ...func(*ec2.Options)) (*ec2.DescribeVpcsOutput, error) |
| + describeSubnets func(context.Context, *ec2.DescribeSubnetsInput, ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) |
| + describeRegions func(context.Context, *ec2.DescribeRegionsInput, ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error) |
| +} |
| + |
| +func (m ec2Methods) DescribeInstances(ctx context.Context, in *ec2.DescribeInstancesInput, opts ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) { |
| + return m.describeInstances(ctx, in, opts...) |
| +} |
| + |
| +func (m ec2Methods) DescribeVolumes(ctx context.Context, in *ec2.DescribeVolumesInput, opts ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error) { |
| + return m.describeVolumes(ctx, in, opts...) |
| +} |
| + |
| +func (m ec2Methods) DescribeAddresses(ctx context.Context, in *ec2.DescribeAddressesInput, opts ...func(*ec2.Options)) (*ec2.DescribeAddressesOutput, error) { |
| + return m.describeAddresses(ctx, in, opts...) |
| +} |
| + |
| +func (m ec2Methods) DescribeSecurityGroups(ctx context.Context, in *ec2.DescribeSecurityGroupsInput, opts ...func(*ec2.Options)) (*ec2.DescribeSecurityGroupsOutput, error) { |
| + return m.describeSecurityGroups(ctx, in, opts...) |
| +} |
| + |
| +func (m ec2Methods) DescribeVpcs(ctx context.Context, in *ec2.DescribeVpcsInput, opts ...func(*ec2.Options)) (*ec2.DescribeVpcsOutput, error) { |
| + return m.describeVpcs(ctx, in, opts...) |
| +} |
| + |
| +func (m ec2Methods) DescribeSubnets(ctx context.Context, in *ec2.DescribeSubnetsInput, opts ...func(*ec2.Options)) (*ec2.DescribeSubnetsOutput, error) { |
| + return m.describeSubnets(ctx, in, opts...) |
| +} |
| + |
| +func (m ec2Methods) DescribeRegions(ctx context.Context, in *ec2.DescribeRegionsInput, opts ...func(*ec2.Options)) (*ec2.DescribeRegionsOutput, error) { |
| + return m.describeRegions(ctx, in, opts...) |
| +} |
| + |
| +func newEc2Methods(c *ec2.Client) ec2Methods { |
| + return ec2Methods{ |
| + describeInstances: c.DescribeInstances, |
| + describeVolumes: c.DescribeVolumes, |
| + describeAddresses: c.DescribeAddresses, |
| + describeSecurityGroups: c.DescribeSecurityGroups, |
| + describeVpcs: c.DescribeVpcs, |
| + describeSubnets: c.DescribeSubnets, |
| + describeRegions: c.DescribeRegions, |
| + } |
| +} |
| + |
| +// s3Methods satisfies S3API from method values of one s3 client. |
| +type s3Methods struct { |
| + listBuckets func(context.Context, *s3.ListBucketsInput, ...func(*s3.Options)) (*s3.ListBucketsOutput, error) |
| + getBucketLocation func(context.Context, *s3.GetBucketLocationInput, ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) |
| + getBucketVersioning func(context.Context, *s3.GetBucketVersioningInput, ...func(*s3.Options)) (*s3.GetBucketVersioningOutput, error) |
| + getBucketEncryption func(context.Context, *s3.GetBucketEncryptionInput, ...func(*s3.Options)) (*s3.GetBucketEncryptionOutput, error) |
| + getPublicAccessBlock func(context.Context, *s3.GetPublicAccessBlockInput, ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) |
| +} |
| + |
| +func (m s3Methods) ListBuckets(ctx context.Context, in *s3.ListBucketsInput, opts ...func(*s3.Options)) (*s3.ListBucketsOutput, error) { |
| + return m.listBuckets(ctx, in, opts...) |
| +} |
| + |
| +func (m s3Methods) GetBucketLocation(ctx context.Context, in *s3.GetBucketLocationInput, opts ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) { |
| + return m.getBucketLocation(ctx, in, opts...) |
| +} |
| + |
| +func (m s3Methods) GetBucketVersioning(ctx context.Context, in *s3.GetBucketVersioningInput, opts ...func(*s3.Options)) (*s3.GetBucketVersioningOutput, error) { |
| + return m.getBucketVersioning(ctx, in, opts...) |
| +} |
| + |
| +func (m s3Methods) GetBucketEncryption(ctx context.Context, in *s3.GetBucketEncryptionInput, opts ...func(*s3.Options)) (*s3.GetBucketEncryptionOutput, error) { |
| + return m.getBucketEncryption(ctx, in, opts...) |
| +} |
| + |
| +func (m s3Methods) GetPublicAccessBlock(ctx context.Context, in *s3.GetPublicAccessBlockInput, opts ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) { |
| + return m.getPublicAccessBlock(ctx, in, opts...) |
| +} |
| + |
| +func newS3Methods(c *s3.Client) s3Methods { |
| + return s3Methods{ |
| + listBuckets: c.ListBuckets, |
| + getBucketLocation: c.GetBucketLocation, |
| + getBucketVersioning: c.GetBucketVersioning, |
| + getBucketEncryption: c.GetBucketEncryption, |
| + getPublicAccessBlock: c.GetPublicAccessBlock, |
| + } |
| +} |
| + |
| +// route53Methods satisfies Route53API from method values of one route53 client. |
| +type route53Methods struct { |
| + listHostedZones func(context.Context, *route53.ListHostedZonesInput, ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error) |
| + listResourceRecordSets func(context.Context, *route53.ListResourceRecordSetsInput, ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error) |
| +} |
| + |
| +func (m route53Methods) ListHostedZones(ctx context.Context, in *route53.ListHostedZonesInput, opts ...func(*route53.Options)) (*route53.ListHostedZonesOutput, error) { |
| + return m.listHostedZones(ctx, in, opts...) |
| +} |
| + |
| +func (m route53Methods) ListResourceRecordSets(ctx context.Context, in *route53.ListResourceRecordSetsInput, opts ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error) { |
| + return m.listResourceRecordSets(ctx, in, opts...) |
| +} |
| + |
| +func newRoute53Methods(c *route53.Client) route53Methods { |
| + return route53Methods{ |
| + listHostedZones: c.ListHostedZones, |
| + listResourceRecordSets: c.ListResourceRecordSets, |
| + } |
| +} |
| + |
| +// iamMethods satisfies IAMAPI from method values of one iam client. |
| +type iamMethods struct { |
| + listRoles func(context.Context, *iam.ListRolesInput, ...func(*iam.Options)) (*iam.ListRolesOutput, error) |
| + listUsers func(context.Context, *iam.ListUsersInput, ...func(*iam.Options)) (*iam.ListUsersOutput, error) |
| + listAttachedRolePolicies func(context.Context, *iam.ListAttachedRolePoliciesInput, ...func(*iam.Options)) (*iam.ListAttachedRolePoliciesOutput, error) |
| +} |
| + |
| +func (m iamMethods) ListRoles(ctx context.Context, in *iam.ListRolesInput, opts ...func(*iam.Options)) (*iam.ListRolesOutput, error) { |
| + return m.listRoles(ctx, in, opts...) |
| +} |
| + |
| +func (m iamMethods) ListUsers(ctx context.Context, in *iam.ListUsersInput, opts ...func(*iam.Options)) (*iam.ListUsersOutput, error) { |
| + return m.listUsers(ctx, in, opts...) |
| +} |
| + |
| +func (m iamMethods) ListAttachedRolePolicies(ctx context.Context, in *iam.ListAttachedRolePoliciesInput, opts ...func(*iam.Options)) (*iam.ListAttachedRolePoliciesOutput, error) { |
| + return m.listAttachedRolePolicies(ctx, in, opts...) |
| +} |
| + |
| +func newIamMethods(c *iam.Client) iamMethods { |
| + return iamMethods{ |
| + listRoles: c.ListRoles, |
| + listUsers: c.ListUsers, |
| + listAttachedRolePolicies: c.ListAttachedRolePolicies, |
| + } |
| +} |
| + |
| +// cloudwatchMethods satisfies CloudWatchAPI from method values of one cloudwatch client. |
| +type cloudwatchMethods struct { |
| + describeAlarms func(context.Context, *cloudwatch.DescribeAlarmsInput, ...func(*cloudwatch.Options)) (*cloudwatch.DescribeAlarmsOutput, error) |
| +} |
| + |
| +func (m cloudwatchMethods) DescribeAlarms(ctx context.Context, in *cloudwatch.DescribeAlarmsInput, opts ...func(*cloudwatch.Options)) (*cloudwatch.DescribeAlarmsOutput, error) { |
| + return m.describeAlarms(ctx, in, opts...) |
| +} |
| + |
| +func newCloudwatchMethods(c *cloudwatch.Client) cloudwatchMethods { |
| + return cloudwatchMethods{ |
| + describeAlarms: c.DescribeAlarms, |
| + } |
| +} |
| + |
| +// acmMethods satisfies ACMAPI from method values of one acm client. |
| +type acmMethods struct { |
| + listCertificates func(context.Context, *acm.ListCertificatesInput, ...func(*acm.Options)) (*acm.ListCertificatesOutput, error) |
| +} |
| + |
| +func (m acmMethods) ListCertificates(ctx context.Context, in *acm.ListCertificatesInput, opts ...func(*acm.Options)) (*acm.ListCertificatesOutput, error) { |
| + return m.listCertificates(ctx, in, opts...) |
| +} |
| + |
| +func newAcmMethods(c *acm.Client) acmMethods { |
| + return acmMethods{ |
| + listCertificates: c.ListCertificates, |
| + } |
| +} |
| + |
| +// resourcegroupstaggingapiMethods satisfies TaggingAPI from method values of one resourcegroupstaggingapi client. |
| +type resourcegroupstaggingapiMethods struct { |
| + getResources func(context.Context, *resourcegroupstaggingapi.GetResourcesInput, ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) |
| +} |
| + |
| +func (m resourcegroupstaggingapiMethods) GetResources(ctx context.Context, in *resourcegroupstaggingapi.GetResourcesInput, opts ...func(*resourcegroupstaggingapi.Options)) (*resourcegroupstaggingapi.GetResourcesOutput, error) { |
| + return m.getResources(ctx, in, opts...) |
| +} |
| + |
| +func newResourcegroupstaggingapiMethods(c *resourcegroupstaggingapi.Client) resourcegroupstaggingapiMethods { |
| + return resourcegroupstaggingapiMethods{ |
| + getResources: c.GetResources, |
| + } |
| +} |
| + |
| // Clients is one region's worth of read-only clients. S3/Route53/IAM are |
| // global services; the assembly calls them once, from the home region's |
| // Clients, rather than once per region. |
| @@ -85,6 +268,14 @@ type Clients struct { |
| // credential chain resolves exactly once. |
| type Factory func(region string) Clients |
| +// factoryFor stores METHOD VALUES, not the SDK clients, in the interfaces. |
| +// html/template (the edge and console pages) switches the linker into a |
| +// mode that keeps every exported method of any concrete type stored in an |
| +// interface value. A *ec2.Client in an interface kept all ~2,400 EC2 |
| +// methods (14 MB), and the other clients kept theirs too: the linux/arm64 |
| +// binary doubled to 77.7 MB (discussion #275). A func value bound to one |
| +// method keeps that method and nothing else. Do not "simplify" these back |
| +// to ec2.NewFromConfig(c) in the struct; the CI size ceiling will say so. |
| func factoryFor(cfg aws.Config) Factory { |
| return func(region string) Clients { |
| c := cfg.Copy() |
| @@ -92,13 +283,13 @@ func factoryFor(cfg aws.Config) Factory { |
| c.Region = region |
| } |
| return Clients{ |
| - EC2: ec2.NewFromConfig(c), |
| - S3: s3.NewFromConfig(c), |
| - Route53: route53.NewFromConfig(c), |
| - IAM: iam.NewFromConfig(c), |
| - CloudWatch: cloudwatch.NewFromConfig(c), |
| - ACM: acm.NewFromConfig(c), |
| - Tagging: resourcegroupstaggingapi.NewFromConfig(c), |
| + EC2: newEc2Methods(ec2.NewFromConfig(c)), |
| + S3: newS3Methods(s3.NewFromConfig(c)), |
| + Route53: newRoute53Methods(route53.NewFromConfig(c)), |
| + IAM: newIamMethods(iam.NewFromConfig(c)), |
| + CloudWatch: newCloudwatchMethods(cloudwatch.NewFromConfig(c)), |
| + ACM: newAcmMethods(acm.NewFromConfig(c)), |
| + Tagging: newResourcegroupstaggingapiMethods(resourcegroupstaggingapi.NewFromConfig(c)), |
| } |
| } |
| } |
diff --git a/internal/edge/route53dns.go b/internal/edge/route53dns.go| index c7ad6fe8b..f19ff8940 100644 |
| --- a/internal/edge/route53dns.go |
| +++ b/internal/edge/route53dns.go |
| @@ -43,6 +43,34 @@ type route53API interface { |
| GetChange(ctx context.Context, in *route53.GetChangeInput, optFns ...func(*route53.Options)) (*route53.GetChangeOutput, error) |
| } |
| +// route53Methods satisfies route53API from method values of one route53 client. |
| +// Storing the *route53.Client itself in the interface would make the linker keep every exported Route53 method once html/template is linked in (discussion #275); method values keep only the calls made here. |
| +type route53Methods struct { |
| + listHostedZonesByName func(ctx context.Context, in *route53.ListHostedZonesByNameInput, optFns ...func(*route53.Options)) (*route53.ListHostedZonesByNameOutput, error) |
| + changeResourceRecordSets func(ctx context.Context, in *route53.ChangeResourceRecordSetsInput, optFns ...func(*route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error) |
| + getChange func(ctx context.Context, in *route53.GetChangeInput, optFns ...func(*route53.Options)) (*route53.GetChangeOutput, error) |
| +} |
| + |
| +func (m route53Methods) ListHostedZonesByName(ctx context.Context, in *route53.ListHostedZonesByNameInput, opts ...func(*route53.Options)) (*route53.ListHostedZonesByNameOutput, error) { |
| + return m.listHostedZonesByName(ctx, in, opts...) |
| +} |
| + |
| +func (m route53Methods) ChangeResourceRecordSets(ctx context.Context, in *route53.ChangeResourceRecordSetsInput, opts ...func(*route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error) { |
| + return m.changeResourceRecordSets(ctx, in, opts...) |
| +} |
| + |
| +func (m route53Methods) GetChange(ctx context.Context, in *route53.GetChangeInput, opts ...func(*route53.Options)) (*route53.GetChangeOutput, error) { |
| + return m.getChange(ctx, in, opts...) |
| +} |
| + |
| +func newRoute53Methods(c *route53.Client) route53Methods { |
| + return route53Methods{ |
| + listHostedZonesByName: c.ListHostedZonesByName, |
| + changeResourceRecordSets: c.ChangeResourceRecordSets, |
| + getChange: c.GetChange, |
| + } |
| +} |
| + |
| type route53DNS struct { |
| client route53API |
| mu sync.Mutex |
| @@ -62,7 +90,7 @@ func newRoute53DNS(ctx context.Context) (dnsProvider, error) { |
| if err != nil { |
| return nil, fmt.Errorf("route53: load aws config: %w", err) |
| } |
| - return &route53DNS{client: route53.NewFromConfig(cfg), zoneIDs: map[string]string{}}, nil |
| + return &route53DNS{client: newRoute53Methods(route53.NewFromConfig(cfg)), zoneIDs: map[string]string{}}, nil |
| } |
| // present satisfies dnsProvider: UPSERT the challenge TXT, wait for INSYNC, |
diff --git a/internal/host/memwatch.go b/internal/host/memwatch.go| index a6eb39e3e..5172cbb45 100644 |
| --- a/internal/host/memwatch.go |
| +++ b/internal/host/memwatch.go |
| @@ -44,6 +44,16 @@ type metricPutter interface { |
| PutMetricData(context.Context, *cloudwatch.PutMetricDataInput, ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) |
| } |
| +// putMetricFunc is the metricPutter over one bound method value. Storing the |
| +// *cloudwatch.Client itself in the interface would make the linker keep every |
| +// exported CloudWatch method once html/template is linked in (discussion |
| +// #275); a method value keeps PutMetricData alone. |
| +type putMetricFunc func(context.Context, *cloudwatch.PutMetricDataInput, ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) |
| + |
| +func (f putMetricFunc) PutMetricData(ctx context.Context, in *cloudwatch.PutMetricDataInput, opts ...func(*cloudwatch.Options)) (*cloudwatch.PutMetricDataOutput, error) { |
| + return f(ctx, in, opts...) |
| +} |
| + |
| type memwatch struct { |
| put metricPutter |
| instanceID string |
| @@ -109,7 +119,7 @@ func memwatchLoop() { |
| return |
| } |
| w := memwatch{ |
| - put: cloudwatch.NewFromConfig(cfg), |
| + put: putMetricFunc(cloudwatch.NewFromConfig(cfg).PutMetricData), |
| instanceID: id, |
| mem: readMemBytes, |
| pressure: readPressureFile, |
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index 4ffe33430..298c5e9db 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -684,6 +684,13 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| // rows, three new proof units and the reclassified instance-control |
| // Server declaration add twelve frozen declarations. |
| seal = "3b2506dd2ab3986d0df0ceda7f596e5a46ea4d1dfbc17b1d0fd8d5f176e8038d" |
| + // Census ruling 275 (amadan #275, platform-1e): envelope-only, for the SDK |
| + // clients held as method values in awsinv, alerting, host, edge and |
| + // steering. No root-read unit changed or was added, so the legacy seal |
| + // above stands. |
| + if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-census-ruling-275.tsv"), &inv, true); err != nil { |
| + t.Fatal(err) |
| + } |
| if errs := checkRootReadLegacy(inv, seal); len(errs) != 0 { |
| t.Fatal(errs) |
| } |
diff --git a/internal/provision/testdata/plan14-root-reads-census-ruling-275.tsv b/internal/provision/testdata/plan14-root-reads-census-ruling-275.tsv| new file mode 100644 |
| index 000000000..1df9a424d |
| --- /dev/null |
| +++ b/internal/provision/testdata/plan14-root-reads-census-ruling-275.tsv |
| @@ -0,0 +1,6 @@ |
| +# Plan 14 census ruling 275 (amadan #275, owner platform-1e, basis census-ruling-275): #275: SDK clients stored as method values; same calls, no new read. The AWS SDK clients in internal/awsinv, internal/alerting (SNS), internal/host (CloudWatch memwatch), internal/edge (Route53 DNS) and internal/steering (Route53) are now held as method values bound to the calls each package already made, instead of as whole clients, so the linker stops keeping every exported SDK method (the linux/arm64 binary is 45.7 MB, was 77.7 MB). Envelope-only: no root-read unit changed or was added, nothing runs as root that did not before, and no registry, /data, release or claim read was added, so the legacy seal stands. |
| +@coverage internal/alerting 11731f92da401e275f068cc916bbded5bb9c8fd6d13aa6081173ef25e992d490 |
| +@coverage internal/awsinv ff24caa8df601c6888977ccd22cb5e09a064a42b513239a9015ee7802982a90f |
| +@coverage internal/edge bbae68d643e8f24ed46127e18f19b3eef67deea5ab8d2f50059b8f3b6d056835 |
| +@coverage internal/host e97a6b497c56ba6f884eebdc1a9806f053a2a88075d2c4b22ceecdeb92dfd36a |
| +@coverage internal/steering a55ccdbc5d6ee2404a50c840fa290807e012df332f14c613b08bf14d0b8e19d4 |
diff --git a/internal/provision/testdata/plan14-root-reads-census-ruling-owed.tsv b/internal/provision/testdata/plan14-root-reads-census-ruling-owed.tsv| index d7e475027..37e2989a7 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-census-ruling-owed.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-census-ruling-owed.tsv |
| @@ -1,4 +1,4 @@ |
| -# Plan 14 census ruling owed (amadan #272, branch zdd-owed-items; approved by platform-1e as census-ruling-owed): the cold wake's build-state record moves from step 2b to the first exec, after the replica-key gate and the cgroup refusals, so a refused launch leaves no record (owed before O8). After the write the exec is decided again (the exec gate, the live check, the wake's context and maintenance fence), and a nested seed writes its obligation before its record (Astra's review, P1 and P2). |
| +# Plan 14 census ruling owed (amadan #272, branch zdd-owed-items; approved by platform-1e as census-ruling-owed; loaded after census-ruling-275): the cold wake's build-state record moves from step 2b to the first exec, after the replica-key gate and the cgroup refusals, so a refused launch leaves no record (owed before O8). After the write the exec is decided again (the exec gate, the live check, the wake's context and maintenance fence), and a nested seed writes its obligation before its record (Astra's review, P1 and P2). |
| # The four changed activator units stay accepted legacy because they stay agent-side, running as carlos: only runAgent builds the activator, under carlos-edge.service with User=carlos. The record is the existing BeforeExec build-state write, moved later, never earlier. No new read and no root decision. If any of these units ever runs as root or reads outside that boundary it stops being legacy and needs the Plan 14 owner's ruling. Earlier overlays are immutable. |
| @legacy bdd27149b9dcce980ebca238207c286eeefd52353b2daa752516123d71a2c0a7 internal/activator/activator.go Service.doWake legacy accepted-legacy accepted-legacy;rev3=census-ruling-owed;basis=Census ruling owed (amadan #272, zdd-owed-items): changed legacy row; step 2b no longer writes the build-state record: the wake carries it pending on its context (settleWakeIdentity's) and the first exec writes it. Boundary: agent-side, running as carlos (only runAgent builds the activator, under carlos-edge.service, User=carlos); the record is the existing BeforeExec build-state write, moved later, never earlier; no new read, no root decision, and no registry, /data, release or claim read beyond what the unit read before. registry:DBPath@1032,registry:DBPath@1048,registry:DBPath@1175,registry:DBPath@1186,registry:DBPath@20401,registry:Host@1014,registry:Host@1166,registry:Host@1277,registry:Host@174,registry:Host@2137,registry:Host@3858,registry:Host@861,registry:Host@936,registry:Replication@20436,registry:Seed@11207,registry:Socket@1061,registry:Socket@1077,registry:Socket@1200,registry:Socket@1211,whole:Route@10058,whole:Route@11308,whole:Route@11379,whole:Route@1231,whole:Route@1237,whole:Route@1303,whole:Route@1311,whole:Route@13763,whole:Route@15621,whole:Route@159,whole:Route@16267,whole:Route@17574,whole:Route@17728,whole:Route@18084,whole:Route@18178,whole:Route@18850,whole:Route@19849,whole:Route@19997,whole:Route@2027,whole:Route@20510,whole:Route@21408,whole:Route@21503,whole:Route@3481,whole:Route@3740,whole:Route@3810,whole:Route@3816,whole:Route@3885,whole:Route@3893,whole:Route@4681,whole:Route@5680,whole:Route@6713,whole:Route@7594,whole:Route@9176,whole:Route@9368,whole:Route@959 f09606c9a22d961f17610105b6b0b126676f62874fe8ef8a5b09649a8b36b73f |
| internal/activator/activator.go Service.doWake legacy accepted-legacy accepted-legacy;rev3=census-ruling-owed;basis=Census ruling owed (amadan #272, zdd-owed-items): changed legacy row; step 2b no longer writes the build-state record: the wake carries it pending on its context (settleWakeIdentity's) and the first exec writes it. Boundary: agent-side, running as carlos (only runAgent builds the activator, under carlos-edge.service, User=carlos); the record is the existing BeforeExec build-state write, moved later, never earlier; no new read, no root decision, and no registry, /data, release or claim read beyond what the unit read before. registry:DBPath@1032,registry:DBPath@1048,registry:DBPath@1175,registry:DBPath@1186,registry:DBPath@20401,registry:Host@1014,registry:Host@1166,registry:Host@1277,registry:Host@174,registry:Host@2137,registry:Host@3858,registry:Host@861,registry:Host@936,registry:Replication@20436,registry:Seed@11207,registry:Socket@1061,registry:Socket@1077,registry:Socket@1200,registry:Socket@1211,whole:Route@10058,whole:Route@11308,whole:Route@11379,whole:Route@1231,whole:Route@1237,whole:Route@1303,whole:Route@1311,whole:Route@13763,whole:Route@15621,whole:Route@159,whole:Route@16267,whole:Route@17574,whole:Route@17728,whole:Route@18084,whole:Route@18178,whole:Route@18850,whole:Route@19849,whole:Route@19997,whole:Route@2027,whole:Route@20510,whole:Route@21408,whole:Route@21503,whole:Route@3481,whole:Route@3740,whole:Route@3810,whole:Route@3816,whole:Route@3885,whole:Route@3893,whole:Route@4681,whole:Route@5680,whole:Route@6713,whole:Route@7594,whole:Route@9176,whole:Route@9368,whole:Route@959 f09606c9a22d961f17610105b6b0b126676f62874fe8ef8a5b09649a8b36b73f |
diff --git a/internal/steering/route53.go b/internal/steering/route53.go| index 6ec1fa7a0..653a3f25d 100644 |
| --- a/internal/steering/route53.go |
| +++ b/internal/steering/route53.go |
| @@ -88,6 +88,58 @@ type route53API interface { |
| ChangeTagsForResource(ctx context.Context, in *route53.ChangeTagsForResourceInput, optFns ...func(*route53.Options)) (*route53.ChangeTagsForResourceOutput, error) |
| } |
| +// route53Methods satisfies route53API from method values of one route53 client. |
| +// Storing the *route53.Client itself in the interface would make the linker keep every exported Route53 method once html/template is linked in (discussion #275); method values keep only the calls made here. |
| +type route53Methods struct { |
| + listResourceRecordSets func(ctx context.Context, in *route53.ListResourceRecordSetsInput, optFns ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error) |
| + changeResourceRecordSets func(ctx context.Context, in *route53.ChangeResourceRecordSetsInput, optFns ...func(*route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error) |
| + listHealthChecks func(ctx context.Context, in *route53.ListHealthChecksInput, optFns ...func(*route53.Options)) (*route53.ListHealthChecksOutput, error) |
| + listTagsForResources func(ctx context.Context, in *route53.ListTagsForResourcesInput, optFns ...func(*route53.Options)) (*route53.ListTagsForResourcesOutput, error) |
| + createHealthCheck func(ctx context.Context, in *route53.CreateHealthCheckInput, optFns ...func(*route53.Options)) (*route53.CreateHealthCheckOutput, error) |
| + deleteHealthCheck func(ctx context.Context, in *route53.DeleteHealthCheckInput, optFns ...func(*route53.Options)) (*route53.DeleteHealthCheckOutput, error) |
| + changeTagsForResource func(ctx context.Context, in *route53.ChangeTagsForResourceInput, optFns ...func(*route53.Options)) (*route53.ChangeTagsForResourceOutput, error) |
| +} |
| + |
| +func (m route53Methods) ListResourceRecordSets(ctx context.Context, in *route53.ListResourceRecordSetsInput, opts ...func(*route53.Options)) (*route53.ListResourceRecordSetsOutput, error) { |
| + return m.listResourceRecordSets(ctx, in, opts...) |
| +} |
| + |
| +func (m route53Methods) ChangeResourceRecordSets(ctx context.Context, in *route53.ChangeResourceRecordSetsInput, opts ...func(*route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error) { |
| + return m.changeResourceRecordSets(ctx, in, opts...) |
| +} |
| + |
| +func (m route53Methods) ListHealthChecks(ctx context.Context, in *route53.ListHealthChecksInput, opts ...func(*route53.Options)) (*route53.ListHealthChecksOutput, error) { |
| + return m.listHealthChecks(ctx, in, opts...) |
| +} |
| + |
| +func (m route53Methods) ListTagsForResources(ctx context.Context, in *route53.ListTagsForResourcesInput, opts ...func(*route53.Options)) (*route53.ListTagsForResourcesOutput, error) { |
| + return m.listTagsForResources(ctx, in, opts...) |
| +} |
| + |
| +func (m route53Methods) CreateHealthCheck(ctx context.Context, in *route53.CreateHealthCheckInput, opts ...func(*route53.Options)) (*route53.CreateHealthCheckOutput, error) { |
| + return m.createHealthCheck(ctx, in, opts...) |
| +} |
| + |
| +func (m route53Methods) DeleteHealthCheck(ctx context.Context, in *route53.DeleteHealthCheckInput, opts ...func(*route53.Options)) (*route53.DeleteHealthCheckOutput, error) { |
| + return m.deleteHealthCheck(ctx, in, opts...) |
| +} |
| + |
| +func (m route53Methods) ChangeTagsForResource(ctx context.Context, in *route53.ChangeTagsForResourceInput, opts ...func(*route53.Options)) (*route53.ChangeTagsForResourceOutput, error) { |
| + return m.changeTagsForResource(ctx, in, opts...) |
| +} |
| + |
| +func newRoute53Methods(c *route53.Client) route53Methods { |
| + return route53Methods{ |
| + listResourceRecordSets: c.ListResourceRecordSets, |
| + changeResourceRecordSets: c.ChangeResourceRecordSets, |
| + listHealthChecks: c.ListHealthChecks, |
| + listTagsForResources: c.ListTagsForResources, |
| + createHealthCheck: c.CreateHealthCheck, |
| + deleteHealthCheck: c.DeleteHealthCheck, |
| + changeTagsForResource: c.ChangeTagsForResource, |
| + } |
| +} |
| + |
| // SteeringZoneID returns the deployment's validated steering hosted-zone id, |
| // or "" when the deployment is dormant (env unset, blank, or malformed — |
| // malformed is logged, because a typo'd zone id is a configuration error |
| @@ -109,7 +161,7 @@ func SteeringZoneID() string { |
| // internal/edge/route53dns.go's newRoute53DNS. Route53 is a global service |
| // but the SDK still needs a signing region, so default to us-east-1 when the |
| // environment names none; an explicit AWS_REGION still wins. |
| -func NewClient(ctx context.Context) (*route53.Client, error) { |
| +func NewClient(ctx context.Context) (route53API, error) { |
| var opts []func(*awsconfig.LoadOptions) error |
| if strings.TrimSpace(os.Getenv("AWS_REGION")) == "" && strings.TrimSpace(os.Getenv("AWS_DEFAULT_REGION")) == "" { |
| opts = append(opts, awsconfig.WithRegion("us-east-1")) |
| @@ -118,7 +170,7 @@ func NewClient(ctx context.Context) (*route53.Client, error) { |
| if err != nil { |
| return nil, fmt.Errorf("steering: load aws config: %w", err) |
| } |
| - return route53.NewFromConfig(cfg), nil |
| + return newRoute53Methods(route53.NewFromConfig(cfg)), nil |
| } |
| // ownedCheck is one Route53 health check this deployment owns: tagged |