Skip to content

Commit 7f23e2f

Browse files
authored
Issue 2090/route table order dependence (aws-controllers-k8s#236)
Fixes issue [#2090](aws-controllers-k8s/community#2090) Description of changes: - Configure generator for RouteTable to ignore the Routes from the generated delta code. - Extend the `customPreCompare` code to take care of the routes (computing the delta ignoring the order of routes). - Added unit test for the custom code. - Change the `syncRoutes` code to use the delta from the compare phase. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 59a0897 commit 7f23e2f

File tree

7 files changed

+168
-91
lines changed

7 files changed

+168
-91
lines changed

apis/v1alpha1/ack-generate-metadata.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ api_directory_checksum: b31faecf6092fab9677498f3624e624fee4cbaed
77
api_version: v1alpha1
88
aws_sdk_go_version: v1.32.6
99
generator_config_info:
10-
file_checksum: b3d6924c2a4a2252ea9a7fa775eb86bd1660eeef
10+
file_checksum: 4f9d21bd7c46b495cd9374e454a3a0e1e6de5d08
1111
original_file_name: generator.yaml
1212
last_modification:
1313
reason: API generation

apis/v1alpha1/generator.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,8 @@ resources:
535535
Routes:
536536
custom_field:
537537
list_of: CreateRouteInput
538+
compare:
539+
is_ignored: true
538540
RouteTableID:
539541
print:
540542
path: Status.RouteTableID

generator.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,8 @@ resources:
535535
Routes:
536536
custom_field:
537537
list_of: CreateRouteInput
538+
compare:
539+
is_ignored: true
538540
RouteTableID:
539541
print:
540542
path: Status.RouteTableID

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/go-logr/logr v1.4.2
1414
github.com/samber/lo v1.37.0
1515
github.com/spf13/pflag v1.0.5
16+
github.com/stretchr/testify v1.9.0
1617
k8s.io/api v0.31.0
1718
k8s.io/apimachinery v0.31.0
1819
k8s.io/client-go v0.31.0
@@ -61,6 +62,7 @@ require (
6162
github.com/modern-go/reflect2 v1.0.2 // indirect
6263
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
6364
github.com/pkg/errors v0.9.1 // indirect
65+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
6466
github.com/prometheus/client_golang v1.19.1 // indirect
6567
github.com/prometheus/client_model v0.6.1 // indirect
6668
github.com/prometheus/common v0.55.0 // indirect

pkg/resource/route_table/delta.go

Lines changed: 0 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/resource/route_table/hooks.go

Lines changed: 55 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -46,52 +46,35 @@ func (rm *resourceManager) syncRoutes(
4646
rlog := ackrtlog.FromContext(ctx)
4747
exit := rlog.Trace("rm.syncRoutes")
4848
defer func(err error) { exit(err) }(err)
49-
toAdd := []*svcapitypes.CreateRouteInput{}
50-
toDelete := []*svcapitypes.CreateRouteInput{}
5149

5250
if latest != nil {
51+
latest.ko.Spec.Routes = removeLocalRoute(latest.ko.Spec.Routes)
5352
latest.ko.Spec.Routes, err = rm.excludeAWSRoute(ctx, latest.ko.Spec.Routes)
5453
if err != nil {
5554
return err
5655
}
5756
}
5857
if desired != nil {
58+
desired.ko.Spec.Routes = removeLocalRoute(desired.ko.Spec.Routes)
5959
desired.ko.Spec.Routes, err = rm.excludeAWSRoute(ctx, desired.ko.Spec.Routes)
6060
if err != nil {
6161
return err
6262
}
6363
}
6464

65-
for _, desiredRoute := range desired.ko.Spec.Routes {
66-
if (*desiredRoute).GatewayID != nil && *desiredRoute.GatewayID == LocalRouteGateway {
67-
// no-op for default route
68-
continue
69-
}
70-
if latestRoute := getMatchingRoute(desiredRoute, latest); latestRoute != nil {
71-
delta := compareCreateRouteInput(desiredRoute, latestRoute)
72-
if len(delta.Differences) > 0 {
73-
// "update" route by deleting old route and adding the new route
74-
toDelete = append(toDelete, latestRoute)
75-
toAdd = append(toAdd, desiredRoute)
76-
}
77-
} else {
78-
// a desired route is not in latest; therefore, create
79-
toAdd = append(toAdd, desiredRoute)
80-
}
81-
}
65+
// Get the routes that need to be added and deleted, by checking for
66+
// differences between desired and latest.
67+
var toAdd, toDelete []*svcapitypes.CreateRouteInput
8268
if latest != nil {
83-
for _, latestRoute := range latest.ko.Spec.Routes {
84-
if (*latestRoute).GatewayID != nil && *latestRoute.GatewayID == LocalRouteGateway {
85-
// no-op for default route
86-
continue
87-
}
88-
if desiredRoute := getMatchingRoute(latestRoute, desired); desiredRoute == nil {
89-
// latest has a route that is not desired; therefore, delete
90-
toDelete = append(toDelete, latestRoute)
91-
}
92-
}
69+
toAdd, toDelete = getRoutesDifference(desired.ko.Spec.Routes, latest.ko.Spec.Routes)
70+
} else {
71+
// If method is called from the createRoutes method, then latest is nil
72+
// and all routes must be added, as non exist yet.
73+
toAdd = desired.ko.Spec.Routes
9374
}
9475

76+
// Delete and add the routes that were found to be different between desired
77+
// and latest.
9578
for _, route := range toDelete {
9679
rlog.Debug("deleting route from route table")
9780
if err = rm.deleteRoute(ctx, latest, *route); err != nil {
@@ -108,60 +91,6 @@ func (rm *resourceManager) syncRoutes(
10891
return nil
10992
}
11093

111-
func getMatchingRoute(
112-
routeToMatch *svcapitypes.CreateRouteInput,
113-
resource *resource,
114-
) *svcapitypes.CreateRouteInput {
115-
if resource == nil {
116-
return nil
117-
}
118-
119-
for _, route := range resource.ko.Spec.Routes {
120-
delta := compareCreateRouteInput(routeToMatch, route)
121-
if len(delta.Differences) == 0 {
122-
return route
123-
} else {
124-
if routeToMatch.CarrierGatewayID != nil {
125-
if !delta.DifferentAt("CreateRouteInput.CarrierGatewayID") {
126-
return route
127-
}
128-
}
129-
if routeToMatch.EgressOnlyInternetGatewayID != nil {
130-
if !delta.DifferentAt("CreateRouteInput.EgressOnlyInternetGatewayID") {
131-
return route
132-
}
133-
}
134-
if routeToMatch.GatewayID != nil {
135-
if !delta.DifferentAt("CreateRouteInput.GatewayID") {
136-
return route
137-
}
138-
}
139-
if routeToMatch.LocalGatewayID != nil {
140-
if !delta.DifferentAt("CreateRouteInput.LocalGatewayID") {
141-
return route
142-
}
143-
}
144-
if routeToMatch.NATGatewayID != nil {
145-
if !delta.DifferentAt("CreateRouteInput.NATGatewayID") {
146-
return route
147-
}
148-
}
149-
if routeToMatch.TransitGatewayID != nil {
150-
if !delta.DifferentAt("CreateRouteInput.TransitGatewayID") {
151-
return route
152-
}
153-
}
154-
if routeToMatch.VPCPeeringConnectionID != nil {
155-
if !delta.DifferentAt("CreateRouteInput.VPCPeeringConnectionID") {
156-
return route
157-
}
158-
}
159-
}
160-
}
161-
162-
return nil
163-
}
164-
16594
func (rm *resourceManager) createRoute(
16695
ctx context.Context,
16796
r *resource,
@@ -270,13 +199,56 @@ var computeTagsDelta = tags.ComputeTagsDelta
270199

271200
// customPreCompare ensures that default values of types are initialised and
272201
// server side defaults are excluded from the delta.
202+
// The left side (`A`) of any `Spec.Routes` diff contains the routes that are
203+
// desired, but do not exist. Analogously, the right side (`B`) contains the
204+
// routes that exist, but are not desired.
273205
func customPreCompare(
274206
delta *ackcompare.Delta,
275207
a *resource,
276208
b *resource,
277209
) {
278210
a.ko.Spec.Routes = removeLocalRoute(a.ko.Spec.Routes)
279211
b.ko.Spec.Routes = removeLocalRoute(b.ko.Spec.Routes)
212+
213+
desired, latest := getRoutesDifference(a.ko.Spec.Routes, b.ko.Spec.Routes)
214+
215+
if len(desired) > 0 || len(latest) > 0 {
216+
delta.Add("Spec.Routes", a.ko.Spec.Routes, b.ko.Spec.Routes)
217+
}
218+
}
219+
220+
// getRoutesDifference compares the desired and latest routes. It returns the
221+
// routes that are different and must be added or deleted.
222+
func getRoutesDifference(desired, latest []*svcapitypes.CreateRouteInput) (toAdd, toDelete []*svcapitypes.CreateRouteInput) {
223+
toDelete = make([]*svcapitypes.CreateRouteInput, len(latest))
224+
copy(toDelete, latest)
225+
226+
remove := func(s []*svcapitypes.CreateRouteInput, i int) []*svcapitypes.CreateRouteInput {
227+
if i < len(s)-1 { // if not last element just copy the last element to where the removed element was
228+
s[i] = s[len(s)-1]
229+
}
230+
return s[:len(s)-1]
231+
}
232+
233+
// Routes that are desired, but already exist in latest, can be ignored. The
234+
// toDelete slice is a copy of latest and will be slowly modified so that at
235+
// the end it only contains routes that exist in latest, but are not
236+
// desired.
237+
for _, routeA := range desired {
238+
found := false
239+
for idx, routeB := range toDelete {
240+
if delta := compareCreateRouteInput(routeA, routeB); len(delta.Differences) == 0 {
241+
toDelete = remove(toDelete, idx)
242+
found = true
243+
break
244+
}
245+
}
246+
if !found {
247+
toAdd = append(toAdd, routeA.DeepCopy())
248+
}
249+
}
250+
251+
return toAdd, toDelete
280252
}
281253

282254
// removeLocalRoute will filter out any routes that have a gateway ID that
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package route_table
2+
3+
import (
4+
"testing"
5+
6+
svcapitypes "github.com/aws-controllers-k8s/ec2-controller/apis/v1alpha1"
7+
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
8+
"github.com/aws/aws-sdk-go/aws"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestCustomerPreCompare(t *testing.T) {
13+
type Routes []*svcapitypes.CreateRouteInput
14+
15+
peeringRoute := func(pcxID string, cidr string) *svcapitypes.CreateRouteInput {
16+
return &svcapitypes.CreateRouteInput{
17+
DestinationCIDRBlock: aws.String(cidr),
18+
VPCPeeringConnectionID: aws.String(pcxID),
19+
}
20+
}
21+
22+
createRouteTableTestResource := func(routes []*svcapitypes.CreateRouteInput) *resource {
23+
return &resource{
24+
ko: &svcapitypes.RouteTable{
25+
Spec: svcapitypes.RouteTableSpec{
26+
Routes: routes,
27+
},
28+
},
29+
}
30+
}
31+
32+
assertRoutesIdentical := func(t *testing.T, a, b []*svcapitypes.CreateRouteInput) {
33+
assert.Len(t, a, len(b))
34+
for i := range a {
35+
assert.EqualValues(t, a[i], b[i])
36+
}
37+
}
38+
39+
tt := []struct {
40+
id string
41+
desiredRoutes Routes
42+
latestRoutes Routes
43+
toAdd Routes
44+
toDelete Routes
45+
}{
46+
{"all identical",
47+
Routes{peeringRoute("pcx-123", "172.30.1.0/24")},
48+
Routes{peeringRoute("pcx-123", "172.30.1.0/24")},
49+
nil, nil,
50+
},
51+
{"add route",
52+
Routes{peeringRoute("pcx-123", "172.30.1.0/24")},
53+
nil,
54+
Routes{peeringRoute("pcx-123", "172.30.1.0/24")},
55+
nil,
56+
},
57+
{"delete route",
58+
nil,
59+
Routes{peeringRoute("pcx-123", "172.30.1.0/24")},
60+
nil,
61+
Routes{peeringRoute("pcx-123", "172.30.1.0/24")},
62+
},
63+
{"keep one delete one",
64+
Routes{peeringRoute("pcx-123", "172.30.1.0/24")},
65+
Routes{peeringRoute("pcx-123", "172.30.1.0/24"), peeringRoute("pcx-123", "172.30.2.0/24")},
66+
nil,
67+
Routes{peeringRoute("pcx-123", "172.30.2.0/24")},
68+
},
69+
{"keep one add one",
70+
Routes{peeringRoute("pcx-123", "172.30.1.0/24"), peeringRoute("pcx-123", "172.30.2.0/24")},
71+
Routes{peeringRoute("pcx-123", "172.30.1.0/24")},
72+
Routes{peeringRoute("pcx-123", "172.30.2.0/24")},
73+
nil,
74+
},
75+
{"keep one add one delete one",
76+
Routes{peeringRoute("pcx-123", "172.30.1.0/24"), peeringRoute("pcx-123", "172.30.2.0/24")},
77+
Routes{peeringRoute("pcx-123", "172.30.1.0/24"), peeringRoute("pcx-123", "172.30.3.0/24")},
78+
Routes{peeringRoute("pcx-123", "172.30.2.0/24")},
79+
Routes{peeringRoute("pcx-123", "172.30.3.0/24")},
80+
},
81+
}
82+
83+
for _, tti := range tt {
84+
t.Run(tti.id, func(t *testing.T) {
85+
delta := ackcompare.NewDelta()
86+
a := createRouteTableTestResource(tti.desiredRoutes)
87+
b := createRouteTableTestResource(tti.latestRoutes)
88+
customPreCompare(delta, a, b)
89+
if len(tti.toAdd) == 0 && len(tti.toDelete) == 0 {
90+
assert.Equal(t, 0, len(delta.Differences))
91+
} else {
92+
diff := delta.Differences[0]
93+
diffA := diff.A.([]*svcapitypes.CreateRouteInput)
94+
diffB := diff.B.([]*svcapitypes.CreateRouteInput)
95+
assert.True(t, diff.Path.Contains("Spec.Routes"))
96+
assert.ElementsMatch(t, tti.desiredRoutes, diffA)
97+
assert.ElementsMatch(t, tti.latestRoutes, diffB)
98+
99+
// Check the different routes are identified correctly
100+
toAdd, toDelete := getRoutesDifference(tti.desiredRoutes, tti.latestRoutes)
101+
assertRoutesIdentical(t, tti.toAdd, toAdd)
102+
assertRoutesIdentical(t, tti.toDelete, toDelete)
103+
}
104+
})
105+
}
106+
}

0 commit comments

Comments
 (0)