Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add two more CNS endpoints to the CNS client #1541

Merged
merged 9 commits into from
Sep 13, 2022
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",
rbtr marked this conversation as resolved.
Show resolved Hide resolved
}

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