diff --git a/Makefile b/Makefile index 58c34ddfe2..95dd7d6137 100644 --- a/Makefile +++ b/Makefile @@ -123,8 +123,9 @@ clean-openapi: codegen-openapi: clean-openapi pkg/package-server/generated/openapi/zz_generated.openapi.go +# our version of hack/update-codegen.sh codegen: $(CODEGEN) - $(CODEGEN) all $(PKG)/pkg/api/client $(PKG)/pkg/api/apis "operators:v1alpha1" + $(CODEGEN) all $(PKG)/pkg/api/client $(PKG)/pkg/api/apis "operators:v1alpha1,v1alpha2" $(CODEGEN) all $(PKG)/pkg/package-server/client $(PKG)/pkg/package-server/apis "packagemanifest:v1alpha1" verify-codegen: codegen diff --git a/deploy/chart/templates/0000_30_14-operatorgroup.crd.yaml b/deploy/chart/templates/0000_30_14-operatorgroup.crd.yaml new file mode 100644 index 0000000000..b8102198f2 --- /dev/null +++ b/deploy/chart/templates/0000_30_14-operatorgroup.crd.yaml @@ -0,0 +1,49 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: operatorgroups.operators.coreos.com +spec: + group: operators.coreos.com + version: v1alpha2 + versions: + - name: v1alpha2 + served: true + storage: true + names: + plural: operatorgroups + singular: operatorgroup + kind: OperatorGroup + listKind: OperatorGroupList + scope: Namespaced + subresources: + # status enables the status subresource. + status: {} + validation: + openAPIV3Schema: + properties: + spec: + properties: + selector: + type: object + serviceAccountName: + type: string + required: + - selector + type: object + status: + properties: + lastUpdated: + format: date-time + type: string + namespaces: + items: + type: object + type: array + required: + - namespaces + - lastUpdated + type: object + required: + - metadata + - spec + version: v1alpha2 diff --git a/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go b/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go index 9351661b76..1d121ff684 100644 --- a/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go +++ b/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go @@ -195,6 +195,7 @@ const ( CSVReasonReplaced ConditionReason = "Replaced" CSVReasonNeedCertRotation ConditionReason = "NeedCertRotation" CSVReasonAPIServiceResourceIssue ConditionReason = "APIServiceResourceIssue" + CSVReasonCopied ConditionReason = "Copied" ) // Conditions appear in the status as a record of state transitions on the ClusterServiceVersion diff --git a/pkg/api/apis/operators/v1alpha2/doc.go b/pkg/api/apis/operators/v1alpha2/doc.go new file mode 100644 index 0000000000..fcaeb24831 --- /dev/null +++ b/pkg/api/apis/operators/v1alpha2/doc.go @@ -0,0 +1,3 @@ +// +k8s:deepcopy-gen=package +// +groupName=operators.coreos.com +package v1alpha2 diff --git a/pkg/api/apis/operators/v1alpha2/operatorgroup_types.go b/pkg/api/apis/operators/v1alpha2/operatorgroup_types.go new file mode 100644 index 0000000000..0f91c3c5f9 --- /dev/null +++ b/pkg/api/apis/operators/v1alpha2/operatorgroup_types.go @@ -0,0 +1,34 @@ +package v1alpha2 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type OperatorGroupSpec struct { + Selector metav1.LabelSelector `json:"selector,omitempty"` + ServiceAccount corev1.ServiceAccount `json:"serviceAccount,omitempty"` +} + +type OperatorGroupStatus struct { + Namespaces []*corev1.Namespace `json:"namespaces"` + LastUpdated metav1.Time `json:"lastUpdated"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +genclient +type OperatorGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + Spec OperatorGroupSpec `json:"spec"` + Status OperatorGroupStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type OperatorGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []OperatorGroup `json:"items"` +} diff --git a/pkg/api/apis/operators/v1alpha2/register.go b/pkg/api/apis/operators/v1alpha2/register.go new file mode 100644 index 0000000000..340f8b40db --- /dev/null +++ b/pkg/api/apis/operators/v1alpha2/register.go @@ -0,0 +1,50 @@ +package v1alpha2 + +import ( + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + k8sscheme "k8s.io/client-go/kubernetes/scheme" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators" +) + +const ( + GroupName = "operators.coreos.com" + GroupVersion = "v1alpha2" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: operators.GroupName, Version: GroupVersion} + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme + serScheme = runtime.NewScheme() +) + +func init() { + k8sscheme.AddToScheme(serScheme) + scheme.AddToScheme(serScheme) +} + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &OperatorGroup{}, + &OperatorGroupList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/api/apis/operators/v1alpha2/zz_generated.deepcopy.go b/pkg/api/apis/operators/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 0000000000..e3d6bb2aa5 --- /dev/null +++ b/pkg/api/apis/operators/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,134 @@ +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + v1 "k8s.io/api/core/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorGroup) DeepCopyInto(out *OperatorGroup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorGroup. +func (in *OperatorGroup) DeepCopy() *OperatorGroup { + if in == nil { + return nil + } + out := new(OperatorGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OperatorGroup) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorGroupList) DeepCopyInto(out *OperatorGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OperatorGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorGroupList. +func (in *OperatorGroupList) DeepCopy() *OperatorGroupList { + if in == nil { + return nil + } + out := new(OperatorGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OperatorGroupList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorGroupSpec) DeepCopyInto(out *OperatorGroupSpec) { + *out = *in + in.Selector.DeepCopyInto(&out.Selector) + in.ServiceAccount.DeepCopyInto(&out.ServiceAccount) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorGroupSpec. +func (in *OperatorGroupSpec) DeepCopy() *OperatorGroupSpec { + if in == nil { + return nil + } + out := new(OperatorGroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorGroupStatus) DeepCopyInto(out *OperatorGroupStatus) { + *out = *in + if in.Namespaces != nil { + in, out := &in.Namespaces, &out.Namespaces + *out = make([]*v1.Namespace, len(*in)) + for i := range *in { + if (*in)[i] == nil { + (*out)[i] = nil + } else { + (*out)[i] = new(v1.Namespace) + (*in)[i].DeepCopyInto((*out)[i]) + } + } + } + in.LastUpdated.DeepCopyInto(&out.LastUpdated) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorGroupStatus. +func (in *OperatorGroupStatus) DeepCopy() *OperatorGroupStatus { + if in == nil { + return nil + } + out := new(OperatorGroupStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/client/clientset/versioned/clientset.go b/pkg/api/client/clientset/versioned/clientset.go index bd96eb1e1c..c95ecaa46e 100644 --- a/pkg/api/client/clientset/versioned/clientset.go +++ b/pkg/api/client/clientset/versioned/clientset.go @@ -20,6 +20,7 @@ package versioned import ( operatorsv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/typed/operators/v1alpha1" + operatorsv1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/typed/operators/v1alpha2" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" @@ -30,6 +31,7 @@ type Interface interface { OperatorsV1alpha1() operatorsv1alpha1.OperatorsV1alpha1Interface // Deprecated: please explicitly pick a version if possible. Operators() operatorsv1alpha1.OperatorsV1alpha1Interface + OperatorsV1alpha2() operatorsv1alpha2.OperatorsV1alpha2Interface } // Clientset contains the clients for groups. Each group has exactly one @@ -37,6 +39,7 @@ type Interface interface { type Clientset struct { *discovery.DiscoveryClient operatorsV1alpha1 *operatorsv1alpha1.OperatorsV1alpha1Client + operatorsV1alpha2 *operatorsv1alpha2.OperatorsV1alpha2Client } // OperatorsV1alpha1 retrieves the OperatorsV1alpha1Client @@ -50,6 +53,11 @@ func (c *Clientset) Operators() operatorsv1alpha1.OperatorsV1alpha1Interface { return c.operatorsV1alpha1 } +// OperatorsV1alpha2 retrieves the OperatorsV1alpha2Client +func (c *Clientset) OperatorsV1alpha2() operatorsv1alpha2.OperatorsV1alpha2Interface { + return c.operatorsV1alpha2 +} + // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { if c == nil { @@ -70,6 +78,10 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { if err != nil { return nil, err } + cs.operatorsV1alpha2, err = operatorsv1alpha2.NewForConfig(&configShallowCopy) + if err != nil { + return nil, err + } cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) if err != nil { @@ -83,6 +95,7 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { func NewForConfigOrDie(c *rest.Config) *Clientset { var cs Clientset cs.operatorsV1alpha1 = operatorsv1alpha1.NewForConfigOrDie(c) + cs.operatorsV1alpha2 = operatorsv1alpha2.NewForConfigOrDie(c) cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) return &cs @@ -92,6 +105,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset { func New(c rest.Interface) *Clientset { var cs Clientset cs.operatorsV1alpha1 = operatorsv1alpha1.New(c) + cs.operatorsV1alpha2 = operatorsv1alpha2.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs diff --git a/pkg/api/client/clientset/versioned/fake/clientset_generated.go b/pkg/api/client/clientset/versioned/fake/clientset_generated.go index 2432a97b7f..361be4e7d3 100644 --- a/pkg/api/client/clientset/versioned/fake/clientset_generated.go +++ b/pkg/api/client/clientset/versioned/fake/clientset_generated.go @@ -22,6 +22,8 @@ import ( clientset "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" operatorsv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/typed/operators/v1alpha1" fakeoperatorsv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/typed/operators/v1alpha1/fake" + operatorsv1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/typed/operators/v1alpha2" + fakeoperatorsv1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/fake" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" @@ -80,3 +82,8 @@ func (c *Clientset) OperatorsV1alpha1() operatorsv1alpha1.OperatorsV1alpha1Inter func (c *Clientset) Operators() operatorsv1alpha1.OperatorsV1alpha1Interface { return &fakeoperatorsv1alpha1.FakeOperatorsV1alpha1{Fake: &c.Fake} } + +// OperatorsV1alpha2 retrieves the OperatorsV1alpha2Client +func (c *Clientset) OperatorsV1alpha2() operatorsv1alpha2.OperatorsV1alpha2Interface { + return &fakeoperatorsv1alpha2.FakeOperatorsV1alpha2{Fake: &c.Fake} +} diff --git a/pkg/api/client/clientset/versioned/fake/register.go b/pkg/api/client/clientset/versioned/fake/register.go index 3f47492a50..60a2974bc4 100644 --- a/pkg/api/client/clientset/versioned/fake/register.go +++ b/pkg/api/client/clientset/versioned/fake/register.go @@ -20,6 +20,7 @@ package fake import ( operatorsv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + operatorsv1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -51,4 +52,5 @@ func init() { // correctly. func AddToScheme(scheme *runtime.Scheme) { operatorsv1alpha1.AddToScheme(scheme) + operatorsv1alpha2.AddToScheme(scheme) } diff --git a/pkg/api/client/clientset/versioned/scheme/register.go b/pkg/api/client/clientset/versioned/scheme/register.go index 213c024cdb..d47c71b65b 100644 --- a/pkg/api/client/clientset/versioned/scheme/register.go +++ b/pkg/api/client/clientset/versioned/scheme/register.go @@ -20,6 +20,7 @@ package scheme import ( operatorsv1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + operatorsv1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -51,4 +52,5 @@ func init() { // correctly. func AddToScheme(scheme *runtime.Scheme) { operatorsv1alpha1.AddToScheme(scheme) + operatorsv1alpha2.AddToScheme(scheme) } diff --git a/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/doc.go b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/doc.go new file mode 100644 index 0000000000..baaf2d9853 --- /dev/null +++ b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha2 diff --git a/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/fake/doc.go b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/fake/doc.go new file mode 100644 index 0000000000..16f4439906 --- /dev/null +++ b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/fake/fake_operatorgroup.go b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/fake/fake_operatorgroup.go new file mode 100644 index 0000000000..361b106a5d --- /dev/null +++ b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/fake/fake_operatorgroup.go @@ -0,0 +1,140 @@ +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeOperatorGroups implements OperatorGroupInterface +type FakeOperatorGroups struct { + Fake *FakeOperatorsV1alpha2 + ns string +} + +var operatorgroupsResource = schema.GroupVersionResource{Group: "operators.coreos.com", Version: "v1alpha2", Resource: "operatorgroups"} + +var operatorgroupsKind = schema.GroupVersionKind{Group: "operators.coreos.com", Version: "v1alpha2", Kind: "OperatorGroup"} + +// Get takes name of the operatorGroup, and returns the corresponding operatorGroup object, and an error if there is any. +func (c *FakeOperatorGroups) Get(name string, options v1.GetOptions) (result *v1alpha2.OperatorGroup, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(operatorgroupsResource, c.ns, name), &v1alpha2.OperatorGroup{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.OperatorGroup), err +} + +// List takes label and field selectors, and returns the list of OperatorGroups that match those selectors. +func (c *FakeOperatorGroups) List(opts v1.ListOptions) (result *v1alpha2.OperatorGroupList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(operatorgroupsResource, operatorgroupsKind, c.ns, opts), &v1alpha2.OperatorGroupList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha2.OperatorGroupList{ListMeta: obj.(*v1alpha2.OperatorGroupList).ListMeta} + for _, item := range obj.(*v1alpha2.OperatorGroupList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested operatorGroups. +func (c *FakeOperatorGroups) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(operatorgroupsResource, c.ns, opts)) + +} + +// Create takes the representation of a operatorGroup and creates it. Returns the server's representation of the operatorGroup, and an error, if there is any. +func (c *FakeOperatorGroups) Create(operatorGroup *v1alpha2.OperatorGroup) (result *v1alpha2.OperatorGroup, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(operatorgroupsResource, c.ns, operatorGroup), &v1alpha2.OperatorGroup{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.OperatorGroup), err +} + +// Update takes the representation of a operatorGroup and updates it. Returns the server's representation of the operatorGroup, and an error, if there is any. +func (c *FakeOperatorGroups) Update(operatorGroup *v1alpha2.OperatorGroup) (result *v1alpha2.OperatorGroup, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(operatorgroupsResource, c.ns, operatorGroup), &v1alpha2.OperatorGroup{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.OperatorGroup), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeOperatorGroups) UpdateStatus(operatorGroup *v1alpha2.OperatorGroup) (*v1alpha2.OperatorGroup, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(operatorgroupsResource, "status", c.ns, operatorGroup), &v1alpha2.OperatorGroup{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.OperatorGroup), err +} + +// Delete takes name of the operatorGroup and deletes it. Returns an error if one occurs. +func (c *FakeOperatorGroups) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(operatorgroupsResource, c.ns, name), &v1alpha2.OperatorGroup{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeOperatorGroups) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(operatorgroupsResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha2.OperatorGroupList{}) + return err +} + +// Patch applies the patch and returns the patched operatorGroup. +func (c *FakeOperatorGroups) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.OperatorGroup, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(operatorgroupsResource, c.ns, name, data, subresources...), &v1alpha2.OperatorGroup{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.OperatorGroup), err +} diff --git a/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/fake/fake_operators_client.go b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/fake/fake_operators_client.go new file mode 100644 index 0000000000..3439aca18b --- /dev/null +++ b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/fake/fake_operators_client.go @@ -0,0 +1,40 @@ +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/typed/operators/v1alpha2" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeOperatorsV1alpha2 struct { + *testing.Fake +} + +func (c *FakeOperatorsV1alpha2) OperatorGroups(namespace string) v1alpha2.OperatorGroupInterface { + return &FakeOperatorGroups{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeOperatorsV1alpha2) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/generated_expansion.go b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/generated_expansion.go new file mode 100644 index 0000000000..00cf395818 --- /dev/null +++ b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/generated_expansion.go @@ -0,0 +1,21 @@ +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +type OperatorGroupExpansion interface{} diff --git a/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/operatorgroup.go b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/operatorgroup.go new file mode 100644 index 0000000000..5ba1a5085b --- /dev/null +++ b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/operatorgroup.go @@ -0,0 +1,174 @@ +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + v1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha2" + scheme "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// OperatorGroupsGetter has a method to return a OperatorGroupInterface. +// A group's client should implement this interface. +type OperatorGroupsGetter interface { + OperatorGroups(namespace string) OperatorGroupInterface +} + +// OperatorGroupInterface has methods to work with OperatorGroup resources. +type OperatorGroupInterface interface { + Create(*v1alpha2.OperatorGroup) (*v1alpha2.OperatorGroup, error) + Update(*v1alpha2.OperatorGroup) (*v1alpha2.OperatorGroup, error) + UpdateStatus(*v1alpha2.OperatorGroup) (*v1alpha2.OperatorGroup, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha2.OperatorGroup, error) + List(opts v1.ListOptions) (*v1alpha2.OperatorGroupList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.OperatorGroup, err error) + OperatorGroupExpansion +} + +// operatorGroups implements OperatorGroupInterface +type operatorGroups struct { + client rest.Interface + ns string +} + +// newOperatorGroups returns a OperatorGroups +func newOperatorGroups(c *OperatorsV1alpha2Client, namespace string) *operatorGroups { + return &operatorGroups{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the operatorGroup, and returns the corresponding operatorGroup object, and an error if there is any. +func (c *operatorGroups) Get(name string, options v1.GetOptions) (result *v1alpha2.OperatorGroup, err error) { + result = &v1alpha2.OperatorGroup{} + err = c.client.Get(). + Namespace(c.ns). + Resource("operatorgroups"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of OperatorGroups that match those selectors. +func (c *operatorGroups) List(opts v1.ListOptions) (result *v1alpha2.OperatorGroupList, err error) { + result = &v1alpha2.OperatorGroupList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("operatorgroups"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested operatorGroups. +func (c *operatorGroups) Watch(opts v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("operatorgroups"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a operatorGroup and creates it. Returns the server's representation of the operatorGroup, and an error, if there is any. +func (c *operatorGroups) Create(operatorGroup *v1alpha2.OperatorGroup) (result *v1alpha2.OperatorGroup, err error) { + result = &v1alpha2.OperatorGroup{} + err = c.client.Post(). + Namespace(c.ns). + Resource("operatorgroups"). + Body(operatorGroup). + Do(). + Into(result) + return +} + +// Update takes the representation of a operatorGroup and updates it. Returns the server's representation of the operatorGroup, and an error, if there is any. +func (c *operatorGroups) Update(operatorGroup *v1alpha2.OperatorGroup) (result *v1alpha2.OperatorGroup, err error) { + result = &v1alpha2.OperatorGroup{} + err = c.client.Put(). + Namespace(c.ns). + Resource("operatorgroups"). + Name(operatorGroup.Name). + Body(operatorGroup). + Do(). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + +func (c *operatorGroups) UpdateStatus(operatorGroup *v1alpha2.OperatorGroup) (result *v1alpha2.OperatorGroup, err error) { + result = &v1alpha2.OperatorGroup{} + err = c.client.Put(). + Namespace(c.ns). + Resource("operatorgroups"). + Name(operatorGroup.Name). + SubResource("status"). + Body(operatorGroup). + Do(). + Into(result) + return +} + +// Delete takes name of the operatorGroup and deletes it. Returns an error if one occurs. +func (c *operatorGroups) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("operatorgroups"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *operatorGroups) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("operatorgroups"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched operatorGroup. +func (c *operatorGroups) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.OperatorGroup, err error) { + result = &v1alpha2.OperatorGroup{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("operatorgroups"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/operators_client.go b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/operators_client.go new file mode 100644 index 0000000000..8db2a72fd9 --- /dev/null +++ b/pkg/api/client/clientset/versioned/typed/operators/v1alpha2/operators_client.go @@ -0,0 +1,90 @@ +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + v1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha2" + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/scheme" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + rest "k8s.io/client-go/rest" +) + +type OperatorsV1alpha2Interface interface { + RESTClient() rest.Interface + OperatorGroupsGetter +} + +// OperatorsV1alpha2Client is used to interact with features provided by the operators.coreos.com group. +type OperatorsV1alpha2Client struct { + restClient rest.Interface +} + +func (c *OperatorsV1alpha2Client) OperatorGroups(namespace string) OperatorGroupInterface { + return newOperatorGroups(c, namespace) +} + +// NewForConfig creates a new OperatorsV1alpha2Client for the given config. +func NewForConfig(c *rest.Config) (*OperatorsV1alpha2Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientFor(&config) + if err != nil { + return nil, err + } + return &OperatorsV1alpha2Client{client}, nil +} + +// NewForConfigOrDie creates a new OperatorsV1alpha2Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *OperatorsV1alpha2Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new OperatorsV1alpha2Client for the given RESTClient. +func New(c rest.Interface) *OperatorsV1alpha2Client { + return &OperatorsV1alpha2Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha2.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *OperatorsV1alpha2Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/api/client/informers/externalversions/generic.go b/pkg/api/client/informers/externalversions/generic.go index 6527f494ca..e41df75b33 100644 --- a/pkg/api/client/informers/externalversions/generic.go +++ b/pkg/api/client/informers/externalversions/generic.go @@ -22,6 +22,7 @@ import ( "fmt" v1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + v1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha2" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) @@ -62,6 +63,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource case v1alpha1.SchemeGroupVersion.WithResource("subscriptions"): return &genericInformer{resource: resource.GroupResource(), informer: f.Operators().V1alpha1().Subscriptions().Informer()}, nil + // Group=operators.coreos.com, Version=v1alpha2 + case v1alpha2.SchemeGroupVersion.WithResource("operatorgroups"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Operators().V1alpha2().OperatorGroups().Informer()}, nil + } return nil, fmt.Errorf("no informer found for %v", resource) diff --git a/pkg/api/client/informers/externalversions/operators/interface.go b/pkg/api/client/informers/externalversions/operators/interface.go index 1758289a88..8ffded8769 100644 --- a/pkg/api/client/informers/externalversions/operators/interface.go +++ b/pkg/api/client/informers/externalversions/operators/interface.go @@ -21,12 +21,15 @@ package operators import ( internalinterfaces "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions/internalinterfaces" v1alpha1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions/operators/v1alpha1" + v1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions/operators/v1alpha2" ) // Interface provides access to each of this group's versions. type Interface interface { // V1alpha1 provides access to shared informers for resources in V1alpha1. V1alpha1() v1alpha1.Interface + // V1alpha2 provides access to shared informers for resources in V1alpha2. + V1alpha2() v1alpha2.Interface } type group struct { @@ -44,3 +47,8 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList func (g *group) V1alpha1() v1alpha1.Interface { return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) } + +// V1alpha2 returns a new v1alpha2.Interface. +func (g *group) V1alpha2() v1alpha2.Interface { + return v1alpha2.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/api/client/informers/externalversions/operators/v1alpha2/interface.go b/pkg/api/client/informers/externalversions/operators/v1alpha2/interface.go new file mode 100644 index 0000000000..2619496111 --- /dev/null +++ b/pkg/api/client/informers/externalversions/operators/v1alpha2/interface.go @@ -0,0 +1,45 @@ +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + internalinterfaces "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // OperatorGroups returns a OperatorGroupInformer. + OperatorGroups() OperatorGroupInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// OperatorGroups returns a OperatorGroupInformer. +func (v *version) OperatorGroups() OperatorGroupInformer { + return &operatorGroupInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/api/client/informers/externalversions/operators/v1alpha2/operatorgroup.go b/pkg/api/client/informers/externalversions/operators/v1alpha2/operatorgroup.go new file mode 100644 index 0000000000..952c737574 --- /dev/null +++ b/pkg/api/client/informers/externalversions/operators/v1alpha2/operatorgroup.go @@ -0,0 +1,89 @@ +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + time "time" + + operators_v1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha2" + versioned "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" + internalinterfaces "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions/internalinterfaces" + v1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// OperatorGroupInformer provides access to a shared informer and lister for +// OperatorGroups. +type OperatorGroupInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha2.OperatorGroupLister +} + +type operatorGroupInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewOperatorGroupInformer constructs a new informer for OperatorGroup type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewOperatorGroupInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredOperatorGroupInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredOperatorGroupInformer constructs a new informer for OperatorGroup type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredOperatorGroupInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OperatorsV1alpha2().OperatorGroups(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OperatorsV1alpha2().OperatorGroups(namespace).Watch(options) + }, + }, + &operators_v1alpha2.OperatorGroup{}, + resyncPeriod, + indexers, + ) +} + +func (f *operatorGroupInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredOperatorGroupInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *operatorGroupInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&operators_v1alpha2.OperatorGroup{}, f.defaultInformer) +} + +func (f *operatorGroupInformer) Lister() v1alpha2.OperatorGroupLister { + return v1alpha2.NewOperatorGroupLister(f.Informer().GetIndexer()) +} diff --git a/pkg/api/client/listers/operators/v1alpha2/expansion_generated.go b/pkg/api/client/listers/operators/v1alpha2/expansion_generated.go new file mode 100644 index 0000000000..aaf9e38cc1 --- /dev/null +++ b/pkg/api/client/listers/operators/v1alpha2/expansion_generated.go @@ -0,0 +1,27 @@ +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +// OperatorGroupListerExpansion allows custom methods to be added to +// OperatorGroupLister. +type OperatorGroupListerExpansion interface{} + +// OperatorGroupNamespaceListerExpansion allows custom methods to be added to +// OperatorGroupNamespaceLister. +type OperatorGroupNamespaceListerExpansion interface{} diff --git a/pkg/api/client/listers/operators/v1alpha2/operatorgroup.go b/pkg/api/client/listers/operators/v1alpha2/operatorgroup.go new file mode 100644 index 0000000000..f07276e62d --- /dev/null +++ b/pkg/api/client/listers/operators/v1alpha2/operatorgroup.go @@ -0,0 +1,94 @@ +/* +Copyright The Kubernetes 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + v1alpha2 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha2" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// OperatorGroupLister helps list OperatorGroups. +type OperatorGroupLister interface { + // List lists all OperatorGroups in the indexer. + List(selector labels.Selector) (ret []*v1alpha2.OperatorGroup, err error) + // OperatorGroups returns an object that can list and get OperatorGroups. + OperatorGroups(namespace string) OperatorGroupNamespaceLister + OperatorGroupListerExpansion +} + +// operatorGroupLister implements the OperatorGroupLister interface. +type operatorGroupLister struct { + indexer cache.Indexer +} + +// NewOperatorGroupLister returns a new OperatorGroupLister. +func NewOperatorGroupLister(indexer cache.Indexer) OperatorGroupLister { + return &operatorGroupLister{indexer: indexer} +} + +// List lists all OperatorGroups in the indexer. +func (s *operatorGroupLister) List(selector labels.Selector) (ret []*v1alpha2.OperatorGroup, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.OperatorGroup)) + }) + return ret, err +} + +// OperatorGroups returns an object that can list and get OperatorGroups. +func (s *operatorGroupLister) OperatorGroups(namespace string) OperatorGroupNamespaceLister { + return operatorGroupNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// OperatorGroupNamespaceLister helps list and get OperatorGroups. +type OperatorGroupNamespaceLister interface { + // List lists all OperatorGroups in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha2.OperatorGroup, err error) + // Get retrieves the OperatorGroup from the indexer for a given namespace and name. + Get(name string) (*v1alpha2.OperatorGroup, error) + OperatorGroupNamespaceListerExpansion +} + +// operatorGroupNamespaceLister implements the OperatorGroupNamespaceLister +// interface. +type operatorGroupNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all OperatorGroups in the indexer for a given namespace. +func (s operatorGroupNamespaceLister) List(selector labels.Selector) (ret []*v1alpha2.OperatorGroup, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.OperatorGroup)) + }) + return ret, err +} + +// Get retrieves the OperatorGroup from the indexer for a given namespace and name. +func (s operatorGroupNamespaceLister) Get(name string) (*v1alpha2.OperatorGroup, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha2.Resource("operatorgroup"), name) + } + return obj.(*v1alpha2.OperatorGroup), nil +} diff --git a/pkg/controller/annotator/annotator.go b/pkg/controller/annotator/annotator.go index 2100e084a2..538c24323a 100644 --- a/pkg/controller/annotator/annotator.go +++ b/pkg/controller/annotator/annotator.go @@ -85,7 +85,9 @@ func (a *Annotator) getNamespaces(namespaceNames []string) (namespaces []corev1. return namespaces, nil } -func (a *Annotator) AnnotateNamespace(namespace *corev1.Namespace) error { +func (a *Annotator) AnnotateNamespace(origNamespace *corev1.Namespace) error { + namespace := origNamespace.DeepCopy() + originalName := namespace.GetName() originalData, err := json.Marshal(namespace) if err != nil { diff --git a/pkg/controller/operators/olm/operator.go b/pkg/controller/operators/olm/operator.go index d39e3a8f57..943431c8cc 100644 --- a/pkg/controller/operators/olm/operator.go +++ b/pkg/controller/operators/olm/operator.go @@ -3,11 +3,14 @@ package olm import ( "errors" "fmt" + "strings" "time" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions" + csvlister "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha1" + operatorgrouplister "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/listers/operators/v1alpha2" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/annotator" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/certs" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" @@ -21,6 +24,7 @@ import ( "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/informers" "k8s.io/client-go/tools/cache" @@ -32,19 +36,24 @@ import ( var ErrRequirementsNotMet = errors.New("requirements were not met") var ErrCRDOwnerConflict = errors.New("CRD owned by another ClusterServiceVersion") +//TODO(jpeeler): copied from catalog/operator.go +var timeNow = func() metav1.Time { return metav1.NewTime(time.Now().UTC()) } + const ( FallbackWakeupInterval = 30 * time.Second ) type Operator struct { *queueinformer.Operator - csvQueue workqueue.RateLimitingInterface - client versioned.Interface - resolver install.StrategyResolverInterface - lister operatorlister.OperatorLister - annotator *annotator.Annotator - recorder record.EventRecorder - cleanupFunc func() + csvQueue workqueue.RateLimitingInterface + client versioned.Interface + resolver install.StrategyResolverInterface + lister operatorlister.OperatorLister + csvLister map[string]csvlister.ClusterServiceVersionLister + operatorGroupLister map[string]operatorgrouplister.OperatorGroupLister + annotator *annotator.Annotator + recorder record.EventRecorder + cleanupFunc func() } func NewOperator(crClient versioned.Interface, opClient operatorclient.ClientInterface, resolver install.StrategyResolverInterface, wakeupInterval time.Duration, annotations map[string]string, namespaces []string) (*Operator, error) { @@ -77,28 +86,25 @@ func NewOperator(crClient versioned.Interface, opClient operatorclient.ClientInt }, } - // If watching all namespaces, set up a watch to annotate new namespaces - if len(namespaces) == 1 && namespaces[0] == metav1.NamespaceAll { - log.Debug("watching all namespaces, setting up queue") - namespaceInformer := informers.NewSharedInformerFactory(queueOperator.OpClient.KubernetesInterface(), wakeupInterval).Core().V1().Namespaces() - queueInformer := queueinformer.NewInformer( - workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "namespaces"), - namespaceInformer.Informer(), - op.annotateNamespace, - nil, - "namespace", - metrics.NewMetricsNil(), - ) - op.RegisterQueueInformer(queueInformer) - op.lister.CoreV1().RegisterNamespaceLister(namespaceInformer.Lister()) - } - // Set up RBAC informers informerFactory := informers.NewSharedInformerFactory(opClient.KubernetesInterface(), wakeupInterval) roleInformer := informerFactory.Rbac().V1().Roles() roleBindingInformer := informerFactory.Rbac().V1().RoleBindings() clusterRoleInformer := informerFactory.Rbac().V1().ClusterRoles() clusterRoleBindingInformer := informerFactory.Rbac().V1().ClusterRoleBindings() + namespaceInformer := informerFactory.Core().V1().Namespaces() + + // register namespace queueinformer + queueInformer := queueinformer.NewInformer( + workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "namespaces"), + namespaceInformer.Informer(), + op.syncNamespace, + nil, + "namespace", + metrics.NewMetricsNil(), + ) + op.RegisterQueueInformer(queueInformer) + op.lister.CoreV1().RegisterNamespaceLister(namespaceInformer.Lister()) // Register RBAC QueueInformers rbacInformers := []cache.SharedIndexInformer{ @@ -164,21 +170,27 @@ func NewOperator(crClient versioned.Interface, opClient operatorclient.ClientInt // Set up watch on CSVs csvInformers := []cache.SharedIndexInformer{} + csvLister := map[string]csvlister.ClusterServiceVersionLister{} for _, namespace := range namespaces { log.Debugf("watching for CSVs in namespace %s", namespace) sharedInformerFactory := externalversions.NewSharedInformerFactoryWithOptions(crClient, wakeupInterval, externalversions.WithNamespace(namespace)) - informer := sharedInformerFactory.Operators().V1alpha1().ClusterServiceVersions().Informer() - csvInformers = append(csvInformers, informer) + informer := sharedInformerFactory.Operators().V1alpha1().ClusterServiceVersions() + csvInformers = append(csvInformers, informer.Informer()) + csvLister[namespace] = informer.Lister() } + op.csvLister = csvLister // csvInformers for each namespace all use the same backing queue // queue keys are namespaced csvQueue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "clusterserviceversions") + csvHandlers := cache.ResourceEventHandlerFuncs{ + DeleteFunc: op.deleteClusterServiceVersion, + } queueInformers := queueinformer.New( csvQueue, csvInformers, op.syncClusterServiceVersion, - nil, + &csvHandlers, "csv", metrics.NewMetricsCSV(op.client), ) @@ -191,8 +203,10 @@ func NewOperator(crClient versioned.Interface, opClient operatorclient.ClientInt depInformers := []cache.SharedIndexInformer{} for _, namespace := range namespaces { log.Debugf("watching deployments in namespace %s", namespace) - informer := informers.NewSharedInformerFactoryWithOptions(opClient.KubernetesInterface(), wakeupInterval, informers.WithNamespace(namespace)).Apps().V1().Deployments().Informer() - depInformers = append(depInformers, informer) + informerFactory := informers.NewSharedInformerFactoryWithOptions(opClient.KubernetesInterface(), wakeupInterval, informers.WithNamespace(namespace)) + informer := informerFactory.Apps().V1().Deployments() + depInformers = append(depInformers, informer.Informer()) + op.lister.AppsV1().RegisterDeploymentLister(namespace, informer.Lister()) } depQueue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "csv-deployments") @@ -207,6 +221,31 @@ func NewOperator(crClient versioned.Interface, opClient operatorclient.ClientInt for _, informer := range depQueueInformers { op.RegisterQueueInformer(informer) } + + // Create an informer for the operator group + operatorGroupInformers := []cache.SharedIndexInformer{} + op.operatorGroupLister = make(map[string]operatorgrouplister.OperatorGroupLister, len(namespaces)) + for _, namespace := range namespaces { + informerFactory := externalversions.NewSharedInformerFactoryWithOptions(crClient, wakeupInterval, externalversions.WithNamespace(namespace)) + informer := informerFactory.Operators().V1alpha2().OperatorGroups() + operatorGroupInformers = append(operatorGroupInformers, informer.Informer()) + op.operatorGroupLister[namespace] = informer.Lister() + } + + // Register OperatorGroup informers. + operatorGroupQueue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "operatorgroups") + operatorGroupQueueInformer := queueinformer.New( + operatorGroupQueue, + operatorGroupInformers, + op.syncOperatorGroups, + nil, + "operatorgroups", + metrics.NewMetricsNil(), + ) + for _, informer := range operatorGroupQueueInformer { + op.RegisterQueueInformer(informer) + } + return op, nil } @@ -318,6 +357,65 @@ func (a *Operator) syncServices(obj interface{}) (syncError error) { return nil } +func (a *Operator) deleteClusterServiceVersion(obj interface{}) { + clusterServiceVersion, ok := obj.(*v1alpha1.ClusterServiceVersion) + if !ok { + log.Debugf("wrong type: %#v", obj) + return + } + + logger := log.WithFields(log.Fields{ + "csv": clusterServiceVersion.GetName(), + "namespace": clusterServiceVersion.GetNamespace(), + "phase": clusterServiceVersion.Status.Phase, + }) + + targetNamespaces, ok := clusterServiceVersion.Annotations["olm.targetNamespaces"] + if !ok { + logger.Debugf("Ignoring CSV with no annotation") + } + + operatorNamespace, ok := clusterServiceVersion.Annotations["olm.operatorNamespace"] + if !ok { + logger.Debugf("missing operator namespace annotation on CSV") + } + + if clusterServiceVersion.Status.Reason == v1alpha1.CSVReasonCopied { + return + } + logger.Info("parent CSV deleted, GC children") + for _, namespace := range strings.Split(targetNamespaces, ",") { + if namespace != operatorNamespace { + if err := a.client.OperatorsV1alpha1().ClusterServiceVersions(namespace).Delete(clusterServiceVersion.GetName(), &metav1.DeleteOptions{}); err != nil { + logger.WithError(err).Debug("error deleting child CSV") + } + } + } +} + +func (a *Operator) removeDanglingChildCSVs(csv *v1alpha1.ClusterServiceVersion) error { + logger := log.WithFields(log.Fields{ + "csv": csv.GetName(), + "namespace": csv.GetNamespace(), + "phase": csv.Status.Phase, + }) + + operatorNamespace, ok := csv.Annotations["olm.operatorNamespace"] + if !ok { + logger.Debug("missing operator namespace annotation on copied CSV") + return fmt.Errorf("missing operator namespace annotation on copied CSV") + } + + _, err := a.csvLister[operatorNamespace].ClusterServiceVersions(operatorNamespace).Get(csv.GetName()) + if k8serrors.IsNotFound(err) || k8serrors.IsGone(err) { + logger.Debug("deleting CSV since parent is missing") + if err := a.client.OperatorsV1alpha1().ClusterServiceVersions(csv.GetNamespace()).Delete(csv.GetName(), &metav1.DeleteOptions{}); err != nil { + return err + } + } + return nil +} + // syncClusterServiceVersion is the method that gets called when we see a CSV event in the cluster func (a *Operator) syncClusterServiceVersion(obj interface{}) (syncError error) { clusterServiceVersion, ok := obj.(*v1alpha1.ClusterServiceVersion) @@ -330,7 +428,13 @@ func (a *Operator) syncClusterServiceVersion(obj interface{}) (syncError error) "namespace": clusterServiceVersion.GetNamespace(), "phase": clusterServiceVersion.Status.Phase, }) - logger.Info("syncing") + + if clusterServiceVersion.Status.Reason == v1alpha1.CSVReasonCopied { + logger.Info("skip sync of dummy CSV") + return a.removeDanglingChildCSVs(clusterServiceVersion) + } + + logger.Info("syncing CSV") outCSV, syncError := a.transitionCSVState(*clusterServiceVersion) @@ -625,19 +729,21 @@ func (a *Operator) crdOwnerConflicts(in *v1alpha1.ClusterServiceVersion, csvsInN return nil } -// annotateNamespace is the method that gets called when we see a namespace event in the cluster -func (a *Operator) annotateNamespace(obj interface{}) (syncError error) { +// syncNamespace is the method that gets called when we see a namespace event in the cluster +func (a *Operator) syncNamespace(obj interface{}) (syncError error) { namespace, ok := obj.(*corev1.Namespace) if !ok { log.Debugf("wrong type: %#v", obj) return fmt.Errorf("casting Namespace failed") } + namespaceName := namespace.GetName() - log.Infof("syncing Namespace: %s", namespace.GetName()) + log.Infof("syncing Namespace: %s", namespaceName) if err := a.annotator.AnnotateNamespace(namespace); err != nil { - log.Infof("error annotating namespace '%s'", namespace.GetName()) + log.Infof("error annotating namespace '%s'", namespaceName) return err } + return nil } diff --git a/pkg/controller/operators/olm/operator_test.go b/pkg/controller/operators/olm/operator_test.go index e9507a4467..0bc459a021 100644 --- a/pkg/controller/operators/olm/operator_test.go +++ b/pkg/controller/operators/olm/operator_test.go @@ -17,12 +17,14 @@ import ( "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextensionsfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" k8sfake "k8s.io/client-go/kubernetes/fake" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" apiregistrationfake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha2" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/fake" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" @@ -99,17 +101,18 @@ func apiResourcesForObjects(objs []runtime.Object) []*metav1.APIResourceList { return apis } -func NewFakeOperator(clientObjs []runtime.Object, k8sObjs []runtime.Object, extObjs []runtime.Object, regObjs []runtime.Object, resolver install.StrategyResolverInterface, namespace string) (*Operator, error) { - clientFake := fake.NewSimpleClientset(clientObjs...) +func NewFakeOperator(clientObjs []runtime.Object, k8sObjs []runtime.Object, extObjs []runtime.Object, regObjs []runtime.Object, resolver install.StrategyResolverInterface, namespaces []v1.Namespace) (*Operator, error) { k8sClientFake := k8sfake.NewSimpleClientset(k8sObjs...) k8sClientFake.Resources = apiResourcesForObjects(append(extObjs, regObjs...)) opClientFake := operatorclient.NewClient(k8sClientFake, apiextensionsfake.NewSimpleClientset(extObjs...), apiregistrationfake.NewSimpleClientset(regObjs...)) annotations := map[string]string{"test": "annotation"} - _, err := opClientFake.KubernetesInterface().CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}) - if err != nil { - return nil, err + clientFake := fake.NewSimpleClientset(clientObjs...) + + var nsList []string + for ix := range namespaces { + nsList = append(nsList, namespaces[ix].Name) } - return NewOperator(clientFake, opClientFake, resolver, 5*time.Second, annotations, []string{namespace}) + return NewOperator(clientFake, opClientFake, resolver, 5*time.Second, annotations, nsList) } func (o *Operator) GetClient() versioned.Interface { @@ -1041,9 +1044,10 @@ func TestTransitionCSV(t *testing.T) { }, } for _, tt := range tests { + namespaceObj := v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} t.Run(tt.name, func(t *testing.T) { // configure cluster state - op, err := NewFakeOperator(tt.initial.csvs, tt.initial.objs, tt.initial.crds, tt.initial.apis, &install.StrategyResolver{}, namespace) + op, err := NewFakeOperator(tt.initial.csvs, tt.initial.objs, tt.initial.crds, tt.initial.apis, &install.StrategyResolver{}, []v1.Namespace{namespaceObj}) require.NoError(t, err) // run csv sync for each CSV @@ -1073,6 +1077,238 @@ func TestTransitionCSV(t *testing.T) { } } +func TestSyncOperatorGroups(t *testing.T) { + //log.SetLevel(log.DebugLevel) + + nowTime := metav1.Date(2006, time.January, 2, 15, 4, 5, 0, time.FixedZone("MST", -7*3600)) + timeNow = func() metav1.Time { return nowTime } + + testNS := "test-ns" + aLabel := map[string]string{"app": "matchLabel"} + + ownedDeployment := deployment("csv1-dep1", testNS) + ownedDeployment.SetOwnerReferences([]metav1.OwnerReference{ + { + Kind: "ClusterServiceVersion", + Name: "csv1-dep1", + }, + }) + + tests := []struct { + name string + expectedEqual bool + // first item in initialObjs must be an OperatorGroup + initialObjs []runtime.Object + initialCrds []runtime.Object + // first item in initialK8sObjs must be a namespace + initialK8sObjs []runtime.Object + initialApis []runtime.Object + expectedStatus v1alpha2.OperatorGroupStatus + expectedAnnotation map[string]string + }{ + { + name: "operator group with no matching namespace, no CSVs", + expectedEqual: true, + initialObjs: []runtime.Object{ + &v1alpha2.OperatorGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-group-1", + Namespace: testNS, + }, + Spec: v1alpha2.OperatorGroupSpec{ + Selector: metav1.LabelSelector{ + MatchLabels: aLabel, + }, + }, + }, + }, + initialK8sObjs: []runtime.Object{ + &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNS, + }, + }, + }, + expectedStatus: v1alpha2.OperatorGroupStatus{}, + }, + { + name: "operator group with matching namespace, no CSVs", + expectedEqual: true, + initialObjs: []runtime.Object{ + &v1alpha2.OperatorGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-group-1", + Namespace: testNS, + }, + Spec: v1alpha2.OperatorGroupSpec{ + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "matchLabel", + }, + }, + }, + }, + }, + initialK8sObjs: []runtime.Object{ + &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNS, + Labels: aLabel, + Annotations: map[string]string{"test": "annotation"}, + }, + }, + }, + expectedStatus: v1alpha2.OperatorGroupStatus{ + Namespaces: []*v1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: testNS, + Labels: aLabel, + Annotations: map[string]string{"test": "annotation"}, + }, + }, + }, + LastUpdated: timeNow(), + }, + }, + { + name: "operator group with matching namespace, CSV present", + expectedEqual: false, + initialObjs: []runtime.Object{ + &v1alpha2.OperatorGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-group-1", + Namespace: testNS, + Labels: aLabel, + }, + Spec: v1alpha2.OperatorGroupSpec{ + Selector: metav1.LabelSelector{ + MatchLabels: aLabel, + }, + }, + }, + csv("csv1", + testNS, + "", + installStrategy("csv1-dep1", nil, nil), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseSucceeded, + ), + }, + initialK8sObjs: []runtime.Object{ + &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNS, + Labels: aLabel, + Annotations: map[string]string{"test": "annotation"}, + }, + }, + deployment("csv1-dep1", testNS), + }, + expectedStatus: v1alpha2.OperatorGroupStatus{ + Namespaces: []*v1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: testNS, + Labels: aLabel, + Annotations: map[string]string{"test": "annotation"}, + }, + }, + }, + LastUpdated: timeNow(), + }, + expectedAnnotation: map[string]string{"NOTFOUND": testNS}, + }, + { + name: "operator group with matching namespace, CSV present", + expectedEqual: true, + initialObjs: []runtime.Object{ + &v1alpha2.OperatorGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "operator-group-1", + Namespace: testNS, + Labels: aLabel, + }, + Spec: v1alpha2.OperatorGroupSpec{ + Selector: metav1.LabelSelector{ + MatchLabels: aLabel, + }, + }, + }, + csv("csv1", + testNS, + "", + installStrategy("csv1-dep1", nil, nil), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseSucceeded, + ), + }, + initialK8sObjs: []runtime.Object{ + &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNS, + Labels: aLabel, + Annotations: map[string]string{"test": "annotation"}, + }, + }, + ownedDeployment, + }, + expectedStatus: v1alpha2.OperatorGroupStatus{ + Namespaces: []*v1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: testNS, + Labels: aLabel, + Annotations: map[string]string{"test": "annotation"}, + }, + }, + }, + LastUpdated: timeNow(), + }, + expectedAnnotation: map[string]string{"olm.targetNamespaces": testNS, "olm.operatorGroup": "operator-group-1", "olm.operatorNamespace": testNS}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + namespaceList := []v1.Namespace{*tc.initialK8sObjs[0].(*v1.Namespace)} + op, err := NewFakeOperator(tc.initialObjs, tc.initialK8sObjs, tc.initialCrds, tc.initialApis, &install.StrategyResolver{}, namespaceList) + require.NoError(t, err) + + stopCh := make(chan struct{}) + defer func() { stopCh <- struct{}{} }() + ready, _ := op.Run(stopCh) + <-ready + + operatorGroup, ok := tc.initialObjs[0].(*v1alpha2.OperatorGroup) + require.True(t, ok) + + err = op.syncOperatorGroups(operatorGroup) + require.NoError(t, err) + assert.Equal(t, tc.expectedStatus, operatorGroup.Status) + + if tc.expectedAnnotation != nil { + // assuming CSVs are in correct namespace + for _, ns := range namespaceList { + deployments, err := op.lister.AppsV1().DeploymentLister().Deployments(ns.GetName()).List(labels.Everything()) + if err != nil { + t.Fatal(err) + } + for _, deploy := range deployments { + if tc.expectedEqual { + assert.Equal(t, tc.expectedAnnotation, deploy.Spec.Template.Annotations) + } else { + assert.NotEqual(t, tc.expectedAnnotation, deploy.Spec.Template.Annotations) + } + } + } + } + }) + } +} + func TestIsReplacing(t *testing.T) { log.SetLevel(log.DebugLevel) namespace := "ns" diff --git a/pkg/controller/operators/olm/operatorgroup.go b/pkg/controller/operators/olm/operatorgroup.go new file mode 100644 index 0000000000..a5d202bdee --- /dev/null +++ b/pkg/controller/operators/olm/operatorgroup.go @@ -0,0 +1,288 @@ +package olm + +import ( + "fmt" + "reflect" + "strings" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha2" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil" + log "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +func (a *Operator) syncOperatorGroups(obj interface{}) error { + op, ok := obj.(*v1alpha2.OperatorGroup) + if !ok { + log.Debugf("wrong type: %#v\n", obj) + return fmt.Errorf("casting OperatorGroup failed") + } + log.Infof("syncing operator group %v", op) + + targetedNamespaces, err := a.updateNamespaceList(op) + log.Debugf("Got targetedNamespaces: '%v'", targetedNamespaces) + if err != nil { + log.Errorf("updateNamespaceList error: %v", err) + return err + } + + if err := a.ensureClusterRoles(op); err != nil { + log.Errorf("ensureClusterRoles error: %v", err) + return err + } + log.Debug("Cluster roles completed") + + var nsList []string + for ix := range targetedNamespaces { + nsList = append(nsList, targetedNamespaces[ix].GetName()) + } + nsListJoined := strings.Join(nsList, ",") + + if err := a.annotateDeployments(op, nsListJoined); err != nil { + log.Errorf("annotateDeployments error: %v", err) + return err + } + log.Debug("Deployment annotation completed") + + // annotate csvs + csvsInNamespace := a.csvsInNamespace(op.Namespace) + for _, csv := range csvsInNamespace { + origCSVannotations := csv.GetAnnotations() + a.addAnnotationsToObjectMeta(&csv.ObjectMeta, op, nsListJoined) + if reflect.DeepEqual(origCSVannotations, csv.GetAnnotations()) == false { + // CRDs don't support strategic merge patching, but in the future if they do this should be updated to patch + if _, err := a.client.OperatorsV1alpha1().ClusterServiceVersions(csv.GetNamespace()).Update(csv); err != nil { + log.Errorf("Update for existing CSV failed: %v", err) + } + } + + if err := a.copyCsvToTargetNamespace(csv, op, targetedNamespaces); err != nil { + return err + } + } + log.Debug("CSV annotation completed") + //TODO: ensure RBAC on operator serviceaccount + + return nil +} + +func (a *Operator) copyCsvToTargetNamespace(csv *v1alpha1.ClusterServiceVersion, operatorGroup *v1alpha2.OperatorGroup, targetNamespaces []*corev1.Namespace) error { + for _, ns := range targetNamespaces { + if ns.Name == operatorGroup.GetNamespace() { + continue + } + fetchedCSV, err := a.csvLister[ns.GetName()].ClusterServiceVersions(ns.GetName()).Get(csv.GetName()) + if k8serrors.IsAlreadyExists(err) { + log.Debugf("Found existing CSV (%v), checking annotations", fetchedCSV.GetName()) + if reflect.DeepEqual(fetchedCSV.Annotations, csv.Annotations) == false { + fetchedCSV.Annotations = csv.Annotations + // CRDs don't support strategic merge patching, but in the future if they do this should be updated to patch + log.Debugf("Updating CSV %v in namespace %v", fetchedCSV.GetName(), ns.GetName()) + if _, err := a.client.OperatorsV1alpha1().ClusterServiceVersions(ns.Name).Update(fetchedCSV); err != nil { + log.Errorf("Update CSV in target namespace failed: %v", err) + return err + } + } + continue + } else if k8serrors.IsNotFound(err) { + // create new CSV instead of DeepCopy as namespace and resource version (and status) will be different + newCSV := v1alpha1.ClusterServiceVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: csv.Name, + Annotations: csv.Annotations, + }, + Spec: *csv.Spec.DeepCopy(), + } + newCSV.SetNamespace(ns.Name) + + log.Debugf("Copying CSV %v to namespace %v", csv.GetName(), ns.GetName()) + createdCSV, err := a.client.OperatorsV1alpha1().ClusterServiceVersions(ns.Name).Create(&newCSV) + if err != nil { + log.Errorf("Create for new CSV failed: %v", err) + return err + } + createdCSV.Status = v1alpha1.ClusterServiceVersionStatus{ + Message: "CSV copied to target namespace", + Reason: v1alpha1.CSVReasonCopied, + LastUpdateTime: timeNow(), + } + if _, err := a.client.OperatorsV1alpha1().ClusterServiceVersions(ns.Name).UpdateStatus(createdCSV); err != nil { + log.Errorf("Status update for CSV failed: %v", err) + return err + } + } else if err != nil { + log.Errorf("CSV fetch for %v failed: %v", csv.GetName(), err) + return err + } + } + return nil +} + +func (a *Operator) addAnnotationsToObjectMeta(obj *metav1.ObjectMeta, op *v1alpha2.OperatorGroup, targetNamespaces string) { + metav1.SetMetaDataAnnotation(obj, "olm.targetNamespaces", targetNamespaces) + metav1.SetMetaDataAnnotation(obj, "olm.operatorNamespace", op.GetNamespace()) + metav1.SetMetaDataAnnotation(obj, "olm.operatorGroup", op.GetName()) +} + +func namespacesChanged(clusterNamespaces []*corev1.Namespace, statusNamespaces []*corev1.Namespace) bool { + if len(clusterNamespaces) != len(statusNamespaces) { + return true + } + + nsMap := map[string]struct{}{} + for _, v := range clusterNamespaces { + nsMap[v.Name] = struct{}{} + } + for _, v := range statusNamespaces { + if _, ok := nsMap[v.Name]; !ok { + return true + } + } + return false +} + +func (a *Operator) updateNamespaceList(op *v1alpha2.OperatorGroup) ([]*corev1.Namespace, error) { + selector, err := metav1.LabelSelectorAsSelector(&op.Spec.Selector) + if err != nil { + return nil, err + } + + namespaceList, err := a.lister.CoreV1().NamespaceLister().List(selector) + if err != nil { + return nil, err + } + + if !namespacesChanged(namespaceList, op.Status.Namespaces) { + // status is current with correct namespaces, so no further updates required + return namespaceList, nil + } + log.Debugf("Namespace change detected, found: %v", namespaceList) + op.Status.Namespaces = make([]*corev1.Namespace, len(namespaceList)) + copy(op.Status.Namespaces, namespaceList) + op.Status.LastUpdated = timeNow() + _, err = a.client.OperatorsV1alpha2().OperatorGroups(op.Namespace).UpdateStatus(op) + if err != nil { + return namespaceList, err + } + return namespaceList, nil +} + +func (a *Operator) ensureClusterRoles(op *v1alpha2.OperatorGroup) error { + currentNamespace := op.GetNamespace() + csvsInNamespace := a.csvsInNamespace(currentNamespace) + for _, csv := range csvsInNamespace { + managerPolicyRules := []rbacv1.PolicyRule{} + apiEditPolicyRules := []rbacv1.PolicyRule{} + apiViewPolicyRules := []rbacv1.PolicyRule{} + for _, owned := range csv.Spec.CustomResourceDefinitions.Owned { + resourceNames := []string{} + for _, resource := range owned.Resources { + resourceNames = append(resourceNames, resource.Name) + } + managerPolicyRules = append(managerPolicyRules, rbacv1.PolicyRule{Verbs: []string{"*"}, APIGroups: []string{owned.Name}, Resources: resourceNames}) + apiEditPolicyRules = append(apiEditPolicyRules, rbacv1.PolicyRule{Verbs: []string{"create", "update", "patch", "delete"}, APIGroups: []string{owned.Name}, Resources: []string{owned.Kind}}) + apiViewPolicyRules = append(apiViewPolicyRules, rbacv1.PolicyRule{Verbs: []string{"get", "list", "watch"}, APIGroups: []string{owned.Name}, Resources: []string{owned.Kind}}) + } + for _, owned := range csv.Spec.APIServiceDefinitions.Owned { + resourceNames := []string{} + for _, resource := range owned.Resources { + resourceNames = append(resourceNames, resource.Name) + } + managerPolicyRules = append(managerPolicyRules, rbacv1.PolicyRule{Verbs: []string{"*"}, APIGroups: []string{owned.Group}, Resources: resourceNames}) + apiEditPolicyRules = append(apiEditPolicyRules, rbacv1.PolicyRule{Verbs: []string{"create", "update", "patch", "delete"}, APIGroups: []string{owned.Group}, Resources: []string{owned.Kind}}) + apiViewPolicyRules = append(apiViewPolicyRules, rbacv1.PolicyRule{Verbs: []string{"get", "list", "watch"}, APIGroups: []string{owned.Group}, Resources: []string{owned.Kind}}) + } + + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("owned-crd-manager-%s", csv.GetName()), + }, + Rules: managerPolicyRules, + } + ownerutil.AddNonBlockingOwner(clusterRole, csv) + _, err := a.OpClient.KubernetesInterface().RbacV1().ClusterRoles().Create(clusterRole) + if k8serrors.IsAlreadyExists(err) { + if _, err = a.OpClient.UpdateClusterRole(clusterRole); err != nil { + log.Errorf("Update CRD existing cluster role failed: %v", err) + return err + } + } else if err != nil { + log.Errorf("Update CRD cluster role failed: %v", err) + return err + } + + // operator group specific roles + operatorGroupEditClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-edit", op.Name), + }, + Rules: apiEditPolicyRules, + } + _, err = a.OpClient.KubernetesInterface().RbacV1().ClusterRoles().Create(operatorGroupEditClusterRole) + if k8serrors.IsAlreadyExists(err) { + if _, err = a.OpClient.UpdateClusterRole(operatorGroupEditClusterRole); err != nil { + log.Errorf("Update existing edit cluster role failed: %v", err) + return err + } + } else if err != nil { + log.Errorf("Update edit cluster role failed: %v", err) + return err + } + operatorGroupViewClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-view", op.GetName()), + }, + Rules: apiViewPolicyRules, + } + _, err = a.OpClient.KubernetesInterface().RbacV1().ClusterRoles().Create(operatorGroupViewClusterRole) + if k8serrors.IsAlreadyExists(err) { + if _, err = a.OpClient.UpdateClusterRole(operatorGroupViewClusterRole); err != nil { + log.Errorf("Update existing view cluster role failed: %v", err) + return err + } + } else if err != nil { + log.Errorf("Update view cluster role failed: %v", err) + return err + } + } + return nil +} + +func (a *Operator) annotateDeployments(op *v1alpha2.OperatorGroup, targetNamespaceString string) error { + // write above namespaces to watch in every deployment in operator namespace + deploymentList, err := a.lister.AppsV1().DeploymentLister().Deployments(op.GetNamespace()).List(labels.Everything()) + if err != nil { + log.Errorf("deployment list failed: %v\n", err) + return err + } + + for _, deploy := range deploymentList { + // TODO: this will be incorrect if two operatorgroups own the same namespace + // also - will be incorrect if a CSV is manually installed into a namespace + if !ownerutil.IsOwnedByKind(deploy, "ClusterServiceVersion") { + log.Debugf("deployment '%v' not owned by CSV, skipping", deploy.GetName()) + continue + } + + if lastAnnotation, ok := deploy.Spec.Template.Annotations["olm.targetNamespaces"]; ok { + if lastAnnotation == targetNamespaceString { + log.Debugf("deployment '%v' already has annotation, skipping", deploy) + continue + } + } + + originalDeploy := deploy.DeepCopy() + a.addAnnotationsToObjectMeta(&deploy.Spec.Template.ObjectMeta, op, targetNamespaceString) + if _, _, err := a.OpClient.PatchDeployment(originalDeploy, deploy); err != nil { + log.Errorf("patch deployment failed: %v\n", err) + return err + } + } + + return nil +} diff --git a/pkg/controller/operators/olm/requirements_test.go b/pkg/controller/operators/olm/requirements_test.go index 080cba6374..39f3214a92 100644 --- a/pkg/controller/operators/olm/requirements_test.go +++ b/pkg/controller/operators/olm/requirements_test.go @@ -44,9 +44,9 @@ func TestRequirementAndPermissionStatus(t *testing.T) { nil, v1alpha1.CSVPhasePending, ), - existingObjs: nil, - existingExtObjs: nil, - met: false, + existingObjs: nil, + existingExtObjs: nil, + met: false, expectedRequirementStatuses: nil, expectedError: fmt.Errorf("unexpected end of JSON input"), }, @@ -457,7 +457,12 @@ func TestRequirementAndPermissionStatus(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { - op, err := NewFakeOperator(nil, test.existingObjs, test.existingExtObjs, nil, &install.StrategyResolver{}, namespace) + namespaceObj := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + op, err := NewFakeOperator(nil, test.existingObjs, test.existingExtObjs, nil, &install.StrategyResolver{}, []corev1.Namespace{namespaceObj}) require.NoError(t, err) stopCh := make(chan struct{}) diff --git a/pkg/lib/queueinformer/queueinformer.go b/pkg/lib/queueinformer/queueinformer.go index 6b17be906a..0b36928af6 100644 --- a/pkg/lib/queueinformer/queueinformer.go +++ b/pkg/lib/queueinformer/queueinformer.go @@ -51,37 +51,43 @@ func (q *QueueInformer) keyFunc(obj interface{}) (string, bool) { return k, true } +func (q *QueueInformer) defaultAddFunc(obj interface{}) { + key, ok := q.keyFunc(obj) + if !ok { + return + } + + log.Infof("%s added", key) + q.enqueue(key) +} + +func (q *QueueInformer) defaultDeleteFunc(obj interface{}) { + key, ok := q.keyFunc(obj) + if !ok { + return + } + + log.Infof("%s deleted", key) + q.queue.Forget(key) +} + +func (q *QueueInformer) defaultUpdateFunc(oldObj, newObj interface{}) { + key, ok := q.keyFunc(newObj) + if !ok { + return + } + + log.Infof("%s updated", key) + q.enqueue(key) +} + // defaultResourceEventhandlerFuncs provides the default implementation for responding to events // these simply log the event and add the object's key to the queue for later processing func (q *QueueInformer) defaultResourceEventHandlerFuncs() *cache.ResourceEventHandlerFuncs { return &cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - key, ok := q.keyFunc(obj) - if !ok { - return - } - - log.Infof("%s added", key) - q.enqueue(key) - }, - DeleteFunc: func(obj interface{}) { - key, ok := q.keyFunc(obj) - if !ok { - return - } - - log.Infof("%s deleted", key) - q.queue.Forget(key) - }, - UpdateFunc: func(oldObj, newObj interface{}) { - key, ok := q.keyFunc(newObj) - if !ok { - return - } - - log.Infof("%s updated", key) - q.enqueue(key) - }, + AddFunc: q.defaultAddFunc, + DeleteFunc: q.defaultDeleteFunc, + UpdateFunc: q.defaultUpdateFunc, } } @@ -105,10 +111,26 @@ func NewInformer(queue workqueue.RateLimitingInterface, informer cache.SharedInd name: name, MetricsProvider: metrics, } - if funcs == nil { - queueInformer.resourceEventHandlerFuncs = queueInformer.defaultResourceEventHandlerFuncs() - } else { - queueInformer.resourceEventHandlerFuncs = funcs + queueInformer.resourceEventHandlerFuncs = queueInformer.defaultResourceEventHandlerFuncs() + if funcs != nil { + if funcs.AddFunc != nil { + queueInformer.resourceEventHandlerFuncs.AddFunc = func(obj interface{}) { + funcs.AddFunc(obj) + queueInformer.defaultAddFunc(obj) + } + } + if funcs.DeleteFunc != nil { + queueInformer.resourceEventHandlerFuncs.DeleteFunc = func(obj interface{}) { + funcs.DeleteFunc(obj) + queueInformer.defaultDeleteFunc(obj) + } + } + if funcs.UpdateFunc != nil { + queueInformer.resourceEventHandlerFuncs.UpdateFunc = func(oldObj, newObj interface{}) { + funcs.UpdateFunc(oldObj, newObj) + queueInformer.defaultUpdateFunc(oldObj, newObj) + } + } } queueInformer.informer.AddEventHandler(queueInformer.resourceEventHandlerFuncs) return queueInformer diff --git a/test/e2e/operator_groups_e2e_test.go b/test/e2e/operator_groups_e2e_test.go new file mode 100644 index 0000000000..3ec2886421 --- /dev/null +++ b/test/e2e/operator_groups_e2e_test.go @@ -0,0 +1,233 @@ +package e2e + +import ( + "fmt" + "regexp" + "strings" + "testing" + + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha2" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" + + "github.com/coreos/go-semver/semver" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func DeploymentComplete(deployment *appsv1.Deployment, newStatus *appsv1.DeploymentStatus) bool { + return newStatus.UpdatedReplicas == *(deployment.Spec.Replicas) && + newStatus.Replicas == *(deployment.Spec.Replicas) && + newStatus.AvailableReplicas == *(deployment.Spec.Replicas) && + newStatus.ObservedGeneration >= deployment.Generation +} + +// Currently this function only modifies the watchedNamespace in the container command +func patchOlmDeployment(t *testing.T, c operatorclient.ClientInterface, newNamespace string) []string { + runningDeploy, err := c.GetDeployment(testNamespace, "olm-operator") + require.NoError(t, err) + + command := runningDeploy.Spec.Template.Spec.Containers[0].Command + re, err := regexp.Compile(`-watchedNamespaces\W(\S+)`) + require.NoError(t, err) + newCommand := re.ReplaceAllString(strings.Join(command, " "), "$0"+","+newNamespace) + log.Debugf("original=%#v newCommand=%#v", command, newCommand) + finalNewCommand := strings.Split(newCommand, " ") + runningDeploy.Spec.Template.Spec.Containers[0].Command = make([]string, len(finalNewCommand)) + copy(runningDeploy.Spec.Template.Spec.Containers[0].Command, finalNewCommand) + + newDeployment, updated, err := c.UpdateDeployment(runningDeploy) + if err != nil || updated == false { + t.Fatalf("Deployment update failed: (updated %v) %v\n", updated, err) + } + require.NoError(t, err) + + err = wait.Poll(pollInterval, pollDuration, func() (bool, error) { + fetchedDeployment, err := c.GetDeployment(newDeployment.Namespace, newDeployment.Name) + if err != nil { + return false, err + } + if DeploymentComplete(newDeployment, &fetchedDeployment.Status) { + return true, nil + } + return false, nil + }) + + require.NoError(t, err) + return command +} + +func checkOperatorGroupAnnotations(obj metav1.Object, op *v1alpha2.OperatorGroup, targetNamespaces string) error { + if annotation, ok := obj.GetAnnotations()["olm.targetNamespaces"]; !ok || annotation != targetNamespaces { + return fmt.Errorf("missing targetNamespaces annotation on %v", obj.GetName()) + } + if annotation, ok := obj.GetAnnotations()["olm.operatorNamespace"]; !ok || annotation != op.GetNamespace() { + return fmt.Errorf("missing operatorNamespace on %v", obj.GetName()) + } + if annotation, ok := obj.GetAnnotations()["olm.operatorGroup"]; !ok || annotation != op.GetName() { + return fmt.Errorf("missing operatorGroup annotation on %v", obj.GetName()) + } + + return nil +} + +func TestOperatorGroup(t *testing.T) { + // Create namespace with specific label + // Create CSV in operator namespace + // Create operator group that watches namespace and uses specific label + // Verify operator group status contains correct status + // Verify csv in target namespace exists, has copied status, has annotations + // Verify deployments have correct namespace annotation + // (Verify that the operator can operate in the target namespace) + + log.SetLevel(log.DebugLevel) + c := newKubeClient(t) + crc := newCRClient(t) + csvName := "another-csv" // must be lowercase for DNS-1123 validation + + matchingLabel := map[string]string{"matchLabel": testNamespace} + otherNamespaceName := testNamespace + "-namespace-two" + + otherNamespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: otherNamespaceName, + Labels: matchingLabel, + }, + } + createdOtherNamespace, err := c.KubernetesInterface().CoreV1().Namespaces().Create(&otherNamespace) + require.NoError(t, err) + + oldCommand := patchOlmDeployment(t, c, otherNamespaceName) + + log.Debug("Creating CSV") + aCSV := newCSV(csvName, testNamespace, "", *semver.New("0.0.0"), nil, nil, newNginxInstallStrategy("operator-deployment", nil, nil)) + createdCSV, err := crc.OperatorsV1alpha1().ClusterServiceVersions(testNamespace).Create(&aCSV) + require.NoError(t, err) + + log.Debug("Creating operator group") + operatorGroup := v1alpha2.OperatorGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-operator-group", + Namespace: testNamespace, + }, + } + _, err = crc.OperatorsV1alpha2().OperatorGroups(testNamespace).Create(&operatorGroup) + require.NoError(t, err) + expectedOperatorGroupStatus := v1alpha2.OperatorGroupStatus{ + Namespaces: []*corev1.Namespace{createdOtherNamespace}, + } + + // instead of setting the label selector initially, do it here to force immediate reconcile + fetchedOpGroup, err := crc.OperatorsV1alpha2().OperatorGroups(testNamespace).Get(operatorGroup.GetName(), metav1.GetOptions{}) + fetchedOpGroup.Spec = v1alpha2.OperatorGroupSpec{ + Selector: metav1.LabelSelector{ + MatchLabels: matchingLabel, + }, + } + _, err = crc.OperatorsV1alpha2().OperatorGroups(testNamespace).Update(fetchedOpGroup) + require.NoError(t, err) + + log.Debug("Waiting on operator group to have correct status") + err = wait.Poll(pollInterval, pollDuration, func() (bool, error) { + fetched, fetchErr := crc.OperatorsV1alpha2().OperatorGroups(testNamespace).Get(operatorGroup.Name, metav1.GetOptions{}) + if fetchErr != nil { + fmt.Println(fetchErr) + return false, fetchErr + } + if len(fetched.Status.Namespaces) > 0 { + require.Equal(t, expectedOperatorGroupStatus.Namespaces[0].Name, fetched.Status.Namespaces[0].Name) + return true, nil + } + return false, nil + }) + + log.Debug("Waiting for operator namespace csv to have annotations") + err = wait.Poll(pollInterval, pollDuration, func() (bool, error) { + fetchedCSV, fetchErr := crc.OperatorsV1alpha1().ClusterServiceVersions(testNamespace).Get(csvName, metav1.GetOptions{}) + if fetchErr != nil { + fmt.Println(fetchErr) + return false, fetchErr + } + if checkOperatorGroupAnnotations(fetchedCSV, &operatorGroup, otherNamespaceName) == nil { + return true, nil + } + return false, nil + }) + + log.Debug("Waiting for target namespace csv to have annotations") + err = wait.Poll(pollInterval, pollDuration, func() (bool, error) { + fetchedCSV, fetchErr := crc.OperatorsV1alpha1().ClusterServiceVersions(otherNamespaceName).Get(csvName, metav1.GetOptions{}) + if fetchErr != nil { + fmt.Println(fetchErr) + return false, fetchErr + } + if checkOperatorGroupAnnotations(fetchedCSV, &operatorGroup, otherNamespaceName) == nil { + return true, nil + } + + return false, nil + }) + // since annotations are set along with status, no reason to poll for this check as done above + log.Debug("Checking status on csv in target namespace") + fetchedCSV, err := crc.OperatorsV1alpha1().ClusterServiceVersions(otherNamespaceName).Get(csvName, metav1.GetOptions{}) + require.NoError(t, err) + require.EqualValues(t, v1alpha1.CSVReasonCopied, fetchedCSV.Status.Reason) + // also check name and spec + require.Equal(t, createdCSV.Name, fetchedCSV.Name) + require.Equal(t, createdCSV.Spec, fetchedCSV.Spec) + + log.Debug("Waiting on deployment to have correct annotations") + err = wait.Poll(pollInterval, pollDuration, func() (bool, error) { + createdDeployment, err := c.GetDeployment(testNamespace, "operator-deployment") + if err != nil { + if errors.IsNotFound(err) { + return false, nil + } + return false, err + } + if checkOperatorGroupAnnotations(&createdDeployment.Spec.Template, &operatorGroup, otherNamespaceName) == nil { + return true, nil + } + return false, nil + }) + + // ensure deletion cleans up copied CSV + log.Debug("Deleting CSV") + err = crc.OperatorsV1alpha1().ClusterServiceVersions(testNamespace).Delete(csvName, &metav1.DeleteOptions{}) + require.NoError(t, err) + + log.Debug("Waiting for orphaned CSV to be deleted") + err = wait.Poll(pollInterval, pollDuration, func() (bool, error) { + _, err = crc.OperatorsV1alpha1().ClusterServiceVersions(otherNamespaceName).Get(csvName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return true, nil + } + return false, err + } + return false, nil + }) + require.NoError(t, err) + + // clean up + // TODO: unpatch function + runningDeploy, err := c.GetDeployment(testNamespace, "olm-operator") + require.NoError(t, err) + runningDeploy.Spec.Template.Spec.Containers[0].Command = oldCommand + _, updated, err := c.UpdateDeployment(runningDeploy) + if err != nil || updated == false { + t.Fatalf("Deployment update failed: (updated %v) %v\n", updated, err) + } + require.NoError(t, err) + + err = c.KubernetesInterface().CoreV1().Namespaces().Delete(otherNamespaceName, &metav1.DeleteOptions{}) + require.NoError(t, err) + err = crc.OperatorsV1alpha2().OperatorGroups(testNamespace).Delete(operatorGroup.Name, &metav1.DeleteOptions{}) + require.NoError(t, err) +}