Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cns/NetworkContainerContract.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const (
PathDebugIPAddresses = "/debug/ipaddresses"
PathDebugPodContext = "/debug/podcontext"
PathDebugRestData = "/debug/restdata"
NumberOfCPUCores = NumberOfCPUCoresPath
NMAgentSupportedAPIs = NmAgentSupportedApisPath
)

// NetworkContainer Prefixes
Expand Down
101 changes: 96 additions & 5 deletions cns/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ var clientPaths = []string{
cns.PublishNetworkContainer,
cns.CreateOrUpdateNetworkContainer,
cns.SetOrchestratorType,
cns.NumberOfCPUCores,
cns.NMAgentSupportedAPIs,
}

type do interface {
Expand Down Expand Up @@ -416,6 +418,42 @@ func (c *Client) GetHTTPServiceData(ctx context.Context) (*restserver.GetHTTPSer
return &resp, nil
}

// NumOfCPUCores returns the number of CPU cores available on the host that
// CNS is running on.
func (c *Client) NumOfCPUCores(ctx context.Context) (*cns.NumOfCPUCoresResponse, error) {
// build the request
u := c.routes[cns.NumberOfCPUCores]
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), http.NoBody)
if err != nil {
return nil, errors.Wrap(err, "building http request")
}

// submit the request
resp, err := c.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "sending HTTP request")
}
defer resp.Body.Close()

// decode the response
var out cns.NumOfCPUCoresResponse
err = json.NewDecoder(resp.Body).Decode(&out)
if err != nil {
return nil, errors.Wrap(err, "decoding response as JSON")
}

// if the return code is non-zero, something went wrong and it should be
// surfaced to the caller
if out.Response.ReturnCode != 0 {
return nil, &CNSClientError{
Code: out.Response.ReturnCode,
Err: errors.New(out.Response.Message),
}
}

return &out, nil
}

// DeleteNetworkContainer destroys the requested network container matching the
// provided ID.
func (c *Client) DeleteNetworkContainer(ctx context.Context, ncID string) error {
Expand All @@ -434,7 +472,7 @@ func (c *Client) DeleteNetworkContainer(ctx context.Context, ncID string) error
return errors.Wrap(err, "encoding request body")
}
u := c.routes[cns.DeleteNetworkContainer]
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body))
if err != nil {
return errors.Wrap(err, "building HTTP request")
}
Expand Down Expand Up @@ -488,7 +526,7 @@ func (c *Client) SetOrchestratorType(ctx context.Context, sotr cns.SetOrchestrat
return errors.Wrap(err, "encoding request body")
}
u := c.routes[cns.SetOrchestratorType]
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body))
if err != nil {
return errors.Wrap(err, "building HTTP request")
}
Expand Down Expand Up @@ -537,7 +575,7 @@ func (c *Client) CreateNetworkContainer(ctx context.Context, cncr cns.CreateNetw
return errors.Wrap(err, "encoding request as JSON")
}
u := c.routes[cns.CreateOrUpdateNetworkContainer]
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body))
if err != nil {
return errors.Wrap(err, "building HTTP request")
}
Expand Down Expand Up @@ -585,7 +623,7 @@ func (c *Client) PublishNetworkContainer(ctx context.Context, pncr cns.PublishNe
return errors.Wrap(err, "encoding request body as json")
}
u := c.routes[cns.PublishNetworkContainer]
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body))
if err != nil {
return errors.Wrap(err, "building HTTP request")
}
Expand Down Expand Up @@ -631,7 +669,7 @@ func (c *Client) UnpublishNC(ctx context.Context, uncr cns.UnpublishNetworkConta
return errors.Wrap(err, "encoding request body as json")
}
u := c.routes[cns.UnpublishNetworkContainer]
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body))
if err != nil {
return errors.Wrap(err, "building HTTP request")
}
Expand Down Expand Up @@ -659,3 +697,56 @@ func (c *Client) UnpublishNC(ctx context.Context, uncr cns.UnpublishNetworkConta
// ...otherwise the request was successful so
return nil
}

// NMAgentSupportedAPIs returns the supported API names from NMAgent. This can
// be used, for example, to detect whether the node is capable for GRE
// allocations.
func (c *Client) NMAgentSupportedAPIs(ctx context.Context) (*cns.NmAgentSupportedApisResponse, error) {
// build the request
reqBody := &cns.NmAgentSupportedApisRequest{
// the IP used below is that of the Wireserver
GetNmAgentSupportedApisURL: "http://168.63.129.16/machine/plugins/?comp=nmagent&type=GetSupportedApis",
}

body, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.Wrap(err, "encoding request body")
}

