-
Notifications
You must be signed in to change notification settings - Fork 70
✨ Add ServiceAccount validation to ClusterExtension reconciliation #2488
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
base: main
Are you sure you want to change the base?
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 | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,11 +15,13 @@ import ( | |||||||||||||||
| "helm.sh/helm/v3/pkg/chart" | ||||||||||||||||
| "helm.sh/helm/v3/pkg/release" | ||||||||||||||||
| "helm.sh/helm/v3/pkg/storage/driver" | ||||||||||||||||
| corev1 "k8s.io/api/core/v1" | ||||||||||||||||
| "k8s.io/apimachinery/pkg/api/equality" | ||||||||||||||||
| apimeta "k8s.io/apimachinery/pkg/api/meta" | ||||||||||||||||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||||||||||||
| "k8s.io/apimachinery/pkg/types" | ||||||||||||||||
| "k8s.io/apimachinery/pkg/util/rand" | ||||||||||||||||
| "k8s.io/client-go/kubernetes/fake" | ||||||||||||||||
| ctrl "sigs.k8s.io/controller-runtime" | ||||||||||||||||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||||||||||||||||
| crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer" | ||||||||||||||||
|
|
@@ -29,7 +31,6 @@ import ( | |||||||||||||||
| "github.com/operator-framework/operator-registry/alpha/declcfg" | ||||||||||||||||
|
|
||||||||||||||||
| ocv1 "github.com/operator-framework/operator-controller/api/v1" | ||||||||||||||||
| "github.com/operator-framework/operator-controller/internal/operator-controller/authentication" | ||||||||||||||||
| "github.com/operator-framework/operator-controller/internal/operator-controller/bundle" | ||||||||||||||||
| "github.com/operator-framework/operator-controller/internal/operator-controller/conditionsets" | ||||||||||||||||
| "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" | ||||||||||||||||
|
|
@@ -767,60 +768,177 @@ func TestClusterExtensionBoxcutterApplierFailsDoesNotLeakDeprecationErrors(t *te | |||||||||||||||
| require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{})) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| func TestClusterExtensionServiceAccountNotFound(t *testing.T) { | ||||||||||||||||
|
Contributor
Author
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. I moved this use-case into a more general test against validation |
||||||||||||||||
| cl, reconciler := newClientAndReconciler(t, func(d *deps) { | ||||||||||||||||
| d.RevisionStatesGetter = &MockRevisionStatesGetter{ | ||||||||||||||||
| Err: &authentication.ServiceAccountNotFoundError{ | ||||||||||||||||
| ServiceAccountName: "missing-sa", | ||||||||||||||||
| ServiceAccountNamespace: "default", | ||||||||||||||||
| }} | ||||||||||||||||
| }) | ||||||||||||||||
|
|
||||||||||||||||
| ctx := context.Background() | ||||||||||||||||
| extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} | ||||||||||||||||
|
|
||||||||||||||||
| t.Log("Given a cluster extension with a missing service account") | ||||||||||||||||
| clusterExtension := &ocv1.ClusterExtension{ | ||||||||||||||||
| ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, | ||||||||||||||||
| Spec: ocv1.ClusterExtensionSpec{ | ||||||||||||||||
| Source: ocv1.SourceConfig{ | ||||||||||||||||
| SourceType: "Catalog", | ||||||||||||||||
| Catalog: &ocv1.CatalogFilter{ | ||||||||||||||||
| PackageName: "test-package", | ||||||||||||||||
| func TestValidateClusterExtension(t *testing.T) { | ||||||||||||||||
| tests := []struct { | ||||||||||||||||
| name string | ||||||||||||||||
| validators []controllers.ClusterExtensionValidator | ||||||||||||||||
| expectError bool | ||||||||||||||||
| errorMessageIncludes string | ||||||||||||||||
| }{ | ||||||||||||||||
| { | ||||||||||||||||
| name: "all validators pass", | ||||||||||||||||
| validators: []controllers.ClusterExtensionValidator{ | ||||||||||||||||
| // Validator that always passes | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return nil | ||||||||||||||||
| }, | ||||||||||||||||
| }, | ||||||||||||||||
| Namespace: "default", | ||||||||||||||||
| ServiceAccount: ocv1.ServiceAccountReference{ | ||||||||||||||||
| Name: "missing-sa", | ||||||||||||||||
| expectError: false, | ||||||||||||||||
| }, | ||||||||||||||||
| { | ||||||||||||||||
| name: "validator fails - sets Progressing condition", | ||||||||||||||||
| validators: []controllers.ClusterExtensionValidator{ | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return errors.New("generic validation error") | ||||||||||||||||
| }, | ||||||||||||||||
| }, | ||||||||||||||||
| expectError: true, | ||||||||||||||||
| errorMessageIncludes: "generic validation error", | ||||||||||||||||
| }, | ||||||||||||||||
| { | ||||||||||||||||
| name: "multiple validators - collects all failures", | ||||||||||||||||
| validators: []controllers.ClusterExtensionValidator{ | ||||||||||||||||
| // First validator fails | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return errors.New("first validator failed") | ||||||||||||||||
| }, | ||||||||||||||||
| // Second validator also fails | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return errors.New("second validator failed") | ||||||||||||||||
| }, | ||||||||||||||||
| // Third validator fails too | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return errors.New("third validator failed") | ||||||||||||||||
| }, | ||||||||||||||||
| }, | ||||||||||||||||
| expectError: true, | ||||||||||||||||
| errorMessageIncludes: "first validator failed\nsecond validator failed\nthird validator failed", | ||||||||||||||||
| }, | ||||||||||||||||
| { | ||||||||||||||||
| name: "multiple validators - all pass", | ||||||||||||||||
| validators: []controllers.ClusterExtensionValidator{ | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return nil | ||||||||||||||||
| }, | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return nil | ||||||||||||||||
| }, | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return nil | ||||||||||||||||
| }, | ||||||||||||||||
| }, | ||||||||||||||||
| expectError: false, | ||||||||||||||||
| }, | ||||||||||||||||
| { | ||||||||||||||||
| name: "multiple validators - some pass, some fail", | ||||||||||||||||
| validators: []controllers.ClusterExtensionValidator{ | ||||||||||||||||
| // First validator passes | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return nil | ||||||||||||||||
| }, | ||||||||||||||||
| // Second validator fails | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return errors.New("validation error 1") | ||||||||||||||||
| }, | ||||||||||||||||
| // Third validator passes | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return nil | ||||||||||||||||
| }, | ||||||||||||||||
| // Fourth validator fails | ||||||||||||||||
| func(_ context.Context, _ *ocv1.ClusterExtension) error { | ||||||||||||||||
| return errors.New("validation error 2") | ||||||||||||||||
| }, | ||||||||||||||||
| }, | ||||||||||||||||
| expectError: true, | ||||||||||||||||
| errorMessageIncludes: "validation error 1\nvalidation error 2", | ||||||||||||||||
| }, | ||||||||||||||||
| { | ||||||||||||||||
| name: "service account not found", | ||||||||||||||||
| validators: []controllers.ClusterExtensionValidator{ | ||||||||||||||||
| serviceAccountValidatorWithFakeClient(&corev1.ServiceAccount{ | ||||||||||||||||
| ObjectMeta: metav1.ObjectMeta{ | ||||||||||||||||
| Name: "missing-sa", | ||||||||||||||||
|
Comment on lines
+858
to
+860
|
||||||||||||||||
| serviceAccountValidatorWithFakeClient(&corev1.ServiceAccount{ | |
| ObjectMeta: metav1.ObjectMeta{ | |
| Name: "missing-sa", | |
| // Create a different ServiceAccount to ensure "test-sa" is not found. | |
| serviceAccountValidatorWithFakeClient(&corev1.ServiceAccount{ | |
| ObjectMeta: metav1.ObjectMeta{ | |
| Name: "other-sa", |
perdasilva marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -21,15 +21,16 @@ import ( | |||||
| "errors" | ||||||
| "fmt" | ||||||
|
|
||||||
| apierrors "k8s.io/apimachinery/pkg/api/errors" | ||||||
| apimeta "k8s.io/apimachinery/pkg/api/meta" | ||||||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| corev1client "k8s.io/client-go/kubernetes/typed/core/v1" | ||||||
| ctrl "sigs.k8s.io/controller-runtime" | ||||||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
| "sigs.k8s.io/controller-runtime/pkg/finalizer" | ||||||
| "sigs.k8s.io/controller-runtime/pkg/log" | ||||||
|
|
||||||
| ocv1 "github.com/operator-framework/operator-controller/api/v1" | ||||||
| "github.com/operator-framework/operator-controller/internal/operator-controller/authentication" | ||||||
| "github.com/operator-framework/operator-controller/internal/operator-controller/bundleutil" | ||||||
| "github.com/operator-framework/operator-controller/internal/operator-controller/labels" | ||||||
| "github.com/operator-framework/operator-controller/internal/operator-controller/resolve" | ||||||
|
|
@@ -63,19 +64,62 @@ func HandleFinalizers(f finalizer.Finalizer) ReconcileStepFunc { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| // ClusterExtensionValidator is a function that validates a ClusterExtension. | ||||||
| // It returns an error if validation fails. Validators are executed sequentially | ||||||
| // in the order they are registered. | ||||||
| type ClusterExtensionValidator func(context.Context, *ocv1.ClusterExtension) error | ||||||
|
|
||||||
| // ValidateClusterExtension returns a ReconcileStepFunc that executes all | ||||||
| // validators sequentially. All validators are executed even if some fail, | ||||||
| // and all errors are collected and returned as a joined error. | ||||||
| // This provides complete validation feedback in a single reconciliation cycle. | ||||||
| func ValidateClusterExtension(validators ...ClusterExtensionValidator) ReconcileStepFunc { | ||||||
|
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. it would be nice to add some unit tests for the new reconcile step.
Contributor
Author
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. I think I've covered those here, no? |
||||||
| return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) { | ||||||
| l := log.FromContext(ctx) | ||||||
|
|
||||||
| l.Info("validating cluster extension") | ||||||
| var validationErrors []error | ||||||
| for _, validator := range validators { | ||||||
| if err := validator(ctx, ext); err != nil { | ||||||
| validationErrors = append(validationErrors, err) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // If there are no validation errors, continue reconciliation | ||||||
| if len(validationErrors) == 0 { | ||||||
| return nil, nil | ||||||
| } | ||||||
|
|
||||||
| // Set status condition with the validation error for other validation failures | ||||||
|
||||||
| // Set status condition with the validation error for other validation failures | |
| // Set status conditions with the validation errors |
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.
this got split between the service account validator and the cluster extension validator
Uh oh!
There was an error while loading. Please reload this page.