[management] Return agent-network settings defaults before bootstrap

Settings endpoints elsewhere in the API always answer with a JSON object:
account settings rows are created with the account and DNS settings live
on it with zero-value defaults. Agent-network settings deviated because
the row is created lazily at bootstrap (cluster is a deployment choice and
the subdomain must be generated uniquely per cluster), and GetSettings
passed the store's not-found straight through to the wire.

The manager now synthesises the defaults on read when no row exists yet,
without persisting: log collection on with the default retention, and an
empty cluster, subdomain and endpoint as the not-bootstrapped signal. The
timestamps stay off the wire until a row is written. Bootstrap persists
the same defaults, so the pre-bootstrap view and the freshly bootstrapped
row agree.
This commit is contained in:
mlsmaycon
2026-08-02 04:20:59 +00:00
parent d88bee4347
commit 30ca6a9809
8 changed files with 136 additions and 79 deletions
+10 -10
View File
@@ -77,9 +77,9 @@ func (a *AgentNetworkAPI) CreateProvider(ctx context.Context, request api.PostAp
return &ret, err
}
// UpdateProvider updates an Agent Network provider. Omitted optional fields
// (api_key, models, extra_values, toggles, identity headers) keep their
// stored values.
// UpdateProvider updates an Agent Network provider. The request replaces the
// provider's mutable state; only an omitted api_key keeps the stored key
// (secrets are never required to round-trip).
func (a *AgentNetworkAPI) UpdateProvider(ctx context.Context, providerID string, request api.PutApiAgentNetworkProvidersProviderIdJSONRequestBody) (*api.AgentNetworkProvider, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
@@ -330,13 +330,13 @@ func (a *AgentNetworkAPI) DeleteBudgetRule(ctx context.Context, ruleID string) e
}
// GetSettings gets the account's Agent Network gateway settings (cluster,
// subdomain, endpoint, collection toggles). Returns an APIError with
// StatusCode 404 (matchable via IsNotFound) when the account has not been
// bootstrapped yet — bootstrap via UpdateSettings with a cluster, or by
// creating the first provider with bootstrap_cluster set. Management servers
// prior to the 404 contract answered 200 with a JSON null body in that case;
// that legacy shape is translated to the same 404 APIError here so callers
// only ever branch on IsNotFound.
// subdomain, endpoint, collection toggles). An account that has not been
// bootstrapped yet — via UpdateSettings with a cluster, or by creating the
// first provider with bootstrap_cluster set — reads as the defaults with an
// empty Cluster, Subdomain and Endpoint. Management servers prior to that
// contract answered 200 with a JSON null body instead; that legacy shape is
// translated to an APIError matchable via IsNotFound rather than fabricating
// defaults the server never stated.
func (a *AgentNetworkAPI) GetSettings(ctx context.Context) (*api.AgentNetworkSettings, error) {
resp, err := a.c.NewRequest(ctx, "GET", "/api/agent-network/settings", nil, nil)
if err != nil {
@@ -404,24 +404,45 @@ func TestAgentNetwork_GetSettings_200(t *testing.T) {
})
}
func TestAgentNetwork_GetSettings_404(t *testing.T) {
// TestAgentNetwork_GetSettings_UnbootstrappedDefaults pins the settings-read
// contract: an unbootstrapped account answers 200 with the defaults and empty
// cluster/subdomain/endpoint, which the client passes through untouched.
func TestAgentNetwork_GetSettings_UnbootstrappedDefaults(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/agent-network/settings", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "agent network settings not found", Code: 404})
w.WriteHeader(404)
retBytes, _ := json.Marshal(api.AgentNetworkSettings{
EnableLogCollection: true,
AccessLogRetentionDays: ptr(30),
})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.AgentNetwork.GetSettings(context.Background())
require.NoError(t, err)
assert.Empty(t, ret.Endpoint, "empty endpoint is the not-bootstrapped signal")
assert.True(t, ret.EnableLogCollection, "defaults must pass through")
})
}
func TestAgentNetwork_GetSettings_Err(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/agent-network/settings", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "no", Code: 403})
w.WriteHeader(403)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
_, err := c.AgentNetwork.GetSettings(context.Background())
require.Error(t, err)
assert.True(t, rest.IsNotFound(err), "unbootstrapped settings must be matchable via IsNotFound")
assert.Equal(t, "no", err.Error())
})
}
// TestAgentNetwork_GetSettings_LegacyNullBody pins the compatibility shim for
// management servers that answered 200 with a JSON null body before the 404
// contract: the client must translate that shape into the same IsNotFound
// error instead of returning a bogus zero-valued settings object.
// management servers that answered 200 with a JSON null body before the
// defaults contract: the client translates that shape into an IsNotFound
// error instead of returning a bogus zero-valued settings object or
// fabricating defaults the server never stated.
func TestAgentNetwork_GetSettings_LegacyNullBody(t *testing.T) {
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
mux.HandleFunc("/api/agent-network/settings", func(w http.ResponseWriter, r *http.Request) {