Skip to content

Commit 45108ae

Browse files
authored
Fix incorrect HTTP status from publish NC (#1757)
* Fix incorrect HTTP status from publish NC CNS was responding with an HTTP status code of "0" from NMAgent. Successes are supposed to be 200. The C-style var block at the beginning of publishNetworkContainer was the reason for this. During refactoring, the location where this status code was set to a successful value of 200 was accidentally removed. Because the var block declared the variable and silently initialized it to 0, the compiler did not flag this bug as it otherwise would have. The status code has been removed from this block and explicitly defined and initialized to a correct value of 200. Subsequent error handling will change this as necessary. Also, despite consumers depending on this status, there were no tests to verify that the status was set correctly. Tests have been added to reflect this dependency. * Ensure that NMAgent body is always set DNC depends on the NMAgent body being set for its vestigial functions of retrying failed requests. Since failed requests will now be retried internally (to CNS) by the NMAgent client, this isn't really necessary anymore. There are versions of DNC out there that depend on this body though, so it needs to be present in order for NC publishing to actually work. * Fix missing NMAgent status for Unpublish It was discovered that the Unpublish endpoints also omitted the status codes and bodies expected by clients. This adds those and fixes the associated tests to guarantee the expected behavior. * Silence the linter There were two instances where the linter was flagging dynamic errors, but this is just in a test. It's perfectly fine to bend the rules there, since we don't expect to re-use the errors (they really should be t.Fatal / t.Error anyway, but due to legacy we're returning errors here instead).
1 parent bcbb605 commit 45108ae

File tree

2 files changed

+156
-62
lines changed

2 files changed

+156
-62
lines changed

