mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-04 19:55:09 -04:00
## Describe your changes Work on the Terraform provider (terraform-provider-netbird #177–#183) surfaced places where the agent-network API broke its own contracts or deviated from the conventions the rest of the management API follows, forcing client-side workarounds. Settings reads now follow the settings-endpoint convention: GET always answers with a JSON object. Before bootstrap it returns the defaults with an empty cluster/subdomain/endpoint (previously 200 with a JSON `null` body, while the spec said 404). The settings PUT can bootstrap the account by carrying a `cluster` — previously the row could only come into existence through the first provider create, and a settings-first setup was impossible; a differing cluster on a bootstrapped account is rejected instead of silently ignored. PUT remains full-state. The provider PUT schema promised omit-preserves semantics for several operator-editable fields that the handler never delivered (it builds the row from the request, like every other update handler). The schema wording now matches the shipped full-state behavior; only the api_key (secret) and session keys stay preserved by the manager. Identity headers are always present in provider responses so an explicitly cleared value round-trips as an empty string. The Go REST client gains the full agent-network surface (catalog, providers, policies, guardrails, budget rules, settings), including a shim translating the legacy 200+`null` settings body from older servers into an `IsNotFound` error. Note for reviewers: the dashboard special-cased the `null` settings body; it needs a small follow-up for the new defaults response (in progress).
382 lines
13 KiB
Go
382 lines
13 KiB
Go
package rest
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
|
)
|
|
|
|
// AgentNetworkAPI APIs for the Agent Network (AI/LLM gateway), do not use directly
|
|
// see more: https://docs.netbird.io/api/resources/agent-network
|
|
type AgentNetworkAPI struct {
|
|
c *Client
|
|
}
|
|
|
|
// ListCatalogProviders lists the catalog of supported upstream AI providers
|
|
// (openai_api, anthropic_api, bedrock_api, ...) with their default models and
|
|
// pricing, used to prefill provider create forms.
|
|
func (a *AgentNetworkAPI) ListCatalogProviders(ctx context.Context) ([]api.AgentNetworkCatalogProvider, error) {
|
|
resp, err := a.c.NewRequest(ctx, "GET", "/api/agent-network/catalog/providers", nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[[]api.AgentNetworkCatalogProvider](resp)
|
|
return ret, err
|
|
}
|
|
|
|
// ListProviders lists all Agent Network providers
|
|
func (a *AgentNetworkAPI) ListProviders(ctx context.Context) ([]api.AgentNetworkProvider, error) {
|
|
resp, err := a.c.NewRequest(ctx, "GET", "/api/agent-network/providers", nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[[]api.AgentNetworkProvider](resp)
|
|
return ret, err
|
|
}
|
|
|
|
// GetProvider gets Agent Network provider info
|
|
func (a *AgentNetworkAPI) GetProvider(ctx context.Context, providerID string) (*api.AgentNetworkProvider, error) {
|
|
resp, err := a.c.NewRequest(ctx, "GET", "/api/agent-network/providers/"+providerID, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkProvider](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// CreateProvider creates a new Agent Network provider. Set
|
|
// request.BootstrapCluster on the account's first provider to bootstrap the
|
|
// per-account gateway endpoint (alternatively bootstrap via UpdateSettings
|
|
// with a cluster).
|
|
func (a *AgentNetworkAPI) CreateProvider(ctx context.Context, request api.PostApiAgentNetworkProvidersJSONRequestBody) (*api.AgentNetworkProvider, error) {
|
|
requestBytes, err := json.Marshal(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := a.c.NewRequest(ctx, "POST", "/api/agent-network/providers", bytes.NewReader(requestBytes), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkProvider](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// 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 {
|
|
return nil, err
|
|
}
|
|
resp, err := a.c.NewRequest(ctx, "PUT", "/api/agent-network/providers/"+providerID, bytes.NewReader(requestBytes), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkProvider](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// DeleteProvider deletes an Agent Network provider. Fails while any policy
|
|
// still references the provider — detach it first.
|
|
func (a *AgentNetworkAPI) DeleteProvider(ctx context.Context, providerID string) error {
|
|
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/agent-network/providers/"+providerID, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListPolicies lists all Agent Network policies
|
|
func (a *AgentNetworkAPI) ListPolicies(ctx context.Context) ([]api.AgentNetworkPolicy, error) {
|
|
resp, err := a.c.NewRequest(ctx, "GET", "/api/agent-network/policies", nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[[]api.AgentNetworkPolicy](resp)
|
|
return ret, err
|
|
}
|
|
|
|
// GetPolicy gets Agent Network policy info
|
|
func (a *AgentNetworkAPI) GetPolicy(ctx context.Context, policyID string) (*api.AgentNetworkPolicy, error) {
|
|
resp, err := a.c.NewRequest(ctx, "GET", "/api/agent-network/policies/"+policyID, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkPolicy](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// CreatePolicy creates a new Agent Network policy
|
|
func (a *AgentNetworkAPI) CreatePolicy(ctx context.Context, request api.PostApiAgentNetworkPoliciesJSONRequestBody) (*api.AgentNetworkPolicy, error) {
|
|
requestBytes, err := json.Marshal(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := a.c.NewRequest(ctx, "POST", "/api/agent-network/policies", bytes.NewReader(requestBytes), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkPolicy](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// UpdatePolicy updates an Agent Network policy
|
|
func (a *AgentNetworkAPI) UpdatePolicy(ctx context.Context, policyID string, request api.PutApiAgentNetworkPoliciesPolicyIdJSONRequestBody) (*api.AgentNetworkPolicy, error) {
|
|
requestBytes, err := json.Marshal(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := a.c.NewRequest(ctx, "PUT", "/api/agent-network/policies/"+policyID, bytes.NewReader(requestBytes), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkPolicy](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// DeletePolicy deletes an Agent Network policy
|
|
func (a *AgentNetworkAPI) DeletePolicy(ctx context.Context, policyID string) error {
|
|
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/agent-network/policies/"+policyID, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListGuardrails lists all Agent Network guardrails
|
|
func (a *AgentNetworkAPI) ListGuardrails(ctx context.Context) ([]api.AgentNetworkGuardrail, error) {
|
|
resp, err := a.c.NewRequest(ctx, "GET", "/api/agent-network/guardrails", nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[[]api.AgentNetworkGuardrail](resp)
|
|
return ret, err
|
|
}
|
|
|
|
// GetGuardrail gets Agent Network guardrail info
|
|
func (a *AgentNetworkAPI) GetGuardrail(ctx context.Context, guardrailID string) (*api.AgentNetworkGuardrail, error) {
|
|
resp, err := a.c.NewRequest(ctx, "GET", "/api/agent-network/guardrails/"+guardrailID, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkGuardrail](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// CreateGuardrail creates a new Agent Network guardrail
|
|
func (a *AgentNetworkAPI) CreateGuardrail(ctx context.Context, request api.PostApiAgentNetworkGuardrailsJSONRequestBody) (*api.AgentNetworkGuardrail, error) {
|
|
requestBytes, err := json.Marshal(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := a.c.NewRequest(ctx, "POST", "/api/agent-network/guardrails", bytes.NewReader(requestBytes), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkGuardrail](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// UpdateGuardrail updates an Agent Network guardrail
|
|
func (a *AgentNetworkAPI) UpdateGuardrail(ctx context.Context, guardrailID string, request api.PutApiAgentNetworkGuardrailsGuardrailIdJSONRequestBody) (*api.AgentNetworkGuardrail, error) {
|
|
requestBytes, err := json.Marshal(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := a.c.NewRequest(ctx, "PUT", "/api/agent-network/guardrails/"+guardrailID, bytes.NewReader(requestBytes), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkGuardrail](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// DeleteGuardrail deletes an Agent Network guardrail
|
|
func (a *AgentNetworkAPI) DeleteGuardrail(ctx context.Context, guardrailID string) error {
|
|
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/agent-network/guardrails/"+guardrailID, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListBudgetRules lists all account-level Agent Network budget rules
|
|
func (a *AgentNetworkAPI) ListBudgetRules(ctx context.Context) ([]api.AgentNetworkBudgetRule, error) {
|
|
resp, err := a.c.NewRequest(ctx, "GET", "/api/agent-network/budget-rules", nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[[]api.AgentNetworkBudgetRule](resp)
|
|
return ret, err
|
|
}
|
|
|
|
// GetBudgetRule gets Agent Network budget rule info
|
|
func (a *AgentNetworkAPI) GetBudgetRule(ctx context.Context, ruleID string) (*api.AgentNetworkBudgetRule, error) {
|
|
resp, err := a.c.NewRequest(ctx, "GET", "/api/agent-network/budget-rules/"+ruleID, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkBudgetRule](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// CreateBudgetRule creates a new Agent Network budget rule
|
|
func (a *AgentNetworkAPI) CreateBudgetRule(ctx context.Context, request api.PostApiAgentNetworkBudgetRulesJSONRequestBody) (*api.AgentNetworkBudgetRule, error) {
|
|
requestBytes, err := json.Marshal(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := a.c.NewRequest(ctx, "POST", "/api/agent-network/budget-rules", bytes.NewReader(requestBytes), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkBudgetRule](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// UpdateBudgetRule updates an Agent Network budget rule
|
|
func (a *AgentNetworkAPI) UpdateBudgetRule(ctx context.Context, ruleID string, request api.PutApiAgentNetworkBudgetRulesRuleIdJSONRequestBody) (*api.AgentNetworkBudgetRule, error) {
|
|
requestBytes, err := json.Marshal(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := a.c.NewRequest(ctx, "PUT", "/api/agent-network/budget-rules/"+ruleID, bytes.NewReader(requestBytes), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkBudgetRule](resp)
|
|
return &ret, err
|
|
}
|
|
|
|
// DeleteBudgetRule deletes an Agent Network budget rule
|
|
func (a *AgentNetworkAPI) DeleteBudgetRule(ctx context.Context, ruleID string) error {
|
|
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/agent-network/budget-rules/"+ruleID, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetSettings gets the account's Agent Network gateway settings (cluster,
|
|
// 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 {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if trimmed := bytes.TrimSpace(body); len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
|
|
return nil, &APIError{StatusCode: http.StatusNotFound, Message: "agent network settings not found"}
|
|
}
|
|
var ret api.AgentNetworkSettings
|
|
if err := json.Unmarshal(body, &ret); err != nil {
|
|
return nil, err
|
|
}
|
|
return &ret, nil
|
|
}
|
|
|
|
// UpdateSettings updates the account's Agent Network settings; the request
|
|
// replaces every mutable field (collection toggles and retention). Setting
|
|
// request.Cluster bootstraps the settings row when the account does not have
|
|
// one yet; on a bootstrapped account it must match the assigned cluster (or
|
|
// be nil) and any other value is rejected — the cluster is immutable.
|
|
func (a *AgentNetworkAPI) UpdateSettings(ctx context.Context, request api.PutApiAgentNetworkSettingsJSONRequestBody) (*api.AgentNetworkSettings, error) {
|
|
requestBytes, err := json.Marshal(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := a.c.NewRequest(ctx, "PUT", "/api/agent-network/settings", bytes.NewReader(requestBytes), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Body != nil {
|
|
defer resp.Body.Close()
|
|
}
|
|
ret, err := parseResponse[api.AgentNetworkSettings](resp)
|
|
return &ret, err
|
|
}
|