diff --git a/internal/authentication/tokengetter.go b/internal/authentication/tokengetter.go index 3fec56f37..868df0e08 100644 --- a/internal/authentication/tokengetter.go +++ b/internal/authentication/tokengetter.go @@ -2,10 +2,12 @@ package authentication import ( "context" + "fmt" "sync" "time" authenticationv1 "k8s.io/api/authentication/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" @@ -19,6 +21,21 @@ type TokenGetter struct { mu sync.RWMutex } +type ServiceAccountNotFoundError struct { + ServiceAccountName string // The name of the missing ServiceAccount. + ServiceAccountNamespace string // The namespace where the ServiceAccount should exist + Err error // The underlying error +} + +func (e *ServiceAccountNotFoundError) Unwrap() error { + return e.Err +} + +// Error implements the error interface for ServiceAccountNotFoundError. +func (e *ServiceAccountNotFoundError) Error() string { + return fmt.Sprintf("service account \"%s\" not found in namespace \"%s\": unable to authenticate with the Kubernetes cluster.", e.ServiceAccountName, e.ServiceAccountNamespace) +} + type TokenGetterOption func(*TokenGetter) const ( @@ -86,6 +103,9 @@ func (t *TokenGetter) getToken(ctx context.Context, key types.NamespacedName) (* Spec: authenticationv1.TokenRequestSpec{ExpirationSeconds: ptr.To(int64(t.expirationDuration / time.Second))}, }, metav1.CreateOptions{}) if err != nil { + if errors.IsNotFound(err) { + return nil, &ServiceAccountNotFoundError{ServiceAccountName: key.Name, ServiceAccountNamespace: key.Namespace} + } return nil, err } return &req.Status, nil diff --git a/internal/authentication/tokengetter_test.go b/internal/authentication/tokengetter_test.go index b9553cac3..663e95f1b 100644 --- a/internal/authentication/tokengetter_test.go +++ b/internal/authentication/tokengetter_test.go @@ -72,13 +72,15 @@ func TestTokenGetterGet(t *testing.T) { "test-namespace-3", "test-token-3", "failed to get token"}, {"Testing error when getting token from fake client", "test-service-account-4", "test-namespace-4", "error when fetching token", "error when fetching token"}, + {"Testing service account not found", "missing-sa", + "test-namespace-5", "", "service account \"missing-sa\" not found in namespace \"test-namespace-5\": unable to authenticate with the Kubernetes cluster."}, } for _, tc := range tests { got, err := tg.Get(context.Background(), types.NamespacedName{Namespace: tc.namespace, Name: tc.serviceAccountName}) if err != nil { t.Logf("%s: expected: %v, got: %v", tc.testName, tc.want, err) - assert.EqualError(t, err, tc.errorMsg) + assert.EqualError(t, err, tc.errorMsg, "Error message should match expected output") } else { t.Logf("%s: expected: %v, got: %v", tc.testName, tc.want, got) assert.Equal(t, tc.want, got, tc.errorMsg) diff --git a/internal/controllers/clusterextension_controller.go b/internal/controllers/clusterextension_controller.go index 66c61de6f..9353790db 100644 --- a/internal/controllers/clusterextension_controller.go +++ b/internal/controllers/clusterextension_controller.go @@ -50,6 +50,7 @@ import ( ocv1 "github.com/operator-framework/operator-controller/api/v1" catalogd "github.com/operator-framework/operator-controller/catalogd/api/v1" + "github.com/operator-framework/operator-controller/internal/authentication" "github.com/operator-framework/operator-controller/internal/bundleutil" "github.com/operator-framework/operator-controller/internal/conditionsets" "github.com/operator-framework/operator-controller/internal/contentmanager" @@ -206,6 +207,12 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1.Cl installedBundle, err := r.InstalledBundleGetter.GetInstalledBundle(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 ctrl.Result{}, err + } setInstalledStatusConditionUnknown(ext, err.Error()) setStatusProgressing(ext, errors.New("retrying to get installed bundle")) return ctrl.Result{}, err diff --git a/test/e2e/cluster_extension_install_test.go b/test/e2e/cluster_extension_install_test.go index 4c05df8b4..7d5906b45 100644 --- a/test/e2e/cluster_extension_install_test.go +++ b/test/e2e/cluster_extension_install_test.go @@ -265,7 +265,9 @@ func testCleanup(t *testing.T, cat *catalogd.ClusterCatalog, clusterExtension *o return errors.IsNotFound(err) }, pollDuration, pollInterval) - ensureNoExtensionResources(t, clusterExtension.Name) + if clusterExtension != nil { + ensureNoExtensionResources(t, clusterExtension.Name) + } t.Logf("By deleting Namespace %q", ns.Name) require.NoError(t, c.Delete(context.Background(), ns))