Skip to content

Commit cda1787

Browse files
committed
Adds support for reconciling EKS AccessEntries
1 parent 464f490 commit cda1787

File tree

3 files changed

+286
-0
lines changed

3 files changed

+286
-0
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package eks
18+
19+
import (
20+
"context"
21+
"slices"
22+
23+
"github.com/aws/aws-sdk-go-v2/service/eks"
24+
ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
25+
"github.com/pkg/errors"
26+
27+
ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/eks/api/v1beta2"
28+
"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/record"
29+
)
30+
31+
func (s *Service) reconcileAccessEntries(ctx context.Context) error {
32+
if s.scope.ControlPlane.Spec.AccessConfig == nil || len(s.scope.ControlPlane.Spec.AccessConfig.AccessEntries) == 0 {
33+
s.scope.Info("no access entries defined, skipping reconcile")
34+
return nil
35+
}
36+
37+
existingAccessEntries, err := s.getExistingAccessEntries(ctx)
38+
if err != nil {
39+
return errors.Wrap(err, "failed to list existing access entries")
40+
}
41+
42+
for _, accessEntry := range s.scope.ControlPlane.Spec.AccessConfig.AccessEntries {
43+
if _, exists := existingAccessEntries[accessEntry.PrincipalARN]; exists {
44+
if err := s.updateAccessEntry(ctx, accessEntry); err != nil {
45+
return errors.Wrapf(err, "failed to update access entry for principal %s", accessEntry.PrincipalARN)
46+
}
47+
delete(existingAccessEntries, accessEntry.PrincipalARN)
48+
} else {
49+
if err := s.createAccessEntry(ctx, accessEntry); err != nil {
50+
return errors.Wrapf(err, "failed to create access entry for principal %s", accessEntry.PrincipalARN)
51+
}
52+
}
53+
}
54+
55+
for principalArn := range existingAccessEntries {
56+
if err := s.deleteAccessEntry(ctx, principalArn); err != nil {
57+
return errors.Wrapf(err, "failed to delete access entry for principal %s", principalArn)
58+
}
59+
}
60+
61+
record.Event(s.scope.ControlPlane, "SuccessfulReconcileAccessEntries", "Reconciled access entries")
62+
return nil
63+
}
64+
65+
func (s *Service) getExistingAccessEntries(ctx context.Context) (map[string]bool, error) {
66+
existingAccessEntries := make(map[string]bool)
67+
var nextToken *string
68+
69+
clusterName := s.scope.KubernetesClusterName()
70+
for {
71+
input := &eks.ListAccessEntriesInput{
72+
ClusterName: &clusterName,
73+
NextToken: nextToken,
74+
}
75+
76+
output, err := s.EKSClient.ListAccessEntries(ctx, input)
77+
if err != nil {
78+
return nil, errors.Wrap(err, "failed to list access entries")
79+
}
80+
81+
for _, principalArn := range output.AccessEntries {
82+
existingAccessEntries[principalArn] = true
83+
}
84+
85+
if output.NextToken == nil {
86+
break
87+
}
88+
89+
nextToken = output.NextToken
90+
}
91+
92+
return existingAccessEntries, nil
93+
}
94+
95+
func (s *Service) createAccessEntry(ctx context.Context, accessEntry ekscontrolplanev1.AccessEntry) error {
96+
clusterName := s.scope.KubernetesClusterName()
97+
createInput := &eks.CreateAccessEntryInput{
98+
ClusterName: &clusterName,
99+
PrincipalArn: &accessEntry.PrincipalARN,
100+
}
101+
102+
if len(accessEntry.KubernetesGroups) > 0 {
103+
createInput.KubernetesGroups = accessEntry.KubernetesGroups
104+
}
105+
106+
if accessEntry.Type != "" {
107+
createInput.Type = &accessEntry.Type
108+
}
109+
110+
if accessEntry.Username != "" {
111+
createInput.Username = &accessEntry.Username
112+
}
113+
114+
if _, err := s.EKSClient.CreateAccessEntry(ctx, createInput); err != nil {
115+
return errors.Wrapf(err, "failed to create access entry for principal %s", accessEntry.PrincipalARN)
116+
}
117+
118+
if err := s.reconcileAccessPolicies(ctx, accessEntry); err != nil {
119+
return errors.Wrapf(err, "failed to reconcile access policies for principal %s", accessEntry.PrincipalARN)
120+
}
121+
122+
return nil
123+
}
124+
125+
func (s *Service) updateAccessEntry(ctx context.Context, accessEntry ekscontrolplanev1.AccessEntry) error {
126+
clusterName := s.scope.KubernetesClusterName()
127+
describeInput := &eks.DescribeAccessEntryInput{
128+
ClusterName: &clusterName,
129+
PrincipalArn: &accessEntry.PrincipalARN,
130+
}
131+
132+
describeOutput, err := s.EKSClient.DescribeAccessEntry(ctx, describeInput)
133+
if err != nil {
134+
return errors.Wrapf(err, "failed to describe access entry for principal %s", accessEntry.PrincipalARN)
135+
}
136+
137+
// EKS requires recreate when changing type
138+
if accessEntry.Type != *describeOutput.AccessEntry.Type {
139+
if err = s.deleteAccessEntry(ctx, accessEntry.PrincipalARN); err != nil {
140+
return errors.Wrapf(err, "failed to delete access entry for principal %s during recreation", accessEntry.PrincipalARN)
141+
}
142+
143+
if err = s.createAccessEntry(ctx, accessEntry); err != nil {
144+
return errors.Wrapf(err, "failed to recreate access entry for principal %s", accessEntry.PrincipalARN)
145+
}
146+
return nil
147+
}
148+
149+
slices.Sort(accessEntry.KubernetesGroups)
150+
slices.Sort(describeOutput.AccessEntry.KubernetesGroups)
151+
152+
updateInput := &eks.UpdateAccessEntryInput{
153+
ClusterName: &clusterName,
154+
PrincipalArn: &accessEntry.PrincipalARN,
155+
}
156+
157+
needsUpdate := false
158+
159+
if accessEntry.Username != *describeOutput.AccessEntry.Username {
160+
updateInput.Username = &accessEntry.Username
161+
needsUpdate = true
162+
}
163+
164+
if !slices.Equal(accessEntry.KubernetesGroups, describeOutput.AccessEntry.KubernetesGroups) {
165+
updateInput.KubernetesGroups = accessEntry.KubernetesGroups
166+
needsUpdate = true
167+
}
168+
169+
if needsUpdate {
170+
if _, err := s.EKSClient.UpdateAccessEntry(ctx, updateInput); err != nil {
171+
return errors.Wrapf(err, "failed to update access entry for principal %s", accessEntry.PrincipalARN)
172+
}
173+
}
174+
175+
if err := s.reconcileAccessPolicies(ctx, accessEntry); err != nil {
176+
return errors.Wrapf(err, "failed to reconcile access policies for principal %s", accessEntry.PrincipalARN)
177+
}
178+
179+
return nil
180+
}
181+
182+
func (s *Service) deleteAccessEntry(ctx context.Context, principalArn string) error {
183+
clusterName := s.scope.KubernetesClusterName()
184+
185+
if _, err := s.EKSClient.DeleteAccessEntry(ctx, &eks.DeleteAccessEntryInput{
186+
ClusterName: &clusterName,
187+
PrincipalArn: &principalArn,
188+
}); err != nil {
189+
return errors.Wrapf(err, "failed to delete access entry for principal %s", principalArn)
190+
}
191+
192+
return nil
193+
}
194+
195+
func (s *Service) reconcileAccessPolicies(ctx context.Context, accessEntry ekscontrolplanev1.AccessEntry) error {
196+
if accessEntry.Type == "EC2_LINUX" || accessEntry.Type == "EC2_WINDOWS" {
197+
s.scope.Info("Skipping access policy reconciliation for EC2 access type", "principalARN", accessEntry.PrincipalARN)
198+
return nil
199+
}
200+
201+
existingPolicies, err := s.getExistingAccessPolicies(ctx, accessEntry.PrincipalARN)
202+
if err != nil {
203+
return errors.Wrapf(err, "failed to get existing access policies for principal %s", accessEntry.PrincipalARN)
204+
}
205+
206+
clusterName := s.scope.KubernetesClusterName()
207+
208+
for _, policy := range accessEntry.AccessPolicies {
209+
input := &eks.AssociateAccessPolicyInput{
210+
ClusterName: &clusterName,
211+
PrincipalArn: &accessEntry.PrincipalARN,
212+
PolicyArn: &policy.PolicyARN,
213+
AccessScope: &ekstypes.AccessScope{
214+
Type: ekstypes.AccessScopeType(policy.AccessScope.Type),
215+
},
216+
}
217+
218+
if policy.AccessScope.Type == "namespace" && len(policy.AccessScope.Namespaces) > 0 {
219+
input.AccessScope.Namespaces = policy.AccessScope.Namespaces
220+
}
221+
222+
if _, err := s.EKSClient.AssociateAccessPolicy(ctx, input); err != nil {
223+
return errors.Wrapf(err, "failed to associate access policy %s", policy.PolicyARN)
224+
}
225+
226+
delete(existingPolicies, policy.PolicyARN)
227+
}
228+
229+
for policyARN := range existingPolicies {
230+
if _, err := s.EKSClient.DisassociateAccessPolicy(ctx, &eks.DisassociateAccessPolicyInput{
231+
ClusterName: &clusterName,
232+
PrincipalArn: &accessEntry.PrincipalARN,
233+
PolicyArn: &policyARN,
234+
}); err != nil {
235+
return errors.Wrapf(err, "failed to disassociate access policy %s", policyARN)
236+
}
237+
}
238+
239+
return nil
240+
}
241+
242+
func (s *Service) getExistingAccessPolicies(ctx context.Context, principalARN string) (map[string]ekstypes.AssociatedAccessPolicy, error) {
243+
existingPolicies := map[string]ekstypes.AssociatedAccessPolicy{}
244+
var nextToken *string
245+
clusterName := s.scope.KubernetesClusterName()
246+
247+
for {
248+
input := &eks.ListAssociatedAccessPoliciesInput{
249+
ClusterName: &clusterName,
250+
PrincipalArn: &principalARN,
251+
NextToken: nextToken,
252+
}
253+
254+
output, err := s.EKSClient.ListAssociatedAccessPolicies(ctx, input)
255+
if err != nil {
256+
return nil, errors.Wrapf(err, "failed to list associated access policies for principal %s", principalARN)
257+
}
258+
259+
for _, policy := range output.AssociatedAccessPolicies {
260+
existingPolicies[*policy.PolicyArn] = policy
261+
}
262+
263+
if output.NextToken == nil {
264+
break
265+
}
266+
267+
nextToken = output.NextToken
268+
}
269+
270+
return existingPolicies, nil
271+
}

