Skip to content

Commit b9ea614

Browse files
authored
Fix: Portal asset and api publication redundant update plans (#323)
* Fix: adds portal asset checking to planner * Fix: updates assets e2e test for fix to asset file comparsion fix * Fix: updates assets e2e test for fix to asset file comparsion fix * Fix: Ensures api publications are idempotent when auth strat ids are present --------- Co-authored-by: Rick Spurgeon <10521262+rspurgeon@users.noreply.github.com>
1 parent df05a5c commit b9ea614

13 files changed

Lines changed: 489 additions & 21 deletions

File tree

.go-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.24.3

.tool-versions

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
jq 1.8.1
2+
gh 2.83.2
3+
git 2.52.0

docs/examples/declarative/basic/api-with-portal-pub.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1+
application_auth_strategies:
2+
- ref: simple-auth
3+
name: my-api-key-auth
4+
display_name: API Key Authentication
5+
strategy_type: key_auth
6+
configs:
7+
key_auth:
8+
key_names:
9+
- X-API-Key
110

211
portals:
312
- ref: simple-portal
413
name: My Simple Portal
514
display_name: Simple Portal ©
615
description: The simplest Portal example
16+
# Portal assets (logo and favicon)
17+
assets:
18+
logo: !file ../portal/assets/logo.png
19+
favicon: !file ../portal/assets/favicon.png
720

821
apis:
922
- ref: simple-api
@@ -13,3 +26,5 @@ apis:
1326
- ref: simple-api-to-simple-portal
1427
portal_id: !ref simple-portal#id
1528
visibility: public
29+
auth_strategy_ids:
30+
- !ref simple-auth#id

internal/declarative/planner/api_planner.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,8 +1266,11 @@ func (p *Planner) shouldUpdateAPIPublication(
12661266
updates := make(map[string]any)
12671267

12681268
// Compare auth strategy IDs (order-independent comparison)
1269-
if !p.compareStringSlices(current.AuthStrategyIDs, desired.AuthStrategyIds) {
1270-
updates["auth_strategy_ids"] = desired.AuthStrategyIds
1269+
if desired.AuthStrategyIds != nil {
1270+
resolvedDesired := p.resolveAuthStrategyIDsForComparison(desired.AuthStrategyIds)
1271+
if !p.compareStringSlices(current.AuthStrategyIDs, resolvedDesired) {
1272+
updates["auth_strategy_ids"] = desired.AuthStrategyIds
1273+
}
12711274
}
12721275

12731276
// Compare auto approve registrations
@@ -1290,6 +1293,33 @@ func (p *Planner) shouldUpdateAPIPublication(
12901293
return len(updates) > 0, updates
12911294
}
12921295

1296+
func (p *Planner) resolveAuthStrategyIDsForComparison(desired []string) []string {
1297+
if p.resources == nil || len(desired) == 0 {
1298+
return desired
1299+
}
1300+
1301+
resolved := make([]string, 0, len(desired))
1302+
for _, ref := range desired {
1303+
lookupRef := ref
1304+
if tags.IsRefPlaceholder(lookupRef) {
1305+
if parsedRef, _, ok := tags.ParseRefPlaceholder(lookupRef); ok && parsedRef != "" {
1306+
lookupRef = parsedRef
1307+
}
1308+
}
1309+
1310+
if strategy := p.resources.GetAuthStrategyByRef(lookupRef); strategy != nil {
1311+
if id := strategy.GetKonnectID(); id != "" {
1312+
resolved = append(resolved, id)
1313+
continue
1314+
}
1315+
}
1316+
1317+
resolved = append(resolved, ref)
1318+
}
1319+
1320+
return resolved
1321+
}
1322+
12931323
// compareStringSlices compares two string slices for equality (order-independent)
12941324
func (p *Planner) compareStringSlices(a, b []string) bool {
12951325
if len(a) != len(b) {

internal/declarative/planner/api_planner_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
kkComps "github.com/Kong/sdk-konnect-go/models/components"
77
"github.com/kong/kongctl/internal/declarative/resources"
88
"github.com/kong/kongctl/internal/declarative/state"
9+
"github.com/kong/kongctl/internal/declarative/tags"
910
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
1012
)
1113

1214
// TestAPIVersionConstraintValidation tests that the loader properly validates API version constraints
@@ -96,3 +98,64 @@ func TestShouldUpdateAPIConsidersSlugAndAttributes(t *testing.T) {
9698
assert.Equal(t, updatedSlug, updateFields["slug"])
9799
assert.Equal(t, expectedUpdatedAttrs, updateFields["attributes"])
98100
}
101+
102+
func TestShouldUpdateAPIPublicationResolvesAuthStrategyRefs(t *testing.T) {
103+
t.Parallel()
104+
105+
authStrategy := resources.ApplicationAuthStrategyResource{
106+
CreateAppAuthStrategyRequest: kkComps.CreateCreateAppAuthStrategyRequestKeyAuth(
107+
kkComps.AppAuthStrategyKeyAuthRequest{
108+
Name: "my-api-key-auth",
109+
StrategyType: kkComps.StrategyTypeKeyAuth,
110+
},
111+
),
112+
Ref: "key-auth",
113+
}
114+
115+
authStrategy.TryMatchKonnectResource(state.ApplicationAuthStrategy{
116+
ID: "auth-id",
117+
Name: "my-api-key-auth",
118+
})
119+
120+
planner := &Planner{
121+
resources: &resources.ResourceSet{
122+
ApplicationAuthStrategies: []resources.ApplicationAuthStrategyResource{authStrategy},
123+
},
124+
}
125+
126+
current := state.APIPublication{
127+
AuthStrategyIDs: []string{"auth-id"},
128+
}
129+
130+
desired := resources.APIPublicationResource{
131+
APIPublication: kkComps.APIPublication{
132+
AuthStrategyIds: []string{tags.RefPlaceholderPrefix + "key-auth#id"},
133+
},
134+
Ref: "pub",
135+
PortalID: "portal-id",
136+
}
137+
138+
needsUpdate, fields := planner.shouldUpdateAPIPublication(current, desired)
139+
require.False(t, needsUpdate)
140+
assert.Empty(t, fields)
141+
}
142+
143+
func TestShouldUpdateAPIPublicationIgnoresAuthStrategyWhenUnset(t *testing.T) {
144+
t.Parallel()
145+
146+
planner := &Planner{}
147+
148+
current := state.APIPublication{
149+
AuthStrategyIDs: []string{"auth-id"},
150+
}
151+
152+
desired := resources.APIPublicationResource{
153+
APIPublication: kkComps.APIPublication{},
154+
Ref: "pub",
155+
PortalID: "portal-id",
156+
}
157+
158+
needsUpdate, fields := planner.shouldUpdateAPIPublication(current, desired)
159+
require.False(t, needsUpdate)
160+
assert.Empty(t, fields)
161+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package planner
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"fmt"
7+
"net/http"
8+
"testing"
9+
10+
kkErrors "github.com/Kong/sdk-konnect-go/models/sdkerrors"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func makeDataURL(mime string, payload []byte) string {
15+
encoded := base64.StdEncoding.EncodeToString(payload)
16+
return fmt.Sprintf("data:%s;base64,%s", mime, encoded)
17+
}
18+
19+
func TestDataURLsEqual_Base64(t *testing.T) {
20+
t.Parallel()
21+
22+
payload := []byte("hello")
23+
first := makeDataURL("image/png", payload)
24+
second := makeDataURL("image/jpeg", payload)
25+
26+
equal, err := dataURLsEqual(first, second)
27+
require.NoError(t, err)
28+
require.True(t, equal)
29+
}
30+
31+
func TestPortalAssetNeedsUpdate_NotFound(t *testing.T) {
32+
t.Parallel()
33+
34+
desired := makeDataURL("image/png", []byte("asset"))
35+
planner := &Planner{}
36+
37+
needsUpdate, err := planner.portalAssetNeedsUpdate(
38+
context.Background(),
39+
"portal-id",
40+
desired,
41+
func(_ context.Context, _ string) (string, error) {
42+
return "", &kkErrors.SDKError{StatusCode: http.StatusNotFound}
43+
},
44+
)
45+
46+
require.NoError(t, err)
47+
require.True(t, needsUpdate)
48+
}
49+
50+
func TestPortalAssetNeedsUpdate_NoChange(t *testing.T) {
51+
t.Parallel()
52+
53+
payload := []byte("asset")
54+
desired := makeDataURL("image/png", payload)
55+
planner := &Planner{}
56+
57+
needsUpdate, err := planner.portalAssetNeedsUpdate(
58+
context.Background(),
59+
"portal-id",
60+
desired,
61+
func(_ context.Context, _ string) (string, error) {
62+
return makeDataURL("image/jpeg", payload), nil
63+
},
64+
)
65+
66+
require.NoError(t, err)
67+
require.False(t, needsUpdate)
68+
}
69+
70+
func TestPortalAssetNeedsUpdate_Change(t *testing.T) {
71+
t.Parallel()
72+
73+
desired := makeDataURL("image/png", []byte("asset"))
74+
planner := &Planner{}
75+
76+
needsUpdate, err := planner.portalAssetNeedsUpdate(
77+
context.Background(),
78+
"portal-id",
79+
desired,
80+
func(_ context.Context, _ string) (string, error) {
81+
return makeDataURL("image/png", []byte("different")), nil
82+
},
83+
)
84+
85+
require.NoError(t, err)
86+
require.True(t, needsUpdate)
87+
}

0 commit comments

Comments
 (0)