Created
November 9, 2025 20:45
-
-
Save prabhakhar/de062ac88c16ea0e69c7a52999197d9d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "fmt" | |
| "strings" | |
| ackv1alpha1 "github.com/aws-controllers-k8s/iam-controller/apis/v1alpha1" | |
| "k8s.io/apimachinery/pkg/api/errors" | |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | |
| "k8s.io/apimachinery/pkg/runtime" | |
| "k8s.io/apimachinery/pkg/types" | |
| ctrl "sigs.k8s.io/controller-runtime" | |
| "sigs.k8s.io/controller-runtime/pkg/client" | |
| "sigs.k8s.io/controller-runtime/pkg/log" | |
| ) | |
| // ================================================================== | |
| // 1. NEW CUSTOM RESOURCE DEFINITIONS (The Inputs) | |
| // ================================================================== | |
| // In a real project, these would be in api/v1alpha1/xxx_types.go | |
| // +kubebuilder:object:root=true | |
| // +kubebuilder:subresource:status | |
| type EKSWorkloadIAM struct { | |
| metav1.TypeMeta `json:",inline"` | |
| metav1.ObjectMeta `json:"metadata,omitempty"` | |
| // This spec matches ActorConfig from the previous script | |
| Spec ActorConfig `json:"spec,omitempty"` | |
| } | |
| // +kubebuilder:object:root=true | |
| type EKSWorkloadIAMList struct { | |
| metav1.TypeMeta `json:",inline"` | |
| metav1.ListMeta `json:"metadata,omitempty"` | |
| Items []EKSWorkloadIAM `json:"items"` | |
| } | |
| // +kubebuilder:object:root=true | |
| // +kubebuilder:subresource:status | |
| type AuroraAccessIAM struct { | |
| metav1.TypeMeta `json:",inline"` | |
| metav1.ObjectMeta `json:"metadata,omitempty"` | |
| // This spec matches TargetConfig from the previous script | |
| Spec TargetConfig `json:"spec,omitempty"` | |
| } | |
| // +kubebuilder:object:root=true | |
| type AuroraAccessIAMList struct { | |
| metav1.TypeMeta `json:",inline"` | |
| metav1.ListMeta `json:"metadata,omitempty"` | |
| Items []AuroraAccessIAM `json:"items"` | |
| } | |
| // Re-using the config structs as Kubernetes Specs | |
| type ActorConfig struct { | |
| // AccountID is the AWS Account ID where this role should live | |
| AccountID string `json:"accountId"` | |
| // RoleName is the desired IAM Role name | |
| RoleName string `json:"roleName"` | |
| // --- Trust Method Selection --- | |
| // Option A: EKS Pod Identity (PIA) | |
| // e.g., "pods.eks.amazonaws.com" | |
| TrustPrincipal string `json:"trustPrincipal,omitempty"` | |
| // Option B: IRSA (OIDC) | |
| // OIDCProvider is the OIDC issuer URL without https:// | |
| // e.g., "oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE" | |
| OIDCProvider string `json:"oidcProvider,omitempty"` | |
| // ServiceAccountName for IRSA condition | |
| ServiceAccountName string `json:"serviceAccountName,omitempty"` | |
| // ServiceAccountNamespace for IRSA condition | |
| ServiceAccountNamespace string `json:"serviceAccountNamespace,omitempty"` | |
| // ------------------------------ | |
| // TargetRoleARNs is a list of DB roles this pod can assume | |
| TargetRoleARNs []string `json:"targetRoleArns,omitempty"` | |
| } | |
| type TargetConfig struct { | |
| AccountID string `json:"accountId"` | |
| RoleName string `json:"roleName"` | |
| Region string `json:"region"` | |
| DBClusterID string `json:"dbClusterId"` | |
| DBUser string `json:"dbUser"` | |
| TrustedRoleARNs []string `json:"trustedRoleArns,omitempty"` | |
| } | |
| // ================================================================== | |
| // 2. RECONCILERS (The Controller Logic) | |
| // ================================================================== | |
| // EKSWorkloadIAMReconciler watches EKSWorkloadIAM inputs and creates ACK Roles | |
| type EKSWorkloadIAMReconciler struct { | |
| client.Client | |
| Scheme *runtime.Scheme | |
| } | |
| func (r *EKSWorkloadIAMReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { | |
| l := log.FromContext(ctx) | |
| // 1. Fetch Input (EKSWorkloadIAM) | |
| var input EKSWorkloadIAM | |
| if err := r.Get(ctx, req.NamespacedName, &input); err != nil { | |
| return ctrl.Result{}, client.IgnoreNotFound(err) | |
| } | |
| // 2. Generate the desired ACK Role | |
| desiredACKRole, err := r.generateACKRole(&input) | |
| if err != nil { | |
| l.Error(err, "failed to generate ACK role definition") | |
| // Don't requeue if it's a configuration error in the CR | |
| if strings.Contains(err.Error(), "invalid configuration") { | |
| return ctrl.Result{}, nil | |
| } | |
| return ctrl.Result{}, err // Retry for other errors | |
| } | |
| // 3. Create or Update the ACK Role on the cluster | |
| var currentACKRole ackv1alpha1.Role | |
| err = r.Get(ctx, types.NamespacedName{Name: desiredACKRole.Name, Namespace: desiredACKRole.Namespace}, ¤tACKRole) | |
| if errors.IsNotFound(err) { | |
| l.Info("Creating new ACK Role", "role", desiredACKRole.Name) | |
| if err := r.Create(ctx, desiredACKRole); err != nil { | |
| return ctrl.Result{}, err | |
| } | |
| return ctrl.Result{}, nil | |
| } else if err != nil { | |
| return ctrl.Result{}, err | |
| } | |
| // Simple update logic: overwrite spec if it exists | |
| // (In prod, you might compare specs to avoid unnecessary API calls) | |
| currentACKRole.Spec = desiredACKRole.Spec | |
| l.Info("Updating existing ACK Role", "role", currentACKRole.Name) | |
| if err := r.Update(ctx, ¤tACKRole); err != nil { | |
| return ctrl.Result{}, err | |
| } | |
| return ctrl.Result{}, nil | |
| } | |
| // generateACKRole adapts the old 'GenerateActorRole' function for K8s | |
| func (r *EKSWorkloadIAMReconciler) generateACKRole(input *EKSWorkloadIAM) (*ackv1alpha1.Role, error) { | |
| var trustPolicy IAMPolicy | |
| // --- Trust Policy Generation Logic --- | |
| if input.Spec.OIDCProvider != "" { | |
| // === OPTION B: IRSA === | |
| if input.Spec.ServiceAccountName == "" || input.Spec.ServiceAccountNamespace == "" { | |
| return nil, fmt.Errorf("invalid configuration: OIDCProvider requires ServiceAccountName and ServiceAccountNamespace") | |
| } | |
| oidcARN := fmt.Sprintf("arn:aws:iam::%s:oidc-provider/%s", input.Spec.AccountID, input.Spec.OIDCProvider) | |
| subCondition := fmt.Sprintf("system:serviceaccount:%s:%s", input.Spec.ServiceAccountNamespace, input.Spec.ServiceAccountName) | |
| trustPolicy = IAMPolicy{ | |
| Version: "2012-10-17", | |
| Statement: []IAMStatement{{ | |
| Sid: "TrustIRSA", | |
| Effect: "Allow", | |
| Principal: map[string]string{ | |
| "Federated": oidcARN, | |
| }, | |
| Action: "sts:AssumeRoleWithWebIdentity", | |
| Condition: map[string]interface{}{ | |
| "StringEquals": map[string]string{ | |
| // "oidc.eks.REGION.amazonaws.com/id/XXX:sub": "system:serviceaccount:ns:sa" | |
| fmt.Sprintf("%s:sub", input.Spec.OIDCProvider): subCondition, | |
| // "oidc.eks.REGION.amazonaws.com/id/XXX:aud": "sts.amazonaws.com" | |
| fmt.Sprintf("%s:aud", input.Spec.OIDCProvider): "sts.amazonaws.com", | |
| }, | |
| }, | |
| }}, | |
| } | |
| } else if input.Spec.TrustPrincipal != "" { | |
| // === OPTION A: EKS Pod Identity === | |
| trustPolicy = IAMPolicy{ | |
| Version: "2012-10-17", | |
| Statement: []IAMStatement{{ | |
| Sid: "TrustEKSPodIdentity", | |
| Effect: "Allow", | |
| Principal: map[string]string{ | |
| "Service": input.Spec.TrustPrincipal, | |
| }, | |
| Action: []string{"sts:AssumeRole", "sts:TagSession"}, | |
| }}, | |
| } | |
| } else { | |
| return nil, fmt.Errorf("invalid configuration: must specify either trustPrincipal (PIA) or oidcProvider (IRSA)") | |
| } | |
| trustJSON, _ := json.Marshal(trustPolicy) | |
| // --- Permissions Policy Generation Logic --- | |
| permPolicy := IAMPolicy{ | |
| Version: "2012-10-17", | |
| Statement: []IAMStatement{{ | |
| Sid: "AllowCrossAccountAssume", | |
| Effect: "Allow", | |
| Action: "sts:AssumeRole", | |
| Resource: input.Spec.TargetRoleARNs, | |
| }}, | |
| } | |
| permJSON, _ := json.Marshal(permPolicy) | |
| // ------------------------------------------------ | |
| ackRole := &ackv1alpha1.Role{ | |
| ObjectMeta: metav1.ObjectMeta{ | |
| Name: "ack-" + strings.ToLower(input.Spec.RoleName), | |
| Namespace: input.Namespace, | |
| }, | |
| Spec: ackv1alpha1.RoleSpec{ | |
| Name: &input.Spec.RoleName, | |
| AssumeRolePolicyDocument: string(trustJSON), | |
| InlinePolicies: map[string]*string{ | |
| "CrossAccountHops": awsString(string(permJSON)), | |
| }, | |
| }, | |
| } | |
| // CRITICAL: Set OwnerReference so deleting the Input deletes the ACK Role | |
| if err := ctrl.SetControllerReference(input, ackRole, r.Scheme); err != nil { | |
| return nil, err | |
| } | |
| return ackRole, nil | |
| } | |
| // AuroraAccessIAMReconciler watches AuroraAccessIAM inputs and creates ACK Roles | |
| type AuroraAccessIAMReconciler struct { | |
| client.Client | |
| Scheme *runtime.Scheme | |
| } | |
| func (r *AuroraAccessIAMReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { | |
| l := log.FromContext(ctx) | |
| var input AuroraAccessIAM | |
| if err := r.Get(ctx, req.NamespacedName, &input); err != nil { | |
| return ctrl.Result{}, client.IgnoreNotFound(err) | |
| } | |
| desiredACKRole, err := r.generateACKRole(&input) | |
| if err != nil { | |
| l.Error(err, "failed to generate ACK role definition") | |
| return ctrl.Result{}, err | |
| } | |
| var currentACKRole ackv1alpha1.Role | |
| err = r.Get(ctx, types.NamespacedName{Name: desiredACKRole.Name, Namespace: desiredACKRole.Namespace}, ¤tACKRole) | |
| if errors.IsNotFound(err) { | |
| l.Info("Creating new Aurora Target ACK Role", "role", desiredACKRole.Name) | |
| if err := r.Create(ctx, desiredACKRole); err != nil { | |
| return ctrl.Result{}, err | |
| } | |
| return ctrl.Result{}, nil | |
| } else if err != nil { | |
| return ctrl.Result{}, err | |
| } | |
| currentACKRole.Spec = desiredACKRole.Spec | |
| l.Info("Updating existing Aurora Target ACK Role", "role", currentACKRole.Name) | |
| if err := r.Update(ctx, ¤tACKRole); err != nil { | |
| return ctrl.Result{}, err | |
| } | |
| return ctrl.Result{}, nil | |
| } | |
| func (r *AuroraAccessIAMReconciler) generateACKRole(input *AuroraAccessIAM) (*ackv1alpha1.Role, error) { | |
| // --- Re-using IAM generation logic --- | |
| trustPolicy := IAMPolicy{ | |
| Version: "2012-10-17", | |
| Statement: []IAMStatement{{ | |
| Sid: "TrustExternalPods", | |
| Effect: "Allow", | |
| Principal: map[string][]string{"AWS": input.Spec.TrustedRoleARNs}, | |
| Action: "sts:AssumeRole", | |
| }}, | |
| } | |
| trustJSON, _ := json.Marshal(trustPolicy) | |
| dbResourceARN := fmt.Sprintf("arn:aws:rds-db:%s:%s:dbuser:%s/%s", | |
| input.Spec.Region, input.Spec.AccountID, input.Spec.DBClusterID, input.Spec.DBUser) | |
| permPolicy := IAMPolicy{ | |
| Version: "2012-10-17", | |
| Statement: []IAMStatement{{ | |
| Sid: "AllowDBConnect", | |
| Effect: "Allow", | |
| Action: "rds-db:connect", | |
| Resource: dbResourceARN, | |
| }}, | |
| } | |
| permJSON, _ := json.Marshal(permPolicy) | |
| // ------------------------------------ | |
| ackRole := &ackv1alpha1.Role{ | |
| ObjectMeta: metav1.ObjectMeta{ | |
| Name: "ack-" + strings.ToLower(input.Spec.RoleName), | |
| Namespace: input.Namespace, | |
| }, | |
| Spec: ackv1alpha1.RoleSpec{ | |
| Name: &input.Spec.RoleName, | |
| AssumeRolePolicyDocument: string(trustJSON), | |
| InlinePolicies: map[string]*string{ | |
| "DBConnectPermissions": awsString(string(permJSON)), | |
| }, | |
| }, | |
| } | |
| if err := ctrl.SetControllerReference(input, ackRole, r.Scheme); err != nil { | |
| return nil, err | |
| } | |
| return ackRole, nil | |
| } | |
| // ================================================================== | |
| // 3. HELPER STRUCTS (from original script) | |
| // ================================================================== | |
| type IAMPolicy struct { | |
| Version string `json:"Version"` | |
| Statement []IAMStatement `json:"Statement"` | |
| } | |
| type IAMStatement struct { | |
| Sid string `json:"Sid,omitempty"` | |
| Effect string `json:"Effect"` | |
| Principal interface{} `json:"Principal,omitempty"` | |
| Action interface{} `json:"Action"` | |
| Resource interface{} `json:"Resource,omitempty"` | |
| Condition map[string]interface{} `json:"Condition,omitempty"` | |
| } | |
| func awsString(v string) *string { return &v } | |
| // ================================================================== | |
| // 4. MAIN (Setup) | |
| // ================================================================== | |
| // In a real operator, this would be in main.go | |
| func Setup(mgr ctrl.Manager) error { | |
| // Register our 2 new controllers with the manager | |
| if err := (&EKSWorkloadIAMReconciler{ | |
| Client: mgr.GetClient(), | |
| Scheme: mgr.GetScheme(), | |
| }).SetupWithManager(mgr); err != nil { | |
| return err | |
| } | |
| if err := (&AuroraAccessIAMReconciler{ | |
| Client: mgr.GetClient(), | |
| Scheme: mgr.GetScheme(), | |
| }).SetupWithManager(mgr); err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| // Boilerplate to attach reconciler to manager | |
| func (r *EKSWorkloadIAMReconciler) SetupWithManager(mgr ctrl.Manager) error { | |
| return ctrl.NewControllerManagedBy(mgr). | |
| For(&EKSWorkloadIAM{}). | |
| Owns(&ackv1alpha1.Role{}). // Watch ACK roles we own | |
| Complete(r) | |
| } | |
| func (r *AuroraAccessIAMReconciler) SetupWithManager(mgr ctrl.Manager) error { | |
| return ctrl.NewControllerManagedBy(mgr). | |
| For(&AuroraAccessIAM{}). | |
| Owns(&ackv1alpha1.Role{}). | |
| Complete(r) | |
| } |
Author
prabhakhar
commented
Nov 10, 2025
Author
// In your AccessSubscription controller's Reconcile loop...
func (r *AccessSubscriptionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 1. Fetch AccessSubscription
var sub AccessSubscription
if err := r.Get(ctx, req.NamespacedName, &sub); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) }
// 2. CALCULATE CONFIG (Mocked here - this comes from your ApplicationInstance CRD)
// Assume you found the app runs in 2 accounts.
actorConfigs := []iamgen.ActorConfig{
{
Name: sub.Name + "-actor-acct1", Namespace: sub.Namespace,
AccountID: "111111111111", RoleName: "my-app-role",
Type: iamgen.ActorTypeIRSA,
IRSA: &iamgen.IRSAConfig{OIDCProvider: "oidc...", ServiceAccount: "sa", ServiceNamespace: "ns"},
// We know the target ARN ahead of time deterministically
TargetRoleARNs: []string{fmt.Sprintf("arn:aws:iam::%s:role/%s", "333333333333", "target-db-role")},
},
{
Name: sub.Name + "-actor-acct2", Namespace: sub.Namespace,
AccountID: "222222222222", RoleName: "my-app-role",
// ... similar config for second account ...
TargetRoleARNs: []string{fmt.Sprintf("arn:aws:iam::%s:role/%s", "333333333333", "target-db-role")},
},
}
targetConfig := iamgen.TargetConfig{
Name: sub.Name + "-target", Namespace: sub.Namespace,
AccountID: "333333333333", RoleName: "target-db-role",
Type: iamgen.TargetTypeDatabase,
Database: &iamgen.DatabaseConfig{Region: "us-west-2", DBClusterID: "mydb", DBUser: "appuser"},
// Trust the known ARNs of the actors
TrustedRoleARNs: []string{
"arn:aws:iam::111111111111:role/my-app-role",
"arn:aws:iam::222222222222:role/my-app-role",
},
}
// 3. RECONCILE ACTORS
allActorsSynced := true
for _, cfg := range actorConfigs {
desiredRole, _ := iamgen.GenerateActorRole(cfg)
// Set owner ref so deleting Subscription deletes Roles
ctrl.SetControllerReference(&sub, desiredRole, r.Scheme)
synced, err := r.applyAndCheckStatus(ctx, desiredRole)
if err != nil { return ctrl.Result{}, err }
if !synced { allActorsSynced = false }
}
// 4. RECONCILE TARGET
desiredTarget, _ := iamgen.GenerateTargetRole(targetConfig)
ctrl.SetControllerReference(&sub, desiredTarget, r.Scheme)
targetSynced, err := r.applyAndCheckStatus(ctx, desiredTarget)
if err != nil { return ctrl.Result{}, err }
// 5. AGGREGATE STATUS
newStatus := AccessSubscriptionStatus{
IAMReady: allActorsSynced && targetSynced,
// ... other status fields ...
}
// Update status if changed
if sub.Status.IAMReady != newStatus.IAMReady {
sub.Status = newStatus
r.Status().Update(ctx, &sub)
}
return ctrl.Result{}, nil
}
// Helper to Apply ACK Role and check its 'ResourceSynced' condition
func (r *AccessSubscriptionReconciler) applyAndCheckStatus(ctx context.Context, desired *ackv1alpha1.Role) (bool, error) {
var current ackv1alpha1.Role
err := r.Get(ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, ¤t)
if errors.IsNotFound(err) {
if err := r.Create(ctx, desired); err != nil { return false, err }
return false, nil // Created, not yet synced
} else if err != nil { return false, err }
// Update if spec changed
// Note: In prod, do a deep comparison of Spec before updating to avoid noise
current.Spec = desired.Spec
if err := r.Update(ctx, ¤t); err != nil { return false, err }
// CHECK ACK STATUS
for _, cond := range current.Status.Conditions {
// ACK standard condition for "AWS resource is created and matches spec"
if cond.Type == ackv1alpha1.ConditionTypeResourceSynced && cond.Status == corev1.ConditionTrue {
return true, nil
}
}
return false, nil
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment