carlos / platform Public

region HA 2b: the SSM adapter holds method values, not the client (#275 size ceiling)

ssmParams passed its *ssm.Client to NewGetParametersByPathPaginator, whose
argument is an interface. With html/template linked in, that conversion
makes the linker keep every exported SSM method, and the linux/arm64
binary went over 20-cross@5's ceiling. ssmParams now holds method values
bound to GetParametersByPath (through a func type the paginator takes)
and PutParameter, as main's #275 fix does for the other SDK clients.
Same SDK calls; tests unchanged.

linux/arm64 (-ldflags "-s -w", as 20-cross@5 builds it): 54,067,362 ->
49,217,698 bytes, ceiling 52,000,000. Main at 04819f9b: 48,693,410.

The census overlay's internal/hostenv @coverage row is re-pinned, with a
comment line saying why; no root-read unit changes and the seal stands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 3d2498474816195b1cef619cb83a13157907500c parent da072ed
2 files changed, +27 −7
  • internal/hostenv/awsssm.go +25 −6
  • internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv +2 −1
diff --git a/internal/hostenv/awsssm.go b/internal/hostenv/awsssm.go
index edc0892a2..9f190fabf 100644
--- a/internal/hostenv/awsssm.go
+++ b/internal/hostenv/awsssm.go
@@ -17,9 +17,23 @@ import (
// kms:Decrypt; an operator seeding them needs ssm:PutParameter and
// kms:Encrypt on that key.
+// ssmParams holds method values bound to the two SSM calls it makes, not
+// the *ssm.Client. Handing the client to the paginator (whose argument is
+// an interface) would make the linker keep every exported SSM method once
+// html/template is linked in: about 5 MB of linux/arm64 binary (discussion
+// #275, .amadan/ci.d/20-cross@5). A method value keeps the one call alone.
type ssmParams struct {
- client *ssm.Client
- kmsKey string
+ getByPath getParametersByPathFunc
+ put func(context.Context, *ssm.PutParameterInput, ...func(*ssm.Options)) (*ssm.PutParameterOutput, error)
+ kmsKey string
+}
+
+// getParametersByPathFunc is ssm.GetParametersByPathAPIClient over one
+// bound method value, for the paginator.
+type getParametersByPathFunc func(context.Context, *ssm.GetParametersByPathInput, ...func(*ssm.Options)) (*ssm.GetParametersByPathOutput, error)
+
+func (f getParametersByPathFunc) GetParametersByPath(ctx context.Context, in *ssm.GetParametersByPathInput, opts ...func(*ssm.Options)) (*ssm.GetParametersByPathOutput, error) {
+ return f(ctx, in, opts...)
}
// openSSMParams opens SSM in awsRegion ("" is the SDK's default region).
@@ -33,14 +47,19 @@ func openSSMParams(ctx context.Context, awsRegion, kmsKey string) (Params, error
if err != nil {
return nil, err
}
- return &ssmParams{client: ssm.NewFromConfig(cfg), kmsKey: kmsKey}, nil
+ client := ssm.NewFromConfig(cfg)
+ return &ssmParams{
+ getByPath: getParametersByPathFunc(client.GetParametersByPath),
+ put: client.PutParameter,
+ kmsKey: kmsKey,
+ }, nil
}
// ByPath reads every parameter one level under prefix, decrypted, page by
// page. Any failed page fails the whole read.
func (p *ssmParams) ByPath(ctx context.Context, prefix string) (map[string]string, error) {
out := map[string]string{}
- pages := ssm.NewGetParametersByPathPaginator(p.client, &ssm.GetParametersByPathInput{
+ pages := ssm.NewGetParametersByPathPaginator(p.getByPath, &ssm.GetParametersByPathInput{
Path: aws.String(strings.TrimSuffix(prefix, "/")),
Recursive: aws.Bool(false),
WithDecryption: aws.Bool(true),
@@ -73,7 +92,7 @@ func (p *ssmParams) Set(ctx context.Context, name, value string) error {
if p.kmsKey != "" {
in.KeyId = aws.String(p.kmsKey)
}
- _, err := p.client.PutParameter(ctx, in)
+ _, err := p.put(ctx, in)
return err
}
@@ -89,7 +108,7 @@ func (p *ssmParams) SetNew(ctx context.Context, name, value string) error {
if p.kmsKey != "" {
in.KeyId = aws.String(p.kmsKey)
}
- _, err := p.client.PutParameter(ctx, in)
+ _, err := p.put(ctx, in)
var exists *ssmtypes.ParameterAlreadyExists
if errors.As(err, &exists) {
return ErrExists
diff --git a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv
index 20ac219b2..b05293fcf 100644
--- a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv
+++ b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv
@@ -8,8 +8,9 @@
# Task 8: joining mode and the gate (internal/host joining.go; RunAgent calls hostingGate before runAgent; ClaimWiring.startRegional sets the health answer's hosting condition from the gate's decision and clears it when the worker ends). runAgent, authority B,C,D,F,J4, is not changed. No root-read unit changed or was added. Envelope-only, and the seal stands. Before the gate, RunAgent has a box carrying the group marker resolve its group reference (agentEntry, through Task 6's resolveGroupBoxReference, with backoff), whatever the switches say, so runAgent's loops never keep an empty label; joining touches the join marker again once a minute while the box stays invited, so a failed carlos system join is retried.
# Task 9 (system.go): the install-channel, join and host-env verbs join systemVerb's list and runSystem's dispatch, and the usage gains their lines. The two seed-overlay authority declarations are re-digested, class, task, basis and (empty) kinds unchanged: no root read is added. Merged onto main 26065378, whose activation-tools overlay (census-ruling-activation-tools) re-ruled both declarations for the claims verb: the two rows carry that overlay's class, basis (B,C,D,F,G,J4) and text, re-digested over the merged code with both sets of verbs.
# CI fix (internal/status fleet.go listBoxKeys): a store whose ListMod fails is listed whole, as one that is not a ModLister already was, so the claim's boundary stores (whose ListMod refuses when the store they wrap has none) no longer fail the move door's edges read. Only the internal/status coverage row moves; no root-read unit is added or changed, and the seal does not move.
+# Size fix (internal/hostenv awsssm.go): ssmParams holds method values bound to GetParametersByPath and PutParameter, not the *ssm.Client, so html/template's method retention no longer keeps every SSM method (#275; linux/arm64 54,067,362 to 49,217,698 bytes, ceiling 52,000,000). Same SDK calls. Only the internal/hostenv coverage row moves; no root-read unit is added or changed, and the seal does not move.
@coverage . 9949422c526be724b50a4614fc79623b9823a1aa0960d1472fd9050f8000c3b0
-@coverage internal/hostenv 6b65e7987026a4c81f410c0afcfaca13bc2b252d7382c45564f670e36a045bcb
+@coverage internal/hostenv 8a7d7a7673ad1078b5888a96cb266f239f1af1ed917dcd8f45af9eff7e0a7497
internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899
internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c
internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1