Skip to content

Commit a9a0a5f

Browse files
authored
Add two more CNS endpoints to the CNS client (#1541)
* Add NumOfCPUCores method to CNS client This is the last part of CNS's API surface that is used by DNC, but not covered by a method from the CNS client. * Remove unnecessary error wrapping lambda The error handling here is simple enough that it doesn't really warrant wrapping it with a lambda. * Add NmAgentSupportedAPIs to the CNS Client DNC uses this API to detect GRE key capabilities. Since it's not supported by the client, it does this with direct HTTP requests currently. In order to ensure that there's only one way of accessing CNS, this implements the method so that the client can be used in DNC instead. * Add NMA supported APIs to client URL list This was forgotten in a previous iteration. * Handle non-200 status code by returning an error The NMAgentSupportedAPIs method did not error when a non-2XX status code was encountered. This leads to surprising behavior on the part of the consumer. * Use http.NoBody instead of nil This was a linter suggestion, and a good one. http.NoBody is more semantically meaningful than passing a nil. * Create a FailedHTTPRequest error type This is in response to a linter complaint that dynamic errors were being used instead of a static error. Declaring an error type with var wouldn't carry along necessary debugging information (namely the HTTP status code), so it wasn't really appropriate. Rather than disable the check entirely with a linter directive, this defines a reusable error type so that HTTP errors can be communicated upward consistently. * Use an alias for CNS client paths These really should be a single set of paths, but there's this existing block of constants to define a "contract with DNC." Whether or not that's complete, the spirit of it is somewhat clear. However, it's not great to be duplicating constants all over the place. This is somewhat of a compromise by defining the newer constants in terms of the older ones. * Add missing context to outbound HTTP requests This was a good suggestion from the linter.
1 parent 0400273 commit a9a0a5f

File tree

4 files changed

+245
-5
lines changed

4 files changed

+245
-5
lines changed

cns/NetworkContainerContract.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const (
2929
PathDebugIPAddresses = "/debug/ipaddresses"
3030
PathDebugPodContext = "/debug/podcontext"
3131
PathDebugRestData = "/debug/restdata"
32+
NumberOfCPUCores = NumberOfCPUCoresPath
33+
NMAgentSupportedAPIs = NmAgentSupportedApisPath
3234
)
3335

3436
// NetworkContainer Prefixes

cns/client/client.go

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ var clientPaths = []string{
3737
cns.PublishNetworkContainer,
3838
cns.CreateOrUpdateNetworkContainer,
3939
cns.SetOrchestratorType,
40+
cns.NumberOfCPUCores,
41+
cns.NMAgentSupportedAPIs,
4042
}
4143

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

421+
// NumOfCPUCores returns the number of CPU cores available on the host that
422+
// CNS is running on.
423+
func (c *Client) NumOfCPUCores(ctx context.Context) (*cns.NumOfCPUCoresResponse, error) {
424+
// build the request
425+
u := c.routes[cns.NumberOfCPUCores]
426+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), http.NoBody)
427+
if err != nil {
428+
return nil, errors.Wrap(err, "building http request")
429+
}
430+
431+
// submit the request
432+
resp, err := c.client.Do(req)
433+
if err != nil {
434+
return nil, errors.Wrap(err, "sending HTTP request")
435+
}
436+
defer resp.Body.Close()
437+
438+
// decode the response
439+
var out cns.NumOfCPUCoresResponse
440+
err = json.NewDecoder(resp.Body).Decode(&out)
441+
if err != nil {
442+
return nil, errors.Wrap(err, "decoding response as JSON")
443+
}
444+
445+
// if the return code is non-zero, something went wrong and it should be
446+
// surfaced to the caller
447+
if out.Response.ReturnCode != 0 {
448+
return nil, &CNSClientError{
449+
Code: out.Response.ReturnCode,
450+
Err: errors.New(out.Response.Message),
451+
}
452+
}
453+
454+
return &out, nil
455+
}
456+
419457
// DeleteNetworkContainer destroys the requested network container matching the
420458
// provided ID.
421459
func (c *Client) DeleteNetworkContainer(ctx context.Context, ncID string) error {
@@ -434,7 +472,7 @@ func (c *Client) DeleteNetworkContainer(ctx context.Context, ncID string) error
434472
return errors.Wrap(err, "encoding request body")
435473
}
436474
u := c.routes[cns.DeleteNetworkContainer]
437-
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
475+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body))
438476
if err != nil {
439477
return errors.Wrap(err, "building HTTP request")
440478
}
@@ -488,7 +526,7 @@ func (c *Client) SetOrchestratorType(ctx context.Context, sotr cns.SetOrchestrat
488526
return errors.Wrap(err, "encoding request body")
489527
}
490528
u := c.routes[cns.SetOrchestratorType]
491-
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
529+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body))
492530
if err != nil {
493531
return errors.Wrap(err, "building HTTP request")
494532
}
@@ -537,7 +575,7 @@ func (c *Client) CreateNetworkContainer(ctx context.Context, cncr cns.CreateNetw
537575
return errors.Wrap(err, "encoding request as JSON")
538576
}
539577
u := c.routes[cns.CreateOrUpdateNetworkContainer]
540-
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
578+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body))
541579
if err != nil {
542580
return errors.Wrap(err, "building HTTP request")
543581
}
@@ -585,7 +623,7 @@ func (c *Client) PublishNetworkContainer(ctx context.Context, pncr cns.PublishNe
585623
return errors.Wrap(err, "encoding request body as json")
586624
}
587625
u := c.routes[cns.PublishNetworkContainer]
588-
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
626+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body))
589627
if err != nil {
590628
return errors.Wrap(err, "building HTTP request")
591629
}
@@ -631,7 +669,7 @@ func (c *Client) UnpublishNC(ctx context.Context, uncr cns.UnpublishNetworkConta
631669
return errors.Wrap(err, "encoding request body as json")
632670
}
633671
u := c.routes[cns.UnpublishNetworkContainer]
634-
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
672+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(body))
635673
if err != nil {
636674
return errors.Wrap(err, "building HTTP request")
637675
}
@@ -659,3 +697,56 @@ func (c *Client) UnpublishNC(ctx context.Context, uncr cns.UnpublishNetworkConta
659697
// ...otherwise the request was successful so
660698
return nil
661699
}
700+
701+
// NMAgentSupportedAPIs returns the supported API names from NMAgent. This can
702+
// be used, for example, to detect whether the node is capable for GRE
703+
// allocations.
704+
func (c *Client) NMAgentSupportedAPIs(ctx context.Context) (*cns.NmAgentSupportedApisResponse, error) {
705+
// build the request
706+
reqBody := &cns.NmAgentSupportedApisRequest{
707+
// the IP used below is that of the Wireserver
708+
GetNmAgentSupportedApisURL: "http://168.63.129.16/machine/plugins/?comp=nmagent&type=GetSupportedApis",
709+
}
710+
711+
body, err := json.Marshal(reqBody)
712+
if err != nil {
713+
return nil, errors.Wrap(err, "encoding request body")
714+
}
715+
716+
u := c.routes[cns.NMAgentSupportedAPIs]
717+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), bytes.NewReader(body))
718+
if err != nil {
719+
return nil, errors.Wrap(err, "building http request")
720+
}
721+
722+
// submit the request
723+
resp, err := c.client.Do(req)
724+
if err != nil {
725+
return nil, errors.Wrap(err, "sending http request")
726+
}
727+
defer resp.Body.Close()
728+
729+
if code := resp.StatusCode; code != http.StatusOK {
730+
return nil, &FailedHTTPRequest{
731+
Code: code,
732+
}
733+
}
734+
735+
// decode response
736+
var out cns.NmAgentSupportedApisResponse
737+
err = json.NewDecoder(resp.Body).Decode(&out)
738+
if err != nil {
739+
return nil, errors.Wrap(err, "decoding response body")
740+
}
741+
742+
// if there was a non-zero status code, that indicates an error and should be
743+
// communicated as such
744+
if out.Response.ReturnCode != 0 {
745+
return nil, &CNSClientError{
746+
Code: out.Response.ReturnCode,
747+
Err: errors.New(out.Response.Message),
748+
}
749+
}
750+
751+
return &out, nil
752+
}

