Skip to content

Commit 2a298fc

Browse files
APIGOV-32644 - stop publishing servers when possible (#1034)
* compare string slices unordered lists * when strip servers is set calculate the hash with and without servers, when both change publish without servers * do not retain original spec definition after stripping servers * add some test for new functionality * more tests * update tag comparison for revision
1 parent 2de1666 commit 2a298fc

11 files changed

Lines changed: 252 additions & 92 deletions

pkg/apic/apiservice_test.go

Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -350,29 +350,128 @@ func Test_PublishServiceError(t *testing.T) {
350350
func Test_processRevision(t *testing.T) {
351351
client, httpClient := GetTestServiceClient()
352352

353-
// tests for updating existing revision
354-
httpClient.SetResponses([]api.MockResponse{
355-
{
356-
FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision
357-
RespCode: http.StatusOK,
353+
testCases := map[string]struct {
354+
skip bool
355+
httpResponses []api.MockResponse
356+
serviceBody ServiceBody
357+
expectedRevName string
358+
}{
359+
"publish new revision": {
360+
httpResponses: []api.MockResponse{
361+
{
362+
FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision
363+
RespCode: http.StatusOK,
364+
},
365+
{
366+
FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision x-agent-details
367+
RespCode: http.StatusOK,
368+
},
369+
{
370+
FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision
371+
RespCode: http.StatusOK,
372+
},
373+
{
374+
FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision x-agent-details
375+
RespCode: http.StatusOK,
376+
},
377+
},
378+
serviceBody: ServiceBody{
379+
APIName: "daleapi",
380+
Documentation: []byte("\"docs\""),
381+
Image: "abcde",
382+
ImageContentType: "image/jpeg",
383+
ResourceType: Oas2,
384+
RestAPIID: "12345",
385+
},
386+
expectedRevName: "daleapi",
358387
},
359-
{
360-
FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision x-agent-details
361-
RespCode: http.StatusOK,
388+
"skip publish when no changes": {
389+
httpResponses: []api.MockResponse{
390+
{
391+
RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]}]`,
392+
RespCode: http.StatusOK,
393+
},
394+
},
395+
serviceBody: ServiceBody{
396+
APIName: "daleapi",
397+
Documentation: []byte("\"docs\""),
398+
Image: "abcde",
399+
ImageContentType: "image/jpeg",
400+
ResourceType: Oas2,
401+
RestAPIID: "12345",
402+
specHash: "abc123",
403+
specHashes: map[string]interface{}{
404+
"abc123": "daleapi",
405+
},
406+
serviceContext: serviceContext{
407+
serviceAction: updateAPI,
408+
},
409+
},
410+
expectedRevName: "daleapi",
362411
},
363-
{
364-
FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision
365-
RespCode: http.StatusOK,
412+
"skip publish when previous revision found": {
413+
httpResponses: []api.MockResponse{
414+
{
415+
RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]},{"name": "daleapi-1","tags": ["tag1","tag2"]}]`,
416+
RespCode: http.StatusOK,
417+
},
418+
},
419+
serviceBody: ServiceBody{
420+
APIName: "daleapi",
421+
Documentation: []byte("\"docs\""),
422+
Image: "abcde",
423+
ImageContentType: "image/jpeg",
424+
ResourceType: Oas2,
425+
RestAPIID: "12345",
426+
specHash: "abc123",
427+
specHashes: map[string]interface{}{
428+
"abc123": "daleapi-1",
429+
},
430+
serviceContext: serviceContext{
431+
serviceAction: updateAPI,
432+
},
433+
},
434+
expectedRevName: "daleapi-1",
366435
},
367-
{
368-
FileName: "./testdata/servicerevision.json", // for call to update the serviceRevision x-agent-details
369-
RespCode: http.StatusOK,
436+
"find revision match using original spec hash": {
437+
httpResponses: []api.MockResponse{
438+
{
439+
RespData: `[{"name": "daleapi","tags": ["tag1","tag2"]},{"name": "daleapi-1","tags": ["tag1","tag2"]}]`,
440+
RespCode: http.StatusOK,
441+
},
442+
},
443+
serviceBody: ServiceBody{
444+
APIName: "daleapi",
445+
Documentation: []byte("\"docs\""),
446+
Image: "abcde",
447+
ImageContentType: "image/jpeg",
448+
ResourceType: Oas2,
449+
RestAPIID: "12345",
450+
specHash: "abc1234",
451+
originalSpecHash: "abc123",
452+
specHashes: map[string]interface{}{
453+
"abc123": "daleapi-1",
454+
},
455+
serviceContext: serviceContext{
456+
serviceAction: updateAPI,
457+
},
458+
},
459+
expectedRevName: "daleapi-1",
370460
},
371-
})
372-
cloneServiceBody := serviceBody
373-
// Normal Revision
374-
client.processRevision(&cloneServiceBody)
375-
assert.NotEqual(t, "", cloneServiceBody.serviceContext.revisionName)
461+
}
462+
for name, tc := range testCases {
463+
t.Run(name, func(t *testing.T) {
464+
if tc.skip {
465+
return
466+
}
467+
// tests for updating existing revision
468+
httpClient.SetResponses(tc.httpResponses)
469+
470+
client.processRevision(&tc.serviceBody)
471+
assert.NotEqual(t, "", tc.serviceBody.serviceContext.revisionName)
472+
assert.Equal(t, tc.expectedRevName, tc.serviceBody.serviceContext.revisionName)
473+
})
474+
}
376475
}
377476

378477
func TestDeleteServiceByAPIID(t *testing.T) {

pkg/apic/apiservicerevision.go

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"net/http"
10-
"reflect"
1110
"regexp"
12-
"sort"
1311
"strconv"
1412
"strings"
1513
"text/template"
@@ -165,7 +163,15 @@ func (c *ServiceClient) getRevisionsIfUpdating(serviceBody *ServiceBody) ([]*man
165163

166164
// checkAndUpdateExistingRevision checks if a revision with the same hash exists and updates tags if needed
167165
func (c *ServiceClient) checkAndUpdateExistingRevision(serviceBody *ServiceBody, apiServiceRevisions []*management.APIServiceRevision) (bool, error) {
166+
// attempt to use the stripped spec hash
168167
revName, found := serviceBody.specHashes[serviceBody.specHash]
168+
if !found && serviceBody.originalSpecHash != "" {
169+
// check if the original spec hash matches an existing revision,
170+
// this is to cover the case where the spec content has not changed since the last publish,
171+
// but the hash has changed due to non-content related changes (e.g. stripping servers)
172+
revName, found = serviceBody.specHashes[serviceBody.originalSpecHash]
173+
}
174+
169175
if !found {
170176
return false, nil
171177
}
@@ -205,24 +211,13 @@ func (c *ServiceClient) checkAndUpdateExistingRevision(serviceBody *ServiceBody,
205211
// different, return the updated tags
206212
func (c *ServiceClient) getUpdatedTagKeys(serviceBodyTags map[string]interface{}, revisionTags []string) []string {
207213
// Extract values from map and convert to []string
208-
var mapValues []string
209-
for _, v := range serviceBodyTags {
210-
if strVal, ok := v.(string); ok {
211-
mapValues = append(mapValues, strVal)
212-
}
213-
}
214-
215-
// Sort both slices to allow unordered comparison
216-
sort.Strings(mapValues)
217-
sort.Strings(revisionTags)
214+
tags := mapToTagsArray(serviceBodyTags, c.cfg.GetTagsToPublish())
218215

219216
// Compare
220-
if reflect.DeepEqual(mapValues, revisionTags) {
217+
if util.StringSlicesEqualUnordered(tags, revisionTags) {
221218
return []string{} // return empty string slice if equal
222219
}
223220

224-
// If not equal, return the keys from serviceBodyTags
225-
tags := mapToTagsArray(serviceBodyTags, c.cfg.GetTagsToPublish())
226221
return tags
227222
}
228223

pkg/apic/client.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"encoding/json"
55
"fmt"
66
"net/http"
7-
"slices"
87
"strconv"
98
"strings"
109
"sync"
@@ -870,7 +869,7 @@ func (c *ServiceClient) updateSpecORCreateResourceInstance(data *apiv1.ResourceI
870869
method = coreapi.PUT
871870

872871
// check if either hash, tags or title have changed and mark for update
873-
equalTags := slices.Equal(existingRI.GetTags(), data.GetTags())
872+
equalTags := util.StringSlicesEqualUnordered(existingRI.GetTags(), data.GetTags())
874873
oldHash, _ := util.GetAgentDetailsValue(existingRI, defs.AttrSpecHash)
875874
newHash, _ := util.GetAgentDetailsValue(data, defs.AttrSpecHash)
876875
if oldHash == newHash && existingRI.Title == data.Title && equalTags {

pkg/apic/servicebody.go

Lines changed: 55 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,59 +14,61 @@ type APIKeyInfo struct {
1414

1515
// ServiceBody - details about a service to create
1616
type ServiceBody struct {
17-
NameToPush string
18-
APIName string
19-
RestAPIID string
20-
PrimaryKey string
21-
URL string
22-
Stage string
23-
StageDescriptor string
24-
StageDisplayName string
25-
Description string
26-
Version string
27-
AuthPolicy string
28-
authPolicies []string
29-
apiKeyInfo []APIKeyInfo
30-
scopes map[string]string
31-
SpecDefinition []byte
32-
Documentation []byte
33-
Tags map[string]interface{}
34-
Image string
35-
ImageContentType string
36-
CreatedBy string
37-
ResourceContentType string
38-
ResourceType string
39-
SubscriptionName string
40-
APIUpdateSeverity string
41-
State string
42-
Status string
43-
ServiceAttributes map[string]string
44-
RevisionAttributes map[string]string
45-
InstanceAttributes map[string]string
46-
ServiceAgentDetails map[string]interface{}
47-
InstanceAgentDetails map[string]interface{}
48-
RevisionAgentDetails map[string]interface{}
49-
serviceContext serviceContext
50-
Endpoints []EndpointDefinition
51-
UnstructuredProps *UnstructuredProperties
52-
TeamName string
53-
teamID string
54-
credentialRequestPolicies []string
55-
ardName string
56-
uniqueARD bool
57-
ignoreSpecBasesCreds bool
58-
stripOASExtensions bool
59-
specHash string
60-
specVersion string
61-
accessRequestDefinition *management.AccessRequestDefinition
62-
specHashes map[string]interface{} // map of hash values to revision names
63-
requestDefinitionsAllowed bool // used to validate if the instance can have request definitions or not. Use case example - v7 unpublished, remove request definitions
64-
dataplaneType DataplaneType
65-
isDesignDataplane bool
66-
referencedServiceName string
67-
referencedInstanceName string
68-
logger log.FieldLogger
69-
instanceLifecycle *management.ApiServiceInstanceLifecycle
17+
NameToPush string
18+
APIName string
19+
RestAPIID string
20+
PrimaryKey string
21+
URL string
22+
Stage string
23+
StageDescriptor string
24+
StageDisplayName string
25+
Description string
26+
Version string
27+
AuthPolicy string
28+
authPolicies []string
29+
apiKeyInfo []APIKeyInfo
30+
scopes map[string]string
31+
SpecDefinition []byte
32+
Documentation []byte
33+
Tags map[string]interface{}
34+
Image string
35+
ImageContentType string
36+
CreatedBy string
37+
ResourceContentType string
38+
ResourceType string
39+
SubscriptionName string
40+
APIUpdateSeverity string
41+
State string
42+
Status string
43+
ServiceAttributes map[string]string
44+
RevisionAttributes map[string]string
45+
InstanceAttributes map[string]string
46+
ServiceAgentDetails map[string]interface{}
47+
InstanceAgentDetails map[string]interface{}
48+
RevisionAgentDetails map[string]interface{}
49+
serviceContext serviceContext
50+
Endpoints []EndpointDefinition
51+
UnstructuredProps *UnstructuredProperties
52+
TeamName string
53+
teamID string
54+
credentialRequestPolicies []string
55+
ardName string
56+
uniqueARD bool
57+
ignoreSpecBasesCreds bool
58+
stripOASExtensions bool
59+
stripOASServersBeforePublish bool
60+
specHash string
61+
specVersion string
62+
accessRequestDefinition *management.AccessRequestDefinition
63+
specHashes map[string]interface{} // map of hash values to revision names
64+
requestDefinitionsAllowed bool // used to validate if the instance can have request definitions or not. Use case example - v7 unpublished, remove request definitions
65+
dataplaneType DataplaneType
66+
isDesignDataplane bool
67+
referencedServiceName string
68+
referencedInstanceName string
69+
logger log.FieldLogger
70+
instanceLifecycle *management.ApiServiceInstanceLifecycle
71+
originalSpecHash string
7072
}
7173

7274
// SetAccessRequestDefinitionName - set the name of the access request definition for this service body

pkg/apic/servicebuilder.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type ServiceBuilder interface {
5656
SetAccessRequestDefinitionName(accessRequestDefName string, isUnique bool) ServiceBuilder
5757
SetIgnoreSpecBasedCreds(ignore bool) ServiceBuilder
5858
SetStripOASExtensions(strip bool) ServiceBuilder
59+
SetStripOASServersBeforePublish() ServiceBuilder
5960

6061
SetUnstructuredType(assetType string) ServiceBuilder
6162
SetUnstructuredContentType(contentType string) ServiceBuilder
@@ -379,6 +380,14 @@ func (b *serviceBodyBuilder) Build() (ServiceBody, error) {
379380
val.StripExtensions()
380381
}
381382

383+
if b.serviceBody.stripOASServersBeforePublish {
384+
b.serviceBody.originalSpecHash = b.serviceBody.specHash
385+
val.stripEndpoints()
386+
b.serviceBody.SpecDefinition = val.GetSpecBytes()
387+
newHash, _ := util.ComputeHash(val.GetSpecBytes())
388+
b.serviceBody.specHash = fmt.Sprintf("%v", newHash)
389+
}
390+
382391
// only set ard name based on spec if not already set, use first auth we find
383392
if b.serviceBody.ardName != "" {
384393
return b.serviceBody, nil
@@ -426,6 +435,11 @@ func (b *serviceBodyBuilder) SetIgnoreSpecBasedCreds(ignore bool) ServiceBuilder
426435
return b
427436
}
428437

438+
func (b *serviceBodyBuilder) SetStripOASServersBeforePublish() ServiceBuilder {
439+
b.serviceBody.stripOASServersBeforePublish = true
440+
return b
441+
}
442+
429443
func (b *serviceBodyBuilder) SetStripOASExtensions(strip bool) ServiceBuilder {
430444
b.serviceBody.stripOASExtensions = strip
431445
return b

0 commit comments

Comments
 (0)