From 65def360ba94296a3aa1954ff8d214524c5f31cb Mon Sep 17 00:00:00 2001 From: Mario Nitchev Date: Wed, 4 Dec 2024 11:23:51 +0200 Subject: [PATCH] WIP --- controllers/poc_controller.go | 280 ++++++++++++++++++++++++++++++++++ main.go | 9 ++ 2 files changed, 289 insertions(+) create mode 100644 controllers/poc_controller.go diff --git a/controllers/poc_controller.go b/controllers/poc_controller.go new file mode 100644 index 00000000..c8feb955 --- /dev/null +++ b/controllers/poc_controller.go @@ -0,0 +1,280 @@ +package controllers + +import ( + "context" + "encoding/json" + "fmt" + + awssdk "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + capa "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" + "sigs.k8s.io/cluster-api/util/patch" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/giantswarm/capa-iam-operator/pkg/key" +) + +var iamRoleNames = []string{ + "ALBControllerRole", + "ebs-csi-driver-role", + "efs-csi-driver-role", + "cluster-autoscaler-role", + "CertManager-Role", + "Route53Manager-Role", +} + +type POCReconciler struct { + k8sClient client.Client + iamClient *iam.IAM +} + +// "Effect": "Allow", +// +// "Principal": { +// "Federated": "arn:{{ $.AWSDomain }}:iam::{{ $.AccountID }}:oidc-provider/{{ $domain }}" +// }, +// +// "Action": "sts:AssumeRoleWithWebIdentity", +// +// "Condition": { +// "StringLike": { +// "{{ $domain }}:sub": "system:serviceaccount:*:{{ $.ServiceAccount }}" +// } +// } + +// { +// "Version": "2012-10-17", +// "Statement": [ +// { +// "Effect": "Allow", +// "Principal": { +// "Federated": "arn:aws:iam:::oidc-provider/irsa." +// }, +// "Action": "sts:AssumeRoleWithWebIdentity", +// "Condition": { +// "StringEquals": { +// "irsa.:sub": "system:serviceaccount:*:aws-load-balancer-controller" +// } +// } +// } +// ] +// } + +type Condition struct { + StringLike map[string]string `json:"StringLike"` + StringEquals map[string]string `json:"StringEquals"` +} + +type Principal struct { + Federated string `json:"Federated"` +} + +type Statement struct { + Effect string `json:"Effect"` + Principal Principal `json:"Principal"` + Action string `json:"Action"` + Condition Condition `json:"Condition"` +} + +type AssumeRolePolicy struct { + Statement []Statement `json:"Statement"` +} + +func (r *POCReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&capa.AWSCluster{}). + Complete(r) +} + +func (r *POCReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + awsCluster := &capa.AWSCluster{} + err := r.k8sClient.Get(ctx, req.NamespacedName, awsCluster) + if client.IgnoreNotFound(err) != nil { + return ctrl.Result{}, nil + } + + awsRoleIdentity := &capa.AWSClusterRoleIdentity{} + err = r.k8sClient.Get(ctx, types.NamespacedName{ + Name: awsCluster.Spec.IdentityRef.Name, + }, awsRoleIdentity) + if err != nil { + return ctrl.Result{}, err + } + + accountID, err := key.GetAWSAccountID(awsRoleIdentity) + if err != nil { + return ctrl.Result{}, err + } + + baseDomain, err := key.GetBaseDomain(ctx, r.k8sClient, awsCluster.Name, awsCluster.Namespace) + if err != nil { + logger.Error(err, "Could not get base domain") + return ctrl.Result{}, errors.WithStack(err) + } + + irsaDomain := key.IRSADomain(baseDomain, awsCluster.Spec.Region, accountID, awsCluster.Name) + principal := fmt.Sprintf("arn:aws:iam::%s:oidc-provider/%s", accountID, irsaDomain) + + if awsCluster.DeletionTimestamp != nil { + return r.reconcileDelete(ctx, awsCluster, principal) + } + + return r.reconcileNormal(ctx, awsCluster, principal) +} + +func (r *POCReconciler) reconcileNormal(ctx context.Context, awsCluster *capa.AWSCluster, principal string) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + patchHelper, err := patch.NewHelper(awsCluster, r.k8sClient) + if err != nil { + return ctrl.Result{}, errors.WithStack(err) + } + controllerutil.AddFinalizer(awsCluster, "capa-iam-operator.finalizers.giantswarm.io/poc") + err = patchHelper.Patch(ctx, awsCluster) + if err != nil { + logger.Error(err, "failed to add finalizer on AWSMachinePool") + return ctrl.Result{}, errors.WithStack(err) + } + + for _, iamRole := range iamRoleNames { + out, err := r.iamClient.GetRole(&iam.GetRoleInput{ + RoleName: &iamRole, + }) + if err != nil { + logger.Error(err, "failed to get IAM role", "role", iamRole) + return ctrl.Result{}, errors.WithStack(err) + } + assumeRolePolicyData := []byte(*out.Role.AssumeRolePolicyDocument) + + var assumeRolePolicy AssumeRolePolicy + err = json.Unmarshal(assumeRolePolicyData, &assumeRolePolicy) + if err != nil { + return ctrl.Result{}, err + } + assumeRolePolicy = r.addStatement(assumeRolePolicy, iamRole, principal) + assumeRolePolicyData, err = json.Marshal(assumeRolePolicy) + if err != nil { + return ctrl.Result{}, err + } + + _, err = r.iamClient.UpdateAssumeRolePolicy(&iam.UpdateAssumeRolePolicyInput{ + PolicyDocument: awssdk.String(string(assumeRolePolicyData)), + RoleName: awssdk.String(iamRole), + }) + if err != nil { + return ctrl.Result{}, err + } + + } + + return ctrl.Result{}, nil +} + +func (r *POCReconciler) reconcileDelete(ctx context.Context, awsCluster *capa.AWSCluster, principal string) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + patchHelper, err := patch.NewHelper(awsCluster, r.k8sClient) + if err != nil { + return ctrl.Result{}, errors.WithStack(err) + } + controllerutil.RemoveFinalizer(awsCluster, "capa-iam-operator.finalizers.giantswarm.io/poc") + err = patchHelper.Patch(ctx, awsCluster) + if err != nil { + logger.Error(err, "failed to remove finalizer on AWSMachinePool") + return ctrl.Result{}, errors.WithStack(err) + } + + for _, iamRole := range iamRoleNames { + out, err := r.iamClient.GetRole(&iam.GetRoleInput{ + RoleName: &iamRole, + }) + if err != nil { + logger.Error(err, "failed to get IAM role", "role", iamRole) + return ctrl.Result{}, errors.WithStack(err) + } + assumeRolePolicyData := []byte(*out.Role.AssumeRolePolicyDocument) + + var assumeRolePolicy AssumeRolePolicy + err = json.Unmarshal(assumeRolePolicyData, &assumeRolePolicy) + if err != nil { + return ctrl.Result{}, err + } + assumeRolePolicy = r.removeStatement(assumeRolePolicy, iamRole, principal) + assumeRolePolicyData, err = json.Marshal(assumeRolePolicy) + if err != nil { + return ctrl.Result{}, err + } + + _, err = r.iamClient.UpdateAssumeRolePolicy(&iam.UpdateAssumeRolePolicyInput{ + PolicyDocument: awssdk.String(string(assumeRolePolicyData)), + RoleName: awssdk.String(iamRole), + }) + if err != nil { + return ctrl.Result{}, err + } + + } + + return ctrl.Result{}, nil +} + +func (r *POCReconciler) addStatement(assumeRolePolicy AssumeRolePolicy, roleName, principal string) AssumeRolePolicy { + for _, statement := range assumeRolePolicy.Statement { + if statement.Principal.Federated == principal { + return assumeRolePolicy + } + } + + assumeRolePolicy.Statement = append(assumeRolePolicy.Statement, Statement{ + Effect: "Allow", + Principal: Principal{ + Federated: principal, + }, + Action: "sts:AssumeRoleWithWebIdentity", + Condition: Condition{ + StringEquals: map[string]string{ + fmt.Sprintf("%s:sub", principal): fmt.Sprintf("system:serviceaccount:*:%s", getServiceAccountName(roleName)), + }, + }, + }) + + return assumeRolePolicy +} + +func (r *POCReconciler) removeStatement(assumeRolePolicy AssumeRolePolicy, roleName, principal string) AssumeRolePolicy { + statements := []Statement{} + for _, statement := range assumeRolePolicy.Statement { + if statement.Principal.Federated != principal { + statements = append(statements, statement) + } + } + + assumeRolePolicy.Statement = statements + return assumeRolePolicy +} + +func getServiceAccountName(roleName string) string { + switch roleName { + case "ALBControllerRole": + return "aws-load-balancer-controller" + case "ebs-csi-driver-role": + return "ebs-csi-controller-sa" + case "efs-csi-driver-role": + return "efs-csi-controller-sa" + case "cluster-autoscaler-role": + return "cluster-autoscaler" + case "CertManager-Role": + return "cert-manager" + case "Route53Manager-Role": + return "external-dns" + default: + return "" + } +} diff --git a/main.go b/main.go index 7603c84a..33589262 100644 --- a/main.go +++ b/main.go @@ -157,6 +157,15 @@ func main() { os.Exit(1) } + if err = (&controllers.AWSManagedControlPlaneReconciler{ + Client: mgr.GetClient(), + AWSClient: awsClientAwsMachine, + IAMClientFactory: iamClientFactory, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AWSManagedControlPlane") + os.Exit(1) + } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {