diff --git a/go.mod b/go.mod index ddf68a39..c684db7d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( github.com/aws-controllers-k8s/pkg v0.0.23 - github.com/aws-controllers-k8s/runtime v0.59.1 + github.com/aws-controllers-k8s/runtime v0.59.2-0.20260603224255-14be98880374 github.com/aws/aws-sdk-go v1.49.0 github.com/aws/aws-sdk-go-v2 v1.32.7 github.com/dlclark/regexp2 v1.10.0 // indirect diff --git a/go.sum b/go.sum index 904359f4..fe2a7c2f 100644 --- a/go.sum +++ b/go.sum @@ -64,10 +64,8 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws-controllers-k8s/pkg v0.0.23 h1:iqu8jKQUnyP/c6TiVcXySQYpkATui0iXFC5ax9x01oM= github.com/aws-controllers-k8s/pkg v0.0.23/go.mod h1:VvdjLWmR6IJ3KU8KByKiq/lJE8M+ur2piXysXKTGUS0= -github.com/aws-controllers-k8s/runtime v0.59.0 h1:VECkUXw3e8WLQo52o3Mw0u1WOrfAzugv8A5fFoaEhxU= -github.com/aws-controllers-k8s/runtime v0.59.0/go.mod h1:ljWD1IdtVx/qC7C4lVobF4vLNhno/xX5A78BOke1Ksk= -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.20260603224255-14be98880374 h1:qeu0+TU976/aCVWA9rGG2UTbOkvRkA6HfG+4ZnNd2Lc= +github.com/aws-controllers-k8s/runtime v0.59.2-0.20260603224255-14be98880374/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.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw= diff --git a/pkg/generate/code/resource_reference.go b/pkg/generate/code/resource_reference.go index 6bd84ee4..93024238 100644 --- a/pkg/generate/code/resource_reference.go +++ b/pkg/generate/code/resource_reference.go @@ -122,6 +122,12 @@ func ReferenceFieldsValidation( // are related to the given concrete field, determining whether its in a valid // condition and updating the concrete field with the referenced value. // +// The generated code calls ackrt.ResolveCrossNamespaceReference to validate +// the reference and, when the reference targets a different namespace and +// the cross-namespace flag is enabled, emit a deprecation warning and set +// the ACK.CrossNamespaceOptInRequired condition on the resource. When the +// flag is disabled, the helper returns a terminal error. +// // Sample output (resolving a singular reference): // // if ko.Spec.APIRef != nil && ko.Spec.APIRef.From != nil { @@ -130,6 +136,18 @@ func ReferenceFieldsValidation( // if arr.Name == nil || *arr.Name == "" { // return hasReferences, fmt.Errorf("provided resource reference is nil or empty: APIRef") // } +// namespace, err := ackrt.ResolveCrossNamespaceReference( +// ctx, +// rm.cfg.EnableCrossNamespace, +// &ko.Status.Conditions, +// ackrt.CrossNamespaceRefKindResource, +// ko.ObjectMeta.GetNamespace(), +// arr.Namespace, +// *arr.Name, +// ) +// if err != nil { +// return hasReferences, err +// } // obj := &svcapitypes.API{} // if err := getReferencedResourceState_API(ctx, apiReader, obj, *arr.Name, namespace); err != nil { // return hasReferences, err @@ -146,6 +164,10 @@ func ReferenceFieldsValidation( // if arr.Name == nil || *arr.Name == "" { // return hasReferences, fmt.Errorf("provided resource reference is nil or empty: SecurityGroupRefs") // } +// namespace, err := ackrt.ResolveCrossNamespaceReference( ... ) +// if err != nil { +// return hasReferences, err +// } // obj := &ec2apitypes.SecurityGroup{} // if err := getReferencedResourceState_SecurityGroup(ctx, apiReader, obj, *arr.Name, namespace); err != nil { // return hasReferences, err @@ -170,6 +192,10 @@ func ReferenceFieldsValidation( // if arr.Name == nil || *arr.Name == "" { // return hasReferences, fmt.Errorf("provided resource reference is nil or empty: Notification.LambdaFunctionConfigurations.Filter.Key.FilterRules.ValueRef") // } +// namespace, err := ackrt.ResolveCrossNamespaceReference( ... ) +// if err != nil { +// return hasReferences, err +// } // obj := &svcapitypes.Bucket{} // if err := getReferencedResourceState_Bucket(ctx, apiReader, obj, *arr.Name, namespace); err != nil { // return hasReferences, err @@ -228,9 +254,17 @@ func ResolveReferencesForField(field *model.Field, sourceVarName string, indentL outPrefix += fmt.Sprintf("%s\treturn hasReferences, fmt.Errorf(\"provided resource reference is nil or empty: %s\")\n", innerIndent, refFieldPath) outPrefix += fmt.Sprintf("%s}\n", innerIndent) - outPrefix += fmt.Sprintf("%snamespace := ko.ObjectMeta.GetNamespace()\n", innerIndent) - outPrefix += fmt.Sprintf("%sif arr.Namespace != nil && *arr.Namespace != \"\" {\n", innerIndent) - outPrefix += fmt.Sprintf("%s\tnamespace = *arr.Namespace\n", innerIndent) + outPrefix += fmt.Sprintf("%snamespace, err := ackrt.ResolveCrossNamespaceReference(\n", innerIndent) + outPrefix += fmt.Sprintf("%s\tctx,\n", innerIndent) + outPrefix += fmt.Sprintf("%s\trm.cfg.EnableCrossNamespace,\n", innerIndent) + outPrefix += fmt.Sprintf("%s\t&ko.Status.Conditions,\n", innerIndent) + outPrefix += fmt.Sprintf("%s\tackrt.CrossNamespaceRefKindResource,\n", innerIndent) + outPrefix += fmt.Sprintf("%s\tko.ObjectMeta.GetNamespace(),\n", innerIndent) + outPrefix += fmt.Sprintf("%s\tarr.Namespace,\n", innerIndent) + outPrefix += fmt.Sprintf("%s\t*arr.Name,\n", innerIndent) + outPrefix += fmt.Sprintf("%s)\n", innerIndent) + outPrefix += fmt.Sprintf("%sif err != nil {\n", innerIndent) + outPrefix += fmt.Sprintf("%s\treturn hasReferences, err\n", innerIndent) outPrefix += fmt.Sprintf("%s}\n", innerIndent) outPrefix += getReferencedStateForField(field, innerIndentLevel) diff --git a/pkg/generate/code/resource_reference_test.go b/pkg/generate/code/resource_reference_test.go index c801a536..34e6d0d7 100644 --- a/pkg/generate/code/resource_reference_test.go +++ b/pkg/generate/code/resource_reference_test.go @@ -144,9 +144,17 @@ func Test_ResolveReferencesForField_SingleReference(t *testing.T) { if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: APIRef") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, err := ackrt.ResolveCrossNamespaceReference( + ctx, + rm.cfg.EnableCrossNamespace, + &ko.Status.Conditions, + ackrt.CrossNamespaceRefKindResource, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err } obj := &svcapitypes.API{} if err := getReferencedResourceState_API(ctx, apiReader, obj, *arr.Name, namespace); err != nil { @@ -180,9 +188,17 @@ func Test_ResolveReferencesForField_ReferencingARN(t *testing.T) { 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, err := ackrt.ResolveCrossNamespaceReference( + ctx, + rm.cfg.EnableCrossNamespace, + &ko.Status.Conditions, + ackrt.CrossNamespaceRefKindResource, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err } obj := &svcapitypes.Policy{} if err := getReferencedResourceState_Policy(ctx, apiReader, obj, *arr.Name, namespace); err != nil { @@ -217,9 +233,17 @@ func Test_ResolveReferencesForField_SliceOfReferences(t *testing.T) { if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: SecurityGroupRefs") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, err := ackrt.ResolveCrossNamespaceReference( + ctx, + rm.cfg.EnableCrossNamespace, + &ko.Status.Conditions, + ackrt.CrossNamespaceRefKindResource, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err } obj := &ec2apitypes.SecurityGroup{} if err := getReferencedResourceState_SecurityGroup(ctx, apiReader, obj, *arr.Name, namespace); err != nil { @@ -258,9 +282,17 @@ func Test_ResolveReferencesForField_NestedSingleReference(t *testing.T) { if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: JWTConfiguration.IssuerRef") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, err := ackrt.ResolveCrossNamespaceReference( + ctx, + rm.cfg.EnableCrossNamespace, + &ko.Status.Conditions, + ackrt.CrossNamespaceRefKindResource, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err } obj := &svcapitypes.API{} if err := getReferencedResourceState_API(ctx, apiReader, obj, *arr.Name, namespace); err != nil { @@ -299,9 +331,17 @@ func Test_ResolveReferencesForField_SingleReference_DeeplyNested(t *testing.T) { if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: Logging.LoggingEnabled.TargetBucketRef") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, err := ackrt.ResolveCrossNamespaceReference( + ctx, + rm.cfg.EnableCrossNamespace, + &ko.Status.Conditions, + ackrt.CrossNamespaceRefKindResource, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err } obj := &svcapitypes.Bucket{} if err := getReferencedResourceState_Bucket(ctx, apiReader, obj, *arr.Name, namespace); err != nil { @@ -340,9 +380,17 @@ func Test_ResolveReferencesForField_SingleReference_WithinSlice(t *testing.T) { if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: Routes.GatewayRef") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, err := ackrt.ResolveCrossNamespaceReference( + ctx, + rm.cfg.EnableCrossNamespace, + &ko.Status.Conditions, + ackrt.CrossNamespaceRefKindResource, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err } obj := &svcapitypes.InternetGateway{} if err := getReferencedResourceState_InternetGateway(ctx, apiReader, obj, *arr.Name, namespace); err != nil { @@ -384,9 +432,17 @@ func Test_ResolveReferencesForField_SingleReference_WithinMultipleSlices(t *test if arr.Name == nil || *arr.Name == "" { return hasReferences, fmt.Errorf("provided resource reference is nil or empty: Notification.LambdaFunctionConfigurations.Filter.Key.FilterRules.ValueRef") } - namespace := ko.ObjectMeta.GetNamespace() - if arr.Namespace != nil && *arr.Namespace != "" { - namespace = *arr.Namespace + namespace, err := ackrt.ResolveCrossNamespaceReference( + ctx, + rm.cfg.EnableCrossNamespace, + &ko.Status.Conditions, + ackrt.CrossNamespaceRefKindResource, + ko.ObjectMeta.GetNamespace(), + arr.Namespace, + *arr.Name, + ) + if err != nil { + return hasReferences, err } obj := &svcapitypes.Bucket{} if err := getReferencedResourceState_Bucket(ctx, apiReader, obj, *arr.Name, namespace); err != nil { diff --git a/pkg/generate/code/set_sdk.go b/pkg/generate/code/set_sdk.go index 583ee2d8..1152819a 100644 --- a/pkg/generate/code/set_sdk.go +++ b/pkg/generate/code/set_sdk.go @@ -1101,6 +1101,11 @@ func setSDKForContainer( // the value of a Secret when the type of the source variable is a // SecretKeyReference. // +// Cross-namespace validation (and the Phase 1 deprecation warning) is +// performed inside the runtime's SecretValueFromReference, so it is not +// emitted here. This ensures every caller is covered, including custom +// update functions and hooks that call SecretValueFromReference directly. +// // The Go code output from this function looks like this: // // tmpSecret, err := rm.rr.SecretValueFromReference(ctx, ko.Spec.MasterUserPassword) @@ -1111,16 +1116,6 @@ func setSDKForContainer( // res.SetMasterUserPassword(tmpSecret) // } // -// or: -// -// tmpSecret, err := rm.rr.SecretValueFromReference(ctx, f3iter) -// if err != nil { -// return nil, ackrequeue.Needed(err) -// } -// if tmpSecret != "" { -// f3elem = tmpSecret -// } -// // The second case is used when the SecretKeyReference field // is a slice of `[]*string` in the original AWS API Input shape. @@ -1140,6 +1135,11 @@ func setSDKForSecret( indent := strings.Repeat("\t", indentLevel) secVar := "tmpSecret" + // Cross-namespace validation for the secret reference is performed inside + // the runtime's SecretValueFromReference, so that every call site is + // covered (including custom update functions and hooks). No per-call + // validation is generated here. + // tmpSecret, err := rm.rr.SecretValueFromReference(ctx, ko.Spec.MasterUserPassword) out += fmt.Sprintf( "%s\t%s, err := rm.rr.SecretValueFromReference(ctx, %s)\n", diff --git a/templates/helm/templates/deployment.yaml.tpl b/templates/helm/templates/deployment.yaml.tpl index a8219bac..7d478d7f 100644 --- a/templates/helm/templates/deployment.yaml.tpl +++ b/templates/helm/templates/deployment.yaml.tpl @@ -104,6 +104,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/templates/helm/values.schema.json.tpl b/templates/helm/values.schema.json.tpl index dcd0eeb3..cca512c3 100644 --- a/templates/helm/values.schema.json.tpl +++ b/templates/helm/values.schema.json.tpl @@ -280,6 +280,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/templates/helm/values.yaml.tpl b/templates/helm/values.yaml.tpl index 15cf4d09..aafedbcf 100644 --- a/templates/helm/values.yaml.tpl +++ b/templates/helm/values.yaml.tpl @@ -180,6 +180,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/templates/pkg/resource/references.go.tpl b/templates/pkg/resource/references.go.tpl index 91da645b..6f9506b7 100644 --- a/templates/pkg/resource/references.go.tpl +++ b/templates/pkg/resource/references.go.tpl @@ -15,6 +15,7 @@ import ( {{ if .CRD.HasReferenceFields -}} 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" {{ end -}} acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" {{ $servicePackageName := .ServicePackageName -}} @@ -133,4 +134,3 @@ func (rm *resourceManager) resolveReferenceFor{{ $field.FieldPathWithUnderscore {{ end -}} {{ end -}} {{ end -}} -