[management] Surface the settings ETag through the REST client

The Terraform provider consumes this client, and it is the client that has the
read-modify-write problem conditional requests solve, so the server-side work
is inert until the validator reaches it.

The new methods are additive and the existing four delegate to them unchanged.
A breaking signature change would buy a tidier surface at the cost of every
current caller, with no deprecation window, and the plain methods remain the
right default for callers that do not care.

CreateSettingsWithETag exists because the bootstrap emits a validator too, and
discarding it would force a client to read again before its first conditional
write — the round trip the header is there to avoid.

IsPreconditionFailed joins IsNotFound because a refused precondition is
otherwise an opaque APIError. Telling "someone else changed this, read again
and retry" apart from a genuine failure is the decision a conditional client
has to make, and it should not have to compare status codes by hand.

Validators arrive unquoted and go back out quoted, so no caller handles the
wire syntax. The e2e coverage drives all of it through the typed client
against a real server, playing out the case that motivates the feature: a
client plans an update, an operator turns PII redaction on in between, and the
client's write is refused instead of silently turning it back off.
This commit is contained in:
Brad Ison
2026-08-11 11:44:15 +02:00
parent 30c2010c09
commit 5085a2f96b
5 changed files with 339 additions and 13 deletions
+31
View File
@@ -153,6 +153,37 @@ func (c *Combined) DeleteSettings(ctx context.Context) error {
return anDelete(ctx, c, "/api/agent-network/settings")
}
// The conditional-request wrappers go through the typed REST client rather
// than anRequest, so the e2e run exercises the client's own header handling —
// the quoting on the way out and the unquoting on the way back — against a
// real server, which is the path the Terraform provider takes.
// GetSettingsWithETag reads the settings along with the entity-tag that makes
// a following write conditional.
func (c *Combined) GetSettingsWithETag(ctx context.Context) (api.AgentNetworkSettings, string, error) {
settings, etag, err := c.api.AgentNetwork.GetSettingsWithETag(ctx)
if err != nil {
return api.AgentNetworkSettings{}, "", err
}
return *settings, etag, nil
}
// UpdateSettingsIfMatch applies the update only if etag is still current,
// returning the entity-tag of the row it wrote.
func (c *Combined) UpdateSettingsIfMatch(ctx context.Context, req api.AgentNetworkSettingsRequest, etag string) (api.AgentNetworkSettings, string, error) {
settings, newETag, err := c.api.AgentNetwork.UpdateSettingsIfMatch(ctx, req, etag)
if err != nil {
return api.AgentNetworkSettings{}, "", err
}
return *settings, newETag, nil
}
// DeleteSettingsIfMatch deletes the settings row only if etag is still
// current.
func (c *Combined) DeleteSettingsIfMatch(ctx context.Context, etag string) error {
return c.api.AgentNetwork.DeleteSettingsIfMatch(ctx, etag)
}
// ListConsumption returns the account's consumption rows (possibly empty).
func (c *Combined) ListConsumption(ctx context.Context) ([]api.AgentNetworkConsumption, error) {
return anRequest[[]api.AgentNetworkConsumption](ctx, c, http.MethodGet, "/api/agent-network/consumption", nil)