diff --git a/cmd/catalogd/main.go b/cmd/catalogd/main.go index 9499a7006..ec7c4946f 100644 --- a/cmd/catalogd/main.go +++ b/cmd/catalogd/main.go @@ -30,9 +30,6 @@ import ( "github.com/containers/image/v5/types" "github.com/spf13/cobra" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/fields" - k8slabels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" k8stypes "k8s.io/apimachinery/pkg/types" apimachineryrand "k8s.io/apimachinery/pkg/util/rand" @@ -61,8 +58,11 @@ import ( "github.com/operator-framework/operator-controller/internal/catalogd/serverutil" "github.com/operator-framework/operator-controller/internal/catalogd/storage" "github.com/operator-framework/operator-controller/internal/catalogd/webhook" + sharedcontrollers "github.com/operator-framework/operator-controller/internal/shared/controllers" fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs" imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image" + "github.com/operator-framework/operator-controller/internal/shared/util/pullsecretcache" + sautil "github.com/operator-framework/operator-controller/internal/shared/util/sa" "github.com/operator-framework/operator-controller/internal/shared/version" ) @@ -246,17 +246,19 @@ func run(ctx context.Context) error { cacheOptions := crcache.Options{ ByObject: map[client.Object]crcache.ByObject{}, } - if cfg.globalPullSecretKey != nil { - cacheOptions.ByObject[&corev1.Secret{}] = crcache.ByObject{ - Namespaces: map[string]crcache.Config{ - cfg.globalPullSecretKey.Namespace: { - LabelSelector: k8slabels.Everything(), - FieldSelector: fields.SelectorFromSet(map[string]string{ - "metadata.name": cfg.globalPullSecretKey.Name, - }), - }, - }, - } + + saKey, err := sautil.GetServiceAccount() + if err != nil { + setupLog.Error(err, "Failed to extract serviceaccount from JWT") + return err + } + setupLog.Info("Successfully extracted serviceaccount from JWT", "serviceaccount", + fmt.Sprintf("%s/%s", saKey.Namespace, saKey.Name)) + + err = pullsecretcache.SetupPullSecretCache(&cacheOptions, cfg.globalPullSecretKey, saKey) + if err != nil { + setupLog.Error(err, "Unable to setup pull-secret cache") + return err } // Create manager @@ -312,7 +314,7 @@ func run(ctx context.Context) error { DockerCertPath: cfg.pullCasDir, OCICertPath: cfg.pullCasDir, } - if _, err := os.Stat(authFilePath); err == nil && cfg.globalPullSecretKey != nil { + if _, err := os.Stat(authFilePath); err == nil { logger.Info("using available authentication information for pulling image") srcContext.AuthFilePath = authFilePath } else if os.IsNotExist(err) { @@ -370,17 +372,16 @@ func run(ctx context.Context) error { return err } - if cfg.globalPullSecretKey != nil { - setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret) - err := (&corecontrollers.PullSecretReconciler{ - Client: mgr.GetClient(), - AuthFilePath: authFilePath, - SecretKey: *cfg.globalPullSecretKey, - }).SetupWithManager(mgr) - if err != nil { - setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer") - return err - } + setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret) + err = (&sharedcontrollers.PullSecretReconciler{ + Client: mgr.GetClient(), + AuthFilePath: authFilePath, + SecretKey: cfg.globalPullSecretKey, + ServiceAccountKey: saKey, + }).SetupWithManager(mgr) + if err != nil { + setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer") + return err } //+kubebuilder:scaffold:builder diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 612547248..d426793d4 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -30,10 +30,8 @@ import ( "github.com/containers/image/v5/types" "github.com/spf13/cobra" - corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" - "k8s.io/apimachinery/pkg/fields" k8slabels "k8s.io/apimachinery/pkg/labels" k8stypes "k8s.io/apimachinery/pkg/types" apimachineryrand "k8s.io/apimachinery/pkg/util/rand" @@ -71,9 +69,12 @@ import ( "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/certproviders" "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1" "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" + sharedcontrollers "github.com/operator-framework/operator-controller/internal/shared/controllers" fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs" httputil "github.com/operator-framework/operator-controller/internal/shared/util/http" imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image" + "github.com/operator-framework/operator-controller/internal/shared/util/pullsecretcache" + sautil "github.com/operator-framework/operator-controller/internal/shared/util/sa" "github.com/operator-framework/operator-controller/internal/shared/version" ) @@ -217,17 +218,19 @@ func run() error { }, DefaultLabelSelector: k8slabels.Nothing(), } - if globalPullSecretKey != nil { - cacheOptions.ByObject[&corev1.Secret{}] = crcache.ByObject{ - Namespaces: map[string]crcache.Config{ - globalPullSecretKey.Namespace: { - LabelSelector: k8slabels.Everything(), - FieldSelector: fields.SelectorFromSet(map[string]string{ - "metadata.name": globalPullSecretKey.Name, - }), - }, - }, - } + + saKey, err := sautil.GetServiceAccount() + if err != nil { + setupLog.Error(err, "Failed to extract serviceaccount from JWT") + return err + } + setupLog.Info("Successfully extracted serviceaccount from JWT", "serviceaccount", + fmt.Sprintf("%s/%s", saKey.Namespace, saKey.Name)) + + err = pullsecretcache.SetupPullSecretCache(&cacheOptions, globalPullSecretKey, saKey) + if err != nil { + setupLog.Error(err, "Unable to setup pull-secret cache") + return err } metricsServerOptions := server.Options{} @@ -360,7 +363,7 @@ func run() error { OCICertPath: cfg.pullCasDir, } logger := log.FromContext(ctx) - if _, err := os.Stat(authFilePath); err == nil && globalPullSecretKey != nil { + if _, err := os.Stat(authFilePath); err == nil { logger.Info("using available authentication information for pulling image") srcContext.AuthFilePath = authFilePath } else if os.IsNotExist(err) { @@ -482,17 +485,16 @@ func run() error { return err } - if globalPullSecretKey != nil { - setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret) - err := (&controllers.PullSecretReconciler{ - Client: mgr.GetClient(), - AuthFilePath: authFilePath, - SecretKey: *globalPullSecretKey, - }).SetupWithManager(mgr) - if err != nil { - setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer") - return err - } + setupLog.Info("creating SecretSyncer controller for watching secret", "Secret", cfg.globalPullSecret) + err = (&sharedcontrollers.PullSecretReconciler{ + Client: mgr.GetClient(), + AuthFilePath: authFilePath, + SecretKey: globalPullSecretKey, + ServiceAccountKey: saKey, + }).SetupWithManager(mgr) + if err != nil { + setupLog.Error(err, "unable to create controller", "controller", "SecretSyncer") + return err } //+kubebuilder:scaffold:builder diff --git a/config/base/catalogd/rbac/role.yaml b/config/base/catalogd/rbac/role.yaml index 40f4095c6..0b15af0c6 100644 --- a/config/base/catalogd/rbac/role.yaml +++ b/config/base/catalogd/rbac/role.yaml @@ -30,3 +30,19 @@ rules: - get - patch - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: manager-role + namespace: system +rules: +- apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch diff --git a/config/base/catalogd/rbac/role_binding.yaml b/config/base/catalogd/rbac/role_binding.yaml index a618c0e47..41dc229bc 100644 --- a/config/base/catalogd/rbac/role_binding.yaml +++ b/config/base/catalogd/rbac/role_binding.yaml @@ -13,3 +13,20 @@ subjects: - kind: ServiceAccount name: controller-manager namespace: system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/part-of: olm + app.kubernetes.io/name: catalogd + name: manager-rolebinding + namespace: system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: manager-role +subjects: + - kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/config/base/operator-controller/rbac/role.yaml b/config/base/operator-controller/rbac/role.yaml index be89deec1..d18eb4c6c 100644 --- a/config/base/operator-controller/rbac/role.yaml +++ b/config/base/operator-controller/rbac/role.yaml @@ -77,3 +77,11 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch diff --git a/go.mod b/go.mod index 6cbe9dfef..7cb2e43a4 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,10 @@ require ( github.com/containers/image/v5 v5.35.0 github.com/fsnotify/fsnotify v1.9.0 github.com/go-logr/logr v1.4.3 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.3 + github.com/google/renameio/v2 v2.0.0 github.com/gorilla/handlers v1.5.2 github.com/klauspost/compress v1.18.0 github.com/opencontainers/go-digest v1.0.0 diff --git a/go.sum b/go.sum index bdfddc181..22805e858 100644 --- a/go.sum +++ b/go.sum @@ -209,6 +209,8 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -253,6 +255,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4= github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= +github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/internal/catalogd/controllers/core/clustercatalog_controller.go b/internal/catalogd/controllers/core/clustercatalog_controller.go index ce1636266..7a5db11f0 100644 --- a/internal/catalogd/controllers/core/clustercatalog_controller.go +++ b/internal/catalogd/controllers/core/clustercatalog_controller.go @@ -79,6 +79,8 @@ type storedCatalogData struct { //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs/status,verbs=get;update;patch //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clustercatalogs/finalizers,verbs=update +//+kubebuilder:rbac:namespace=system,groups=core,resources=secrets,verbs=get;list;watch +//+kubebuilder:rbac:namespace=system,groups=core,resources=serviceaccounts,verbs=get;list;watch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. diff --git a/internal/catalogd/controllers/core/pull_secret_controller.go b/internal/catalogd/controllers/core/pull_secret_controller.go deleted file mode 100644 index 810581047..000000000 --- a/internal/catalogd/controllers/core/pull_secret_controller.go +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package core - -import ( - "context" - "fmt" - "os" - - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" -) - -// PullSecretReconciler reconciles a specific Secret object -// that contains global pull secrets for pulling Catalog images -type PullSecretReconciler struct { - client.Client - SecretKey types.NamespacedName - AuthFilePath string -} - -func (r *PullSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx) - if req.Name != r.SecretKey.Name || req.Namespace != r.SecretKey.Namespace { - logger.Error(fmt.Errorf("received unexpected request for Secret %v/%v", req.Namespace, req.Name), "reconciliation error") - return ctrl.Result{}, nil - } - - secret := &corev1.Secret{} - err := r.Get(ctx, req.NamespacedName, secret) - if err != nil { - if apierrors.IsNotFound(err) { - logger.Info("secret not found") - return r.deleteSecretFile(logger) - } - logger.Error(err, "failed to get Secret") - return ctrl.Result{}, err - } - - return r.writeSecretToFile(logger, secret) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *PullSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { - _, err := ctrl.NewControllerManagedBy(mgr). - For(&corev1.Secret{}). - Named("catalogd-pull-secret-controller"). - WithEventFilter(newSecretPredicate(r.SecretKey)). - Build(r) - - return err -} - -func newSecretPredicate(key types.NamespacedName) predicate.Predicate { - return predicate.NewPredicateFuncs(func(obj client.Object) bool { - return obj.GetName() == key.Name && obj.GetNamespace() == key.Namespace - }) -} - -// writeSecretToFile writes the secret data to the specified file -func (r *PullSecretReconciler) writeSecretToFile(logger logr.Logger, secret *corev1.Secret) (ctrl.Result, error) { - // image registry secrets are always stored with the key .dockerconfigjson - // ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials - dockerConfigJSON, ok := secret.Data[".dockerconfigjson"] - if !ok { - logger.Error(fmt.Errorf("expected secret.Data key not found"), "expected secret Data to contain key .dockerconfigjson") - return ctrl.Result{}, nil - } - // expected format for auth.json - // https://github.com/containers/image/blob/main/docs/containers-auth.json.5.md - err := os.WriteFile(r.AuthFilePath, dockerConfigJSON, 0600) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to write secret data to file: %w", err) - } - logger.Info("saved global pull secret data locally") - return ctrl.Result{}, nil -} - -// deleteSecretFile deletes the auth file if the secret is deleted -func (r *PullSecretReconciler) deleteSecretFile(logger logr.Logger) (ctrl.Result, error) { - logger.Info("deleting local auth file", "file", r.AuthFilePath) - if err := os.Remove(r.AuthFilePath); err != nil { - if os.IsNotExist(err) { - logger.Info("auth file does not exist, nothing to delete") - return ctrl.Result{}, nil - } - return ctrl.Result{}, fmt.Errorf("failed to delete secret file: %w", err) - } - logger.Info("auth file deleted successfully") - return ctrl.Result{}, nil -} diff --git a/internal/catalogd/controllers/core/pull_secret_controller_test.go b/internal/catalogd/controllers/core/pull_secret_controller_test.go deleted file mode 100644 index 8b91da340..000000000 --- a/internal/catalogd/controllers/core/pull_secret_controller_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package core - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -func TestSecretSyncerReconciler(t *testing.T) { - secretData := []byte(`{"auths":{"exampleRegistry": "exampledata"}}`) - authFileName := "test-auth.json" - for _, tt := range []struct { - name string - secret *corev1.Secret - addSecret bool - wantErr string - fileShouldExistBefore bool - fileShouldExistAfter bool - }{ - { - name: "secret exists, content gets saved to authFile", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", - Namespace: "test-secret-namespace", - }, - Data: map[string][]byte{ - ".dockerconfigjson": secretData, - }, - }, - addSecret: true, - fileShouldExistBefore: false, - fileShouldExistAfter: true, - }, - { - name: "secret does not exist, file exists previously, file should get deleted", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", - Namespace: "test-secret-namespace", - }, - Data: map[string][]byte{ - ".dockerconfigjson": secretData, - }, - }, - addSecret: false, - fileShouldExistBefore: true, - fileShouldExistAfter: false, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - tempAuthFile := filepath.Join(t.TempDir(), authFileName) - clientBuilder := fake.NewClientBuilder() - if tt.addSecret { - clientBuilder = clientBuilder.WithObjects(tt.secret) - } - cl := clientBuilder.Build() - - secretKey := types.NamespacedName{Namespace: tt.secret.Namespace, Name: tt.secret.Name} - r := &PullSecretReconciler{ - Client: cl, - SecretKey: secretKey, - AuthFilePath: tempAuthFile, - } - if tt.fileShouldExistBefore { - err := os.WriteFile(tempAuthFile, secretData, 0600) - require.NoError(t, err) - } - res, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: secretKey}) - if tt.wantErr == "" { - require.NoError(t, err) - } else { - require.ErrorContains(t, err, tt.wantErr) - } - require.Equal(t, ctrl.Result{}, res) - - if tt.fileShouldExistAfter { - _, err := os.Stat(tempAuthFile) - require.NoError(t, err) - } else { - _, err := os.Stat(tempAuthFile) - require.True(t, os.IsNotExist(err)) - } - }) - } -} diff --git a/internal/operator-controller/controllers/clusterextension_controller.go b/internal/operator-controller/controllers/clusterextension_controller.go index 9a79e8c75..7d268df05 100644 --- a/internal/operator-controller/controllers/clusterextension_controller.go +++ b/internal/operator-controller/controllers/clusterextension_controller.go @@ -95,6 +95,7 @@ type InstalledBundleGetter interface { //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions/finalizers,verbs=update //+kubebuilder:rbac:namespace=system,groups=core,resources=secrets,verbs=create;update;patch;delete;deletecollection;get;list;watch //+kubebuilder:rbac:groups=core,resources=serviceaccounts/token,verbs=create +//+kubebuilder:rbac:namespace=system,groups=core,resources=serviceaccounts,verbs=get;list;watch //+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get //+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings;roles;rolebindings,verbs=list;watch diff --git a/internal/operator-controller/controllers/pull_secret_controller.go b/internal/operator-controller/controllers/pull_secret_controller.go deleted file mode 100644 index d73ccddb3..000000000 --- a/internal/operator-controller/controllers/pull_secret_controller.go +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "fmt" - "os" - - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" -) - -// PullSecretReconciler reconciles a specific Secret object -// that contains global pull secrets for pulling bundle images -type PullSecretReconciler struct { - client.Client - SecretKey types.NamespacedName - AuthFilePath string -} - -func (r *PullSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx) - if req.Name != r.SecretKey.Name || req.Namespace != r.SecretKey.Namespace { - logger.Error(fmt.Errorf("received unexpected request for Secret %v/%v", req.Namespace, req.Name), "reconciliation error") - return ctrl.Result{}, nil - } - - secret := &corev1.Secret{} - err := r.Get(ctx, req.NamespacedName, secret) - if err != nil { - if apierrors.IsNotFound(err) { - logger.Info("secret not found") - return r.deleteSecretFile(logger) - } - logger.Error(err, "failed to get Secret") - return ctrl.Result{}, err - } - - return r.writeSecretToFile(logger, secret) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *PullSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { - _, err := ctrl.NewControllerManagedBy(mgr). - Named("controller-operator-pull-secret-controller"). - For(&corev1.Secret{}). - WithEventFilter(newSecretPredicate(r.SecretKey)). - Build(r) - - return err -} - -func newSecretPredicate(key types.NamespacedName) predicate.Predicate { - return predicate.NewPredicateFuncs(func(obj client.Object) bool { - return obj.GetName() == key.Name && obj.GetNamespace() == key.Namespace - }) -} - -// writeSecretToFile writes the secret data to the specified file -func (r *PullSecretReconciler) writeSecretToFile(logger logr.Logger, secret *corev1.Secret) (ctrl.Result, error) { - // image registry secrets are always stored with the key .dockerconfigjson - // ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials - dockerConfigJSON, ok := secret.Data[".dockerconfigjson"] - if !ok { - logger.Error(fmt.Errorf("expected secret.Data key not found"), "expected secret Data to contain key .dockerconfigjson") - return ctrl.Result{}, nil - } - // expected format for auth.json - // https://github.com/containers/image/blob/main/docs/containers-auth.json.5.md - err := os.WriteFile(r.AuthFilePath, dockerConfigJSON, 0600) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to write secret data to file: %w", err) - } - logger.Info("saved global pull secret data locally") - return ctrl.Result{}, nil -} - -// deleteSecretFile deletes the auth file if the secret is deleted -func (r *PullSecretReconciler) deleteSecretFile(logger logr.Logger) (ctrl.Result, error) { - logger.Info("deleting local auth file", "file", r.AuthFilePath) - if err := os.Remove(r.AuthFilePath); err != nil { - if os.IsNotExist(err) { - logger.Info("auth file does not exist, nothing to delete") - return ctrl.Result{}, nil - } - return ctrl.Result{}, fmt.Errorf("failed to delete secret file: %w", err) - } - logger.Info("auth file deleted successfully") - return ctrl.Result{}, nil -} diff --git a/internal/operator-controller/controllers/pull_secret_controller_test.go b/internal/operator-controller/controllers/pull_secret_controller_test.go deleted file mode 100644 index 4406ffa2f..000000000 --- a/internal/operator-controller/controllers/pull_secret_controller_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package controllers_test - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - "github.com/operator-framework/operator-controller/internal/operator-controller/controllers" - "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" -) - -func TestSecretSyncerReconciler(t *testing.T) { - secretData := []byte(`{"auths":{"exampleRegistry": "exampledata"}}`) - authFileName := "test-auth.json" - for _, tt := range []struct { - name string - secret *corev1.Secret - addSecret bool - wantErr string - fileShouldExistBefore bool - fileShouldExistAfter bool - }{ - { - name: "secret exists, content gets saved to authFile", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", - Namespace: "test-secret-namespace", - }, - Data: map[string][]byte{ - ".dockerconfigjson": secretData, - }, - }, - addSecret: true, - fileShouldExistBefore: false, - fileShouldExistAfter: true, - }, - { - name: "secret does not exist, file exists previously, file should get deleted", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", - Namespace: "test-secret-namespace", - }, - Data: map[string][]byte{ - ".dockerconfigjson": secretData, - }, - }, - addSecret: false, - fileShouldExistBefore: true, - fileShouldExistAfter: false, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - tempAuthFile := filepath.Join(t.TempDir(), authFileName) - clientBuilder := fake.NewClientBuilder().WithScheme(scheme.Scheme) - if tt.addSecret { - clientBuilder = clientBuilder.WithObjects(tt.secret) - } - cl := clientBuilder.Build() - - secretKey := types.NamespacedName{Namespace: tt.secret.Namespace, Name: tt.secret.Name} - r := &controllers.PullSecretReconciler{ - Client: cl, - SecretKey: secretKey, - AuthFilePath: tempAuthFile, - } - if tt.fileShouldExistBefore { - err := os.WriteFile(tempAuthFile, secretData, 0600) - require.NoError(t, err) - } - res, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: secretKey}) - if tt.wantErr == "" { - require.NoError(t, err) - } else { - require.ErrorContains(t, err, tt.wantErr) - } - require.Equal(t, ctrl.Result{}, res) - - if tt.fileShouldExistAfter { - _, err := os.Stat(tempAuthFile) - require.NoError(t, err) - } else { - _, err := os.Stat(tempAuthFile) - require.True(t, os.IsNotExist(err)) - } - }) - } -} diff --git a/internal/shared/controllers/pull_secret_controller.go b/internal/shared/controllers/pull_secret_controller.go new file mode 100644 index 000000000..43a2ceec8 --- /dev/null +++ b/internal/shared/controllers/pull_secret_controller.go @@ -0,0 +1,243 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/go-logr/logr" + "github.com/google/renameio/v2" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// PullSecretReconciler reconciles a specific Secret object +// that contains global pull secrets for pulling Catalog images +type PullSecretReconciler struct { + client.Client + SecretKey *types.NamespacedName + ServiceAccountKey types.NamespacedName + ServiceAccountPullSecrets []types.NamespacedName + AuthFilePath string +} + +func (r *PullSecretReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx).WithName("pull-secret-reconciler") + + logger.Info("starting reconciliation") + defer logger.Info("finishing reconciliation") + + secrets := []*corev1.Secret{} + + if r.SecretKey != nil { + secret, err := r.getSecret(ctx, logger, *r.SecretKey) + if err != nil { + return ctrl.Result{}, err + } + // Add the configured pull secret to the list of secrets + if secret != nil { + secrets = append(secrets, secret) + } + } + + // Grab all the pull secrets from the serviceaccount and add them to the list of secrets + sa := &corev1.ServiceAccount{} + if err := r.Get(ctx, r.ServiceAccountKey, sa); err != nil { //nolint:nestif + if apierrors.IsNotFound(err) { + logger.Info("serviceaccount not found", "pod-sa", logNamespacedName(r.ServiceAccountKey)) + } else { + logger.Error(err, "failed to get serviceaccount", "pod-sa", logNamespacedName(r.ServiceAccountKey)) + return ctrl.Result{}, err + } + } else { + logger.Info("found serviceaccount", "pod-sa", logNamespacedName(r.ServiceAccountKey)) + nn := types.NamespacedName{Namespace: r.ServiceAccountKey.Namespace} + pullSecrets := []types.NamespacedName{} + for _, ips := range sa.ImagePullSecrets { + nn.Name = ips.Name + // This is to update the list of secrets that we are filtering on + // Add all secrets regardless if they exist or not + pullSecrets = append(pullSecrets, nn) + + secret, err := r.getSecret(ctx, logger, nn) + if err != nil { + return ctrl.Result{}, err + } + if secret != nil { + secrets = append(secrets, secret) + } + } + // update list of pull secrets from service account + r.ServiceAccountPullSecrets = pullSecrets + // Log ever-so slightly nicer + names := []string{} + for _, ps := range pullSecrets { + names = append(names, logNamespacedName(ps)) + } + logger.Info("updating list of pull-secrets", "pull-secrets", names) + } + + if len(secrets) == 0 { + return ctrl.Result{}, r.deleteSecretFile(logger) + } + return ctrl.Result{}, r.writeSecretToFile(logger, secrets) +} + +func (r *PullSecretReconciler) getSecret(ctx context.Context, logger logr.Logger, nn types.NamespacedName) (*corev1.Secret, error) { + secret := &corev1.Secret{} + if err := r.Get(ctx, nn, secret); err != nil { + if apierrors.IsNotFound(err) { + logger.Info("pull-secret not found", "pull-secret", logNamespacedName(nn)) + return nil, nil + } + logger.Error(err, "failed to get pull-secret", "pull-secret", logNamespacedName(nn)) + return nil, err + } + logger.Info("found pull-secret", "pull-secret", logNamespacedName(nn)) + return secret, nil +} + +func logNamespacedName(nn types.NamespacedName) string { + return fmt.Sprintf("%s/%s", nn.Namespace, nn.Name) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *PullSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { + _, err := ctrl.NewControllerManagedBy(mgr). + For(&corev1.Secret{}). + Named("pull-secret-controller"). + WithEventFilter(newSecretPredicate(r)). + Build(r) + if err != nil { + return err + } + + _, err = ctrl.NewControllerManagedBy(mgr). + For(&corev1.ServiceAccount{}). + Named("service-account-controller"). + WithEventFilter(newNamespacedPredicate(r.ServiceAccountKey)). + Build(r) + + return err +} + +// Filters based on the global SecretKey, or any pull secret from the serviceaccount +func newSecretPredicate(r *PullSecretReconciler) predicate.Predicate { + return predicate.NewPredicateFuncs(func(obj client.Object) bool { + nn := types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()} + if r.SecretKey != nil && nn == *r.SecretKey { + return true + } + for _, ps := range r.ServiceAccountPullSecrets { + if nn == ps { + return true + } + } + return false + }) +} + +func newNamespacedPredicate(key types.NamespacedName) predicate.Predicate { + return predicate.NewPredicateFuncs(func(obj client.Object) bool { + return obj.GetName() == key.Name && obj.GetNamespace() == key.Namespace + }) +} + +// Golang representation of the docker configuration - either dockerconfigjson or dockercfg formats. +// This allows us to merge the two formats together, regardless of type, and dump it out as a +// dockerconfigjson for use my contaners/images +type dockerConfigJSON struct { + Auths dockerCfg `json:"auths"` +} + +type dockerCfg map[string]authEntries + +type authEntries struct { + Auth string `json:"auth"` + Email string `json:"email,omitempty"` +} + +// writeSecretToFile writes the secret data to the specified file +func (r *PullSecretReconciler) writeSecretToFile(logger logr.Logger, secrets []*corev1.Secret) error { + // image registry secrets are always stored with the key .dockerconfigjson or .dockercfg + // ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials + // expected format for auth.json + // ref: https://github.com/containers/image/blob/main/docs/containers-auth.json.5.md + + jsonData := dockerConfigJSON{} + jsonData.Auths = make(dockerCfg) + + for _, s := range secrets { + if secretData, ok := s.Data[".dockerconfigjson"]; ok { + // process as dockerconfigjson + dcj := &dockerConfigJSON{} + if err := json.Unmarshal(secretData, dcj); err != nil { + return err + } + for n, v := range dcj.Auths { + jsonData.Auths[n] = v + } + continue + } + if secretData, ok := s.Data[".dockercfg"]; ok { + // process as dockercfg, despite being a map, this has to be Unmarshal'd as a pointer + dc := &dockerCfg{} + if err := json.Unmarshal(secretData, dc); err != nil { + return err + } + for n, v := range *dc { + jsonData.Auths[n] = v + } + continue + } + // Ignore the unknown secret + logger.Info("expected secret.Data key not found", "pull-secret", logNamespacedName(types.NamespacedName{Name: s.Name, Namespace: s.Namespace})) + } + + data, err := json.Marshal(jsonData) + if err != nil { + return fmt.Errorf("failed to marshal secret data: %w", err) + } + err = renameio.WriteFile(r.AuthFilePath, data, 0600) + if err != nil { + return fmt.Errorf("failed to write secret data to file: %w", err) + } + logger.Info("saved global pull secret data locally") + return nil +} + +// deleteSecretFile deletes the auth file if the secret is deleted +func (r *PullSecretReconciler) deleteSecretFile(logger logr.Logger) error { + logger.Info("deleting local auth file", "file", r.AuthFilePath) + if err := os.Remove(r.AuthFilePath); err != nil { + if os.IsNotExist(err) { + logger.Info("auth file does not exist, nothing to delete") + return nil + } + return fmt.Errorf("failed to delete secret file: %w", err) + } + logger.Info("auth file deleted successfully") + return nil +} diff --git a/internal/shared/controllers/pull_secret_controller_test.go b/internal/shared/controllers/pull_secret_controller_test.go new file mode 100644 index 000000000..161926755 --- /dev/null +++ b/internal/shared/controllers/pull_secret_controller_test.go @@ -0,0 +1,154 @@ +package controllers + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestSecretSyncerReconciler(t *testing.T) { + secretFullData := []byte(`{"auths":{"exampleRegistry": {"auth": "exampledata"}}}`) + secretPartData := []byte(`{"exampleRegistry": {"auth": "exampledata"}}`) + authFileName := "test-auth.json" + for _, tt := range []struct { + name string + secretKey *types.NamespacedName + sa *corev1.ServiceAccount + secrets []corev1.Secret + wantErr string + fileShouldExistBefore bool + fileShouldExistAfter bool + }{ + { + name: "secret exists, dockerconfigjson content gets saved to authFile", + secretKey: &types.NamespacedName{Namespace: "test-secret-namespace", Name: "test-secret"}, + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "test-secret-namespace", + }, + Data: map[string][]byte{ + ".dockerconfigjson": secretFullData, + }, + }, + }, + fileShouldExistBefore: false, + fileShouldExistAfter: true, + }, + { + name: "secret exists, dockercfg content gets saved to authFile", + secretKey: &types.NamespacedName{Namespace: "test-secret-namespace", Name: "test-secret"}, + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "test-secret-namespace", + }, + Data: map[string][]byte{ + ".dockercfg": secretPartData, + }, + }, + }, + fileShouldExistBefore: false, + fileShouldExistAfter: true, + }, + { + name: "secret does not exist, file exists previously, file should get deleted", + secretKey: &types.NamespacedName{Namespace: "test-secret-namespace", Name: "test-secret"}, + fileShouldExistBefore: true, + fileShouldExistAfter: false, + }, + { + name: "serviceaccount secrets, both dockerconfigjson and dockercfg content gets saved to authFile", + sa: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-sa", + Namespace: "test-secret-namespace", + }, + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "test-secret1"}, + {Name: "test-secret2"}, + }, + }, + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret1", + Namespace: "test-secret-namespace", + }, + Data: map[string][]byte{ + ".dockerconfigjson": secretFullData, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret2", + Namespace: "test-secret-namespace", + }, + Data: map[string][]byte{ + ".dockerconfigjson": secretFullData, + }, + }, + }, + fileShouldExistBefore: false, + fileShouldExistAfter: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + tempAuthFile := filepath.Join(t.TempDir(), authFileName) + clientBuilder := fake.NewClientBuilder() + for _, ps := range tt.secrets { + clientBuilder = clientBuilder.WithObjects(ps.DeepCopy()) + } + if tt.sa != nil { + clientBuilder = clientBuilder.WithObjects(tt.sa) + } + cl := clientBuilder.Build() + + var triggerKey types.NamespacedName + if tt.secretKey != nil { + triggerKey = *tt.secretKey + } + var saKey types.NamespacedName + if tt.sa != nil { + saKey = types.NamespacedName{Namespace: tt.sa.Namespace, Name: tt.sa.Name} + triggerKey = saKey + } + r := &PullSecretReconciler{ + Client: cl, + SecretKey: tt.secretKey, + ServiceAccountKey: saKey, + AuthFilePath: tempAuthFile, + } + if tt.fileShouldExistBefore { + err := os.WriteFile(tempAuthFile, secretFullData, 0600) + require.NoError(t, err) + } + res, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: triggerKey}) + if tt.wantErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + require.Equal(t, ctrl.Result{}, res) + + if tt.fileShouldExistAfter { + _, err := os.Stat(tempAuthFile) + require.NoError(t, err) + } else { + _, err := os.Stat(tempAuthFile) + require.True(t, os.IsNotExist(err)) + } + }) + } +} diff --git a/internal/shared/util/pullsecretcache/pullsecretcache.go b/internal/shared/util/pullsecretcache/pullsecretcache.go new file mode 100644 index 000000000..910b4b50b --- /dev/null +++ b/internal/shared/util/pullsecretcache/pullsecretcache.go @@ -0,0 +1,40 @@ +package pullsecretcache + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/cache" +) + +func SetupPullSecretCache(cacheOptions *cache.Options, globalPullSecretKey *types.NamespacedName, saKey types.NamespacedName) error { + cacheOptions.ByObject[&corev1.ServiceAccount{}] = cache.ByObject{ + Namespaces: map[string]cache.Config{ + saKey.Namespace: { + LabelSelector: labels.Everything(), + FieldSelector: fields.SelectorFromSet(map[string]string{ + "metadata.name": saKey.Name, + }), + }, + }, + } + + secretCache := cache.ByObject{} + secretCache.Namespaces = make(map[string]cache.Config, 2) + secretCache.Namespaces[saKey.Namespace] = cache.Config{ + LabelSelector: labels.Everything(), + FieldSelector: fields.Everything(), + } + if globalPullSecretKey != nil && globalPullSecretKey.Namespace != saKey.Namespace { + secretCache.Namespaces[globalPullSecretKey.Namespace] = cache.Config{ + LabelSelector: labels.Everything(), + FieldSelector: fields.SelectorFromSet(map[string]string{ + "metadata.name": globalPullSecretKey.Name, + }), + } + } + cacheOptions.ByObject[&corev1.Secret{}] = secretCache + + return nil +} diff --git a/internal/shared/util/sa/serviceaccount.go b/internal/shared/util/sa/serviceaccount.go new file mode 100644 index 000000000..f668c859e --- /dev/null +++ b/internal/shared/util/sa/serviceaccount.go @@ -0,0 +1,51 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sa + +import ( + "fmt" + "os" + "strings" + + "github.com/golang-jwt/jwt/v5" + k8stypes "k8s.io/apimachinery/pkg/types" +) + +// Returns nameaspce/serviceaccount name +func GetServiceAccount() (k8stypes.NamespacedName, error) { + return getServiceAccountInternal(os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")) +} + +func getServiceAccountInternal(data []byte, err error) (k8stypes.NamespacedName, error) { + if err != nil { + return k8stypes.NamespacedName{}, err + } + // Not verifying the token, we just want to extract the subject + token, _, err := jwt.NewParser([]jwt.ParserOption{}...).ParseUnverified(string(data), jwt.MapClaims{}) + if err != nil { + return k8stypes.NamespacedName{}, err + } + subject, err := token.Claims.GetSubject() + if err != nil { + return k8stypes.NamespacedName{}, err + } + subjects := strings.Split(subject, ":") + if len(subjects) != 4 || subjects[2] == "" || subjects[3] == "" { + return k8stypes.NamespacedName{}, fmt.Errorf("badly formatted subject: %s", subject) + } + return k8stypes.NamespacedName{Namespace: subjects[2], Name: subjects[3]}, nil +} diff --git a/internal/shared/util/sa/serviceaccount_test.go b/internal/shared/util/sa/serviceaccount_test.go new file mode 100644 index 000000000..b18663e66 --- /dev/null +++ b/internal/shared/util/sa/serviceaccount_test.go @@ -0,0 +1,49 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sa + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + // taken from a kind run + goodSa = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdyM3VIbkJ0VlRQVy1uWWlsSVFCV2pfQmdTS0RIdjZHNDBVT1hDSVFtZmcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgwNTEwMjAwLCJpYXQiOjE3NDg5NzQyMDAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTQ2OThmZGYtNzg4NC00YzhkLWI5NzctYTg4YThiYmY3ODQxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJvbG12MS1zeXN0ZW0iLCJub2RlIjp7Im5hbWUiOiJvcGVyYXRvci1jb250cm9sbGVyLWUyZS1jb250cm9sLXBsYW5lIiwidWlkIjoiZWY0YjdkNGQtZmUxZi00MThkLWIyZDAtM2ZmYWJmMWQ0ZDI3In0sInBvZCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXItNjU3Njg1ZGNkYy01cTZ0dCIsInVpZCI6IjE4MmFkNTkxLWUzYTktNDMyNC1hMjk4LTg0NzIxY2Q0OTAzYSJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXIiLCJ1aWQiOiI3MDliZTA4OS00OTI1LTQ2NjYtYjA1Ny1iYWMyNmVmYWJjMGIifSwid2FybmFmdGVyIjoxNzQ4OTc3ODA3fSwibmJmIjoxNzQ4OTc0MjAwLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6b2xtdjEtc3lzdGVtOm9wZXJhdG9yLWNvbnRyb2xsZXItY29udHJvbGxlci1tYW5hZ2VyIn0.OjExhuNHdMZjdGwDXM0bWQnJKcfLNpEJ2S47BzlAa560uNw8EwMItlfpG970umQBbVPWhyhUBFimUD5XmXWAlrNvhFwpOLXw2W978Obs1mna5JWcHliC6IkwrOMCh5k9XReQ9-KBdw36QY1G2om77-7mNtPNPg9lg5TQaLuNGrIhX9EC_tucbflXSvB-SA243J_X004W4HkJirt6vVH5FoRg-MDohXm0C4bhTeaXfOtTW6fwsnpomCKso7apu_eOG9E2h8CXXYKhZg4Jrank_Ata8J1lANh06FuxRQK-vwqFrW3_9rscGxweM5CbeicZFOc6MDIuYtgR515YTHPbUA" // notsecret + badSa1 = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdyM3VIbkJ0VlRQVy1uWWlsSVFCV2pfQmdTS0RIdjZHNDBVT1hDSVFtZmcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgwNTEwMjAwLCJpYXQiOjE3NDg5NzQyMDAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNTQ2OThmZGYtNzg4NC00YzhkLWI5NzctYTg4YThiYmY3ODQxIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJvbG12MS1zeXN0ZW0iLCJub2RlIjp7Im5hbWUiOiJvcGVyYXRvci1jb250cm9sbGVyLWUyZS1jb250cm9sLXBsYW5lIiwidWlkIjoiZWY0YjdkNGQtZmUxZi00MThkLWIyZDAtM2ZmYWJmMWQ0ZDI3In0sInBvZCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXItNjU3Njg1ZGNkYy01cTZ0dCIsInVpZCI6IjE4MmFkNTkxLWUzYTktNDMyNC1hMjk4LTg0NzIxY2Q0OTAzYSJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoib3BlcmF0b3ItY29udHJvbGxlci1jb250cm9sbGVyLW1hbmFnZXIiLCJ1aWQiOiI3MDliZTA4OS00OTI1LTQ2NjYtYjA1Ny1iYWMyNmVmYWJjMGIifSwid2FybmFmdGVyIjoxNzQ4OTc3ODA3fSwibmJmIjoxNzQ4OTc0MjAwLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnRzOm9sbXYxLXN5c3RlbSJ9.OjExhuNHdMZjdGwDXM0bWQnJKcfLNpEJ2S47BzlAa560uNw8EwMItlfpG970umQBbVPWhyhUBFimUD5XmXWAlrNvhFwpOLXw2W978Obs1mna5JWcHliC6IkwrOMCh5k9XReQ9-KBdw36QY1G2om77-7mNtPNPg9lg5TQaLuNGrIhX9EC_tucbflXSvB-SA243J_X004W4HkJirt6vVH5FoRg-MDohXm0C4bhTeaXfOtTW6fwsnpomCKso7apu_eOG9E2h8CXXYKhZg4Jrank_Ata8J1lANh06FuxRQK-vwqFrW3_9rscGxweM5CbeicZFOc6MDIuYtgR515YTHPbUA" // notsecret + badSa2 = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdyM3VIbkJ0VlRQVy1uWWlsSVFCV2pfQmdTS0RIdjZHNDBVT1hDSVFtZmcifQ" // notsecret +) + +func TestGetServiceAccount(t *testing.T) { + nn, err := getServiceAccountInternal([]byte(goodSa), nil) + require.NoError(t, err) + require.Equal(t, "olmv1-system", nn.Namespace) + require.Equal(t, "operator-controller-controller-manager", nn.Name) + + _, err = getServiceAccountInternal([]byte{}, fmt.Errorf("this is a test error")) + require.ErrorContains(t, err, "this is a test") + + // Modified the subject to be invalid + _, err = getServiceAccountInternal([]byte(badSa1), nil) + require.ErrorContains(t, err, "badly formatted subject") + + // Only includes a header + _, err = getServiceAccountInternal([]byte(badSa2), nil) + require.ErrorContains(t, err, "token is malformed") +} diff --git a/manifests/standard.yaml b/manifests/standard.yaml index 669684d93..da08382a7 100644 --- a/manifests/standard.yaml +++ b/manifests/standard.yaml @@ -1095,6 +1095,22 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role +metadata: + name: catalogd-manager-role + namespace: olmv1-system +rules: +- apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role metadata: name: operator-controller-leader-election-role namespace: olmv1-system @@ -1150,6 +1166,14 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -1355,6 +1379,23 @@ subjects: --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: catalogd + app.kubernetes.io/part-of: olm + name: catalogd-manager-rolebinding + namespace: olmv1-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: catalogd-manager-role +subjects: +- kind: ServiceAccount + name: catalogd-controller-manager + namespace: olmv1-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding metadata: name: operator-controller-leader-election-rolebinding namespace: olmv1-system