Files
netbird/management/internals/modules/agentnetwork/handlers/settings_handler.go
Claude bf38f20198 [management] Fix agent-network settings and provider update contracts
Declarative API clients (the Terraform provider, scripted PUTs) hit three
places where the agent-network API broke its own contract or left users
stuck, forcing client-side workarounds.

The settings GET answered 200 with a JSON null body for unbootstrapped
accounts while the OpenAPI spec documented 404; it now returns the 404.
The settings PUT still replaces every mutable field like the other PUT
endpoints, but a request may now carry the cluster: on an account without
a settings row it bootstraps one (assigning the subdomain), so settings
can be managed before the first provider exists; on a bootstrapped account
a differing cluster is rejected instead of silently ignored.

The provider PUT handler built a fresh row from the request, so the fields
the schema documents as omit-preserves (extra_values, metadata_disabled,
enabled, skip_tls_verification, identity headers) silently reset to zero
when omitted. The handler now overlays the request onto the stored row,
making the documented nil-gating real. Identity headers are always present
in provider responses so an explicitly cleared header round-trips as an
empty string rather than vanishing.
2026-08-02 03:29:50 +00:00

69 lines
2.4 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/shared/management/http/api"
"github.com/netbirdio/netbird/shared/management/http/util"
)
// addSettingsEndpoints registers the Agent Network settings routes. The
// settings row is bootstrapped server-side on first provider create or on the
// first PUT carrying a cluster; GET reads it and PUT applies a partial update
// of the mutable collection toggles (cluster/subdomain stay immutable).
func (h *handler) addSettingsEndpoints(router *mux.Router) {
router.HandleFunc("/agent-network/settings", h.getSettings).Methods("GET", "OPTIONS")
router.HandleFunc("/agent-network/settings", h.updateSettings).Methods("PUT", "OPTIONS")
}
// updateSettings replaces the mutable settings fields on the account's row.
// A request carrying a cluster bootstraps the row when the account doesn't
// have one yet.
func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
var req api.AgentNetworkSettingsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
return
}
settings := &types.Settings{AccountID: userAuth.AccountId}
settings.FromAPIRequest(&req)
updated, err := h.manager.UpdateSettings(r.Context(), userAuth.UserId, settings)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
util.WriteJSONObject(r.Context(), w, updated.ToAPIResponse())
}
// getSettings returns the account's agent-network settings. Freshly-onboarded
// accounts have no settings row until a provider create (or a settings PUT
// with a cluster) bootstraps one; per the OpenAPI contract that reads as a
// plain 404.
func (h *handler) getSettings(w http.ResponseWriter, r *http.Request) {
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
settings, err := h.manager.GetSettings(r.Context(), userAuth.AccountId, userAuth.UserId)
if err != nil {
util.WriteError(r.Context(), err, w)
return
}
util.WriteJSONObject(r.Context(), w, settings.ToAPIResponse())
}