diff --git a/Makefile b/Makefile index e46cc379a..972a84d37 100644 --- a/Makefile +++ b/Makefile @@ -152,7 +152,7 @@ build-push-e2e-catalog: ## Build the testdata catalog used for e2e tests and pus test-e2e: KIND_CLUSTER_NAME := operator-controller-e2e test-e2e: KUSTOMIZE_BUILD_DIR := config/overlays/e2e test-e2e: GO_BUILD_FLAGS := -cover -test-e2e: run image-registry build-push-e2e-catalog registry-load-bundles e2e e2e-coverage kind-clean #HELP Run e2e test suite on local kind cluster +test-e2e: run image-registry build-push-e2e-catalog registry-load-bundles apply-rbac e2e e2e-coverage kind-clean #HELP Run e2e test suite on local kind cluster .PHONY: extension-developer-e2e extension-developer-e2e: KIND_CLUSTER_NAME := operator-controller-ext-dev-e2e #EXHELP Run extension-developer e2e on local kind cluster @@ -193,6 +193,9 @@ registry-load-bundles: ## Load selected e2e testdata container images created in testdata/bundles/registry-v1/build-push-e2e-bundle.sh ${E2E_REGISTRY_NAMESPACE} $(REGISTRY_ROOT)/bundles/registry-v1/prometheus-operator:v1.2.0 prometheus-operator.v1.2.0 prometheus-operator.v1.0.0 testdata/bundles/registry-v1/build-push-e2e-bundle.sh ${E2E_REGISTRY_NAMESPACE} $(REGISTRY_ROOT)/bundles/registry-v1/prometheus-operator:v2.0.0 prometheus-operator.v2.0.0 prometheus-operator.v1.0.0 +apply-rbac: ## Apply RBAC expected when using service account from spec + kubectl apply -f testdata/rbac/prometheus-operator-bundle-rbac.yaml -n default + #SECTION Build ifeq ($(origin VERSION), undefined) @@ -238,7 +241,7 @@ run: docker-build kind-cluster kind-load kind-deploy #HELP Build the operator-co .PHONY: docker-build docker-build: build-linux #EXHELP Build docker image for operator-controller with GOOS=linux and local GOARCH. - $(CONTAINER_RUNTIME) build -t $(IMG) -f Dockerfile ./bin/linux + $(CONTAINER_RUNTIME) build -t $(IMG) -f Dockerfile ./bin/linux --load #SECTION Release ifeq ($(origin ENABLE_RELEASE_PIPELINE), undefined) diff --git a/api/v1alpha1/clusterextension_types.go b/api/v1alpha1/clusterextension_types.go index 907e762d1..b2a644ecf 100644 --- a/api/v1alpha1/clusterextension_types.go +++ b/api/v1alpha1/clusterextension_types.go @@ -78,6 +78,12 @@ type ClusterExtensionSpec struct { // the bundle may contain resources that are cluster-scoped or that are // installed in a different namespace. This namespace is expected to exist. InstallNamespace string `json:"installNamespace"` + + //+kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$ + //+kubebuilder:validation:MaxLength:=253 + // ServiceAccountName is the name of the ServiceAccount to use to manage the resources in the bundle. + // The service account is expected to exist in the InstallNamespace. + ServiceAccountName string `json:"serviceAccountName"` } const ( diff --git a/cmd/manager/main.go b/cmd/manager/main.go index a91a51b86..c8442e441 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -28,10 +28,16 @@ import ( "go.uber.org/zap/zapcore" k8slabels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" crcache "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -44,6 +50,7 @@ import ( "github.com/operator-framework/rukpak/pkg/storage" ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" + "github.com/operator-framework/operator-controller/internal/authentication" "github.com/operator-framework/operator-controller/internal/catalogmetadata/cache" catalogclient "github.com/operator-framework/operator-controller/internal/catalogmetadata/client" "github.com/operator-framework/operator-controller/internal/controllers" @@ -159,19 +166,44 @@ func main() { cl := mgr.GetClient() catalogClient := catalogclient.New(cl, cache.NewFilesystemCache(cachePath, httpClient)) - installNamespaceMapper := helmclient.ObjectToStringMapper(func(obj client.Object) (string, error) { - ext := obj.(*ocv1alpha1.ClusterExtension) + saGetter, err := corev1client.NewForConfig(ctrl.GetConfigOrDie()) + if err != nil { + setupLog.Error(err, "unable to create service account client") + os.Exit(1) + } + + tg := authentication.NewTokenGetter(saGetter, 3600) + nsMapper := func(obj client.Object) (string, error) { + ext, ok := obj.(*ocv1alpha1.ClusterExtension) + if !ok { + return "", fmt.Errorf("cannot derive namespace from object of type %T", obj) + } return ext.Spec.InstallNamespace, nil - }) + } + + rcm := func(ctx context.Context, obj client.Object, baseRestConfig *rest.Config) (*rest.Config, error) { + cfg := rest.AnonymousClientConfig(rest.CopyConfig(baseRestConfig)) + ext, ok := obj.(*ocv1alpha1.ClusterExtension) + if !ok { + return cfg, nil + } + token, err := tg.Get(ctx, types.NamespacedName{Namespace: ext.Spec.InstallNamespace, Name: ext.Spec.ServiceAccountName}) + if err != nil { + return nil, err + } + cfg.BearerToken = token + return cfg, nil + } + cfgGetter, err := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), - helmclient.StorageNamespaceMapper(installNamespaceMapper), - helmclient.ClientNamespaceMapper(installNamespaceMapper), + helmclient.ClientNamespaceMapper(nsMapper), + helmclient.StorageNamespaceMapper(nsMapper), + helmclient.RestConfigMapper(rcm), ) if err != nil { setupLog.Error(err, "unable to config for creating helm client") os.Exit(1) } - acg, err := helmclient.NewActionClientGetter(cfgGetter) if err != nil { setupLog.Error(err, "unable to create helm client") @@ -217,6 +249,10 @@ func main() { InstalledBundleGetter: &controllers.DefaultInstalledBundleGetter{ActionClientGetter: acg}, Handler: registryv1handler.HandlerFunc(registry.HandleBundleDeployment), Finalizers: clusterExtensionFinalizers, + InformerClientMap: make(map[types.UID]kubernetes.Interface), + InformerFactoryMap: make(map[types.UID]informers.SharedInformerFactory), + InformerMap: make(map[string]informers.GenericInformer), + EventChannel: make(chan event.GenericEvent), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClusterExtension") os.Exit(1) diff --git a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml index 9c41127f5..0c4947b2a 100644 --- a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml +++ b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml @@ -56,6 +56,13 @@ spec: maxLength: 48 pattern: ^[a-z0-9]+(-[a-z0-9]+)*$ type: string + serviceAccountName: + description: |- + ServiceAccountName is the name of the ServiceAccount to use to manage the resources in the bundle. + The service account is expected to exist in the InstallNamespace. + maxLength: 253 + pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$ + type: string upgradeConstraintPolicy: default: Enforce description: Defines the policy for how to handle upgrade constraints @@ -77,6 +84,7 @@ spec: required: - installNamespace - packageName + - serviceAccountName type: object status: description: ClusterExtensionStatus defines the observed state of ClusterExtension diff --git a/config/base/rbac/role.yaml b/config/base/rbac/role.yaml index 3d36de44e..2a9909450 100644 --- a/config/base/rbac/role.yaml +++ b/config/base/rbac/role.yaml @@ -4,12 +4,6 @@ kind: ClusterRole metadata: name: manager-role rules: -- apiGroups: - - '*' - resources: - - '*' - verbs: - - '*' - apiGroups: - catalogd.operatorframework.io resources: @@ -27,15 +21,16 @@ rules: - apiGroups: - "" resources: - - secrets + - configmaps verbs: - - create - - delete - - get - list - - patch - - update - watch +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create - apiGroups: - olm.operatorframework.io resources: @@ -43,6 +38,7 @@ rules: verbs: - get - list + - update - watch - apiGroups: - olm.operatorframework.io diff --git a/config/samples/olm_v1alpha1_clusterextension.yaml b/config/samples/olm_v1alpha1_clusterextension.yaml index 483ef34f1..81aa4c588 100644 --- a/config/samples/olm_v1alpha1_clusterextension.yaml +++ b/config/samples/olm_v1alpha1_clusterextension.yaml @@ -4,5 +4,6 @@ metadata: name: clusterextension-sample spec: installNamespace: default + serviceAccountName: argocd-operator-bundle-sa packageName: argocd-operator version: 0.6.0 diff --git a/config/samples/sample-operator-bundle-rbac.yaml b/config/samples/sample-operator-bundle-rbac.yaml new file mode 100644 index 000000000..a1b0a2a7c --- /dev/null +++ b/config/samples/sample-operator-bundle-rbac.yaml @@ -0,0 +1,223 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: argocd-operator-bundle-sa + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: argocd-operator-bundle-clusterrole +rules: + - apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["list","get","create","delete"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get","create","delete"] + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterroles","clusterrolebindings"] + verbs: ["get","create","delete"] + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get","create","patch","delete"] +# Adding permissions from bundle in order to be able to create rbac for the extension + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - configmaps + - endpoints + - events + - namespaces + - persistentvolumeclaims + - pods + - secrets + - serviceaccounts + - services + - services/finalizers + verbs: + - '*' + - apiGroups: + - "" + resources: + - pods + - pods/log + verbs: + - get + - apiGroups: + - apps + resources: + - daemonsets + - deployments + - replicasets + - statefulsets + verbs: + - '*' + - apiGroups: + - apps + resourceNames: + - argocd-operator + resources: + - deployments/finalizers + verbs: + - update + - apiGroups: + - apps.openshift.io + resources: + - deploymentconfigs + verbs: + - '*' + - apiGroups: + - argoproj.io + resources: + - applications + - appprojects + verbs: + - '*' + - apiGroups: + - argoproj.io + resources: + - argocdexports + - argocdexports/finalizers + - argocdexports/status + verbs: + - '*' + - apiGroups: + - argoproj.io + resources: + - argocds + - argocds/finalizers + - argocds/status + verbs: + - '*' + - apiGroups: + - argoproj.io + resources: + - notificationsconfigurations + - notificationsconfigurations/finalizers + verbs: + - '*' + - apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - '*' + - apiGroups: + - batch + resources: + - cronjobs + - jobs + verbs: + - '*' + - apiGroups: + - config.openshift.io + resources: + - clusterversions + verbs: + - get + - list + - watch + - apiGroups: + - monitoring.coreos.com + resources: + - prometheuses + - prometheusrules + - servicemonitors + verbs: + - '*' + - apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - '*' + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - oauth.openshift.io + resources: + - oauthclients + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - '*' + verbs: + - '*' + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + verbs: + - '*' + - apiGroups: + - route.openshift.io + resources: + - routes + - routes/custom-host + verbs: + - '*' + - apiGroups: + - template.openshift.io + resources: + - templateconfigs + - templateinstances + - templates + verbs: + - '*' + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: argocd-operator-bundle-clusterrolebinding +roleRef: + kind: ClusterRole + name: argocd-operator-bundle-clusterrole + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: argocd-operator-bundle-sa + namespace: default diff --git a/internal/authentication/tokengetter.go b/internal/authentication/tokengetter.go new file mode 100644 index 000000000..e2eda7769 --- /dev/null +++ b/internal/authentication/tokengetter.go @@ -0,0 +1,97 @@ +package authentication + +import ( + "context" + "sync" + "time" + + authenticationv1 "k8s.io/api/authentication/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/utils/ptr" +) + +type TokenGetter struct { + client corev1client.ServiceAccountsGetter + expirationSeconds int64 + tokens map[types.NamespacedName]*authenticationv1.TokenRequestStatus + tokenLocks keyLock[types.NamespacedName] + mu sync.RWMutex +} + +func NewTokenGetter(client corev1client.ServiceAccountsGetter, expirationSeconds int64) *TokenGetter { + return &TokenGetter{ + client: client, + expirationSeconds: expirationSeconds, + tokenLocks: newKeyLock[types.NamespacedName](), + tokens: map[types.NamespacedName]*authenticationv1.TokenRequestStatus{}, + } +} + +type keyLock[K comparable] struct { + locks map[K]*sync.Mutex + mu sync.Mutex +} + +func newKeyLock[K comparable]() keyLock[K] { + return keyLock[K]{locks: map[K]*sync.Mutex{}} +} + +func (k *keyLock[K]) Lock(key K) { + k.getLock(key).Lock() +} + +func (k *keyLock[K]) Unlock(key K) { + k.getLock(key).Unlock() +} + +func (k *keyLock[K]) getLock(key K) *sync.Mutex { + k.mu.Lock() + defer k.mu.Unlock() + + lock, ok := k.locks[key] + if !ok { + lock = &sync.Mutex{} + k.locks[key] = lock + } + return lock +} + +func (t *TokenGetter) Get(ctx context.Context, key types.NamespacedName) (string, error) { + t.tokenLocks.Lock(key) + defer t.tokenLocks.Unlock(key) + + t.mu.RLock() + token, ok := t.tokens[key] + t.mu.RUnlock() + + expireTime := time.Time{} + if ok { + expireTime = token.ExpirationTimestamp.Time + } + + fiveMinutesAfterNow := metav1.Now().Add(5 * time.Minute) + if expireTime.Before(fiveMinutesAfterNow) { + var err error + token, err = t.getToken(ctx, key) + if err != nil { + return "", err + } + t.mu.Lock() + t.tokens[key] = token + t.mu.Unlock() + } + + return token.Token, nil +} + +func (t *TokenGetter) getToken(ctx context.Context, key types.NamespacedName) (*authenticationv1.TokenRequestStatus, error) { + req, err := t.client.ServiceAccounts(key.Namespace).CreateToken(ctx, key.Name, &authenticationv1.TokenRequest{Spec: authenticationv1.TokenRequestSpec{ + ExpirationSeconds: ptr.To[int64](3600), + }}, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + return &req.Status, nil +} diff --git a/internal/controllers/clusterextension_admission_test.go b/internal/controllers/clusterextension_admission_test.go index 16629c410..c3799d727 100644 --- a/internal/controllers/clusterextension_admission_test.go +++ b/internal/controllers/clusterextension_admission_test.go @@ -43,8 +43,9 @@ func TestClusterExtensionAdmissionPackageName(t *testing.T) { t.Parallel() cl := newClient(t) err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ - PackageName: tc.pkgName, - InstallNamespace: "default", + PackageName: tc.pkgName, + InstallNamespace: "default", + ServiceAccountName: "default", })) if tc.errMsg == "" { require.NoError(t, err, "unexpected error for package name %q: %w", tc.pkgName, err) @@ -131,9 +132,10 @@ func TestClusterExtensionAdmissionVersion(t *testing.T) { t.Parallel() cl := newClient(t) err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ - PackageName: "package", - Version: tc.version, - InstallNamespace: "default", + PackageName: "package", + Version: tc.version, + InstallNamespace: "default", + ServiceAccountName: "default", })) if tc.errMsg == "" { require.NoError(t, err, "unexpected error for version %q: %w", tc.version, err) @@ -176,9 +178,10 @@ func TestClusterExtensionAdmissionChannel(t *testing.T) { t.Parallel() cl := newClient(t) err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ - PackageName: "package", - Channel: tc.channelName, - InstallNamespace: "default", + PackageName: "package", + Channel: tc.channelName, + InstallNamespace: "default", + ServiceAccountName: "default", })) if tc.errMsg == "" { require.NoError(t, err, "unexpected error for channel %q: %w", tc.channelName, err) @@ -222,8 +225,9 @@ func TestClusterExtensionAdmissionInstallNamespace(t *testing.T) { t.Parallel() cl := newClient(t) err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ - PackageName: "package", - InstallNamespace: tc.installNamespace, + PackageName: "package", + InstallNamespace: tc.installNamespace, + ServiceAccountName: "default", })) if tc.errMsg == "" { require.NoError(t, err, "unexpected error for installNamespace %q: %w", tc.installNamespace, err) @@ -235,6 +239,51 @@ func TestClusterExtensionAdmissionInstallNamespace(t *testing.T) { } } +func TestClusterExtensionAdmissionServiceAccountName(t *testing.T) { + tooLongError := "spec.serviceAccountName: Too long: may not be longer than 253" + regexMismatchError := "spec.serviceAccountName in body should match" + + testCases := []struct { + name string + serviceAccountName string + errMsg string + }{ + {"just alphanumeric", "justalphanumberic1", ""}, + {"hypen-separated", "hyphenated-name", ""}, + {"no service account name", "", regexMismatchError}, + {"longest valid service account name", strings.Repeat("x", 253), ""}, + {"too long service account name", strings.Repeat("x", 254), tooLongError}, + {"spaces", "spaces spaces", regexMismatchError}, + {"capitalized", "Capitalized", regexMismatchError}, + {"camel case", "camelCase", regexMismatchError}, + {"invalid characters", "many/invalid$characters+in_name", regexMismatchError}, + {"starts with hyphen", "-start-with-hyphen", regexMismatchError}, + {"ends with hyphen", "end-with-hyphen-", regexMismatchError}, + {"starts with period", ".start-with-period", regexMismatchError}, + {"ends with period", "end-with-period.", regexMismatchError}, + } + + t.Parallel() + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + cl := newClient(t) + err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ + PackageName: "package", + InstallNamespace: "default", + ServiceAccountName: tc.serviceAccountName, + })) + if tc.errMsg == "" { + require.NoError(t, err, "unexpected error for serviceAccountName %q: %w", tc.serviceAccountName, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg) + } + }) + } +} + func buildClusterExtension(spec ocv1alpha1.ClusterExtensionSpec) *ocv1alpha1.ClusterExtension { return &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/controllers/clusterextension_controller.go b/internal/controllers/clusterextension_controller.go index b7572fe89..b38169790 100644 --- a/internal/controllers/clusterextension_controller.go +++ b/internal/controllers/clusterextension_controller.go @@ -30,6 +30,17 @@ import ( mmsemver "github.com/Masterminds/semver/v3" bsemver "github.com/blang/semver/v4" "github.com/go-logr/logr" + "github.com/operator-framework/api/pkg/operators/v1alpha1" + catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" + helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" + "github.com/operator-framework/operator-controller/internal/authentication" + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2" + registryv1handler "github.com/operator-framework/rukpak/pkg/handler" + rukpaksource "github.com/operator-framework/rukpak/pkg/source" + "github.com/operator-framework/rukpak/pkg/storage" + "github.com/operator-framework/rukpak/pkg/util" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" @@ -44,6 +55,11 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" apimachyaml "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" + clientgocache "k8s.io/client-go/tools/cache" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" @@ -56,18 +72,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" - "github.com/operator-framework/api/pkg/operators/v1alpha1" - catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" - helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client" - "github.com/operator-framework/operator-registry/alpha/declcfg" - "github.com/operator-framework/operator-registry/alpha/property" - rukpakv1alpha2 "github.com/operator-framework/rukpak/api/v1alpha2" - registryv1handler "github.com/operator-framework/rukpak/pkg/handler" - helmpredicate "github.com/operator-framework/rukpak/pkg/helm-operator-plugins/predicate" - rukpaksource "github.com/operator-framework/rukpak/pkg/source" - "github.com/operator-framework/rukpak/pkg/storage" - "github.com/operator-framework/rukpak/pkg/util" - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" "github.com/operator-framework/operator-controller/internal/catalogmetadata" catalogfilter "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter" @@ -90,6 +94,11 @@ type ClusterExtensionReconciler struct { cache cache.Cache InstalledBundleGetter InstalledBundleGetter Finalizers crfinalizer.Finalizers + InformerClientMap map[types.UID]kubernetes.Interface + InformerFactoryMap map[types.UID]informers.SharedInformerFactory + EventChannel chan event.GenericEvent + InformerMap map[string]informers.GenericInformer + watchCreated bool } type InstalledBundleGetter interface { @@ -100,11 +109,12 @@ const ( bundleConnectionAnnotation string = "bundle.connection.config/insecureSkipTLSVerify" ) -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions,verbs=get;list;watch +//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions,verbs=get;list;watch;update //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions/status,verbs=update;patch //+kubebuilder:rbac:groups=olm.operatorframework.io,resources=clusterextensions/finalizers,verbs=update -//+kubebuilder:rbac:groups=core,resources=secrets,verbs=create;update;patch;delete;get;list;watch -//+kubebuilder:rbac:groups=*,resources=*,verbs=* + +//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=list;watch +//+kubebuilder:rbac:groups=core,resources=serviceaccounts/token,verbs=create //+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=clustercatalogs,verbs=list;watch //+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=catalogmetadata,verbs=list;watch @@ -113,8 +123,8 @@ const ( // This has been taken from rukpak, and an issue was created before to discuss it: https://github.com/operator-framework/rukpak/issues/800. func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { l := log.FromContext(ctx).WithName("operator-controller") - l.V(1).Info("starting") - defer l.V(1).Info("ending") + l.V(1).Info(fmt.Sprintf("starting reconcile for %s", req.String())) + defer l.V(1).Info(fmt.Sprintf("ending reconcile for %s", req.String())) var existingExt = &ocv1alpha1.ClusterExtension{} if err := r.Client.Get(ctx, req.NamespacedName, existingExt); err != nil { @@ -194,6 +204,7 @@ func checkForUnexpectedFieldChange(a, b ocv1alpha1.ClusterExtension) bool { //nolint:unparam func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alpha1.ClusterExtension) (ctrl.Result, error) { finalizeResult, err := r.Finalizers.Finalize(ctx, ext) + logger := log.FromContext(ctx) if err != nil { // TODO: For now, this error handling follows the pattern of other error handling. // Namely: zero just about everything out, throw our hands up, and return an error. @@ -346,31 +357,133 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp return ctrl.Result{}, err } + // build client with service account token + informerClient, clientSetExists := r.InformerClientMap[ext.UID] + if !clientSetExists { + saGetter, err := corev1client.NewForConfig(ctrl.GetConfigOrDie()) + if err != nil { + logger.Error(err, "unable to create client to get service account") + } + tg := authentication.NewTokenGetter(saGetter, 0) + token, err := tg.Get(ctx, types.NamespacedName{Namespace: ext.Spec.InstallNamespace, Name: ext.Spec.ServiceAccountName}) + if err != nil { + logger.Error(err, "unable to get service account token") + } + cfg := rest.AnonymousClientConfig(rest.CopyConfig(ctrl.GetConfigOrDie())) + cfg.BearerToken = token + informerClient, err = kubernetes.NewForConfig(cfg) + r.InformerClientMap[ext.UID] = informerClient + //TODO clean up client when extension is removed + } + + // build informer factory with custom clientset + informerFactory, informerFactoryExists := r.InformerFactoryMap[ext.UID] + if !informerFactoryExists { + informerFactory = informers.NewSharedInformerFactoryWithOptions(informerClient, 0, informers.WithNamespace(ext.Spec.InstallNamespace)) + r.InformerFactoryMap[ext.UID] = informerFactory + //TODO clean up informer factory when extension is removed + } + + //setup informers for all bundle objects to emit events to channel for _, obj := range relObjects { - if err := func() error { - r.dynamicWatchMutex.Lock() - defer r.dynamicWatchMutex.Unlock() - - _, isWatched := r.dynamicWatchGVKs[obj.GetObjectKind().GroupVersionKind()] - if !isWatched { - if err := r.controller.Watch( - source.Kind(r.cache, - obj, - crhandler.EnqueueRequestForOwner(r.Scheme(), r.RESTMapper(), ext, crhandler.OnlyControllerOwner()), - helmpredicate.DependentPredicateFuncs[client.Object](), - ), - ); err != nil { - return err + logger.Info(fmt.Sprintf("processing release object %s/%s with gvk %s", obj.GetNamespace(), obj.GetName(), obj.GetObjectKind().GroupVersionKind().GroupKind())) + gvk := obj.GetObjectKind().GroupVersionKind() + mapping, err := r.RESTMapper().RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + logger.Error(err, "Unable to convert gvk to gvr", "gvr", gvk.String()) + continue + } + objKey := obj.GetName() + obj.GetObjectKind().GroupVersionKind().Kind + genericInformer, infomerExists := r.InformerMap[objKey] + if !infomerExists { + logger.Info(fmt.Sprintf("Creating generic informer for %s", mapping.Resource.String())) + genericInformer, err = informerFactory.ForResource(mapping.Resource) + if err != nil { + logger.Error(err, fmt.Sprintf("Unable to create generic informer for %s", mapping.Resource.String()), "resource", mapping.Resource.String()) + continue + } + logger.Info(fmt.Sprintf("Adding event handler for %s", mapping.Resource.String()), "resource", mapping.Resource.String()) + _, err = genericInformer.Informer().AddEventHandler(clientgocache.ResourceEventHandlerFuncs{ + //AddFunc: func(obj interface{}) { + // event := event.GenericEvent{ + // Object: obj.(client.Object), + // } + // logger.Info(fmt.Sprintf("got add event with name %s,and namespace %s", event.Object.GetName(), event.Object.GetNamespace())) + // r.EventChannel <- event + //}, + //UpdateFunc: func(oldObj, newObj interface{}) { + // event := event.GenericEvent{ + // Object: oldObj.(client.Object), + // } + // logger.Info(fmt.Sprintf("got update event with name %s,and namespace %s", event.Object.GetName(), event.Object.GetNamespace())) + // r.EventChannel <- event + //}, + DeleteFunc: func(obj interface{}) { + event := event.GenericEvent{ + Object: obj.(client.Object), + } + logger.Info(fmt.Sprintf("got delete event with name %s,and namespace %s", event.Object.GetName(), event.Object.GetNamespace())) + r.EventChannel <- event + }, + }) + if err != nil { + logger.Error(err, "Unable to add event handlers for informer with %v", mapping.Resource.String()) + } + informerFactory.Start(ctx.Done()) + synced := informerFactory.WaitForCacheSync(ctx.Done()) + for v, ok := range synced { + if !ok { + logger.Error(fmt.Errorf("cache failed to sync %s with type %v", obj.GetName(), v), "Problem with informer setup") + } else { + logger.Info(fmt.Sprintf("Cache has synced for %s with type %v", obj.GetName(), v)) } - r.dynamicWatchGVKs[obj.GetObjectKind().GroupVersionKind()] = sets.Empty{} } - return nil - }(); err != nil { + logger.Info(fmt.Sprintf("Saving informer for object with key %s", objKey)) + r.InformerMap[objKey] = genericInformer + //TODO clean up informer factory when extension is removed + } else { + logger.Info(fmt.Sprintf("Informer already exists for %s", objKey)) + } + } + + // set up watch for channel so that we can trigger reconciles when bundle content changes + if !r.watchCreated { + err = r.controller.Watch( + source.Channel(r.EventChannel, + crhandler.EnqueueRequestForOwner(r.Scheme(), r.RESTMapper(), ext, crhandler.OnlyControllerOwner()), + )) + if err != nil { ext.Status.InstalledBundle = nil setInstalledStatusConditionFailed(ext, fmt.Sprintf("%s:%v", ocv1alpha1.ReasonCreateDynamicWatchFailed, err)) return ctrl.Result{}, err } - } + r.watchCreated = true + } + + //for _, obj := range relObjects { + // if err := func() error { + // r.dynamicWatchMutex.Lock() + // defer r.dynamicWatchMutex.Unlock() + // + // _, isWatched := r.dynamicWatchGVKs[obj.GetObjectKind().GroupVersionKind()] + // if !isWatched { + // if err := r.controller.Watch( + // source.Kind(r.cache, + // obj, + // crhandler.EnqueueRequestForOwner(r.Scheme(), r.RESTMapper(), ext, crhandler.OnlyControllerOwner()), + // ), + // ); err != nil { + // return err + // } + // r.dynamicWatchGVKs[obj.GetObjectKind().GroupVersionKind()] = sets.Empty{} + // } + // return nil + // }(); err != nil { + // ext.Status.InstalledBundle = nil + // setInstalledStatusConditionFailed(ext, fmt.Sprintf("%s:%v", ocv1alpha1.ReasonCreateDynamicWatchFailed, err)) + // return ctrl.Result{}, err + // } + //} ext.Status.InstalledBundle = bundleMetadataFor(bundle) setInstalledStatusConditionSuccess(ext, fmt.Sprintf("Instantiated bundle %s successfully", ext.GetName())) diff --git a/internal/controllers/clusterextension_controller_test.go b/internal/controllers/clusterextension_controller_test.go index f7cabf87f..fb179896b 100644 --- a/internal/controllers/clusterextension_controller_test.go +++ b/internal/controllers/clusterextension_controller_test.go @@ -52,8 +52,9 @@ func TestClusterExtensionNonExistentPackage(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - InstallNamespace: "default", + PackageName: pkgName, + InstallNamespace: "default", + ServiceAccountName: "default", }, } require.NoError(t, cl.Create(ctx, clusterExtension)) @@ -93,9 +94,10 @@ func TestClusterExtensionNonExistentVersion(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: "0.50.0", // this version of the package does not exist - InstallNamespace: "default", + PackageName: pkgName, + Version: "0.50.0", // this version of the package does not exist + InstallNamespace: "default", + ServiceAccountName: "default", }, } require.NoError(t, cl.Create(ctx, clusterExtension)) @@ -145,14 +147,16 @@ func TestClusterExtensionChannelVersionExists(t *testing.T) { pkgVer := "1.0.0" pkgChan := "beta" installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccountName := fmt.Sprintf("test-sa-%s", rand.String(8)) clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, - InstallNamespace: installNamespace, + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + InstallNamespace: installNamespace, + ServiceAccountName: serviceAccountName, }, } err := cl.Create(ctx, clusterExtension) @@ -204,13 +208,16 @@ func TestClusterExtensionChannelExistsNoVersion(t *testing.T) { pkgVer := "" pkgChan := "beta" installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccountName := fmt.Sprintf("test-sa-%s", rand.String(8)) + clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, - InstallNamespace: installNamespace, + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + InstallNamespace: installNamespace, + ServiceAccountName: serviceAccountName, }, } err := cl.Create(ctx, clusterExtension) @@ -259,10 +266,11 @@ func TestClusterExtensionVersionNoChannel(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, - InstallNamespace: "default", + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + InstallNamespace: "default", + ServiceAccountName: "default", }, } require.NoError(t, cl.Create(ctx, clusterExtension)) @@ -308,9 +316,10 @@ func TestClusterExtensionNoChannel(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Channel: pkgChan, - InstallNamespace: "default", + PackageName: pkgName, + Channel: pkgChan, + InstallNamespace: "default", + ServiceAccountName: "default", }, } require.NoError(t, cl.Create(ctx, clusterExtension)) @@ -357,10 +366,11 @@ func TestClusterExtensionNoVersion(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, - InstallNamespace: "default", + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + InstallNamespace: "default", + ServiceAccountName: "default", }, } require.NoError(t, cl.Create(ctx, clusterExtension)) @@ -441,14 +451,16 @@ func TestClusterExtensionUpgrade(t *testing.T) { pkgVer := "1.0.0" pkgChan := "beta" installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccountName := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, - InstallNamespace: installNamespace, + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + InstallNamespace: installNamespace, + ServiceAccountName: serviceAccountName, }, } // Create a cluster extension @@ -541,14 +553,16 @@ func TestClusterExtensionUpgrade(t *testing.T) { pkgVer := "1.0.0" pkgChan := "beta" installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccountName := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, - InstallNamespace: installNamespace, + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + InstallNamespace: installNamespace, + ServiceAccountName: serviceAccountName, }, } // Create a cluster extension @@ -652,6 +666,7 @@ func TestClusterExtensionUpgrade(t *testing.T) { }() installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccountName := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, @@ -661,6 +676,7 @@ func TestClusterExtensionUpgrade(t *testing.T) { Channel: "beta", UpgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyIgnore, InstallNamespace: installNamespace, + ServiceAccountName: serviceAccountName, }, } // Create a cluster extension @@ -752,14 +768,16 @@ func TestClusterExtensionDowngrade(t *testing.T) { }() installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccountName := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", - Version: "1.0.1", - Channel: "beta", - InstallNamespace: installNamespace, + PackageName: "prometheus", + Version: "1.0.1", + Channel: "beta", + InstallNamespace: installNamespace, + ServiceAccountName: serviceAccountName, }, } // Create a cluster extension @@ -840,6 +858,7 @@ func TestClusterExtensionDowngrade(t *testing.T) { }() installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccountName := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, @@ -849,6 +868,7 @@ func TestClusterExtensionDowngrade(t *testing.T) { Channel: "beta", UpgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyIgnore, InstallNamespace: installNamespace, + ServiceAccountName: serviceAccountName, }, } // Create a cluster extension diff --git a/internal/controllers/clusterextension_registryv1_validation_test.go b/internal/controllers/clusterextension_registryv1_validation_test.go index 3518a8b56..28924b3a6 100644 --- a/internal/controllers/clusterextension_registryv1_validation_test.go +++ b/internal/controllers/clusterextension_registryv1_validation_test.go @@ -121,12 +121,14 @@ func TestClusterExtensionRegistryV1DisallowDependencies(t *testing.T) { } installNamespace := fmt.Sprintf("test-ns-%s", rand.String(8)) + serviceAccountName := fmt.Sprintf("test-sa-%s", rand.String(8)) extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))} clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: tt.bundle.Package, - InstallNamespace: installNamespace, + PackageName: tt.bundle.Package, + InstallNamespace: installNamespace, + ServiceAccountName: serviceAccountName, }, } require.NoError(t, cl.Create(ctx, clusterExtension)) diff --git a/test/e2e/cluster_extension_install_test.go b/test/e2e/cluster_extension_install_test.go index 112dcc7af..34a28570f 100644 --- a/test/e2e/cluster_extension_install_test.go +++ b/test/e2e/cluster_extension_install_test.go @@ -75,8 +75,9 @@ func TestClusterExtensionInstallRegistry(t *testing.T) { defer getArtifactsOutput(t) clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", - InstallNamespace: "default", + PackageName: "prometheus", + InstallNamespace: "default", + ServiceAccountName: "prometheus-operator-bundle-sa", } t.Log("It resolves the specified package with correct bundle path") t.Log("By creating the ClusterExtension resource") @@ -132,8 +133,9 @@ func TestClusterExtensionInstallReResolvesWhenNewCatalog(t *testing.T) { pkgName := "prometheus" clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - InstallNamespace: "default", + PackageName: pkgName, + InstallNamespace: "default", + ServiceAccountName: "prometheus-operator-bundle-sa", } t.Log("By deleting the catalog first") @@ -198,9 +200,10 @@ func TestClusterExtensionBlockInstallNonSuccessorVersion(t *testing.T) { t.Log("By creating an ClusterExtension at a specified version") clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", - Version: "1.0.0", - InstallNamespace: "default", + PackageName: "prometheus", + Version: "1.0.0", + InstallNamespace: "default", + ServiceAccountName: "prometheus-operator-bundle-sa", } require.NoError(t, c.Create(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful installation") @@ -244,9 +247,10 @@ func TestClusterExtensionForceInstallNonSuccessorVersion(t *testing.T) { t.Log("By creating an ClusterExtension at a specified version") clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", - Version: "1.0.0", - InstallNamespace: "default", + PackageName: "prometheus", + Version: "1.0.0", + InstallNamespace: "default", + ServiceAccountName: "prometheus-operator-bundle-sa", } require.NoError(t, c.Create(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful resolution") @@ -289,9 +293,10 @@ func TestClusterExtensionInstallSuccessorVersion(t *testing.T) { t.Log("By creating an ClusterExtension at a specified version") clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", - Version: "1.0.0", - InstallNamespace: "default", + PackageName: "prometheus", + Version: "1.0.0", + InstallNamespace: "default", + ServiceAccountName: "prometheus-operator-bundle-sa", } require.NoError(t, c.Create(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful resolution") diff --git a/test/e2e/cluster_extension_registryV1_limitations_test.go b/test/e2e/cluster_extension_registryV1_limitations_test.go new file mode 100644 index 000000000..d2a15965b --- /dev/null +++ b/test/e2e/cluster_extension_registryV1_limitations_test.go @@ -0,0 +1,40 @@ +package e2e + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" +) + +func TestClusterExtensionPackagesWithWebhooksAreNotAllowed(t *testing.T) { + ctx := context.Background() + clusterExtension, catalog := testInit(t) + defer testCleanup(t, catalog, clusterExtension) + defer getArtifactsOutput(t) + + clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ + PackageName: "package-with-webhooks", + Version: "1.0.0", + InstallNamespace: "default", + ServiceAccountName: "package-with-webhooks-sa", + } + require.NoError(t, c.Create(ctx, clusterExtension)) + require.EventuallyWithT(t, func(ct *assert.CollectT) { + assert.NoError(ct, c.Get(ctx, types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled) + if !assert.NotNil(ct, cond) { + return + } + assert.Equal(ct, metav1.ConditionFalse, cond.Status) + assert.Equal(ct, ocv1alpha1.ReasonInstallationFailed, cond.Reason) + assert.Contains(ct, cond.Message, "webhookDefinitions are not supported") + assert.Equal(ct, &ocv1alpha1.BundleMetadata{Name: "package-with-webhooks.1.0.0", Version: "1.0.0"}, clusterExtension.Status.ResolvedBundle) + }, pollDuration, pollInterval) +} diff --git a/test/extension-developer-e2e/extension_developer_test.go b/test/extension-developer-e2e/extension_developer_test.go index 52f333273..882d75a57 100644 --- a/test/extension-developer-e2e/extension_developer_test.go +++ b/test/extension-developer-e2e/extension_developer_test.go @@ -40,8 +40,9 @@ func TestExtensionDeveloper(t *testing.T) { }, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: os.Getenv("REG_PKG_NAME"), - InstallNamespace: "default", + PackageName: os.Getenv("REG_PKG_NAME"), + InstallNamespace: "default", + ServiceAccountName: "ext-dev-sa", }, }, } diff --git a/test/extension-developer-e2e/setup.sh b/test/extension-developer-e2e/setup.sh index ab8e48e79..aed5cc4ee 100755 --- a/test/extension-developer-e2e/setup.sh +++ b/test/extension-developer-e2e/setup.sh @@ -158,6 +158,7 @@ EOF kubectl create configmap -n "${namespace}" --from-file="${TMP_ROOT}"/catalog.Dockerfile extension-dev-e2e.dockerfile kubectl create configmap -n "${namespace}" --from-file="${TMP_ROOT}"/catalog extension-dev-e2e.build-contents +kubectl apply -f testdata/rbac/ext-dev-rbac.yaml -n default kubectl apply -f - << EOF apiVersion: batch/v1 diff --git a/testdata/rbac/ext-dev-rbac.yaml b/testdata/rbac/ext-dev-rbac.yaml new file mode 100644 index 000000000..fdebadab3 --- /dev/null +++ b/testdata/rbac/ext-dev-rbac.yaml @@ -0,0 +1,65 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ext-dev-sa + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: ext-dev-clusterrole +rules: +- apiGroups: [""] + resources: ["secrets","serviceaccounts","services"] + verbs: ["list","get","create","delete"] +- apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get","create","delete"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterroles","clusterrolebindings"] + verbs: ["get","create","delete"] +- apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get","create","delete"] + +# Adding permissions from bundle in order to be able to create rbac for the extension +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get","list","watch","create","update","patch","delete"] +- apiGroups: [""] + resources: ["events"] + verbs: ["create","patch"] +- apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get","list","watch","create","update","patch","delete"] +- apiGroups: [ "authentication.k8s.io" ] + resources: [ "tokenreviews" ] + verbs: [ "create" ] +- apiGroups: [ "authorization.k8s.io" ] + resources: [ "subjectaccessreviews" ] + verbs: [ "create" ] +- apiGroups: ["oc-opdev-e2e.operatorframework.io.oc-opdev-e2e.operatorframework.io"] + resources: ["registries"] + verbs: ["get","list","watch","create","update","patch","delete"] +- apiGroups: ["oc-opdev-e2e.operatorframework.io.oc-opdev-e2e.operatorframework.io"] + resources: ["registries/finalizers"] + verbs: ["update"] +- apiGroups: ["oc-opdev-e2e.operatorframework.io.oc-opdev-e2e.operatorframework.io"] + resources: ["registries/status"] + verbs: ["get","update","patch"] +- nonResourceURLs: ["/metrics"] + verbs: ["get"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ext-dev-clusterrolebinding +subjects: +- kind: ServiceAccount + name: ext-dev-sa + namespace: default +roleRef: + kind: ClusterRole + name: ext-dev-clusterrole + apiGroup: rbac.authorization.k8s.io diff --git a/testdata/rbac/prometheus-operator-bundle-rbac.yaml b/testdata/rbac/prometheus-operator-bundle-rbac.yaml new file mode 100644 index 000000000..6173aa6d7 --- /dev/null +++ b/testdata/rbac/prometheus-operator-bundle-rbac.yaml @@ -0,0 +1,114 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: prometheus-operator-bundle-sa + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: prometheus-operator-bundle-clusterrole +rules: + - apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["list","get","create","delete"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get","create","delete"] + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterroles","clusterrolebindings"] + verbs: ["get","create","delete"] + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get","create","delete"] +# Adding permissions from bundle in order to be able to create rbac for the extension + - apiGroups: + - monitoring.coreos.com + resources: + - alertmanagers + - alertmanagers/finalizers + - alertmanagers/status + - alertmanagerconfigs + - prometheuses + - prometheuses/finalizers + - prometheuses/status + - prometheusagents + - prometheusagents/finalizers + - prometheusagents/status + - thanosrulers + - thanosrulers/finalizers + - thanosrulers/status + - scrapeconfigs + - servicemonitors + - podmonitors + - probes + - prometheusrules + verbs: + - "*" + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - "*" + - apiGroups: + - "" + resources: + - configmaps + - secrets + verbs: + - "*" + - apiGroups: + - "" + resources: + - pods + verbs: + - list + - delete + - apiGroups: + - "" + resources: + - services + - services/finalizers + - endpoints + verbs: + - get + - create + - update + - delete + - apiGroups: + - "" + resources: + - nodes + verbs: + - list + - watch + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: prometheus-operator-bundle-clusterrolebinding +roleRef: + kind: ClusterRole + name: prometheus-operator-bundle-clusterrole + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: prometheus-operator-bundle-sa + namespace: default diff --git a/testdata/rbac/test-rbac.yaml b/testdata/rbac/test-rbac.yaml new file mode 100644 index 000000000..6e0ece311 --- /dev/null +++ b/testdata/rbac/test-rbac.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: default + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + namespace: default + name: test-clusterrole +rules: +- apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: test-clusterrolebinding + namespace: default +subjects: +- kind: ServiceAccount + name: default + namespace: default +roleRef: + kind: ClusterRole + name: test-clusterrole + apiGroup: rbac.authorization.k8s.io