cns/restserver/api.go

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,15 +1159,17 @@ func (service *HTTPRestService) publishNetworkContainer(w http.ResponseWriter, r
11591159
ctx := r.Context()
11601160

11611161
var (
1162-
req cns.PublishNetworkContainerRequest
1163-
returnCode types.ResponseCode
1164-
returnMessage string
1165-
publishStatusCode int
1166-
publishResponseBody []byte
1167-
publishErrorStr string
1168-
isNetworkJoined bool
1162+
req cns.PublishNetworkContainerRequest
1163+
returnCode types.ResponseCode
1164+
returnMessage string
1165+
publishErrorStr string
1166+
isNetworkJoined bool
11691167
)
11701168

1169+
// publishing is assumed to succeed unless some other error handling sets it
1170+
// otherwise
1171+
publishStatusCode := http.StatusOK
1172+
11711173
err := service.Listener.Decode(w, r, &req)
11721174

11731175
creteNcURLCopy := req.CreateNetworkContainerURL
@@ -1245,14 +1247,18 @@ func (service *HTTPRestService) publishNetworkContainer(w http.ResponseWriter, r
12451247
returnCode = types.UnsupportedVerb
12461248
}
12471249

1250+
// create a synthetic response from NMAgent so that clients that previously
1251+
// relied on its presence can continue to do so.
1252+
publishResponseBody := fmt.Sprintf(`{"httpStatusCode":"%d"}`, publishStatusCode)
1253+
12481254
response := cns.PublishNetworkContainerResponse{
12491255
Response: cns.Response{
12501256
ReturnCode: returnCode,
12511257
Message: returnMessage,
12521258
},
12531259
PublishErrorStr: publishErrorStr,
12541260
PublishStatusCode: publishStatusCode,
1255-
PublishResponseBody: publishResponseBody,
1261+
PublishResponseBody: []byte(publishResponseBody),
12561262
}
12571263

12581264
err = service.Listener.Encode(w, &response)
@@ -1265,15 +1271,15 @@ func (service *HTTPRestService) unpublishNetworkContainer(w http.ResponseWriter,
12651271
ctx := r.Context()
12661272

12671273
var (
1268-
req cns.UnpublishNetworkContainerRequest
1269-
returnCode types.ResponseCode
1270-
returnMessage string
1271-
unpublishStatusCode int
1272-
unpublishResponseBody []byte
1273-
unpublishErrorStr string
1274-
isNetworkJoined bool
1274+
req cns.UnpublishNetworkContainerRequest
1275+
returnCode types.ResponseCode
1276+
returnMessage string
1277+
unpublishErrorStr string
1278+
isNetworkJoined bool
12751279
)
12761280

1281+
unpublishStatusCode := http.StatusOK
1282+
12771283
err := service.Listener.Decode(w, r, &req)
12781284

12791285
deleteNcURLCopy := req.DeleteNetworkContainerURL
@@ -1355,14 +1361,18 @@ func (service *HTTPRestService) unpublishNetworkContainer(w http.ResponseWriter,
13551361
returnCode = types.UnsupportedVerb
13561362
}
13571363

1364+
// create a synthetic response from NMAgent so that clients that previously
1365+
// relied on its presence can continue to do so.
1366+
unpublishResponseBody := fmt.Sprintf(`{"httpStatusCode":"%d"}`, unpublishStatusCode)
1367+
13581368
response := cns.UnpublishNetworkContainerResponse{
13591369
Response: cns.Response{
13601370
ReturnCode: returnCode,
13611371
Message: returnMessage,
13621372
},
13631373
UnpublishErrorStr: unpublishErrorStr,
13641374
UnpublishStatusCode: unpublishStatusCode,
1365-
UnpublishResponseBody: unpublishResponseBody,
1375+
UnpublishResponseBody: []byte(unpublishResponseBody),
13661376
}
13671377

13681378
err = service.Listener.Encode(w, &response)

cns/restserver/api_test.go

Lines changed: 130 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -843,67 +843,139 @@ func publishNCViaCNS(
843843
return fmt.Errorf("decoding response: %w", err)
844844
}
845845

846+
expStatus := http.StatusOK
847+
gotStatus := resp.PublishStatusCode
848+
if gotStatus != expStatus {
849+
// nolint:goerr113 // this is okay in a test:
850+
return fmt.Errorf("unsuccessful request. exp: %d, got: %d", expStatus, gotStatus)
851+
}
852+
853+
// ensure that there is an NMA body for legacy purposes
854+
nmaResp := make(map[string]any)
855+
err = json.Unmarshal(resp.PublishResponseBody, &nmaResp)
856+
if err != nil {
857+
return fmt.Errorf("decoding response body from nmagent: %w", err)
858+
}
859+
860+
if statusStr, ok := nmaResp["httpStatusCode"]; ok {
861+
bodyStatus, err := strconv.Atoi(statusStr.(string))
862+
if err != nil {
863+
return fmt.Errorf("parsing http status string from nmagent: %w", err)
864+
}
865+
866+
if bodyStatus != expStatus {
867+
// nolint:goerr113 // this is okay in a test:
868+
return fmt.Errorf("unexpected status in body. exp: %d, got %d", expStatus, bodyStatus)
869+
}
870+
}
871+
846872
fmt.Printf("PublishNetworkContainer succeded with response %+v, raw:%+v\n", resp, w.Body)
847873
return nil
848874
}
849875

850876
func TestUnpublishNCViaCNS(t *testing.T) {
877+
// Publishing and Unpublishing via CNS effectively uses CNS as a proxy to
878+
// NMAgent. This test asserts that the correct methods are invoked on the
879+
// NMAgent client in the correct order based on a sequence of creating and
880+
// destroying an example NC.
881+
882+
// create a mock NMAgent that allows assertions on the methods called. There
883+
// should be a certain sequence of invocations based on the high-level
884+
// actions that are taken here.
885+
const (
886+
joinNetwork = "JoinNetwork"
887+
deleteNetworkContainer = "DeleteNetworkContainer"
888+
putNetworkContainer = "PutNetworkContainer"
889+
)
890+
891+
got := []string{}
851892
mnma := &fakes.NMAgentClientFake{
852893
JoinNetworkF: func(_ context.Context, _ nmagent.JoinNetworkRequest) error {
894+
got = append(got, joinNetwork)
853895
return nil
854896
},
855897
DeleteNetworkContainerF: func(_ context.Context, _ nmagent.DeleteContainerRequest) error {
898+
got = append(got, deleteNetworkContainer)
856899
return nil
857900
},
858901
PutNetworkContainerF: func(_ context.Context, _ *nmagent.PutNetworkContainerRequest) error {
902+
got = append(got, putNetworkContainer)
859903
return nil
860904
},
861905
}
862906

863907
cleanup := setMockNMAgent(svc, mnma)
864908
defer cleanup()
865909

866-
deleteNetworkContainerURL := "http://" + nmagentEndpoint +
867-
"/machine/plugins/?comp=nmagent&type=NetworkManagement/interfaces/dummyIntf/networkContainers/dummyNCURL/authenticationToken/dummyT/api-version/1/method/DELETE"
868-
err := publishNCViaCNS("vnet1", "ethWebApp", deleteNetworkContainerURL)
910+
// create a network container as the subject of this test.
911+
createNetworkContainerURL := "http://" + nmagentEndpoint +
912+
"/machine/plugins/?comp=nmagent&type=NetworkManagement/interfaces/dummyIntf/networkContainers/dummyNCURL/authenticationToken/dummyT/api-version/1"
913+
err := publishNCViaCNS("vnet1", "ethWebApp", createNetworkContainerURL)
869914
if err != nil {
870-
t.Fatal(err)
915+
t.Fatal(fmt.Errorf("publish container failed %w ", err))
871916
}
872917

873-
deleteNetworkContainerURL = "http://" + nmagentEndpoint +
918+
// prior to the actual deletion, attempt to delete using an invalid URL (by
919+
// omitting a letter from "authenticationToken"). This should fail:
920+
deleteNetworkContainerURL := "http://" + nmagentEndpoint +
874921
"/machine/plugins/?comp=nmagent&type=NetworkManagement/interfaces/dummyIntf/networkContainers/dummyNCURL/authenticationToke/" +
875922
"8636c99d-7861-401f-b0d3-7e5b7dc8183c" +
876923
"/api-version/1/method/DELETE"
877-
878-
err = publishNCViaCNS("vnet1", "ethWebApp", deleteNetworkContainerURL)
924+
err = unpublishNCViaCNS("vnet1", "ethWebApp", deleteNetworkContainerURL)
879925
if err == nil {
880926
t.Fatal("Expected a bad request error due to delete network url being incorrect")
881927
}
882928

929+
// also ensure that deleting a network container with an invalid
930+
// authentication token (one that is too long) also fails:
883931
deleteNetworkContainerURL = "http://" + nmagentEndpoint +
884932
"/machine/plugins/?comp=nmagent&NetworkManagement/interfaces/dummyIntf/networkContainers/dummyNCURL/authenticationToken/" +
885933
"8636c99d-7861-401f-b0d3-7e5b7dc8183c8636c99d-7861-401f-b0d3-7e5b7dc8183c" +
886934
"/api-version/1/method/DELETE"
887-
888-
err = testUnpublishNCViaCNS(t, "vnet1", "ethWebApp", deleteNetworkContainerURL, true)
935+
err = unpublishNCViaCNS("vnet1", "ethWebApp", deleteNetworkContainerURL)
889936
if err == nil {
890937
t.Fatal("Expected a bad request error due to create network url having more characters than permitted in auth token")
891938
}
892-
}
893939

894-
func testUnpublishNCViaCNS(t *testing.T,
895-
networkID,
896-
networkContainerID,
897-
deleteNetworkContainerURL string,
898-
expectError bool,
899-
) error {
900-
var (
901-
body bytes.Buffer
902-
resp cns.UnpublishNetworkContainerResponse
903-
)
940+
// now actually perform the deletion:
941+
deleteNetworkContainerURL = "http://" + nmagentEndpoint +
942+
"/machine/plugins/?comp=nmagent&type=NetworkManagement/interfaces/dummyIntf/networkContainers/dummyNCURL/authenticationToken/dummyT/api-version/1/method/DELETE"
943+
err = unpublishNCViaCNS("vnet1", "ethWebApp", deleteNetworkContainerURL)
944+
if err != nil {
945+
t.Fatal(err)
946+
}
904947

905-
fmt.Println("Test: unpublishNetworkContainer")
948+
// Assert the correct sequence of invocations on the NMAgent client. Creating
949+
// a network container involves first joining the network and then creating a
950+
// network container. Deleting the network container only involved the Delete
951+
// call. Even though there were two other invalid Delete calls, we do not
952+
// expect them to generate invocations of methods on the NMAgent
953+
// client--these should be captured by CNS.
954+
exp := []string{
955+
// These two methods in the sequence are from the NC creation:
956+
joinNetwork,
957+
putNetworkContainer,
906958

959+
// This one is from the delete. There is technically one code path where a
960+
// "JoinNetwork" can appear here, but we don't expect it because
961+
// "JoinNetwork" appeared as part of the NC creation.
962+
deleteNetworkContainer,
963+
}
964+
965+
// with the expectation set, match up expectations with the method calls
966+
// received:
967+
if len(exp) != len(got) {
968+
t.Fatal("unexpected sequence of methods invoked on NMAgent client: exp:", exp, "got:", got)
969+
}
970+
971+
for idx := range exp {
972+
if got[idx] != exp[idx] {
973+
t.Error("unexpected sequence of methods invoked on NMAgent client: exp:", exp, "got:", got)
974+
}
975+
}
976+
}
977+
978+
func unpublishNCViaCNS(networkID, networkContainerID, deleteNetworkContainerURL string) error {
907979
joinNetworkURL := "http://" + nmagentEndpoint + "/dummyVnetURL"
908980

909981
unpublishNCRequest := &cns.UnpublishNetworkContainerRequest{
@@ -913,36 +985,50 @@ func testUnpublishNCViaCNS(t *testing.T,
913985
DeleteNetworkContainerURL: deleteNetworkContainerURL,
914986
}
915987

988+
var body bytes.Buffer
916989
json.NewEncoder(&body).Encode(unpublishNCRequest)
917990
req, err := http.NewRequest(http.MethodPost, cns.UnpublishNetworkContainer, &body)
918991
if err != nil {
919992
return fmt.Errorf("Failed to create unpublish request %w", err)
920993
}
921994

922-
mnma := &fakes.NMAgentClientFake{
923-
DeleteNetworkContainerF: func(_ context.Context, _ nmagent.DeleteContainerRequest) error {
924-
return nil
925-
},
926-
JoinNetworkF: func(_ context.Context, _ nmagent.JoinNetworkRequest) error {
927-
return nil
928-
},
929-
}
930-
931-
cleanup := setMockNMAgent(svc, mnma)
932-
defer cleanup()
933-
934995
w := httptest.NewRecorder()
935996
mux.ServeHTTP(w, req)
936997

998+
var resp cns.UnpublishNetworkContainerResponse
937999
err = decodeResponse(w, &resp)
938-
if err != nil || resp.Response.ReturnCode != 0 {
939-
if !expectError {
940-
t.Errorf("UnpublishNetworkContainer failed with response %+v Err:%+v", resp, err)
941-
}
942-
return err
1000+
if err != nil {
1001+
return fmt.Errorf("error decoding json: err: %w", err)
1002+
}
1003+
1004+
if resp.Response.ReturnCode != 0 {
1005+
// nolint:goerr113 // this is okay in a test:
1006+
return fmt.Errorf("UnpublishNetworkContainer failed with response %+v: err: %w", resp, err)
1007+
}
1008+
1009+
code := resp.UnpublishStatusCode
1010+
if code != http.StatusOK {
1011+
// nolint:goerr113 // this is okay in a test:
1012+
return fmt.Errorf("unsuccessful NMAgent response: status code %d", code)
9431013
}
9441014

945-
fmt.Printf("UnpublishNetworkContainer succeded with response %+v, raw:%+v\n", resp, w.Body)
1015+
nmaBody := struct {
1016+
StatusCode string `json:"httpStatusCode"`
1017+
}{}
1018+
err = json.Unmarshal(resp.UnpublishResponseBody, &nmaBody)
1019+
if err != nil {
1020+
return fmt.Errorf("unmarshaling NMAgent response body: %w", err)
1021+
}
1022+
1023+
bodyCode, err := strconv.Atoi(nmaBody.StatusCode)
1024+
if err != nil {
1025+
return fmt.Errorf("parsing NMAgent body status code as an integer: %w", err)
1026+
}
1027+
1028+
if bodyCode != code {
1029+
// nolint:goerr113 // this is okay in a test:
1030+
return fmt.Errorf("mismatch between NMAgent status code (%d) and NMAgent body status code (%d)", code, bodyCode)
1031+
}
9461032

9471033
return nil
9481034
}
@@ -1367,14 +1453,16 @@ func setEnv(t *testing.T) *httptest.ResponseRecorder {
13671453
}
13681454

13691455
func startService() error {
1370-
var err error
13711456
// Create the service.
13721457
config := common.ServiceConfig{}
1373-
// Create the key value store.
1374-
if config.Store, err = store.NewJsonFileStore(cnsJsonFileName, processlock.NewMockFileLock(false)); err != nil {
1458+
1459+
// Create the key value fileStore.
1460+
fileStore, err := store.NewJsonFileStore(cnsJsonFileName, processlock.NewMockFileLock(false))
1461+
if err != nil {
13751462
logger.Errorf("Failed to create store file: %s, due to error %v\n", cnsJsonFileName, err)
13761463
return err
13771464
}
1465+
config.Store = fileStore
13781466

13791467
nmagentClient := &fakes.NMAgentClientFake{}
13801468
service, err = NewHTTPRestService(&config, &fakes.WireserverClientFake{}, nmagentClient, nil, nil, nil)
@@ -1383,10 +1471,6 @@ func startService() error {
13831471
}
13841472
svc = service.(*HTTPRestService)
13851473
svc.Name = "cns-test-server"
1386-
if err != nil {
1387-
logger.Errorf("Failed to create CNS object, err:%v.\n", err)
1388-
return err
1389-
}
13901474

13911475
svc.IPAMPoolMonitor = &fakes.MonitorFake{}
13921476
nmagentClient.GetNCVersionListF = func(context.Context) (nmagent.NCVersionList, error) {

0 commit comments

Comments
 (0)