mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-30 11:31:29 +02:00
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.
300 lines
8.4 KiB
Go
300 lines
8.4 KiB
Go
package rest
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/netbirdio/netbird/shared/management/http/util"
|
|
)
|
|
|
|
// APIError represents an error response from the management API.
|
|
type APIError struct {
|
|
StatusCode int
|
|
Message string
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e *APIError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// IsNotFound returns true if the error represents a 404 Not Found response.
|
|
func IsNotFound(err error) bool {
|
|
var apiErr *APIError
|
|
if ok := errors.As(err, &apiErr); ok {
|
|
return apiErr.StatusCode == http.StatusNotFound
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsPreconditionFailed returns true if the error represents a 412 Precondition
|
|
// Failed response — an If-Match the server refused, or an endpoint's own
|
|
// precondition. A caller that sent a conditional request can use this to tell
|
|
// "someone else changed it, read again and retry" apart from a real failure;
|
|
// the message distinguishes it from an endpoint's other 412s.
|
|
func IsPreconditionFailed(err error) bool {
|
|
var apiErr *APIError
|
|
if ok := errors.As(err, &apiErr); ok {
|
|
return apiErr.StatusCode == http.StatusPreconditionFailed
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Client Management service HTTP REST API Client
|
|
type Client struct {
|
|
managementURL string
|
|
authHeader string
|
|
httpClient HttpClient
|
|
userAgent string
|
|
|
|
// Accounts NetBird account APIs
|
|
// see more: https://docs.netbird.io/api/resources/accounts
|
|
Accounts *AccountsAPI
|
|
|
|
// Users NetBird users APIs
|
|
// see more: https://docs.netbird.io/api/resources/users
|
|
Users *UsersAPI
|
|
|
|
// Tokens NetBird tokens APIs
|
|
// see more: https://docs.netbird.io/api/resources/tokens
|
|
Tokens *TokensAPI
|
|
|
|
// Peers NetBird peers APIs
|
|
// see more: https://docs.netbird.io/api/resources/peers
|
|
Peers *PeersAPI
|
|
|
|
// SetupKeys NetBird setup keys APIs
|
|
// see more: https://docs.netbird.io/api/resources/setup-keys
|
|
SetupKeys *SetupKeysAPI
|
|
|
|
// Groups NetBird groups APIs
|
|
// see more: https://docs.netbird.io/api/resources/groups
|
|
Groups *GroupsAPI
|
|
|
|
// Policies NetBird policies APIs
|
|
// see more: https://docs.netbird.io/api/resources/policies
|
|
Policies *PoliciesAPI
|
|
|
|
// PostureChecks NetBird posture checks APIs
|
|
// see more: https://docs.netbird.io/api/resources/posture-checks
|
|
PostureChecks *PostureChecksAPI
|
|
|
|
// Networks NetBird networks APIs
|
|
// see more: https://docs.netbird.io/api/resources/networks
|
|
Networks *NetworksAPI
|
|
|
|
// Routes NetBird routes APIs
|
|
// see more: https://docs.netbird.io/api/resources/routes
|
|
Routes *RoutesAPI
|
|
|
|
// DNS NetBird DNS APIs
|
|
// see more: https://docs.netbird.io/api/resources/dns
|
|
DNS *DNSAPI
|
|
|
|
// DNSZones NetBird DNS Zones APIs
|
|
// see more: https://docs.netbird.io/api/resources/dns-zones
|
|
DNSZones *DNSZonesAPI
|
|
|
|
// GeoLocation NetBird Geo Location APIs
|
|
// see more: https://docs.netbird.io/api/resources/geo-locations
|
|
GeoLocation *GeoLocationAPI
|
|
|
|
// Events NetBird Events APIs
|
|
// see more: https://docs.netbird.io/api/resources/events
|
|
Events *EventsAPI
|
|
|
|
// Billing NetBird Billing APIs for subscriptions, plans, and invoices
|
|
// see more: https://docs.netbird.io/api/resources/billing
|
|
Billing *BillingAPI
|
|
|
|
// MSP NetBird MSP tenant management APIs
|
|
// see more: https://docs.netbird.io/api/resources/msp
|
|
MSP *MSPAPI
|
|
|
|
// EDR NetBird EDR integration APIs (Intune, SentinelOne, Falcon, Huntress)
|
|
// see more: https://docs.netbird.io/api/resources/edr
|
|
EDR *EDRAPI
|
|
|
|
// SCIM NetBird SCIM IDP integration APIs
|
|
// see more: https://docs.netbird.io/api/resources/scim
|
|
SCIM *SCIMAPI
|
|
|
|
// GoogleIDP NetBird Google Workspace IDP integration APIs
|
|
GoogleIDP *GoogleIDPAPI
|
|
|
|
// AzureIDP NetBird Azure AD IDP integration APIs
|
|
AzureIDP *AzureIDPAPI
|
|
|
|
// OktaScimIDP NetBird Okta SCIM IDP integration APIs
|
|
OktaScimIDP *OktaScimIDPAPI
|
|
|
|
// EventStreaming NetBird Event Streaming integration APIs
|
|
// see more: https://docs.netbird.io/api/resources/event-streaming
|
|
EventStreaming *EventStreamingAPI
|
|
|
|
// IdentityProviders NetBird Identity Providers APIs
|
|
// see more: https://docs.netbird.io/api/resources/identity-providers
|
|
IdentityProviders *IdentityProvidersAPI
|
|
|
|
// Ingress NetBird Ingress Peers APIs
|
|
// see more: https://docs.netbird.io/api/resources/ingress-ports
|
|
Ingress *IngressAPI
|
|
|
|
// Instance NetBird Instance API
|
|
// see more: https://docs.netbird.io/api/resources/instance
|
|
Instance *InstanceAPI
|
|
|
|
// ReverseProxyServices NetBird reverse proxy services APIs
|
|
ReverseProxyServices *ReverseProxyServicesAPI
|
|
|
|
// ReverseProxyClusters NetBird reverse proxy clusters APIs
|
|
ReverseProxyClusters *ReverseProxyClustersAPI
|
|
|
|
// ReverseProxyDomains NetBird reverse proxy domains APIs
|
|
ReverseProxyDomains *ReverseProxyDomainsAPI
|
|
|
|
// ReverseProxyTokens account-scoped proxy access tokens used to register
|
|
// self-hosted (bring-your-own-proxy) `netbird proxy` instances.
|
|
ReverseProxyTokens *ReverseProxyTokensAPI
|
|
|
|
// AgentNetwork NetBird Agent Network (AI/LLM gateway) APIs: catalog,
|
|
// providers, policies, guardrails, budget rules and account settings.
|
|
AgentNetwork *AgentNetworkAPI
|
|
}
|
|
|
|
// New initialize new Client instance using PAT token
|
|
func New(managementURL, token string) *Client {
|
|
return NewWithOptions(
|
|
WithManagementURL(managementURL),
|
|
WithPAT(token),
|
|
)
|
|
}
|
|
|
|
// NewWithBearerToken initialize new Client instance using Bearer token type
|
|
func NewWithBearerToken(managementURL, token string) *Client {
|
|
return NewWithOptions(
|
|
WithManagementURL(managementURL),
|
|
WithBearerToken(token),
|
|
)
|
|
}
|
|
|
|
// NewWithOptions initialize new Client instance with options
|
|
func NewWithOptions(opts ...option) *Client {
|
|
client := &Client{
|
|
httpClient: http.DefaultClient,
|
|
}
|
|
|
|
for _, option := range opts {
|
|
option(client)
|
|
}
|
|
|
|
client.initialize()
|
|
return client
|
|
}
|
|
|
|
func (c *Client) initialize() {
|
|
c.Accounts = &AccountsAPI{c}
|
|
c.Users = &UsersAPI{c}
|
|
c.Tokens = &TokensAPI{c}
|
|
c.Peers = &PeersAPI{c}
|
|
c.SetupKeys = &SetupKeysAPI{c}
|
|
c.Groups = &GroupsAPI{c}
|
|
c.Policies = &PoliciesAPI{c}
|
|
c.PostureChecks = &PostureChecksAPI{c}
|
|
c.Networks = &NetworksAPI{c}
|
|
c.Routes = &RoutesAPI{c}
|
|
c.DNS = &DNSAPI{c}
|
|
c.DNSZones = &DNSZonesAPI{c}
|
|
c.GeoLocation = &GeoLocationAPI{c}
|
|
c.Events = &EventsAPI{c}
|
|
c.Billing = &BillingAPI{c}
|
|
c.MSP = &MSPAPI{c}
|
|
c.EDR = &EDRAPI{c}
|
|
c.SCIM = &SCIMAPI{c}
|
|
c.GoogleIDP = &GoogleIDPAPI{c}
|
|
c.AzureIDP = &AzureIDPAPI{c}
|
|
c.OktaScimIDP = &OktaScimIDPAPI{c}
|
|
c.EventStreaming = &EventStreamingAPI{c}
|
|
c.IdentityProviders = &IdentityProvidersAPI{c}
|
|
c.Ingress = &IngressAPI{c}
|
|
c.Instance = &InstanceAPI{c}
|
|
c.ReverseProxyServices = &ReverseProxyServicesAPI{c}
|
|
c.ReverseProxyClusters = &ReverseProxyClustersAPI{c}
|
|
c.ReverseProxyDomains = &ReverseProxyDomainsAPI{c}
|
|
c.ReverseProxyTokens = &ReverseProxyTokensAPI{c}
|
|
c.AgentNetwork = &AgentNetworkAPI{c}
|
|
}
|
|
|
|
// NewRequest creates and executes new management API request
|
|
func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader, query map[string]string) (*http.Response, error) {
|
|
return c.newRequest(ctx, method, path, body, query, nil)
|
|
}
|
|
|
|
// newRequest is NewRequest with request headers, for the endpoints whose
|
|
// contract includes one — conditional requests carrying If-Match.
|
|
func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader, query, headers map[string]string) (*http.Response, error) {
|
|
req, err := http.NewRequestWithContext(ctx, method, c.managementURL+path, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Add("Authorization", c.authHeader)
|
|
req.Header.Add("Accept", "application/json")
|
|
if body != nil {
|
|
req.Header.Add("Content-Type", "application/json")
|
|
}
|
|
if c.userAgent != "" {
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
}
|
|
for name, value := range headers {
|
|
req.Header.Set(name, value)
|
|
}
|
|
|
|
if len(query) != 0 {
|
|
q := req.URL.Query()
|
|
for k, v := range query {
|
|
q.Add(k, v)
|
|
}
|
|
req.URL.RawQuery = q.Encode()
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode > 299 {
|
|
parsedErr, pErr := parseResponse[util.ErrorResponse](resp)
|
|
if pErr != nil {
|
|
return nil, pErr
|
|
}
|
|
return nil, &APIError{
|
|
StatusCode: resp.StatusCode,
|
|
Message: parsedErr.Message,
|
|
}
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func parseResponse[T any](resp *http.Response) (T, error) {
|
|
var ret T
|
|
if resp.Body == nil {
|
|
return ret, fmt.Errorf("body missing, HTTP Error code %d", resp.StatusCode)
|
|
}
|
|
bs, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
err = json.Unmarshal(bs, &ret)
|
|
if err != nil {
|
|
return ret, fmt.Errorf("error code %d, error unmarshalling body: %w", resp.StatusCode, err)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|