Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
134c6d9
:seedling: Bump markdown from 3.10.1 to 3.10.2 (#2521)
dependabot[bot] Feb 23, 2026
0655ce1
Add regression test cases for DeploymentConfig options (#2493)
trgeiger Feb 23, 2026
06da96a
Call RevisionEngine.Teardown when CER is archived (#2502)
perdasilva Feb 23, 2026
ddf921f
feat: Add validation framework with ServiceAccount validator to Clust…
perdasilva Feb 23, 2026
df01be5
Merge branch 'main' into synchronize
Feb 24, 2026
cd1a70c
UPSTREAM: <carry>: Add OpenShift specific files
dtfranz Oct 26, 2023
4abc5ad
UPSTREAM: <carry>: Add new tests for single/own namespaces install modes
camilamacedo86 Oct 6, 2025
4c4c17b
UPSTREAM: <carry>: Upgrade OCP image from 4.20 to 4.21
camilamacedo86 Oct 13, 2025
9412a22
UPSTREAM: <carry>: [Default Catalog Tests] - Change logic to get ocp …
camilamacedo86 Oct 13, 2025
679df30
UPSTREAM: <carry>: Update OCP catalogs to v4.21
tmshort Oct 13, 2025
bc23613
UPSTREAM: <carry>: support singleown cases in disconnected
kuiwang02 Oct 16, 2025
fe21b53
UPSTREAM: <carry>: fix cases 81696 and 74618 for product code changes
kuiwang02 Oct 17, 2025
a32b74c
UPSTREAM: <carry>: Define Default timeouts and apply their usage accr…
camilamacedo86 Oct 22, 2025
4fd1d26
UPSTREAM: <carry>: Update to new feature-gate options in helm
tmshort Oct 22, 2025
4f24e41
UPSTREAM: <carry>: Fix flake for single/own ns tests by ensuring uniq…
camilamacedo86 Oct 22, 2025
46ca18c
UPSTREAM: <carry>: [OTE]: Enhance single/own ns based on review comme…
camilamacedo86 Oct 24, 2025
033d41b
UPSTREAM: <carry>: Update OwnSingle template to use spec.config.inlin…
kuiwang02 Nov 3, 2025
fea9cf8
UPSTREAM: <carry>: [OTE]: Add webhook cleanup validation on extension…
camilamacedo86 Nov 4, 2025
c43d03e
UPSTREAM: <carry>: Add [OTP] to migrated cases
kuiwang02 Nov 7, 2025
ce02f86
UPSTREAM: <carry>: [OTE]: Upgrade dependencies used
camilamacedo86 Nov 5, 2025
cc58691
UPSTREAM: <carry>: fix(OTE): fix OpenShift Kubernetes replace version…
camilamacedo86 Nov 10, 2025
bf84584
UPSTREAM: <carry>: [Default Catalog Tests] Upgrade go 1.24.6 and depe…
camilamacedo86 Nov 11, 2025
6dbb4ef
UPSTREAM: <carry>: add disconnected environment support with custom p…
kuiwang02 Nov 12, 2025
362f9f1
UPSTREAM: <carry>: migrate jiazha test cases to OTE
jianzhangbjz Nov 14, 2025
090ddef
UPSTREAM: <carry>: migrate clustercatalog case to ote
Xia-Zhao-rh Oct 17, 2025
55e1baf
UPSTREAM: <carry>: migrate olmv1 QE stress cases
kuiwang02 Nov 20, 2025
0fb0179
UPSTREAM: <carry>: Use busybox/httpd to simulate probes
tmshort Nov 25, 2025
9f00262
UPSTREAM: <carry>: migrate olmv1 QE cases
Xia-Zhao-rh Nov 25, 2025
0b872fa
UPSTREAM: <carry>: add agent for olmv1 qe cases
kuiwang02 Oct 21, 2025
645d6f7
UPSTREAM: <carry>: Disable upstream PodDisruptionBudget
tmshort Dec 3, 2025
e6f2b4d
UPSTREAM: <carry>: Add AGENTS.md for AI code contributions
rashmigottipati Dec 11, 2025
58d92d6
UPSTREAM: <carry>: address review comments through addl prompts
rashmigottipati Dec 11, 2025
49c7a54
UPSTREAM: <carry>: addressing some more review comments
rashmigottipati Dec 11, 2025
f7f7b3a
UPSTREAM: <carry>: remove DCO line
rashmigottipati Dec 11, 2025
c1b847e
UPSTREAM: <carry>: migrate bandrade test cases to OTE
bandrade Nov 18, 2025
4fdbacf
UPSTREAM: <carry>: update metadata
bandrade Dec 3, 2025
2b679de
UPSTREAM: <carry>: remove originalName
bandrade Dec 3, 2025
e86983c
UPSTREAM: <carry>: update 80458's timeout to 180s
jianzhangbjz Dec 8, 2025
10c88d2
UPSTREAM: <carry>: update 83026 to specify the clustercatalog
jianzhangbjz Dec 15, 2025
7d5266e
UPSTREAM: <carry>: Update to golang 1.25 and ocp 4.22
oceanc80 Dec 18, 2025
07270a8
UPSTREAM: <carry>: Use oc client for running e2e tests
pedjak Jan 13, 2026
b04fe17
UPSTREAM: <carry>: Run upstream e2e tests tagged with `@catalogd-update`
pedjak Jan 14, 2026
48fe325
UPSTREAM: <carry>: enhance case to make it more stable
kuiwang02 Jan 6, 2026
489ad7b
UPSTREAM: <carry>: add service account to curl job
ehearne-redhat Jan 7, 2026
f888eb3
UPSTREAM: <carry>: move sa creation out of buildCurlJob()
ehearne-redhat Jan 8, 2026
ac0d058
UPSTREAM: <carry>: comment out delete service account
ehearne-redhat Jan 9, 2026
c75ebba
UPSTREAM: <carry>: move defercleanup for sa for LIFO
ehearne-redhat Jan 9, 2026
1f30685
UPSTREAM: <carry>: add polling so job fully deleted before proceed
ehearne-redhat Jan 12, 2026
daa9197
UPSTREAM: <carry>: Revert "Merge pull request #594 from ehearne-redha…
sosiouxme Jan 20, 2026
7c7c11e
UPSTREAM: <carry>: Remove openshift-redhat-marketplace catalog tests
camilamacedo86 Jan 8, 2026
dfc4761
UPSTREAM: <carry>: config watchnamespace cases
kuiwang02 Jan 6, 2026
b566620
UPSTREAM: <carry>: enhance ocp-79770
Xia-Zhao-rh Jan 26, 2026
51bda22
UPSTREAM: <carry>: upgrade version support case
kuiwang02 Jan 28, 2026
85eabc5
UPSTREAM: <carry>: Remove installed condition check from auth preflig…
Jan 30, 2026
bd521d4
UPSTREAM: <carry>: Add openshift/api dependency
Jan 30, 2026
b8e888e
UPSTREAM: <carry>: Add boxcutter specific preflight auth test
Jan 30, 2026
d7880f8
UPSTREAM: <carry>: adjust watchnamespace case based on change
kuiwang02 Feb 2, 2026
0e500b9
UPSTREAM: <carry>: fix(ote): Use as operator-controller dep from root…
camilamacedo86 Feb 3, 2026
0c27b41
UPSTREAM: <carry>: add 83979 automation
bandrade Feb 2, 2026
fdc1b9d
UPSTREAM: <carry>: add 85889 automation
bandrade Feb 2, 2026
5ef9e29
UPSTREAM: <carry>: Update test-operator startup script to fix pod pro…
Feb 4, 2026
6518753
UPSTREAM: <carry>: Fix up own-namespace invalid configuration test
Feb 7, 2026
828dc2b
UPSTREAM: <drop>: go mod vendor
Feb 24, 2026
2caf79b
UPSTREAM: <drop>: remove upstream GitHub configuration
Feb 24, 2026
08460f4
UPSTREAM: <drop>: configure the commit-checker
Feb 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/operator-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,9 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl
}
ceReconciler.ReconcileSteps = []controllers.ReconcileStepFunc{
controllers.HandleFinalizers(c.finalizers),
controllers.ValidateClusterExtension(
controllers.ServiceAccountValidator(coreClient),
),
controllers.MigrateStorage(storageMigrator),
controllers.RetrieveRevisionStates(revisionStatesGetter),
controllers.ResolveBundle(c.resolver, c.mgr.GetClient()),
Expand Down Expand Up @@ -747,6 +750,9 @@ func (c *helmReconcilerConfigurator) Configure(ceReconciler *controllers.Cluster
revisionStatesGetter := &controllers.HelmRevisionStatesGetter{ActionClientGetter: acg}
ceReconciler.ReconcileSteps = []controllers.ReconcileStepFunc{
controllers.HandleFinalizers(c.finalizers),
controllers.ValidateClusterExtension(
controllers.ServiceAccountValidator(coreClient),
),
controllers.RetrieveRevisionStates(revisionStatesGetter),
controllers.ResolveBundle(c.resolver, c.mgr.GetClient()),
controllers.UnpackBundle(c.imagePuller, c.imageCache),
Expand Down
2 changes: 1 addition & 1 deletion commitchecker.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
expectedMergeBase: 1ef820f0ca56126586fca2dc7a422c71edd7deef
expectedMergeBase: ddf921fee666b42a8f3c75c505f3174635041e9c
upstreamBranch: main
upstreamOrg: operator-framework
upstreamRepo: operator-controller
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ rules:
- serviceaccounts/token
verbs:
- create
- apiGroups:
- ""
resources:
- serviceaccounts
verbs:
- get
- apiGroups:
- apiextensions.k8s.io
resources:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -767,60 +768,178 @@ func TestClusterExtensionBoxcutterApplierFailsDoesNotLeakDeprecationErrors(t *te
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{}))
}

func TestClusterExtensionServiceAccountNotFound(t *testing.T) {
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{
// Create a different ServiceAccount to ensure "test-sa" is not found.
serviceAccountValidatorWithFakeClient(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "not-test-sa",
Namespace: "test-ns",
},
}),
},
expectError: true,
errorMessageIncludes: `service account "test-sa" not found in namespace "test-ns"`,
},
{
name: "service account found",
validators: []controllers.ClusterExtensionValidator{
serviceAccountValidatorWithFakeClient(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "test-sa",
Namespace: "test-ns",
},
}),
},
expectError: false,
},
}

