carlos / platform Public

fix-binary-size-275

Squashed from fix-binary-size-275 into main.
claude agent, operated by paul@keymail.dev 132cf996ee438fd9760a5920307608559acb9720 parent 2683f16
9 files changed, +370 −12
  • .amadan/ci.d/20-cross@5 +51 −0
  • README.md +2 −0
  • internal/alerting/alerting.go +12 −1
  • internal/awsinv/clients.go +198 −7
  • internal/edge/route53dns.go +29 −1
  • internal/host/memwatch.go +11 −1
  • internal/provision/plan14_root_reads_test.go +7 −0
  • internal/provision/testdata/plan14-root-reads-census-ruling-275.tsv +6 −0
  • internal/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	281.141s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.580s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.255s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.729s
ok  	github.com/carlosframework/platform/internal/activator	237.649s
ok  	github.com/carlosframework/platform/internal/agentsock	0.014s
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.328s
ok  	github.com/carlosframework/platform/internal/apperr	0.072s
ok  	github.com/carlosframework/platform/internal/applog	0.125s
ok  	github.com/carlosframework/platform/internal/awsinv	0.005s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.005s
ok  	github.com/carlosframework/platform/internal/boxid	0.685s
?   	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.009s
ok  	github.com/carlosframework/platform/internal/caps	0.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.701s
ok  	github.com/carlosframework/platform/internal/changelog	0.309s
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.666s
?   	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.706s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.005s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.221s
ok  	github.com/carlosframework/platform/internal/doorkey	0.007s
ok  	github.com/carlosframework/platform/internal/economics	0.003s
ok  	github.com/carlosframework/platform/internal/edge	25.934s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.142s
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.017s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/host	126.128s
ok  	github.com/carlosframework/platform/internal/hostactors	0.400s
ok  	github.com/carlosframework/platform/internal/hostbox	2.065s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.484s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.013s
ok  	github.com/carlosframework/platform/internal/hostexec	4.718s
ok  	github.com/carlosframework/platform/internal/hostfence	0.031s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.516s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.013s
ok  	github.com/carlosframework/platform/internal/hostguard	0.978s
ok  	github.com/carlosframework/platform/internal/hostops	1.417s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.013s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.013s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.455s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.136s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.013s
ok  	github.com/carlosframework/platform/internal/infraview	0.013s
ok  	github.com/carlosframework/platform/internal/instancectl	3.498s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.699s
ok  	github.com/carlosframework/platform/internal/instancefs	0.017s
ok  	github.com/carlosframework/platform/internal/instancekey	0.018s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.021s
ok  	github.com/carlosframework/platform/internal/layout	1.404s
ok  	github.com/carlosframework/platform/internal/lease	9.804s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.018s
ok  	github.com/carlosframework/platform/internal/ledger	0.011s
ok  	github.com/carlosframework/platform/internal/mail	0.165s
ok  	github.com/carlosframework/platform/internal/maintenance	0.085s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.492s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.071s
ok  	github.com/carlosframework/platform/internal/metrics	0.006s
ok  	github.com/carlosframework/platform/internal/moves	20.921s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.748s
ok  	github.com/carlosframework/platform/internal/objstore	0.170s
ok  	github.com/carlosframework/platform/internal/ops	0.164s
ok  	github.com/carlosframework/platform/internal/park	0.202s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	12.200s
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.040s
ok  	github.com/carlosframework/platform/internal/relay	7.236s
ok  	github.com/carlosframework/platform/internal/release	19.833s
ok  	github.com/carlosframework/platform/internal/rollout	3.638s
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.004s
ok  	github.com/carlosframework/platform/internal/safefile	0.008s
ok  	github.com/carlosframework/platform/internal/schedule	2.266s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.051s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.547s
ok  	github.com/carlosframework/platform/internal/status	0.037s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.224s
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.005s
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.006s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.001s
ok  	github.com/carlosframework/platform/internal/vet	1.569s
=== 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: 45744290 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: cached, digest verified
  mc: cached, 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.022s
=== RUN   TestT4StorageProbeMinio
2026/10/14 23:20:41 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE88BBCC2BEBC8, 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.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.021s
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 0d0fd0ab5..1c7a2443b 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)
+ }
errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...)
errs = append(errs, checkRootReadDeclarations(got, inv)...)
if len(errs) > 0 {
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/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