From 6390f37f68e511a3b83072ec5a7c8d68a7753284 Mon Sep 17 00:00:00 2001 From: Moulick Aggarwal <15780903+Moulick@users.noreply.github.com> Date: Tue, 12 Sep 2023 16:15:37 +0200 Subject: [PATCH] Merge pull request #32 * Add support for Externally Managed Policies --- .gitignore | 3 +- .goreleaser.yml | 1 + api/v1beta1/group_types.go | 2 +- api/v1beta1/policyattachment_types.go | 24 ++- api/v1beta1/zz_generated.deepcopy.go | 16 ++ ...s-iam.redradrat.xyz_policyattachments.yaml | 7 + config/rbac/role.yaml | 4 - controllers/group_controller.go | 21 +-- controllers/helpers.go | 10 +- controllers/policy_controller.go | 10 +- controllers/policyattachment_controller.go | 137 +++++++++++------- controllers/role_controller.go | 14 +- controllers/user_controller.go | 12 +- 13 files changed, 162 insertions(+), 99 deletions(-) diff --git a/.gitignore b/.gitignore index 76b9cde..f04e722 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ dist/ *.so *.dylib bin +aws-iam-operator # Test binary, build with `go test -c` *.test @@ -24,4 +25,4 @@ bin *.swo *~ -**/.DS_Store \ No newline at end of file +**/.DS_Store diff --git a/.goreleaser.yml b/.goreleaser.yml index d5b2917..d7719b5 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -14,6 +14,7 @@ builds: - windows - darwin ldflags: + - -s -w # strip symbols, reduces binary size - -X main.operatorversion={{.Version}} - -X main.operatorbuilddate={{.Date}} archives: diff --git a/api/v1beta1/group_types.go b/api/v1beta1/group_types.go index ed38cc9..f39fb4c 100644 --- a/api/v1beta1/group_types.go +++ b/api/v1beta1/group_types.go @@ -40,7 +40,7 @@ type GroupStatus struct { // +kubebuilder:printcolumn:name="Message",type=string,JSONPath=`.status.message` // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.state` // +kubebuilder:printcolumn:name="Last Sync",type=string,JSONPath=`.status.lastSyncAttempt` -// + // Group is the Schema for the roles API type Group struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1beta1/policyattachment_types.go b/api/v1beta1/policyattachment_types.go index 29892a2..a8ee754 100644 --- a/api/v1beta1/policyattachment_types.go +++ b/api/v1beta1/policyattachment_types.go @@ -20,6 +20,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// ResourceReference refrences the Policy resource to attach to another resource +// +kubebuilder:validation:Optional +// +optional type ResourceReference struct { // +kubebuilder:validation:Required @@ -29,6 +32,15 @@ type ResourceReference struct { Namespace string `json:"namespace,omitempty"` } +// ExternalResource is a reference to a policy ARN that is not created by the controller +// +kubebuilder:validation:Optional +// +optional +type ExternalResource struct { + + // +kubebuilder:validation:Required + ARN string `json:"arn,omitempty"` +} + type TargetType string const ( @@ -54,14 +66,18 @@ type TargetReference struct { // PolicyAttachmentSpec defines the desired state of PolicyAttachment type PolicyAttachmentSpec struct { - // +kubebuilder:validation:Required - // // PolicyReference refrences the Policy resource to attach to another resource + // +kubebuilder:validation:Optional + // +optional PolicyReference ResourceReference `json:"policy,omitempty"` - // +kubebuilder:validation:Required - // + // ExternalPolicy is a reference to a resource that is not created by the controller + // +kubebuilder:validation:Optional + // +optional + ExternalPolicy ExternalResource `json:"externalPolicy,omitempty"` + // Attachments holds all defined attachments + // +kubebuilder:validation:Required TargetReference TargetReference `json:"target,omitempty"` } diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 5f3be13..0e67973 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -181,6 +181,21 @@ func (in *AssumeRolePolicyStatus) DeepCopy() *AssumeRolePolicyStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalResource) DeepCopyInto(out *ExternalResource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalResource. +func (in *ExternalResource) DeepCopy() *ExternalResource { + if in == nil { + return nil + } + out := new(ExternalResource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Group) DeepCopyInto(out *Group) { *out = *in @@ -366,6 +381,7 @@ func (in *PolicyAttachmentList) DeepCopyObject() runtime.Object { func (in *PolicyAttachmentSpec) DeepCopyInto(out *PolicyAttachmentSpec) { *out = *in out.PolicyReference = in.PolicyReference + out.ExternalPolicy = in.ExternalPolicy out.TargetReference = in.TargetReference } diff --git a/config/crd/bases/aws-iam.redradrat.xyz_policyattachments.yaml b/config/crd/bases/aws-iam.redradrat.xyz_policyattachments.yaml index f354d6a..4b39aa3 100644 --- a/config/crd/bases/aws-iam.redradrat.xyz_policyattachments.yaml +++ b/config/crd/bases/aws-iam.redradrat.xyz_policyattachments.yaml @@ -50,6 +50,13 @@ spec: spec: description: PolicyAttachmentSpec defines the desired state of PolicyAttachment properties: + externalPolicy: + description: ExternalPolicy is a reference to a resource that is not + created by the controller + properties: + arn: + type: string + type: object policy: description: PolicyReference refrences the Policy resource to attach to another resource diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index d825c1c..0a82602 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -106,12 +106,8 @@ rules: resources: - policyattachments verbs: - - create - - delete - get - list - - patch - - update - watch - apiGroups: - aws-iam.redradrat.xyz diff --git a/controllers/group_controller.go b/controllers/group_controller.go index e1912ba..6bba5ee 100644 --- a/controllers/group_controller.go +++ b/controllers/group_controller.go @@ -19,6 +19,7 @@ package controllers import ( "context" "fmt" + "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -59,7 +60,7 @@ func (r *GroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl // Get our actual IAM Service to communicate with AWS; we don't need to continue without it iamsvc, err := IAMService(r.Region) if err != nil { - return ctrl.Result{}, errWithStatus(&group, err, r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &group, err, r.Status()) } // new group instance @@ -68,7 +69,7 @@ func (r *GroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl if group.Status.ARN != "" { parsedArn, err := aws.ARNify(group.Status.ARN) if err != nil { - return ctrl.Result{}, errWithStatus(&group, fmt.Errorf("ARN in Group status is not valid/parsable"), r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &group, fmt.Errorf("ARN in Group status is not valid/parsable"), r.Status()) } ins = iam.NewExistingGroupInstance(groupName, parsedArn[len(parsedArn)-1]) } else { @@ -104,7 +105,7 @@ func (r *GroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl // delete the actual AWS Object and pass the cleanup function statusUpdater, err := DeleteAWSObject(iamsvc, ins, cleanupFunc) // we got a StatusUpdater function returned... let's execute it - statusUpdater(ins, &group, ctx, r.Status(), log) + statusUpdater(ctx, ins, &group, r.Status(), log) if err != nil { // we had an error during AWS Object deletion... so we return here to retry log.Error(err, "unable to delete Group") @@ -130,7 +131,7 @@ func (r *GroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl if group.Status.ARN != "" { // Delete the actual AWS Object and pass the cleanup function statusWriter, err := DeleteAWSObject(iamsvc, ins, cleanupFunc) - statusWriter(ins, &group, ctx, r.Status(), log) + statusWriter(ctx, ins, &group, r.Status(), log) if err != nil { // we had an error during AWS Object deletion... so we return here to retry log.Error(err, "error while deleting Group during reconciliation") @@ -139,7 +140,7 @@ func (r *GroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl } statusWriter, err := CreateAWSObject(iamsvc, ins, DoNothingPreFunc) - statusWriter(ins, &group, ctx, r.Status(), log) + statusWriter(ctx, ins, &group, r.Status(), log) if err != nil { log.Error(err, "error while creating Group during reconciliation") return ctrl.Result{}, err @@ -151,23 +152,23 @@ func (r *GroupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl userObj := iamv1beta1.User{} r.Client.Get(ctx, client.ObjectKey{Name: user.Name, Namespace: user.Namespace}, &userObj) if err != nil { - return ctrl.Result{}, errWithStatus(&group, err, r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &group, err, r.Status()) } // Err if ARN is not available in the user obj if userObj.Status.ARN == "" { - return ctrl.Result{}, errWithStatus(&group, fmt.Errorf("referenced user resource '%s/%s' has not yet been created", user.Namespace, user.Name), r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &group, fmt.Errorf("referenced user resource '%s/%s' has not yet been created", user.Namespace, user.Name), r.Status()) } // parse the user arn parsedArn, err := aws.ARNify(userObj.Status.ARN) if err != nil { - return ctrl.Result{}, errWithStatus(&group, fmt.Errorf("ARN in referenced User status is not valid/parsable"), r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &group, fmt.Errorf("ARN in referenced User status is not valid/parsable"), r.Status()) } // Now add the user to our Group Instance if err = ins.AddUser(iamsvc, parsedArn[len(parsedArn)-1]); err != nil { - return ctrl.Result{}, errWithStatus(&group, err, r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &group, err, r.Status()) } } @@ -195,7 +196,7 @@ func groupCleanup(r *GroupReconciler, ctx context.Context, group iamv1beta1.Grou for _, att := range attachments.Items { if att.Spec.TargetReference.Type == iamv1beta1.GroupTargetType { if att.Spec.TargetReference.Name == group.Name && att.Spec.TargetReference.Namespace == group.Namespace { - err := fmt.Errorf(fmt.Sprintf("cannot delete Group due to existing PolicyAttachment '%s/%s'", att.Name, att.Namespace)) + err := fmt.Errorf("cannot delete Group due to existing PolicyAttachment '%s/%s'", att.Name, att.Namespace) return err } } diff --git a/controllers/helpers.go b/controllers/helpers.go index 8757572..3a2e103 100644 --- a/controllers/helpers.go +++ b/controllers/helpers.go @@ -94,7 +94,7 @@ func ignoreDoesNotExistError(err error) error { func DoNothingPreFunc() error { return nil } -func errWithStatus(obj AWSObjectStatusResource, err error, sw client.StatusWriter, ctx context.Context) error { +func errWithStatus(ctx context.Context, obj AWSObjectStatusResource, err error, sw client.StatusWriter) error { origerr := err obj.GetStatus().Message = origerr.Error() obj.GetStatus().State = iamv1beta1.ErrorSyncState @@ -115,10 +115,10 @@ func IAMService(region string) (*awsiam.IAM, error) { return iam.Client(session), nil } -type StatusUpdater func(ins aws.Instance, obj AWSObjectStatusResource, ctx context.Context, sw client.StatusWriter, log logr.Logger) +type StatusUpdater func(ctx context.Context, ins aws.Instance, obj AWSObjectStatusResource, sw client.StatusWriter, log logr.Logger) func SuccessStatusUpdater() StatusUpdater { - return func(ins aws.Instance, obj AWSObjectStatusResource, ctx context.Context, sw client.StatusWriter, log logr.Logger) { + return func(ctx context.Context, ins aws.Instance, obj AWSObjectStatusResource, sw client.StatusWriter, log logr.Logger) { obj.GetStatus().ARN = ins.ARN().String() obj.GetStatus().Message = "Succesfully reconciled" obj.GetStatus().State = iamv1beta1.OkSyncState @@ -132,7 +132,7 @@ func SuccessStatusUpdater() StatusUpdater { } func ErrorStatusUpdater(reason string) StatusUpdater { - return func(ins aws.Instance, obj AWSObjectStatusResource, ctx context.Context, sw client.StatusWriter, log logr.Logger) { + return func(ctx context.Context, ins aws.Instance, obj AWSObjectStatusResource, sw client.StatusWriter, log logr.Logger) { obj.GetStatus().Message = reason obj.GetStatus().State = iamv1beta1.ErrorSyncState obj.GetStatus().LastSyncAttempt = time.Now().Format(time.RFC822Z) @@ -144,5 +144,5 @@ func ErrorStatusUpdater(reason string) StatusUpdater { } } -func DoNothingStatusUpdater(ins aws.Instance, obj AWSObjectStatusResource, ctx context.Context, sw client.StatusWriter, log logr.Logger) { +func DoNothingStatusUpdater(ctx context.Context, ins aws.Instance, obj AWSObjectStatusResource, sw client.StatusWriter, log logr.Logger) { } diff --git a/controllers/policy_controller.go b/controllers/policy_controller.go index 6246262..c2851c1 100644 --- a/controllers/policy_controller.go +++ b/controllers/policy_controller.go @@ -96,7 +96,7 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr policy.ObjectMeta.Finalizers = append(policy.ObjectMeta.Finalizers, policiesFinalizer) if err := r.Update(context.Background(), &policy); err != nil { log.Error(err, "unable to register finalizer for Policy") - return ctrl.Result{}, errWithStatus(&policy, err, r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &policy, err, r.Status()) } } } else { @@ -105,7 +105,7 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // delete the actual AWS Object and pass the cleanup function statusWriter, err := DeleteAWSObject(iamsvc, ins, cleanupFunc) - statusWriter(ins, &policy, ctx, r.Status(), log) + statusWriter(ctx, ins, &policy, r.Status(), log) if err != nil { // we had an error during AWS Object deletion... so we return here to retry log.Error(err, "unable to delete Policy") @@ -128,14 +128,14 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // if there is already an ARN in our status, then we update the object statusWriter, err := CreateAWSObject(iamsvc, ins, DoNothingPreFunc) - statusWriter(ins, &policy, ctx, r.Status(), log) + statusWriter(ctx, ins, &policy, r.Status(), log) if err != nil { // If already exists, we just update the status if aerr, ok := err.(awserr.Error); ok { if aerr.Code() == awsiam.ErrCodeEntityAlreadyExistsException { // Update the actual AWS Object and pass the DoNothing function statusWriter, err := UpdateAWSObject(iamsvc, ins, DoNothingPreFunc) - statusWriter(ins, &policy, ctx, r.Status(), log) + statusWriter(ctx, ins, &policy, r.Status(), log) if err != nil { // we had an error during AWS Object update... so we return here to retry log.Error(err, "error while updating Policy during reconciliation") @@ -172,7 +172,7 @@ func policyCleanup(r *PolicyReconciler, ctx context.Context, policy *iamv1beta1. } for _, att := range attachments.Items { if att.Spec.PolicyReference.Name == policy.Name && att.Spec.PolicyReference.Namespace == policy.Namespace { - err := fmt.Errorf(fmt.Sprintf("cannot delete policy due to existing PolicyAttachment '%s/%s'", att.Name, att.Namespace)) + err := fmt.Errorf("cannot delete policy due to existing PolicyAttachment '%s/%s'", att.Name, att.Namespace) return err } } diff --git a/controllers/policyattachment_controller.go b/controllers/policyattachment_controller.go index 417448d..71189a0 100644 --- a/controllers/policyattachment_controller.go +++ b/controllers/policyattachment_controller.go @@ -20,17 +20,22 @@ import ( "context" "fmt" - awsarn "github.com/aws/aws-sdk-go/aws/arn" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - iamv1beta1 "github.com/redradrat/aws-iam-operator/api/v1beta1" + awsarn "github.com/aws/aws-sdk-go/aws/arn" + "github.com/redradrat/cloud-objects/aws/iam" + + iamv1beta1 "github.com/redradrat/aws-iam-operator/api/v1beta1" ) -// PolicyAssignmentReconciler reconciles a PolicyAssignment object +// finalizer for deleting the actual aws resources +const policyAttachmentFinalizer = "policyattachment.aws-iam.redradrat.xyz" + +// PolicyAttachmentReconciler reconciles a PolicyAssignment object type PolicyAttachmentReconciler struct { client.Client Region string @@ -38,9 +43,9 @@ type PolicyAttachmentReconciler struct { Scheme *runtime.Scheme } +// Reconcile PolicyAttachment // +kubebuilder:rbac:groups=aws-iam.redradrat.xyz,resources=policyattachments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=aws-iam.redradrat.xyz,resources=policyattachments/status,verbs=get;update;patch - func (r *PolicyAttachmentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("policyattachment", req.NamespacedName) @@ -56,25 +61,22 @@ func (r *PolicyAttachmentReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, nil } - // the finalizer for deleting the actual aws resources - policyAttachmentFinalizer := "policyattachment.aws-iam.redradrat.xyz" - // first let's get the ARNs from the referenced resources in the spec - policyArn, targetArn, err := getPolicyAttachmentARNs(&policyattachment, ctx, r.Client) + policyArn, targetArn, err := getPolicyAttachmentARNs(ctx, &policyattachment, r.Client) if err != nil { - return ctrl.Result{}, errWithStatus(&policyattachment, err, r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &policyattachment, err, r.Status()) } // now we need to translate the specified target resource in the CR to an IAM AttachmentType attachType, err := policyattachment.GetAttachmentType() if err != nil { - return ctrl.Result{}, errWithStatus(&policyattachment, err, r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &policyattachment, err, r.Status()) } // Get our actual IAM Service to communicate with AWS; we don't need to continue without it iamsvc, err := IAMService(r.Region) if err != nil { - return ctrl.Result{}, errWithStatus(&policyattachment, err, r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &policyattachment, err, r.Status()) } // now let's instantiate our PolicyAttachmentInstance @@ -87,7 +89,7 @@ func (r *PolicyAttachmentReconciler) Reconcile(ctx context.Context, req ctrl.Req // registering our finalizer. if !containsString(policyattachment.ObjectMeta.Finalizers, policyAttachmentFinalizer) { policyattachment.ObjectMeta.Finalizers = append(policyattachment.ObjectMeta.Finalizers, policyAttachmentFinalizer) - if err := r.Update(context.Background(), &policyattachment); err != nil { + if err := r.Update(ctx, &policyattachment); err != nil { log.Error(err, "unable to register finalizer for PolicyAttachment") return ctrl.Result{}, err } @@ -99,7 +101,7 @@ func (r *PolicyAttachmentReconciler) Reconcile(ctx context.Context, req ctrl.Req // delete the actual AWS Object and pass the cleanup function statusUpdater, err := DeleteAWSObject(iamsvc, ins, DoNothingPreFunc) // we got a StatusUpdater function returned... let's execute it - statusUpdater(ins, &policyattachment, ctx, r.Status(), log) + statusUpdater(ctx, ins, &policyattachment, r.Status(), log) if err != nil { // we had an error during AWS Object deletion... so we return here to retry log.Error(err, "unable to delete PolicyAttachment") @@ -108,7 +110,7 @@ func (r *PolicyAttachmentReconciler) Reconcile(ctx context.Context, req ctrl.Req // remove our finalizer from the list and update it. policyattachment.ObjectMeta.Finalizers = removeString(policyattachment.ObjectMeta.Finalizers, policyAttachmentFinalizer) - if err := r.Update(context.Background(), &policyattachment); err != nil { + if err := r.Update(ctx, &policyattachment); err != nil { log.Error(err, "unable to remove finalizer from PolicyAttachment") return ctrl.Result{}, err } @@ -130,7 +132,7 @@ func (r *PolicyAttachmentReconciler) Reconcile(ctx context.Context, req ctrl.Req // delete the actual AWS Object and pass the cleanup function statusUpdater, err := DeleteAWSObject(iamsvc, ins, DoNothingPreFunc) // we got a StatusUpdater function returned... let's execute it - statusUpdater(ins, &policyattachment, ctx, r.Status(), log) + statusUpdater(ctx, ins, &policyattachment, r.Status(), log) if err != nil { // we had an error during AWS Object deletion... so we return here to retry log.Error(err, "error while deleting PolicyAttachment during reconciliation") @@ -138,10 +140,10 @@ func (r *PolicyAttachmentReconciler) Reconcile(ctx context.Context, req ctrl.Req } } statusUpdater, err := CreateAWSObject(iamsvc, ins, DoNothingPreFunc) - statusUpdater(ins, &policyattachment, ctx, r.Status(), log) + statusUpdater(ctx, ins, &policyattachment, r.Status(), log) if err != nil { log.Error(err, "error while creating PolicyAttachment during reconciliation") - return ctrl.Result{}, errWithStatus(&policyattachment, err, r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &policyattachment, err, r.Status()) } policyattachment.Status.ObservedGeneration = policyattachment.ObjectMeta.Generation @@ -154,7 +156,7 @@ func (r *PolicyAttachmentReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, nil } -func checkPolicyAttachmentRefs(policyAttachment *iamv1beta1.PolicyAttachment, c client.Client, ctx context.Context) error { +func checkPolicyAttachmentRefs(ctx context.Context, policyAttachment *iamv1beta1.PolicyAttachment, c client.Client) error { policies := iamv1beta1.PolicyList{} if err := c.List(ctx, &policies); err != nil { return err @@ -207,83 +209,106 @@ func checkPolicyAttachmentRefs(policyAttachment *iamv1beta1.PolicyAttachment, c } } if !(foundtarget == true && foundpolicy == true) { - err := fmt.Errorf(fmt.Sprintf("defined references do not exist for PolicyAttachment '%s/%s", policyAttachment.Name, policyAttachment.Namespace)) + err := fmt.Errorf("defined references do not exist for PolicyAttachment '%s/%s", policyAttachment.Name, policyAttachment.Namespace) return err } return nil } -func (r *PolicyAttachmentReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&iamv1beta1.PolicyAttachment{}). - Complete(r) -} - -func getPolicyAttachmentARNs(policyAttachment *iamv1beta1.PolicyAttachment, ctx context.Context, c client.Client) (awsarn.ARN, awsarn.ARN, error) { - var policyArn awsarn.ARN - var targetArn awsarn.ARN +func getPolicyAttachmentARNs(ctx context.Context, policyAttachment *iamv1beta1.PolicyAttachment, c client.Client) (targetArn, policyArn awsarn.ARN, err error) { - if err := checkPolicyAttachmentRefs(policyAttachment, c, ctx); err != nil { - return policyArn, targetArn, err + if policyAttachment.Spec.ExternalPolicy.ARN == "" && policyAttachment.Spec.PolicyReference.Name == "" { + return policyArn, targetArn, fmt.Errorf("one of policy or externalPolicy must be set") } - polref := policyAttachment.Spec.PolicyReference - tarref := policyAttachment.Spec.TargetReference - policy := iamv1beta1.Policy{} - if err := c.Get(ctx, client.ObjectKey{Name: polref.Name, Namespace: polref.Namespace}, &policy); err != nil { - return policyArn, targetArn, err - } + // If there is Policy ARN given, we need to attach that policy to the target + if policyAttachment.Spec.ExternalPolicy.ARN != "" { - if policy.Status.ARN == "" { - return policyArn, targetArn, fmt.Errorf("ARN is empty in status for policy reference") + if policyAttachment.Spec.PolicyReference.Name != "" { + return policyArn, targetArn, fmt.Errorf("cannot define both policy and externalPolicy") + } + + // Check if valid ARN + if awsarn.IsARN(policyAttachment.Spec.ExternalPolicy.ARN) == false { + return policyArn, targetArn, fmt.Errorf("given ARN '%s' is not valid", policyAttachment.Spec.ExternalPolicy.ARN) + } + policyArn, err = awsarn.Parse(policyAttachment.Spec.ExternalPolicy.ARN) + if err != nil { + return policyArn, targetArn, err + } + } else { + polRef := policyAttachment.Spec.PolicyReference + if err := checkPolicyAttachmentRefs(ctx, policyAttachment, c); err != nil { + return policyArn, targetArn, err + } + + policy := iamv1beta1.Policy{} + if err := c.Get(ctx, client.ObjectKey{Name: polRef.Name, Namespace: polRef.Namespace}, &policy); err != nil { + return policyArn, targetArn, err + } + + if policy.Status.ARN == "" { + return policyArn, targetArn, fmt.Errorf("ARN is empty in status for policy reference") + } + policyArn, err = awsarn.Parse(policy.Status.ARN) + if err != nil { + return policyArn, targetArn, err + } } - policyArn, err := awsarn.Parse(policy.Status.ARN) - if err != nil { - return policyArn, targetArn, err + + targetObj := &client.ObjectKey{ + Name: policyAttachment.Spec.TargetReference.Name, + Namespace: policyAttachment.Spec.TargetReference.Namespace, } - targetReferenceType := policyAttachment.Spec.TargetReference.Type - switch targetReferenceType { + targetType := policyAttachment.Spec.TargetReference.Type + switch targetType { case iamv1beta1.RoleTargetType: - role := iamv1beta1.Role{} - if err := c.Get(ctx, client.ObjectKey{Name: tarref.Name, Namespace: tarref.Namespace}, &role); err != nil { + target := iamv1beta1.Role{} + if err := c.Get(ctx, *targetObj, &target); err != nil { return policyArn, targetArn, err } - if role.Status.ARN == "" { + if target.Status.ARN == "" { return policyArn, targetArn, fmt.Errorf("ARN is empty in status for target reference") } - targetArn, err = awsarn.Parse(role.Status.ARN) + targetArn, err = awsarn.Parse(target.Status.ARN) if err != nil { return policyArn, targetArn, err } case iamv1beta1.UserTargetType: - user := iamv1beta1.User{} - if err := c.Get(ctx, client.ObjectKey{Name: tarref.Name, Namespace: tarref.Namespace}, &user); err != nil { + target := iamv1beta1.User{} + if err := c.Get(ctx, *targetObj, &target); err != nil { return policyArn, targetArn, err } - if user.Status.ARN == "" { + if target.Status.ARN == "" { return policyArn, targetArn, fmt.Errorf("ARN is empty in status for target reference") } - targetArn, err = awsarn.Parse(user.Status.ARN) + targetArn, err = awsarn.Parse(target.Status.ARN) if err != nil { return policyArn, targetArn, err } case iamv1beta1.GroupTargetType: - group := iamv1beta1.Group{} - if err := c.Get(ctx, client.ObjectKey{Name: tarref.Name, Namespace: tarref.Namespace}, &group); err != nil { + target := iamv1beta1.Group{} + if err := c.Get(ctx, *targetObj, &target); err != nil { return policyArn, targetArn, err } - if group.Status.ARN == "" { + if target.Status.ARN == "" { return policyArn, targetArn, fmt.Errorf("ARN is empty in status for target reference") } - targetArn, err = awsarn.Parse(group.Status.ARN) + targetArn, err = awsarn.Parse(target.Status.ARN) if err != nil { return policyArn, targetArn, err } default: - return policyArn, targetArn, fmt.Errorf("defined target reference type '%s' is unknown", targetReferenceType) + return policyArn, targetArn, fmt.Errorf("defined target reference type '%s' is unknown", targetType) } return policyArn, targetArn, nil } + +func (r *PolicyAttachmentReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&iamv1beta1.PolicyAttachment{}). + Complete(r) +} diff --git a/controllers/role_controller.go b/controllers/role_controller.go index 8ac36c4..e7f31ba 100644 --- a/controllers/role_controller.go +++ b/controllers/role_controller.go @@ -70,7 +70,7 @@ func (r *RoleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. // get the policy doc polDoc, resVer, err := getPolicyDoc(&role, r.OidcProviderARN, r.Client, ctx) if err != nil { - return ctrl.Result{}, errWithStatus(&role, err, r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &role, err, r.Status()) } reconcileUnneccessary := @@ -90,7 +90,7 @@ func (r *RoleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. // Get our actual IAM Service to communicate with AWS; we don't need to continue without it iamsvc, err := IAMService(r.Region) if err != nil { - return ctrl.Result{}, errWithStatus(&role, err, r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &role, err, r.Status()) } // new role instance @@ -103,7 +103,7 @@ func (r *RoleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. if role.Status.ARN != "" { parsedArn, err := aws.ARNify(role.Status.ARN) if err != nil { - return ctrl.Result{}, errWithStatus(&role, fmt.Errorf("ARN in Role status is not valid/parsable"), r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &role, fmt.Errorf("ARN in Role status is not valid/parsable"), r.Status()) } ins = iam.NewExistingRoleInstance(roleName, role.Spec.Description, duration, polDoc, parsedArn[len(parsedArn)-1]) } else { @@ -131,7 +131,7 @@ func (r *RoleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. // delete the actual AWS Object and pass the cleanup function statusUpdater, err := DeleteAWSObject(iamsvc, ins, cleanupFunc) // we got a StatusUpdater function returned... let's execute it - statusUpdater(ins, &role, ctx, r.Status(), log) + statusUpdater(ctx, ins, &role, r.Status(), log) if err != nil { // we had an error during AWS Object deletion... so we return here to retry log.Error(err, "unable to delete Role") @@ -158,7 +158,7 @@ func (r *RoleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. // delete the actual AWS Object and pass the cleanup function statusUpdater, err := DeleteAWSObject(iamsvc, ins, cleanupFunc) // we got a StatusUpdater function returned... let's execute it - statusUpdater(ins, &role, ctx, r.Status(), log) + statusUpdater(ctx, ins, &role, r.Status(), log) if err != nil { // we had an error during AWS Object deletion... so we return here to retry log.Error(err, "error while deleting Role during reconciliation") @@ -167,7 +167,7 @@ func (r *RoleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. } statusUpdater, err := CreateAWSObject(iamsvc, ins, DoNothingPreFunc) - statusUpdater(ins, &role, ctx, r.Status(), log) + statusUpdater(ctx, ins, &role, r.Status(), log) if err != nil { log.Error(err, "error while creating Role during reconciliation") return ctrl.Result{}, err @@ -214,7 +214,7 @@ func roleCleanup(r *RoleReconciler, ctx context.Context, role iamv1beta1.Role) f for _, att := range attachments.Items { if att.Spec.TargetReference.Type == iamv1beta1.RoleTargetType { if att.Spec.TargetReference.Name == role.Name && att.Spec.TargetReference.Namespace == role.Namespace { - err := fmt.Errorf(fmt.Sprintf("cannot delete Role due to existing PolicyAttachment '%s/%s'", att.Name, att.Namespace)) + err := fmt.Errorf("cannot delete Role due to existing PolicyAttachment '%s/%s'", att.Name, att.Namespace) return err } } diff --git a/controllers/user_controller.go b/controllers/user_controller.go index 8ff3c01..e1e3bba 100644 --- a/controllers/user_controller.go +++ b/controllers/user_controller.go @@ -82,7 +82,7 @@ func (r *UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. // Get our actual IAM Service to communicate with AWS; we don't need to continue without it iamsvc, err := IAMService(r.Region) if err != nil { - return ctrl.Result{}, errWithStatus(&user, err, r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &user, err, r.Status()) } // new user instance @@ -91,7 +91,7 @@ func (r *UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. if user.Status.ARN != "" { parsedArn, err := aws.ARNify(user.Status.ARN) if err != nil { - return ctrl.Result{}, errWithStatus(&user, fmt.Errorf("ARN in User status is not valid/parsable"), r.Status(), ctx) + return ctrl.Result{}, errWithStatus(ctx, &user, fmt.Errorf("ARN in User status is not valid/parsable"), r.Status()) } ins = iam.NewExistingUserInstance(userName, user.Spec.CreateLoginProfile, user.Status.LoginProfileCreated, user.Spec.CreateProgrammaticAccess, user.Status.ProgrammaticAccessCreated, parsedArn[len(parsedArn)-1]) } else { @@ -119,7 +119,7 @@ func (r *UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. // delete the actual AWS Object and pass the cleanup function statusUpdater, err := DeleteAWSObject(iamsvc, ins, cleanupFunc) // we got a StatusUpdater function returned... let's execute it - statusUpdater(ins, &user, ctx, r.Status(), log) + statusUpdater(ctx, ins, &user, r.Status(), log) if err != nil { // we had an error during AWS Object deletion... so we return here to retry log.Error(err, "unable to delete User") @@ -148,7 +148,7 @@ func (r *UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. if user.Status.ARN != "" { // User already exists; we need to update it statusUpdater, err := UpdateAWSObject(iamsvc, ins, DoNothingPreFunc) - statusUpdater(ins, &user, ctx, r.Status(), log) + statusUpdater(ctx, ins, &user, r.Status(), log) if err != nil { log.Error(err, "error while updating User during reconciliation") return ctrl.Result{}, err @@ -156,7 +156,7 @@ func (r *UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. } else { // User does not yet exist, let's create it statusUpdater, err := CreateAWSObject(iamsvc, ins, DoNothingPreFunc) - statusUpdater(ins, &user, ctx, r.Status(), log) + statusUpdater(ctx, ins, &user, r.Status(), log) if err != nil { log.Error(err, "error while creating User during reconciliation") return ctrl.Result{}, err @@ -245,7 +245,7 @@ func userCleanup(r *UserReconciler, ctx context.Context, user iamv1beta1.User) f for _, att := range attachments.Items { if att.Spec.TargetReference.Type == iamv1beta1.UserTargetType { if att.Spec.TargetReference.Name == user.Name && att.Spec.TargetReference.Namespace == user.Namespace { - err := fmt.Errorf(fmt.Sprintf("cannot delete User due to existing PolicyAttachment '%s/%s'", att.Name, att.Namespace)) + err := fmt.Errorf("cannot delete User due to existing PolicyAttachment '%s/%s'", att.Name, att.Namespace) return err } }