Skip to content

Commit

Permalink
[management] REST client package (#3278)
Browse files Browse the repository at this point in the history
  • Loading branch information
mohamed-essam authored Feb 4, 2025
1 parent f930ef2 commit 7d385b8
Show file tree
Hide file tree
Showing 29 changed files with 4,215 additions and 0 deletions.
54 changes: 54 additions & 0 deletions management/client/rest/accounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package rest

import (
"bytes"
"context"
"encoding/json"

"github.com/netbirdio/netbird/management/server/http/api"
)

// AccountsAPI APIs for accounts, do not use directly
type AccountsAPI struct {
c *Client
}

// List list all accounts, only returns one account always
// See more: https://docs.netbird.io/api/resources/accounts#list-all-accounts
func (a *AccountsAPI) List(ctx context.Context) ([]api.Account, error) {
resp, err := a.c.newRequest(ctx, "GET", "/api/accounts", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
ret, err := parseResponse[[]api.Account](resp)
return ret, err
}

// Update update account settings
// See more: https://docs.netbird.io/api/resources/accounts#update-an-account
func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api.PutApiAccountsAccountIdJSONRequestBody) (*api.Account, error) {
requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
resp, err := a.c.newRequest(ctx, "PUT", "/api/accounts/"+accountID, bytes.NewReader(requestBytes))
if err != nil {
return nil, err
}
defer resp.Body.Close()
ret, err := parseResponse[api.Account](resp)
return &ret, err
}

// Delete delete account
// See more: https://docs.netbird.io/api/resources/accounts#delete-an-account
func (a *AccountsAPI) Delete(ctx context.Context, accountID string) error {
resp, err := a.c.newRequest(ctx, "DELETE", "/api/accounts/"+accountID, nil)
if err != nil {
return err
}
defer resp.Body.Close()

return nil
}
169 changes: 169 additions & 0 deletions management/client/rest/accounts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package rest

import (
"context"
"encoding/json"
"io"
"net/http"
"testing"

"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/http/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var (
testAccount = api.Account{
Id: "Test",
Settings: api.AccountSettings{
Extra: &api.AccountExtraSettings{
PeerApprovalEnabled: ptr(false),
},
GroupsPropagationEnabled: ptr(true),
JwtGroupsEnabled: ptr(false),
PeerInactivityExpiration: 7,
PeerInactivityExpirationEnabled: true,
PeerLoginExpiration: 24,
PeerLoginExpirationEnabled: true,
RegularUsersViewBlocked: false,
RoutingPeerDnsResolutionEnabled: ptr(false),
},
}
)

func TestAccounts_List_200(t *testing.T) {
withMockClient(func(c *Client, mux *http.ServeMux) {
mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal([]api.Account{testAccount})
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Accounts.List(context.Background())
require.NoError(t, err)
assert.Len(t, ret, 1)
assert.Equal(t, testAccount, ret[0])
})
}

func TestAccounts_List_Err(t *testing.T) {
withMockClient(func(c *Client, mux *http.ServeMux) {
mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Accounts.List(context.Background())
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Empty(t, ret)
})
}

func TestAccounts_Update_200(t *testing.T) {
withMockClient(func(c *Client, mux *http.ServeMux) {
mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "PUT", r.Method)
reqBytes, err := io.ReadAll(r.Body)
require.NoError(t, err)
var req api.PutApiAccountsAccountIdJSONRequestBody
err = json.Unmarshal(reqBytes, &req)
require.NoError(t, err)
assert.Equal(t, true, *req.Settings.RoutingPeerDnsResolutionEnabled)
retBytes, _ := json.Marshal(testAccount)
_, err = w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Accounts.Update(context.Background(), "Test", api.PutApiAccountsAccountIdJSONRequestBody{
Settings: api.AccountSettings{
RoutingPeerDnsResolutionEnabled: ptr(true),
},
})
require.NoError(t, err)
assert.Equal(t, testAccount, *ret)
})

}

func TestAccounts_Update_Err(t *testing.T) {
withMockClient(func(c *Client, mux *http.ServeMux) {
mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400})
w.WriteHeader(400)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
ret, err := c.Accounts.Update(context.Background(), "Test", api.PutApiAccountsAccountIdJSONRequestBody{
Settings: api.AccountSettings{
RoutingPeerDnsResolutionEnabled: ptr(true),
},
})
assert.Error(t, err)
assert.Equal(t, "No", err.Error())
assert.Nil(t, ret)
})
}

func TestAccounts_Delete_200(t *testing.T) {
withMockClient(func(c *Client, mux *http.ServeMux) {
mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "DELETE", r.Method)
w.WriteHeader(200)
})
err := c.Accounts.Delete(context.Background(), "Test")
require.NoError(t, err)
})
}

