Skip to content

Commit

Permalink
irsa create/update/delete role and attach policy
Browse files Browse the repository at this point in the history
  • Loading branch information
kkb0318 committed May 20, 2024
1 parent 7b94a15 commit eb2cc64
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 300 deletions.
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ GOLANGCI_LINT_VERSION ?= v1.57.2


.PHONY: all
all: fmt vet lint generate manifests kustomize helmify generate-docs mock
all: fmt vet lint generate manifests kustomize helmify generate-docs


##@ Development
Expand Down Expand Up @@ -223,3 +223,9 @@ OPERATOR_SDK = $(shell which operator-sdk)
endif
endif

.PHONY: test-deploy
test-deploy: build install
docker build . -t ghcr.io/kkb0318/irsa-manager
docker push ghcr.io/kkb0318/irsa-manager:latest


5 changes: 0 additions & 5 deletions api/v1alpha1/irsa_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// IRSASpec defines the desired state of IRSA
type IRSASpec struct {
// ServiceAccount represents the Kubernetes service account associated with the IRSA
Expand All @@ -43,8 +40,6 @@ type IRSAServiceAccount struct {

// IamRole represents the IAM role configuration
type IamRole struct {
// Create specifies whether to create the IAM role or not
Create bool `json:"create,omitempty"`
// Name represents the name of the IAM role
Name string `json:"name,omitempty"`
}
Expand Down
4 changes: 0 additions & 4 deletions config/crd/bases/irsa.kkb0318.github.io_irsas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ spec:
description: IamRole represents the IAM role details associated with
the IRSA
properties:
create:
description: Create specifies whether to create the IAM role or
not
type: boolean
name:
description: Name represents the name of the IAM role
type: string
Expand Down
1 change: 0 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ _Appears in:_

| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `create` _boolean_ | Create specifies whether to create the IAM role or not | | |
| `name` _string_ | Name represents the name of the IAM role | | |


Expand Down
5 changes: 5 additions & 0 deletions internal/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ type AwsClientFactory struct {
type AwsIamAPI interface {
CreateOpenIDConnectProvider(ctx context.Context, params *iam.CreateOpenIDConnectProviderInput, optFns ...func(*iam.Options)) (*iam.CreateOpenIDConnectProviderOutput, error)
DeleteOpenIDConnectProvider(ctx context.Context, params *iam.DeleteOpenIDConnectProviderInput, optFns ...func(*iam.Options)) (*iam.DeleteOpenIDConnectProviderOutput, error)
CreateRole(ctx context.Context, params *iam.CreateRoleInput, optFns ...func(*iam.Options)) (*iam.CreateRoleOutput, error)
UpdateAssumeRolePolicy(ctx context.Context, params *iam.UpdateAssumeRolePolicyInput, optFns ...func(*iam.Options)) (*iam.UpdateAssumeRolePolicyOutput, error)
AttachRolePolicy(ctx context.Context, params *iam.AttachRolePolicyInput, optFns ...func(*iam.Options)) (*iam.AttachRolePolicyOutput, error)
DeleteRole(ctx context.Context, params *iam.DeleteRoleInput, optFns ...func(*iam.Options)) (*iam.DeleteRoleOutput, error)
DetachRolePolicy(ctx context.Context, params *iam.DetachRolePolicyInput, optFns ...func(*iam.Options)) (*iam.DetachRolePolicyOutput, error)
}

type AwsStsAPI interface {
Expand Down
134 changes: 134 additions & 0 deletions internal/aws/aws_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package aws

import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"slices"
"strings"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/smithy-go"
)

// RoleManager represents the details needed to manage IAM roles
type RoleManager struct {
// RoleName represents the name of the IAM role
RoleName string
// Namespaces represents the list of namespaces associated with the role
Namespaces []string
// Policies represents the list of policies to be attached to the role
Policies []string
}

func (r *RoleManager) PolicyArn(policy string) *string {
prefix := "arn:aws:iam::"
if strings.HasPrefix(policy, prefix) {
return aws.String(policy)
}
return aws.String(fmt.Sprintf("%saws:policy/%s", prefix, policy))
}

// DeleteIRSARole detaches specified policies from the IAM role and deletes the IAM role
func (a *AwsIamClient) DeleteIRSARole(ctx context.Context, r RoleManager) error {
for _, policy := range r.Policies {
detachRolePolicyInput := &iam.DetachRolePolicyInput{
RoleName: aws.String(r.RoleName),
PolicyArn: r.PolicyArn(policy),
}
_, err := a.Client.DetachRolePolicy(ctx, detachRolePolicyInput)
// Ignore error if the policy is already detached or the role does not exist
if errorHandle(err, []string{"NoSuchEntity"}) != nil {
return err
}
log.Printf("Policy %s detached from role %s successfully", policy, r.RoleName)

}
input := &iam.DeleteRoleInput{RoleName: aws.String(r.RoleName)}
_, err := a.Client.DeleteRole(ctx, input)
// Ignore error if the role does not exist or there are other policies that this controller does not manage
if errorHandle(err, []string{"DeleteConflict", "NoSuchEntity"}) != nil {
return err
}
log.Printf("Role %s deleted successfully", r.RoleName)
return nil
}

// CreateIRSARole creates an IAM role with the specified trust policy and attaches specified policies to it
func (a *AwsIamClient) CreateIRSARole(ctx context.Context, accountId, issuerHostPath string, r RoleManager) error {
providerArn := fmt.Sprintf("arn:aws:iam::%s:oidc-provider/%s", accountId, issuerHostPath)
statement := make([]map[string]interface{}, len(r.Namespaces))
for i, ns := range r.Namespaces {
statement[i] = map[string]interface{}{
"Effect": "Allow",
"Principal": map[string]interface{}{
"Federated": providerArn,
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": map[string]interface{}{
"StringEquals": map[string]interface{}{
fmt.Sprintf("%s:sub", issuerHostPath): fmt.Sprintf("system:serviceaccount:%s:%s", ns, r.RoleName),
},
},
}
}
trustPolicy := map[string]interface{}{
"Version": "2012-10-17",
"Statement": statement,
}
trustPolicyJSON, err := json.Marshal(trustPolicy)
if err != nil {
return fmt.Errorf("failed to marshal trust policy: %w", err)
}
createRoleInput := &iam.CreateRoleInput{
RoleName: aws.String(r.RoleName),
AssumeRolePolicyDocument: aws.String(string(trustPolicyJSON)),
}

_, err = a.Client.CreateRole(context.TODO(), createRoleInput)
if errorHandle(err, []string{"EntityAlreadyExists"}) != nil {
return err
}
log.Printf("Role %s created successfully", r.RoleName)

updateRoleInput := &iam.UpdateAssumeRolePolicyInput{
RoleName: aws.String(r.RoleName),
PolicyDocument: aws.String(string(trustPolicyJSON)),
}

_, err = a.Client.UpdateAssumeRolePolicy(context.TODO(), updateRoleInput)
if err != nil {
return fmt.Errorf("failed to update assume role policy for role %s: %w", r.RoleName, err)
}
log.Printf("Assume role policy for %s updated successfully", r.RoleName)

for _, policy := range r.Policies {
attachRolePolicyInput := &iam.AttachRolePolicyInput{
RoleName: aws.String(r.RoleName),
PolicyArn: r.PolicyArn(policy),
}

_, err = a.Client.AttachRolePolicy(context.TODO(), attachRolePolicyInput)
if err != nil {
return err
}
log.Printf("Policy %s attached to role %s successfully", policy, r.RoleName)

}
return nil
}

// errorHandle handles specific errors by checking the error code against a list of codes to ignore
func errorHandle(err error, errorCodes []string) error {
if err != nil {
var ae smithy.APIError
if errors.As(err, &ae) && slices.Contains(errorCodes, ae.ErrorCode()) {
fmt.Printf("Skipped error: %s \n", err.Error())
return nil
}
}
return err
}
20 changes: 20 additions & 0 deletions internal/controller/irsasetup_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,26 @@ func (m *mockAwsIamAPI) DeleteOpenIDConnectProvider(ctx context.Context, params
return &iam.DeleteOpenIDConnectProviderOutput{}, nil
}

func (m *mockAwsIamAPI) CreateRole(ctx context.Context, params *iam.CreateRoleInput, optFns ...func(*iam.Options)) (*iam.CreateRoleOutput, error) {
return nil, nil
}

func (m *mockAwsIamAPI) UpdateAssumeRolePolicy(ctx context.Context, params *iam.UpdateAssumeRolePolicyInput, optFns ...func(*iam.Options)) (*iam.UpdateAssumeRolePolicyOutput, error) {
return nil, nil
}

func (m *mockAwsIamAPI) AttachRolePolicy(ctx context.Context, params *iam.AttachRolePolicyInput, optFns ...func(*iam.Options)) (*iam.AttachRolePolicyOutput, error) {
return nil, nil
}

func (m *mockAwsIamAPI) DeleteRole(ctx context.Context, params *iam.DeleteRoleInput, optFns ...func(*iam.Options)) (*iam.DeleteRoleOutput, error) {
return nil, nil
}

func (m *mockAwsIamAPI) DetachRolePolicy(ctx context.Context, params *iam.DetachRolePolicyInput, optFns ...func(*iam.Options)) (*iam.DetachRolePolicyOutput, error) {
return nil, nil
}

func (m *mockAwsStsAPI) GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) {
return &sts.GetCallerIdentityOutput{Account: aws.String("123456789012")}, nil
}
Expand Down

0 comments on commit eb2cc64

Please sign in to comment.