require.NoError(t, cl.Create(ctx, clusterExtension))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()

t.Log("When reconciling the cluster extension")
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey})
cl, reconciler := newClientAndReconciler(t, func(d *deps) {
d.RevisionStatesGetter = &MockRevisionStatesGetter{
RevisionStates: &controllers.RevisionStates{},
}
d.Validators = tt.validators
})

require.Equal(t, ctrl.Result{}, res)
require.Error(t, err)
var saErr *authentication.ServiceAccountNotFoundError
require.ErrorAs(t, err, &saErr)
t.Log("By fetching updated cluster extension after reconcile")
require.NoError(t, cl.Get(ctx, extKey, clusterExtension))
extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))}

t.Log("By checking the status conditions")
installedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled)
require.NotNil(t, installedCond)
require.Equal(t, metav1.ConditionUnknown, installedCond.Status)
require.Contains(t, installedCond.Message, fmt.Sprintf("service account %q not found in namespace %q: unable to authenticate with the Kubernetes cluster.",
"missing-sa", "default"))
clusterExtension := &ocv1.ClusterExtension{
ObjectMeta: metav1.ObjectMeta{Name: extKey.Name},
Spec: ocv1.ClusterExtensionSpec{
Source: ocv1.SourceConfig{
SourceType: "Catalog",
Catalog: &ocv1.CatalogFilter{
PackageName: "test-package",
},
},
Namespace: "test-ns",
ServiceAccount: ocv1.ServiceAccountReference{
Name: "test-sa",
},
},
}

progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing)
require.NotNil(t, progressingCond)
require.Equal(t, metav1.ConditionTrue, progressingCond.Status)
require.Equal(t, ocv1.ReasonRetrying, progressingCond.Reason)
require.Contains(t, progressingCond.Message, "installation cannot proceed due to missing ServiceAccount")
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{}))
require.NoError(t, cl.Create(ctx, clusterExtension))

t.Log("When reconciling the cluster extension")
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey})
require.Equal(t, ctrl.Result{}, res)
if tt.expectError {
require.Error(t, err)
t.Log("By fetching updated cluster extension after reconcile")
require.NoError(t, cl.Get(ctx, extKey, clusterExtension))

t.Log("By checking the status conditions")
installedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeInstalled)
require.NotNil(t, installedCond)
require.Equal(t, metav1.ConditionUnknown, installedCond.Status)
require.Contains(t, installedCond.Message, "operation cannot proceed due to the following validation error(s)")
require.Contains(t, installedCond.Message, tt.errorMessageIncludes)

progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing)
require.NotNil(t, progressingCond)
require.Equal(t, metav1.ConditionTrue, progressingCond.Status)
require.Equal(t, ocv1.ReasonRetrying, progressingCond.Reason)
require.Contains(t, progressingCond.Message, "operation cannot proceed due to the following validation error(s)")
require.Contains(t, progressingCond.Message, tt.errorMessageIncludes)
} else {
require.NoError(t, err)
require.NoError(t, cl.Get(ctx, extKey, clusterExtension))
require.Empty(t, clusterExtension.Status.Conditions)
}
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1.ClusterExtension{}))
})
}
}

