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>
2 files changed,
+27
−7
internal/hostenv/awsssm.go+25 −6internal/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 |