pkg/cloud/services/eks/cluster.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,13 @@ func (s *Service) reconcileAccessConfig(ctx context.Context, accessConfig *eksty
606606
}
607607
}
608608

609+
if expectedAuthenticationMode == string(ekscontrolplanev1.EKSAuthenticationModeAPI) ||
610+
expectedAuthenticationMode == string(ekscontrolplanev1.EKSAuthenticationModeAPIAndConfigMap) {
611+
if err := s.reconcileAccessEntries(ctx); err != nil {
612+
return errors.Wrap(err, "failed to reconcile access entries")
613+
}
614+
}
615+
609616
return nil
610617
}
611618

pkg/cloud/services/eks/service.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ type EKSAPI interface {
6262
TagResource(ctx context.Context, params *eks.TagResourceInput, optFns ...func(*eks.Options)) (*eks.TagResourceOutput, error)
6363
UntagResource(ctx context.Context, params *eks.UntagResourceInput, optFns ...func(*eks.Options)) (*eks.UntagResourceOutput, error)
6464
DisassociateIdentityProviderConfig(ctx context.Context, params *eks.DisassociateIdentityProviderConfigInput, optFns ...func(*eks.Options)) (*eks.DisassociateIdentityProviderConfigOutput, error)
65+
ListAccessEntries(ctx context.Context, params *eks.ListAccessEntriesInput, optFns ...func(*eks.Options)) (*eks.ListAccessEntriesOutput, error)
66+
DescribeAccessEntry(ctx context.Context, params *eks.DescribeAccessEntryInput, optFns ...func(*eks.Options)) (*eks.DescribeAccessEntryOutput, error)
67+
CreateAccessEntry(ctx context.Context, params *eks.CreateAccessEntryInput, optFns ...func(*eks.Options)) (*eks.CreateAccessEntryOutput, error)
68+
UpdateAccessEntry(ctx context.Context, params *eks.UpdateAccessEntryInput, optFns ...func(*eks.Options)) (*eks.UpdateAccessEntryOutput, error)
69+
DeleteAccessEntry(ctx context.Context, params *eks.DeleteAccessEntryInput, optFns ...func(*eks.Options)) (*eks.DeleteAccessEntryOutput, error)
70+
ListAssociatedAccessPolicies(ctx context.Context, params *eks.ListAssociatedAccessPoliciesInput, optFns ...func(*eks.Options)) (*eks.ListAssociatedAccessPoliciesOutput, error)
71+
AssociateAccessPolicy(ctx context.Context, params *eks.AssociateAccessPolicyInput, optFns ...func(*eks.Options)) (*eks.AssociateAccessPolicyOutput, error)
72+
DisassociateAccessPolicy(ctx context.Context, params *eks.DisassociateAccessPolicyInput, optFns ...func(*eks.Options)) (*eks.DisassociateAccessPolicyOutput, error)
6573

6674
// Waiters for EKS Cluster
6775
WaitUntilClusterActive(ctx context.Context, params *eks.DescribeClusterInput, maxWait time.Duration) error

0 commit comments

Comments
 (0)