diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index e7ae6ea..e0eb623 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,8 +1,8 @@ ack_generate_info: - build_date: "2026-05-19T18:43:12Z" - build_hash: e581e886276bd781409a3fb11fa665ea31de17d4 + build_date: "2026-05-27T23:46:24Z" + build_hash: 30bdd708ddc94889d99d4d34ac5217ada38f9fc7 go_version: go1.26.3 - version: v0.59.1 + version: v0.59.1-3-g30bdd70 api_directory_checksum: fcb205ac280ed1b0f107a291e5ea43d93c0991e9 api_version: v1alpha1 aws_sdk_go_version: v1.32.6 diff --git a/go.mod b/go.mod index bc12f40..d42471f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/aws-controllers-k8s/iam-controller go 1.25.0 require ( - github.com/aws-controllers-k8s/runtime v0.59.1 + github.com/aws-controllers-k8s/runtime v0.59.2-0.20260527214203-0e3ba692e1c5 github.com/aws/aws-sdk-go v1.49.0 github.com/aws/aws-sdk-go-v2 v1.34.0 github.com/aws/aws-sdk-go-v2/service/iam v1.38.8 diff --git a/go.sum b/go.sum index 8ec5916..3fb1f07 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/aws-controllers-k8s/runtime v0.59.1 h1:7UDKl9/dif8oNjxx/5Z5ciUfuyn86MS4BvoG9LqF6h4= -github.com/aws-controllers-k8s/runtime v0.59.1/go.mod h1:ljWD1IdtVx/qC7C4lVobF4vLNhno/xX5A78BOke1Ksk= +github.com/aws-controllers-k8s/runtime v0.59.2-0.20260527214203-0e3ba692e1c5 h1:TuqQF4BHtWFvBeShoTlCLSdkqSbBNM1yRn4EYCCj48Y= +github.com/aws-controllers-k8s/runtime v0.59.2-0.20260527214203-0e3ba692e1c5/go.mod h1:ljWD1IdtVx/qC7C4lVobF4vLNhno/xX5A78BOke1Ksk= github.com/aws/aws-sdk-go v1.49.0 h1:g9BkW1fo9GqKfwg2+zCD+TW/D36Ux+vtfJ8guF4AYmY= github.com/aws/aws-sdk-go v1.49.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v1.34.0 h1:9iyL+cjifckRGEVpRKZP3eIxVlL06Qk1Tk13vreaVQU= diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 75c98ec..67b6ec7 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -99,6 +99,7 @@ spec: - "$(FEATURE_GATES)" {{- end }} - --enable-carm={{ .Values.enableCARM }} + - --enable-cross-namespace={{ .Values.enableCrossNamespace }} image: {{ .Values.image.repository }}:{{ .Values.image.tag }} imagePullPolicy: {{ .Values.image.pullPolicy }} name: controller diff --git a/helm/values.schema.json b/helm/values.schema.json index 619cfe3..5cc01db 100644 --- a/helm/values.schema.json +++ b/helm/values.schema.json @@ -274,6 +274,11 @@ "description": "Parameter to enable or disable cross account resource management.", "type": "boolean", "default": true + }, + "enableCrossNamespace": { + "description": "Enable cross-namespace behavior (resource references, secret references, field exports). When false, the controller rejects any operation that crosses namespace boundaries.", + "type": "boolean", + "default": true }, "serviceAccount": { "description": "ServiceAccount settings", diff --git a/helm/values.yaml b/helm/values.yaml index 8a9be29..43eb143 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -181,6 +181,11 @@ leaderElection: # Enable Cross Account Resource Management (default = true). Set this to false to disable cross account resource management. enableCARM: true +# Enable cross-namespace behavior including resource references, secret references, +# and field exports (default = true). When false, the controller rejects any operation +# that crosses namespace boundaries. +enableCrossNamespace: true + # Configuration for feature gates. These are optional controller features that # can be individually enabled ("true") or disabled ("false") by adding key/value # pairs below. diff --git a/pkg/resource/group/references.go b/pkg/resource/group/references.go index 595dea8..9f0455c 100644 --- a/pkg/resource/group/references.go +++ b/pkg/resource/group/references.go @@ -25,11 +25,18 @@ import ( ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" svcapitypes "github.com/aws-controllers-k8s/iam-controller/apis/v1alpha1" ) +// Hack to avoid import errors during build... +var ( + _ = &ackrtlog.ResourceLogger{} +) + // ClearResolvedReferences removes any reference values that were made // concrete in the spec. It returns a copy of the input AWSResource which // contains the original *Ref values, but none of their respective concrete @@ -95,9 +102,29 @@ func (rm *resourceManager) resolveReferenceForPolicies( if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: PolicyRefs") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, isCrossNs, err := ackrt.ValidateCrossNamespaceReference( + rm.cfg.EnableCrossNamespace, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err + } + if isCrossNs { + ackrtlog.FromContext(ctx).Info("cross-namespace resource reference detected; "+ + "this behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace to preserve this behavior.", + "ownerNamespace", ko.ObjectMeta.GetNamespace(), + "targetNamespace", *arr.Namespace, + "referenceName", *arr.Name, + ) + crossNsMsg := fmt.Sprintf("Cross-namespace resource reference detected: "+ + "resource in namespace %q references %q in namespace %q. "+ + "Cross-namespace behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace=true to preserve this behavior.", + ko.ObjectMeta.GetNamespace(), *arr.Name, *arr.Namespace) + setCrossNamespaceCondition(ko, crossNsMsg) } obj := &svcapitypes.Policy{} if err := getReferencedResourceState_Policy(ctx, apiReader, obj, *arr.Name, namespace); err != nil { diff --git a/pkg/resource/group/sdk.go b/pkg/resource/group/sdk.go index 6ea2341..734271f 100644 --- a/pkg/resource/group/sdk.go +++ b/pkg/resource/group/sdk.go @@ -27,6 +27,7 @@ import ( ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition" ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" "github.com/aws/aws-sdk-go-v2/aws" svcsdk "github.com/aws/aws-sdk-go-v2/service/iam" @@ -50,6 +51,7 @@ var ( _ = fmt.Sprintf("") _ = &ackrequeue.NoRequeue{} _ = &aws.Config{} + _ = ackrt.ValidateCrossNamespaceReferenceString ) // sdkFind returns SDK-specific information about a supplied resource @@ -476,3 +478,29 @@ func (rm *resourceManager) terminalAWSError(err error) bool { return false } } + +// setCrossNamespaceCondition sets the ACK.CrossNamespaceOptInRequired condition +// on the resource to notify users that their cross-namespace usage will require +// explicit opt-in in a future release. +func setCrossNamespaceCondition(ko *svcapitypes.Group, message string) { + if ko.Status.Conditions == nil { + ko.Status.Conditions = []*ackv1alpha1.Condition{} + } + var crossNsCond *ackv1alpha1.Condition + for _, c := range ko.Status.Conditions { + if c.Type == ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired { + crossNsCond = c + break + } + } + if crossNsCond == nil { + crossNsCond = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired, + } + ko.Status.Conditions = append(ko.Status.Conditions, crossNsCond) + } + now := metav1.Now() + crossNsCond.LastTransitionTime = &now + crossNsCond.Status = corev1.ConditionTrue + crossNsCond.Message = &message +} diff --git a/pkg/resource/instance_profile/references.go b/pkg/resource/instance_profile/references.go index 8ab8a11..38acf32 100644 --- a/pkg/resource/instance_profile/references.go +++ b/pkg/resource/instance_profile/references.go @@ -25,11 +25,18 @@ import ( ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" svcapitypes "github.com/aws-controllers-k8s/iam-controller/apis/v1alpha1" ) +// Hack to avoid import errors during build... +var ( + _ = &ackrtlog.ResourceLogger{} +) + // ClearResolvedReferences removes any reference values that were made // concrete in the spec. It returns a copy of the input AWSResource which // contains the original *Ref values, but none of their respective concrete @@ -94,9 +101,29 @@ func (rm *resourceManager) resolveReferenceForRole( if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: RoleRef") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, isCrossNs, err := ackrt.ValidateCrossNamespaceReference( + rm.cfg.EnableCrossNamespace, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err + } + if isCrossNs { + ackrtlog.FromContext(ctx).Info("cross-namespace resource reference detected; "+ + "this behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace to preserve this behavior.", + "ownerNamespace", ko.ObjectMeta.GetNamespace(), + "targetNamespace", *arr.Namespace, + "referenceName", *arr.Name, + ) + crossNsMsg := fmt.Sprintf("Cross-namespace resource reference detected: "+ + "resource in namespace %q references %q in namespace %q. "+ + "Cross-namespace behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace=true to preserve this behavior.", + ko.ObjectMeta.GetNamespace(), *arr.Name, *arr.Namespace) + setCrossNamespaceCondition(ko, crossNsMsg) } obj := &svcapitypes.Role{} if err := getReferencedResourceState_Role(ctx, apiReader, obj, *arr.Name, namespace); err != nil { diff --git a/pkg/resource/instance_profile/sdk.go b/pkg/resource/instance_profile/sdk.go index bbd696d..77f1c6f 100644 --- a/pkg/resource/instance_profile/sdk.go +++ b/pkg/resource/instance_profile/sdk.go @@ -27,6 +27,7 @@ import ( ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition" ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" "github.com/aws/aws-sdk-go-v2/aws" svcsdk "github.com/aws/aws-sdk-go-v2/service/iam" @@ -51,6 +52,7 @@ var ( _ = fmt.Sprintf("") _ = &ackrequeue.NoRequeue{} _ = &aws.Config{} + _ = ackrt.ValidateCrossNamespaceReferenceString ) // sdkFind returns SDK-specific information about a supplied resource @@ -454,3 +456,29 @@ func (rm *resourceManager) terminalAWSError(err error) bool { return false } } + +// setCrossNamespaceCondition sets the ACK.CrossNamespaceOptInRequired condition +// on the resource to notify users that their cross-namespace usage will require +// explicit opt-in in a future release. +func setCrossNamespaceCondition(ko *svcapitypes.InstanceProfile, message string) { + if ko.Status.Conditions == nil { + ko.Status.Conditions = []*ackv1alpha1.Condition{} + } + var crossNsCond *ackv1alpha1.Condition + for _, c := range ko.Status.Conditions { + if c.Type == ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired { + crossNsCond = c + break + } + } + if crossNsCond == nil { + crossNsCond = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired, + } + ko.Status.Conditions = append(ko.Status.Conditions, crossNsCond) + } + now := metav1.Now() + crossNsCond.LastTransitionTime = &now + crossNsCond.Status = corev1.ConditionTrue + crossNsCond.Message = &message +} diff --git a/pkg/resource/open_id_connect_provider/sdk.go b/pkg/resource/open_id_connect_provider/sdk.go index c6f0ea0..dce4a0d 100644 --- a/pkg/resource/open_id_connect_provider/sdk.go +++ b/pkg/resource/open_id_connect_provider/sdk.go @@ -27,6 +27,7 @@ import ( ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition" ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" "github.com/aws/aws-sdk-go-v2/aws" svcsdk "github.com/aws/aws-sdk-go-v2/service/iam" @@ -51,6 +52,7 @@ var ( _ = fmt.Sprintf("") _ = &ackrequeue.NoRequeue{} _ = &aws.Config{} + _ = ackrt.ValidateCrossNamespaceReferenceString ) // sdkFind returns SDK-specific information about a supplied resource @@ -410,3 +412,29 @@ func (rm *resourceManager) terminalAWSError(err error) bool { return false } } + +// setCrossNamespaceCondition sets the ACK.CrossNamespaceOptInRequired condition +// on the resource to notify users that their cross-namespace usage will require +// explicit opt-in in a future release. +func setCrossNamespaceCondition(ko *svcapitypes.OpenIDConnectProvider, message string) { + if ko.Status.Conditions == nil { + ko.Status.Conditions = []*ackv1alpha1.Condition{} + } + var crossNsCond *ackv1alpha1.Condition + for _, c := range ko.Status.Conditions { + if c.Type == ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired { + crossNsCond = c + break + } + } + if crossNsCond == nil { + crossNsCond = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired, + } + ko.Status.Conditions = append(ko.Status.Conditions, crossNsCond) + } + now := metav1.Now() + crossNsCond.LastTransitionTime = &now + crossNsCond.Status = corev1.ConditionTrue + crossNsCond.Message = &message +} diff --git a/pkg/resource/policy/sdk.go b/pkg/resource/policy/sdk.go index c51af68..ed542eb 100644 --- a/pkg/resource/policy/sdk.go +++ b/pkg/resource/policy/sdk.go @@ -27,6 +27,7 @@ import ( ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition" ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" "github.com/aws/aws-sdk-go-v2/aws" svcsdk "github.com/aws/aws-sdk-go-v2/service/iam" @@ -51,6 +52,7 @@ var ( _ = fmt.Sprintf("") _ = &ackrequeue.NoRequeue{} _ = &aws.Config{} + _ = ackrt.ValidateCrossNamespaceReferenceString ) // sdkFind returns SDK-specific information about a supplied resource @@ -512,3 +514,29 @@ func (rm *resourceManager) terminalAWSError(err error) bool { return false } } + +// setCrossNamespaceCondition sets the ACK.CrossNamespaceOptInRequired condition +// on the resource to notify users that their cross-namespace usage will require +// explicit opt-in in a future release. +func setCrossNamespaceCondition(ko *svcapitypes.Policy, message string) { + if ko.Status.Conditions == nil { + ko.Status.Conditions = []*ackv1alpha1.Condition{} + } + var crossNsCond *ackv1alpha1.Condition + for _, c := range ko.Status.Conditions { + if c.Type == ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired { + crossNsCond = c + break + } + } + if crossNsCond == nil { + crossNsCond = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired, + } + ko.Status.Conditions = append(ko.Status.Conditions, crossNsCond) + } + now := metav1.Now() + crossNsCond.LastTransitionTime = &now + crossNsCond.Status = corev1.ConditionTrue + crossNsCond.Message = &message +} diff --git a/pkg/resource/role/references.go b/pkg/resource/role/references.go index 94c4f6f..b060f78 100644 --- a/pkg/resource/role/references.go +++ b/pkg/resource/role/references.go @@ -25,11 +25,18 @@ import ( ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" svcapitypes "github.com/aws-controllers-k8s/iam-controller/apis/v1alpha1" ) +// Hack to avoid import errors during build... +var ( + _ = &ackrtlog.ResourceLogger{} +) + // ClearResolvedReferences removes any reference values that were made // concrete in the spec. It returns a copy of the input AWSResource which // contains the original *Ref values, but none of their respective concrete @@ -108,9 +115,29 @@ func (rm *resourceManager) resolveReferenceForPermissionsBoundary( if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: PermissionsBoundaryRef") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, isCrossNs, err := ackrt.ValidateCrossNamespaceReference( + rm.cfg.EnableCrossNamespace, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err + } + if isCrossNs { + ackrtlog.FromContext(ctx).Info("cross-namespace resource reference detected; "+ + "this behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace to preserve this behavior.", + "ownerNamespace", ko.ObjectMeta.GetNamespace(), + "targetNamespace", *arr.Namespace, + "referenceName", *arr.Name, + ) + crossNsMsg := fmt.Sprintf("Cross-namespace resource reference detected: "+ + "resource in namespace %q references %q in namespace %q. "+ + "Cross-namespace behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace=true to preserve this behavior.", + ko.ObjectMeta.GetNamespace(), *arr.Name, *arr.Namespace) + setCrossNamespaceCondition(ko, crossNsMsg) } obj := &svcapitypes.Policy{} if err := getReferencedResourceState_Policy(ctx, apiReader, obj, *arr.Name, namespace); err != nil { @@ -192,9 +219,29 @@ func (rm *resourceManager) resolveReferenceForPolicies( if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: PolicyRefs") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, isCrossNs, err := ackrt.ValidateCrossNamespaceReference( + rm.cfg.EnableCrossNamespace, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err + } + if isCrossNs { + ackrtlog.FromContext(ctx).Info("cross-namespace resource reference detected; "+ + "this behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace to preserve this behavior.", + "ownerNamespace", ko.ObjectMeta.GetNamespace(), + "targetNamespace", *arr.Namespace, + "referenceName", *arr.Name, + ) + crossNsMsg := fmt.Sprintf("Cross-namespace resource reference detected: "+ + "resource in namespace %q references %q in namespace %q. "+ + "Cross-namespace behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace=true to preserve this behavior.", + ko.ObjectMeta.GetNamespace(), *arr.Name, *arr.Namespace) + setCrossNamespaceCondition(ko, crossNsMsg) } obj := &svcapitypes.Policy{} if err := getReferencedResourceState_Policy(ctx, apiReader, obj, *arr.Name, namespace); err != nil { diff --git a/pkg/resource/role/sdk.go b/pkg/resource/role/sdk.go index f5f3cd7..bb817e8 100644 --- a/pkg/resource/role/sdk.go +++ b/pkg/resource/role/sdk.go @@ -28,6 +28,7 @@ import ( ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition" ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" "github.com/aws/aws-sdk-go-v2/aws" svcsdk "github.com/aws/aws-sdk-go-v2/service/iam" @@ -52,6 +53,7 @@ var ( _ = fmt.Sprintf("") _ = &ackrequeue.NoRequeue{} _ = &aws.Config{} + _ = ackrt.ValidateCrossNamespaceReferenceString ) // sdkFind returns SDK-specific information about a supplied resource @@ -636,3 +638,29 @@ func (rm *resourceManager) terminalAWSError(err error) bool { return false } } + +// setCrossNamespaceCondition sets the ACK.CrossNamespaceOptInRequired condition +// on the resource to notify users that their cross-namespace usage will require +// explicit opt-in in a future release. +func setCrossNamespaceCondition(ko *svcapitypes.Role, message string) { + if ko.Status.Conditions == nil { + ko.Status.Conditions = []*ackv1alpha1.Condition{} + } + var crossNsCond *ackv1alpha1.Condition + for _, c := range ko.Status.Conditions { + if c.Type == ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired { + crossNsCond = c + break + } + } + if crossNsCond == nil { + crossNsCond = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired, + } + ko.Status.Conditions = append(ko.Status.Conditions, crossNsCond) + } + now := metav1.Now() + crossNsCond.LastTransitionTime = &now + crossNsCond.Status = corev1.ConditionTrue + crossNsCond.Message = &message +} diff --git a/pkg/resource/service_linked_role/sdk.go b/pkg/resource/service_linked_role/sdk.go index 9ec1031..9b94949 100644 --- a/pkg/resource/service_linked_role/sdk.go +++ b/pkg/resource/service_linked_role/sdk.go @@ -27,6 +27,7 @@ import ( ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition" ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" "github.com/aws/aws-sdk-go-v2/aws" svcsdk "github.com/aws/aws-sdk-go-v2/service/iam" @@ -50,6 +51,7 @@ var ( _ = fmt.Sprintf("") _ = &ackrequeue.NoRequeue{} _ = &aws.Config{} + _ = ackrt.ValidateCrossNamespaceReferenceString ) // sdkFind returns SDK-specific information about a supplied resource @@ -360,3 +362,29 @@ func (rm *resourceManager) terminalAWSError(err error) bool { return false } } + +// setCrossNamespaceCondition sets the ACK.CrossNamespaceOptInRequired condition +// on the resource to notify users that their cross-namespace usage will require +// explicit opt-in in a future release. +func setCrossNamespaceCondition(ko *svcapitypes.ServiceLinkedRole, message string) { + if ko.Status.Conditions == nil { + ko.Status.Conditions = []*ackv1alpha1.Condition{} + } + var crossNsCond *ackv1alpha1.Condition + for _, c := range ko.Status.Conditions { + if c.Type == ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired { + crossNsCond = c + break + } + } + if crossNsCond == nil { + crossNsCond = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired, + } + ko.Status.Conditions = append(ko.Status.Conditions, crossNsCond) + } + now := metav1.Now() + crossNsCond.LastTransitionTime = &now + crossNsCond.Status = corev1.ConditionTrue + crossNsCond.Message = &message +} diff --git a/pkg/resource/user/references.go b/pkg/resource/user/references.go index b07e706..7df9e49 100644 --- a/pkg/resource/user/references.go +++ b/pkg/resource/user/references.go @@ -25,11 +25,18 @@ import ( ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" svcapitypes "github.com/aws-controllers-k8s/iam-controller/apis/v1alpha1" ) +// Hack to avoid import errors during build... +var ( + _ = &ackrtlog.ResourceLogger{} +) + // ClearResolvedReferences removes any reference values that were made // concrete in the spec. It returns a copy of the input AWSResource which // contains the original *Ref values, but none of their respective concrete @@ -108,9 +115,29 @@ func (rm *resourceManager) resolveReferenceForPermissionsBoundary( if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: PermissionsBoundaryRef") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, isCrossNs, err := ackrt.ValidateCrossNamespaceReference( + rm.cfg.EnableCrossNamespace, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err + } + if isCrossNs { + ackrtlog.FromContext(ctx).Info("cross-namespace resource reference detected; "+ + "this behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace to preserve this behavior.", + "ownerNamespace", ko.ObjectMeta.GetNamespace(), + "targetNamespace", *arr.Namespace, + "referenceName", *arr.Name, + ) + crossNsMsg := fmt.Sprintf("Cross-namespace resource reference detected: "+ + "resource in namespace %q references %q in namespace %q. "+ + "Cross-namespace behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace=true to preserve this behavior.", + ko.ObjectMeta.GetNamespace(), *arr.Name, *arr.Namespace) + setCrossNamespaceCondition(ko, crossNsMsg) } obj := &svcapitypes.Policy{} if err := getReferencedResourceState_Policy(ctx, apiReader, obj, *arr.Name, namespace); err != nil { @@ -192,9 +219,29 @@ func (rm *resourceManager) resolveReferenceForPolicies( if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: PolicyRefs") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, isCrossNs, err := ackrt.ValidateCrossNamespaceReference( + rm.cfg.EnableCrossNamespace, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err + } + if isCrossNs { + ackrtlog.FromContext(ctx).Info("cross-namespace resource reference detected; "+ + "this behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace to preserve this behavior.", + "ownerNamespace", ko.ObjectMeta.GetNamespace(), + "targetNamespace", *arr.Namespace, + "referenceName", *arr.Name, + ) + crossNsMsg := fmt.Sprintf("Cross-namespace resource reference detected: "+ + "resource in namespace %q references %q in namespace %q. "+ + "Cross-namespace behavior will be disabled by default in a future release. "+ + "Set --enable-cross-namespace=true to preserve this behavior.", + ko.ObjectMeta.GetNamespace(), *arr.Name, *arr.Namespace) + setCrossNamespaceCondition(ko, crossNsMsg) } obj := &svcapitypes.Policy{} if err := getReferencedResourceState_Policy(ctx, apiReader, obj, *arr.Name, namespace); err != nil { diff --git a/pkg/resource/user/sdk.go b/pkg/resource/user/sdk.go index 90f4de4..704f49d 100644 --- a/pkg/resource/user/sdk.go +++ b/pkg/resource/user/sdk.go @@ -27,6 +27,7 @@ import ( ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition" ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors" ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" + ackrt "github.com/aws-controllers-k8s/runtime/pkg/runtime" ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" "github.com/aws/aws-sdk-go-v2/aws" svcsdk "github.com/aws/aws-sdk-go-v2/service/iam" @@ -51,6 +52,7 @@ var ( _ = fmt.Sprintf("") _ = &ackrequeue.NoRequeue{} _ = &aws.Config{} + _ = ackrt.ValidateCrossNamespaceReferenceString ) // sdkFind returns SDK-specific information about a supplied resource @@ -562,3 +564,29 @@ func (rm *resourceManager) terminalAWSError(err error) bool { return false } } + +// setCrossNamespaceCondition sets the ACK.CrossNamespaceOptInRequired condition +// on the resource to notify users that their cross-namespace usage will require +// explicit opt-in in a future release. +func setCrossNamespaceCondition(ko *svcapitypes.User, message string) { + if ko.Status.Conditions == nil { + ko.Status.Conditions = []*ackv1alpha1.Condition{} + } + var crossNsCond *ackv1alpha1.Condition + for _, c := range ko.Status.Conditions { + if c.Type == ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired { + crossNsCond = c + break + } + } + if crossNsCond == nil { + crossNsCond = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired, + } + ko.Status.Conditions = append(ko.Status.Conditions, crossNsCond) + } + now := metav1.Now() + crossNsCond.LastTransitionTime = &now + crossNsCond.Status = corev1.ConditionTrue + crossNsCond.Message = &message +} diff --git a/test/e2e/tests/test_cross_namespace_refs.py b/test/e2e/tests/test_cross_namespace_refs.py new file mode 100644 index 0000000..1a354ad --- /dev/null +++ b/test/e2e/tests/test_cross_namespace_refs.py @@ -0,0 +1,501 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may +# not use this file except in compliance with the License. A copy of the +# License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +"""Integration tests for cross-namespace resource reference behavior. + +These tests validate the --enable-cross-namespace flag behavior with an +IAM Role whose policyRefs[].from.namespace points to another namespace. + +Scenario 1 (flag=true, Phase 1 default): + - Create an IAM Policy in one namespace + - Create an IAM Role in a different namespace with + policyRefs[].from.namespace pointing to the policy's namespace + - Assert the Role reconciles successfully (policy ARN attached) + - Assert ACK.CrossNamespaceOptInRequired condition is present on the Role + +Scenario 2 (flag=false): + - Redeploy the controller with --enable-cross-namespace=false + - Create the same Role/Policy pair across namespaces + - Assert the Role enters a terminal state with ACK.Terminal=True + - Assert the condition message contains "enable-cross-namespace" + +Scenario 3 (same-namespace): + - Create Policy and Role in the same namespace + - Assert the Role reconciles successfully regardless of flag value + - Assert NO ACK.CrossNamespaceOptInRequired condition + +""" + +import os +import time + +import pytest + +from acktest.k8s import condition +from acktest.k8s import resource as k8s +from acktest.resources import random_suffix_name +from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_resource +from e2e.common.types import POLICY_RESOURCE_PLURAL, ROLE_RESOURCE_PLURAL +from e2e.replacement_values import REPLACEMENT_VALUES +from e2e import role +from e2e import policy + +DELETE_ROLE_TIMEOUT_SECONDS = 10 +DELETE_POLICY_TIMEOUT_SECONDS = 30 +WAIT_AFTER_CREATE_SECONDS = 10 +TERMINAL_CONDITION_WAIT_PERIODS = 10 +TERMINAL_CONDITION_PERIOD_LENGTH = 15 +DEPRECATION_CONDITION_WAIT_PERIODS = 10 +DEPRECATION_CONDITION_PERIOD_LENGTH = 15 + + +@service_marker +class TestCrossNamespaceRefs: + """Tests for cross-namespace resource reference behavior. + + These tests use two separate namespaces: one containing a Policy and + another containing a Role that references the Policy across namespaces. + + The controller must be deployed with the appropriate --enable-cross-namespace + flag value for each scenario. Scenario 1 uses the Phase 1 default (true), + Scenario 2 requires redeployment with --enable-cross-namespace=false. + """ + + @pytest.mark.skipif( + os.environ.get("ENABLE_CROSS_NAMESPACE", "true").lower() == "false", + reason="requires controller deployed with --enable-cross-namespace=true (Phase 1 default); " + "skipped when ENABLE_CROSS_NAMESPACE=false", + ) + def test_cross_namespace_ref_allowed_with_deprecation_warning(self): + """Scenario 1: When --enable-cross-namespace is true (Phase 1 default), + a Role referencing a Policy in a different namespace should reconcile + successfully AND emit an ACK.CrossNamespaceOptInRequired condition. + + This test requires the controller to be deployed with + --enable-cross-namespace=true (the Phase 1 default). It validates + that cross-namespace references work but produce a deprecation warning. + + """ + policy_ns = random_suffix_name("policy-ns", 24) + role_ns = random_suffix_name("role-ns", 24) + policy_name = random_suffix_name("xns-policy", 24) + role_name = random_suffix_name("xns-role", 24) + + policy_ref = None + role_ref = None + + try: + # Create the two namespaces + k8s.create_k8s_namespace(policy_ns) + k8s.create_k8s_namespace(role_ns) + time.sleep(WAIT_AFTER_CREATE_SECONDS) + + # Create the Policy in the policy namespace. + # NOTE: POLICY_NAMESPACE must be set BEFORE POLICY_NAME because + # the placeholder replacement is a naive string replace and + # $POLICY_NAME would otherwise be substituted as a prefix of + # $POLICY_NAMESPACE. + replacements = REPLACEMENT_VALUES.copy() + replacements["POLICY_NAMESPACE"] = policy_ns + replacements["POLICY_NAME"] = policy_name + replacements["POLICY_DESCRIPTION"] = "cross-namespace test policy" + + policy_data = load_resource( + "policy_simple_namespace", + additional_replacements=replacements, + ) + + policy_ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, POLICY_RESOURCE_PLURAL, + policy_name, namespace=policy_ns, + ) + k8s.create_custom_resource(policy_ref, policy_data) + cr = k8s.wait_resource_consumed_by_controller(policy_ref) + assert cr is not None + assert k8s.get_resource_exists(policy_ref) + + # Wait for the Policy to be created in IAM + cr = k8s.get_resource(policy_ref) + assert "status" in cr + assert "ackResourceMetadata" in cr["status"] + assert "arn" in cr["status"]["ackResourceMetadata"] + policy_arn = cr["status"]["ackResourceMetadata"]["arn"] + policy.wait_until_exists(policy_arn) + + # Create the Role in the role namespace referencing the + # Policy in the policy namespace + replacements = REPLACEMENT_VALUES.copy() + replacements["POLICY_NAMESPACE"] = policy_ns + replacements["POLICY_NAME"] = policy_name + replacements["ROLE_NAME"] = role_name + + role_data = load_resource( + "role_referring_namespace", + additional_replacements=replacements, + ) + + role_ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, ROLE_RESOURCE_PLURAL, + role_name, namespace=role_ns, + ) + k8s.create_custom_resource(role_ref, role_data) + cr = k8s.wait_resource_consumed_by_controller(role_ref) + assert cr is not None + assert k8s.get_resource_exists(role_ref) + + # Wait for the Role to sync successfully + time.sleep(WAIT_AFTER_CREATE_SECONDS) + condition.assert_synced(role_ref) + + # Verify the Role was created in IAM + role.wait_until_exists(role_name) + + # Verify the policy from the namespace containing the Policy is attached + attached_arns = role.get_attached_policy_arns(role_name) + assert attached_arns is not None, ( + "Role should have attached policies" + ) + assert policy_arn in attached_arns, ( + f"Policy ARN {policy_arn} from policy namespace should be " + f"attached to the role, but found: {attached_arns}" + ) + + # Verify the ACK.CrossNamespaceOptInRequired condition is present + assert k8s.wait_on_condition( + role_ref, + "ACK.CrossNamespaceOptInRequired", + "True", + wait_periods=DEPRECATION_CONDITION_WAIT_PERIODS, + period_length=DEPRECATION_CONDITION_PERIOD_LENGTH, + ), ( + "Expected ACK.CrossNamespaceOptInRequired condition to be True " + "for cross-namespace ref when flag is enabled" + ) + + deprecation_condition = k8s.get_resource_condition( + role_ref, "ACK.CrossNamespaceOptInRequired", + ) + assert deprecation_condition is not None + assert deprecation_condition["status"] == "True" + # Verify the deprecation message mentions the flag name + assert "enable-cross-namespace" in deprecation_condition.get( + "message", "" + ), ( + "Deprecation condition message should reference the " + "--enable-cross-namespace flag" + ) + + finally: + # Clean up: delete Role first to avoid cascading delete issues + if role_ref is not None and k8s.get_resource_exists(role_ref): + _, deleted = k8s.delete_custom_resource( + role_ref, + period_length=DELETE_ROLE_TIMEOUT_SECONDS, + ) + assert deleted + role.wait_until_deleted(role_name) + + if policy_ref is not None and k8s.get_resource_exists(policy_ref): + _, deleted = k8s.delete_custom_resource( + policy_ref, + period_length=DELETE_POLICY_TIMEOUT_SECONDS, + ) + assert deleted + policy.wait_until_deleted(policy_arn) + + # Clean up namespaces + try: + k8s.delete_k8s_namespace(role_ns) + except Exception: + pass + try: + k8s.delete_k8s_namespace(policy_ns) + except Exception: + pass + + @pytest.mark.skipif( + os.environ.get("ENABLE_CROSS_NAMESPACE", "true").lower() != "false", + reason="requires controller deployed with --enable-cross-namespace=false; " + "set ENABLE_CROSS_NAMESPACE=false to run", + ) + def test_cross_namespace_ref_rejected_when_flag_disabled(self): + """Scenario 2: When --enable-cross-namespace is set to false, + a Role referencing a Policy in a different namespace should enter + a terminal state. + + This test requires the controller to be redeployed with + --enable-cross-namespace=false (e.g., via Helm: + --set enableCrossNamespace=false). + + """ + policy_ns = random_suffix_name("policy-ns", 24) + role_ns = random_suffix_name("role-ns", 24) + policy_name = random_suffix_name("xns-policy", 24) + role_name = random_suffix_name("xns-role", 24) + + policy_ref = None + role_ref = None + + try: + # Create the two namespaces + k8s.create_k8s_namespace(policy_ns) + k8s.create_k8s_namespace(role_ns) + time.sleep(WAIT_AFTER_CREATE_SECONDS) + + # Create the Policy in the policy namespace. + # NOTE: POLICY_NAMESPACE must be set BEFORE POLICY_NAME because + # the placeholder replacement is a naive string replace and + # $POLICY_NAME would otherwise be substituted as a prefix of + # $POLICY_NAMESPACE. + replacements = REPLACEMENT_VALUES.copy() + replacements["POLICY_NAMESPACE"] = policy_ns + replacements["POLICY_NAME"] = policy_name + replacements["POLICY_DESCRIPTION"] = "cross-namespace test policy" + + policy_data = load_resource( + "policy_simple_namespace", + additional_replacements=replacements, + ) + + policy_ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, POLICY_RESOURCE_PLURAL, + policy_name, namespace=policy_ns, + ) + k8s.create_custom_resource(policy_ref, policy_data) + cr = k8s.wait_resource_consumed_by_controller(policy_ref) + assert cr is not None + assert k8s.get_resource_exists(policy_ref) + + # Wait for the Policy to be created in IAM + cr = k8s.get_resource(policy_ref) + assert "status" in cr + assert "ackResourceMetadata" in cr["status"] + assert "arn" in cr["status"]["ackResourceMetadata"] + policy_arn = cr["status"]["ackResourceMetadata"]["arn"] + policy.wait_until_exists(policy_arn) + + # Create the Role in the role namespace referencing the + # Policy in the policy namespace + replacements = REPLACEMENT_VALUES.copy() + replacements["POLICY_NAMESPACE"] = policy_ns + replacements["POLICY_NAME"] = policy_name + replacements["ROLE_NAME"] = role_name + + role_data = load_resource( + "role_referring_namespace", + additional_replacements=replacements, + ) + + role_ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, ROLE_RESOURCE_PLURAL, + role_name, namespace=role_ns, + ) + k8s.create_custom_resource(role_ref, role_data) + cr = k8s.wait_resource_consumed_by_controller(role_ref) + assert cr is not None + assert k8s.get_resource_exists(role_ref) + + # Wait for the controller to process the Role and set the + # terminal condition + assert k8s.wait_on_condition( + role_ref, + "ACK.Terminal", + "True", + wait_periods=TERMINAL_CONDITION_WAIT_PERIODS, + period_length=TERMINAL_CONDITION_PERIOD_LENGTH, + ), "Expected ACK.Terminal condition to be True for cross-namespace ref" + + # Verify the terminal condition message contains the flag name + terminal_condition = k8s.get_resource_condition( + role_ref, "ACK.Terminal", + ) + assert terminal_condition is not None + assert terminal_condition["status"] == "True" + assert "enable-cross-namespace" in terminal_condition.get( + "message", "" + ), ( + "Terminal condition message should reference the " + "--enable-cross-namespace flag" + ) + + # Verify the Role was NOT created in IAM + assert role.get(role_name) is None, ( + "Role should not exist in IAM when cross-namespace ref is rejected" + ) + + finally: + # Clean up resources in reverse order + if role_ref is not None and k8s.get_resource_exists(role_ref): + _, deleted = k8s.delete_custom_resource( + role_ref, + period_length=DELETE_ROLE_TIMEOUT_SECONDS, + ) + assert deleted + + if policy_ref is not None and k8s.get_resource_exists(policy_ref): + _, deleted = k8s.delete_custom_resource( + policy_ref, + period_length=DELETE_POLICY_TIMEOUT_SECONDS, + ) + assert deleted + policy.wait_until_deleted(policy_arn) + + # Clean up namespaces + try: + k8s.delete_k8s_namespace(role_ns) + except Exception: + pass + try: + k8s.delete_k8s_namespace(policy_ns) + except Exception: + pass + + def test_same_namespace_ref_always_allowed(self): + """Scenario 3: A Role referencing a Policy in the same namespace + should always reconcile successfully regardless of the + --enable-cross-namespace flag value. + + This test validates that same-namespace references are never + affected by the cross-namespace flag. It should pass with both + --enable-cross-namespace=true and --enable-cross-namespace=false. + + """ + test_ns = random_suffix_name("same-ns", 24) + policy_name = random_suffix_name("sns-policy", 24) + role_name = random_suffix_name("sns-role", 24) + + policy_ref = None + role_ref = None + + try: + # Create the test namespace + k8s.create_k8s_namespace(test_ns) + time.sleep(WAIT_AFTER_CREATE_SECONDS) + + # Create the Policy in the test namespace. + # NOTE: POLICY_NAMESPACE must be set BEFORE POLICY_NAME because + # the placeholder replacement is a naive string replace and + # $POLICY_NAME would otherwise be substituted as a prefix of + # $POLICY_NAMESPACE. + replacements = REPLACEMENT_VALUES.copy() + replacements["POLICY_NAMESPACE"] = test_ns + replacements["POLICY_NAME"] = policy_name + replacements["POLICY_DESCRIPTION"] = "same-namespace test policy" + + policy_data = load_resource( + "policy_simple_namespace", + additional_replacements=replacements, + ) + + policy_ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, POLICY_RESOURCE_PLURAL, + policy_name, namespace=test_ns, + ) + k8s.create_custom_resource(policy_ref, policy_data) + cr = k8s.wait_resource_consumed_by_controller(policy_ref) + assert cr is not None + assert k8s.get_resource_exists(policy_ref) + + # Wait for the Policy to be created in IAM + cr = k8s.get_resource(policy_ref) + assert "status" in cr + assert "ackResourceMetadata" in cr["status"] + assert "arn" in cr["status"]["ackResourceMetadata"] + policy_arn = cr["status"]["ackResourceMetadata"]["arn"] + policy.wait_until_exists(policy_arn) + + # Create the Role in the SAME namespace referencing the Policy + replacements = REPLACEMENT_VALUES.copy() + replacements["POLICY_NAMESPACE"] = test_ns + replacements["POLICY_NAME"] = policy_name + replacements["ROLE_NAME"] = role_name + + role_data = load_resource( + "role_referring_namespace", + additional_replacements=replacements, + ) + + role_ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, ROLE_RESOURCE_PLURAL, + role_name, namespace=test_ns, + ) + k8s.create_custom_resource(role_ref, role_data) + cr = k8s.wait_resource_consumed_by_controller(role_ref) + assert cr is not None + assert k8s.get_resource_exists(role_ref) + + # Wait for the Role to sync successfully + time.sleep(WAIT_AFTER_CREATE_SECONDS) + condition.assert_synced(role_ref) + + # Verify the Role was created in IAM + role.wait_until_exists(role_name) + + # Verify the policy is attached + attached_arns = role.get_attached_policy_arns(role_name) + assert attached_arns is not None, ( + "Role should have attached policies" + ) + assert policy_arn in attached_arns, ( + f"Policy ARN {policy_arn} should be attached to the role, " + f"but found: {attached_arns}" + ) + + # Verify NO ACK.CrossNamespaceOptInRequired condition is present + # (same-namespace refs should never trigger the deprecation warning) + cr = k8s.get_resource(role_ref) + conditions = cr.get("status", {}).get("conditions", []) + deprecation_conditions = [ + c for c in conditions + if c.get("type") == "ACK.CrossNamespaceOptInRequired" + ] + assert len(deprecation_conditions) == 0, ( + "Same-namespace reference should NOT have " + "ACK.CrossNamespaceOptInRequired condition, but found: " + f"{deprecation_conditions}" + ) + + # Also verify no terminal condition (sanity check) + terminal_conditions = [ + c for c in conditions + if c.get("type") == "ACK.Terminal" + and c.get("status") == "True" + ] + assert len(terminal_conditions) == 0, ( + "Same-namespace reference should NOT have ACK.Terminal " + f"condition, but found: {terminal_conditions}" + ) + + finally: + # Clean up: delete Role first to avoid cascading delete issues + if role_ref is not None and k8s.get_resource_exists(role_ref): + _, deleted = k8s.delete_custom_resource( + role_ref, + period_length=DELETE_ROLE_TIMEOUT_SECONDS, + ) + assert deleted + role.wait_until_deleted(role_name) + + if policy_ref is not None and k8s.get_resource_exists(policy_ref): + _, deleted = k8s.delete_custom_resource( + policy_ref, + period_length=DELETE_POLICY_TIMEOUT_SECONDS, + ) + assert deleted + policy.wait_until_deleted(policy_arn) + + # Clean up namespace + try: + k8s.delete_k8s_namespace(test_ns) + except Exception: + pass