func TestAccounts_Delete_Err(t *testing.T) {
withMockClient(func(c *Client, mux *http.ServeMux) {
mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) {
retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404})
w.WriteHeader(404)
_, err := w.Write(retBytes)
require.NoError(t, err)
})
err := c.Accounts.Delete(context.Background(), "Test")
assert.Error(t, err)
assert.Equal(t, "Not found", err.Error())
})
}

func TestAccounts_Integration_List(t *testing.T) {
withBlackBoxServer(t, func(c *Client) {
accounts, err := c.Accounts.List(context.Background())
require.NoError(t, err)
assert.Len(t, accounts, 1)
assert.Equal(t, "bf1c8084-ba50-4ce7-9439-34653001fc3b", accounts[0].Id)
assert.Equal(t, false, *accounts[0].Settings.Extra.PeerApprovalEnabled)
})
}

func TestAccounts_Integration_Update(t *testing.T) {
withBlackBoxServer(t, func(c *Client) {
accounts, err := c.Accounts.List(context.Background())
require.NoError(t, err)
assert.Len(t, accounts, 1)
accounts[0].Settings.JwtAllowGroups = ptr([]string{"test"})
account, err := c.Accounts.Update(context.Background(), accounts[0].Id, api.AccountRequest{
Settings: accounts[0].Settings,
})
require.NoError(t, err)
assert.Equal(t, accounts[0].Id, account.Id)
assert.Equal(t, []string{"test"}, *account.Settings.JwtAllowGroups)
})
}

// Account deletion on MySQL and PostgreSQL databases causes unknown errors
// func TestAccounts_Integration_Delete(t *testing.T) {
// withBlackBoxServer(t, func(c *Client) {
// accounts, err := c.Accounts.List(context.Background())
// require.NoError(t, err)
// assert.Len(t, accounts, 1)
// err = c.Accounts.Delete(context.Background(), accounts[0].Id)
// require.NoError(t, err)
// _, err = c.Accounts.List(context.Background())
// assert.Error(t, err)
// })
// }
133 changes: 133 additions & 0 deletions management/client/rest/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package rest

import (
"context"
"encoding/json"
"errors"
"io"
"net/http"

"github.com/netbirdio/netbird/management/server/http/util"
)

// Client Management service HTTP REST API Client
type Client struct {
managementURL string
authHeader 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/routes
DNS *DNSAPI

// 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
}

// New initialize new Client instance
func New(managementURL, token string) *Client {
client := &Client{
managementURL: managementURL,
authHeader: "Token " + token,
}
client.Accounts = &AccountsAPI{client}
client.Users = &UsersAPI{client}
client.Tokens = &TokensAPI{client}
client.Peers = &PeersAPI{client}
client.SetupKeys = &SetupKeysAPI{client}
client.Groups = &GroupsAPI{client}
client.Policies = &PoliciesAPI{client}
client.PostureChecks = &PostureChecksAPI{client}
client.Networks = &NetworksAPI{client}
client.Routes = &RoutesAPI{client}
client.DNS = &DNSAPI{client}
client.GeoLocation = &GeoLocationAPI{client}
client.Events = &EventsAPI{client}
return client
}

func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*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")
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

if resp.StatusCode > 299 {
parsedErr, pErr := parseResponse[util.ErrorResponse](resp)
if pErr != nil {
return nil, err
}
return nil, errors.New(parsedErr.Message)
}

return resp, nil
}

func parseResponse[T any](resp *http.Response) (T, error) {
var ret T
if resp.Body == nil {
return ret, errors.New("No body")
}
bs, err := io.ReadAll(resp.Body)
if err != nil {
return ret, err
}
err = json.Unmarshal(bs, &ret)

return ret, err
}
30 changes: 30 additions & 0 deletions management/client/rest/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package rest

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/netbirdio/netbird/management/server/http/testing/testing_tools"
)

func withMockClient(callback func(*Client, *http.ServeMux)) {
mux := &http.ServeMux{}
server := httptest.NewServer(mux)
defer server.Close()
c := New(server.URL, "ABC")
callback(c, mux)
}

func ptr[T any, PT *T](x T) PT {
return &x
}

func withBlackBoxServer(t *testing.T, callback func(*Client)) {
t.Helper()
handler, _, _ := testing_tools.BuildApiBlackBoxWithDBState(t, "../../server/testdata/store.sql", nil, false)
server := httptest.NewServer(handler)
defer server.Close()
c := New(server.URL, "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp")
callback(c)
}
Loading

0 comments on commit 7d385b8

Please sign in to comment.