-
Notifications
You must be signed in to change notification settings - Fork 193
feat(runtime): Add --enable-cross-namespace flag #239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,14 @@ var ( | |
| ResourceReferenceMissingTargetField = fmt.Errorf( | ||
| "the referenced resource is missing the target field", | ||
| ) | ||
| // ResourceReferenceCrossNamespaceNotAllowed indicates that a resource | ||
| // reference specifies a Namespace that differs from the namespace of the | ||
| // resource containing the reference, and the | ||
| // --enable-cross-namespace flag is not enabled. | ||
| ResourceReferenceCrossNamespaceNotAllowed = fmt.Errorf( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #248 — error message now says |
||
| "cross-namespace resource reference is not allowed. " + | ||
| "Set '--enable-cross-namespace' flag to allow it", | ||
| ) | ||
| ) | ||
|
|
||
| // ResourceReferenceOrIDRequiredFor returns a ResourceReferenceOrIDRequired error | ||
|
|
@@ -90,3 +98,18 @@ func ResourceReferenceMissingTargetFieldFor(resource string, namespace string, | |
| ", targetField:%s", ResourceReferenceMissingTargetField, | ||
| resource, namespace, name, targetField) | ||
| } | ||
|
|
||
| // ResourceReferenceCrossNamespaceNotAllowedFor returns a | ||
| // ResourceReferenceCrossNamespaceNotAllowed error annotated with the | ||
| // source namespace, target namespace, and referenced resource name. | ||
| func ResourceReferenceCrossNamespaceNotAllowedFor( | ||
| sourceNamespace string, | ||
| targetNamespace string, | ||
| name string, | ||
| ) error { | ||
| return fmt.Errorf( | ||
| "%w. sourceNamespace:%s, targetNamespace:%s, name:%s", | ||
| ResourceReferenceCrossNamespaceNotAllowed, | ||
| sourceNamespace, targetNamespace, name, | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -153,6 +153,23 @@ func (r *fieldExportReconciler) Sync( | |
| r.patchResourceStatus(ctx, &desired, latest) | ||
| }() | ||
|
|
||
| // Validate cross-namespace access for the field export target | ||
| resolvedNamespace, isCrossNamespace, nsErr := r.validateFieldExportNamespace(&desired) | ||
| if nsErr != nil { | ||
| return desired, r.onError(ctx, &desired, ackerr.NewTerminalError(nsErr)) | ||
| } | ||
| if isCrossNamespace { | ||
| r.log.V(0).Info( | ||
| "cross-namespace field export detected; this behavior will be "+ | ||
| "disabled by default in a future release. Set --enable-cross-namespace "+ | ||
| "to preserve this behavior.", | ||
| "fieldExportNamespace", desired.Namespace, | ||
| "targetNamespace", r.getTargetNamespace(&desired), | ||
| "targetName", *desired.Spec.To.Name, | ||
| ) | ||
| r.setCrossNsOptInRequiredCondition(&desired) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #248 - using nil-safe |
||
| } | ||
|
|
||
| // Get the field from the resource | ||
| value, err := r.getSourcePathFromResource(from, *desired.Spec.From.Path) | ||
| if err != nil { | ||
|
|
@@ -163,11 +180,11 @@ func (r *fieldExportReconciler) Sync( | |
|
|
||
| switch desired.Spec.To.Kind { | ||
| case ackv1alpha1.FieldExportOutputTypeConfigMap: | ||
| if err = r.writeToConfigMap(ctx, *value, &desired); err != nil { | ||
| if err = r.writeToConfigMap(ctx, *value, &desired, resolvedNamespace); err != nil { | ||
| return desired, r.onError(ctx, &desired, err) | ||
| } | ||
| case ackv1alpha1.FieldExportOutputTypeSecret: | ||
| if err = r.writeToSecret(ctx, *value, &desired); err != nil { | ||
| if err = r.writeToSecret(ctx, *value, &desired, resolvedNamespace); err != nil { | ||
| return desired, r.onError(ctx, &desired, err) | ||
| } | ||
| } | ||
|
|
@@ -177,6 +194,55 @@ func (r *fieldExportReconciler) Sync( | |
| return desired, r.onSuccess(ctx, &desired) | ||
| } | ||
|
|
||
| // validateFieldExportNamespace checks whether the field export target namespace | ||
| // is allowed given the current cross-namespace flag setting. | ||
| func (r *fieldExportReconciler) validateFieldExportNamespace( | ||
| desired *ackv1alpha1.FieldExport, | ||
| ) (string, bool, error) { | ||
| targetNamespace := r.getTargetNamespace(desired) | ||
| targetName := "" | ||
| if desired.Spec.To != nil && desired.Spec.To.Name != nil { | ||
| targetName = *desired.Spec.To.Name | ||
| } | ||
|
|
||
| return ValidateCrossNamespaceReferenceString( | ||
| r.cfg.EnableCrossNamespace, | ||
| desired.Namespace, | ||
| targetNamespace, | ||
| targetName, | ||
| ) | ||
| } | ||
|
|
||
| // getTargetNamespace returns the target namespace for the field export write. | ||
| // If Spec.To.Namespace is set, it returns that value; otherwise it returns | ||
| // the FieldExport's own namespace. | ||
| func (r *fieldExportReconciler) getTargetNamespace( | ||
| desired *ackv1alpha1.FieldExport, | ||
| ) string { | ||
| if desired.Spec.To != nil && desired.Spec.To.Namespace != nil && *desired.Spec.To.Namespace != "" { | ||
| return *desired.Spec.To.Namespace | ||
| } | ||
| return desired.Namespace | ||
| } | ||
|
|
||
| // setCrossNsOptInRequiredCondition sets the ACK.CrossNamespaceOptInRequired | ||
| // condition on the FieldExport resource to notify users that cross-namespace | ||
| // behavior will require explicit opt-in in a future release. | ||
| func (r *fieldExportReconciler) setCrossNsOptInRequiredCondition( | ||
| desired *ackv1alpha1.FieldExport, | ||
| ) { | ||
| message := "Cross-namespace field export detected: FieldExport in namespace \"" + | ||
| desired.Namespace + "\" targets namespace \"" + r.getTargetNamespace(desired) + | ||
| "\". Cross-namespace behavior will require explicit opt-in in a future release. " + | ||
| "Set --enable-cross-namespace=true to preserve this behavior." | ||
| condition := &ackv1alpha1.Condition{ | ||
| Type: ackv1alpha1.ConditionTypeCrossNamespaceOptInRequired, | ||
| Status: corev1.ConditionTrue, | ||
| Message: &message, | ||
| } | ||
| desired.Status.Conditions = append(desired.Status.Conditions, condition) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in #248 — now uses lookup-or-create pattern (search by type, update if present, append if not) matching |
||
| } | ||
|
|
||
| // cleanup removes the finalizer from FieldExport so that k8s object can | ||
| // be deleted. | ||
| func (r *fieldExportReconciler) cleanup( | ||
|
|
@@ -293,6 +359,7 @@ func (r *fieldExportReconciler) writeToConfigMap( | |
| ctx context.Context, | ||
| sourceValue string, | ||
| desired *ackv1alpha1.FieldExport, | ||
| resolvedNamespace string, | ||
| ) error { | ||
| // Construct the data key | ||
| key := fmt.Sprintf("%s.%s", desired.Namespace, desired.Name) | ||
|
|
@@ -302,12 +369,8 @@ func (r *fieldExportReconciler) writeToConfigMap( | |
|
|
||
| // Get the initial configmap | ||
| nsn := types.NamespacedName{ | ||
| Name: *desired.Spec.To.Name, | ||
| } | ||
| if desired.Spec.To.Namespace != nil { | ||
| nsn.Namespace = *desired.Spec.To.Namespace | ||
| } else { | ||
| nsn.Namespace = desired.Namespace | ||
| Name: *desired.Spec.To.Name, | ||
| Namespace: resolvedNamespace, | ||
| } | ||
|
|
||
| cm := &corev1.ConfigMap{} | ||
|
|
@@ -340,6 +403,7 @@ func (r *fieldExportReconciler) writeToSecret( | |
| ctx context.Context, | ||
| sourceValue string, | ||
| desired *ackv1alpha1.FieldExport, | ||
| resolvedNamespace string, | ||
| ) error { | ||
| // Construct the data key | ||
| key := fmt.Sprintf("%s.%s", desired.Namespace, desired.Name) | ||
|
|
@@ -349,12 +413,8 @@ func (r *fieldExportReconciler) writeToSecret( | |
|
|
||
| // Get the initial secret | ||
| nsn := types.NamespacedName{ | ||
| Name: *desired.Spec.To.Name, | ||
| } | ||
| if desired.Spec.To.Namespace != nil { | ||
| nsn.Namespace = *desired.Spec.To.Namespace | ||
| } else { | ||
| nsn.Namespace = desired.Namespace | ||
| Name: *desired.Spec.To.Name, | ||
| Namespace: resolvedNamespace, | ||
| } | ||
|
|
||
| secret := &corev1.Secret{} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -131,21 +131,28 @@ func (r *reconciler) SecretValueFromReference( | |
| return "", nil | ||
| } | ||
|
|
||
| namespace := ref.Namespace | ||
| // During the reconcile process, the resourceNamespace is stored in the context | ||
| // and can be used to fetch the secret if the namespace is not provided in the | ||
| // SecretKeyReference. | ||
| // | ||
| // NOTE(a-hilaly): When refactoring the runtime, we might want to consider passing | ||
| // the ObjectMeta in the context. | ||
| ownerNamespace := "" | ||
| ctxResourceNamespace := ctx.Value("resourceNamespace") | ||
| if namespace == "" && ctxResourceNamespace != nil { | ||
| ctxNamespace, ok := ctxResourceNamespace.(string) | ||
| if ok { | ||
| namespace = ctxNamespace | ||
| if ctxResourceNamespace != nil { | ||
| if ctxNamespace, ok := ctxResourceNamespace.(string); ok { | ||
| ownerNamespace = ctxNamespace | ||
| } | ||
| } | ||
|
|
||
| // Determine the namespace to use for fetching the secret. | ||
| // Cross-namespace validation, logging, and condition-setting are handled | ||
| // by the generated code before calling this method. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR description says this PR "Integrate[s] cross-namespace check into Net effect on this branch:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in #248 - updated PR description to clarify that cross-namespace validation for secrets is handled by generated code in aws-controllers-k8s/code-generator#699. SecretValueFromReference here only extracts ownerNamespace; validation is deferred to the caller |
||
| namespace := ref.Namespace | ||
| if namespace == "" { | ||
| namespace = ownerNamespace | ||
| } | ||
|
|
||
| nsn := client.ObjectKey{ | ||
| Namespace: namespace, | ||
| Name: ref.Name, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2405,3 +2405,74 @@ func TestPreDeleteSync_UpdateAndDeleteBothFail(t *testing.T) { | |
| // Delete should NOT have been called — fail fast on pre-delete sync error | ||
| rm.AssertNotCalled(t, "Delete", mock.Anything, mock.Anything) | ||
| } | ||
|
|
||
| func TestReconcilerSync_CrossNamespaceReferenceRejected(t *testing.T) { | ||
| require := require.New(t) | ||
| assert := assert.New(t) | ||
|
|
||
| ctx := context.TODO() | ||
|
|
||
| desired, _, _ := resourceMocks() | ||
| desired.On("ReplaceConditions", []*ackv1alpha1.Condition{}).Return() | ||
|
|
||
| crossNsErr := ackerr.ResourceReferenceCrossNamespaceNotAllowedFor( | ||
| "default", "other-ns", "my-ref", | ||
| ) | ||
|
|
||
| // When ResolveReferences returns a cross-namespace error, the reconciler | ||
| // should set ReferencesResolved=False on the resource and return the | ||
| // error without calling ReadOne. | ||
| desired.On("Conditions").Return([]*ackv1alpha1.Condition{}) | ||
| desired.On( | ||
| "ReplaceConditions", | ||
| mock.AnythingOfType("[]*v1alpha1.Condition"), | ||
| ).Return().Run(func(args mock.Arguments) { | ||
| conditions := args.Get(0).([]*ackv1alpha1.Condition) | ||
| // Find the ReferencesResolved condition | ||
| var refCond *ackv1alpha1.Condition | ||
| for _, c := range conditions { | ||
| if c.Type == ackv1alpha1.ConditionTypeReferencesResolved { | ||
| refCond = c | ||
| break | ||
| } | ||
| } | ||
| require.NotNil(refCond, "expected ReferencesResolved condition to be set") | ||
| assert.Equal(corev1.ConditionFalse, refCond.Status, | ||
| "ReferencesResolved should be False for cross-namespace rejection") | ||
| assert.Equal(ackcondition.FailedReferenceResolutionMessage, *refCond.Message) | ||
| assert.Contains(*refCond.Reason, "cross-namespace resource reference is not allowed") | ||
| assert.Contains(*refCond.Reason, "default") | ||
| assert.Contains(*refCond.Reason, "other-ns") | ||
| assert.Contains(*refCond.Reason, "my-ref") | ||
| }) | ||
|
|
||
| rm := &ackmocks.AWSResourceManager{} | ||
| rm.On("ResolveReferences", ctx, nil, desired).Return( | ||
| desired, true, crossNsErr, | ||
| ) | ||
| rm.On("ClearResolvedReferences", desired).Return(desired) | ||
|
|
||
| latest, latestRTObj, _ := resourceMocks() | ||
| rm.On("ClearResolvedReferences", latest).Return(latest) | ||
| // ReadOne should NOT be called — set it up but assert it's never invoked | ||
| rm.On("ReadOne", ctx, desired).Return(latest, nil) | ||
|
|
||
| rmf, _ := managedResourceManagerFactoryMocks(desired, latest) | ||
|
|
||
| r, kc, scmd := reconcilerMocks(rmf) | ||
| rm.On("EnsureTags", ctx, desired, scmd).Return(nil) | ||
| kc.On("Patch", withoutCancelContextMatcher, latestRTObj, mock.AnythingOfType("*client.mergeFromPatch")).Return(nil) | ||
|
|
||
| _, err := r.Sync(ctx, rm, desired) | ||
| require.NotNil(err) | ||
| assert.True(errors.Is(err, ackerr.ResourceReferenceCrossNamespaceNotAllowed), | ||
| "error should wrap ResourceReferenceCrossNamespaceNotAllowed sentinel") | ||
|
|
||
| // ReadOne must NOT be invoked when references fail to resolve | ||
| rm.AssertNotCalled(t, "ReadOne", ctx, desired) | ||
| // No downstream operations should occur | ||
| rm.AssertNotCalled(t, "Create", ctx, desired) | ||
| rm.AssertNotCalled(t, "Update") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good coverage on the reference-resolution rejection path. Three gaps still uncovered for the new code paths most likely to regress later:
Non-blocking, but easy adds while the helper signature is fresh.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will add as a follow up |
||
| rm.AssertNotCalled(t, "LateInitialize") | ||
| rm.AssertNotCalled(t, "EnsureTags", ctx, desired, scmd) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming inconsistency: PR description calls this
ACK.CrossNamespaceDeprecation, the type added here isACK.CrossNamespaceOptInRequired. The latter is the better of the two (describes the action operators must take), but please update the PR body so downstream release notes don't spread the wrong name — and be aware that the conditionTypestring is part of the user-visible API.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in #248 - PR description updated to use
ACK.CrossNamespaceOptInRequired.