func TestClusterExtensionApplierFailsWithBundleInstalled(t *testing.T) {
Expand Down Expand Up @@ -2878,3 +2997,10 @@ func TestCheckCatalogsExist(t *testing.T) {
require.False(t, exists)
})
}

func serviceAccountValidatorWithFakeClient(serviceAccount *corev1.ServiceAccount) controllers.ClusterExtensionValidator {
if serviceAccount == nil {
return controllers.ServiceAccountValidator(fake.NewClientset().CoreV1())
}
return controllers.ServiceAccountValidator(fake.NewClientset(serviceAccount).CoreV1())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) {
l := log.FromContext(ctx)

l.V(1).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 conditions with the validation errors
err := fmt.Errorf("operation cannot proceed due to the following validation error(s): %w", errors.Join(validationErrors...))
setInstalledStatusConditionUnknown(ext, err.Error())
setStatusProgressing(ext, err)
return nil, err
}
}

// ServiceAccountValidator returns a validator that checks if the specified
// ServiceAccount exists in the cluster by performing a direct Get call.
func ServiceAccountValidator(saClient corev1client.ServiceAccountsGetter) ClusterExtensionValidator {
return func(ctx context.Context, ext *ocv1.ClusterExtension) error {
_, err := saClient.ServiceAccounts(ext.Spec.Namespace).Get(ctx, ext.Spec.ServiceAccount.Name, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return fmt.Errorf("service account %q not found in namespace %q", ext.Spec.ServiceAccount.Name, ext.Spec.Namespace)
}
return fmt.Errorf("error getting service account: %w", err)
}
return nil
}
}

func RetrieveRevisionStates(r RevisionStatesGetter) ReconcileStepFunc {
return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) {
l := log.FromContext(ctx)
l.Info("getting installed bundle")
revisionStates, err := r.GetRevisionStates(ctx, ext)
if err != nil {
setInstallStatus(ext, nil)
var saerr *authentication.ServiceAccountNotFoundError
if errors.As(err, &saerr) {
setInstalledStatusConditionUnknown(ext, saerr.Error())
setStatusProgressing(ext, errors.New("installation cannot proceed due to missing ServiceAccount"))
return nil, err
}
setInstalledStatusConditionUnknown(ext, err.Error())
setStatusProgressing(ext, errors.New("retrying to get installed bundle"))
return nil, err
Expand Down
Loading