diff --git a/pkg/api/mockhttpclient.go b/pkg/api/mockhttpclient.go index f9c444bb4..0985513eb 100644 --- a/pkg/api/mockhttpclient.go +++ b/pkg/api/mockhttpclient.go @@ -26,10 +26,11 @@ type MockHTTPClient struct { // MockResponse - use for mocking the MockHTTPClient responses type MockResponse struct { - FileName string - RespData string - RespCode int - ErrString string + FileName string + RespData string + RespCode int + RespHeaders map[string][]string + ErrString string } // SetResponse - @@ -122,7 +123,7 @@ func (c *MockHTTPClient) sendMultiple(request Request) (*Response, error) { response := Response{ Code: c.Responses[c.RespCount].RespCode, Body: dat, - Headers: map[string][]string{}, + Headers: c.Responses[c.RespCount].RespHeaders, } if c.Responses[c.RespCount].ErrString != "" { diff --git a/pkg/apic/apiservice.go b/pkg/apic/apiservice.go index 2e3956b34..1c413dcd8 100644 --- a/pkg/apic/apiservice.go +++ b/pkg/apic/apiservice.go @@ -81,6 +81,16 @@ func (c *ServiceClient) getOwnerObject(serviceBody *ServiceBody, warning bool) ( return nil, nil } +func (c *ServiceClient) setSpecHashesOnServiceBody(serviceBody *ServiceBody, svc apiv1.Interface) { + agentDetails := util.GetAgentDetails(svc) + if revDetails, found := agentDetails[specHashes]; found { + if specHashes, ok := revDetails.(map[string]interface{}); ok { + serviceBody.specHashes = util.MapStringInterfaceToStringString(specHashes) + } + } + +} + func (c *ServiceClient) updateAPIService(serviceBody *ServiceBody, svc *management.APIService) { owner, ownerErr := c.getOwnerObject(serviceBody, true) @@ -100,11 +110,7 @@ func (c *ServiceClient) updateAPIService(serviceBody *ServiceBody, svc *manageme util.SetAgentDetails(svc, newSVCDetails) // get the specHashes from the existing service - if revDetails, found := newSVCDetails[specHashes]; found { - if specHashes, ok := revDetails.(map[string]interface{}); ok { - serviceBody.specHashes = specHashes - } - } + c.setSpecHashesOnServiceBody(serviceBody, svc) if serviceBody.Image != "" { svc.Spec.Icon = management.ApiServiceSpecIcon{ @@ -185,7 +191,7 @@ func (c *ServiceClient) processService(serviceBody *ServiceBody) (*management.AP serviceURL := c.cfg.GetServicesURL() httpMethod := http.MethodPost serviceBody.serviceContext.serviceAction = addAPI - serviceBody.specHashes = map[string]interface{}{} + serviceBody.specHashes = map[string]string{} // If service exists, update existing service svc, err := c.getAPIServiceFromCache(serviceBody) @@ -193,7 +199,13 @@ func (c *ServiceClient) processService(serviceBody *ServiceBody) (*management.AP return nil, err } - if svc != nil { + if svc != nil && serviceBody.IsRevisionOnly() { + serviceBody.serviceContext.serviceAction = updateAPI + serviceBody.serviceContext.serviceName = svc.Name + serviceBody.serviceContext.serviceID = svc.Metadata.ID + c.setSpecHashesOnServiceBody(serviceBody, svc) + return svc, nil + } else if svc != nil { serviceBody.serviceContext.serviceAction = updateAPI httpMethod = http.MethodPut serviceURL += "/" + svc.Name diff --git a/pkg/apic/apiservice_test.go b/pkg/apic/apiservice_test.go index 9bb2c3b6b..380d2935e 100644 --- a/pkg/apic/apiservice_test.go +++ b/pkg/apic/apiservice_test.go @@ -2,26 +2,37 @@ package apic import ( "fmt" - "io" "net/http" "os" "testing" - "github.com/Axway/agent-sdk/pkg/util" - - defs "github.com/Axway/agent-sdk/pkg/apic/definitions" + "github.com/stretchr/testify/assert" "github.com/Axway/agent-sdk/pkg/api" apiv1 "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/api/v1" management "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/management/v1alpha1" - "github.com/stretchr/testify/assert" + defs "github.com/Axway/agent-sdk/pkg/apic/definitions" + "github.com/Axway/agent-sdk/pkg/util" +) + +const ( + testDocs = `"docs"` + testImageContentType = "image/jpeg" + testAPIServiceFile = "./testdata/apiservice.json" + testAgentDetailsFile = "./testdata/agent-details-sr.json" + testRevisionFile = "./testdata/servicerevision.json" + testInstanceFile = "./testdata/serviceinstance.json" + testRevisionListFile = "./testdata/servicerevisionlist.json" + testEmptyListFile = "./testdata/empty-list.json" + testPetstoreSpec = "./testdata/petstore-swagger2.json" + testRevisionNameAlt = "daleapi-1" ) var serviceBody = ServiceBody{ APIName: "daleapi", - Documentation: []byte("\"docs\""), + Documentation: []byte(testDocs), Image: "abcde", - ImageContentType: "image/jpeg", + ImageContentType: testImageContentType, ResourceType: Oas2, RestAPIID: "12345", } @@ -40,43 +51,42 @@ func TestCreateService(t *testing.T) { // this should be a full go right path httpClient.SetResponses([]api.MockResponse{ { - FileName: "./testdata/apiservice.json", // this for call to create the service + FileName: testAPIServiceFile, // this for call to create the service RespCode: http.StatusCreated, }, { - FileName: "./testdata/agent-details-sr.json", // this for call to create the service + FileName: testAgentDetailsFile, // this for call to create the service RespCode: http.StatusOK, }, { - FileName: "./testdata/agent-details-sr.json", // this for call to create the service + FileName: testAgentDetailsFile, // this for call to create the service RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevision.json", // this for call to create the serviceRevision + FileName: testRevisionFile, // this for call to create the serviceRevision RespCode: http.StatusCreated, }, { - FileName: "./testdata/agent-details-sr.json", // this for call to create the service + FileName: testAgentDetailsFile, // this for call to create the service RespCode: http.StatusOK, }, { - FileName: "./testdata/serviceinstance.json", // this for call to create the serviceInstance + FileName: testInstanceFile, // this for call to create the serviceInstance RespCode: http.StatusCreated, }, { - FileName: "./testdata/agent-details-sr.json", // this for call to create the service + FileName: testAgentDetailsFile, // this for call to create the service RespCode: http.StatusOK, }, { - FileName: "./testdata/agent-details-sr.json", // this for call to create the service + FileName: testAgentDetailsFile, // this for call to create the service RespCode: http.StatusOK, }, }) // Test oas2 object - oas2Json, _ := os.Open("./testdata/petstore-swagger2.json") // OAS2 - defer oas2Json.Close() - oas2Bytes, _ := io.ReadAll(oas2Json) + oas2Bytes, err := os.ReadFile(testPetstoreSpec) + assert.Nil(t, err) cloneServiceBody := serviceBody cloneServiceBody.SpecDefinition = oas2Bytes @@ -89,7 +99,7 @@ func TestCreateService(t *testing.T) { RespCode: http.StatusNotFound, }, { - FileName: "./testdata/apiservice.json", // this for call to create the service + FileName: testAPIServiceFile, // this for call to create the service RespCode: http.StatusRequestTimeout, }, }) @@ -104,15 +114,15 @@ func TestCreateService(t *testing.T) { RespCode: http.StatusNotFound, }, { - FileName: "./testdata/apiservice.json", // this for call to create the service + FileName: testAPIServiceFile, // this for call to create the service RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevision.json", // this for call to create the serviceRevision + FileName: testRevisionFile, // this for call to create the serviceRevision RespCode: http.StatusRequestTimeout, }, { - FileName: "./testdata/empty-list.json", // this for call to rollback apiservice + FileName: testEmptyListFile, // this for call to rollback apiservice RespCode: http.StatusOK, }, }) @@ -127,19 +137,19 @@ func TestCreateService(t *testing.T) { RespCode: http.StatusNotFound, }, { - FileName: "./testdata/apiservice.json", // this for call to create the service + FileName: testAPIServiceFile, // this for call to create the service RespCode: http.StatusCreated, }, { - FileName: "./testdata/servicerevision.json", // this for call to create the serviceRevision + FileName: testRevisionFile, // this for call to create the serviceRevision RespCode: http.StatusCreated, }, { - FileName: "./testdata/serviceinstance.json", // this for call to create the serviceInstance + FileName: testInstanceFile, // this for call to create the serviceInstance RespCode: http.StatusRequestTimeout, }, { - FileName: "./testdata/empty-list.json", // this for call to rollback apiservice + FileName: testEmptyListFile, // this for call to rollback apiservice RespCode: http.StatusOK, }, }) @@ -154,15 +164,15 @@ func TestCreateService(t *testing.T) { RespCode: http.StatusNotFound, }, { - FileName: "./testdata/apiservice.json", // this for call to create the service + FileName: testAPIServiceFile, // this for call to create the service RespCode: http.StatusCreated, }, { - FileName: "./testdata/servicerevision.json", // this for call to create the serviceRevision + FileName: testRevisionFile, // this for call to create the serviceRevision RespCode: http.StatusCreated, }, { - FileName: "./testdata/serviceinstance.json", // this for call to create the serviceInstance + FileName: testInstanceFile, // this for call to create the serviceInstance RespCode: http.StatusCreated, }, { @@ -175,7 +185,101 @@ func TestCreateService(t *testing.T) { assert.Nil(t, apiSvc) } -func Test_getAPIServiceFromCache(t *testing.T) { +func TestPublishServiceRevisionOnly(t *testing.T) { + oas2Bytes, err := os.ReadFile(testPetstoreSpec) + assert.Nil(t, err) + + tests := map[string]struct { + revisionOnly bool + existingSvc *management.APIService + responses []api.MockResponse + }{ + "new service: full publish creates service, revision, and instance": { + revisionOnly: false, + responses: []api.MockResponse{ + {FileName: testAPIServiceFile, RespCode: http.StatusCreated}, // POST service + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // service source subresource + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // service x-agent-details subresource + {FileName: testRevisionFile, RespCode: http.StatusCreated}, // POST revision + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // revision subresource + {FileName: testInstanceFile, RespCode: http.StatusCreated}, // POST instance + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // instance subresource + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes update + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // service source update + }, + }, + "new service: revision-only creates service and revision, skips instance": { + revisionOnly: true, + responses: []api.MockResponse{ + {FileName: testAPIServiceFile, RespCode: http.StatusCreated}, // POST service + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // service source subresource + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // service x-agent-details subresource + {FileName: testRevisionFile, RespCode: http.StatusCreated}, // POST revision + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // revision subresource + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes update + }, + }, + "existing service: full publish updates service, revision, and instance": { + revisionOnly: false, + existingSvc: createAPIService(serviceBody.APIName, serviceBody.RestAPIID, "", "", false), + responses: []api.MockResponse{ + {FileName: testAPIServiceFile, RespCode: http.StatusOK}, // PUT service + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // service x-agent-details subresource + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // service source subresource + {FileName: testRevisionListFile, RespCode: http.StatusOK}, // GET revision list (updateAPI path) + {FileName: testRevisionFile, RespCode: http.StatusOK}, // GET revision count + {FileName: testRevisionFile, RespCode: http.StatusOK}, // GET revision by name + {FileName: testRevisionFile, RespCode: http.StatusOK}, // PUT revision + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // revision x-agent-details subresource + {FileName: testInstanceFile, RespCode: http.StatusCreated}, // POST instance + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // instance x-agent-details subresource + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes update + }, + }, + "existing service: revision-only skips service update and instance, creates revision only": { + revisionOnly: true, + existingSvc: createAPIService(serviceBody.APIName, serviceBody.RestAPIID, "", "", false), + responses: []api.MockResponse{ + {FileName: testRevisionListFile, RespCode: http.StatusOK}, // GET revision list (updateAPI path) + {FileName: testRevisionFile, RespCode: http.StatusCreated}, // POST revision (addAPI path, no list fetch) + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // revision x-agent-details subresource + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes update + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + client, httpClient := GetTestServiceClient() + httpClient.SetResponses(tc.responses) + + if tc.existingSvc != nil { + ri, _ := tc.existingSvc.AsInstance() + client.caches.AddAPIService(ri) + } + + b := NewServiceBodyBuilder(). + SetAPIName(serviceBody.APIName). + SetDocumentation(serviceBody.Documentation). + SetImage(serviceBody.Image). + SetImageContentType(serviceBody.ImageContentType). + SetResourceType(serviceBody.ResourceType). + SetID(serviceBody.RestAPIID). + SetAPISpec(oas2Bytes) + if tc.revisionOnly { + b = b.SetRevisionOnly() + } + body, err := b.Build() + assert.Nil(t, err) + + apiSvc, err := client.PublishService(&body) + assert.Nil(t, err) + assert.NotNil(t, apiSvc) + }) + } +} + +func TestGetAPIServiceFromCache(t *testing.T) { cloneServiceBody := serviceBody cloneServiceBody.APIName = "fake-name" cloneServiceBody.RestAPIID = "123" @@ -240,40 +344,39 @@ func TestUpdateService(t *testing.T) { // tests for updating existing revision httpClient.SetResponses([]api.MockResponse{ { - FileName: "./testdata/apiservice.json", // for call to update the service + FileName: testAPIServiceFile, // for call to update the service RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // for call to update the service subresource + FileName: testAPIServiceFile, // for call to update the service subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevision.json", // for call to update the service subresource + FileName: testRevisionFile, // for call to update the service subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision + FileName: testRevisionFile, // for call to update the serviceRevision RespCode: http.StatusOK, }, { - FileName: "./testdata/serviceinstance.json", // for call to update the serviceRevision subresource + FileName: testInstanceFile, // for call to update the serviceRevision subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/serviceinstance.json", // for call to update the serviceRevision subresource + FileName: testInstanceFile, // for call to update the serviceRevision subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // for call to update the service subresource + FileName: testAPIServiceFile, // for call to update the service subresource RespCode: http.StatusOK, }, }) cloneServiceBody := serviceBody cloneServiceBody.APIUpdateSeverity = "MINOR" - oas2Json, _ := os.Open("./testdata/petstore-swagger2.json") // OAS2 - defer oas2Json.Close() - oas2Bytes, _ := io.ReadAll(oas2Json) + oas2Bytes, err := os.ReadFile(testPetstoreSpec) + assert.Nil(t, err) cloneServiceBody.SpecDefinition = oas2Bytes apiSvc, err := client.PublishService(&cloneServiceBody) assert.Nil(t, err) @@ -284,46 +387,45 @@ func TestUpdateService(t *testing.T) { // tests for updating existing instance with same endpoint httpClient.SetResponses([]api.MockResponse{ { - FileName: "./testdata/apiservice.json", // for call to update the service + FileName: testAPIServiceFile, // for call to update the service RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevisionlist.json", // for call to update the service subresource + FileName: testRevisionListFile, // for call to update the service subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevision.json", // for call to get the serviceRevision count + FileName: testRevisionFile, // for call to get the serviceRevision count RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevision.json", // for call to get the serviceRevision count based on name + FileName: testRevisionFile, // for call to get the serviceRevision count based on name RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision + FileName: testRevisionFile, // for call to update the serviceRevision RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision subresource + FileName: testRevisionFile, // for call to update the serviceRevision subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/serviceinstance.json", // for call to update the serviceinstance + FileName: testInstanceFile, // for call to update the serviceinstance RespCode: http.StatusOK, }, { - FileName: "./testdata/serviceinstance.json", // for call to update the serviceinstance + FileName: testInstanceFile, // for call to update the serviceinstance RespCode: http.StatusOK, }, { - FileName: "./testdata/serviceinstance.json", // for call to update the serviceinstance subresource + FileName: testInstanceFile, // for call to update the serviceinstance subresource RespCode: http.StatusOK, }, }) // Test oas2 object - oas2Json, _ = os.Open("./testdata/petstore-swagger2.json") // OAS2 - defer oas2Json.Close() - oas2Bytes, _ = io.ReadAll(oas2Json) + oas2Bytes, err = os.ReadFile(testPetstoreSpec) + assert.Nil(t, err) cloneServiceBody = serviceBody cloneServiceBody.SpecDefinition = oas2Bytes @@ -332,7 +434,7 @@ func TestUpdateService(t *testing.T) { assert.NotNil(t, apiSvc) } -func Test_PublishServiceError(t *testing.T) { +func TestPublishServiceError(t *testing.T) { client, httpClient := GetTestServiceClient() // this is a failure test @@ -347,11 +449,8 @@ func Test_PublishServiceError(t *testing.T) { assert.Nil(t, apiSvc) } -func Test_processRevision(t *testing.T) { - client, httpClient := GetTestServiceClient() - - testCases := map[string]struct { - skip bool +func TestProcessRevision(t *testing.T) { + tests := map[string]struct { httpResponses []api.MockResponse serviceBody ServiceBody expectedRevName string @@ -359,27 +458,27 @@ func Test_processRevision(t *testing.T) { "publish new revision": { httpResponses: []api.MockResponse{ { - FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision + FileName: testRevisionFile, // for call to update the serviceRevision RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision x-agent-details + FileName: testRevisionFile, // for call to update the serviceRevision x-agent-details RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision + FileName: testRevisionFile, // for call to update the serviceRevision RespCode: http.StatusOK, }, { - FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision x-agent-details + FileName: testRevisionFile, // for call to update the serviceRevision x-agent-details RespCode: http.StatusOK, }, }, serviceBody: ServiceBody{ APIName: "daleapi", - Documentation: []byte("\"docs\""), + Documentation: []byte(testDocs), Image: "abcde", - ImageContentType: "image/jpeg", + ImageContentType: testImageContentType, ResourceType: Oas2, RestAPIID: "12345", }, @@ -388,19 +487,22 @@ func Test_processRevision(t *testing.T) { "skip publish when no changes": { httpResponses: []api.MockResponse{ { - RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]}]`, + RespData: `[{"name": "daleapi","tags": []}]`, RespCode: http.StatusOK, + RespHeaders: map[string][]string{ + "X-Axway-Total-Count": {"1"}, + }, }, }, serviceBody: ServiceBody{ APIName: "daleapi", - Documentation: []byte("\"docs\""), + Documentation: []byte(testDocs), Image: "abcde", - ImageContentType: "image/jpeg", + ImageContentType: testImageContentType, ResourceType: Oas2, RestAPIID: "12345", specHash: "abc123", - specHashes: map[string]interface{}{ + specHashes: map[string]string{ "abc123": "daleapi", }, serviceContext: serviceContext{ @@ -412,63 +514,66 @@ func Test_processRevision(t *testing.T) { "skip publish when previous revision found": { httpResponses: []api.MockResponse{ { - RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]},{"name": "daleapi-1","tags": ["tag1","tag2"]}]`, + RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]}]`, RespCode: http.StatusOK, + RespHeaders: map[string][]string{ + "X-Axway-Total-Count": {"1"}, + }, }, }, serviceBody: ServiceBody{ APIName: "daleapi", - Documentation: []byte("\"docs\""), + Documentation: []byte(testDocs), Image: "abcde", - ImageContentType: "image/jpeg", + ImageContentType: testImageContentType, ResourceType: Oas2, RestAPIID: "12345", specHash: "abc123", - specHashes: map[string]interface{}{ - "abc123": "daleapi-1", + specHashes: map[string]string{ + "abc123": testRevisionNameAlt, }, serviceContext: serviceContext{ serviceAction: updateAPI, }, }, - expectedRevName: "daleapi-1", + expectedRevName: testRevisionNameAlt, }, "find revision match using original spec hash": { httpResponses: []api.MockResponse{ { - RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]},{"name": "daleapi-1","tags": ["tag1","tag2"]}]`, + RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]}]`, RespCode: http.StatusOK, + RespHeaders: map[string][]string{ + "X-Axway-Total-Count": {"1"}, + }, }, }, serviceBody: ServiceBody{ APIName: "daleapi", - Documentation: []byte("\"docs\""), + Documentation: []byte(testDocs), Image: "abcde", - ImageContentType: "image/jpeg", + ImageContentType: testImageContentType, ResourceType: Oas2, RestAPIID: "12345", specHash: "abc1234", originalSpecHash: "abc123", - specHashes: map[string]interface{}{ - "abc123": "daleapi-1", + specHashes: map[string]string{ + "abc123": testRevisionNameAlt, }, serviceContext: serviceContext{ serviceAction: updateAPI, }, }, - expectedRevName: "daleapi-1", + expectedRevName: testRevisionNameAlt, }, } - for name, tc := range testCases { + for name, tc := range tests { t.Run(name, func(t *testing.T) { - if tc.skip { - return - } - // tests for updating existing revision + client, httpClient := GetTestServiceClient() httpClient.SetResponses(tc.httpResponses) client.processRevision(&tc.serviceBody) - assert.NotEqual(t, "", tc.serviceBody.serviceContext.revisionName) + assert.NotEmpty(t, tc.serviceBody.serviceContext.revisionName) assert.Equal(t, tc.expectedRevName, tc.serviceBody.serviceContext.revisionName) }) } @@ -504,7 +609,7 @@ func TestDeleteServiceByAPIID(t *testing.T) { assert.Nil(t, err) } -func TestServiceClient_buildAPIService(t *testing.T) { +func TestBuildAPIService(t *testing.T) { body := &ServiceBody{ Description: "description", ImageContentType: "content-type", @@ -571,7 +676,7 @@ func TestServiceClient_buildAPIService(t *testing.T) { assert.NotContains(t, sub, "subresource_revision_key") } -func TestServiceClient_updateAPIService(t *testing.T) { +func TestUpdateAPIService(t *testing.T) { body := &ServiceBody{ Description: "description", ImageContentType: "content-type", @@ -655,7 +760,7 @@ func TestServiceClient_updateAPIService(t *testing.T) { assert.NotContains(t, sub, "subresource_revision_key") } -func Test_buildAPIServiceNilAttributes(t *testing.T) { +func TestBuildAPIServiceNilAttributes(t *testing.T) { client, _ := GetTestServiceClient() body := &ServiceBody{} @@ -676,6 +781,9 @@ func createAPIService(name, id string, refSvc string, dpType string, isDesign bo defs.XAgentDetails: map[string]interface{}{ defs.AttrExternalAPIID: id, defs.AttrExternalAPIName: name, + "specHashes": map[string]string{ + "10422419514905716117": "revision1", + }, }, }, }, @@ -720,15 +828,15 @@ func TestServiceSourceUpdates(t *testing.T) { managedDataplane: AWS, apiserverResponses: []api.MockResponse{ { - FileName: "./testdata/apiservice.json", // call to create the service + FileName: testAPIServiceFile, // call to create the service RespCode: http.StatusCreated, }, { - FileName: "./testdata/apiservice.json", // call to update x-agent-details subresource + FileName: testAPIServiceFile, // call to update x-agent-details subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // call to update source subresource + FileName: testAPIServiceFile, // call to update source subresource RespCode: http.StatusOK, }, }, @@ -739,15 +847,15 @@ func TestServiceSourceUpdates(t *testing.T) { designDataplane: GitLab, apiserverResponses: []api.MockResponse{ { - FileName: "./testdata/apiservice.json", // call to create the service + FileName: testAPIServiceFile, // call to create the service RespCode: http.StatusCreated, }, { - FileName: "./testdata/apiservice.json", // call to update x-agent-details subresource + FileName: testAPIServiceFile, // call to update x-agent-details subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // call to update source subresource + FileName: testAPIServiceFile, // call to update source subresource RespCode: http.StatusOK, }, }, @@ -759,15 +867,15 @@ func TestServiceSourceUpdates(t *testing.T) { referenceService: "refSvc", apiserverResponses: []api.MockResponse{ { - FileName: "./testdata/apiservice.json", // call to create the service + FileName: testAPIServiceFile, // call to create the service RespCode: http.StatusCreated, }, { - FileName: "./testdata/apiservice.json", // call to update x-agent-details subresource + FileName: testAPIServiceFile, // call to update x-agent-details subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // call to update source subresource + FileName: testAPIServiceFile, // call to update source subresource RespCode: http.StatusOK, }, }, @@ -779,15 +887,15 @@ func TestServiceSourceUpdates(t *testing.T) { existingSvc: createAPIService("existingSvcNoSource", "existingSvcNoSource", "", "", false), apiserverResponses: []api.MockResponse{ { - FileName: "./testdata/apiservice.json", // call to update the service + FileName: testAPIServiceFile, // call to update the service RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // call to update x-agent-details subresource + FileName: testAPIServiceFile, // call to update x-agent-details subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // call to update source subresource + FileName: testAPIServiceFile, // call to update source subresource RespCode: http.StatusOK, }, }, @@ -799,15 +907,15 @@ func TestServiceSourceUpdates(t *testing.T) { existingSvc: createAPIService("existingSvcDiffDpType", "existingSvcDiffDpType", "", Unidentified.String(), false), apiserverResponses: []api.MockResponse{ { - FileName: "./testdata/apiservice.json", // call to update the service + FileName: testAPIServiceFile, // call to update the service RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // call to update x-agent-details subresource + FileName: testAPIServiceFile, // call to update x-agent-details subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // call to update source subresource + FileName: testAPIServiceFile, // call to update source subresource RespCode: http.StatusOK, }, }, @@ -820,15 +928,15 @@ func TestServiceSourceUpdates(t *testing.T) { referenceService: "newRefSvc", apiserverResponses: []api.MockResponse{ { - FileName: "./testdata/apiservice.json", // call to update the service + FileName: testAPIServiceFile, // call to update the service RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // call to update x-agent-details subresource + FileName: testAPIServiceFile, // call to update x-agent-details subresource RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // call to update source subresource + FileName: testAPIServiceFile, // call to update source subresource RespCode: http.StatusOK, }, }, @@ -841,11 +949,11 @@ func TestServiceSourceUpdates(t *testing.T) { referenceService: "refSvc", apiserverResponses: []api.MockResponse{ { - FileName: "./testdata/apiservice.json", // call to update the service + FileName: testAPIServiceFile, // call to update the service RespCode: http.StatusOK, }, { - FileName: "./testdata/apiservice.json", // call to update x-agent-details subresource + FileName: testAPIServiceFile, // call to update x-agent-details subresource RespCode: http.StatusOK, }, // no source subresource update diff --git a/pkg/apic/apiserviceinstance.go b/pkg/apic/apiserviceinstance.go index 85a264c5f..7ed5c5c02 100644 --- a/pkg/apic/apiserviceinstance.go +++ b/pkg/apic/apiserviceinstance.go @@ -179,6 +179,9 @@ func buildAPIServiceInstanceLifecycleSubResource(instance *management.APIService // processInstance - Creates or updates an API Service Instance based on the current API Service Revision. func (c *ServiceClient) processInstance(serviceBody *ServiceBody) error { + if serviceBody.IsRevisionOnly() { + return nil + } endpoints, err := createInstanceEndpoint(serviceBody.Endpoints) if err != nil { return err diff --git a/pkg/apic/apiservicerevision.go b/pkg/apic/apiservicerevision.go index 235ca4c9d..d29640cea 100644 --- a/pkg/apic/apiservicerevision.go +++ b/pkg/apic/apiservicerevision.go @@ -18,7 +18,6 @@ import ( coreapi "github.com/Axway/agent-sdk/pkg/api" utilerrors "github.com/Axway/agent-sdk/pkg/util/errors" - v1 "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/api/v1" management "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/management/v1alpha1" "github.com/Axway/agent-sdk/pkg/util/log" ) @@ -75,13 +74,8 @@ func (c *ServiceClient) processRevision(serviceBody *ServiceBody) error { logProcess = "Updating" } - apiServiceRevisions, err := c.getRevisionsIfUpdating(serviceBody) - if err != nil { - return err - } - // check if a revision with the same hash was already published and update tags if needed - found, err := c.checkAndUpdateExistingRevision(serviceBody, apiServiceRevisions) + found, err := c.checkAndUpdateExistingRevision(serviceBody) if err != nil { return err } @@ -146,23 +140,8 @@ func (c *ServiceClient) getRevisions(queryString string) ([]*management.APIServi return apiServiceRevisions, count, nil } -// getRevisionsIfUpdating returns revisions when the service action is an update -func (c *ServiceClient) getRevisionsIfUpdating(serviceBody *ServiceBody) ([]*management.APIServiceRevision, error) { - apiServiceRevisions := make([]*management.APIServiceRevision, 0) - if serviceBody.serviceContext.serviceAction == updateAPI { - // get current revisions - revisions, totalCount, err := c.getRevisions("metadata.references.id==" + serviceBody.serviceContext.serviceID) - if err != nil { - return nil, err - } - serviceBody.serviceContext.revisionCount = totalCount - apiServiceRevisions = revisions - } - return apiServiceRevisions, nil -} - // checkAndUpdateExistingRevision checks if a revision with the same hash exists and updates tags if needed -func (c *ServiceClient) checkAndUpdateExistingRevision(serviceBody *ServiceBody, apiServiceRevisions []*management.APIServiceRevision) (bool, error) { +func (c *ServiceClient) checkAndUpdateExistingRevision(serviceBody *ServiceBody) (bool, error) { // attempt to use the stripped spec hash revName, found := serviceBody.specHashes[serviceBody.specHash] if !found && serviceBody.originalSpecHash != "" { @@ -172,39 +151,26 @@ func (c *ServiceClient) checkAndUpdateExistingRevision(serviceBody *ServiceBody, revName, found = serviceBody.specHashes[serviceBody.originalSpecHash] } - if !found { + if !found || revName == "" { return false, nil } - name := revName.(string) - - // check to see if the tags have changed from the latest - for _, apiServiceRevision := range apiServiceRevisions { - if apiServiceRevision.Name != name { - continue - } - - serviceBody.serviceContext.revisionName = name - - updatedTags := c.getUpdatedTagKeys(serviceBody.Tags, apiServiceRevision.Tags) - if len(updatedTags) == 0 { - return true, nil - } + // get the revision by name to compare tags and update if needed + revisions, totalCount, err := c.getRevisions(fmt.Sprintf("name==%s;metadata.references.id==%s", revName, serviceBody.serviceContext.serviceID)) + if err != nil { + return false, err + } - updatedRevision := c.buildAPIServiceRevision(serviceBody) - updatedRevision.Name = apiServiceRevision.Name - updatedRevision.Metadata.Scope = v1.MetadataScope{ - Kind: management.EnvironmentGVK().Kind, - Name: c.cfg.GetEnvironmentName(), - } - _, err := c.UpdateResourceInstance(updatedRevision) - if err != nil { - return false, err - } + if totalCount == 0 { + return false, nil + } - return true, nil + if len(c.getUpdatedTagKeys(serviceBody.Tags, revisions[0].Tags)) != 0 { + return false, nil } - return false, nil + + serviceBody.serviceContext.revisionName = revName + return true, nil } // verify last revision tags against the serviceBody tags that are coming in to see if they are equal or not. If they are not, return an empty[]. If they are diff --git a/pkg/apic/servicebody.go b/pkg/apic/servicebody.go index fa398b5bb..8c4a5c06c 100644 --- a/pkg/apic/servicebody.go +++ b/pkg/apic/servicebody.go @@ -60,8 +60,9 @@ type ServiceBody struct { specHash string specVersion string accessRequestDefinition *management.AccessRequestDefinition - specHashes map[string]interface{} // map of hash values to revision names - requestDefinitionsAllowed bool // used to validate if the instance can have request definitions or not. Use case example - v7 unpublished, remove request definitions + specHashes map[string]string // map of hash values to revision names + requestDefinitionsAllowed bool // used to validate if the instance can have request definitions or not. Use case example - v7 unpublished, remove request definitions + revisionOnly bool dataplaneType DataplaneType isDesignDataplane bool referencedServiceName string @@ -163,6 +164,10 @@ func (s *ServiceBody) IsDesignDataplane() bool { return s.isDesignDataplane } +func (s *ServiceBody) IsRevisionOnly() bool { + return s.revisionOnly +} + func (s *ServiceBody) GetReferencedServiceName() string { return s.referencedServiceName } diff --git a/pkg/apic/servicebuilder.go b/pkg/apic/servicebuilder.go index c4bca3a8b..5b8367f35 100644 --- a/pkg/apic/servicebuilder.go +++ b/pkg/apic/servicebuilder.go @@ -71,6 +71,7 @@ type ServiceBuilder interface { SetReferenceServiceName(serviceName, envName string) ServiceBuilder SetReferenceInstanceName(instanceName, envName string) ServiceBuilder SetInstanceLifecycle(stage, releaseState, message string) ServiceBuilder + SetRevisionOnly() ServiceBuilder Build() (ServiceBody, error) } @@ -482,3 +483,8 @@ func (b *serviceBodyBuilder) SetInstanceLifecycle(stage, releaseState, message s } return b } + +func (b *serviceBodyBuilder) SetRevisionOnly() ServiceBuilder { + b.serviceBody.revisionOnly = true + return b +} diff --git a/pkg/apic/servicebuilder_test.go b/pkg/apic/servicebuilder_test.go index 6b73ea9f1..aea68196f 100644 --- a/pkg/apic/servicebuilder_test.go +++ b/pkg/apic/servicebuilder_test.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/assert" ) +const invalidSpec = `{"test":"123"}` + const longDescription = `This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! You can now help us improve the API whether it's by making changes to the definition itself or to the code. @@ -171,24 +173,52 @@ func TestServiceBodySetters(t *testing.T) { assert.NotNil(t, sb) } +func TestSetRevisionOnly(t *testing.T) { + tests := map[string]struct { + callSetter bool + want bool + }{ + "defaults to false": { + callSetter: false, + want: false, + }, + "SetRevisionOnly sets flag": { + callSetter: true, + want: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + b := NewServiceBodyBuilder().SetResourceType(Unstructured).SetAPISpec([]byte{}) + if tc.callSetter { + b = b.SetRevisionOnly() + } + sb, err := b.Build() + assert.Nil(t, err) + assert.Equal(t, tc.want, sb.IsRevisionOnly()) + }) + } +} + func TestServiceBodyWithParseError(t *testing.T) { serviceBuilder := NewServiceBodyBuilder() - _, err := serviceBuilder.SetResourceType(Oas3).SetAPISpec([]byte("{\"test\":\"123\"}")).Build() + _, err := serviceBuilder.SetResourceType(Oas3).SetAPISpec([]byte(invalidSpec)).Build() assert.NotNil(t, err) - _, err = serviceBuilder.SetResourceType(Oas2).SetAPISpec([]byte("{\"test\":\"123\"}")).Build() + _, err = serviceBuilder.SetResourceType(Oas2).SetAPISpec([]byte(invalidSpec)).Build() assert.NotNil(t, err) - _, err = serviceBuilder.SetResourceType(Wsdl).SetAPISpec([]byte("{\"test\":\"123\"}")).Build() + _, err = serviceBuilder.SetResourceType(Wsdl).SetAPISpec([]byte(invalidSpec)).Build() assert.NotNil(t, err) - _, err = serviceBuilder.SetResourceType(Protobuf).SetAPISpec([]byte("{\"test\":\"123\"}")).Build() + _, err = serviceBuilder.SetResourceType(Protobuf).SetAPISpec([]byte(invalidSpec)).Build() assert.NotNil(t, err) - _, err = serviceBuilder.SetResourceType(AsyncAPI).SetAPISpec([]byte("{\"test\":\"123\"}")).Build() + _, err = serviceBuilder.SetResourceType(AsyncAPI).SetAPISpec([]byte(invalidSpec)).Build() assert.NotNil(t, err) - _, err = serviceBuilder.SetResourceType(Unstructured).SetAPISpec([]byte("{\"test\":\"123\"}")).Build() + _, err = serviceBuilder.SetResourceType(Unstructured).SetAPISpec([]byte(invalidSpec)).Build() assert.Nil(t, err) }