cns/client/client_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2009,3 +2009,139 @@ func TestGetHTTPServiceData(t *testing.T) {
20092009
})
20102010
}
20112011
}
2012+
2013+
func TestNumberOfCPUCores(t *testing.T) {
2014+
emptyRoutes, _ := buildRoutes(defaultBaseURL, clientPaths)
2015+
tests := []struct {
2016+
name string
2017+
shouldErr bool
2018+
exp *cns.NumOfCPUCoresResponse
2019+
}{
2020+
{
2021+
"happy path",
2022+
false,
2023+
&cns.NumOfCPUCoresResponse{
2024+
Response: cns.Response{
2025+
ReturnCode: 0,
2026+
Message: "success",
2027+
},
2028+
NumOfCPUCores: 42,
2029+
},
2030+
},
2031+
{
2032+
"unspecified error",
2033+
true,
2034+
&cns.NumOfCPUCoresResponse{
2035+
Response: cns.Response{
2036+
ReturnCode: types.MalformedSubnet,
2037+
Message: "malformed subnet",
2038+
},
2039+
NumOfCPUCores: 0,
2040+
},
2041+
},
2042+
}
2043+
2044+
for _, test := range tests {
2045+
test := test
2046+
t.Run(test.name, func(t *testing.T) {
2047+
t.Parallel()
2048+
2049+
client := &Client{
2050+
client: &mockdo{
2051+
errToReturn: nil,
2052+
objToReturn: test.exp,
2053+
httpStatusCodeToReturn: http.StatusOK,
2054+
},
2055+
routes: emptyRoutes,
2056+
}
2057+
2058+
got, err := client.NumOfCPUCores(context.Background())
2059+
if err != nil && !test.shouldErr {
2060+
t.Fatal("unexpected error: err:", err)
2061+
}
2062+
2063+
if err == nil && test.shouldErr {
2064+
t.Fatal("expected an error but received none")
2065+
}
2066+
2067+
if !test.shouldErr && !cmp.Equal(got, test.exp) {
2068+
t.Error("received response differs from expectation: diff:", cmp.Diff(got, test.exp))
2069+
}
2070+
})
2071+
}
2072+
}
2073+
2074+
func TestNMASupportedAPIs(t *testing.T) {
2075+
emptyRoutes, _ := buildRoutes(defaultBaseURL, clientPaths)
2076+
tests := []struct {
2077+
name string
2078+
shouldErr bool
2079+
respCode int
2080+
exp *cns.NmAgentSupportedApisResponse
2081+
}{
2082+
{
2083+
"happy",
2084+
false,
2085+
http.StatusOK,
2086+
&cns.NmAgentSupportedApisResponse{
2087+
Response: cns.Response{
2088+
ReturnCode: 0,
2089+
Message: "success",
2090+
},
2091+
SupportedApis: []string{},
2092+
},
2093+
},
2094+
{
2095+
"unspecified error",
2096+
true,
2097+
http.StatusOK,
2098+
&cns.NmAgentSupportedApisResponse{
2099+
Response: cns.Response{
2100+
ReturnCode: types.MalformedSubnet,
2101+
Message: "malformed subnet",
2102+
},
2103+
SupportedApis: []string{},
2104+
},
2105+
},
2106+
{
2107+
"not found",
2108+
true,
2109+
http.StatusNotFound,
2110+
nil,
2111+
},
2112+
}
2113+
2114+
for _, test := range tests {
2115+
test := test
2116+
t.Run(test.name, func(t *testing.T) {
2117+
t.Parallel()
2118+
2119+
client := &Client{
2120+
client: &mockdo{
2121+
errToReturn: nil,
2122+
objToReturn: test.exp,
2123+
httpStatusCodeToReturn: test.respCode,
2124+
},
2125+
routes: emptyRoutes,
2126+
}
2127+
2128+
got, err := client.NMAgentSupportedAPIs(context.Background())
2129+
if err != nil && !test.shouldErr {
2130+
t.Fatal("unexpected error: err:", err)
2131+
}
2132+
2133+
if err == nil && test.shouldErr {
2134+
t.Fatal("expected an error but received none")
2135+
}
2136+
2137+
// there should always be a response when there's no error
2138+
if err == nil && got == nil {
2139+
t.Fatal("expected a response but received none")
2140+
}
2141+
2142+
if !test.shouldErr && !cmp.Equal(got, test.exp) {
2143+
t.Error("received response differs from expectation: diff:", cmp.Diff(got, test.exp))
2144+
}
2145+
})
2146+
}
2147+
}

cns/client/error.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,21 @@ package client
33
import (
44
"errors"
55
"fmt"
6+
"net/http"
67

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

11+
// FailedHTTPRequest describes an HTTP request to CNS that has returned a
12+
// non-200 status code.
13+
type FailedHTTPRequest struct {
14+
Code int
15+
}
16+
17+
func (f *FailedHTTPRequest) Error() string {
18+
return fmt.Sprintf("http request failed: %s (%d)", http.StatusText(f.Code), f.Code)
19+
}
20+
1021
// CNSClientError records an error and relevant code
1122
type CNSClientError struct {
1223
Code types.ResponseCode

0 commit comments

Comments
 (0)