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 9, 2025
Author
package iamgen
// ==================================================================
// ACTOR CONFIGURATION (The Pod/Workload)
// ==================================================================
type ActorType string
const (
ActorTypePodIdentity ActorType = "PodIdentity"
ActorTypeIRSA ActorType = "IRSA"
ActorTypeSPIFFE ActorType = "SPIFFE"
)
type ActorConfig struct {
// K8s Metadata for the resulting ACK Role
Name string
Namespace string
// AWS Specifics
AccountID string
RoleName string
// Trust Configuration (Discriminated Union)
Type ActorType
PodIdentity *PodIdentityConfig
IRSA *IRSAConfig
SPIFFE *SPIFFEConfig
// List of target role ARNs this actor needs to assume
TargetRoleARNs []string
}
type PodIdentityConfig struct {
TrustPrincipal string // e.g., "pods.eks.amazonaws.com"
}
type IRSAConfig struct {
OIDCProvider string
OIDCAudience string // defaults to sts.amazonaws.com
ServiceAccount string
ServiceNamespace string
}
type SPIFFEConfig struct {
OIDCProvider string
OIDCAudience string // defaults to sts.amazonaws.com
SPIFFEID string
}
// ==================================================================
// TARGET CONFIGURATION (The Database/Resource)
// ==================================================================
type TargetType string
const (
TargetTypeDatabase TargetType = "Database"
TargetTypeGeneric TargetType = "Generic"
)
type TargetConfig struct {
// K8s Metadata
Name string
Namespace string
// AWS Specifics
AccountID string
RoleName string
// The Actor roles allowed to assume this role
TrustedRoleARNs []string
// Resource Configuration
Type TargetType
Database *DatabaseConfig
Generic *GenericConfig
}
type DatabaseConfig struct {
Region string
DBClusterID string
DBUser string
}
type GenericConfig struct {
Action string // e.g., "s3:GetObject"
ResourceARN string // e.g., "arn:aws:s3:::my-bucket/*"
}
Author
package iamgen
import (
"encoding/json"
"fmt"
ackv1alpha1 "github.com/aws-controllers-k8s/iam-controller/apis/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// GenerateActorRole creates an ACK Role for a workload
func GenerateActorRole(cfg ActorConfig) (*ackv1alpha1.Role, error) {
var trustPolicy IAMPolicy
var err error
switch cfg.Type {
case ActorTypePodIdentity:
trustPolicy, err = generatePodIdentityTrust(cfg.PodIdentity)
case ActorTypeIRSA:
trustPolicy, err = generateIRSATrust(cfg.AccountID, cfg.IRSA)
case ActorTypeSPIFFE:
trustPolicy, err = generateSPIFFETrust(cfg.AccountID, cfg.SPIFFE)
default:
return nil, fmt.Errorf("unknown actor type: %s", cfg.Type)
}
if err != nil {
return nil, err
}
return buildACKRole(
cfg.Name, cfg.Namespace, cfg.RoleName,
trustPolicy,
"sts:AssumeRole", cfg.TargetRoleARNs,
"CrossAccountHops",
), nil
}
// GenerateTargetRole creates an ACK Role for a resource
func GenerateTargetRole(cfg TargetConfig) (*ackv1alpha1.Role, error) {
// 1. Build Trust Policy (trusting the actors)
trustPolicy := IAMPolicy{
Version: "2012-10-17",
Statement: []IAMStatement{{
Sid: "TrustActors",
Effect: "Allow",
Principal: map[string][]string{"AWS": cfg.TrustedRoleARNs},
Action: "sts:AssumeRole",
}},
}
// 2. Determine Permissions
var action, resource string
switch cfg.Type {
case TargetTypeDatabase:
if cfg.Database == nil {
return nil, fmt.Errorf("database config missing")
}
action = "rds-db:connect"
resource = fmt.Sprintf("arn:aws:rds-db:%s:%s:dbuser:%s/%s",
cfg.Database.Region, cfg.AccountID, cfg.Database.DBClusterID, cfg.Database.DBUser)
case TargetTypeGeneric:
if cfg.Generic == nil {
return nil, fmt.Errorf("generic config missing")
}
action = cfg.Generic.Action
resource = cfg.Generic.ResourceARN
}
return buildACKRole(
cfg.Name, cfg.Namespace, cfg.RoleName,
trustPolicy,
action, []string{resource},
"ResourcePermissions",
), nil
}
// --- INTERNAL HELPERS ---
func buildACKRole(k8sName, ns, awsRoleName string, trust IAMPolicy, action string, resources []string, policyName string) *ackv1alpha1.Role {
trustJSON, _ := json.Marshal(trust)
permPolicy := IAMPolicy{
Version: "2012-10-17",
Statement: []IAMStatement{{
Sid: "AllowAction",
Effect: "Allow",
Action: action,
Resource: resources,
}},
}
permJSON, _ := json.Marshal(permPolicy)
return &ackv1alpha1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: k8sName,
Namespace: ns,
},
Spec: ackv1alpha1.RoleSpec{
Name: &awsRoleName,
AssumeRolePolicyDocument: string(trustJSON),
InlinePolicies: map[string]*string{policyName: awsString(string(permJSON))},
},
}
}
func generatePodIdentityTrust(cfg *PodIdentityConfig) (IAMPolicy, error) {
if cfg == nil {
return IAMPolicy{}, fmt.Errorf("PodIdentity config missing")
}
return IAMPolicy{
Version: "2012-10-17",
Statement: []IAMStatement{{
Sid: "TrustPIA",
Effect: "Allow",
Principal: map[string]string{"Service": cfg.TrustPrincipal},
Action: []string{"sts:AssumeRole", "sts:TagSession"},
}},
}, nil
}
func generateIRSATrust(accountID string, cfg *IRSAConfig) (IAMPolicy, error) {
if cfg == nil {
return IAMPolicy{}, fmt.Errorf("IRSA config missing")
}
aud := cfg.OIDCAudience
if aud == "" {
aud = "sts.amazonaws.com"
}
sub := fmt.Sprintf("system:serviceaccount:%s:%s", cfg.ServiceNamespace, cfg.ServiceAccount)
return buildOIDCTrust(accountID, cfg.OIDCProvider, sub, aud), nil
}
func generateSPIFFETrust(accountID string, cfg *SPIFFEConfig) (IAMPolicy, error) {
if cfg == nil {
return IAMPolicy{}, fmt.Errorf("SPIFFE config missing")
}
aud := cfg.OIDCAudience
if aud == "" {
aud = "sts.amazonaws.com"
}
return buildOIDCTrust(accountID, cfg.OIDCProvider, cfg.SPIFFEID, aud), nil
}
func buildOIDCTrust(accountID, provider, sub, aud string) IAMPolicy {
oidcARN := fmt.Sprintf("arn:aws:iam::%s:oidc-provider/%s", accountID, provider)
return IAMPolicy{
Version: "2012-10-17",
Statement: []IAMStatement{{
Sid: "TrustOIDC",
Effect: "Allow",
Principal: map[string]string{"Federated": oidcARN},
Action: "sts:AssumeRoleWithWebIdentity",
Condition: map[string]interface{}{
"StringEquals": map[string]string{
fmt.Sprintf("%s:sub", provider): sub,
fmt.Sprintf("%s:aud", provider): aud,
},
},
}},
}
}
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 }
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