u := c.routes[cns.NMAgentSupportedAPIs]
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), bytes.NewReader(body))
if err != nil {
return nil, errors.Wrap(err, "building http request")
}

// submit the request
resp, err := c.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "sending http request")
}
defer resp.Body.Close()

if code := resp.StatusCode; code != http.StatusOK {
return nil, &FailedHTTPRequest{
Code: code,
}
}

// decode response
var out cns.NmAgentSupportedApisResponse
err = json.NewDecoder(resp.Body).Decode(&out)
if err != nil {
return nil, errors.Wrap(err, "decoding response body")
}

// if there was a non-zero status code, that indicates an error and should be
// communicated as such
if out.Response.ReturnCode != 0 {
return nil, &CNSClientError{
Code: out.Response.ReturnCode,
Err: errors.New(out.Response.Message),
}
}

return &out, nil
}
136 changes: 136 additions & 0 deletions cns/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2009,3 +2009,139 @@ func TestGetHTTPServiceData(t *testing.T) {
})
}
}

func TestNumberOfCPUCores(t *testing.T) {
emptyRoutes, _ := buildRoutes(defaultBaseURL, clientPaths)
tests := []struct {
name string
shouldErr bool
exp *cns.NumOfCPUCoresResponse
}{
{
"happy path",
false,
&cns.NumOfCPUCoresResponse{
Response: cns.Response{
ReturnCode: 0,
Message: "success",
},
NumOfCPUCores: 42,
},
},
{
"unspecified error",
true,
&cns.NumOfCPUCoresResponse{
Response: cns.Response{
ReturnCode: types.MalformedSubnet,
Message: "malformed subnet",
},
NumOfCPUCores: 0,
},
},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()

client := &Client{
client: &mockdo{
errToReturn: nil,
objToReturn: test.exp,
httpStatusCodeToReturn: http.StatusOK,
},
routes: emptyRoutes,
}

got, err := client.NumOfCPUCores(context.Background())
if err != nil && !test.shouldErr {
t.Fatal("unexpected error: err:", err)
}

if err == nil && test.shouldErr {
t.Fatal("expected an error but received none")
}

if !test.shouldErr && !cmp.Equal(got, test.exp) {
t.Error("received response differs from expectation: diff:", cmp.Diff(got, test.exp))
}
})
}
}

func TestNMASupportedAPIs(t *testing.T) {
emptyRoutes, _ := buildRoutes(defaultBaseURL, clientPaths)
tests := []struct {
name string
shouldErr bool
respCode int
exp *cns.NmAgentSupportedApisResponse
}{
{
"happy",
false,
http.StatusOK,
&cns.NmAgentSupportedApisResponse{
Response: cns.Response{
ReturnCode: 0,
Message: "success",
},
SupportedApis: []string{},
},
},
{
"unspecified error",
true,
http.StatusOK,
&cns.NmAgentSupportedApisResponse{
Response: cns.Response{
ReturnCode: types.MalformedSubnet,
Message: "malformed subnet",
},
SupportedApis: []string{},
},
},
{
"not found",
true,
http.StatusNotFound,
nil,
},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()

client := &Client{
client: &mockdo{
errToReturn: nil,
objToReturn: test.exp,
httpStatusCodeToReturn: test.respCode,
},
routes: emptyRoutes,
}

got, err := client.NMAgentSupportedAPIs(context.Background())
if err != nil && !test.shouldErr {
t.Fatal("unexpected error: err:", err)
}

if err == nil && test.shouldErr {
t.Fatal("expected an error but received none")
}

// there should always be a response when there's no error
if err == nil && got == nil {
t.Fatal("expected a response but received none")
}

if !test.shouldErr && !cmp.Equal(got, test.exp) {
t.Error("received response differs from expectation: diff:", cmp.Diff(got, test.exp))
}
})
}
}
11 changes: 11 additions & 0 deletions cns/client/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@ package client
import (
"errors"
"fmt"
"net/http"

"github.com/Azure/azure-container-networking/cns/types"
)

// FailedHTTPRequest describes an HTTP request to CNS that has returned a
// non-200 status code.
type FailedHTTPRequest struct {
Code int
}

func (f *FailedHTTPRequest) Error() string {
return fmt.Sprintf("http request failed: %s (%d)", http.StatusText(f.Code), f.Code)
}

// CNSClientError records an error and relevant code
type CNSClientError struct {
Code types.ResponseCode
Expand Down