Skip to content

Commit 67b4f1a

Browse files
✨ Add AWS IAM support (open-cluster-management-io#677)
* Adding AWS IAM authentication support Signed-off-by: EmilyL <[email protected]> * Remove the bootstrapKubeconfigEventHandler field that's no longer used from the NewSpokeAgentConfig function based on code review comments. Signed-off-by: Suvaansh <[email protected]> * Add a comment on what the IsEksArnWellFormed function does and an example EKS ARN. Signed-off-by: Suvaansh <[email protected]> Signed-off-by: Emily Li <[email protected]> --------- Signed-off-by: EmilyL <[email protected]> Signed-off-by: Suvaansh <[email protected]> Signed-off-by: Emily Li <[email protected]> Co-authored-by: EmilyL <[email protected]>
1 parent 2549388 commit 67b4f1a

File tree

14 files changed

+841
-27
lines changed

14 files changed

+841
-27
lines changed

CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ kubectl edit klusterlet klusterlet
169169

170170
### Integration tests
171171

172-
The integration tests are written in the [test/integration](test/integration) directory. They start a kubenretes
172+
The integration tests are written in the [test/integration](test/integration) directory. They start a kubernetes
173173
api server locally with [controller-runtime](https://book.kubebuilder.io/reference/envtest), and run the tests against
174174
the local api server.
175175

manifests/klusterlet/management/klusterlet-agent-deployment.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ spec:
109109
{{if .AppliedManifestWorkEvictionGracePeriod}}
110110
- "--appliedmanifestwork-eviction-grace-period={{ .AppliedManifestWorkEvictionGracePeriod }}"
111111
{{end}}
112+
{{if .RegistrationDriver.AuthType}}
113+
- "--registration-auth={{ .RegistrationDriver.AuthType }}"
114+
{{end}}
115+
{{if eq .RegistrationDriver.AuthType "awsirsa"}}
116+
- "--hub-cluster-arn={{ .RegistrationDriver.AwsIrsa.HubClusterArn }}"
117+
{{end}}
112118
env:
113119
- name: POD_NAME
114120
valueFrom:

manifests/klusterlet/management/klusterlet-registration-deployment.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ spec:
9797
{{if gt .RegistrationKubeAPIBurst 0}}
9898
- "--kube-api-burst={{ .RegistrationKubeAPIBurst }}"
9999
{{end}}
100+
{{if .RegistrationDriver.AuthType}}
101+
- "--registration-auth={{ .RegistrationDriver.AuthType }}"
102+
{{end}}
103+
{{if eq .RegistrationDriver.AuthType "awsirsa"}}
104+
- "--hub-cluster-arn={{ .RegistrationDriver.AwsIrsa.HubClusterArn }}"
105+
{{end}}
100106
env:
101107
- name: POD_NAME
102108
valueFrom:

pkg/operator/operators/klusterlet/controllers/klusterletcontroller/klusterlet_controller.go

+26-2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const (
4141
klusterletFinalizer = "operator.open-cluster-management.io/klusterlet-cleanup"
4242
managedResourcesEvictionTimestampAnno = "operator.open-cluster-management.io/managed-resources-eviction-timestamp"
4343
klusterletNamespaceLabelKey = "operator.open-cluster-management.io/klusterlet"
44+
AwsIrsaAuthType = "awsirsa"
4445
)
4546

4647
type klusterletController struct {
@@ -111,6 +112,15 @@ func NewKlusterletController(
111112
ToController("KlusterletController", recorder)
112113
}
113114

115+
type AwsIrsa struct {
116+
HubClusterArn string
117+
}
118+
119+
type RegistrationDriver struct {
120+
AuthType string
121+
AwsIrsa *AwsIrsa
122+
}
123+
114124
// klusterletConfig is used to render the template of hub manifests
115125
type klusterletConfig struct {
116126
KlusterletName string
@@ -174,7 +184,8 @@ type klusterletConfig struct {
174184
DisableAddonNamespace bool
175185

176186
// Labels of the agents are synced from klusterlet CR.
177-
Labels map[string]string
187+
Labels map[string]string
188+
RegistrationDriver RegistrationDriver
178189
}
179190

180191
// If multiplehubs feature gate is enabled, using the bootstrapkubeconfigs from klusterlet CR.
@@ -309,7 +320,20 @@ func (n *klusterletController) sync(ctx context.Context, controllerContext facto
309320
config.ClientCertExpirationSeconds = klusterlet.Spec.RegistrationConfiguration.ClientCertExpirationSeconds
310321
config.RegistrationKubeAPIQPS = float32(klusterlet.Spec.RegistrationConfiguration.KubeAPIQPS)
311322
config.RegistrationKubeAPIBurst = klusterlet.Spec.RegistrationConfiguration.KubeAPIBurst
312-
323+
//Configuring Registration driver depending on registration auth
324+
if &klusterlet.Spec.RegistrationConfiguration.RegistrationDriver != nil &&
325+
klusterlet.Spec.RegistrationConfiguration.RegistrationDriver.AuthType == AwsIrsaAuthType {
326+
config.RegistrationDriver = RegistrationDriver{
327+
AuthType: klusterlet.Spec.RegistrationConfiguration.RegistrationDriver.AuthType,
328+
AwsIrsa: &AwsIrsa{
329+
HubClusterArn: klusterlet.Spec.RegistrationConfiguration.RegistrationDriver.AwsIrsa.HubClusterArn,
330+
},
331+
}
332+
} else {
333+
config.RegistrationDriver = RegistrationDriver{
334+
AuthType: klusterlet.Spec.RegistrationConfiguration.RegistrationDriver.AuthType,
335+
}
336+
}
313337
// construct cluster annotations string, the final format is "key1=value1,key2=value2"
314338
var annotationsArray []string
315339
for k, v := range commonhelpers.FilterClusterAnnotations(klusterlet.Spec.RegistrationConfiguration.ClusterAnnotations) {

pkg/operator/operators/klusterlet/controllers/klusterletcontroller/klusterlet_controller_test.go

+110-7
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func newKlusterlet(name, namespace, clustername string) *operatorapiv1.Klusterle
134134

135135
func newKlusterletHosted(name, namespace, clustername string) *operatorapiv1.Klusterlet {
136136
klusterlet := newKlusterlet(name, namespace, clustername)
137+
klusterlet.Spec.RegistrationConfiguration.RegistrationDriver = operatorapiv1.RegistrationDriver{}
137138
klusterlet.Spec.DeployOption.Mode = operatorapiv1.InstallModeHosted
138139
klusterlet.Finalizers = append(klusterlet.Finalizers, klusterletHostedFinalizer)
139140
return klusterlet
@@ -374,7 +375,46 @@ func getDeployments(actions []clienttesting.Action, verb, suffix string) *appsv1
374375
return nil
375376
}
376377

377-
func assertRegistrationDeployment(t *testing.T, actions []clienttesting.Action, verb, serverURL, clusterName string, replica int32) {
378+
func assertKlusterletDeployment(t *testing.T, actions []clienttesting.Action, verb, serverURL, clusterName string) {
379+
deployment := getDeployments(actions, verb, "agent")
380+
if deployment == nil {
381+
t.Errorf("klusterlet deployment not found")
382+
return
383+
}
384+
if len(deployment.Spec.Template.Spec.Containers) != 1 {
385+
t.Errorf("Expect 1 containers in deployment spec, actual %d", len(deployment.Spec.Template.Spec.Containers))
386+
return
387+
}
388+
389+
args := deployment.Spec.Template.Spec.Containers[0].Args
390+
expectedArgs := []string{
391+
"/registration-operator",
392+
"agent",
393+
fmt.Sprintf("--spoke-cluster-name=%s", clusterName),
394+
"--bootstrap-kubeconfig=/spoke/bootstrap/kubeconfig",
395+
}
396+
397+
if serverURL != "" {
398+
expectedArgs = append(expectedArgs, fmt.Sprintf("--spoke-external-server-urls=%s", serverURL))
399+
}
400+
401+
expectedArgs = append(expectedArgs, "--agent-id=", "--workload-source-driver=kube", "--workload-source-config=/spoke/hub-kubeconfig/kubeconfig")
402+
403+
if *deployment.Spec.Replicas == 1 {
404+
expectedArgs = append(expectedArgs, "--disable-leader-election")
405+
}
406+
407+
expectedArgs = append(expectedArgs, "--status-sync-interval=60s", "--kube-api-qps=20", "--kube-api-burst=60",
408+
"--registration-auth=awsirsa", "--hub-cluster-arn=arneks:us-west-2:123456789012:cluster/hub-cluster1")
409+
410+
if !equality.Semantic.DeepEqual(args, expectedArgs) {
411+
t.Errorf("Expect args %v, but got %v", expectedArgs, args)
412+
return
413+
}
414+
415+
}
416+
417+
func assertRegistrationDeployment(t *testing.T, actions []clienttesting.Action, verb, serverURL, clusterName string, replica int32, awsAuth bool) {
378418
deployment := getDeployments(actions, verb, "registration-agent")
379419
if deployment == nil {
380420
t.Errorf("registration deployment not found")
@@ -402,7 +442,9 @@ func assertRegistrationDeployment(t *testing.T, actions []clienttesting.Action,
402442
}
403443

404444
expectedArgs = append(expectedArgs, "--kube-api-qps=10", "--kube-api-burst=60")
405-
445+
if awsAuth {
446+
expectedArgs = append(expectedArgs, "--registration-auth=awsirsa", "--hub-cluster-arn=arneks:us-west-2:123456789012:cluster/hub-cluster1")
447+
}
406448
if !equality.Semantic.DeepEqual(args, expectedArgs) {
407449
t.Errorf("Expect args %v, but got %v", expectedArgs, args)
408450
return
@@ -944,6 +986,67 @@ func TestGetServersFromKlusterlet(t *testing.T) {
944986
}
945987
}
946988

989+
func TestAWSIrsaAuthInSingletonMode(t *testing.T) {
990+
klusterlet := newKlusterlet("klusterlet", "testns", "cluster1")
991+
awsIrsaRegistrationDriver := operatorapiv1.RegistrationDriver{
992+
AuthType: AwsIrsaAuthType,
993+
AwsIrsa: &operatorapiv1.AwsIrsa{
994+
HubClusterArn: "arneks:us-west-2:123456789012:cluster/hub-cluster1",
995+
},
996+
}
997+
klusterlet.Spec.RegistrationConfiguration.RegistrationDriver = awsIrsaRegistrationDriver
998+
klusterlet.Spec.DeployOption.Mode = operatorapiv1.InstallModeSingleton
999+
hubSecret := newSecret(helpers.HubKubeConfig, "testns")
1000+
hubSecret.Data["kubeconfig"] = []byte("dummuykubeconnfig")
1001+
hubSecret.Data["cluster-name"] = []byte("cluster1")
1002+
objects := []runtime.Object{
1003+
newNamespace("testns"),
1004+
newSecret(helpers.BootstrapHubKubeConfig, "testns"),
1005+
hubSecret,
1006+
}
1007+
1008+
syncContext := testingcommon.NewFakeSyncContext(t, "klusterlet")
1009+
controller := newTestController(t, klusterlet, syncContext.Recorder(), nil, false,
1010+
objects...)
1011+
1012+
err := controller.controller.sync(context.TODO(), syncContext)
1013+
if err != nil {
1014+
t.Errorf("Expected non error when sync, %v", err)
1015+
}
1016+
1017+
assertKlusterletDeployment(t, controller.kubeClient.Actions(), createVerb, "", "cluster1")
1018+
}
1019+
1020+
func TestAWSIrsaAuthInNonSingletonMode(t *testing.T) {
1021+
klusterlet := newKlusterlet("klusterlet", "testns", "cluster1")
1022+
awsIrsaRegistrationDriver := operatorapiv1.RegistrationDriver{
1023+
AuthType: AwsIrsaAuthType,
1024+
AwsIrsa: &operatorapiv1.AwsIrsa{
1025+
HubClusterArn: "arneks:us-west-2:123456789012:cluster/hub-cluster1",
1026+
},
1027+
}
1028+
klusterlet.Spec.RegistrationConfiguration.RegistrationDriver = awsIrsaRegistrationDriver
1029+
hubSecret := newSecret(helpers.HubKubeConfig, "testns")
1030+
hubSecret.Data["kubeconfig"] = []byte("dummuykubeconnfig")
1031+
hubSecret.Data["cluster-name"] = []byte("cluster1")
1032+
objects := []runtime.Object{
1033+
newNamespace("testns"),
1034+
newSecret(helpers.BootstrapHubKubeConfig, "testns"),
1035+
hubSecret,
1036+
}
1037+
1038+
syncContext := testingcommon.NewFakeSyncContext(t, "klusterlet")
1039+
controller := newTestController(t, klusterlet, syncContext.Recorder(), nil, false,
1040+
objects...)
1041+
1042+
err := controller.controller.sync(context.TODO(), syncContext)
1043+
if err != nil {
1044+
t.Errorf("Expected non error when sync, %v", err)
1045+
}
1046+
1047+
assertRegistrationDeployment(t, controller.kubeClient.Actions(), createVerb, "", "cluster1", 1, true)
1048+
}
1049+
9471050
func TestReplica(t *testing.T) {
9481051
klusterlet := newKlusterlet("klusterlet", "testns", "cluster1")
9491052
hubSecret := newSecret(helpers.HubKubeConfig, "testns")
@@ -965,7 +1068,7 @@ func TestReplica(t *testing.T) {
9651068
}
9661069

9671070
// should have 1 replica for registration deployment and 0 for work
968-
assertRegistrationDeployment(t, controller.kubeClient.Actions(), createVerb, "", "cluster1", 1)
1071+
assertRegistrationDeployment(t, controller.kubeClient.Actions(), createVerb, "", "cluster1", 1, false)
9691072
assertWorkDeployment(t, controller.kubeClient.Actions(), createVerb, "cluster1", operatorapiv1.InstallModeDefault, 0)
9701073

9711074
klusterlet = newKlusterlet("klusterlet", "testns", "cluster1")
@@ -1010,7 +1113,7 @@ func TestReplica(t *testing.T) {
10101113
}
10111114

10121115
// should have 3 replicas for clusters with multiple nodes
1013-
assertRegistrationDeployment(t, controller.kubeClient.Actions(), "update", "", "cluster1", 3)
1116+
assertRegistrationDeployment(t, controller.kubeClient.Actions(), "update", "", "cluster1", 3, false)
10141117
assertWorkDeployment(t, controller.kubeClient.Actions(), "update", "cluster1", operatorapiv1.InstallModeDefault, 3)
10151118
}
10161119

@@ -1031,7 +1134,7 @@ func TestClusterNameChange(t *testing.T) {
10311134
}
10321135

10331136
// Check if deployment has the right cluster name set
1034-
assertRegistrationDeployment(t, controller.kubeClient.Actions(), createVerb, "", "cluster1", 1)
1137+
assertRegistrationDeployment(t, controller.kubeClient.Actions(), createVerb, "", "cluster1", 1, false)
10351138

10361139
operatorAction := controller.operatorClient.Actions()
10371140
testingcommon.AssertActions(t, operatorAction, "patch")
@@ -1061,7 +1164,7 @@ func TestClusterNameChange(t *testing.T) {
10611164
if err != nil {
10621165
t.Errorf("Expected non error when sync, %v", err)
10631166
}
1064-
assertRegistrationDeployment(t, controller.kubeClient.Actions(), "update", "", "", 1)
1167+
assertRegistrationDeployment(t, controller.kubeClient.Actions(), "update", "", "", 1, false)
10651168

10661169
// Update hubconfigsecret and sync again
10671170
hubSecret.Data["cluster-name"] = []byte("cluster2")
@@ -1099,7 +1202,7 @@ func TestClusterNameChange(t *testing.T) {
10991202
if err != nil {
11001203
t.Errorf("Expected non error when sync, %v", err)
11011204
}
1102-
assertRegistrationDeployment(t, controller.kubeClient.Actions(), "update", "https://localhost", "cluster3", 1)
1205+
assertRegistrationDeployment(t, controller.kubeClient.Actions(), "update", "https://localhost", "cluster3", 1, false)
11031206
assertWorkDeployment(t, controller.kubeClient.Actions(), "update", "cluster3", "", 0)
11041207
}
11051208

pkg/registration/helpers/helpers.go

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package helpers
33
import (
44
"embed"
55
"net/url"
6+
"regexp"
67

78
"github.com/openshift/library-go/pkg/assets"
89
"github.com/openshift/library-go/pkg/operator/resource/resourceapply"
@@ -176,3 +177,14 @@ func IsCSRSupported(nativeClient kubernetes.Interface) (bool, bool, error) {
176177
}
177178
return v1CSRSupported, v1beta1CSRSupported, nil
178179
}
180+
181+
// IsEksArnWellFormed checks if the EKS cluster ARN is well-formed
182+
// Example of a well-formed ARN: arn:aws:eks:us-west-2:123456789012:cluster/my-cluster
183+
func IsEksArnWellFormed(eksArn string) bool {
184+
pattern := "^arn:aws:eks:([a-zA-Z0-9-]+):(\\d{12}):cluster/([a-zA-Z0-9-]+)$"
185+
matched, err := regexp.MatchString(pattern, eksArn)
186+
if err != nil {
187+
return false
188+
}
189+
return matched
190+
}

pkg/registration/helpers/testing/testinghelpers.go

+4
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,10 @@ type TestCert struct {
421421
Key []byte
422422
}
423423

424+
// TODO: Remove this struct once we have the function fully implemented for the AWSIRSADriver
425+
type TestIrsaRequest struct {
426+
}
427+
424428
func NewHubKubeconfigSecret(namespace, name, resourceVersion string, cert *TestCert, data map[string][]byte) *corev1.Secret {
425429
secret := &corev1.Secret{
426430
ObjectMeta: metav1.ObjectMeta{
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package aws_irsa
2+
3+
import (
4+
"k8s.io/client-go/tools/cache"
5+
6+
cluster "open-cluster-management.io/api/client/cluster/clientset/versioned"
7+
managedclusterv1client "open-cluster-management.io/api/client/cluster/clientset/versioned/typed/cluster/v1"
8+
managedclusterinformers "open-cluster-management.io/api/client/cluster/informers/externalversions/cluster"
9+
managedclusterv1lister "open-cluster-management.io/api/client/cluster/listers/cluster/v1"
10+
)
11+
12+
type AWSIRSAControl interface {
13+
isApproved(name string) (bool, error)
14+
generateEKSKubeConfig(name string) ([]byte, error)
15+
16+
// Informer is public so we can add indexer outside
17+
Informer() cache.SharedIndexInformer
18+
}
19+
20+
var _ AWSIRSAControl = &v1AWSIRSAControl{}
21+
22+
type v1AWSIRSAControl struct {
23+
hubManagedClusterInformer cache.SharedIndexInformer
24+
hubManagedClusterLister managedclusterv1lister.ManagedClusterLister
25+
hubManagedClusterClient managedclusterv1client.ManagedClusterInterface
26+
}
27+
28+
func (v *v1AWSIRSAControl) isApproved(name string) (bool, error) {
29+
// TODO: check if the managedclusuter cr on hub has required condition and is approved
30+
approved := false
31+
32+
return approved, nil
33+
}
34+
35+
func (v *v1AWSIRSAControl) generateEKSKubeConfig(name string) ([]byte, error) {
36+
// TODO: generate and return kubeconfig
37+
return nil, nil
38+
}
39+
40+
func (v *v1AWSIRSAControl) Informer() cache.SharedIndexInformer {
41+
return v.hubManagedClusterInformer
42+
}
43+
44+
//TODO: Uncomment the below once required in the aws irsa authentication implementation
45+
/*
46+
func (v *v1AWSIRSAControl) get(name string) (metav1.Object, error) {
47+
managedcluster, err := v.hubManagedClusterLister.Get(name)
48+
switch {
49+
case apierrors.IsNotFound(err):
50+
// fallback to fetching managedcluster from hub apiserver in case it is not cached by informer yet
51+
managedcluster, err = v.hubManagedClusterClient.Get(context.Background(), name, metav1.GetOptions{})
52+
if apierrors.IsNotFound(err) {
53+
return nil, fmt.Errorf("unable to get managedcluster %q. It might have already been deleted", name)
54+
}
55+
case err != nil:
56+
return nil, err
57+
}
58+
return managedcluster, nil
59+
}
60+
*/
61+
62+
func NewAWSIRSAControl(hubManagedClusterInformer managedclusterinformers.Interface, hubManagedClusterClient cluster.Interface) (AWSIRSAControl, error) {
63+
return &v1AWSIRSAControl{
64+
hubManagedClusterInformer: hubManagedClusterInformer.V1().ManagedClusters().Informer(),
65+
hubManagedClusterLister: hubManagedClusterInformer.V1().ManagedClusters().Lister(),
66+
hubManagedClusterClient: hubManagedClusterClient.ClusterV1().ManagedClusters(),
67+
}, nil
68+
}

0 commit comments

Comments
 (0)