Make ocm changes to tag aws resources by OCM hub (#862)

* Implementing aws resource tags

Signed-off-by: Amrutha <amrutha.hari12@gmail.com>
Signed-off-by: Alex <alexchan2988@gmail.com>
Signed-off-by: Gaurav Jaswal <jaswalkiranavtar@gmail.com>

* Fixing tests

Signed-off-by: Gaurav Jaswal <jaswalkiranavtar@gmail.com>

---------

Signed-off-by: Amrutha <amrutha.hari12@gmail.com>
Signed-off-by: Alex <alexchan2988@gmail.com>
Signed-off-by: Gaurav Jaswal <jaswalkiranavtar@gmail.com>
Co-authored-by: Gaurav Jaswal <jaswalkiranavtar@gmail.com>
This commit is contained in:
Amrutha Harikumar
2025-03-06 19:29:29 +00:00
committed by GitHub
co-authored by Gaurav Jaswal
parent 26e80ab089
commit c4e706f123
19 changed files with 667 additions and 92 deletions
@@ -269,19 +269,40 @@ spec:
- csr
- awsirsa
type: string
autoApprovedIdentities:
description: |-
For csr authentication type, AutoApprovedIdentities represent a list of approved users
For awsirsa authentication type, AutoApprovedIdentities represent a list of approved arn patterns
items:
type: string
type: array
hubClusterArn:
description: |-
This represents the hub cluster ARN
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
awsisra:
description: AwsIrsa represents the configuration for awsisra
driver.
properties:
autoApprovedIdentities:
description: AutoApprovedIdentities represent a list
of approved arn patterns
items:
type: string
type: array
hubClusterArn:
description: |-
This represents the hub cluster ARN
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
tags:
description: |-
List of tags to be added to AWS resources created by hub while processing awsirsa registration request
Example - "product:v1:tenant:app-name=My-App"
items:
type: string
type: array
type: object
csr:
description: CSR represents the configuration for csr driver.
properties:
autoApprovedIdentities:
description: AutoApprovedIdentities represent a list
of approved users
items:
type: string
type: array
type: object
type: object
type: array
x-kubernetes-list-map-keys:
@@ -269,19 +269,40 @@ spec:
- csr
- awsirsa
type: string
autoApprovedIdentities:
description: |-
For csr authentication type, AutoApprovedIdentities represent a list of approved users
For awsirsa authentication type, AutoApprovedIdentities represent a list of approved arn patterns
items:
type: string
type: array
hubClusterArn:
description: |-
This represents the hub cluster ARN
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
awsisra:
description: AwsIrsa represents the configuration for awsisra
driver.
properties:
autoApprovedIdentities:
description: AutoApprovedIdentities represent a list
of approved arn patterns
items:
type: string
type: array
hubClusterArn:
description: |-
This represents the hub cluster ARN
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
tags:
description: |-
List of tags to be added to AWS resources created by hub while processing awsirsa registration request
Example - "product:v1:tenant:app-name=My-App"
items:
type: string
type: array
type: object
csr:
description: CSR represents the configuration for csr driver.
properties:
autoApprovedIdentities:
description: AutoApprovedIdentities represent a list
of approved users
items:
type: string
type: array
type: object
type: object
type: array
x-kubernetes-list-map-keys:
@@ -269,19 +269,40 @@ spec:
- csr
- awsirsa
type: string
autoApprovedIdentities:
description: |-
For csr authentication type, AutoApprovedIdentities represent a list of approved users
For awsirsa authentication type, AutoApprovedIdentities represent a list of approved arn patterns
items:
type: string
type: array
hubClusterArn:
description: |-
This represents the hub cluster ARN
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
awsisra:
description: AwsIrsa represents the configuration for awsisra
driver.
properties:
autoApprovedIdentities:
description: AutoApprovedIdentities represent a list
of approved arn patterns
items:
type: string
type: array
hubClusterArn:
description: |-
This represents the hub cluster ARN
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
tags:
description: |-
List of tags to be added to AWS resources created by hub while processing awsirsa registration request
Example - "product:v1:tenant:app-name=My-App"
items:
type: string
type: array
type: object
csr:
description: CSR represents the configuration for csr driver.
properties:
autoApprovedIdentities:
description: AutoApprovedIdentities represent a list
of approved users
items:
type: string
type: array
type: object
type: object
type: array
x-kubernetes-list-map-keys:
+1 -1
View File
@@ -36,7 +36,7 @@ require (
k8s.io/kube-aggregator v0.31.4
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6
open-cluster-management.io/addon-framework v0.11.1-0.20250303151103-b2865de5c39b
open-cluster-management.io/api v0.15.1-0.20250219064651-4281b7684d9b
open-cluster-management.io/api v0.15.1-0.20250226073118-8c9793267c9e
open-cluster-management.io/sdk-go v0.15.1-0.20241125015855-1536c3970f8f
sigs.k8s.io/cluster-inventory-api v0.0.0-20240730014211-ef0154379848
sigs.k8s.io/controller-runtime v0.19.3
+2 -2
View File
@@ -489,8 +489,8 @@ k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
open-cluster-management.io/addon-framework v0.11.1-0.20250303151103-b2865de5c39b h1:vEemE32F9iiVvKfFsFEdiyGdDnSb9Cp9Dch2Jkc4Nfg=
open-cluster-management.io/addon-framework v0.11.1-0.20250303151103-b2865de5c39b/go.mod h1:3+UAkReHIEyqsDuq0Iv5w+ZRgZr254iehYV/JR2j038=
open-cluster-management.io/api v0.15.1-0.20250219064651-4281b7684d9b h1:1ScdOKBMLbzA/k84P9Z64uSq3sxRclquej3tT1zhsqU=
open-cluster-management.io/api v0.15.1-0.20250219064651-4281b7684d9b/go.mod h1:9erZEWEn4bEqh0nIX2wA7f/s3KCuFycQdBrPrRzi0QM=
open-cluster-management.io/api v0.15.1-0.20250226073118-8c9793267c9e h1:4iQneGfxartfFSR+IHZRrjEuwtRpiHyKQ15Kd33YCVk=
open-cluster-management.io/api v0.15.1-0.20250226073118-8c9793267c9e/go.mod h1:9erZEWEn4bEqh0nIX2wA7f/s3KCuFycQdBrPrRzi0QM=
open-cluster-management.io/sdk-go v0.15.1-0.20241125015855-1536c3970f8f h1:zeC7QrFNarfK2zY6jGtd+mX+yDrQQmnH/J8A7n5Nh38=
open-cluster-management.io/sdk-go v0.15.1-0.20241125015855-1536c3970f8f/go.mod h1:fi5WBsbC5K3txKb8eRLuP0Sim/Oqz/PHX18skAEyjiA=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY=
@@ -76,6 +76,9 @@ spec:
{{ if .AutoApprovedARNPatterns }}
- "--auto-approved-arn-patterns={{ .AutoApprovedARNPatterns }}"
{{ end }}
{{if .AwsResourceTags}}
- "--aws-resource-tags={{ .AwsResourceTags }}"
{{end}}
env:
- name: POD_NAME
valueFrom:
+1
View File
@@ -37,6 +37,7 @@ type HubConfig struct {
EnabledRegistrationDrivers string
AutoApprovedCSRUsers string
AutoApprovedARNPatterns string
AwsResourceTags string
}
type Webhook struct {
@@ -212,7 +212,7 @@ func (n *clusterManagerController) sync(ctx context.Context, controllerContext f
config.AddOnManagerEnabled = helpers.FeatureGateEnabled(addonFeatureGates, ocmfeature.DefaultHubAddonManagerFeatureGates, ocmfeature.AddonManagement)
// Compute and populate the value of managed cluster identity creator role to be used in cluster manager registration service account
config.ManagedClusterIdentityCreatorRole = getManagedClusterIdentityCreatorRolename(*clusterManager)
config.ManagedClusterIdentityCreatorRole = getIdentityCreatorRoleAndTags(*clusterManager)
// If we are deploying in the hosted mode, it requires us to create webhook in a different way with the default mode.
// In the hosted mode, the webhook servers is running in the management cluster but the users are accessing the hub cluster.
@@ -424,11 +424,11 @@ func (n *clusterManagerController) getImagePullSecret(ctx context.Context) (stri
return helpers.ImagePullSecret, nil
}
func getManagedClusterIdentityCreatorRolename(cm operatorapiv1.ClusterManager) string {
func getIdentityCreatorRoleAndTags(cm operatorapiv1.ClusterManager) string {
if cm.Spec.RegistrationConfiguration != nil {
for _, registrationDriver := range cm.Spec.RegistrationConfiguration.RegistrationDrivers {
if registrationDriver.AuthType == "awsirsa" {
hubClusterArn := registrationDriver.HubClusterArn
if registrationDriver.AuthType == "awsirsa" && registrationDriver.AwsIrsa != nil {
hubClusterArn := registrationDriver.AwsIrsa.HubClusterArn
hubClusterAccountId, hubClusterName := commonhelper.GetAwsAccountIdAndClusterName(hubClusterArn)
return "arn:aws:iam::" + hubClusterAccountId + ":role/" + hubClusterName + "_managed-cluster-identity-creator"
}
@@ -77,11 +77,12 @@ func (c *runtimeReconcile) reconcile(ctx context.Context, cm *operatorapiv1.Clus
var enabledRegistrationDrivers []string
for _, registrationDriver := range cm.Spec.RegistrationConfiguration.RegistrationDrivers {
enabledRegistrationDrivers = append(enabledRegistrationDrivers, registrationDriver.AuthType)
if registrationDriver.AuthType == "awsirsa" {
config.HubClusterArn = registrationDriver.HubClusterArn
config.AutoApprovedARNPatterns = strings.Join(registrationDriver.AutoApprovedIdentities, ",")
} else if registrationDriver.AuthType == "csr" {
config.AutoApprovedCSRUsers = strings.Join(registrationDriver.AutoApprovedIdentities, ",")
if registrationDriver.AuthType == "awsirsa" && registrationDriver.AwsIrsa != nil {
config.HubClusterArn = registrationDriver.AwsIrsa.HubClusterArn
config.AutoApprovedARNPatterns = strings.Join(registrationDriver.AwsIrsa.AutoApprovedIdentities, ",")
config.AwsResourceTags = strings.Join(registrationDriver.AwsIrsa.Tags, ",")
} else if registrationDriver.AuthType == "csr" && registrationDriver.CSR != nil {
config.AutoApprovedCSRUsers = strings.Join(registrationDriver.CSR.AutoApprovedIdentities, ",")
}
}
config.EnabledRegistrationDrivers = strings.Join(enabledRegistrationDrivers, ",")
+3 -1
View File
@@ -55,6 +55,7 @@ type HubManagerOptions struct {
HubClusterArn string
AutoApprovedCSRUsers []string
AutoApprovedARNPatterns []string
AwsResourceTags []string
}
// NewHubManagerOptions returns a HubManagerOptions
@@ -86,6 +87,7 @@ func (m *HubManagerOptions) AddFlags(fs *pflag.FlagSet) {
"A bootstrap user list whose cluster registration requests can be automatically approved.")
fs.StringSliceVar(&m.AutoApprovedARNPatterns, "auto-approved-arn-patterns", m.AutoApprovedARNPatterns,
"A list of AWS EKS ARN patterns such that an EKS cluster will be auto approved if its ARN matches with any of the patterns")
fs.StringSliceVar(&m.AwsResourceTags, "aws-resource-tags", m.AwsResourceTags, "A list of tags to apply to AWS resources created through the OCM controllers")
m.ImportOption.AddFlags(fs)
}
@@ -179,7 +181,7 @@ func (m *HubManagerOptions) RunControllerManagerWithInformers(
}
drivers = append(drivers, csrDriver)
case "awsirsa":
awsIRSAHubDriver, err := awsirsa.NewAWSIRSAHubDriver(ctx, m.HubClusterArn, m.AutoApprovedARNPatterns)
awsIRSAHubDriver, err := awsirsa.NewAWSIRSAHubDriver(ctx, m.HubClusterArn, m.AutoApprovedARNPatterns, m.AwsResourceTags)
if err != nil {
return err
}
@@ -15,6 +15,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/eks"
"github.com/aws/aws-sdk-go-v2/service/eks/types"
"github.com/aws/aws-sdk-go-v2/service/iam"
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"k8s.io/klog/v2"
clusterv1 "open-cluster-management.io/api/cluster/v1"
@@ -34,6 +35,7 @@ type AWSIRSAHubDriver struct {
hubClusterArn string
cfg aws.Config
autoApprovedARNPatterns []*regexp.Regexp
awsResourceTags []string
}
func (a *AWSIRSAHubDriver) Accept(cluster *clusterv1.ManagedCluster) bool {
@@ -107,12 +109,12 @@ func (a *AWSIRSAHubDriver) CreatePermissions(ctx context.Context, cluster *clust
// Create an EKS client
eksClient := eks.NewFromConfig(a.cfg)
hubClusterName, roleArn, err := createIAMRoleAndPolicy(ctx, a.hubClusterArn, cluster, a.cfg)
hubClusterName, roleArn, err := createIAMRoleAndPolicy(ctx, a.hubClusterArn, cluster, a.cfg, a.awsResourceTags)
if err != nil {
return err
}
err = createAccessEntry(ctx, eksClient, roleArn, hubClusterName, cluster.Name)
err = createAccessEntry(ctx, eksClient, roleArn, hubClusterName, cluster.Name, a.awsResourceTags)
if err != nil {
return err
}
@@ -123,7 +125,8 @@ func (a *AWSIRSAHubDriver) CreatePermissions(ctx context.Context, cluster *clust
// This function creates:
// 1. IAM Role and Policy in the hub cluster IAM
// 2. Returns the hubClusterName and the roleArn to be used for Access Entry creation
func createIAMRoleAndPolicy(ctx context.Context, hubClusterArn string, managedCluster *v1.ManagedCluster, cfg aws.Config) (string, string, error) {
func createIAMRoleAndPolicy(ctx context.Context, hubClusterArn string, managedCluster *v1.ManagedCluster, cfg aws.Config,
awsResourceTags []string) (string, string, error) {
logger := klog.FromContext(ctx)
var managedClusterIamRoleSuffix string
var createRoleOutput *iam.CreateRoleOutput
@@ -172,9 +175,17 @@ func createIAMRoleAndPolicy(ctx context.Context, hubClusterArn string, managedCl
return hubClusterName, roleArn, err
}
parsedTags, err := parseTagsForRolesAndPolicies(awsResourceTags)
if err != nil {
logger.V(4).Error(err, "Failed to parse tags for AWS roles and policies", "ManagedCluster", managedClusterName)
return hubClusterName, roleArn, err
}
createRoleOutput, err = iamClient.CreateRole(ctx, &iam.CreateRoleInput{
RoleName: aws.String(roleName),
AssumeRolePolicyDocument: aws.String(renderedTemplates[1]),
Tags: parsedTags,
})
if err != nil {
// Ignore error when role already exists as we will always create the same role
@@ -248,13 +259,21 @@ func renderTemplates(argTemplates []string, data interface{}) (args []string, er
}
// This function creates access entry which allow access to an IAM role from outside the cluster
func createAccessEntry(ctx context.Context, eksClient *eks.Client, roleArn string, hubClusterName string, managedClusterName string) error {
func createAccessEntry(ctx context.Context, eksClient *eks.Client, roleArn string, hubClusterName string, managedClusterName string,
awsResourceTags []string) error {
logger := klog.FromContext(ctx)
tagsForAccessEntry, err := parseTagsForAccessEntry(awsResourceTags)
if err != nil {
logger.V(4).Error(err, "Failed to parse tags during AWS access entry creation", "ManagedCluster", managedClusterName)
return err
}
params := &eks.CreateAccessEntryInput{
ClusterName: aws.String(hubClusterName),
PrincipalArn: aws.String(roleArn),
Username: aws.String(managedClusterName),
KubernetesGroups: []string{fmt.Sprintf("open-cluster-management:%s", managedClusterName)},
Tags: tagsForAccessEntry,
}
createAccessEntryOutput, err := eksClient.CreateAccessEntry(ctx, params, func(opts *eks.Options) {
@@ -362,7 +381,40 @@ func deleteAccessEntry(ctx context.Context, eksClient *eks.Client, roleArn strin
return nil
}
func NewAWSIRSAHubDriver(ctx context.Context, hubClusterArn string, autoApprovedIdentityPatterns []string) (register.HubDriver, error) {
func parseTagsForAccessEntry(tags []string) (map[string]string, error) {
parsedTags := map[string]string{}
for _, tag := range tags {
splitTag := strings.Split(tag, "=")
if len(splitTag) != 2 {
return nil, fmt.Errorf("missing value in the tag")
}
key, value := splitTag[0], splitTag[1]
parsedTags[key] = value
}
return parsedTags, nil
}
func parseTagsForRolesAndPolicies(tags []string) ([]iamtypes.Tag, error) {
var parsedTags []iamtypes.Tag
for _, tag := range tags {
splitTag := strings.Split(tag, "=")
if len(splitTag) != 2 {
return nil, fmt.Errorf("missing value from tag")
}
key, value := splitTag[0], splitTag[1]
parsedTags = append(parsedTags, iamtypes.Tag{
Key: &key,
Value: &value,
})
}
return parsedTags, nil
}
func NewAWSIRSAHubDriver(ctx context.Context, hubClusterArn string, autoApprovedIdentityPatterns []string,
awsResourceTags []string) (register.HubDriver, error) {
logger := klog.FromContext(ctx)
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
@@ -383,6 +435,7 @@ func NewAWSIRSAHubDriver(ctx context.Context, hubClusterArn string, autoApproved
hubClusterArn: hubClusterArn,
cfg: cfg,
autoApprovedARNPatterns: compiledPatterns,
awsResourceTags: awsResourceTags,
}
return awsIRSADriverForHub, nil
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"reflect"
"regexp"
"strings"
"testing"
@@ -123,7 +124,7 @@ func TestAccept(t *testing.T) {
[]string{
"arn:aws:eks:us-west-2:123456789012:cluster/.*",
"arn:aws:eks:us-west-1:123456789012:cluster/.*",
},
}, []string{},
)
if err != nil {
@@ -144,7 +145,7 @@ func TestNewDriverValidation(t *testing.T) {
// Test with an invalid manager cluster approval pattern
_, err := NewAWSIRSAHubDriver(context.Background(), "arn:aws:eks:us-west-2:123456789012:cluster/hub-cluster", []string{
"arn:(aws:eks:us-west-2:123456789012:cluster/.*", // bad pattern
})
}, []string{})
if err == nil {
t.Errorf("Error expected")
}
@@ -568,7 +569,7 @@ func TestCleanup(t *testing.T) {
t.Fatal(err)
}
awsIrsaHubDriver, err := NewAWSIRSAHubDriver(context.Background(), "arn:aws:eks:us-west-2:123456789012:cluster/hub-cluster", []string{})
awsIrsaHubDriver, err := NewAWSIRSAHubDriver(context.Background(), "arn:aws:eks:us-west-2:123456789012:cluster/hub-cluster", []string{}, []string{})
if err != nil {
t.Errorf("error creating AWSIRSAHubDriver")
return
@@ -939,8 +940,9 @@ func TestCreateIAMRoleAndPolicy(t *testing.T) {
managedCluster := testinghelpers.NewManagedCluster()
managedCluster.Annotations = tt.managedClusterAnnotations
tags := []string{}
_, _, err = createIAMRoleAndPolicy(tt.args.ctx, HubClusterArn, managedCluster, cfg)
_, _, err = createIAMRoleAndPolicy(tt.args.ctx, HubClusterArn, managedCluster, cfg, tags)
if (err != nil) != tt.wantErr {
t.Errorf("error = %#v, wantErr %#v", err, tt.wantErr)
return
@@ -1045,8 +1047,9 @@ func TestCreateAccessEntries(t *testing.T) {
managedCluster := testinghelpers.NewManagedCluster()
managedCluster.Annotations = tt.managedClusterAnnotations
tags := []string{}
err = createAccessEntry(tt.args.ctx, eksClient, principalArn, hubClusterName, managedClusterName)
err = createAccessEntry(tt.args.ctx, eksClient, principalArn, hubClusterName, managedClusterName, tags)
if (err != nil) != tt.wantErr {
t.Errorf("error = %#v, wantErr %#v", err, tt.wantErr)
return
@@ -1057,3 +1060,250 @@ func TestCreateAccessEntries(t *testing.T) {
})
}
}
func TestCreateTags(t *testing.T) {
type args struct {
ctx context.Context
withAPIOptionsFunc func(*middleware.Stack) error
}
cases := []struct {
name string
args args
managedClusterAnnotations map[string]string
want error
wantErr bool
tags []string
}{
{
name: "test create IAM Role and Policy with Tags",
args: args{
ctx: context.Background(),
withAPIOptionsFunc: func(stack *middleware.Stack) error {
return stack.Finalize.Add(
middleware.FinalizeMiddlewareFunc(
"CreateRoleAndPolicyWithTagsMock",
func(ctx context.Context, input middleware.FinalizeInput, handler middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) {
operationName := middleware.GetOperationName(ctx)
if operationName == "CreateRole" {
return middleware.FinalizeOutput{
Result: &iam.CreateRoleOutput{Role: &iamtypes.Role{
RoleName: aws.String("TestRole"),
Arn: aws.String("arn:aws:iam::123456789012:role/TestRole"),
Tags: []iamtypes.Tag{
{
Key: aws.String("product:v1:tenant:app-name"),
Value: aws.String("My-App"),
},
{
Key: aws.String("product:v1:tenant:created-by"),
Value: aws.String("Team-1"),
},
},
},
},
}, middleware.Metadata{}, nil
}
if operationName == "CreatePolicy" {
return middleware.FinalizeOutput{
Result: &iam.CreatePolicyOutput{Policy: &iamtypes.Policy{
PolicyName: aws.String("TestPolicy"),
Arn: aws.String("arn:aws:iam::123456789012:role/TestPolicy"),
Tags: []iamtypes.Tag{
{
Key: aws.String("product:v1:tenant:app-name"),
Value: aws.String("My-App"),
},
{
Key: aws.String("product:v1:tenant:created-by"),
Value: aws.String("Team-1"),
},
},
},
},
}, middleware.Metadata{}, nil
}
if operationName == "AttachRolePolicy" {
return middleware.FinalizeOutput{
Result: &iam.AttachRolePolicyOutput{},
}, middleware.Metadata{}, nil
}
return middleware.FinalizeOutput{}, middleware.Metadata{}, nil
},
),
middleware.Before,
)
},
},
managedClusterAnnotations: map[string]string{
"agent.open-cluster-management.io/managed-cluster-iam-role-suffix": "960c4e56c25ba0b571ddcdaa7edc943e",
"agent.open-cluster-management.io/managed-cluster-arn": "arn:aws:eks:us-west-2:123456789012:cluster/spoke-cluster",
},
want: nil,
wantErr: false,
tags: []string{"product:v1:tenant:app-name=My-App", "product:v1:tenant:created-by=Team-1"},
},
{
name: "test create IAM Role and Policy with invalid Tag with key beginning with aws",
args: args{
ctx: context.Background(),
withAPIOptionsFunc: func(stack *middleware.Stack) error {
return stack.Finalize.Add(
middleware.FinalizeMiddlewareFunc(
"CreateRoleWithInvalidTagBeginsAwsMock",
func(ctx context.Context, input middleware.FinalizeInput, handler middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) {
operationName := middleware.GetOperationName(ctx)
if operationName == "CreateRole" {
return middleware.FinalizeOutput{
Result: nil,
}, middleware.Metadata{}, fmt.Errorf("failed to create IAM role")
}
return middleware.FinalizeOutput{}, middleware.Metadata{}, nil
},
),
middleware.Before,
)
},
},
managedClusterAnnotations: map[string]string{
"agent.open-cluster-management.io/managed-cluster-iam-role-suffix": "960c4e56c25ba0b571ddcdaa7edc943e",
"agent.open-cluster-management.io/managed-cluster-arn": "arn:aws:eks:us-west-2:123456789012:cluster/spoke-cluster",
},
want: fmt.Errorf("operation error IAM: CreateRole, failed to create IAM role"),
wantErr: true,
tags: []string{"aws:invalid:tag=invalid-tag"},
},
{
name: "test create IAM Role and Policy with invalid Tag with empty key",
args: args{
ctx: context.Background(),
withAPIOptionsFunc: func(stack *middleware.Stack) error {
return stack.Finalize.Add(
middleware.FinalizeMiddlewareFunc(
"CreateRoleWithInvalidTagEmptyKeyMock",
func(ctx context.Context, input middleware.FinalizeInput, handler middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) {
operationName := middleware.GetOperationName(ctx)
if operationName == "CreateRole" {
return middleware.FinalizeOutput{
Result: nil,
}, middleware.Metadata{}, fmt.Errorf("failed to create IAM role")
}
return middleware.FinalizeOutput{}, middleware.Metadata{}, nil
},
),
middleware.Before,
)
},
},
managedClusterAnnotations: map[string]string{
"agent.open-cluster-management.io/managed-cluster-iam-role-suffix": "960c4e56c25ba0b571ddcdaa7edc943e",
"agent.open-cluster-management.io/managed-cluster-arn": "arn:aws:eks:us-west-2:123456789012:cluster/spoke-cluster",
},
want: fmt.Errorf("operation error IAM: CreateRole, failed to create IAM role"),
wantErr: true,
tags: []string{"=emptykey"},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
os.Setenv("AWS_ACCESS_KEY_ID", "test")
os.Setenv("AWS_SECRET_ACCESS_KEY", "test")
os.Setenv("AWS_ACCOUNT_ID", "test")
cfg, err := config.LoadDefaultConfig(
tt.args.ctx,
config.WithAPIOptions([]func(*middleware.Stack) error{tt.args.withAPIOptionsFunc}),
)
if err != nil {
t.Fatal(err)
}
HubClusterArn := "arn:aws:eks:us-west-2:123456789012:cluster/hub-cluster"
managedCluster := testinghelpers.NewManagedCluster()
managedCluster.Annotations = tt.managedClusterAnnotations
_, _, err = createIAMRoleAndPolicy(tt.args.ctx, HubClusterArn, managedCluster, cfg, tt.tags)
if (err != nil) != tt.wantErr {
t.Errorf("error = %#v, wantErr %#v", err, tt.wantErr)
return
}
if tt.wantErr && err.Error() != tt.want.Error() {
t.Errorf("err = %#v, want %#v", err, tt.want)
}
})
}
}
func TestParseTagsForRolesAndPolicies(t *testing.T) {
cases := []struct {
name string
tags []string
result []iamtypes.Tag
err error
}{
{
name: "Test Parsing Tags Correctly",
tags: []string{"product:v1:tenant:app-name=My-App"},
result: []iamtypes.Tag{
{
Key: &[]string{"product:v1:tenant:app-name"}[0],
Value: &[]string{"My-App"}[0],
},
},
err: nil,
},
{
name: "Test Parsing Tags Incorrectly",
tags: []string{"product:v1:tenant:app-nameMy-App"},
result: nil,
err: fmt.Errorf("missing value from tag"),
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
output, err := parseTagsForRolesAndPolicies(tt.tags)
if !reflect.DeepEqual(output, tt.result) && err != tt.err {
for idx := range output {
t.Errorf("Expected error to be %#v, but got %#v", tt.err, err)
t.Errorf("Expected {Key: %s, Value: %s}, but got {Key: %s, Value: %s}", *tt.result[idx].Key, *tt.result[idx].Value, *output[idx].Key, *output[idx].Value)
}
}
})
}
}
func TestParseTagsForAccessEntries(t *testing.T) {
cases := []struct {
name string
tags []string
result map[string]string
err error
}{
{
name: "Test Parsing Tags Correctly for access entries",
tags: []string{"product:v1:tenant:app-name=My-App"},
result: map[string]string{"product:v1:tenant:app-name": "My-App"},
err: nil,
},
{
name: "Test Parsing Tags Incorrectly access entries",
tags: []string{"product:v1:tenant:app-nameMy-App"},
result: nil,
err: fmt.Errorf("missing value in the tag"),
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
output, err := parseTagsForAccessEntry(tt.tags)
if !reflect.DeepEqual(output, tt.result) && err != tt.err {
for key := range output {
t.Errorf("Expected error to be %#v, but got %#v", tt.err, err)
t.Errorf("Expected {Key: %s, Value: %s}, but got {Key: %s, Value: %s}", key, tt.result[key], key, output[key])
}
}
})
}
}
@@ -15,6 +15,8 @@ import (
var _ = ginkgo.Describe("ClusterManager Default Mode with aws registration", func() {
var cancel context.CancelFunc
var hubRegistrationSA = "registration-controller-sa"
var hubClusterArn = "arn:aws:eks:us-west-2:123456789012:cluster/hub-cluster"
var tags = "product:v1:tenant:app-name=My-App,product:v1:tenant:created-by=Team-1"
ginkgo.BeforeEach(func() {
var ctx context.Context
@@ -44,9 +46,15 @@ var _ = ginkgo.Describe("ClusterManager Default Mode with aws registration", fun
clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{}
clusterManager.Spec.RegistrationConfiguration.RegistrationDrivers = []operatorapiv1.RegistrationDriverHub{
{
AuthType: "awsirsa",
HubClusterArn: "arn:aws:eks:us-west-2:123456789012:cluster/hub-cluster",
AutoApprovedIdentities: []string{"arn:aws:eks:us-west-2:123456789013:cluster/.*", "arn:aws:eks:us-west-2:123456789012:cluster/.*"},
AuthType: "awsirsa",
AwsIrsa: &operatorapiv1.AwsIrsaConfig{
HubClusterArn: hubClusterArn,
Tags: []string{
"product:v1:tenant:app-name=My-App",
"product:v1:tenant:created-by=Team-1",
},
AutoApprovedIdentities: []string{"arn:aws:eks:us-west-2:123456789013:cluster/.*", "arn:aws:eks:us-west-2:123456789012:cluster/.*"},
},
},
}
}
@@ -80,6 +88,33 @@ var _ = ginkgo.Describe("ClusterManager Default Mode with aws registration", fun
return annotation == "arn:aws:iam::123456789012:role/hub-cluster_managed-cluster-identity-creator"
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
ginkgo.It("registration-controller should have command line option hub cluster arn when initialized with awsirsa", func() {
gomega.Eventually(func() bool {
registrationControllerDeployment, err := kubeClient.AppsV1().Deployments(hubNamespace).
Get(context.Background(), fmt.Sprintf("%s-registration-controller", clusterManagerName), metav1.GetOptions{})
if err != nil {
return false
}
commandLineArgs := registrationControllerDeployment.Spec.Template.Spec.Containers[0].Args
hubClusterArnArg, present := findMatchingArg(commandLineArgs, "--hub-cluster-arn")
return present && strings.Split(hubClusterArnArg, "=")[1] == hubClusterArn
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
ginkgo.It("registration-controller should have command line option aws-resource-tags when initialized with awsirsa", func() {
gomega.Eventually(func() bool {
registrationControllerDeployment, err := kubeClient.AppsV1().Deployments(hubNamespace).
Get(context.Background(), fmt.Sprintf("%s-registration-controller", clusterManagerName), metav1.GetOptions{})
if err != nil {
return false
}
commandLineArgs := registrationControllerDeployment.Spec.Template.Spec.Containers[0].Args
tagsArg, present := findMatchingArg(commandLineArgs, "--aws-resource-tags")
return present && strings.SplitN(tagsArg, "=", 2)[1] == tags
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
ginkgo.It("should have auto approved arn patterns separated by comma with awsirsa", func() {
gomega.Eventually(func() bool {
registrationControllerDeployment, err := kubeClient.AppsV1().Deployments(hubNamespace).
@@ -89,16 +124,70 @@ var _ = ginkgo.Describe("ClusterManager Default Mode with aws registration", fun
}
commandLineArgs := registrationControllerDeployment.Spec.Template.Spec.Containers[0].Args
autoApprovedArnPatterns, present := findMatchingArg(commandLineArgs, "--auto-approved-arn-patterns")
return present && strings.SplitN(autoApprovedArnPatterns, "=", 2)[1] ==
return present && strings.Split(autoApprovedArnPatterns, "=")[1] ==
"arn:aws:eks:us-west-2:123456789013:cluster/.*,arn:aws:eks:us-west-2:123456789012:cluster/.*"
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
})
ginkgo.Context("Deploy hub with aws auth with no tags", func() {
ginkgo.BeforeEach(func() {
gomega.Eventually(func() error {
clusterManager, err := operatorClient.OperatorV1().ClusterManagers().Get(context.Background(),
clusterManagerName, metav1.GetOptions{})
if err != nil {
return err
}
if clusterManager.Spec.RegistrationConfiguration == nil {
clusterManager.Spec.RegistrationConfiguration = &operatorapiv1.RegistrationHubConfiguration{}
clusterManager.Spec.RegistrationConfiguration.RegistrationDrivers = []operatorapiv1.RegistrationDriverHub{
{
AuthType: "awsirsa",
AwsIrsa: &operatorapiv1.AwsIrsaConfig{
HubClusterArn: hubClusterArn,
},
},
}
}
_, err = operatorClient.OperatorV1().ClusterManagers().Update(context.Background(),
clusterManager, metav1.UpdateOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
})
ginkgo.AfterEach(func() {
gomega.Eventually(func() error {
clusterManager, err := operatorClient.OperatorV1().ClusterManagers().Get(context.Background(),
clusterManagerName, metav1.GetOptions{})
if err != nil {
return err
}
clusterManager.Spec.RegistrationConfiguration = nil
_, err = operatorClient.OperatorV1().ClusterManagers().Update(context.Background(),
clusterManager, metav1.UpdateOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
})
ginkgo.It("registration-controller should not have command line option aws-resource-tags when not initialized with awsirsa", func() {
gomega.Eventually(func() bool {
registrationControllerDeployment, err := kubeClient.AppsV1().Deployments(hubNamespace).
Get(context.Background(), fmt.Sprintf("%s-registration-controller", clusterManagerName), metav1.GetOptions{})
if err != nil {
return false
}
commandLineArgs := registrationControllerDeployment.Spec.Template.Spec.Containers[0].Args
_, present := findMatchingArg(commandLineArgs, "--aws-resource-tags")
return !present
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
})
})
})
func findMatchingArg(args []string, pattern string) (string, bool) {
for _, commandLineArg := range args {
if strings.SplitN(commandLineArg, "=", 2)[0] == pattern {
if strings.Split(commandLineArg, "=")[0] == pattern {
return commandLineArg, true
}
}
@@ -1155,8 +1155,10 @@ var _ = ginkgo.Describe("ClusterManager Default Mode", func() {
}
clusterManager.Spec.RegistrationConfiguration.RegistrationDrivers = []operatorapiv1.RegistrationDriverHub{
{
AuthType: "csr",
AutoApprovedIdentities: []string{"user3", "user4"},
AuthType: "csr",
CSR: &operatorapiv1.CSRConfig{
AutoApprovedIdentities: []string{"user3", "user4"},
},
},
}
_, err = operatorClient.OperatorV1().ClusterManagers().Update(context.Background(), clusterManager, metav1.UpdateOptions{})
+1 -1
View File
@@ -1708,7 +1708,7 @@ open-cluster-management.io/addon-framework/pkg/agent
open-cluster-management.io/addon-framework/pkg/assets
open-cluster-management.io/addon-framework/pkg/index
open-cluster-management.io/addon-framework/pkg/utils
# open-cluster-management.io/api v0.15.1-0.20250219064651-4281b7684d9b
# open-cluster-management.io/api v0.15.1-0.20250226073118-8c9793267c9e
## explicit; go 1.22.0
open-cluster-management.io/api/addon/v1alpha1
open-cluster-management.io/api/client/addon/clientset/versioned
@@ -269,19 +269,40 @@ spec:
- csr
- awsirsa
type: string
autoApprovedIdentities:
description: |-
For csr authentication type, AutoApprovedIdentities represent a list of approved users
For awsirsa authentication type, AutoApprovedIdentities represent a list of approved arn patterns
items:
type: string
type: array
hubClusterArn:
description: |-
This represents the hub cluster ARN
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
awsisra:
description: AwsIrsa represents the configuration for awsisra
driver.
properties:
autoApprovedIdentities:
description: AutoApprovedIdentities represent a list
of approved arn patterns
items:
type: string
type: array
hubClusterArn:
description: |-
This represents the hub cluster ARN
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
tags:
description: |-
List of tags to be added to AWS resources created by hub while processing awsirsa registration request
Example - "product:v1:tenant:app-name=My-App"
items:
type: string
type: array
type: object
csr:
description: CSR represents the configuration for csr driver.
properties:
autoApprovedIdentities:
description: AutoApprovedIdentities represent a list
of approved users
items:
type: string
type: array
type: object
type: object
type: array
x-kubernetes-list-map-keys:
+22 -2
View File
@@ -125,16 +125,36 @@ type RegistrationDriverHub struct {
// +kubebuilder:validation:Enum=csr;awsirsa
AuthType string `json:"authType,omitempty"`
// CSR represents the configuration for csr driver.
// +optional
CSR *CSRConfig `json:"csr,omitempty"`
// AwsIrsa represents the configuration for awsisra driver.
// +optional
AwsIrsa *AwsIrsaConfig `json:"awsisra,omitempty"`
}
type CSRConfig struct {
// AutoApprovedIdentities represent a list of approved users
// +optional
AutoApprovedIdentities []string `json:"autoApprovedIdentities,omitempty"`
}
type AwsIrsaConfig struct {
// This represents the hub cluster ARN
// Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1
// +optional
// +kubebuilder:validation:Pattern=`^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$`
HubClusterArn string `json:"hubClusterArn,omitempty"`
// For csr authentication type, AutoApprovedIdentities represent a list of approved users
// For awsirsa authentication type, AutoApprovedIdentities represent a list of approved arn patterns
// AutoApprovedIdentities represent a list of approved arn patterns
// +optional
AutoApprovedIdentities []string `json:"autoApprovedIdentities,omitempty"`
// List of tags to be added to AWS resources created by hub while processing awsirsa registration request
// Example - "product:v1:tenant:app-name=My-App"
// +optional
Tags []string `json:"tags,omitempty"`
}
type WorkConfiguration struct {
+56 -4
View File
@@ -48,6 +48,32 @@ func (in *AwsIrsa) DeepCopy() *AwsIrsa {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AwsIrsaConfig) DeepCopyInto(out *AwsIrsaConfig) {
*out = *in
if in.AutoApprovedIdentities != nil {
in, out := &in.AutoApprovedIdentities, &out.AutoApprovedIdentities
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Tags != nil {
in, out := &in.Tags, &out.Tags
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AwsIrsaConfig.
func (in *AwsIrsaConfig) DeepCopy() *AwsIrsaConfig {
if in == nil {
return nil
}
out := new(AwsIrsaConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BootstrapKubeConfigs) DeepCopyInto(out *BootstrapKubeConfigs) {
*out = *in
@@ -65,6 +91,27 @@ func (in *BootstrapKubeConfigs) DeepCopy() *BootstrapKubeConfigs {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CSRConfig) DeepCopyInto(out *CSRConfig) {
*out = *in
if in.AutoApprovedIdentities != nil {
in, out := &in.AutoApprovedIdentities, &out.AutoApprovedIdentities
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSRConfig.
func (in *CSRConfig) DeepCopy() *CSRConfig {
if in == nil {
return nil
}
out := new(CSRConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClusterManager) DeepCopyInto(out *ClusterManager) {
*out = *in
@@ -560,10 +607,15 @@ func (in *RegistrationDriver) DeepCopy() *RegistrationDriver {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RegistrationDriverHub) DeepCopyInto(out *RegistrationDriverHub) {
*out = *in
if in.AutoApprovedIdentities != nil {
in, out := &in.AutoApprovedIdentities, &out.AutoApprovedIdentities
*out = make([]string, len(*in))
copy(*out, *in)
if in.CSR != nil {
in, out := &in.CSR, &out.CSR
*out = new(CSRConfig)
(*in).DeepCopyInto(*out)
}
if in.AwsIrsa != nil {
in, out := &in.AwsIrsa, &out.AwsIrsa
*out = new(AwsIrsaConfig)
(*in).DeepCopyInto(*out)
}
return
}
@@ -19,6 +19,24 @@ func (AddOnManagerConfiguration) SwaggerDoc() map[string]string {
return map_AddOnManagerConfiguration
}
var map_AwsIrsaConfig = map[string]string{
"hubClusterArn": "This represents the hub cluster ARN Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1",
"autoApprovedIdentities": "AutoApprovedIdentities represent a list of approved arn patterns",
"tags": "List of tags to be added to AWS resources created by hub while processing awsirsa registration request Example - \"product:v1:tenant:app-name=My-App\"",
}
func (AwsIrsaConfig) SwaggerDoc() map[string]string {
return map_AwsIrsaConfig
}
var map_CSRConfig = map[string]string{
"autoApprovedIdentities": "AutoApprovedIdentities represent a list of approved users",
}
func (CSRConfig) SwaggerDoc() map[string]string {
return map_CSRConfig
}
var map_ClusterManager = map[string]string{
"": "ClusterManager configures the controllers on the hub that govern registration and work distribution for attached Klusterlets. In Default mode, ClusterManager will only be deployed in open-cluster-management-hub namespace. In Hosted mode, ClusterManager will be deployed in the namespace with the same name as cluster manager.",
"spec": "Spec represents a desired deployment configuration of controllers that govern registration and work distribution for attached Klusterlets.",
@@ -123,9 +141,9 @@ func (NodePlacement) SwaggerDoc() map[string]string {
}
var map_RegistrationDriverHub = map[string]string{
"authType": "Type of the authentication used by hub to initialize the Hub cluster. Possible values are csr and awsirsa.",
"hubClusterArn": "This represents the hub cluster ARN Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1",
"autoApprovedIdentities": "For csr authentication type, AutoApprovedIdentities represent a list of approved users For awsirsa authentication type, AutoApprovedIdentities represent a list of approved arn patterns",
"authType": "Type of the authentication used by hub to initialize the Hub cluster. Possible values are csr and awsirsa.",
"csr": "CSR represents the configuration for csr driver.",
"awsisra": "AwsIrsa represents the configuration for awsisra driver.",
}
func (RegistrationDriverHub) SwaggerDoc() map[string]string {