diff --git a/internal/olm/operator/bundle/install.go b/internal/olm/operator/bundle/install.go index a2ab4c7319..54c812e22f 100644 --- a/internal/olm/operator/bundle/install.go +++ b/internal/olm/operator/bundle/install.go @@ -23,6 +23,7 @@ import ( apimanifests "github.com/operator-framework/api/pkg/manifests" "github.com/operator-framework/api/pkg/operators/v1alpha1" + registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle" "github.com/spf13/pflag" "github.com/operator-framework/operator-sdk/internal/olm/operator" @@ -75,10 +76,11 @@ func (i *Install) setup(ctx context.Context) error { return err } - i.OperatorInstaller.PackageName = labels["operators.operatorframework.io.bundle.package.v1"] + i.OperatorInstaller.PackageName = labels[registrybundle.PackageLabel] i.OperatorInstaller.CatalogSourceName = fmt.Sprintf("%s-catalog", i.OperatorInstaller.PackageName) i.OperatorInstaller.StartingCSV = csv.Name - i.OperatorInstaller.Channel = strings.Split(labels["operators.operatorframework.io.bundle.channels.v1"], ",")[0] + i.OperatorInstaller.SupportedInstallModes = operator.GetSupportedInstallModes(csv.Spec.InstallModes) + i.OperatorInstaller.Channel = strings.Split(labels[registrybundle.ChannelsLabel], ",")[0] i.IndexImageCatalogCreator.BundleImage = i.BundleImage i.IndexImageCatalogCreator.PackageName = i.OperatorInstaller.PackageName i.IndexImageCatalogCreator.InjectBundles = []string{i.BundleImage} diff --git a/internal/olm/operator/install_mode.go b/internal/olm/operator/install_mode.go index 0be3cb3589..56399afce8 100644 --- a/internal/olm/operator/install_mode.go +++ b/internal/olm/operator/install_mode.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/operator-framework/api/pkg/operators/v1alpha1" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" ) @@ -31,6 +32,8 @@ type InstallMode struct { var _ flag.Value = &InstallMode{} +// Set is called when the --install-mode flag is passed to the CLI. It will +// configure the InstallMode based on the values passed in. func (i *InstallMode) Set(str string) error { split := strings.SplitN(str, "=", 2) i.InstallModeType = v1alpha1.InstallModeType(split[0]) @@ -40,10 +43,13 @@ func (i *InstallMode) Set(str string) error { i.TargetNamespaces = append(i.TargetNamespaces, strings.TrimSpace(ns)) } sort.Strings(i.TargetNamespaces) + } else { + i.TargetNamespaces = []string{} } return i.Validate() } +// IsEmpty returns true if the InstallModeType is empty. func (i InstallMode) IsEmpty() bool { return i.InstallModeType == "" } @@ -93,11 +99,28 @@ func (i InstallMode) Validate() error { // CheckCompatibility checks if an InstallMode is compatible with the operator's namespace and is supported by csv. func (i InstallMode) CheckCompatibility(csv *v1alpha1.ClusterServiceVersion, operatorNamespace string) error { + // allnamespace was validated in Validate() + + // own namespace and targetns != opname if i.InstallModeType == v1alpha1.InstallModeTypeOwnNamespace { - if i.TargetNamespaces[0] != operatorNamespace { + if len(i.TargetNamespaces) > 0 && i.TargetNamespaces[0] != operatorNamespace { return fmt.Errorf("install mode %s must match operator namespace %q", i, operatorNamespace) } } + + // single namespace and targetns == opname + if i.InstallModeType == v1alpha1.InstallModeTypeSingleNamespace { + if len(i.TargetNamespaces) < 1 || i.TargetNamespaces[0] == operatorNamespace { + return fmt.Errorf("use install mode %q to watch operator's namespace %q", v1alpha1.InstallModeTypeOwnNamespace, i.TargetNamespaces[0]) + } + } + + // ensure the CSV has an installmode + if len(csv.Spec.InstallModes) == 0 { + return fmt.Errorf("operator %q is not installable: no supported install modes", csv.Name) + } + + // ensure the CSV supports the given installmode for _, mode := range csv.Spec.InstallModes { if mode.Type == i.InstallModeType && !mode.Supported { return fmt.Errorf("install mode type %q not supported in CSV %q", i.InstallModeType, csv.GetName()) @@ -105,3 +128,15 @@ func (i InstallMode) CheckCompatibility(csv *v1alpha1.ClusterServiceVersion, ope } return nil } + +// GetSupportedInstallModes returns the given slice of InstallModes as a +// String set. +func GetSupportedInstallModes(csvInstallModes []v1alpha1.InstallMode) sets.String { + supported := sets.NewString() + for _, im := range csvInstallModes { + if im.Supported { + supported.Insert(string(im.Type)) + } + } + return supported +} diff --git a/internal/olm/operator/install_mode_test.go b/internal/olm/operator/install_mode_test.go new file mode 100644 index 0000000000..acd490a3a7 --- /dev/null +++ b/internal/olm/operator/install_mode_test.go @@ -0,0 +1,66 @@ +// Copyright 2020 The Operator-SDK Authors +// +// 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 operator + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/operator-framework/api/pkg/operators/v1alpha1" +) + +var _ = Describe("InstallMode", func() { + + Describe("GetSupportedInstallModes", func() { + It("should return empty set if empty installmodes", func() { + supported := GetSupportedInstallModes([]v1alpha1.InstallMode{}) + Expect(supported.Len()).To(Equal(0)) + }) + It("should return empty set if no installmodes are supported", func() { + installModes := []v1alpha1.InstallMode{ + { + Type: v1alpha1.InstallModeTypeSingleNamespace, + Supported: false, + }, + { + Type: v1alpha1.InstallModeTypeOwnNamespace, + Supported: false, + }, + } + supported := GetSupportedInstallModes(installModes) + Expect(supported.Len()).To(Equal(0)) + }) + It("should return set with supported installmodes", func() { + installModes := []v1alpha1.InstallMode{ + { + Type: v1alpha1.InstallModeTypeSingleNamespace, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeOwnNamespace, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeAllNamespaces, + Supported: false, + }, + } + supported := GetSupportedInstallModes(installModes) + Expect(supported.Len()).To(Equal(2)) + Expect(supported.Has(string(v1alpha1.InstallModeTypeSingleNamespace))).Should(BeTrue()) + Expect(supported.Has(string(v1alpha1.InstallModeTypeOwnNamespace))).Should(BeTrue()) + Expect(supported.Has(string(v1alpha1.InstallModeTypeAllNamespaces))).Should(BeFalse()) + }) + }) +}) diff --git a/internal/olm/operator/operator_suite_test.go b/internal/olm/operator/operator_suite_test.go new file mode 100644 index 0000000000..79f492273b --- /dev/null +++ b/internal/olm/operator/operator_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2020 The Operator-SDK Authors +// +// 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 operator + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestOperator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Operator Suite") +} diff --git a/internal/olm/operator/packagemanifests/install.go b/internal/olm/operator/packagemanifests/install.go index 259f9a2ea0..f62767f573 100644 --- a/internal/olm/operator/packagemanifests/install.go +++ b/internal/olm/operator/packagemanifests/install.go @@ -69,9 +69,6 @@ func (i *Install) setup() error { return err } - if i.InstallMode.IsEmpty() { - i.InstallMode.InstallModeType = v1alpha1.InstallModeTypeAllNamespaces - } if err := i.InstallMode.CheckCompatibility(bundle.CSV, i.cfg.Namespace); err != nil { return err } @@ -79,6 +76,12 @@ func (i *Install) setup() error { i.OperatorInstaller.PackageName = pkg.PackageName i.OperatorInstaller.CatalogSourceName = fmt.Sprintf("%s-catalog", i.OperatorInstaller.PackageName) i.OperatorInstaller.StartingCSV = bundle.CSV.GetName() + i.OperatorInstaller.SupportedInstallModes = operator.GetSupportedInstallModes(bundle.CSV.Spec.InstallModes) + + if i.OperatorInstaller.SupportedInstallModes.Len() == 0 { + return fmt.Errorf("operator %q is not installable: no supported install modes", bundle.CSV.GetName()) + } + i.OperatorInstaller.Channel, err = getChannelForCSVName(pkg, i.OperatorInstaller.StartingCSV) if err != nil { return err diff --git a/internal/olm/operator/registry/operator_installer.go b/internal/olm/operator/registry/operator_installer.go index 44c3868aba..4d5448ba58 100644 --- a/internal/olm/operator/registry/operator_installer.go +++ b/internal/olm/operator/registry/operator_installer.go @@ -17,14 +17,13 @@ package registry import ( "context" "fmt" - "reflect" - "sort" "time" v1 "github.com/operator-framework/api/pkg/operators/v1" "github.com/operator-framework/api/pkg/operators/v1alpha1" log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" @@ -34,12 +33,13 @@ import ( ) type OperatorInstaller struct { - CatalogSourceName string - PackageName string - StartingCSV string - Channel string - InstallMode operator.InstallMode - CatalogCreator CatalogCreator + CatalogSourceName string + PackageName string + StartingCSV string + Channel string + InstallMode operator.InstallMode + CatalogCreator CatalogCreator + SupportedInstallModes sets.String cfg *operator.Configuration } @@ -55,16 +55,18 @@ func (o OperatorInstaller) InstallOperator(ctx context.Context) (*v1alpha1.Clust } log.Infof("Created CatalogSource: %s", cs.GetName()) - // TODO: OLM doesn't appear to propagate the "READY" connection status to the catalogsource in a timely manner - // even though its catalog-operator reports a connection almost immediately. This condition either needs - // to be propagated more quickly by OLM or we need to find a different resource to probe for readiness. - // wait for catalog source to be ready + // TODO: OLM doesn't appear to propagate the "READY" connection status to the + // catalogsource in a timely manner even though its catalog-operator reports + // a connection almost immediately. This condition either needs to be + // propagated more quickly by OLM or we need to find a different resource to + // probe for readiness. + // // if err := o.waitForCatalogSource(ctx, cs); err != nil { // return nil, err // } // Ensure Operator Group - if err = o.createOperatorGroup(ctx); err != nil { + if err = o.ensureOperatorGroup(ctx); err != nil { return nil, err } @@ -122,48 +124,66 @@ func (o OperatorInstaller) waitForCatalogSource(ctx context.Context, cs *v1alpha return nil } -// createOperatorGroup creates an OperatorGroup using package name if an OperatorGroup does not exist. -// If one exists in the desired namespace and it's target namespaces do not match the desired set, -// createOperatorGroup will return an error. -func (o OperatorInstaller) createOperatorGroup(ctx context.Context) error { - targetNamespaces := make([]string, len(o.InstallMode.TargetNamespaces), cap(o.InstallMode.TargetNamespaces)) - copy(targetNamespaces, o.InstallMode.TargetNamespaces) +func (o OperatorInstaller) ensureOperatorGroup(ctx context.Context) error { // Check OperatorGroup existence, since we cannot create a second OperatorGroup in namespace. og, ogFound, err := o.getOperatorGroup(ctx) if err != nil { return err } - // TODO: we may need to poll for status updates, since status.namespaces may not be updated immediately. - if ogFound { - // targetNamespaces will always be initialized, but the operator group's namespaces may not be - // (required for comparison). - if og.Status.Namespaces == nil { - og.Status.Namespaces = []string{} + + supported := o.SupportedInstallModes + + // --install-mode was given + if !o.InstallMode.IsEmpty() { + if o.InstallMode.InstallModeType == v1alpha1.InstallModeTypeSingleNamespace && + o.InstallMode.TargetNamespaces[0] == o.cfg.Namespace { + return fmt.Errorf("use install mode %q to watch operator's namespace %q", v1alpha1.InstallModeTypeOwnNamespace, o.cfg.Namespace) } - // Simple check for OperatorGroup compatibility: if namespaces are not an exact match, - // the user must manage the resource themselves. - sort.Strings(og.Status.Namespaces) - sort.Strings(targetNamespaces) - if !reflect.DeepEqual(og.Status.Namespaces, targetNamespaces) { - msg := fmt.Sprintf("namespaces %+q do not match desired namespaces %+q", og.Status.Namespaces, targetNamespaces) - if og.GetName() == operator.SDKOperatorGroupName { - return fmt.Errorf("existing SDK-managed operator group's %s, "+ - "please clean up existing operators `operator-sdk cleanup` before running package %q", msg, o.PackageName) - } - return fmt.Errorf("existing operator group %q's %s, "+ - "please ensure it has the exact namespace set before running package %q", og.GetName(), msg, o.PackageName) + + supported = supported.Intersection(sets.NewString(string(o.InstallMode.InstallModeType))) + if supported.Len() == 0 { + return fmt.Errorf("operator %q does not support install mode %q", o.StartingCSV, o.InstallMode.InstallModeType) } - log.Infof("Using existing operator group %q", og.GetName()) - } else { - // New SDK-managed OperatorGroup. - og = newSDKOperatorGroup(o.cfg.Namespace, - withTargetNamespaces(targetNamespaces...)) - if err = o.cfg.Client.Create(ctx, og); err != nil { - return fmt.Errorf("error creating OperatorGroup: %w", err) + } + + targetNamespaces, err := o.getTargetNamespaces(supported) + if err != nil { + return err + } + + if !ogFound { + if og, err = o.createOperatorGroup(ctx, targetNamespaces); err != nil { + return fmt.Errorf("create operator group: %v", err) } - log.Infof("Created OperatorGroup: %s", og.GetName()) + log.Infof("OperatorGroup %q created", og.Name) + } else if err := o.isOperatorGroupCompatible(*og, targetNamespaces); err != nil { + return err + } + + return nil +} +func (o *OperatorInstaller) createOperatorGroup(ctx context.Context, targetNamespaces []string) (*v1.OperatorGroup, error) { + og := newSDKOperatorGroup(o.cfg.Namespace, withTargetNamespaces(targetNamespaces...)) + if err := o.cfg.Client.Create(ctx, og); err != nil { + return nil, err + } + return og, nil +} + +func (o *OperatorInstaller) isOperatorGroupCompatible(og v1.OperatorGroup, targetNamespaces []string) error { + // no install mode use the existing operator group + if o.InstallMode.IsEmpty() { + return nil } + + // otherwise, check that the target namespaces match + targets := sets.NewString(targetNamespaces...) + ogtargets := sets.NewString(og.Spec.TargetNamespaces...) + if !ogtargets.Equal(targets) { + return fmt.Errorf("existing operatorgroup %q is not compatible with install mode %q", og.Name, o.InstallMode) + } + return nil } @@ -278,3 +298,16 @@ func (o OperatorInstaller) waitForInstallPlan(ctx context.Context, sub *v1alpha1 } return nil } + +func (o *OperatorInstaller) getTargetNamespaces(supported sets.String) ([]string, error) { + switch { + case supported.Has(string(v1alpha1.InstallModeTypeAllNamespaces)): + return nil, nil + case supported.Has(string(v1alpha1.InstallModeTypeOwnNamespace)): + return []string{o.cfg.Namespace}, nil + case supported.Has(string(v1alpha1.InstallModeTypeSingleNamespace)): + return o.InstallMode.TargetNamespaces, nil + default: + return nil, fmt.Errorf("no supported install modes") + } +} diff --git a/internal/olm/operator/registry/operator_installer_test.go b/internal/olm/operator/registry/operator_installer_test.go new file mode 100644 index 0000000000..c6a48b3836 --- /dev/null +++ b/internal/olm/operator/registry/operator_installer_test.go @@ -0,0 +1,400 @@ +// Copyright 2020 The Operator-SDK Authors +// +// 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 registry + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + v1 "github.com/operator-framework/api/pkg/operators/v1" + "github.com/operator-framework/api/pkg/operators/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + crclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/operator-framework/operator-sdk/internal/olm/operator" +) + +var _ = Describe("OperatorInstaller", func() { + Describe("InstallOperator", func() { + // TODO: fill this in once run bundle is done + }) + + Describe("ensureOperatorGroup", func() { + var ( + oi OperatorInstaller + client crclient.Client + ) + BeforeEach(func() { + sch := runtime.NewScheme() + Expect(v1.AddToScheme(sch)).To(Succeed()) + client = fake.NewFakeClientWithScheme(sch) + oi = OperatorInstaller{ + cfg: &operator.Configuration{ + Scheme: sch, + Client: client, + Namespace: "testns", + }, + } + + // setup supported install modes + modes := []v1alpha1.InstallMode{ + { + Type: v1alpha1.InstallModeTypeSingleNamespace, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeOwnNamespace, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeAllNamespaces, + Supported: true, + }, + } + oi.SupportedInstallModes = operator.GetSupportedInstallModes(modes) + }) + It("should return an error when problems finding OperatorGroup", func() { + oi.cfg.Client = fake.NewFakeClient() + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).To(HaveOccurred()) + }) + It("should return an error if there are no supported modes", func() { + oi.SupportedInstallModes = operator.GetSupportedInstallModes([]v1alpha1.InstallMode{}) + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("no supported install modes")) + }) + Context("with no existing OperatorGroup", func() { + Context("given SingleNamespace", func() { + It("should create one with the given target namespaces", func() { + _ = oi.InstallMode.Set(string(v1alpha1.InstallModeTypeSingleNamespace)) + oi.InstallMode.TargetNamespaces = []string{"anotherns"} + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + + og, found, err := oi.getOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + Expect(found).To(BeTrue()) + Expect(og).ToNot(BeNil()) + Expect(og.Name).To(Equal("operator-sdk-og")) + Expect(og.Namespace).To(Equal("testns")) + Expect(og.Spec.TargetNamespaces).To(Equal([]string{"anotherns"})) + }) + It("should return an error if target matches operator ns", func() { + _ = oi.InstallMode.Set(string(v1alpha1.InstallModeTypeSingleNamespace)) + oi.InstallMode.TargetNamespaces = []string{"testns"} + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).ToNot(BeNil()) + Expect(err.Error()).Should(ContainSubstring("use install mode \"OwnNamespace\"")) + }) + }) + Context("given OwnNamespace", func() { + It("should create one with the given target namespaces", func() { + _ = oi.InstallMode.Set(string(v1alpha1.InstallModeTypeOwnNamespace)) + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + + og, found, err := oi.getOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + Expect(found).To(BeTrue()) + Expect(og).ToNot(BeNil()) + Expect(og.Name).To(Equal("operator-sdk-og")) + Expect(og.Namespace).To(Equal("testns")) + Expect(len(og.Spec.TargetNamespaces)).To(Equal(1)) + }) + }) + Context("given AllNamespaces", func() { + It("should create one with the given target namespaces", func() { + _ = oi.InstallMode.Set(string(v1alpha1.InstallModeTypeAllNamespaces)) + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + + og, found, err := oi.getOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + Expect(found).To(BeTrue()) + Expect(og).ToNot(BeNil()) + Expect(og.Name).To(Equal("operator-sdk-og")) + Expect(og.Namespace).To(Equal("testns")) + Expect(len(og.Spec.TargetNamespaces)).To(Equal(0)) + }) + }) + }) + Context("with an existing OperatorGroup", func() { + Context("given AllNamespaces", func() { + BeforeEach(func() { + _ = oi.InstallMode.Set(string(v1alpha1.InstallModeTypeAllNamespaces)) + }) + It("should return nil for AllNamespaces with empty targets", func() { + // context, client, name, ns, targets + oog := createOperatorGroupHelper(context.TODO(), client, "existing-og", "testns") + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + + og, found, err := oi.getOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + Expect(found).To(BeTrue()) + Expect(og.Name).To(Equal(oog.Name)) + Expect(og.Namespace).To(Equal(oog.Namespace)) + }) + It("should return an error for AllNamespaces when target is not empty", func() { + // context, client, name, ns, targets + _ = createOperatorGroupHelper(context.TODO(), client, "existing-og", + "testns", "incompatiblens") + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).To(ContainSubstring("is not compatible")) + }) + }) + Context("given OwnNamespace", func() { + BeforeEach(func() { + _ = oi.InstallMode.Set(string(v1alpha1.InstallModeTypeOwnNamespace)) + }) + It("should return nil for OwnNamespace when target matches operator", func() { + oog := createOperatorGroupHelper(context.TODO(), client, "existing-og", + "testns", "testns") + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + + og, found, err := oi.getOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + Expect(found).To(BeTrue()) + Expect(og.Name).To(Equal(oog.Name)) + Expect(og.Namespace).To(Equal(oog.Namespace)) + }) + It("should return an error for OwnNamespace when target does not match operator", func() { + _ = createOperatorGroupHelper(context.TODO(), client, "existing-og", + "testns", "incompatiblens") + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).To(ContainSubstring("is not compatible")) + }) + }) + Context("given SingleNamespace", func() { + BeforeEach(func() { + _ = oi.InstallMode.Set(string(v1alpha1.InstallModeTypeSingleNamespace)) + }) + It("should return nil for SingleNamespace when target differs from operator", func() { + oi.InstallMode.TargetNamespaces = []string{"anotherns"} + oog := createOperatorGroupHelper(context.TODO(), client, "existing-og", + "testns", "anotherns") + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + + og, found, err := oi.getOperatorGroup(context.TODO()) + Expect(err).To(BeNil()) + Expect(found).To(BeTrue()) + Expect(og.Name).To(Equal(oog.Name)) + Expect(og.Namespace).To(Equal(oog.Namespace)) + }) + It("should return an error for SingleNamespace when target matches operator", func() { + oi.InstallMode.TargetNamespaces = []string{"testns"} + _ = createOperatorGroupHelper(context.TODO(), client, "existing-og", + "testns", "testns") + err := oi.ensureOperatorGroup(context.TODO()) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).To(ContainSubstring("use install mode \"OwnNamespace\"")) + }) + }) + }) + }) + + Describe("createOperatorGroup", func() { + var ( + oi OperatorInstaller + client crclient.Client + ) + BeforeEach(func() { + sch := runtime.NewScheme() + Expect(v1.AddToScheme(sch)).To(Succeed()) + client = fake.NewFakeClientWithScheme(sch) + oi = OperatorInstaller{ + cfg: &operator.Configuration{ + Scheme: sch, + Client: client, + Namespace: "testnamespace", + }, + } + }) + It("should return an error if OperatorGroup already exists", func() { + _ = createOperatorGroupHelper(context.TODO(), client, + operator.SDKOperatorGroupName, "testnamespace") + + og, err := oi.createOperatorGroup(context.TODO(), nil) + Expect(og).Should(BeNil()) + Expect(err).To(HaveOccurred()) + }) + It("should create the OperatorGroup", func() { + og, err := oi.createOperatorGroup(context.TODO(), nil) + Expect(og).ShouldNot(BeNil()) + Expect(og.Name).To(Equal(operator.SDKOperatorGroupName)) + Expect(og.Namespace).To(Equal("testnamespace")) + Expect(err).To(BeNil()) + }) + }) + + Describe("isOperatorGroupCompatible", func() { + var ( + oi OperatorInstaller + og v1.OperatorGroup + ) + BeforeEach(func() { + oi = OperatorInstaller{} + og = createOperatorGroupHelper(context.TODO(), nil, "existing-og", "default", "default") + }) + It("should return an error if namespaces do not match", func() { + oi.InstallMode = operator.InstallMode{ + InstallModeType: v1alpha1.InstallModeTypeOwnNamespace, + TargetNamespaces: []string{"wontmatchns"}, + } + + err := oi.isOperatorGroupCompatible(og, oi.InstallMode.TargetNamespaces) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(ContainSubstring("is not compatible")) + }) + It("should return nil if no installmode is empty", func() { + // empty install mode + oi.InstallMode = operator.InstallMode{} + Expect(oi.InstallMode.IsEmpty()).To(BeTrue()) + err := oi.isOperatorGroupCompatible(og, oi.InstallMode.TargetNamespaces) + Expect(err).Should(BeNil()) + }) + It("should return nil if namespaces match", func() { + oi.InstallMode = operator.InstallMode{ + InstallModeType: v1alpha1.InstallModeTypeOwnNamespace, + TargetNamespaces: []string{"matchingns"}, + } + aog := createOperatorGroupHelper(context.TODO(), nil, "existing-og", "testns", "matchingns") + err := oi.isOperatorGroupCompatible(aog, oi.InstallMode.TargetNamespaces) + Expect(err).Should(BeNil()) + }) + }) + + Describe("getOperatorGroup", func() { + var ( + oi OperatorInstaller + client crclient.Client + ) + BeforeEach(func() { + sch := runtime.NewScheme() + Expect(v1.AddToScheme(sch)).To(Succeed()) + client = fake.NewFakeClientWithScheme(sch) + oi = OperatorInstaller{ + cfg: &operator.Configuration{ + Scheme: sch, + Client: client, + Namespace: "atestns", + }, + } + }) + It("should return an error if no OperatorGroups exist", func() { + oi.cfg.Client = fake.NewFakeClient() + grp, found, err := oi.getOperatorGroup(context.TODO()) + Expect(grp).To(BeNil()) + Expect(found).To(BeFalse()) + Expect(err).To(HaveOccurred()) + }) + It("should return nothing if namespace does not match", func() { + oi.cfg.Namespace = "fakens" + _ = createOperatorGroupHelper(context.TODO(), client, "og1", "atestns") + grp, found, err := oi.getOperatorGroup(context.TODO()) + Expect(grp).To(BeNil()) + Expect(found).To(BeFalse()) + Expect(err).Should(BeNil()) + }) + It("should return an error when more than OperatorGroup found", func() { + _ = createOperatorGroupHelper(context.TODO(), client, "og1", "atestns") + _ = createOperatorGroupHelper(context.TODO(), client, "og2", "atestns") + grp, found, err := oi.getOperatorGroup(context.TODO()) + Expect(grp).To(BeNil()) + Expect(found).To(BeTrue()) + Expect(err).Should(HaveOccurred()) + }) + It("should return list of OperatorGroups", func() { + og := createOperatorGroupHelper(context.TODO(), client, "og1", "atestns") + grp, found, err := oi.getOperatorGroup(context.TODO()) + Expect(grp).ShouldNot(BeNil()) + Expect(grp.Name).To(Equal(og.Name)) + Expect(grp.Namespace).To(Equal(og.Namespace)) + Expect(found).To(BeTrue()) + Expect(err).Should(BeNil()) + }) + }) + + Describe("createSubscription", func() { + // TODO: add them as part of a different story + }) + + Describe("getTargetNamespaces", func() { + var ( + oi OperatorInstaller + supported sets.String + ) + BeforeEach(func() { + oi = OperatorInstaller{ + cfg: &operator.Configuration{}, + } + supported = sets.NewString() + }) + It("should return an error when nothing is supported", func() { + target, err := oi.getTargetNamespaces(supported) + Expect(target).To(BeNil()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("no supported install modes")) + }) + It("should return nothing when AllNamespaces is supported", func() { + supported.Insert(string(v1alpha1.InstallModeTypeAllNamespaces)) + target, err := oi.getTargetNamespaces(supported) + Expect(target).To(BeNil()) + Expect(err).To(BeNil()) + }) + It("should return operator's namespace when OwnNamespace is supported", func() { + oi.cfg.Namespace = "test-ns" + supported.Insert(string(v1alpha1.InstallModeTypeOwnNamespace)) + target, err := oi.getTargetNamespaces(supported) + Expect(len(target)).To(Equal(1)) + Expect(target[0]).To(Equal("test-ns")) + Expect(err).To(BeNil()) + }) + It("should return configured namespace when SingleNamespace is passed in", func() { + + oi.InstallMode = operator.InstallMode{ + InstallModeType: v1alpha1.InstallModeTypeSingleNamespace, + TargetNamespaces: []string{"test-ns"}, + } + + supported.Insert(string(v1alpha1.InstallModeTypeSingleNamespace)) + target, err := oi.getTargetNamespaces(supported) + Expect(len(target)).To(Equal(1)) + Expect(target[0]).To(Equal("test-ns")) + Expect(err).To(BeNil()) + }) + }) +}) + +func createOperatorGroupHelper(ctx context.Context, c crclient.Client, name, namespace string, targetNamespaces ...string) v1.OperatorGroup { + og := v1.OperatorGroup{} + og.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("OperatorGroup")) + og.SetName(name) + og.SetNamespace(namespace) + og.Spec.TargetNamespaces = targetNamespaces + og.Status.Namespaces = targetNamespaces + if c != nil { + ExpectWithOffset(1, c.Create(ctx, &og)).Should(Succeed()) + } + return og +} diff --git a/internal/olm/operator/registry/operatorgroup_test.go b/internal/olm/operator/registry/operatorgroup_test.go deleted file mode 100644 index 1a7aa8403d..0000000000 --- a/internal/olm/operator/registry/operatorgroup_test.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// 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 registry - -import ( - "context" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - v1 "github.com/operator-framework/api/pkg/operators/v1" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - "github.com/operator-framework/operator-sdk/internal/olm/operator" -) - -var _ = Describe("Tenancy", func() { - Describe("createOperatorGroup", func() { - var ( - o *OperatorInstaller - ctx context.Context - err error - - packageName = "test-operator" - namespace = "default" - nonSDKOperatorGroupName = "my-og" - ) - - BeforeEach(func() { - sch := runtime.NewScheme() - Expect(v1.AddToScheme(sch)).To(Succeed()) - o = &OperatorInstaller{ - PackageName: packageName, - cfg: &operator.Configuration{ - Scheme: sch, - Namespace: namespace, - Client: fake.NewFakeClientWithScheme(sch), - }, - } - ctx = context.TODO() - }) - - Context("with no existing OperatorGroup", func() { - It("creates one successfully", func() { - Expect(o.createOperatorGroup(ctx)).To(Succeed()) - og, ogExists, err := o.getOperatorGroup(ctx) - Expect(err).To(BeNil()) - Expect(ogExists).To(BeTrue()) - Expect(og.GetName()).To(Equal(operator.SDKOperatorGroupName)) - }) - }) - - Context("with an existing, valid OperatorGroup", func() { - It("returns no error and the existing SDK OperatorGroup with no target namespaces is unchanged", func() { - existingOG := createOperatorGroupHelper(ctx, o.cfg.Client, operator.SDKOperatorGroupName, namespace) - Expect(o.createOperatorGroup(ctx)).To(Succeed()) - og, ogExists, err := o.getOperatorGroup(ctx) - Expect(err).To(BeNil()) - Expect(ogExists).To(BeTrue()) - Expect(og.GetName()).To(Equal(existingOG.GetName())) - }) - It("returns no error and the existing SDK OperatorGroup with the same set of target namespaces is unchanged", func() { - targetNamespaces := []string{"foo", "bar"} - o.InstallMode.TargetNamespaces = targetNamespaces - existingOG := createOperatorGroupHelper(ctx, o.cfg.Client, operator.SDKOperatorGroupName, namespace, targetNamespaces...) - Expect(o.createOperatorGroup(ctx)).To(Succeed()) - og, ogExists, err := o.getOperatorGroup(ctx) - Expect(err).To(BeNil()) - Expect(ogExists).To(BeTrue()) - Expect(og.GetName()).To(Equal(existingOG.GetName())) - }) - It("returns no error and the existing non-SDK OperatorGroup is unchanged", func() { - existingOG := createOperatorGroupHelper(ctx, o.cfg.Client, nonSDKOperatorGroupName, namespace) - Expect(o.createOperatorGroup(ctx)).To(Succeed()) - og, ogExists, err := o.getOperatorGroup(ctx) - Expect(err).To(BeNil()) - Expect(ogExists).To(BeTrue()) - Expect(og.GetName()).To(Equal(existingOG.GetName())) - }) - It("returns no error and the existing OperatorGroup in another namespace is unchanged", func() { - otherNS := "my-ns" - existingOG := createOperatorGroupHelper(ctx, o.cfg.Client, operator.SDKOperatorGroupName, otherNS) - Expect(o.createOperatorGroup(ctx)).To(Succeed()) - og, ogExists, err := o.getOperatorGroup(ctx) - Expect(err).To(BeNil()) - Expect(ogExists).To(BeTrue()) - Expect(og.GetName()).To(Equal(existingOG.GetName())) - Expect(og.GetNamespace()).NotTo(Equal(existingOG.GetNamespace())) - }) - }) - - Context("with an existing, invalid OperatorGroup", func() { - It("returns an error for an SDK OperatorGroup", func() { - _ = createOperatorGroupHelper(ctx, o.cfg.Client, operator.SDKOperatorGroupName, namespace, "foo") - err = o.createOperatorGroup(ctx) - Expect(err.Error()).To(ContainSubstring(`existing SDK-managed operator group's namespaces ["foo"] do not match desired namespaces []`)) - }) - It("returns an error for a non-SDK OperatorGroup", func() { - _ = createOperatorGroupHelper(ctx, o.cfg.Client, nonSDKOperatorGroupName, namespace, "foo") - err = o.createOperatorGroup(ctx) - Expect(err.Error()).To(ContainSubstring(`existing operator group "my-og"'s namespaces ["foo"] do not match desired namespaces []`)) - }) - }) - }) - -}) - -func createOperatorGroupHelper(ctx context.Context, c client.Client, name, namespace string, targetNamespaces ...string) (og v1.OperatorGroup) { - og.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("OperatorGroup")) - og.SetName(name) - og.SetNamespace(namespace) - og.Status.Namespaces = targetNamespaces - ExpectWithOffset(1, c.Create(ctx, &og)).Should(Succeed()) - return -}