From d2abf72b504b1235f619b15ba0cc7120d55d8c3a Mon Sep 17 00:00:00 2001 From: Shane Bolosan Date: Tue, 12 May 2026 15:53:09 -0700 Subject: [PATCH 1/9] APIGOV-32662 - add SetPublishRevisionOnly to ServiceBuilder to skip instance processing for loser proxies --- pkg/apic/apiservice_test.go | 57 +++++++++++++++++++++++++++++++++ pkg/apic/apiserviceinstance.go | 3 ++ pkg/apic/servicebody.go | 1 + pkg/apic/servicebuilder.go | 6 ++++ pkg/apic/servicebuilder_test.go | 27 ++++++++++++++++ 5 files changed, 94 insertions(+) diff --git a/pkg/apic/apiservice_test.go b/pkg/apic/apiservice_test.go index 9bb2c3b6b..bb0b5b69e 100644 --- a/pkg/apic/apiservice_test.go +++ b/pkg/apic/apiservice_test.go @@ -175,6 +175,63 @@ func TestCreateService(t *testing.T) { assert.Nil(t, apiSvc) } +func TestPublishServiceRevisionOnly(t *testing.T) { + oas2Json, _ := os.Open("./testdata/petstore-swagger2.json") + defer oas2Json.Close() + oas2Bytes, _ := io.ReadAll(oas2Json) + + // Mirrors the happy-path sequence in TestCreateService (cache lookup is in-memory, no HTTP). + fullResponses := []api.MockResponse{ + {FileName: "./testdata/apiservice.json", RespCode: http.StatusCreated}, // POST service + {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // service subresource + {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // spec hashes + {FileName: "./testdata/servicerevision.json", RespCode: http.StatusCreated}, // POST revision + {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // revision subresource + {FileName: "./testdata/serviceinstance.json", RespCode: http.StatusCreated}, // POST instance + {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // instance subresource + {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // spec hashes update + } + + // Same as above minus the POST instance and its subresource call. + revisionOnlyResponses := []api.MockResponse{ + {FileName: "./testdata/apiservice.json", RespCode: http.StatusCreated}, // POST service + {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // service subresource + {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // spec hashes + {FileName: "./testdata/servicerevision.json", RespCode: http.StatusCreated}, // POST revision + {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // revision subresource + {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // spec hashes update + } + + tests := map[string]struct { + revisionOnly bool + responses []api.MockResponse + }{ + "normal publish creates service, revision, and instance": { + revisionOnly: false, + responses: fullResponses, + }, + "revision only publish skips instance creation": { + revisionOnly: true, + responses: revisionOnlyResponses, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + client, httpClient := GetTestServiceClient() + httpClient.SetResponses(tc.responses) + + body := serviceBody + body.SpecDefinition = oas2Bytes + body.publishRevisionOnly = tc.revisionOnly + + apiSvc, err := client.PublishService(&body) + assert.Nil(t, err) + assert.NotNil(t, apiSvc) + }) + } +} + func Test_getAPIServiceFromCache(t *testing.T) { cloneServiceBody := serviceBody cloneServiceBody.APIName = "fake-name" diff --git a/pkg/apic/apiserviceinstance.go b/pkg/apic/apiserviceinstance.go index 85a264c5f..d22e5df4c 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.publishRevisionOnly { + return nil + } endpoints, err := createInstanceEndpoint(serviceBody.Endpoints) if err != nil { return err diff --git a/pkg/apic/servicebody.go b/pkg/apic/servicebody.go index fa398b5bb..e86218258 100644 --- a/pkg/apic/servicebody.go +++ b/pkg/apic/servicebody.go @@ -62,6 +62,7 @@ type ServiceBody struct { 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 + publishRevisionOnly bool // when true, skip instance processing (loser proxies that share a service with a published winner) dataplaneType DataplaneType isDesignDataplane bool referencedServiceName string diff --git a/pkg/apic/servicebuilder.go b/pkg/apic/servicebuilder.go index c4bca3a8b..d77b1a7ae 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 + SetPublishRevisionOnly() ServiceBuilder Build() (ServiceBody, error) } @@ -482,3 +483,8 @@ func (b *serviceBodyBuilder) SetInstanceLifecycle(stage, releaseState, message s } return b } + +func (b *serviceBodyBuilder) SetPublishRevisionOnly() ServiceBuilder { + b.serviceBody.publishRevisionOnly = true + return b +} diff --git a/pkg/apic/servicebuilder_test.go b/pkg/apic/servicebuilder_test.go index 6b73ea9f1..c3e229493 100644 --- a/pkg/apic/servicebuilder_test.go +++ b/pkg/apic/servicebuilder_test.go @@ -171,6 +171,33 @@ func TestServiceBodySetters(t *testing.T) { assert.NotNil(t, sb) } +func TestSetPublishRevisionOnly(t *testing.T) { + tests := map[string]struct { + setRevisionOnly bool + wantRevisionOnly bool + }{ + "default is false": { + setRevisionOnly: false, + wantRevisionOnly: false, + }, + "set publishes revision only": { + setRevisionOnly: true, + wantRevisionOnly: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + b := NewServiceBodyBuilder() + if tc.setRevisionOnly { + b = b.SetPublishRevisionOnly() + } + sb, _ := b.Build() + assert.Equal(t, tc.wantRevisionOnly, sb.publishRevisionOnly) + }) + } +} + func TestServiceBodyWithParseError(t *testing.T) { serviceBuilder := NewServiceBodyBuilder() _, err := serviceBuilder.SetResourceType(Oas3).SetAPISpec([]byte("{\"test\":\"123\"}")).Build() From d1a7394daacc4afb31c988af34c1141de20259db Mon Sep 17 00:00:00 2001 From: Shane Bolosan Date: Wed, 13 May 2026 09:29:29 -0700 Subject: [PATCH 2/9] =?UTF-8?q?APIGOV-32662=20=E2=80=94=20Add=20SetRevisio?= =?UTF-8?q?nOnly=20to=20ServiceBuilder=20to=20skip=20instance=20processing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/apic/apiservice_test.go | 260 ++++++++++++++++---------------- pkg/apic/apiserviceinstance.go | 2 +- pkg/apic/servicebody.go | 6 +- pkg/apic/servicebuilder.go | 6 +- pkg/apic/servicebuilder_test.go | 43 +++--- 5 files changed, 161 insertions(+), 156 deletions(-) diff --git a/pkg/apic/apiservice_test.go b/pkg/apic/apiservice_test.go index bb0b5b69e..c2b1d38e6 100644 --- a/pkg/apic/apiservice_test.go +++ b/pkg/apic/apiservice_test.go @@ -2,11 +2,12 @@ package apic import ( "fmt" - "io" "net/http" "os" "testing" + "github.com/stretchr/testify/require" + "github.com/Axway/agent-sdk/pkg/util" defs "github.com/Axway/agent-sdk/pkg/apic/definitions" @@ -17,11 +18,24 @@ import ( "github.com/stretchr/testify/assert" ) +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 +54,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 +102,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 +117,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 +140,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 +167,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, }, { @@ -176,43 +189,36 @@ func TestCreateService(t *testing.T) { } func TestPublishServiceRevisionOnly(t *testing.T) { - oas2Json, _ := os.Open("./testdata/petstore-swagger2.json") - defer oas2Json.Close() - oas2Bytes, _ := io.ReadAll(oas2Json) - - // Mirrors the happy-path sequence in TestCreateService (cache lookup is in-memory, no HTTP). - fullResponses := []api.MockResponse{ - {FileName: "./testdata/apiservice.json", RespCode: http.StatusCreated}, // POST service - {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // service subresource - {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // spec hashes - {FileName: "./testdata/servicerevision.json", RespCode: http.StatusCreated}, // POST revision - {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // revision subresource - {FileName: "./testdata/serviceinstance.json", RespCode: http.StatusCreated}, // POST instance - {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // instance subresource - {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // spec hashes update - } - - // Same as above minus the POST instance and its subresource call. - revisionOnlyResponses := []api.MockResponse{ - {FileName: "./testdata/apiservice.json", RespCode: http.StatusCreated}, // POST service - {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // service subresource - {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // spec hashes - {FileName: "./testdata/servicerevision.json", RespCode: http.StatusCreated}, // POST revision - {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // revision subresource - {FileName: "./testdata/agent-details-sr.json", RespCode: http.StatusOK}, // spec hashes update - } + oas2Bytes, err := os.ReadFile(testPetstoreSpec) + require.NoError(t, err) tests := map[string]struct { revisionOnly bool responses []api.MockResponse }{ - "normal publish creates service, revision, and instance": { + "full publish creates service, revision, and instance": { revisionOnly: false, - responses: fullResponses, + responses: []api.MockResponse{ + {FileName: testAPIServiceFile, RespCode: http.StatusCreated}, // POST service + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // service subresource + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes + {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 + }, }, - "revision only publish skips instance creation": { + "revision-only publish skips instance creation": { revisionOnly: true, - responses: revisionOnlyResponses, + responses: []api.MockResponse{ + {FileName: testAPIServiceFile, RespCode: http.StatusCreated}, // POST service + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // service subresource + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes + {FileName: testRevisionFile, RespCode: http.StatusCreated}, // POST revision + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // revision subresource + {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes update + }, }, } @@ -223,7 +229,7 @@ func TestPublishServiceRevisionOnly(t *testing.T) { body := serviceBody body.SpecDefinition = oas2Bytes - body.publishRevisionOnly = tc.revisionOnly + body.revisionOnly = tc.revisionOnly apiSvc, err := client.PublishService(&body) assert.Nil(t, err) @@ -232,7 +238,7 @@ func TestPublishServiceRevisionOnly(t *testing.T) { } } -func Test_getAPIServiceFromCache(t *testing.T) { +func TestGetAPIServiceFromCache(t *testing.T) { cloneServiceBody := serviceBody cloneServiceBody.APIName = "fake-name" cloneServiceBody.RestAPIID = "123" @@ -297,40 +303,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) @@ -341,46 +346,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 @@ -389,7 +393,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 @@ -404,11 +408,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 @@ -416,27 +417,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", }, @@ -451,9 +452,9 @@ func Test_processRevision(t *testing.T) { }, serviceBody: ServiceBody{ APIName: "daleapi", - Documentation: []byte("\"docs\""), + Documentation: []byte(testDocs), Image: "abcde", - ImageContentType: "image/jpeg", + ImageContentType: testImageContentType, ResourceType: Oas2, RestAPIID: "12345", specHash: "abc123", @@ -469,63 +470,60 @@ 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"]},{"name": "` + testRevisionNameAlt + `","tags": ["tag1","tag2"]}]`, RespCode: http.StatusOK, }, }, 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", + "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"]},{"name": "` + testRevisionNameAlt + `","tags": ["tag1","tag2"]}]`, RespCode: http.StatusOK, }, }, 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", + "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) }) } @@ -561,7 +559,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", @@ -628,7 +626,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", @@ -712,7 +710,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{} @@ -777,15 +775,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, }, }, @@ -796,15 +794,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, }, }, @@ -816,15 +814,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, }, }, @@ -836,15 +834,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, }, }, @@ -856,15 +854,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, }, }, @@ -877,15 +875,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, }, }, @@ -898,11 +896,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 d22e5df4c..7ed5c5c02 100644 --- a/pkg/apic/apiserviceinstance.go +++ b/pkg/apic/apiserviceinstance.go @@ -179,7 +179,7 @@ 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.publishRevisionOnly { + if serviceBody.IsRevisionOnly() { return nil } endpoints, err := createInstanceEndpoint(serviceBody.Endpoints) diff --git a/pkg/apic/servicebody.go b/pkg/apic/servicebody.go index e86218258..c8b08d558 100644 --- a/pkg/apic/servicebody.go +++ b/pkg/apic/servicebody.go @@ -62,7 +62,7 @@ type ServiceBody struct { 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 - publishRevisionOnly bool // when true, skip instance processing (loser proxies that share a service with a published winner) + revisionOnly bool dataplaneType DataplaneType isDesignDataplane bool referencedServiceName string @@ -164,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 d77b1a7ae..5b8367f35 100644 --- a/pkg/apic/servicebuilder.go +++ b/pkg/apic/servicebuilder.go @@ -71,7 +71,7 @@ type ServiceBuilder interface { SetReferenceServiceName(serviceName, envName string) ServiceBuilder SetReferenceInstanceName(instanceName, envName string) ServiceBuilder SetInstanceLifecycle(stage, releaseState, message string) ServiceBuilder - SetPublishRevisionOnly() ServiceBuilder + SetRevisionOnly() ServiceBuilder Build() (ServiceBody, error) } @@ -484,7 +484,7 @@ func (b *serviceBodyBuilder) SetInstanceLifecycle(stage, releaseState, message s return b } -func (b *serviceBodyBuilder) SetPublishRevisionOnly() ServiceBuilder { - b.serviceBody.publishRevisionOnly = true +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 c3e229493..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,51 +173,52 @@ func TestServiceBodySetters(t *testing.T) { assert.NotNil(t, sb) } -func TestSetPublishRevisionOnly(t *testing.T) { +func TestSetRevisionOnly(t *testing.T) { tests := map[string]struct { - setRevisionOnly bool - wantRevisionOnly bool + callSetter bool + want bool }{ - "default is false": { - setRevisionOnly: false, - wantRevisionOnly: false, + "defaults to false": { + callSetter: false, + want: false, }, - "set publishes revision only": { - setRevisionOnly: true, - wantRevisionOnly: true, + "SetRevisionOnly sets flag": { + callSetter: true, + want: true, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { - b := NewServiceBodyBuilder() - if tc.setRevisionOnly { - b = b.SetPublishRevisionOnly() + b := NewServiceBodyBuilder().SetResourceType(Unstructured).SetAPISpec([]byte{}) + if tc.callSetter { + b = b.SetRevisionOnly() } - sb, _ := b.Build() - assert.Equal(t, tc.wantRevisionOnly, sb.publishRevisionOnly) + 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) } From 5feb232c00f4d84f150af0c64d2ec8ac05d06255 Mon Sep 17 00:00:00 2001 From: Shane Bolosan Date: Mon, 18 May 2026 15:48:18 -0700 Subject: [PATCH 3/9] APIGOV-32662 - update tests --- pkg/apic/apiservice_test.go | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/pkg/apic/apiservice_test.go b/pkg/apic/apiservice_test.go index c2b1d38e6..86be3cc1d 100644 --- a/pkg/apic/apiservice_test.go +++ b/pkg/apic/apiservice_test.go @@ -6,16 +6,13 @@ import ( "os" "testing" - "github.com/stretchr/testify/require" - - "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" + defs "github.com/Axway/agent-sdk/pkg/apic/definitions" 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" + "github.com/Axway/agent-sdk/pkg/util" ) const ( @@ -190,7 +187,7 @@ func TestCreateService(t *testing.T) { func TestPublishServiceRevisionOnly(t *testing.T) { oas2Bytes, err := os.ReadFile(testPetstoreSpec) - require.NoError(t, err) + assert.Nil(t, err) tests := map[string]struct { revisionOnly bool @@ -227,9 +224,19 @@ func TestPublishServiceRevisionOnly(t *testing.T) { client, httpClient := GetTestServiceClient() httpClient.SetResponses(tc.responses) - body := serviceBody - body.SpecDefinition = oas2Bytes - body.revisionOnly = tc.revisionOnly + 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) From 0a29cd1da3bd2b972ceb20714af72bc4af5c7488 Mon Sep 17 00:00:00 2001 From: Shane Bolosan Date: Mon, 18 May 2026 16:25:20 -0700 Subject: [PATCH 4/9] APIGOV-32262 - update on unit test --- pkg/apic/apiservice_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/apic/apiservice_test.go b/pkg/apic/apiservice_test.go index 86be3cc1d..84749fb7b 100644 --- a/pkg/apic/apiservice_test.go +++ b/pkg/apic/apiservice_test.go @@ -196,14 +196,15 @@ func TestPublishServiceRevisionOnly(t *testing.T) { "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 subresource - {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes - {FileName: testRevisionFile, RespCode: http.StatusCreated}, // POST revision + {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: 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 }, }, "revision-only publish skips instance creation": { From 4ebdd53b0aef08b47e1604813f8bf0eabe741ccb Mon Sep 17 00:00:00 2001 From: Shane Bolosan Date: Tue, 19 May 2026 15:40:14 -0700 Subject: [PATCH 5/9] APIGOV-32662 - skip API Service update when revision only is set and service already exists --- pkg/apic/apiservice.go | 9 ++++- pkg/apic/apiservice_test.go | 69 ++++++++++++++++++++++++++++--------- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/pkg/apic/apiservice.go b/pkg/apic/apiservice.go index 2e3956b34..f1dab393e 100644 --- a/pkg/apic/apiservice.go +++ b/pkg/apic/apiservice.go @@ -195,9 +195,16 @@ func (c *ServiceClient) processService(serviceBody *ServiceBody) (*management.AP if svc != nil { serviceBody.serviceContext.serviceAction = updateAPI + c.updateAPIService(serviceBody, svc) // populates from existing service + if serviceBody.IsRevisionOnly() { + serviceBody.serviceContext.serviceName = svc.Name + serviceBody.serviceContext.serviceID = svc.Metadata.ID + ri, _ := svc.AsInstance() + c.caches.AddAPIService(ri) + return svc, nil + } httpMethod = http.MethodPut serviceURL += "/" + svc.Name - c.updateAPIService(serviceBody, svc) } else { svc = c.buildAPIService(serviceBody) } diff --git a/pkg/apic/apiservice_test.go b/pkg/apic/apiservice_test.go index 84749fb7b..be60beec3 100644 --- a/pkg/apic/apiservice_test.go +++ b/pkg/apic/apiservice_test.go @@ -191,31 +191,61 @@ func TestPublishServiceRevisionOnly(t *testing.T) { tests := map[string]struct { revisionOnly bool + existingSvc *management.APIService responses []api.MockResponse }{ - "full publish creates service, revision, and instance": { + "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 + {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 }, }, - "revision-only publish skips instance creation": { + "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 subresource - {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes - {FileName: testRevisionFile, RespCode: http.StatusCreated}, // POST revision - {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // revision subresource - {FileName: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes update + {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.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: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes update }, }, } @@ -225,6 +255,11 @@ func TestPublishServiceRevisionOnly(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). From 4e4067b6992f6b7375bcc93641190dd405210b8b Mon Sep 17 00:00:00 2001 From: Shane Bolosan Date: Tue, 19 May 2026 16:53:57 -0700 Subject: [PATCH 6/9] APIGOV-32662 - updated processService to use cached service name and ID when rev only is set, skip the service update entirely --- pkg/apic/apiservice.go | 15 ++++++--------- pkg/apic/apiservice_test.go | 9 +++------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/pkg/apic/apiservice.go b/pkg/apic/apiservice.go index f1dab393e..d30a3f76c 100644 --- a/pkg/apic/apiservice.go +++ b/pkg/apic/apiservice.go @@ -193,18 +193,15 @@ func (c *ServiceClient) processService(serviceBody *ServiceBody) (*management.AP return nil, err } - if svc != nil { + if svc != nil && serviceBody.IsRevisionOnly() { + serviceBody.serviceContext.serviceName = svc.Name + serviceBody.serviceContext.serviceID = svc.Metadata.ID + return svc, nil + } else if svc != nil { serviceBody.serviceContext.serviceAction = updateAPI - c.updateAPIService(serviceBody, svc) // populates from existing service - if serviceBody.IsRevisionOnly() { - serviceBody.serviceContext.serviceName = svc.Name - serviceBody.serviceContext.serviceID = svc.Metadata.ID - ri, _ := svc.AsInstance() - c.caches.AddAPIService(ri) - return svc, nil - } httpMethod = http.MethodPut serviceURL += "/" + svc.Name + c.updateAPIService(serviceBody, svc) } else { svc = c.buildAPIService(serviceBody) } diff --git a/pkg/apic/apiservice_test.go b/pkg/apic/apiservice_test.go index be60beec3..1c2ee00bf 100644 --- a/pkg/apic/apiservice_test.go +++ b/pkg/apic/apiservice_test.go @@ -240,12 +240,9 @@ func TestPublishServiceRevisionOnly(t *testing.T) { 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.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: testAgentDetailsFile, RespCode: http.StatusOK}, // spec hashes update + {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 }, }, } From a2a1e5e7ebcd99889af5cf2f490a954beb4b4b50 Mon Sep 17 00:00:00 2001 From: Jason Collins Date: Wed, 20 May 2026 14:24:35 -0700 Subject: [PATCH 7/9] handling of the revision only processing --- pkg/apic/apiservice.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pkg/apic/apiservice.go b/pkg/apic/apiservice.go index d30a3f76c..90199606e 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 = 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{ @@ -194,8 +200,10 @@ func (c *ServiceClient) processService(serviceBody *ServiceBody) (*management.AP } 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 From 4f8f2e932df99eec69be5f4a6ed896cbce40a4a5 Mon Sep 17 00:00:00 2001 From: Jason Collins Date: Wed, 20 May 2026 16:51:23 -0700 Subject: [PATCH 8/9] fix test for new api get call --- pkg/apic/apiservice_test.go | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/pkg/apic/apiservice_test.go b/pkg/apic/apiservice_test.go index 1c2ee00bf..e0d234e5c 100644 --- a/pkg/apic/apiservice_test.go +++ b/pkg/apic/apiservice_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/Axway/agent-sdk/pkg/api" - defs "github.com/Axway/agent-sdk/pkg/apic/definitions" apiv1 "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/api/v1" management "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/management/v1alpha1" + defs "github.com/Axway/agent-sdk/pkg/apic/definitions" "github.com/Axway/agent-sdk/pkg/util" ) @@ -223,26 +223,27 @@ func TestPublishServiceRevisionOnly(t *testing.T) { 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 + {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: 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 + {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 }, }, } From 1672b7f52fdc2e6769962c8ac034ee015d91b481 Mon Sep 17 00:00:00 2001 From: Jason Collins Date: Thu, 21 May 2026 07:40:30 -0700 Subject: [PATCH 9/9] small updates about confirming revision with specific hash still exists on engage --- pkg/api/mockhttpclient.go | 11 +++--- pkg/apic/apiservice.go | 4 +-- pkg/apic/apiservice_test.go | 24 +++++++++---- pkg/apic/apiservicerevision.go | 66 +++++++++------------------------- pkg/apic/servicebody.go | 4 +-- 5 files changed, 44 insertions(+), 65 deletions(-) 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 90199606e..1c413dcd8 100644 --- a/pkg/apic/apiservice.go +++ b/pkg/apic/apiservice.go @@ -85,7 +85,7 @@ func (c *ServiceClient) setSpecHashesOnServiceBody(serviceBody *ServiceBody, svc agentDetails := util.GetAgentDetails(svc) if revDetails, found := agentDetails[specHashes]; found { if specHashes, ok := revDetails.(map[string]interface{}); ok { - serviceBody.specHashes = specHashes + serviceBody.specHashes = util.MapStringInterfaceToStringString(specHashes) } } @@ -191,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) diff --git a/pkg/apic/apiservice_test.go b/pkg/apic/apiservice_test.go index e0d234e5c..380d2935e 100644 --- a/pkg/apic/apiservice_test.go +++ b/pkg/apic/apiservice_test.go @@ -487,8 +487,11 @@ func TestProcessRevision(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{ @@ -499,7 +502,7 @@ func TestProcessRevision(t *testing.T) { ResourceType: Oas2, RestAPIID: "12345", specHash: "abc123", - specHashes: map[string]interface{}{ + specHashes: map[string]string{ "abc123": "daleapi", }, serviceContext: serviceContext{ @@ -511,8 +514,11 @@ func TestProcessRevision(t *testing.T) { "skip publish when previous revision found": { httpResponses: []api.MockResponse{ { - RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]},{"name": "` + testRevisionNameAlt + `","tags": ["tag1","tag2"]}]`, + RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]}]`, RespCode: http.StatusOK, + RespHeaders: map[string][]string{ + "X-Axway-Total-Count": {"1"}, + }, }, }, serviceBody: ServiceBody{ @@ -523,7 +529,7 @@ func TestProcessRevision(t *testing.T) { ResourceType: Oas2, RestAPIID: "12345", specHash: "abc123", - specHashes: map[string]interface{}{ + specHashes: map[string]string{ "abc123": testRevisionNameAlt, }, serviceContext: serviceContext{ @@ -535,8 +541,11 @@ func TestProcessRevision(t *testing.T) { "find revision match using original spec hash": { httpResponses: []api.MockResponse{ { - RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]},{"name": "` + testRevisionNameAlt + `","tags": ["tag1","tag2"]}]`, + RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]}]`, RespCode: http.StatusOK, + RespHeaders: map[string][]string{ + "X-Axway-Total-Count": {"1"}, + }, }, }, serviceBody: ServiceBody{ @@ -548,7 +557,7 @@ func TestProcessRevision(t *testing.T) { RestAPIID: "12345", specHash: "abc1234", originalSpecHash: "abc123", - specHashes: map[string]interface{}{ + specHashes: map[string]string{ "abc123": testRevisionNameAlt, }, serviceContext: serviceContext{ @@ -772,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", + }, }, }, }, 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 c8b08d558..8c4a5c06c 100644 --- a/pkg/apic/servicebody.go +++ b/pkg/apic/servicebody.go @@ -60,8 +60,8 @@ 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