Skip to content

Commit 307d36b

Browse files
authored
Enhance EKS AccessEntries policy attachements and delta logic (#102)
Prior to this patch, we observed some issues with the controller behaviour regarding access policy association and delta computations, specifically: - False positive deltas were detected at every reconciliation (or controller restart) - When a delta was detected, the controller disociate the latest policies, and patch the desired ones. (Even when it's unnecessary). The root cause was identifier as the usage of `reflect.DeepEqual` for slices. According to the `reflect` library [documentation](https://pkg.go.dev/reflect#DeepEqual) `DeepEqual` returns false if the comapred elements are a nil slice and a "non-nil empty slice" (e.g `nil` and `[]string{}`). This combined with the possibility of a user providing a nil `namespaces` slice, while the controller sets an empty one (based on the API response), cause behaviour inconsistencies. Users shouldn't care/worry about the "nil-ity" of their arrays/slices; it is up to the controller to accurately compute the deltas. This patch replaces the usage of `reflect.DeepEqual` with a meticulous custom delta function. Signed-off-by: Amine Hilaly <[email protected]> By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent d0f1e03 commit 307d36b

File tree

7 files changed

+342
-51
lines changed

7 files changed

+342
-51
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
ack_generate_info:
2-
build_date: "2024-02-08T16:38:43Z"
2+
build_date: "2024-02-09T21:18:38Z"
33
build_hash: 5b4565ec2712d29988b8123aeeed6a4af57467bf
44
go_version: go1.21.5
55
version: v0.29.2-4-g5b4565e
66
api_directory_checksum: d960a9f06b58cc445e5ab21fb26ee6d92c441374
77
api_version: v1alpha1
88
aws_sdk_go_version: v1.49.13
99
generator_config_info:
10-
file_checksum: 89a00ff29a62ca0e86a8c08b275a6e9e708004b0
10+
file_checksum: ac8c0c6f258d7b552ccbaf81401e6c511de3373e
1111
original_file_name: generator.yaml
1212
last_modification:
1313
reason: API generation

apis/v1alpha1/generator.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,11 +395,15 @@ resources:
395395
AccessPolicies:
396396
custom_field:
397397
list_of: AssociateAccessPolicyInput
398+
compare:
399+
is_ignored: true
398400
AccessPolicies.AccessScope.Type:
399401
go_tag: json:"type,omitempty"
400402
Type:
401403
go_tag: json:"type,omitempty"
402404
hooks:
405+
delta_pre_compare:
406+
code: customPreCompare(delta, a, b)
403407
sdk_read_one_post_set_output:
404408
template_path: hooks/access_entry/sdk_read_one_post_set_output.go.tpl
405409
sdk_update_pre_build_request:

generator.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,11 +395,15 @@ resources:
395395
AccessPolicies:
396396
custom_field:
397397
list_of: AssociateAccessPolicyInput
398+
compare:
399+
is_ignored: true
398400
AccessPolicies.AccessScope.Type:
399401
go_tag: json:"type,omitempty"
400402
Type:
401403
go_tag: json:"type,omitempty"
402404
hooks:
405+
delta_pre_compare:
406+
code: customPreCompare(delta, a, b)
403407
sdk_read_one_post_set_output:
404408
template_path: hooks/access_entry/sdk_read_one_post_set_output.go.tpl
405409
sdk_update_pre_build_request:

pkg/resource/access_entry/delta.go

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

pkg/resource/access_entry/hooks.go

Lines changed: 121 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ package access_entry
1515

1616
import (
1717
"context"
18-
"reflect"
1918

19+
ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
2020
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
21+
"github.com/aws/aws-sdk-go/aws"
2122
svcsdk "github.com/aws/aws-sdk-go/service/eks"
2223

2324
"github.com/aws-controllers-k8s/eks-controller/apis/v1alpha1"
@@ -87,34 +88,21 @@ func (rm *resourceManager) syncAccessPolicies(ctx context.Context, desired, late
8788
rlog := ackrtlog.FromContext(ctx)
8889
exit := rlog.Trace("rm.syncAccessPolicies")
8990
defer func() { exit(err) }()
90-
toAdd := []*v1alpha1.AssociateAccessPolicyInput{}
91-
toDelete := []*string{}
9291

9392
existingPolicies := latest.ko.Spec.AccessPolicies
93+
desiredPolicies := desired.ko.Spec.AccessPolicies
9494

95-
// find the policies to add
96-
for _, p := range desired.ko.Spec.AccessPolicies {
97-
if !exactMatchInAccessPolicies(p, existingPolicies) {
98-
toAdd = append(toAdd, p)
99-
}
100-
}
95+
toAdd, toDelete := computeAccessPoliciesDelta(desiredPolicies, existingPolicies)
10196

102-
// find the policies to delete
103-
for _, p := range existingPolicies {
104-
if !inAccessPolicies(p, desired.ko.Spec.AccessPolicies) {
105-
toDelete = append(toDelete, p.PolicyARN)
106-
}
107-
}
108-
109-
// manage policies...
97+
// remove policies first (to avoid conflicts)
11098
for _, p := range toDelete {
111-
rlog.Debug("disassociating access policy from role", "policy_arn", *p)
99+
rlog.Debug("disassociating access policy from access entry", "policy_arn", *p)
112100
if err = rm.disassociateAccessPolicy(ctx, desired, p); err != nil {
113101
return err
114102
}
115103
}
116104
for _, p := range toAdd {
117-
rlog.Debug("associate access policy to access entry", "policy_arn", *p.PolicyARN)
105+
rlog.Debug("associating access policy to access entry", "policy_arn", *p.PolicyARN)
118106
if err = rm.associateAccessPolicy(ctx, desired, p); err != nil {
119107
return err
120108
}
@@ -123,28 +111,6 @@ func (rm *resourceManager) syncAccessPolicies(ctx context.Context, desired, late
123111
return nil
124112
}
125113

126-
// inAccessPolicies returns true if the supplied AccessPolicy ARN exists
127-
// in the slice of AccessPolicy objects.
128-
func inAccessPolicies(policy *v1alpha1.AssociateAccessPolicyInput, policies []*v1alpha1.AssociateAccessPolicyInput) bool {
129-
for _, p := range policies {
130-
if p.PolicyARN == policy.PolicyARN {
131-
return false
132-
}
133-
}
134-
return false
135-
}
136-
137-
// exactMatchInAccessPolicies returns true if the supplied AccessPolicy is in the
138-
// slice of AccessPolicy objects and the AccessScope is exactly the same.
139-
func exactMatchInAccessPolicies(policy *v1alpha1.AssociateAccessPolicyInput, policies []*v1alpha1.AssociateAccessPolicyInput) bool {
140-
for _, p := range policies {
141-
if p.PolicyARN == policy.PolicyARN {
142-
return reflect.DeepEqual(p.AccessScope, policy.AccessScope)
143-
}
144-
}
145-
return false
146-
}
147-
148114
// associateAccessPolicy adds the supplied AccessPolicy to the supplied
149115
// AccessEntry resource.
150116
func (rm *resourceManager) associateAccessPolicy(
@@ -153,7 +119,7 @@ func (rm *resourceManager) associateAccessPolicy(
153119
entry *v1alpha1.AssociateAccessPolicyInput,
154120
) (err error) {
155121
rlog := ackrtlog.FromContext(ctx)
156-
exit := rlog.Trace("rm.addManagedPolicy")
122+
exit := rlog.Trace("rm.associateAccessPolicy")
157123
defer func() { exit(err) }()
158124

159125
input := &svcsdk.AssociateAccessPolicyInput{
@@ -190,3 +156,116 @@ func (rm *resourceManager) disassociateAccessPolicy(
190156
rm.metrics.RecordAPICall("UPDATE", "DisassociateAccessPolicy", err)
191157
return err
192158
}
159+
160+
// inAccessPolicies returns true if the supplied AccessPolicy ARN exists
161+
// in the slice of AccessPolicy objects.
162+
func inAccessPolicies(policy *v1alpha1.AssociateAccessPolicyInput, policies []*v1alpha1.AssociateAccessPolicyInput) bool {
163+
for _, p := range policies {
164+
if *p.PolicyARN == *policy.PolicyARN {
165+
return true
166+
}
167+
}
168+
return false
169+
}
170+
171+
// equalAccessScopes returns true if the supplied AccessScope objects are
172+
// exactly the same.
173+
func equalAccessScopes(a, b *v1alpha1.AccessScope) bool {
174+
if a == nil && b == nil {
175+
return true
176+
}
177+
if a == nil {
178+
return equalZeroString(b.Type) && len(b.Namespaces) == 0
179+
}
180+
if b == nil {
181+
return equalZeroString(b.Type) && len(a.Namespaces) == 0
182+
}
183+
return equalStrings(a.Type, b.Type) && ackcompare.SliceStringPEqual(a.Namespaces, b.Namespaces)
184+
}
185+
186+
// exactMatchInAccessPolicies returns true if the supplied AccessPolicy is in the
187+
// slice of AccessPolicy objects and the AccessScope is exactly the same.
188+
func exactMatchInAccessPolicies(policy *v1alpha1.AssociateAccessPolicyInput, policies []*v1alpha1.AssociateAccessPolicyInput) bool {
189+
for _, p := range policies {
190+
if p.PolicyARN == nil {
191+
continue
192+
}
193+
if *p.PolicyARN == *policy.PolicyARN {
194+
return equalAccessScopes(p.AccessScope, policy.AccessScope)
195+
}
196+
}
197+
return false
198+
}
199+
200+
// computeAccessPoliciesDelta returns two slices of AccessPolicy objects: one
201+
// slice of AccessPolicy objects that are in the desired slice but not in the
202+
// latest slice, and one slice of AccessPolicy objects that are in the latest
203+
// slice but not in the desired slice.
204+
func computeAccessPoliciesDelta(desired, latest []*v1alpha1.AssociateAccessPolicyInput) (toAdd []*v1alpha1.AssociateAccessPolicyInput, toDelete []*string) {
205+
// useful for the toDelete elements
206+
visited := map[string]bool{}
207+
208+
// First we need to loop through the desired policies and see if they are
209+
// in the latest policies. If they are, we need to check if the AccessScope
210+
// is the same. If it is, we don't need to do anything. If it's not, we need
211+
// to add the policy to the toAdd slice and remove it from the toDelete slice.
212+
//
213+
// The delete is necessary because the API dosen't allow us to update the
214+
// AccessScope of an existing policy. We need to disassociate the policy and
215+
// then reassociate it.
216+
for _, p := range desired {
217+
visited[*p.PolicyARN] = true
218+
// If it's an exact match, we don't need to do anything.
219+
if exactMatchInAccessPolicies(p, latest) {
220+
continue
221+
}
222+
// If it's in the latest policies, but the AccessScope is different, we
223+
// need to remove it from the toDelete slice (update).
224+
if inAccessPolicies(p, latest) {
225+
toAdd = append(toAdd, p)
226+
toDelete = append(toDelete, p.PolicyARN)
227+
} else {
228+
// If it's not in the latest policies, we need to add it.
229+
toAdd = append(toAdd, p)
230+
}
231+
}
232+
233+
// Now that we've handled the desired policies, we need to loop through the
234+
// latest policies and see if they are in the desired policies. If they are
235+
// not, we need to add them to the toDelete slice.
236+
for _, p := range latest {
237+
// If we've already visited this policy, we don't need to do anything.
238+
if visited[*p.PolicyARN] {
239+
continue
240+
}
241+
// If it's not in the desired policies, we need to remove it.
242+
if !inAccessPolicies(p, desired) {
243+
toDelete = append(toDelete, p.PolicyARN)
244+
}
245+
}
246+
return toAdd, toDelete
247+
}
248+
249+
func customPreCompare(delta *ackcompare.Delta, a, b *resource) {
250+
if len(a.ko.Spec.AccessPolicies) != len(b.ko.Spec.AccessPolicies) {
251+
delta.Add("Spec.AccessPolicies", a.ko.Spec.AccessPolicies, b.ko.Spec.AccessPolicies)
252+
} else if toAdd, toRemove := computeAccessPoliciesDelta(a.ko.Spec.AccessPolicies, b.ko.Spec.AccessPolicies); len(toAdd) > 0 || len(toRemove) > 0 {
253+
delta.Add("Spec.AccessPolicies", a.ko.Spec.AccessPolicies, b.ko.Spec.AccessPolicies)
254+
}
255+
}
256+
257+
// EqualStrings returns true if two strings are equal e.g., both are nil, one is
258+
// nil and the other is empty string, or both non-zero strings are equal.
259+
func equalStrings(a, b *string) bool {
260+
if a == nil {
261+
return b == nil || *b == ""
262+
}
263+
if b == nil {
264+
return *a == ""
265+
}
266+
return *a == *b
267+
}
268+
269+
func equalZeroString(a *string) bool {
270+
return equalStrings(a, aws.String(""))
271+
}

0 commit comments

Comments
 (0)