diff --git a/cns/NetworkContainerContract.go b/cns/NetworkContainerContract.go index d253eb0b19..8b47eb14df 100644 --- a/cns/NetworkContainerContract.go +++ b/cns/NetworkContainerContract.go @@ -29,6 +29,8 @@ const ( PathDebugIPAddresses = "/debug/ipaddresses" PathDebugPodContext = "/debug/podcontext" PathDebugRestData = "/debug/restdata" + NumberOfCPUCores = NumberOfCPUCoresPath + NMAgentSupportedAPIs = NmAgentSupportedApisPath ) // NetworkContainer Prefixes diff --git a/cns/client/client.go b/cns/client/client.go index 32cbbbaa86..9843709657 100644 --- a/cns/client/client.go +++ b/cns/client/client.go @@ -37,6 +37,8 @@ var clientPaths = []string{ cns.PublishNetworkContainer, cns.CreateOrUpdateNetworkContainer, cns.SetOrchestratorType, + cns.NumberOfCPUCores, + cns.NMAgentSupportedAPIs, } type do interface { @@ -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 { @@ -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") } @@ -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") } @@ -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") } @@ -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") } @@ -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") } @@ -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 +} diff --git a/cns/client/client_test.go b/cns/client/client_test.go index e1e03f7d55..e986848239 100644 --- a/cns/client/client_test.go +++ b/cns/client/client_test.go @@ -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)) + } + }) + } +} diff --git a/cns/client/error.go b/cns/client/error.go index 66ac7afc31..36881a707e 100644 --- a/cns/client/error.go +++ b/cns/client/error.go @@ -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