From d00e52a0d00ca2873c1e7460cfc925095871ac48 Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Fri, 3 Aug 2018 11:22:13 -0400 Subject: [PATCH 1/9] wip(olm): add failing upgrade csv test the optimization that marks multiple CSVs for deleting during one loop is broken --- pkg/controller/operators/olm/operator.go | 51 +-- pkg/controller/operators/olm/operator_test.go | 428 ++++++++++++++++++ pkg/lib/operatorclient/fake/client.go | 34 ++ .../operatorclient/fake/customresources.go | 302 ++++++++++++ pkg/lib/operatorclient/fake/deployment.go | 207 +++++++++ pkg/lib/operatorclient/fake/patch.go | 358 +++++++++++++++ pkg/lib/operatorclient/fake/serviceaccount.go | 39 ++ test/e2e/csv_e2e_test.go | 147 ++++++ 8 files changed, 1536 insertions(+), 30 deletions(-) create mode 100644 pkg/lib/operatorclient/fake/client.go create mode 100644 pkg/lib/operatorclient/fake/customresources.go create mode 100644 pkg/lib/operatorclient/fake/deployment.go create mode 100644 pkg/lib/operatorclient/fake/patch.go create mode 100644 pkg/lib/operatorclient/fake/serviceaccount.go diff --git a/pkg/controller/operators/olm/operator.go b/pkg/controller/operators/olm/operator.go index 38c15c68f6..f1bf74879c 100644 --- a/pkg/controller/operators/olm/operator.go +++ b/pkg/controller/operators/olm/operator.go @@ -8,7 +8,6 @@ import ( log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/informers" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" @@ -105,16 +104,16 @@ func NewOperator(kubeconfig string, wakeupInterval time.Duration, annotations ma return op, nil } -func (a *Operator) requeueCSV(csv *v1alpha1.ClusterServiceVersion) { - k, err := cache.DeletionHandlingMetaNamespaceKeyFunc(csv) - if err != nil { - log.Infof("creating key failed: %s", err) - return - } - log.Infof("requeueing %s", csv.SelfLink) - a.csvQueue.AddRateLimited(k) - return -} +//func (a *Operator) requeueCSV(csv *v1alpha1.ClusterServiceVersion) { +// k, err := cache.DeletionHandlingMetaNamespaceKeyFunc(csv) +// if err != nil { +// log.Infof("creating key failed: %s", err) +// return +// } +// log.Infof("requeueing %s", csv.SelfLink) +// a.csvQueue.AddRateLimited(k) +// return +//} // syncClusterServiceVersion is the method that gets called when we see a CSV event in the cluster func (a *Operator) syncClusterServiceVersion(obj interface{}) (syncError error) { @@ -201,7 +200,7 @@ func (a *Operator) transitionCSVState(in v1alpha1.ClusterServiceVersion) (out *v } out.SetPhase(v1alpha1.CSVPhaseInstalling, v1alpha1.CSVReasonInstallSuccessful, "waiting for install components to report healthy") - a.requeueCSV(out) + //a.requeueCSV(out) return case v1alpha1.CSVPhaseInstalling: installer, strategy, _ := a.parseStrategiesAndUpdateStatus(out) @@ -242,7 +241,7 @@ func (a *Operator) transitionCSVState(in v1alpha1.ClusterServiceVersion) (out *v } // if there's no newer version, requeue for processing (likely will be GCable before resync) - a.requeueCSV(out) + //a.requeueCSV(out) case v1alpha1.CSVPhaseDeleting: syncError := a.OpClient.DeleteCustomResource(v1alpha1.GroupName, v1alpha1.GroupVersion, out.GetNamespace(), v1alpha1.ClusterServiceVersionKind, out.GetName()) if syncError != nil { @@ -282,15 +281,12 @@ func (a *Operator) findIntermediatesForDeletion(csv *v1alpha1.ClusterServiceVers // csvsInNamespace finds all CSVs in a namespace func (a *Operator) csvsInNamespace(namespace string) (csvs []*v1alpha1.ClusterServiceVersion) { - csvsInNamespace, err := a.OpClient.ListCustomResource(v1alpha1.GroupName, v1alpha1.GroupVersion, namespace, v1alpha1.ClusterServiceVersionKind) + // TODO: read from indexer + csvsInNamespace, err := a.client.OperatorsV1alpha1().ClusterServiceVersions(namespace).List(metav1.ListOptions{}) if err != nil { return nil } - for _, csvUnst := range csvsInNamespace.Items { - csv := v1alpha1.ClusterServiceVersion{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(csvUnst.UnstructuredContent(), &csv); err != nil { - continue - } + for _, csv := range csvsInNamespace.Items { csvs = append(csvs, &csv) } return @@ -308,7 +304,7 @@ func (a *Operator) checkReplacementsAndUpdateStatus(csv *v1alpha1.ClusterService csv.SetPhase(v1alpha1.CSVPhaseReplacing, v1alpha1.CSVReasonBeingReplaced, msg) // requeue so that we quickly pick up on replacement status changes - a.requeueCSV(csv) + //a.requeueCSV(csv) return fmt.Errorf("replacing") } @@ -334,7 +330,7 @@ func (a *Operator) updateInstallStatus(csv *v1alpha1.ClusterServiceVersion, inst // if there's an error checking install that shouldn't fail the strategy, requeue with message if strategyErr != nil { csv.SetPhase(v1alpha1.CSVPhaseInstalling, requeueConditionReason, fmt.Sprintf("installing: %s", strategyErr)) - a.requeueCSV(csv) + //a.requeueCSV(csv) return strategyErr } @@ -359,7 +355,7 @@ func (a *Operator) parseStrategiesAndUpdateStatus(csv *v1alpha1.ClusterServiceVe } if previousStrategy != nil { // check for status changes if we know we're replacing a CSV - a.requeueCSV(previousCSV) + //a.requeueCSV(previousCSV) } strName := strategy.GetStrategyName() @@ -443,20 +439,15 @@ func (a *Operator) isBeingReplaced(in *v1alpha1.ClusterServiceVersion, csvsInNam return } -func (a *Operator) isReplacing(in *v1alpha1.ClusterServiceVersion) (previous *v1alpha1.ClusterServiceVersion) { +func (a *Operator) isReplacing(in *v1alpha1.ClusterServiceVersion) *v1alpha1.ClusterServiceVersion { log.Debugf("checking if csv is replacing an older version") if in.Spec.Replaces == "" { return nil } - oldCSVUnst, err := a.OpClient.GetCustomResource(v1alpha1.GroupName, v1alpha1.GroupVersion, in.GetNamespace(), v1alpha1.ClusterServiceVersionKind, in.Spec.Replaces) + previous, err := a.client.OperatorsV1alpha1().ClusterServiceVersions(in.GetNamespace()).Get(in.Spec.Replaces, metav1.GetOptions{}) if err != nil { log.Debugf("unable to get previous csv: %s", err.Error()) return nil } - p := v1alpha1.ClusterServiceVersion{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(oldCSVUnst.UnstructuredContent(), &p); err != nil { - log.Debugf("unable to parse previous csv: %s", err.Error()) - return nil - } - return &p + return previous } diff --git a/pkg/controller/operators/olm/operator_test.go b/pkg/controller/operators/olm/operator_test.go index 12bab23ee3..d09ddd6124 100644 --- a/pkg/controller/operators/olm/operator_test.go +++ b/pkg/controller/operators/olm/operator_test.go @@ -1,17 +1,21 @@ package olm import ( + "encoding/json" "fmt" "strings" "testing" "github.com/golang/mock/gomock" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "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/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + k8sfake "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" @@ -21,7 +25,10 @@ import ( "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" "github.com/operator-framework/operator-lifecycle-manager/pkg/fakes" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" + opFake "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient/fake" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/queueinformer" + "k8s.io/api/apps/v1beta2" + "k8s.io/api/core/v1" ) type MockALMOperator struct { @@ -1845,3 +1852,424 @@ func TestIsReplacing(t *testing.T) { ctrl.Finish() } } + +func deployment(deploymentName, namespace string) *v1beta2.Deployment { + var singleInstance = int32(1) + return &v1beta2.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: deploymentName, + Namespace: namespace, + }, + Spec: v1beta2.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": deploymentName, + }, + }, + Replicas: &singleInstance, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": deploymentName, + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: deploymentName + "-c1", + Image: "nginx:1.7.9", + Ports: []v1.ContainerPort{ + { + ContainerPort: 80, + }, + }, + }, + }, + }, + }, + }, + Status: v1beta2.DeploymentStatus{ + Replicas: singleInstance, + ReadyReplicas: singleInstance, + AvailableReplicas: singleInstance, + UpdatedReplicas: singleInstance, + }, + } +} + +func installStrategy(deploymentName string) v1alpha1.NamedInstallStrategy { + var singleInstance = int32(1) + strategy := install.StrategyDetailsDeployment{ + DeploymentSpecs: []install.StrategyDeploymentSpec{ + { + Name: deploymentName, + Spec: v1beta2.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": deploymentName, + }, + }, + Replicas: &singleInstance, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": deploymentName, + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: deploymentName + "-c1", + Image: "nginx:1.7.9", + Ports: []v1.ContainerPort{ + { + ContainerPort: 80, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + strategyRaw, err := json.Marshal(strategy) + if err != nil { + panic(err) + } + + return v1alpha1.NamedInstallStrategy{ + StrategyName: install.InstallStrategyNameDeployment, + StrategySpecRaw: strategyRaw, + } +} + +func csv(name, namespace, replaces string, installStrategy v1alpha1.NamedInstallStrategy, owned, required []*v1beta1.CustomResourceDefinition, phase v1alpha1.ClusterServiceVersionPhase) *v1alpha1.ClusterServiceVersion { + requiredCRDDescs := make([]v1alpha1.CRDDescription, 0) + for _, crd := range required { + requiredCRDDescs = append(requiredCRDDescs, v1alpha1.CRDDescription{Name: crd.GetName(), Version: crd.Spec.Versions[0].Name, Kind: crd.GetName()}) + } + + ownedCRDDescs := make([]v1alpha1.CRDDescription, 0) + for _, crd := range owned { + ownedCRDDescs = append(ownedCRDDescs, v1alpha1.CRDDescription{Name: crd.GetName(), Version: crd.Spec.Versions[0].Name, Kind: crd.GetName()}) + } + + return &v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: v1alpha1.ClusterServiceVersionKind, + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + Replaces: replaces, + InstallStrategy: installStrategy, + CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ + Owned: ownedCRDDescs, + Required: requiredCRDDescs, + }, + }, + Status: v1alpha1.ClusterServiceVersionStatus{ + Phase: phase, + }, + } +} + +func crd(name string, version string) *v1beta1.CustomResourceDefinition { + return &v1beta1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: v1beta1.CustomResourceDefinitionSpec{ + Group: name + "group", + Versions: []v1beta1.CustomResourceDefinitionVersion{ + { + Name: version, + Storage: true, + Served: true, + }, + }, + Names: v1beta1.CustomResourceDefinitionNames{ + Kind: name, + }, + }, + } +} + +func TestCSVUpgrades(t *testing.T) { + log.SetLevel(log.DebugLevel) + namespace := "ns" + + type csvState struct { + exists bool + phase v1alpha1.ClusterServiceVersionPhase + } + type initial struct { + csvs []runtime.Object + crds []runtime.Object + objs []runtime.Object + } + type expected struct { + csvStates map[string]csvState + } + tests := []struct { + name string + initial initial + expected expected + }{ + { + name: "SingleCSVNoneToPending", + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", + namespace, + "", + installStrategy("csv1-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseNone, + ), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: true, phase: v1alpha1.CSVPhasePending}, + }, + }, + }, + { + name: "SingleCSVPendingToInstallReady", + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", + namespace, + "", + installStrategy("csv1-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhasePending, + ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: true, phase: v1alpha1.CSVPhaseInstallReady}, + }, + }, + }, + { + name: "SingleCSVInstallReadyToInstalling", + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", + namespace, + "", + installStrategy("csv1-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseInstallReady, + ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: true, phase: v1alpha1.CSVPhaseInstalling}, + }, + }, + }, + { + name: "SingleCSVInstallingToSucceeded", + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", + namespace, + "", + installStrategy("csv1-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseInstalling, + ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + objs: []runtime.Object{ + deployment("csv1-dep1", namespace), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: true, phase: v1alpha1.CSVPhaseSucceeded}, + }, + }, + }, + { + name: "CSVSucceededToReplacing", + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", + namespace, + "", + installStrategy("csv1-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseSucceeded, + ), + csv("csv2", + namespace, + "csv1", + installStrategy("csv2-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseNone, + ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + objs: []runtime.Object{ + deployment("csv1-dep1", namespace), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: true, phase: v1alpha1.CSVPhaseReplacing}, + "csv2": {exists: true, phase: v1alpha1.CSVPhasePending}, + }, + }, + }, + { + name: "CSVReplacingToDeleted", + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", + namespace, + "", + installStrategy("csv1-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseReplacing, + ), + csv("csv2", + namespace, + "csv1", + installStrategy("csv2-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseSucceeded, + ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + objs: []runtime.Object{ + deployment("csv1-dep1", namespace), + deployment("csv2-dep1", namespace), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: true, phase: v1alpha1.CSVPhaseDeleting}, + "csv2": {exists: true, phase: v1alpha1.CSVPhaseSucceeded}, + }, + }, + }, + { + name: "CSVMultipleReplacingToDeleted", + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", + namespace, + "", + installStrategy("csv1-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseReplacing, + ), + csv("csv2", + namespace, + "csv1", + installStrategy("csv2-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseReplacing, + ), + csv("csv3", + namespace, + "csv2", + installStrategy("csv3-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseSucceeded, + ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + objs: []runtime.Object{ + deployment("csv1-dep1", namespace), + deployment("csv2-dep1", namespace), + deployment("csv3-dep1", namespace), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: true, phase: v1alpha1.CSVPhaseDeleting}, + "csv2": {exists: true, phase: v1alpha1.CSVPhaseDeleting}, + "csv3": {exists: true, phase: v1alpha1.CSVPhaseSucceeded}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // configure cluster state + clientFake := fake.NewSimpleClientset(tt.initial.csvs...) + + opClientFake := opFake.NewClient( + k8sfake.NewSimpleClientset(tt.initial.objs...), + apiextensionsfake.NewSimpleClientset(tt.initial.crds...)) + + op := &Operator{ + Operator: &queueinformer.Operator{ + OpClient: opClientFake, + }, + client: clientFake, + resolver: &install.StrategyResolver{}, + } + + // run csv sync for each CSV + for _, csv := range tt.initial.csvs { + err := op.syncClusterServiceVersion(csv) + require.NoError(t, err) + } + + // get csvs in the cluster + outCSVMap := map[string]*v1alpha1.ClusterServiceVersion{} + outCSVs, err := clientFake.OperatorsV1alpha1().ClusterServiceVersions("ns").List(metav1.ListOptions{}) + require.NoError(t, err) + for _, csv := range outCSVs.Items { + outCSVMap[csv.GetName()] = csv.DeepCopy() + } + + // verify expectations of csvs in cluster + for csvName, csvState := range tt.expected.csvStates { + csv, ok := outCSVMap[csvName] + require.Equal(t, ok, csvState.exists) + if csvState.exists { + assert.Equal(t, csvState.phase, csv.Status.Phase, "%s had incorrect phase", csvName) + } + } + }) + } +} diff --git a/pkg/lib/operatorclient/fake/client.go b/pkg/lib/operatorclient/fake/client.go new file mode 100644 index 0000000000..5d47468bd2 --- /dev/null +++ b/pkg/lib/operatorclient/fake/client.go @@ -0,0 +1,34 @@ +package fake + +import ( + apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + extfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + "k8s.io/client-go/kubernetes" + k8sfake "k8s.io/client-go/kubernetes/fake" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" +) + +// Interface assertion. +var _ operatorclient.ClientInterface = &Client{} + +// Client is a kubernetes client that can talk to the API server. +type Client struct { + *k8sfake.Clientset + extClientset *extfake.Clientset +} + +// NewClient creates a kubernetes client or bails out on on failures. +func NewClient(kClient *k8sfake.Clientset, eClient *extfake.Clientset) operatorclient.ClientInterface { + return &Client{kClient, eClient} +} + +// KubernetesInterface returns the Kubernetes interface. +func (c *Client) KubernetesInterface() kubernetes.Interface { + return c.Clientset +} + +// ApiextensionsV1beta1Interface returns the API extention interface. +func (c *Client) ApiextensionsV1beta1Interface() apiextensions.Interface { + return c.extClientset +} diff --git a/pkg/lib/operatorclient/fake/customresources.go b/pkg/lib/operatorclient/fake/customresources.go new file mode 100644 index 0000000000..b674b64ad6 --- /dev/null +++ b/pkg/lib/operatorclient/fake/customresources.go @@ -0,0 +1,302 @@ +package fake + +import ( + "encoding/json" + "fmt" + "path" + "strings" + "time" + + "github.com/golang/glog" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" +) + +// GetCustomResource returns the custom resource as *unstructured.Unstructured by the given name. +func (c *Client) GetCustomResource(apiGroup, version, namespace, resourceKind, resourceName string) (*unstructured.Unstructured, error) { + glog.V(4).Infof("[GET CUSTOM RESOURCE]: %s:%s", namespace, resourceName) + var object unstructured.Unstructured + + b, err := c.GetCustomResourceRaw(apiGroup, version, namespace, resourceKind, resourceName) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(b, &object); err != nil { + return nil, fmt.Errorf("failed to unmarshal CUSTOM RESOURCE: %v", err) + } + return &object, nil +} + +// GetCustomResourceRaw returns the custom resource's raw body data by the given name. +func (c *Client) GetCustomResourceRaw(apiGroup, version, namespace, resourceKind, resourceName string) ([]byte, error) { + glog.V(4).Infof("[GET CUSTOM RESOURCE RAW]: %s:%s", namespace, resourceName) + httpRestClient := c.extClientset.ApiextensionsV1beta1().RESTClient() + uri := customResourceURI(apiGroup, version, namespace, resourceKind, resourceName) + glog.V(4).Infof("[GET]: %s", uri) + + return httpRestClient.Get().RequestURI(uri).DoRaw() +} + +// CreateCustomResource creates the custom resource. +func (c *Client) CreateCustomResource(item *unstructured.Unstructured) error { + glog.V(4).Infof("[CREATE CUSTOM RESOURCE]: %s:%s", item.GetNamespace(), item.GetName()) + kind := item.GetKind() + namespace := item.GetNamespace() + apiVersion := item.GetAPIVersion() + apiGroup, version, err := parseAPIVersion(apiVersion) + if err != nil { + return err + } + + data, err := json.Marshal(item) + if err != nil { + return err + } + + return c.CreateCustomResourceRaw(apiGroup, version, namespace, kind, data) +} + +// CreateCustomResourceRaw creates the raw bytes of the custom resource. +func (c *Client) CreateCustomResourceRaw(apiGroup, version, namespace, kind string, data []byte) error { + glog.V(4).Infof("[CREATE CUSTOM RESOURCE RAW]: %s:%s", namespace, kind) + var statusCode int + + httpRestClient := c.extClientset.ApiextensionsV1beta1().RESTClient() + uri := customResourceDefinitionURI(apiGroup, version, namespace, kind) + glog.V(4).Infof("[POST]: %s", uri) + result := httpRestClient.Post().RequestURI(uri).Body(data).Do() + + if result.Error() != nil { + return result.Error() + } + + result.StatusCode(&statusCode) + glog.V(4).Infof("Written %s, status: %d", uri, statusCode) + + if statusCode != 201 { + return fmt.Errorf("unexpected status code %d, expecting 201", statusCode) + } + return nil +} + +// CreateCustomResourceRawIfNotFound creates the raw bytes of the custom resource if it doesn't exist. +// It also returns a boolean to indicate whether a new custom resource is created. +func (c *Client) CreateCustomResourceRawIfNotFound(apiGroup, version, namespace, kind, name string, data []byte) (bool, error) { + glog.V(4).Infof("[CREATE CUSTOM RESOURCE RAW if not found]: %s:%s", namespace, name) + _, err := c.GetCustomResource(apiGroup, version, namespace, kind, name) + if err == nil { + return false, nil + } + if !errors.IsNotFound(err) { + return false, err + } + err = c.CreateCustomResourceRaw(apiGroup, version, namespace, kind, data) + if err != nil { + return false, err + } + return true, nil +} + +// UpdateCustomResource updates the custom resource. +// To do an atomic update, use AtomicModifyCustomResource(). +func (c *Client) UpdateCustomResource(item *unstructured.Unstructured) error { + glog.V(4).Infof("[UPDATE CUSTOM RESOURCE]: %s:%s", item.GetNamespace(), item.GetName()) + kind := item.GetKind() + name := item.GetName() + namespace := item.GetNamespace() + apiVersion := item.GetAPIVersion() + apiGroup, version, err := parseAPIVersion(apiVersion) + if err != nil { + return err + } + + data, err := json.Marshal(item) + if err != nil { + return err + } + + return c.UpdateCustomResourceRaw(apiGroup, version, namespace, kind, name, data) +} + +// UpdateCustomResourceRaw updates the thirdparty resource with the raw data. +func (c *Client) UpdateCustomResourceRaw(apiGroup, version, namespace, resourceKind, resourceName string, data []byte) error { + glog.V(4).Infof("[UPDATE CUSTOM RESOURCE RAW]: %s:%s", namespace, resourceName) + var statusCode int + + httpRestClient := c.extClientset.ApiextensionsV1beta1().RESTClient() + uri := customResourceURI(apiGroup, version, namespace, resourceKind, resourceName) + glog.V(4).Infof("[PUT]: %s", uri) + result := httpRestClient.Put().RequestURI(uri).Body(data).Do() + + if result.Error() != nil { + return result.Error() + } + + result.StatusCode(&statusCode) + glog.V(4).Infof("Updated %s, status: %d", uri, statusCode) + + if statusCode != 200 { + return fmt.Errorf("unexpected status code %d, expecting 200", statusCode) + } + return nil +} + +// CreateOrUpdateCustomeResourceRaw creates the custom resource if it doesn't exist. +// If the custom resource exists, it updates the existing one. +func (c *Client) CreateOrUpdateCustomeResourceRaw(apiGroup, version, namespace, resourceKind, resourceName string, data []byte) error { + glog.V(4).Infof("[CREATE OR UPDATE UPDATE CUSTOM RESOURCE RAW]: %s:%s", namespace, resourceName) + old, err := c.GetCustomResourceRaw(apiGroup, version, namespace, resourceKind, resourceName) + if err != nil { + if !errors.IsNotFound(err) { + return err + } + return c.CreateCustomResourceRaw(apiGroup, version, namespace, resourceKind, data) + } + + var oldSpec, newSpec unstructured.Unstructured + if err := json.Unmarshal(old, &oldSpec); err != nil { + return err + } + if err := json.Unmarshal(data, &newSpec); err != nil { + return err + } + + // Set the resource version. + newSpec.SetResourceVersion(oldSpec.GetResourceVersion()) + + data, err = json.Marshal(&newSpec) + if err != nil { + return err + } + + return c.UpdateCustomResourceRaw(apiGroup, version, namespace, resourceKind, resourceName, data) +} + +// DeleteCustomResource deletes the with the given name. +func (c *Client) DeleteCustomResource(apiGroup, version, namespace, resourceKind, resourceName string) error { + glog.V(4).Infof("[DELETE CUSTOM RESOURCE]: %s:%s", namespace, resourceName) + httpRestClient := c.extClientset.ApiextensionsV1beta1().RESTClient() + uri := customResourceURI(apiGroup, version, namespace, resourceKind, resourceName) + + glog.V(4).Infof("[DELETE]: %s", uri) + _, err := httpRestClient.Delete().RequestURI(uri).DoRaw() + return err +} + +// AtomicModifyCustomResource gets the custom resource, modifies it and writes it back. +// If it's modified by other writers, we will retry until it succeeds. +func (c *Client) AtomicModifyCustomResource(apiGroup, version, namespace, resourceKind, resourceName string, f operatorclient.CustomResourceModifier, data interface{}) error { + glog.V(4).Infof("[ATOMIC MODIFY CUSTOM RESOURCE]: %s:%s", namespace, resourceName) + return wait.PollInfinite(time.Second, func() (bool, error) { + var customResource unstructured.Unstructured + b, err := c.GetCustomResourceRaw(apiGroup, version, namespace, resourceKind, resourceName) + if err != nil { + glog.Errorf("Failed to get CUSTOM RESOURCE %q, kind:%q: %v", resourceName, resourceKind, err) + return false, err + } + + if err := json.Unmarshal(b, &customResource); err != nil { + glog.Errorf("Failed to unmarshal CUSTOM RESOURCE %q, kind:%q: %v", resourceName, resourceKind, err) + return false, err + } + + if err := f(&customResource, data); err != nil { + glog.Errorf("Failed to modify the CUSTOM RESOURCE %q, kind:%q: %v", resourceName, resourceKind, err) + return false, err + } + + if err := c.UpdateCustomResource(&customResource); err != nil { + if errors.IsConflict(err) { + glog.Errorf("Failed to update CUSTOM RESOURCE %q, kind:%q: %v, will retry", resourceName, resourceKind, err) + return false, nil + } + glog.Errorf("Failed to update CUSTOM RESOURCE %q, kind:%q: %v", resourceName, resourceKind, err) + return false, err + } + + return true, nil + }) +} + +// customResourceURI returns the URI for the thirdparty resource. +// +// Example of apiGroup: "tco.coreos.com" +// Example of version: "v1" +// Example of namespace: "default" +// Example of resourceKind: "ChannelOperatorConfig" +// Example of resourceName: "test-config" +func customResourceURI(apiGroup, version, namespace, resourceKind, resourceName string) string { + if namespace == "" { + namespace = metav1.NamespaceDefault + } + plural, _ := meta.UnsafeGuessKindToResource(schema.GroupVersionKind{ + Group: apiGroup, + Version: version, + Kind: resourceKind, + }) + return fmt.Sprintf("/apis/%s/%s/namespaces/%s/%s/%s", + strings.ToLower(apiGroup), + strings.ToLower(version), + strings.ToLower(namespace), + strings.ToLower(plural.Resource), + strings.ToLower(resourceName)) +} + +// customResourceDefinitionURI returns the URI for the CRD. +// +// Example of apiGroup: "tco.coreos.com" +// Example of version: "v1" +// Example of namespace: "default" +// Example of resourceKind: "ChannelOperatorConfig" +func customResourceDefinitionURI(apiGroup, version, namespace, resourceKind string) string { + if namespace == "" { + namespace = metav1.NamespaceDefault + } + plural, _ := meta.UnsafeGuessKindToResource(schema.GroupVersionKind{ + Group: apiGroup, + Version: version, + Kind: resourceKind, + }) + return fmt.Sprintf("/apis/%s/%s/namespaces/%s/%s", + strings.ToLower(apiGroup), + strings.ToLower(version), + strings.ToLower(namespace), + strings.ToLower(plural.Resource)) +} + +// ListCustomResource lists all custom resources for the given namespace. +func (c *Client) ListCustomResource(apiGroup, version, namespace, resourceKind string) (*operatorclient.CustomResourceList, error) { + glog.V(4).Infof("LIST CUSTOM RESOURCE]: %s", resourceKind) + + var crList operatorclient.CustomResourceList + + httpRestClient := c.extClientset.ApiextensionsV1beta1().RESTClient() + uri := customResourceDefinitionURI(apiGroup, version, namespace, resourceKind) + glog.V(4).Infof("[GET]: %s", uri) + bytes, err := httpRestClient.Get().RequestURI(uri).DoRaw() + if err != nil { + return nil, fmt.Errorf("failed to get custom resource list: %v", err) + } + + if err := json.Unmarshal(bytes, &crList); err != nil { + return nil, err + } + + return &crList, nil +} + +// parseAPIVersion splits "coreos.com/v1" into +// "coreos.com" and "v1". +func parseAPIVersion(apiVersion string) (apiGroup, version string, err error) { + parts := strings.Split(apiVersion, "/") + if len(parts) < 2 { + return "", "", fmt.Errorf("invalid format of api version %q, expecting APIGroup/Version", apiVersion) + } + return path.Join(parts[:len(parts)-1]...), parts[len(parts)-1], nil +} diff --git a/pkg/lib/operatorclient/fake/deployment.go b/pkg/lib/operatorclient/fake/deployment.go new file mode 100644 index 0000000000..341b508767 --- /dev/null +++ b/pkg/lib/operatorclient/fake/deployment.go @@ -0,0 +1,207 @@ +package fake + +import ( + "errors" + "fmt" + "time" + + "github.com/golang/glog" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" + appsv1beta2 "k8s.io/api/apps/v1beta2" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" +) + +const ( + deploymentRolloutPollInterval = time.Second +) + +// GetDeployment returns the Deployment object for the given namespace and name. +func (c *Client) GetDeployment(namespace, name string) (*appsv1beta2.Deployment, error) { + glog.V(4).Infof("[GET Deployment]: %s:%s", namespace, name) + return c.AppsV1beta2().Deployments(namespace).Get(name, metav1.GetOptions{}) +} + +// CreateDeployment creates the Deployment object. +func (c *Client) CreateDeployment(dep *appsv1beta2.Deployment) (*appsv1beta2.Deployment, error) { + glog.V(4).Infof("[CREATE Deployment]: %s:%s", dep.Namespace, dep.Name) + return c.AppsV1beta2().Deployments(dep.Namespace).Create(dep) +} + +// DeleteDeployment deletes the Deployment object. +func (c *Client) DeleteDeployment(namespace, name string, options *metav1.DeleteOptions) error { + glog.V(4).Infof("[DELETE Deployment]: %s:%s", namespace, name) + return c.AppsV1beta2().Deployments(namespace).Delete(name, options) +} + +// UpdateDeployment updates a Deployment object by performing a 2-way patch between the existing +// Deployment and the result of the UpdateFunction. +// +// Returns the latest Deployment and true if it was updated, or an error. +func (c *Client) UpdateDeployment(dep *appsv1beta2.Deployment) (*appsv1beta2.Deployment, bool, error) { + return c.PatchDeployment(nil, dep) +} + +// PatchDeployment updates a Deployment object by performing a 3-way patch merge between the existing +// Deployment and `original` and `modified` manifests. +// +// Returns the latest Deployment and true if it was updated, or an error. +func (c *Client) PatchDeployment(original, modified *appsv1beta2.Deployment) (*appsv1beta2.Deployment, bool, error) { + namespace, name := modified.Namespace, modified.Name + glog.V(4).Infof("[PATCH Deployment]: %s:%s", namespace, name) + + current, err := c.AppsV1beta2().Deployments(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + return nil, false, err + } + if modified == nil { + return nil, false, errors.New("modified cannot be nil") + } + if original == nil { + original = current // Emulate 2-way merge. + } + current.TypeMeta = modified.TypeMeta // make sure the type metas won't conflict. + patchBytes, err := createThreeWayMergePatchPreservingCommands(original, modified, current) + if err != nil { + return nil, false, err + } + updated, err := c.AppsV1beta2().Deployments(namespace).Patch(name, types.StrategicMergePatchType, patchBytes) + if err != nil { + return nil, false, err + } + return updated, current.GetResourceVersion() != updated.GetResourceVersion(), nil +} + +// RollingUpdateDeployment performs a rolling update on the given Deployment. It requires that the +// Deployment uses the RollingUpdateDeploymentStrategyType update strategy. +func (c *Client) RollingUpdateDeployment(dep *appsv1beta2.Deployment) (*appsv1beta2.Deployment, bool, error) { + return c.RollingUpdateDeploymentMigrations(dep.Namespace, dep.Name, operatorclient.Update(dep)) +} + +// RollingUpdateDeploymentMigrations performs a rolling update on the given Deployment. It +// requires that the Deployment uses the RollingUpdateDeploymentStrategyType update strategy. +// +// RollingUpdateDeploymentMigrations will run any before / during / after migrations that have been +// specified in the upgrade options. +func (c *Client) RollingUpdateDeploymentMigrations(namespace, name string, f operatorclient.UpdateFunction) (*appsv1beta2.Deployment, bool, error) { + glog.V(4).Infof("[ROLLING UPDATE Deployment]: %s:%s", namespace, name) + return c.RollingPatchDeploymentMigrations(namespace, name, updateToPatch(f)) +} + +// RollingPatchDeployment performs a 3-way patch merge followed by rolling update on the given +// Deployment. It requires that the Deployment uses the RollingUpdateDeploymentStrategyType update +// strategy. +// +// RollingPatchDeployment will run any before / after migrations that have been specified in the +// upgrade options. +func (c *Client) RollingPatchDeployment(original, modified *appsv1beta2.Deployment) (*appsv1beta2.Deployment, bool, error) { + return c.RollingPatchDeploymentMigrations(modified.Namespace, modified.Name, patch(original, modified)) +} + +// RollingPatchDeploymentMigrations performs a 3-way patch merge followed by rolling update on +// the given Deployment. It requires that the Deployment uses the RollingUpdateDeploymentStrategyType +// update strategy. +// +// RollingPatchDeploymentMigrations will run any before / after migrations that have been specified +// in the upgrade options. +func (c *Client) RollingPatchDeploymentMigrations(namespace, name string, f operatorclient.PatchFunction) (*appsv1beta2.Deployment, bool, error) { + glog.V(4).Infof("[ROLLING PATCH Deployment]: %s:%s", namespace, name) + + current, err := c.AppsV1beta2().Deployments(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + return nil, false, err + } + if err := checkDeploymentRollingUpdateEnabled(current); err != nil { + return nil, false, err + } + + originalObj, modifiedObj, err := f(current.DeepCopy()) + if err != nil { + return nil, false, err + } + // Check for nil interfaces. + if modifiedObj == nil { + return nil, false, errors.New("modified cannot be nil") + } + if originalObj == nil { + originalObj = current // Emulate 2-way merge. + } + original, modified := originalObj.(*appsv1beta2.Deployment), modifiedObj.(*appsv1beta2.Deployment) + // Check for nil pointers. + if modified == nil { + return nil, false, errors.New("modified cannot be nil") + } + if original == nil { + original = current // Emulate 2-way merge. + } + current.TypeMeta = modified.TypeMeta // make sure the type metas won't conflict. + patchBytes, err := createThreeWayMergePatchPreservingCommands(original, modified, current) + if err != nil { + return nil, false, err + } + updated, err := c.AppsV1beta2().Deployments(namespace).Patch(name, types.StrategicMergePatchType, patchBytes) + if err != nil { + return nil, false, err + } + if err = c.waitForDeploymentRollout(updated); err != nil { + return nil, false, err + } + + return updated, current.GetResourceVersion() != updated.GetResourceVersion(), nil +} + +func checkDeploymentRollingUpdateEnabled(dep *appsv1beta2.Deployment) error { + enabled := dep.Spec.Strategy.Type == appsv1beta2.RollingUpdateDeploymentStrategyType || dep.Spec.Strategy.Type == "" // Deployments rolling update by default + if !enabled { + return fmt.Errorf("Deployment %s/%s does not have rolling update strategy enabled", dep.GetNamespace(), dep.GetName()) + } + return nil +} + +func (c *Client) waitForDeploymentRollout(dep *appsv1beta2.Deployment) error { + return wait.PollInfinite(deploymentRolloutPollInterval, func() (bool, error) { + d, err := c.GetDeployment(dep.Namespace, dep.Name) + if err != nil { + // Do not return error here, as we could be updating the API Server itself, in which case we + // want to continue waiting. + glog.Errorf("error getting Deployment %s during rollout: %v", dep.Name, err) + return false, nil + } + if d.Generation <= d.Status.ObservedGeneration && d.Status.UpdatedReplicas == d.Status.Replicas && d.Status.UnavailableReplicas == 0 { + return true, nil + } + return false, nil + }) +} + +// CreateOrRollingUpdateDeployment creates the Deployment if it doesn't exist. If the Deployment +// already exists, it will update the Deployment and wait for it to rollout. Returns true if the +// Deployment was created or updated, false if there was no update. +func (c *Client) CreateOrRollingUpdateDeployment(dep *appsv1beta2.Deployment) (*appsv1beta2.Deployment, bool, error) { + glog.V(4).Infof("[CREATE OR ROLLING UPDATE Deployment]: %s:%s", dep.Namespace, dep.Name) + + _, err := c.GetDeployment(dep.Namespace, dep.Name) + if err != nil { + if !apierrors.IsNotFound(err) { + return nil, false, err + } + created, err := c.CreateDeployment(dep) + if err != nil { + return nil, false, err + } + return created, true, err + } + return c.RollingUpdateDeployment(dep) +} + +// ListDeploymentsWithLabels returns a list of deployments that matches the label selector. +// An empty list will be returned if no such deployments is found. +func (c *Client) ListDeploymentsWithLabels(namespace string, labels labels.Set) (*appsv1beta2.DeploymentList, error) { + glog.V(4).Infof("[LIST Deployments] in %s, labels: %v", namespace, labels) + + opts := metav1.ListOptions{LabelSelector: labels.String()} + return c.AppsV1beta2().Deployments(namespace).List(opts) +} diff --git a/pkg/lib/operatorclient/fake/patch.go b/pkg/lib/operatorclient/fake/patch.go new file mode 100644 index 0000000000..1521c29174 --- /dev/null +++ b/pkg/lib/operatorclient/fake/patch.go @@ -0,0 +1,358 @@ +package fake + +import ( + "encoding/json" + "fmt" + + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" + appsv1beta2 "k8s.io/api/apps/v1beta2" + "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + v1beta1ext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/strategicpatch" +) + +// Update returns a default UpdateFunction implementation that passes its argument through to the +// Update* function directly, ignoring the current object. +// +// Example usage: +// +// client.UpdateDaemonSet(namespace, name, types.Update(obj)) +func Update(obj metav1.Object) operatorclient.UpdateFunction { + return func(_ metav1.Object) (metav1.Object, error) { + return obj, nil + } +} + +// Patch returns a default PatchFunction implementation that passes its arguments through to the +// patcher directly, ignoring the current object. +// +// Example usage: +// +// client.PatchDaemonSet(namespace, name, types.Patch(original, current)) +func patch(original metav1.Object, modified metav1.Object) operatorclient.PatchFunction { + return func(_ metav1.Object) (metav1.Object, metav1.Object, error) { + return original, modified, nil + } +} + +// UpdateToPatch wraps an UpdateFunction as a PatchFunction. +func updateToPatch(f operatorclient.UpdateFunction) operatorclient.PatchFunction { + return func(obj metav1.Object) (metav1.Object, metav1.Object, error) { + obj, err := f(obj) + return nil, obj, err + } +} + +func createPatch(original, modified runtime.Object) ([]byte, error) { + originalData, err := json.Marshal(original) + if err != nil { + return nil, err + } + modifiedData, err := json.Marshal(modified) + if err != nil { + return nil, err + } + return strategicpatch.CreateTwoWayMergePatch(originalData, modifiedData, original) +} + +func createThreeWayMergePatchPreservingCommands(original, modified, current runtime.Object) ([]byte, error) { + var datastruct runtime.Object + switch { + case original != nil: + datastruct = original + case modified != nil: + datastruct = modified + case current != nil: + datastruct = current + default: + return nil, nil // A 3-way merge of `nil`s is `nil`. + } + patchMeta, err := strategicpatch.NewPatchMetaFromStruct(datastruct) + if err != nil { + return nil, err + } + + // Create normalized clones of objects. + original, err = cloneAndNormalizeObject(original) + if err != nil { + return nil, err + } + modified, err = cloneAndNormalizeObject(modified) + if err != nil { + return nil, err + } + current, err = cloneAndNormalizeObject(current) + if err != nil { + return nil, err + } + // Perform 3-way merge of annotations and labels. + if err := mergeAnnotationsAndLabels(original, modified, current); err != nil { + return nil, err + } + // Construct 3-way JSON merge patch. + originalData, err := json.Marshal(original) + if err != nil { + return nil, err + } + modifiedData, err := json.Marshal(modified) + if err != nil { + return nil, err + } + currentData, err := json.Marshal(current) + if err != nil { + return nil, err + } + return strategicpatch.CreateThreeWayMergePatch(originalData, modifiedData, currentData, patchMeta, false /* overwrite */) +} + +func cloneAndNormalizeObject(obj runtime.Object) (runtime.Object, error) { + if obj == nil { + return obj, nil + } + + // Clone the object since it will be modified. + obj = obj.DeepCopyObject() + switch obj := obj.(type) { + case *appsv1beta2.DaemonSet: + if obj != nil { + // These are only extracted from current; should not be considered for diffs. + obj.ObjectMeta.ResourceVersion = "" + obj.ObjectMeta.CreationTimestamp = metav1.Time{} + obj.Status = appsv1beta2.DaemonSetStatus{} + } + case *appsv1beta2.Deployment: + if obj != nil { + // These are only extracted from current; should not be considered for diffs. + obj.ObjectMeta.ResourceVersion = "" + obj.ObjectMeta.CreationTimestamp = metav1.Time{} + obj.Status = appsv1beta2.DeploymentStatus{} + } + case *v1.Service: + if obj != nil { + // These are only extracted from current; should not be considered for diffs. + obj.ObjectMeta.ResourceVersion = "" + obj.ObjectMeta.CreationTimestamp = metav1.Time{} + obj.Status = v1.ServiceStatus{} + // ClusterIP for service is immutable, so cannot patch. + obj.Spec.ClusterIP = "" + } + case *extensionsv1beta1.Ingress: + if obj != nil { + // These are only extracted from current; should not be considered for diffs. + obj.ObjectMeta.ResourceVersion = "" + obj.ObjectMeta.CreationTimestamp = metav1.Time{} + obj.Status = extensionsv1beta1.IngressStatus{} + } + case *v1beta1ext.CustomResourceDefinition: + if obj != nil { + // These are only extracted from current; should not be considered for diffs. + obj.ObjectMeta.ResourceVersion = "" + obj.ObjectMeta.CreationTimestamp = metav1.Time{} + obj.ObjectMeta.SelfLink = "" + obj.ObjectMeta.UID = "" + obj.Status = v1beta1ext.CustomResourceDefinitionStatus{} + } + default: + return nil, fmt.Errorf("unhandled type: %T", obj) + } + return obj, nil +} + +// mergeAnnotationsAndLabels performs a 3-way merge of all annotations and labels using custom +// 3-way merge logic defined in mergeMaps() below. +func mergeAnnotationsAndLabels(original, modified, current runtime.Object) error { + if original == nil || modified == nil || current == nil { + return nil + } + + accessor := meta.NewAccessor() + if err := mergeMaps(original, modified, current, accessor.Annotations, accessor.SetAnnotations); err != nil { + return err + } + if err := mergeMaps(original, modified, current, accessor.Labels, accessor.SetLabels); err != nil { + return err + } + + switch current := current.(type) { + case *appsv1beta2.DaemonSet: + getter := func(obj runtime.Object) (map[string]string, error) { + return obj.(*appsv1beta2.DaemonSet).Spec.Template.Annotations, nil + } + setter := func(obj runtime.Object, val map[string]string) error { + obj.(*appsv1beta2.DaemonSet).Spec.Template.Annotations = val + return nil + } + if err := mergeMaps(original, modified, current, getter, setter); err != nil { + return err + } + getter = func(obj runtime.Object) (map[string]string, error) { + return obj.(*appsv1beta2.DaemonSet).Spec.Template.Labels, nil + } + setter = func(obj runtime.Object, val map[string]string) error { + obj.(*appsv1beta2.DaemonSet).Spec.Template.Labels = val + return nil + } + if err := mergeMaps(original, modified, current, getter, setter); err != nil { + return err + } + case *appsv1beta2.Deployment: + getter := func(obj runtime.Object) (map[string]string, error) { + return obj.(*appsv1beta2.Deployment).Spec.Template.Annotations, nil + } + setter := func(obj runtime.Object, val map[string]string) error { + obj.(*appsv1beta2.Deployment).Spec.Template.Annotations = val + return nil + } + if err := mergeMaps(original, modified, current, getter, setter); err != nil { + return err + } + getter = func(obj runtime.Object) (map[string]string, error) { + return obj.(*appsv1beta2.Deployment).Spec.Template.Labels, nil + } + setter = func(obj runtime.Object, val map[string]string) error { + obj.(*appsv1beta2.Deployment).Spec.Template.Labels = val + return nil + } + if err := mergeMaps(original, modified, current, getter, setter); err != nil { + return err + } + } + return nil +} + +// mergeMaps creates a patch using createThreeWayMapPatch and if the patch is non-empty applies +// the patch to the input. The getter and setter are used to access the map inside the given +// objects. +func mergeMaps(original, modified, current runtime.Object, getter func(runtime.Object) (map[string]string, error), setter func(runtime.Object, map[string]string) error) error { + originalMap, err := getter(original) + if err != nil { + return err + } + modifiedMap, err := getter(modified) + if err != nil { + return err + } + currentMap, err := getter(current) + if err != nil { + return err + } + + patch, err := createThreeWayMapPatch(originalMap, modifiedMap, currentMap) + if err != nil { + return err + } + if len(patch) == 0 { + return nil // nothing to apply. + } + modifiedMap = applyMapPatch(originalMap, currentMap, patch) + + if err := setter(original, originalMap); err != nil { + return err + } + if err := setter(modified, modifiedMap); err != nil { + return err + } + return setter(current, currentMap) +} + +// applyMapPatch creates a copy of current and applies the three-way map patch to it. +func applyMapPatch(original, current map[string]string, patch map[string]interface{}) map[string]string { + merged := make(map[string]string, len(current)) + for k, v := range current { + merged[k] = v + } + for k, v := range patch { + if v == nil { + delete(merged, k) + } else { + merged[k] = v.(string) + if _, ok := current[k]; !ok { + // If we are re-adding something that may have already been in original then ensure it is + // removed from `original` to avoid a conflict in upstream patch code. + delete(original, k) + } + } + } + return merged +} + +// createThreeWayMapPatch constructs a 3-way patch between original, modified, and current. The +// patch contains only keys that are added, keys that are removed (with their values set to nil) or +// keys whose values are modified. Returns an error if there is a conflict for any key. +// +// The behavior is defined as follows: +// +// - If an item is present in modified, ensure it exists in current. +// - If an item is present in original and removed in modified, remove it from current. +// - If an item is present only in current, leave it as-is. +// +// This effectively "enforces" that all items present in modified are present in current, and all +// items deleted from original => modified are deleted in current. +// +// The following will cause a conflict: +// +// (1) An item was deleted from original => modified but modified from original => current. +// (2) An item was modified differently from original => modified and original => current. +func createThreeWayMapPatch(original, modified, current map[string]string) (map[string]interface{}, error) { + // Create union of keys. + keys := make(map[string]struct{}) + for k := range original { + keys[k] = struct{}{} + } + for k := range modified { + keys[k] = struct{}{} + } + for k := range current { + keys[k] = struct{}{} + } + + // Create patch according to rules. + patch := make(map[string]interface{}) + for k := range keys { + oVal, oOk := original[k] + mVal, mOk := modified[k] + cVal, cOk := current[k] + + switch { + case oOk && mOk && cOk: + // present in all three. + if mVal != cVal { + if oVal != cVal { + // conflict type 2: changed to different values in modified and current. + return nil, fmt.Errorf("conflict at key %v: original = %v, modified = %v, current = %v", k, oVal, mVal, cVal) + } + patch[k] = mVal + } + case !oOk && mOk && cOk: + // added in modified and current. + if mVal != cVal { + // conflict type 2: added different values in modified and current. + return nil, fmt.Errorf("conflict at key %v: original = , modified = %v, current = %v", k, mVal, cVal) + } + case oOk && !mOk && cOk: + // deleted in modified. + if oVal != cVal { + // conflict type 1: changed from original to current, removed in modified. + return nil, fmt.Errorf("conflict at key %v, original = %v, modified = , current = %v", k, oVal, cVal) + } + patch[k] = nil + case oOk && mOk && !cOk: + // deleted in current. + patch[k] = mVal + case !oOk && !mOk && cOk: + // only exists in current. + case !oOk && mOk && !cOk: + // added in modified. + patch[k] = mVal + case oOk && !mOk && !cOk: + // deleted in both modified and current. + case !oOk && !mOk && !cOk: + // unreachable. + } + } + return patch, nil +} diff --git a/pkg/lib/operatorclient/fake/serviceaccount.go b/pkg/lib/operatorclient/fake/serviceaccount.go new file mode 100644 index 0000000000..269872d894 --- /dev/null +++ b/pkg/lib/operatorclient/fake/serviceaccount.go @@ -0,0 +1,39 @@ +package fake + +import ( + "fmt" + + "github.com/golang/glog" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// CreateServiceAccount creates the serviceAccount. +func (c *Client) CreateServiceAccount(ig *v1.ServiceAccount) (*v1.ServiceAccount, error) { + return c.CoreV1().ServiceAccounts(ig.GetNamespace()).Create(ig) +} + +// GetServiceAccount returns the existing serviceAccount. +func (c *Client) GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error) { + return c.CoreV1().ServiceAccounts(namespace).Get(name, metav1.GetOptions{}) +} + +// DeleteServiceAccount deletes the serviceAccount. +func (c *Client) DeleteServiceAccount(namespace, name string, options *metav1.DeleteOptions) error { + return c.CoreV1().ServiceAccounts(namespace).Delete(name, options) +} + +// UpdateServiceAccount will update the given ServiceAccount resource. +func (c *Client) UpdateServiceAccount(sa *v1.ServiceAccount) (*v1.ServiceAccount, error) { + glog.V(4).Infof("[UPDATE ServiceAccount]: %s", sa.GetName()) + oldSa, err := c.GetServiceAccount(sa.GetNamespace(), sa.GetName()) + if err != nil { + return nil, err + } + patchBytes, err := createPatch(oldSa, sa) + if err != nil { + return nil, fmt.Errorf("error creating patch for ServiceAccount: %v", err) + } + return c.Core().ServiceAccounts(sa.GetNamespace()).Patch(sa.GetName(), types.StrategicMergePatchType, patchBytes) +} diff --git a/test/e2e/csv_e2e_test.go b/test/e2e/csv_e2e_test.go index 6bf0c8595c..e5897e0b4f 100644 --- a/test/e2e/csv_e2e_test.go +++ b/test/e2e/csv_e2e_test.go @@ -641,4 +641,151 @@ func TestUpdateCSVDifferentDeploymentName(t *testing.T) { require.NoError(t, err) } +func TestUpdateCSVMultipleIntermediates(t *testing.T) { + c := newKubeClient(t) + crc := newCRClient(t) + + // create "current" CSV + strategy := install.StrategyDetailsDeployment{ + DeploymentSpecs: []install.StrategyDeploymentSpec{ + { + Name: genName("dep-"), + Spec: newNginxDeployment(genName("nginx-")), + }, + }, + } + strategyRaw, err := json.Marshal(strategy) + require.NoError(t, err) + + csv := v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: v1alpha1.ClusterServiceVersionKind, + APIVersion: v1alpha1.ClusterServiceVersionAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: genName("csv"), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + InstallStrategy: v1alpha1.NamedInstallStrategy{ + StrategyName: install.InstallStrategyNameDeployment, + StrategySpecRaw: strategyRaw, + }, + CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ + Owned: []v1alpha1.CRDDescription{ + { + Name: "ins2.cluster.com", + Version: "v1alpha1", + Kind: "InCluster2", + DisplayName: "Ins2", + Description: "In the cluster2", + }, + }, + }, + }, + } + + // Create dependency first (CRD) + cleanupCRD, err := createCRD(c, extv1beta1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ins2.cluster.com", + }, + Spec: extv1beta1.CustomResourceDefinitionSpec{ + Group: "cluster.com", + Versions: []extv1beta1.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Served: true, + Storage: true, + }, + }, + Names: extv1beta1.CustomResourceDefinitionNames{ + Plural: "ins2", + Singular: "in2", + Kind: "InCluster2", + ListKind: "InClusterList2", + }, + Scope: "Namespaced", + }, + }) + require.NoError(t, err) + defer cleanupCRD() + + // don't need to clean up this CSV, it will be deleted by the upgrade process + _, err = createCSV(t, c, crc, csv, true) + require.NoError(t, err) + + // Wait for current CSV to succeed + _, err = fetchCSV(t, crc, csv.Name, csvSucceededChecker) + require.NoError(t, err) + + // Should have created deployment + dep, err := c.GetDeployment(testNamespace, strategy.DeploymentSpecs[0].Name) + require.NoError(t, err) + require.NotNil(t, dep) + + // Create "updated" CSV + strategyNew := install.StrategyDetailsDeployment{ + DeploymentSpecs: []install.StrategyDeploymentSpec{ + { + Name: genName("dep2"), + Spec: newNginxDeployment(genName("nginx-")), + }, + }, + } + strategyNewRaw, err := json.Marshal(strategyNew) + require.NoError(t, err) + + csvNew := v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: v1alpha1.ClusterServiceVersionKind, + APIVersion: v1alpha1.ClusterServiceVersionAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: genName("csv2"), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + Replaces: csv.Name, + InstallStrategy: v1alpha1.NamedInstallStrategy{ + StrategyName: install.InstallStrategyNameDeployment, + StrategySpecRaw: strategyNewRaw, + }, + CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ + Owned: []v1alpha1.CRDDescription{ + { + Name: "ins2.cluster.com", + Version: "v1alpha1", + Kind: "InCluster2", + DisplayName: "Ins2", + Description: "In the cluster2", + }, + }, + }, + }, + } + + cleanupNewCSV, err := createCSV(t, c, crc, csvNew, true) + require.NoError(t, err) + defer cleanupNewCSV() + + // Wait for updated CSV to succeed + fetchedCSV, err := fetchCSV(t, crc, csvNew.Name, csvSucceededChecker) + require.NoError(t, err) + + // Fetch cluster service version again to check for unnecessary control loops + sameCSV, err := fetchCSV(t, crc, csvNew.Name, csvSucceededChecker) + require.NoError(t, err) + compareResources(t, fetchedCSV, sameCSV) + + // Should have created new deployment and deleted old + depNew, err := c.GetDeployment(testNamespace, strategyNew.DeploymentSpecs[0].Name) + require.NoError(t, err) + require.NotNil(t, depNew) + err = waitForDeploymentToDelete(t, c, strategy.DeploymentSpecs[0].Name) + require.NoError(t, err) + + // Should eventually GC the CSV + err = waitForCSVToDelete(t, crc, csv.Name) + require.NoError(t, err) +} + // TODO: test behavior when replaces field doesn't point to existing CSV From 9d694b824db09e8d20eb18f76db6e71e92fd371d Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Fri, 3 Aug 2018 20:44:18 -0400 Subject: [PATCH 2/9] fix(olm): add fixes for GC optimization --- pkg/controller/operators/olm/operator.go | 6 +- pkg/controller/operators/olm/operator_test.go | 160 +++++++++++++++++- 2 files changed, 162 insertions(+), 4 deletions(-) diff --git a/pkg/controller/operators/olm/operator.go b/pkg/controller/operators/olm/operator.go index f1bf74879c..3948e180e6 100644 --- a/pkg/controller/operators/olm/operator.go +++ b/pkg/controller/operators/olm/operator.go @@ -235,15 +235,17 @@ func (a *Operator) transitionCSVState(in v1alpha1.ClusterServiceVersion) (out *v // if we can find a newer version that's successfully installed, we're safe to mark all intermediates for _, csv := range a.findIntermediatesForDeletion(out) { - // TODO fix this // we only mark them in this step, in case some get deleted but others fail and break the replacement chain csv.SetPhase(v1alpha1.CSVPhaseDeleting, v1alpha1.CSVReasonReplaced, "has been replaced by a newer ClusterServiceVersion that has successfully installed.") + // ignore errors and success here; this step is just an optimization to speed up GC + a.client.OperatorsV1alpha1().ClusterServiceVersions(csv.GetNamespace()).UpdateStatus(csv) } // if there's no newer version, requeue for processing (likely will be GCable before resync) //a.requeueCSV(out) case v1alpha1.CSVPhaseDeleting: - syncError := a.OpClient.DeleteCustomResource(v1alpha1.GroupName, v1alpha1.GroupVersion, out.GetNamespace(), v1alpha1.ClusterServiceVersionKind, out.GetName()) + foreground := metav1.DeletePropagationForeground + syncError = a.client.OperatorsV1alpha1().ClusterServiceVersions(out.GetNamespace()).Delete(out.GetName(), &metav1.DeleteOptions{PropagationPolicy: &foreground}) if syncError != nil { logger.Debugf("unable to get delete csv marked for deletion: %s", syncError.Error()) } diff --git a/pkg/controller/operators/olm/operator_test.go b/pkg/controller/operators/olm/operator_test.go index d09ddd6124..6f96b53f64 100644 --- a/pkg/controller/operators/olm/operator_test.go +++ b/pkg/controller/operators/olm/operator_test.go @@ -2184,10 +2184,55 @@ func TestCSVUpgrades(t *testing.T) { }, }, }, + { + name: "CSVDeletedToGone", + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", + namespace, + "", + installStrategy("csv1-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseDeleting, + ), + csv("csv2", + namespace, + "csv1", + installStrategy("csv2-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseSucceeded, + ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + objs: []runtime.Object{ + deployment("csv1-dep1", namespace), + deployment("csv2-dep1", namespace), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: false, phase: v1alpha1.CSVPhaseNone}, + "csv2": {exists: true, phase: v1alpha1.CSVPhaseSucceeded}, + }, + }, + }, { name: "CSVMultipleReplacingToDeleted", initial: initial{ + // order matters in this test case - we want to apply the latest CSV first to test the GC marking csvs: []runtime.Object{ + csv("csv3", + namespace, + "csv2", + installStrategy("csv3-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseSucceeded, + ), csv("csv1", namespace, "", @@ -2204,6 +2249,28 @@ func TestCSVUpgrades(t *testing.T) { []*v1beta1.CustomResourceDefinition{}, v1alpha1.CSVPhaseReplacing, ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + objs: []runtime.Object{ + deployment("csv1-dep1", namespace), + deployment("csv2-dep1", namespace), + deployment("csv3-dep1", namespace), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: true, phase: v1alpha1.CSVPhaseDeleting}, + "csv2": {exists: true, phase: v1alpha1.CSVPhaseReplacing}, + "csv3": {exists: true, phase: v1alpha1.CSVPhaseSucceeded}, + }, + }, + }, + { + name: "CSVMultipleDeletedToGone", + initial: initial{ + csvs: []runtime.Object{ csv("csv3", namespace, "csv2", @@ -2212,6 +2279,22 @@ func TestCSVUpgrades(t *testing.T) { []*v1beta1.CustomResourceDefinition{}, v1alpha1.CSVPhaseSucceeded, ), + csv("csv1", + namespace, + "", + installStrategy("csv1-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseDeleting, + ), + csv("csv2", + namespace, + "csv1", + installStrategy("csv2-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseReplacing, + ), }, crds: []runtime.Object{ crd("c1", "v1"), @@ -2224,12 +2307,85 @@ func TestCSVUpgrades(t *testing.T) { }, expected: expected{ csvStates: map[string]csvState{ - "csv1": {exists: true, phase: v1alpha1.CSVPhaseDeleting}, + "csv1": {exists: false, phase: v1alpha1.CSVPhaseNone}, + "csv2": {exists: true, phase: v1alpha1.CSVPhaseReplacing}, + "csv3": {exists: true, phase: v1alpha1.CSVPhaseSucceeded}, + }, + }, + }, + { + name: "CSVMultipleDeletedToGone/AfterOneDeleted", + initial: initial{ + csvs: []runtime.Object{ + csv("csv2", + namespace, + "csv1", + installStrategy("csv2-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseReplacing, + ), + csv("csv3", + namespace, + "csv2", + installStrategy("csv3-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseSucceeded, + ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + objs: []runtime.Object{ + deployment("csv2-dep1", namespace), + deployment("csv3-dep1", namespace), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: false, phase: v1alpha1.CSVPhaseNone}, "csv2": {exists: true, phase: v1alpha1.CSVPhaseDeleting}, "csv3": {exists: true, phase: v1alpha1.CSVPhaseSucceeded}, }, }, }, + { + name: "CSVMultipleDeletedToGone/AfterTwoDeleted", + initial: initial{ + csvs: []runtime.Object{ + csv("csv2", + namespace, + "csv1", + installStrategy("csv2-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseDeleting, + ), + csv("csv3", + namespace, + "csv2", + installStrategy("csv3-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseSucceeded, + ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + objs: []runtime.Object{ + deployment("csv2-dep1", namespace), + deployment("csv3-dep1", namespace), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv2": {exists: false, phase: v1alpha1.CSVPhaseNone}, + "csv3": {exists: true, phase: v1alpha1.CSVPhaseSucceeded}, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2265,7 +2421,7 @@ func TestCSVUpgrades(t *testing.T) { // verify expectations of csvs in cluster for csvName, csvState := range tt.expected.csvStates { csv, ok := outCSVMap[csvName] - require.Equal(t, ok, csvState.exists) + assert.Equal(t, ok, csvState.exists, "%s existence should be %t", csvName, csvState.exists) if csvState.exists { assert.Equal(t, csvState.phase, csv.Status.Phase, "%s had incorrect phase", csvName) } From 112b603f73140a4355d7d2855140055976b46cb1 Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Sat, 4 Aug 2018 11:15:10 -0400 Subject: [PATCH 3/9] chore(tests): rewrite TestIsReplacing with fakes --- pkg/controller/operators/olm/operator_test.go | 130 +++++++++--------- 1 file changed, 67 insertions(+), 63 deletions(-) diff --git a/pkg/controller/operators/olm/operator_test.go b/pkg/controller/operators/olm/operator_test.go index 6f96b53f64..cf42290d4f 100644 --- a/pkg/controller/operators/olm/operator_test.go +++ b/pkg/controller/operators/olm/operator_test.go @@ -1791,68 +1791,6 @@ func TestIsBeingReplaced(t *testing.T) { } } -func TestIsReplacing(t *testing.T) { - type clusterState struct { - oldCSV *v1alpha1.ClusterServiceVersion - csvQueryErr error - } - tests := []struct { - in *v1alpha1.ClusterServiceVersion - state clusterState - out *v1alpha1.ClusterServiceVersion - description string - }{ - { - in: testCSV(""), - state: clusterState{ - oldCSV: nil, - csvQueryErr: fmt.Errorf("couldn't query"), - }, - out: nil, - description: "QueryErr", - }, - { - in: testCSV(""), - state: clusterState{ - oldCSV: testCSV("test2"), - csvQueryErr: nil, - }, - out: nil, - description: "CSVInCluster/NotReplacing", - }, - { - in: withReplaces(testCSV("test2"), "test"), - state: clusterState{ - oldCSV: testCSV("test"), - csvQueryErr: nil, - }, - out: testCSV("test"), - description: "CSVInCluster/Replacing", - }, - { - in: withReplaces(testCSV("test2"), "test"), - state: clusterState{ - oldCSV: nil, - csvQueryErr: fmt.Errorf("not found"), - }, - out: nil, - description: "CSVInCluster/ReplacingNotFound", - }, - } - for _, tt := range tests { - ctrl := gomock.NewController(t) - mockOp := NewMockALMOperator(ctrl) - - mockIsReplacing(t, mockOp.MockOpClient, tt.state.oldCSV, tt.in, tt.state.csvQueryErr) - - t.Run(tt.description, func(t *testing.T) { - out := mockOp.isReplacing(tt.in) - require.EqualValues(t, out, tt.out) - }) - ctrl.Finish() - } -} - func deployment(deploymentName, namespace string) *v1beta2.Deployment { var singleInstance = int32(1) return &v1beta2.Deployment{ @@ -2000,7 +1938,7 @@ func crd(name string, version string) *v1beta1.CustomResourceDefinition { } } -func TestCSVUpgrades(t *testing.T) { +func TestTransitionCSVHappyPath(t *testing.T) { log.SetLevel(log.DebugLevel) namespace := "ns" @@ -2429,3 +2367,69 @@ func TestCSVUpgrades(t *testing.T) { }) } } + +func TestIsReplacing(t *testing.T) { + log.SetLevel(log.DebugLevel) + namespace := "ns" + + type initial struct { + csvs []runtime.Object + } + tests := []struct { + name string + initial initial + in *v1alpha1.ClusterServiceVersion + expected *v1alpha1.ClusterServiceVersion + }{ + { + name: "QueryErr", + in: csv("name", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + initial: initial{ + csvs: []runtime.Object{}, + }, + expected: nil, + }, + { + name: "CSVInCluster/NotReplacing", + in: csv("csv1", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + }, + }, + expected: nil, + }, + { + name: "CSVInCluster/Replacing", + in: csv("csv2", namespace, "csv1", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + }, + }, + expected: csv("csv1", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + }, + { + name: "CSVInCluster/ReplacingNotFound", + in: csv("csv2", namespace, "csv1", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + initial: initial{ + csvs: []runtime.Object{ + csv("csv3", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + }, + }, + expected: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // configure cluster state + clientFake := fake.NewSimpleClientset(tt.initial.csvs...) + + op := &Operator{ + client: clientFake, + } + + require.Equal(t, tt.expected, op.isReplacing(tt.in)) + }) + } +} From d2f9f803fef49e4c9d9000f90bfe38a4b0ccb660 Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Sat, 4 Aug 2018 11:25:20 -0400 Subject: [PATCH 4/9] chore(tests): rewrite TestIsBeingReplaced with fakes --- pkg/controller/operators/olm/operator_test.go | 111 ++++++++---------- 1 file changed, 48 insertions(+), 63 deletions(-) diff --git a/pkg/controller/operators/olm/operator_test.go b/pkg/controller/operators/olm/operator_test.go index cf42290d4f..ec420261df 100644 --- a/pkg/controller/operators/olm/operator_test.go +++ b/pkg/controller/operators/olm/operator_test.go @@ -1728,69 +1728,6 @@ func TestReplacingCSV(t *testing.T) { } } -func TestIsBeingReplaced(t *testing.T) { - type clusterState struct { - csvsInNamespace []*v1alpha1.ClusterServiceVersion - csvQueryErr error - } - tests := []struct { - in *v1alpha1.ClusterServiceVersion - state clusterState - out *v1alpha1.ClusterServiceVersion - description string - }{ - { - in: testCSV(""), - state: clusterState{ - csvsInNamespace: nil, - csvQueryErr: fmt.Errorf("couldn't query"), - }, - out: nil, - description: "QueryErr", - }, - { - in: testCSV(""), - state: clusterState{ - csvsInNamespace: nil, - csvQueryErr: nil, - }, - out: nil, - description: "NoOtherCSVs", - }, - { - in: testCSV(""), - state: clusterState{ - csvsInNamespace: []*v1alpha1.ClusterServiceVersion{testCSV("test2")}, - csvQueryErr: nil, - }, - out: nil, - description: "CSVInCluster/NotReplacing", - }, - { - in: testCSV("test"), - state: clusterState{ - csvsInNamespace: []*v1alpha1.ClusterServiceVersion{withReplaces(testCSV("test2"), "test")}, - csvQueryErr: nil, - }, - out: withReplaces(testCSV("test2"), "test"), - description: "CSVInCluster/Replacing", - }, - } - for _, tt := range tests { - ctrl := gomock.NewController(t) - mockOp := NewMockALMOperator(ctrl) - - mockCSVsInNamespace(t, mockOp.MockOpClient, tt.in.GetNamespace(), tt.state.csvsInNamespace, tt.state.csvQueryErr) - - t.Run(tt.description, func(t *testing.T) { - csvsInNamespace := mockOp.csvsInNamespace(tt.in.GetNamespace()) - out := mockOp.isBeingReplaced(tt.in, csvsInNamespace) - require.EqualValues(t, out, tt.out) - }) - ctrl.Finish() - } -} - func deployment(deploymentName, namespace string) *v1beta2.Deployment { var singleInstance = int32(1) return &v1beta2.Deployment{ @@ -2433,3 +2370,51 @@ func TestIsReplacing(t *testing.T) { }) } } + +func TestIsBeingReplaced(t *testing.T) { + namespace := "ns" + + type initial struct { + csvs []*v1alpha1.ClusterServiceVersion + } + tests := []struct { + name string + initial initial + in *v1alpha1.ClusterServiceVersion + expected *v1alpha1.ClusterServiceVersion + }{ + { + name: "QueryErr", + in: csv("name", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + expected: nil, + }, + { + name: "CSVInCluster/NotReplacing", + in: csv("csv1", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + initial: initial{ + csvs: []*v1alpha1.ClusterServiceVersion{ + csv("csv2", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + }, + }, + expected: nil, + }, + { + name: "CSVInCluster/Replacing", + in: csv("csv1", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + initial: initial{ + csvs: []*v1alpha1.ClusterServiceVersion{ + csv("csv2", namespace, "csv1", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + }, + }, + expected: csv("csv2", namespace, "csv1", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // configure cluster state + op := &Operator{} + + require.Equal(t, tt.expected, op.isBeingReplaced(tt.in, tt.initial.csvs)) + }) + } +} From fd6b54b04866747f56ab916625adecf155b7afef Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Sun, 5 Aug 2018 11:13:47 -0400 Subject: [PATCH 5/9] chore(tests): remove old mock-based tests in favor of fakes --- Makefile | 2 +- cmd/olm/main.go | 13 +- ...operator.v0.9.0.clusterserviceversion.yaml | 2 +- ...operator.v0.9.2.clusterserviceversion.yaml | 2 +- pkg/controller/operators/olm/operator.go | 80 +- pkg/controller/operators/olm/operator_test.go | 1848 ++--------------- .../registry/directory_loader_test.go | 2 +- .../mock_queueinformer_operator.go | 60 - .../queueinformer/queueinformer_operator.go | 11 + pkg/lib/queueinformer/testqueueinformer.go | 33 - scripts/run_e2e_local.sh | 6 +- test/e2e/csv_e2e_test.go | 43 +- test/e2e/installplan_e2e_test.go | 40 - 13 files changed, 235 insertions(+), 1907 deletions(-) delete mode 100644 pkg/lib/queueinformer/mock_queueinformer_operator.go delete mode 100644 pkg/lib/queueinformer/testqueueinformer.go diff --git a/Makefile b/Makefile index 82a54fc5a4..7c424f8416 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ build-coverage: $(CMDS) $(CMDS): .FORCE @if [ cover-$(GENCOVER) = cover-true ]; then \ echo "building bin/$(shell basename $@)" with coverage; \ - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go test -o $@ -c -covermode=count -coverpkg ./pkg/... $(PKG)/cmd/$(shell basename $@); \ + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go test -o $@ -c -covermode=count -coverpkg ./pkg/controller/... $(PKG)/cmd/$(shell basename $@); \ else \ echo "building bin/$(shell basename $@)"; \ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o $@ $(PKG)/cmd/$(shell basename $@); \ diff --git a/cmd/olm/main.go b/cmd/olm/main.go index 562026174a..540e384349 100644 --- a/cmd/olm/main.go +++ b/cmd/olm/main.go @@ -10,7 +10,10 @@ import ( log "github.com/sirupsen/logrus" + "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/olm" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/signals" ) @@ -73,8 +76,16 @@ func main() { // the empty string, the resulting array will be `[]string{""}`. namespaces := strings.Split(*watchedNamespaces, ",") + // Create a client for OLM + crClient, err := client.NewClient(*kubeConfigPath) + if err != nil { + log.Fatalf("error configuring client: %s", err.Error()) + } + + opClient := operatorclient.NewClient(*kubeConfigPath) + // Create a new instance of the operator. - operator, err := olm.NewOperator(*kubeConfigPath, *wakeupInterval, annotation, namespaces) + operator, err := olm.NewOperator(crClient, opClient, &install.StrategyResolver{}, *wakeupInterval, annotation, namespaces) if err != nil { log.Fatalf("error configuring operator: %s", err.Error()) diff --git a/deploy/chart/catalog_resources/ocs/etcdoperator.v0.9.0.clusterserviceversion.yaml b/deploy/chart/catalog_resources/ocs/etcdoperator.v0.9.0.clusterserviceversion.yaml index 2fa1fcd42e..6ab15ca2b3 100644 --- a/deploy/chart/catalog_resources/ocs/etcdoperator.v0.9.0.clusterserviceversion.yaml +++ b/deploy/chart/catalog_resources/ocs/etcdoperator.v0.9.0.clusterserviceversion.yaml @@ -1,6 +1,6 @@ #! validate-crd: ./deploy/chart/templates/03-clusterserviceversion.crd.yaml #! parse-kind: ClusterServiceVersion -apiVersion: app.coreos.com/v1alpha1 +apiVersion: operators.coreos.com/v1alpha1 kind: ClusterServiceVersion metadata: name: etcdoperator.v0.9.0 diff --git a/deploy/chart/catalog_resources/ocs/etcdoperator.v0.9.2.clusterserviceversion.yaml b/deploy/chart/catalog_resources/ocs/etcdoperator.v0.9.2.clusterserviceversion.yaml index 70dfdffab7..26226f4b59 100644 --- a/deploy/chart/catalog_resources/ocs/etcdoperator.v0.9.2.clusterserviceversion.yaml +++ b/deploy/chart/catalog_resources/ocs/etcdoperator.v0.9.2.clusterserviceversion.yaml @@ -1,6 +1,6 @@ #! validate-crd: ./deploy/chart/templates/03-clusterserviceversion.crd.yaml #! parse-kind: ClusterServiceVersion -apiVersion: app.coreos.com/v1alpha1 +apiVersion: operators.coreos.com/v1alpha1 kind: ClusterServiceVersion metadata: name: etcdoperator.v0.9.2 diff --git a/pkg/controller/operators/olm/operator.go b/pkg/controller/operators/olm/operator.go index 3948e180e6..4404eece91 100644 --- a/pkg/controller/operators/olm/operator.go +++ b/pkg/controller/operators/olm/operator.go @@ -13,15 +13,16 @@ import ( "k8s.io/client-go/util/workqueue" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" - "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/informers/externalversions" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/annotator" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/queueinformer" ) var ErrRequirementsNotMet = errors.New("requirements were not met") +var ErrCRDOwnerConflict = errors.New("CRD owned by another ClusterServiceVersion") const ( FallbackWakeupInterval = 30 * time.Second @@ -35,7 +36,7 @@ type Operator struct { annotator *annotator.Annotator } -func NewOperator(kubeconfig string, wakeupInterval time.Duration, annotations map[string]string, namespaces []string) (*Operator, error) { +func NewOperator(crClient versioned.Interface, opClient operatorclient.ClientInterface, resolver install.StrategyResolverInterface, wakeupInterval time.Duration, annotations map[string]string, namespaces []string) (*Operator, error) { if wakeupInterval < 0 { wakeupInterval = FallbackWakeupInterval } @@ -43,13 +44,7 @@ func NewOperator(kubeconfig string, wakeupInterval time.Duration, annotations ma namespaces = []string{metav1.NamespaceAll} } - // Create a new client for ALM types (CRs) - crClient, err := client.NewClient(kubeconfig) - if err != nil { - return nil, err - } - - queueOperator, err := queueinformer.NewOperator(kubeconfig) + queueOperator, err := queueinformer.NewOperatorFromClient(opClient) if err != nil { return nil, err } @@ -58,7 +53,7 @@ func NewOperator(kubeconfig string, wakeupInterval time.Duration, annotations ma op := &Operator{ Operator: queueOperator, client: crClient, - resolver: &install.StrategyResolver{}, + resolver: resolver, annotator: namespaceAnnotator, } @@ -85,7 +80,8 @@ func NewOperator(kubeconfig string, wakeupInterval time.Duration, annotations ma for _, namespace := range namespaces { log.Debugf("watching for CSVs in namespace %s", namespace) sharedInformerFactory := externalversions.NewSharedInformerFactoryWithOptions(crClient, wakeupInterval, externalversions.WithNamespace(namespace)) - csvInformers = append(csvInformers, sharedInformerFactory.Operators().V1alpha1().ClusterServiceVersions().Informer()) + informer := sharedInformerFactory.Operators().V1alpha1().ClusterServiceVersions().Informer() + csvInformers = append(csvInformers, informer) } // csvInformers for each namespace all use the same backing queue @@ -104,17 +100,6 @@ func NewOperator(kubeconfig string, wakeupInterval time.Duration, annotations ma return op, nil } -//func (a *Operator) requeueCSV(csv *v1alpha1.ClusterServiceVersion) { -// k, err := cache.DeletionHandlingMetaNamespaceKeyFunc(csv) -// if err != nil { -// log.Infof("creating key failed: %s", err) -// return -// } -// log.Infof("requeueing %s", csv.SelfLink) -// a.csvQueue.AddRateLimited(k) -// return -//} - // 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) @@ -282,16 +267,16 @@ func (a *Operator) findIntermediatesForDeletion(csv *v1alpha1.ClusterServiceVers } // csvsInNamespace finds all CSVs in a namespace -func (a *Operator) csvsInNamespace(namespace string) (csvs []*v1alpha1.ClusterServiceVersion) { - // TODO: read from indexer +func (a *Operator) csvsInNamespace(namespace string) map[string]*v1alpha1.ClusterServiceVersion { csvsInNamespace, err := a.client.OperatorsV1alpha1().ClusterServiceVersions(namespace).List(metav1.ListOptions{}) if err != nil { return nil } + csvs := make(map[string]*v1alpha1.ClusterServiceVersion, len(csvsInNamespace.Items)) for _, csv := range csvsInNamespace.Items { - csvs = append(csvs, &csv) + csvs[csv.Name] = &csv } - return + return csvs } // checkReplacementsAndUpdateStatus returns an error if we can find a newer CSV and sets the status if so @@ -305,9 +290,6 @@ func (a *Operator) checkReplacementsAndUpdateStatus(csv *v1alpha1.ClusterService msg := fmt.Sprintf("being replaced by csv: %s", replacement.SelfLink) csv.SetPhase(v1alpha1.CSVPhaseReplacing, v1alpha1.CSVReasonBeingReplaced, msg) - // requeue so that we quickly pick up on replacement status changes - //a.requeueCSV(csv) - return fmt.Errorf("replacing") } return nil @@ -332,7 +314,6 @@ func (a *Operator) updateInstallStatus(csv *v1alpha1.ClusterServiceVersion, inst // if there's an error checking install that shouldn't fail the strategy, requeue with message if strategyErr != nil { csv.SetPhase(v1alpha1.CSVPhaseInstalling, requeueConditionReason, fmt.Sprintf("installing: %s", strategyErr)) - //a.requeueCSV(csv) return strategyErr } @@ -355,10 +336,6 @@ func (a *Operator) parseStrategiesAndUpdateStatus(csv *v1alpha1.ClusterServiceVe previousStrategy = nil } } - if previousStrategy != nil { - // check for status changes if we know we're replacing a CSV - //a.requeueCSV(previousCSV) - } strName := strategy.GetStrategyName() installer := a.resolver.InstallerForStrategy(strName, a.OpClient, csv, previousStrategy) @@ -387,31 +364,24 @@ func (a *Operator) requirementStatus(csv *v1alpha1.ClusterServiceVersion) (met b return } -func (a *Operator) crdOwnerConflicts(in *v1alpha1.ClusterServiceVersion, csvsInNamespace []*v1alpha1.ClusterServiceVersion) error { +func (a *Operator) crdOwnerConflicts(in *v1alpha1.ClusterServiceVersion, csvsInNamespace map[string]*v1alpha1.ClusterServiceVersion) error { + owned := false for _, crd := range in.Spec.CustomResourceDefinitions.Owned { - for _, csv := range csvsInNamespace { + for csvName, csv := range csvsInNamespace { + if csvName == in.GetName() { + continue + } if csv.OwnsCRD(crd.Name) { - // two csvs own the same CRD, only valid if there's a replacing chain between them - // TODO: this and the other replacement checking should just load the replacement chain DAG into memory - current := csv - for { - if in.Spec.Replaces == current.GetName() { - return nil - } - next := a.isBeingReplaced(current, csvsInNamespace) - if next != nil { - current = next - continue - } - if in.Name == csv.Name { - return nil - } - // couldn't find a chain between the two csvs - return fmt.Errorf("%s and %s both own %s, but there is no replacement chain linking them", in.Name, csv.Name, crd.Name) - } + owned = true + } + if owned && in.Spec.Replaces == csvName { + return nil } } } + if owned { + return ErrCRDOwnerConflict + } return nil } @@ -431,7 +401,7 @@ func (a *Operator) annotateNamespace(obj interface{}) (syncError error) { return nil } -func (a *Operator) isBeingReplaced(in *v1alpha1.ClusterServiceVersion, csvsInNamespace []*v1alpha1.ClusterServiceVersion) (replacedBy *v1alpha1.ClusterServiceVersion) { +func (a *Operator) isBeingReplaced(in *v1alpha1.ClusterServiceVersion, csvsInNamespace map[string]*v1alpha1.ClusterServiceVersion) (replacedBy *v1alpha1.ClusterServiceVersion) { for _, csv := range csvsInNamespace { if csv.Spec.Replaces == in.GetName() { replacedBy = csv diff --git a/pkg/controller/operators/olm/operator_test.go b/pkg/controller/operators/olm/operator_test.go index ec420261df..4629e864ea 100644 --- a/pkg/controller/operators/olm/operator_test.go +++ b/pkg/controller/operators/olm/operator_test.go @@ -2,161 +2,27 @@ package olm import ( "encoding/json" - "fmt" - "strings" "testing" + "time" - "github.com/golang/mock/gomock" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "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/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" k8sfake "k8s.io/client-go/kubernetes/fake" - "k8s.io/client-go/tools/cache" - "k8s.io/client-go/util/workqueue" "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/clientset/versioned/fake" - "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/annotator" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" - "github.com/operator-framework/operator-lifecycle-manager/pkg/fakes" - "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" opFake "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient/fake" - "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/queueinformer" "k8s.io/api/apps/v1beta2" "k8s.io/api/core/v1" ) -type MockALMOperator struct { - Operator - MockQueueOperator *queueinformer.MockOperator - ClientFake *fake.Clientset - MockOpClient *operatorclient.MockClientInterface - TestQueueInformer queueinformer.TestQueueInformer - StrategyResolverFake *fakes.FakeStrategyResolverInterface -} - -type Expect func() - -// Helpers - -func mockCRDExistence(mockClient operatorclient.MockClientInterface, crdDescriptions []v1alpha1.CRDDescription) { - for _, crd := range crdDescriptions { - if strings.HasPrefix(crd.Name, "nonExistent") { - mockClient.EXPECT().ApiextensionsV1beta1Interface().Return(apiextensionsfake.NewSimpleClientset()) - } - if strings.HasPrefix(crd.Name, "found") { - crd := v1beta1.CustomResourceDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Name: crd.Name, - }, - } - var objects []runtime.Object - objects = append(objects, &crd) - mockClient.EXPECT().ApiextensionsV1beta1Interface().Return(apiextensionsfake.NewSimpleClientset(objects...)) - } - } -} - -func mockIntermediates(t *testing.T, mockOpClient *operatorclient.MockClientInterface, resolverFake *fakes.FakeStrategyResolverInterface, current *v1alpha1.ClusterServiceVersion, intermediates []*v1alpha1.ClusterServiceVersion) Expect { - mockCSVsInNamespace(t, mockOpClient, current.GetNamespace(), intermediates, nil) - prevCSV := current - - expectFns := []func(){} - call := -2 - for i, csv := range intermediates { - call += 2 - mockIsReplacing(t, mockOpClient, prevCSV, csv, nil) - testInstallStrategy := TestStrategy{} - resolverFake.UnmarshalStrategyReturns(&testInstallStrategy, nil) - resolverFake.UnmarshalStrategyReturns(&testInstallStrategy, nil) - resolverFake.InstallerForStrategyReturns(NewTestInstaller(nil, nil)) - expectFns = append(expectFns, func() { - require.Equal(t, csv.Spec.InstallStrategy, resolverFake.UnmarshalStrategyArgsForCall(call)) - require.Equal(t, prevCSV.Spec.InstallStrategy, resolverFake.UnmarshalStrategyArgsForCall(call+1)) - name, opClient, _, strategy := resolverFake.InstallerForStrategyArgsForCall(i) - require.Equal(t, testInstallStrategy.GetStrategyName(), name) - require.Equal(t, mockOpClient, opClient) - require.Equal(t, &testInstallStrategy, strategy) - }) - prevCSV = csv - } - // Return a set of expectations that can be deferred until the test fn has finished - return func() { - for _, fn := range expectFns { - fn() - } - } -} - -func mockIsReplacing(t *testing.T, mockOpClient *operatorclient.MockClientInterface, prevCSV *v1alpha1.ClusterServiceVersion, currentCSV *v1alpha1.ClusterServiceVersion, csvQueryErr error) { - var unstructuredOldCSV *unstructured.Unstructured = nil - if prevCSV != nil { - unst, err := runtime.DefaultUnstructuredConverter.ToUnstructured(prevCSV) - require.NoError(t, err) - unstructuredOldCSV = &unstructured.Unstructured{Object: unst} - } else { - unstructuredOldCSV = nil - } - - if currentCSV.Spec.Replaces != "" { - mockOpClient.EXPECT().GetCustomResource(v1alpha1.GroupName, v1alpha1.GroupVersion, currentCSV.GetNamespace(), v1alpha1.ClusterServiceVersionKind, currentCSV.Spec.Replaces).Return(unstructuredOldCSV, csvQueryErr) - } -} - -func mockCSVsInNamespace(t *testing.T, mockOpClient *operatorclient.MockClientInterface, namespace string, csvsInNamespace []*v1alpha1.ClusterServiceVersion, csvQueryErr error) { - unstructuredCSVs := []*unstructured.Unstructured{} - for _, csv := range csvsInNamespace { - unst, err := runtime.DefaultUnstructuredConverter.ToUnstructured(csv) - require.NoError(t, err) - unstructuredCSVs = append(unstructuredCSVs, &unstructured.Unstructured{Object: unst}) - } - csvList := &operatorclient.CustomResourceList{Items: unstructuredCSVs} - - mockOpClient.EXPECT().ListCustomResource(v1alpha1.GroupName, v1alpha1.GroupVersion, namespace, v1alpha1.ClusterServiceVersionKind).Return(csvList, csvQueryErr) -} - -func mockInstallStrategy(t *testing.T, resolverFake *fakes.FakeStrategyResolverInterface, strategy *v1alpha1.NamedInstallStrategy, installErr error, checkInstallErr error, prevStrategy *v1alpha1.NamedInstallStrategy, prevCSVQueryErr error) Expect { - testInstallStrategy := TestStrategy{} - expectFns := []func(){} - stratErr := fmt.Errorf("couldn't unmarshal install strategy") - if strategy.StrategyName == "teststrategy" { - stratErr = nil - } - resolverFake.UnmarshalStrategyReturns(&testInstallStrategy, stratErr) - expectFns = append(expectFns, func() { - strat := resolverFake.UnmarshalStrategyArgsForCall(0) - require.Equal(t, strategy, strat) - }) - if stratErr == nil { - resolverFake.InstallerForStrategyReturns(NewTestInstaller(installErr, checkInstallErr)) - expectFns = append(expectFns, func() { - strategyName, _, _, prev := resolverFake.InstallerForStrategyArgsForCall(0) - require.Equal(t, (&testInstallStrategy).GetStrategyName(), strategyName) - if prevStrategy != nil { - require.NotNil(t, prev) - } - }) - } - if prevStrategy != nil { - resolverFake.UnmarshalStrategyReturns(&testInstallStrategy, prevCSVQueryErr) - expectFns = append(expectFns, func() { - strat := resolverFake.UnmarshalStrategyArgsForCall(1) - require.Equal(t, prevStrategy, strat) - }) - } - // Return a set of expectations that can be deferred until the test fn has finished - return func() { - for _, fn := range expectFns { - fn() - } - } -} - // Fakes type TestStrategy struct{} @@ -188,1546 +54,23 @@ func (i *TestInstaller) CheckInstalled(s install.Strategy) (bool, error) { return true, nil } -func testCSV(name string) *v1alpha1.ClusterServiceVersion { - if name == "" { - name = "test-csv" - } - return &v1alpha1.ClusterServiceVersion{ - TypeMeta: metav1.TypeMeta{ - Kind: v1alpha1.ClusterServiceVersionKind, - APIVersion: v1alpha1.ClusterServiceVersionAPIVersion, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - SelfLink: "/link/" + name, - }, - Spec: v1alpha1.ClusterServiceVersionSpec{ - DisplayName: name, - }, - } -} - -func makeCRDDescriptions(names ...string) []v1alpha1.CRDDescription { - crds := []v1alpha1.CRDDescription{} - for _, name := range names { - crds = append(crds, v1alpha1.CRDDescription{ - Name: name, - }) +func NewFakeOperator(clientObjs []runtime.Object, k8sObjs []runtime.Object, extObjs []runtime.Object, resolver install.StrategyResolverInterface, namespace string) (*Operator, error) { + clientFake := fake.NewSimpleClientset(clientObjs...) + opClientFake := opFake.NewClient(k8sfake.NewSimpleClientset(k8sObjs...), apiextensionsfake.NewSimpleClientset(extObjs...)) + 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 } - return crds -} - -func withStatus(csv *v1alpha1.ClusterServiceVersion, status *v1alpha1.ClusterServiceVersionStatus) *v1alpha1.ClusterServiceVersion { - status.DeepCopyInto(&csv.Status) - return csv + return NewOperator(clientFake, opClientFake, resolver, 5*time.Second, annotations, []string{namespace}) } -func withSpec(csv *v1alpha1.ClusterServiceVersion, spec *v1alpha1.ClusterServiceVersionSpec) *v1alpha1.ClusterServiceVersion { - spec.DeepCopyInto(&csv.Spec) - return csv -} - -func withReplaces(csv *v1alpha1.ClusterServiceVersion, replaces string) *v1alpha1.ClusterServiceVersion { - csv.Spec.Replaces = replaces - return csv -} - -func NewMockALMOperator(gomockCtrl *gomock.Controller) *MockALMOperator { - clientFake := fake.NewSimpleClientset() - resolverFake := new(fakes.FakeStrategyResolverInterface) - - almOperator := Operator{ - client: clientFake, - resolver: resolverFake, - } - queue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "test-clusterserviceversions") - csvQueueInformer := queueinformer.NewTestQueueInformer( - queue, - cache.NewSharedIndexInformer(&queueinformer.MockListWatcher{}, &v1alpha1.ClusterServiceVersion{}, 0, nil), - almOperator.syncClusterServiceVersion, - nil, - ) - - qOp := queueinformer.NewMockOperator(gomockCtrl, csvQueueInformer) - almOperator.Operator = &qOp.Operator - almOperator.annotator = annotator.NewAnnotator(qOp.OpClient, map[string]string{}) - almOperator.csvQueue = queue - return &MockALMOperator{ - Operator: almOperator, - ClientFake: clientFake, - MockQueueOperator: qOp, - MockOpClient: qOp.MockClient, - TestQueueInformer: *csvQueueInformer, - StrategyResolverFake: resolverFake, - } +func (o *Operator) GetClient() versioned.Interface { + return o.client } // Tests -func TestCSVStateTransitionsFromNone(t *testing.T) { - tests := []struct { - in *v1alpha1.ClusterServiceVersion - out *v1alpha1.ClusterServiceVersion - err error - description string - }{ - { - in: testCSV(""), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - Message: "requirements not yet checked", - Reason: v1alpha1.CSVReasonRequirementsUnknown, - }), - description: "ToRequirementsUnknown", - }, - } - - for _, tt := range tests { - ctrl := gomock.NewController(t) - mockOp := NewMockALMOperator(ctrl) - - mockOp.MockOpClient.EXPECT().ListCustomResource(v1alpha1.GroupName, v1alpha1.GroupVersion, tt.in.GetNamespace(), v1alpha1.ClusterServiceVersionKind).Return(&operatorclient.CustomResourceList{}, nil) - - // Test the transition - t.Run(tt.description, func(t *testing.T) { - out, err := mockOp.transitionCSVState(*tt.in) - t.Log(out) - require.EqualValues(t, tt.err, err) - require.EqualValues(t, tt.out.Status.Phase, out.Status.Phase) - require.EqualValues(t, tt.out.Status.Message, out.Status.Message) - require.EqualValues(t, tt.out.Status.Reason, out.Status.Reason) - }) - ctrl.Finish() - } -} - -func TestCSVStateTransitionsFromPending(t *testing.T) { - type clusterState struct { - csvs []*v1alpha1.ClusterServiceVersion - } - tests := []struct { - in *v1alpha1.ClusterServiceVersion - out *v1alpha1.ClusterServiceVersion - state *clusterState - err error - description string - }{ - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("nonExistent"), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - Message: "one or more requirements couldn't be found", - Reason: v1alpha1.CSVReasonRequirementsNotMet, - }), - description: "RequirementsNotMet/OwnedMissing", - err: ErrRequirementsNotMet, - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Required: makeCRDDescriptions("nonExistent"), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - Message: "one or more requirements couldn't be found", - Reason: v1alpha1.CSVReasonRequirementsNotMet, - }), - description: "RequirementsNotMet/RequiredMissing", - err: ErrRequirementsNotMet, - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("nonExistent1", "found1"), - Required: makeCRDDescriptions("nonExistent2", "found2"), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - Message: "one or more requirements couldn't be found", - Reason: v1alpha1.CSVReasonRequirementsNotMet, - }), - description: "RequirementsNotMet/OwnedAndRequiredMissingWithFound", - err: ErrRequirementsNotMet, - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found"), - Required: makeCRDDescriptions("nonExistent"), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - Message: "one or more requirements couldn't be found", - Reason: v1alpha1.CSVReasonRequirementsNotMet, - }), - description: "RequirementsNotMet/OwnedFoundRequiredMissing", - err: ErrRequirementsNotMet, - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("nonExistent"), - Required: makeCRDDescriptions("found"), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - Message: "one or more requirements couldn't be found", - Reason: v1alpha1.CSVReasonRequirementsNotMet, - }), - description: "RequirementsNotMet/OwnedMissingRequiredFound", - err: ErrRequirementsNotMet, - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found1", "found2"), - Required: makeCRDDescriptions("found3", "found4"), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstallReady, - Message: "all requirements found, attempting install", - Reason: v1alpha1.CSVReasonRequirementsMet, - }), - state: &clusterState{ - csvs: []*v1alpha1.ClusterServiceVersion{}, - }, - description: "RequirementsMet/OwnedAndRequiredFound", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found"), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstallReady, - Message: "all requirements found, attempting install", - Reason: v1alpha1.CSVReasonRequirementsMet, - }), - state: &clusterState{ - csvs: []*v1alpha1.ClusterServiceVersion{}, - }, - description: "RequirementsMet/OwnedFound", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Required: makeCRDDescriptions("found"), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstallReady, - Message: "all requirements found, attempting install", - Reason: v1alpha1.CSVReasonRequirementsMet, - }), - state: &clusterState{ - csvs: []*v1alpha1.ClusterServiceVersion{}, - }, - description: "RequirementsMet/RequiredFound", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found1", "found2"), - Required: makeCRDDescriptions("found3", "found4"), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseFailed, - Message: "owner conflict: test-csv and existing-owner both own found1, but there is no replacement chain linking them", - Reason: v1alpha1.CSVReasonOwnerConflict, - }), - state: &clusterState{ - csvs: []*v1alpha1.ClusterServiceVersion{withSpec(testCSV("existing-owner"), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found1"), - }, - })}, - }, - description: "RequirementsMet/OwnedAndRequiredFound/CRDAlreadyOwnedNoReplacementChain", - err: fmt.Errorf("test-csv and existing-owner both own found1, but there is no replacement chain linking them"), - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found1", "found2"), - Required: makeCRDDescriptions("found3", "found4"), - }, - Replaces: "existing-owner-2", - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstallReady, - Message: "all requirements found, attempting install", - Reason: v1alpha1.CSVReasonRequirementsMet, - }), - state: &clusterState{ - csvs: []*v1alpha1.ClusterServiceVersion{ - withSpec(testCSV("existing-owner-1"), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found1"), - }, - }), - withSpec(testCSV("existing-owner-2"), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found1", "found2"), - }, - Replaces: "existing-owner-1", - }), - }, - }, - description: "RequirementsMet/OwnedAndRequiredFound/CRDOwnedInReplacementChain", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found1", "found2"), - Required: makeCRDDescriptions("found3", "found4"), - }, - Replaces: "existing-owner-2", - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstallReady, - Message: "all requirements found, attempting install", - Reason: v1alpha1.CSVReasonRequirementsMet, - }), - state: &clusterState{ - csvs: []*v1alpha1.ClusterServiceVersion{ - withSpec(testCSV("existing-owner-1"), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found1"), - }, - Replaces: "existing-owner-3", - }), - withSpec(testCSV("existing-owner-2"), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found1", "found2"), - }, - Replaces: "existing-owner-1", - }), - withSpec(testCSV("existing-owner-3"), - &v1alpha1.ClusterServiceVersionSpec{ - CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ - Owned: makeCRDDescriptions("found1", "found2"), - }, - Replaces: "existing-owner-2", - }), - }, - }, - description: "RequirementsMet/OwnedAndRequiredFound/CRDOwnedInReplacementChainLoop", - }, - } - - for _, tt := range tests { - // Test the transition - t.Run(tt.description, func(t *testing.T) { - ctrl := gomock.NewController(t) - mockOp := NewMockALMOperator(ctrl) - - mockCRDExistence(*mockOp.MockQueueOperator.MockClient, tt.in.Spec.CustomResourceDefinitions.Owned) - mockCRDExistence(*mockOp.MockQueueOperator.MockClient, tt.in.Spec.CustomResourceDefinitions.Required) - - // mock for call to short-circuit when replacing - mockCSVsInNamespace(t, mockOp.MockQueueOperator.MockClient, tt.in.Namespace, nil, nil) - - // mock for pending, check that no other CSV owns the CRDs (unless being replaced) - if tt.state != nil { - mockCSVsInNamespace(t, mockOp.MockQueueOperator.MockClient, tt.in.Namespace, tt.state.csvs, nil) - } - - out, err := mockOp.transitionCSVState(*tt.in) - require.EqualValues(t, tt.err, err) - require.EqualValues(t, tt.out.Status.Phase, out.Status.Phase) - require.EqualValues(t, tt.out.Status.Message, out.Status.Message) - require.EqualValues(t, tt.out.Status.Reason, out.Status.Reason) - ctrl.Finish() - }) - } -} - -func TestCSVStateTransitionsFromInstallReady(t *testing.T) { - type clusterState struct { - csvsInNamespace []*v1alpha1.ClusterServiceVersion - csvQueryErr error - prevCSV *v1alpha1.ClusterServiceVersion - prevCSVQueryErr error - installErr error - checkInstallErr error - } - tests := []struct { - in *v1alpha1.ClusterServiceVersion - state clusterState - out *v1alpha1.ClusterServiceVersion - err error - description string - }{ - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "bad", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstallReady, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "bad", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseFailed, - Message: "install strategy invalid: couldn't unmarshal install strategy", - Reason: v1alpha1.CSVReasonInvalidStrategy, - }), - description: "InvalidInstallStrategy", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstallReady, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - Message: "waiting for install components to report healthy", - Reason: v1alpha1.CSVReasonInstallSuccessful, - }), - description: "InstallStrategy/NotReplacing/Installing", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstallReady, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseFailed, - Message: "install strategy failed: error installing component", - Reason: v1alpha1.CSVReasonComponentFailed, - }), - state: clusterState{ - installErr: fmt.Errorf("error installing component"), - }, - err: fmt.Errorf("error installing component"), - description: "InstallStrategy/NotReplacing/ComponentFailed", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - Replaces: "prev", - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstallReady, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - Message: "waiting for install components to report healthy", - Reason: v1alpha1.CSVReasonInstallSuccessful, - }), - state: clusterState{ - prevCSV: withStatus(withSpec(testCSV("prev"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - }), - }, - description: "InstallStrategy/Replacing/Installing", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - Replaces: "prev", - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstallReady, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - Message: "waiting for install components to report healthy", - Reason: v1alpha1.CSVReasonInstallSuccessful, - }), - state: clusterState{ - prevCSVQueryErr: fmt.Errorf("error getting prev csv"), - }, - description: "InstallStrategy/Replacing/PrevCSVErr/Installing", - }, - } - - for _, tt := range tests { - ctrl := gomock.NewController(t) - mockOp := NewMockALMOperator(ctrl) - - var prevStrategy *v1alpha1.NamedInstallStrategy - if tt.state.prevCSV != nil { - prevStrategy = &tt.state.prevCSV.Spec.InstallStrategy - } - - mockCSVsInNamespace(t, mockOp.MockOpClient, tt.in.GetNamespace(), tt.state.csvsInNamespace, tt.state.csvQueryErr) - mockInstallStrategy(t, mockOp.StrategyResolverFake, &tt.in.Spec.InstallStrategy, tt.state.installErr, tt.state.checkInstallErr, prevStrategy, tt.state.prevCSVQueryErr) - mockIsReplacing(t, mockOp.MockOpClient, tt.state.prevCSV, tt.in, tt.state.prevCSVQueryErr) - - t.Run(tt.description, func(t *testing.T) { - out, err := mockOp.transitionCSVState(*tt.in) - require.EqualValues(t, tt.err, err) - require.EqualValues(t, tt.out.Status.Phase, out.Status.Phase) - require.EqualValues(t, tt.out.Status.Message, out.Status.Message) - require.EqualValues(t, tt.out.Status.Reason, out.Status.Reason) - }) - ctrl.Finish() - } -} - -func TestCSVStateTransitionsFromInstalling(t *testing.T) { - type clusterState struct { - csvsInNamespace []*v1alpha1.ClusterServiceVersion - csvQueryErr error - prevCSV *v1alpha1.ClusterServiceVersion - prevCSVQueryErr error - installErr error - checkInstallErr error - } - tests := []struct { - in *v1alpha1.ClusterServiceVersion - state clusterState - out *v1alpha1.ClusterServiceVersion - err error - description string - }{ - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "bad", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "bad", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseFailed, - Message: "install strategy invalid: couldn't unmarshal install strategy", - Reason: v1alpha1.CSVReasonInvalidStrategy, - }), - description: "InvalidInstallStrategy", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - Message: "install strategy completed with no errors", - Reason: v1alpha1.CSVReasonInstallSuccessful, - }), - description: "InstallStrategy/NotReplacing/Installing", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - Message: "installing: error installing component", - Reason: v1alpha1.CSVReasonWaiting, - }), - state: clusterState{ - checkInstallErr: fmt.Errorf("error installing component"), - }, - description: "InstallStrategy/NotReplacing/WaitingForInstall", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseFailed, - Message: "install failed: Timeout: timeout error", - Reason: v1alpha1.CSVReasonInstallCheckFailed, - }), - state: clusterState{ - checkInstallErr: &install.StrategyError{Reason: install.StrategyErrReasonTimeout, Message: "timeout error"}, - }, - description: "InstallStrategy/NotReplacing/UnrecoverableError", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - Replaces: "prev", - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - Message: "install strategy completed with no errors", - Reason: v1alpha1.CSVReasonInstallSuccessful, - }), - state: clusterState{ - prevCSV: withStatus(withSpec(testCSV("prev"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - }), - }, - description: "InstallStrategy/Replacing/Installing", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - Replaces: "prev", - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - Message: "installing: error installing component", - Reason: v1alpha1.CSVReasonWaiting, - }), - state: clusterState{ - prevCSV: withStatus(withSpec(testCSV("prev"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - }), - checkInstallErr: fmt.Errorf("error installing component"), - }, - description: "InstallStrategy/Replacing/WaitingForInstall", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - Replaces: "prev", - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseFailed, - Message: "install failed: Timeout: timeout error", - Reason: v1alpha1.CSVReasonInstallCheckFailed, - }), - state: clusterState{ - prevCSV: withStatus(withSpec(testCSV("prev"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - }), - checkInstallErr: &install.StrategyError{Reason: install.StrategyErrReasonTimeout, Message: "timeout error"}, - }, - description: "InstallStrategy/Replacing/UnrecoverableError", - }, - } - - for _, tt := range tests { - ctrl := gomock.NewController(t) - mockOp := NewMockALMOperator(ctrl) - - var prevStrategy *v1alpha1.NamedInstallStrategy - if tt.state.prevCSV != nil { - prevStrategy = &tt.state.prevCSV.Spec.InstallStrategy - } - - mockCSVsInNamespace(t, mockOp.MockOpClient, tt.in.GetNamespace(), tt.state.csvsInNamespace, tt.state.csvQueryErr) - mockInstallStrategy(t, mockOp.StrategyResolverFake, &tt.in.Spec.InstallStrategy, tt.state.installErr, tt.state.checkInstallErr, prevStrategy, tt.state.prevCSVQueryErr) - mockIsReplacing(t, mockOp.MockOpClient, tt.state.prevCSV, tt.in, tt.state.prevCSVQueryErr) - - t.Run(tt.description, func(t *testing.T) { - out, err := mockOp.transitionCSVState(*tt.in) - require.EqualValues(t, tt.err, err) - require.EqualValues(t, tt.out.Status.Phase, out.Status.Phase) - require.EqualValues(t, tt.out.Status.Message, out.Status.Message) - require.EqualValues(t, tt.out.Status.Reason, out.Status.Reason) - }) - ctrl.Finish() - } -} - -func TestCSVStateTransitionsFromSucceeded(t *testing.T) { - type clusterState struct { - csvsInNamespace []*v1alpha1.ClusterServiceVersion - csvQueryErr error - prevCSV *v1alpha1.ClusterServiceVersion - prevCSVQueryErr error - installErr error - checkInstallErr error - } - tests := []struct { - in *v1alpha1.ClusterServiceVersion - state clusterState - out *v1alpha1.ClusterServiceVersion - err error - description string - }{ - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "bad", - StrategySpecRaw: []byte(`"test":"spec"`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "bad", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseFailed, - Message: "install strategy invalid: couldn't unmarshal install strategy", - Reason: v1alpha1.CSVReasonInvalidStrategy, - }), - description: "InvalidInstallStrategy", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - Message: "install strategy completed with no errors", - Reason: v1alpha1.CSVReasonInstallSuccessful, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - Message: "install strategy completed with no errors", - Reason: v1alpha1.CSVReasonInstallSuccessful, - }), - description: "InstallStrategy/LookingGood", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - Reason: v1alpha1.CSVReasonInstallSuccessful, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - Message: "installing: error installing component", - Reason: v1alpha1.CSVReasonComponentUnhealthy, - }), - state: clusterState{ - checkInstallErr: fmt.Errorf("error installing component"), - }, - description: "InstallStrategy/ComponentWentUnhealthy", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - Reason: v1alpha1.CSVReasonInstallSuccessful, - }), - out: withStatus(testCSV(""), &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseFailed, - Message: "install failed: Timeout: timeout error", - Reason: v1alpha1.CSVReasonInstallCheckFailed, - }), - state: clusterState{ - checkInstallErr: &install.StrategyError{Reason: install.StrategyErrReasonTimeout, Message: "timeout error"}, - }, - description: "InstallStrategy/ComponentWentUnrecoverable", - }, - } - - for _, tt := range tests { - ctrl := gomock.NewController(t) - mockOp := NewMockALMOperator(ctrl) - - var prevStrategy *v1alpha1.NamedInstallStrategy - if tt.state.prevCSV != nil { - prevStrategy = &tt.state.prevCSV.Spec.InstallStrategy - } - - mockCSVsInNamespace(t, mockOp.MockOpClient, tt.in.GetNamespace(), tt.state.csvsInNamespace, tt.state.csvQueryErr) - mockInstallStrategy(t, mockOp.StrategyResolverFake, &tt.in.Spec.InstallStrategy, tt.state.installErr, tt.state.checkInstallErr, prevStrategy, tt.state.prevCSVQueryErr) - mockIsReplacing(t, mockOp.MockOpClient, tt.state.prevCSV, tt.in, tt.state.prevCSVQueryErr) - - t.Run(tt.description, func(t *testing.T) { - out, err := mockOp.transitionCSVState(*tt.in) - require.EqualValues(t, tt.err, err) - require.EqualValues(t, tt.out.Status.Phase, out.Status.Phase) - require.EqualValues(t, tt.out.Status.Message, out.Status.Message) - require.EqualValues(t, tt.out.Status.Reason, out.Status.Reason) - }) - ctrl.Finish() - } -} - -func TestCSVStateTransitionsFromReplacing(t *testing.T) { - type clusterState struct { - csvsInNamespace []*v1alpha1.ClusterServiceVersion - csvQueryErr error - prevCSV *v1alpha1.ClusterServiceVersion - prevCSVQueryErr error - installErr error - checkInstallErr error - } - tests := []struct { - in *v1alpha1.ClusterServiceVersion - state clusterState - out *v1alpha1.ClusterServiceVersion - err error - description string - }{ - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - Replaces: "prev", - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - }), - state: clusterState{ - prevCSV: withStatus(withSpec(testCSV("prev"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - }), - }, - description: "NotALeaf", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - }), - description: "Leaf/NoNewCSV", - }, - { - in: withStatus(withSpec(testCSV("current"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - }), - out: withStatus(withSpec(testCSV("current"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseDeleting, - Message: "has been replaced by a newer ClusterServiceVersion that has successfully installed.", - Reason: v1alpha1.CSVReasonReplaced, - }), - state: clusterState{ - csvsInNamespace: []*v1alpha1.ClusterServiceVersion{ - withStatus(withSpec(testCSV("next"), - &v1alpha1.ClusterServiceVersionSpec{ - Replaces: "current", - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - Reason: v1alpha1.CSVReasonInstallSuccessful, - }), - }, - }, - description: "Leaf/NewCSVRunning/GCSelf", - }, - { - in: withStatus(withSpec(testCSV("current"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - }), - out: withStatus(withSpec(testCSV("current"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - }), - state: clusterState{ - csvsInNamespace: []*v1alpha1.ClusterServiceVersion{ - withStatus(withSpec(testCSV("next"), - &v1alpha1.ClusterServiceVersionSpec{ - Replaces: "current", - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - Conditions: []v1alpha1.ClusterServiceVersionCondition{ - { - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - }, - }, - }), - }, - }, - description: "Leaf/NewCSV/NotRunning", - }, - { - in: withStatus(withSpec(testCSV("1"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - }), - out: withStatus(withSpec(testCSV("current"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "1", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseDeleting, - Message: "has been replaced by a newer ClusterServiceVersion that has successfully installed.", - Reason: v1alpha1.CSVReasonReplaced, - }), - state: clusterState{ - csvsInNamespace: []*v1alpha1.ClusterServiceVersion{ - withStatus(withSpec(testCSV("3"), - &v1alpha1.ClusterServiceVersionSpec{ - Replaces: "2", - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - Reason: v1alpha1.CSVReasonInstallSuccessful, - }), - withStatus(withSpec(testCSV("2"), - &v1alpha1.ClusterServiceVersionSpec{ - Replaces: "1", - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - Conditions: []v1alpha1.ClusterServiceVersionCondition{ - { - Phase: v1alpha1.CSVPhaseReplacing, - Reason: v1alpha1.CSVReasonBeingReplaced, - }, - }, - }), - }, - }, - description: "Leaf/ManyNewCSVRunning/GCSet", - }, - } - - for _, tt := range tests { - ctrl := gomock.NewController(t) - mockOp := NewMockALMOperator(ctrl) - - mockIsReplacing(t, mockOp.MockOpClient, tt.state.prevCSV, tt.in, tt.state.prevCSVQueryErr) - - // transition short circuits if there's a prevCSV, so we only mock the rest if there isn't - if tt.state.prevCSV == nil { - intermediateExpect := mockIntermediates(t, mockOp.MockOpClient, mockOp.StrategyResolverFake, tt.in, tt.state.csvsInNamespace) - defer intermediateExpect() - } - - t.Run(tt.description, func(t *testing.T) { - out, err := mockOp.transitionCSVState(*tt.in) - require.EqualValues(t, tt.err, err) - require.EqualValues(t, tt.out.Status.Phase, out.Status.Phase) - require.EqualValues(t, tt.out.Status.Message, out.Status.Message) - require.EqualValues(t, tt.out.Status.Reason, out.Status.Reason) - }) - ctrl.Finish() - } -} - -func TestCSVStateTransitionsFromDeleting(t *testing.T) { - type clusterState struct { - deleteErr error - } - tests := []struct { - in *v1alpha1.ClusterServiceVersion - state clusterState - out *v1alpha1.ClusterServiceVersion - err error - description string - }{ - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseDeleting, - Reason: v1alpha1.CSVReasonReplaced, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseDeleting, - Reason: v1alpha1.CSVReasonReplaced, - }), - description: "DeleteSuccessful", - }, - { - in: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseDeleting, - Reason: v1alpha1.CSVReasonReplaced, - }), - out: withStatus(withSpec(testCSV(""), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseDeleting, - Reason: v1alpha1.CSVReasonReplaced, - }), - state: clusterState{ - deleteErr: fmt.Errorf("couldn't delete"), - }, - description: "DeleteUnsuccessful", - }, - } - - for _, tt := range tests { - ctrl := gomock.NewController(t) - mockOp := NewMockALMOperator(ctrl) - - mockOp.MockOpClient.EXPECT(). - DeleteCustomResource(v1alpha1.GroupName, v1alpha1.GroupVersion, tt.in.GetNamespace(), v1alpha1.ClusterServiceVersionKind, tt.in.GetName()). - Return(tt.state.deleteErr) - - t.Run(tt.description, func(t *testing.T) { - out, err := mockOp.transitionCSVState(*tt.in) - require.EqualValues(t, tt.err, err) - require.EqualValues(t, tt.out.Status.Phase, out.Status.Phase) - require.EqualValues(t, tt.out.Status.Message, out.Status.Message) - require.EqualValues(t, tt.out.Status.Reason, out.Status.Reason) - }) - ctrl.Finish() - } -} - -func TestReplacingCSV(t *testing.T) { - type clusterState struct { - newerCSV *v1alpha1.ClusterServiceVersion - csvQueryErr error - } - - newCSV := withStatus(withSpec(testCSV("new"), - &v1alpha1.ClusterServiceVersionSpec{ - Replaces: "old", - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - }) - - beingReplacedStatus := &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseReplacing, - Message: "being replaced by csv: /link/new", - Reason: v1alpha1.CSVReasonBeingReplaced, - } - - tests := []struct { - in *v1alpha1.ClusterServiceVersion - state clusterState - out *v1alpha1.ClusterServiceVersion - err error - description string - }{ - { - in: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseSucceeded, - }), - out: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), beingReplacedStatus), - state: clusterState{ - newerCSV: newCSV, - }, - err: fmt.Errorf("replacing"), - description: "FromSucceeded", - }, - { - in: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstalling, - }), - out: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), beingReplacedStatus), - state: clusterState{ - newerCSV: newCSV, - }, - err: fmt.Errorf("replacing"), - description: "FromInstalling", - }, - { - in: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhasePending, - }), - out: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), beingReplacedStatus), - state: clusterState{ - newerCSV: newCSV, - }, - err: fmt.Errorf("replacing"), - description: "FromPending", - }, - { - in: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseFailed, - }), - out: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), beingReplacedStatus), - state: clusterState{ - newerCSV: newCSV, - }, - err: fmt.Errorf("replacing"), - description: "FromFailed", - }, - { - in: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseInstallReady, - }), - out: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), beingReplacedStatus), - state: clusterState{ - newerCSV: newCSV, - }, - err: fmt.Errorf("replacing"), - description: "FromInstallReady", - }, - { - in: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseNone, - }), - out: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), beingReplacedStatus), - state: clusterState{ - newerCSV: newCSV, - }, - err: fmt.Errorf("replacing"), - description: "FromNone", - }, - { - in: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), - &v1alpha1.ClusterServiceVersionStatus{ - Phase: v1alpha1.CSVPhaseUnknown, - }), - out: withStatus(withSpec(testCSV("old"), - &v1alpha1.ClusterServiceVersionSpec{ - InstallStrategy: v1alpha1.NamedInstallStrategy{ - StrategyName: "teststrategy", - StrategySpecRaw: []byte(`{"test":"spec"}`), - }, - }), beingReplacedStatus), - state: clusterState{ - newerCSV: newCSV, - }, - err: fmt.Errorf("replacing"), - description: "FromUnknown", - }, - } - - for _, tt := range tests { - ctrl := gomock.NewController(t) - mockOp := NewMockALMOperator(ctrl) - - csvsInNamespace := []*v1alpha1.ClusterServiceVersion{tt.state.newerCSV} - mockCSVsInNamespace(t, mockOp.MockOpClient, tt.in.GetNamespace(), csvsInNamespace, tt.state.csvQueryErr) - - t.Run(tt.description, func(t *testing.T) { - err := mockOp.checkReplacementsAndUpdateStatus(tt.in) - require.EqualValues(t, tt.err, err) - require.EqualValues(t, tt.out.Status.Phase, tt.in.Status.Phase) - require.EqualValues(t, tt.out.Status.Message, tt.in.Status.Message) - require.EqualValues(t, tt.out.Status.Reason, tt.in.Status.Reason) - }) - ctrl.Finish() - } -} - func deployment(deploymentName, namespace string) *v1beta2.Deployment { var singleInstance = int32(1) return &v1beta2.Deployment{ @@ -1875,7 +218,7 @@ func crd(name string, version string) *v1beta1.CustomResourceDefinition { } } -func TestTransitionCSVHappyPath(t *testing.T) { +func TestTransitionCSV(t *testing.T) { log.SetLevel(log.DebugLevel) namespace := "ns" @@ -1890,6 +233,7 @@ func TestTransitionCSVHappyPath(t *testing.T) { } type expected struct { csvStates map[string]csvState + err map[string]error } tests := []struct { name string @@ -1916,6 +260,68 @@ func TestTransitionCSVHappyPath(t *testing.T) { }, }, }, + { + name: "SingleCSVPendingToPending", + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", + namespace, + "", + installStrategy("csv1-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhasePending, + ), + }, + crds: []runtime.Object{}, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: true, phase: v1alpha1.CSVPhasePending}, + }, + err: map[string]error{ + "csv1": ErrRequirementsNotMet, + }, + }, + }, + { + name: "CSVPendingToFailed/OwnerConflict", + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", + namespace, + "", + installStrategy("csv1-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseSucceeded, + ), + csv("csv2", + namespace, + "", + installStrategy("csv2-dep1"), + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhasePending, + ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + objs: []runtime.Object{ + deployment("csv1-dep1", namespace), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: true, phase: v1alpha1.CSVPhaseSucceeded}, + "csv2": {exists: true, phase: v1alpha1.CSVPhaseFailed}, + }, + err: map[string]error{ + "csv2": ErrCRDOwnerConflict, + }, + }, + }, { name: "SingleCSVPendingToInstallReady", initial: initial{ @@ -1962,6 +368,30 @@ func TestTransitionCSVHappyPath(t *testing.T) { }, }, }, + { + name: "SingleCSVInstallReadyToFailed/BadStrategy", + initial: initial{ + csvs: []runtime.Object{ + csv("csv1", + namespace, + "", + v1alpha1.NamedInstallStrategy{"deployment", json.RawMessage{}}, + []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, + []*v1beta1.CustomResourceDefinition{}, + v1alpha1.CSVPhaseInstallReady, + ), + }, + crds: []runtime.Object{ + crd("c1", "v1"), + }, + }, + expected: expected{ + csvStates: map[string]csvState{ + "csv1": {exists: true, phase: v1alpha1.CSVPhaseFailed}, + }, + }, + }, + { name: "SingleCSVInstallingToSucceeded", initial: initial{ @@ -2265,29 +695,19 @@ func TestTransitionCSVHappyPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // configure cluster state - clientFake := fake.NewSimpleClientset(tt.initial.csvs...) - - opClientFake := opFake.NewClient( - k8sfake.NewSimpleClientset(tt.initial.objs...), - apiextensionsfake.NewSimpleClientset(tt.initial.crds...)) - - op := &Operator{ - Operator: &queueinformer.Operator{ - OpClient: opClientFake, - }, - client: clientFake, - resolver: &install.StrategyResolver{}, - } + op, err := NewFakeOperator(tt.initial.csvs, tt.initial.objs, tt.initial.crds, &install.StrategyResolver{}, namespace) + require.NoError(t, err) // run csv sync for each CSV for _, csv := range tt.initial.csvs { err := op.syncClusterServiceVersion(csv) - require.NoError(t, err) + expectedErr := tt.expected.err[csv.(*v1alpha1.ClusterServiceVersion).Name] + require.Equal(t, expectedErr, err) } // get csvs in the cluster outCSVMap := map[string]*v1alpha1.ClusterServiceVersion{} - outCSVs, err := clientFake.OperatorsV1alpha1().ClusterServiceVersions("ns").List(metav1.ListOptions{}) + outCSVs, err := op.GetClient().OperatorsV1alpha1().ClusterServiceVersions("ns").List(metav1.ListOptions{}) require.NoError(t, err) for _, csv := range outCSVs.Items { outCSVMap[csv.GetName()] = csv.DeepCopy() @@ -2375,7 +795,55 @@ func TestIsBeingReplaced(t *testing.T) { namespace := "ns" type initial struct { - csvs []*v1alpha1.ClusterServiceVersion + csvs map[string]*v1alpha1.ClusterServiceVersion + } + tests := []struct { + name string + initial initial + in *v1alpha1.ClusterServiceVersion + expected *v1alpha1.ClusterServiceVersion + }{ + { + name: "QueryErr", + in: csv("name", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + expected: nil, + }, + { + name: "CSVInCluster/NotReplacing", + in: csv("csv1", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + initial: initial{ + csvs: map[string]*v1alpha1.ClusterServiceVersion{ + "csv2": csv("csv2", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + }, + }, + expected: nil, + }, + { + name: "CSVInCluster/Replacing", + in: csv("csv1", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + initial: initial{ + csvs: map[string]*v1alpha1.ClusterServiceVersion{ + "csv2": csv("csv2", namespace, "csv1", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + }, + }, + expected: csv("csv2", namespace, "csv1", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // configure cluster state + op := &Operator{} + + require.Equal(t, tt.expected, op.isBeingReplaced(tt.in, tt.initial.csvs)) + }) + } +} + +func TestCheckReplacement(t *testing.T) { + namespace := "ns" + + type initial struct { + csvs map[string]*v1alpha1.ClusterServiceVersion } tests := []struct { name string @@ -2392,8 +860,8 @@ func TestIsBeingReplaced(t *testing.T) { name: "CSVInCluster/NotReplacing", in: csv("csv1", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), initial: initial{ - csvs: []*v1alpha1.ClusterServiceVersion{ - csv("csv2", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + csvs: map[string]*v1alpha1.ClusterServiceVersion{ + "csv2": csv("csv2", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), }, }, expected: nil, @@ -2402,8 +870,8 @@ func TestIsBeingReplaced(t *testing.T) { name: "CSVInCluster/Replacing", in: csv("csv1", namespace, "", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), initial: initial{ - csvs: []*v1alpha1.ClusterServiceVersion{ - csv("csv2", namespace, "csv1", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), + csvs: map[string]*v1alpha1.ClusterServiceVersion{ + "csv2": csv("csv2", namespace, "csv1", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), }, }, expected: csv("csv2", namespace, "csv1", installStrategy("dep"), nil, nil, v1alpha1.CSVPhaseSucceeded), diff --git a/pkg/controller/registry/directory_loader_test.go b/pkg/controller/registry/directory_loader_test.go index 6fd90563c1..2716376ed1 100644 --- a/pkg/controller/registry/directory_loader_test.go +++ b/pkg/controller/registry/directory_loader_test.go @@ -18,7 +18,7 @@ func TestDirectoryLoader(t *testing.T) { require.Contains(t, catalog.packages, "etcd") require.Contains(t, catalog.packages, "vault") require.Contains(t, catalog.packages, "prometheus") - require.Len(t, catalog.packages, 3) + require.Len(t, catalog.packages, 2) } func TestDirectoryLoaderHiddenDirs(t *testing.T) { diff --git a/pkg/lib/queueinformer/mock_queueinformer_operator.go b/pkg/lib/queueinformer/mock_queueinformer_operator.go deleted file mode 100644 index 606359b0a8..0000000000 --- a/pkg/lib/queueinformer/mock_queueinformer_operator.go +++ /dev/null @@ -1,60 +0,0 @@ -package queueinformer - -import ( - "github.com/golang/mock/gomock" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/watch" - - "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" -) - -// MockListWatcher mocks k8s.io/client-go/tools/cache.ListerWatcher. -// -// This is type is useful for mocking Informers. -type MockListWatcher struct{} - -// List always returns (nil, nil). -func (l *MockListWatcher) List(options metav1.ListOptions) (runtime.Object, error) { - return nil, nil -} - -// Watch always returns (nil, nil). -func (l *MockListWatcher) Watch(options metav1.ListOptions) (watch.Interface, error) { - return nil, nil -} - -// MockOperator uses TestQueueinformers and a Mock operator client -type MockOperator struct { - Operator - testQueueInformers []*TestQueueInformer - MockClient *operatorclient.MockClientInterface -} - -// NewMockOperator creates a new Operator configured to manage the cluster defined in kubeconfig. -func NewMockOperator(gomockCtrl *gomock.Controller, testQueueInformers ...*TestQueueInformer) *MockOperator { - mockClient := operatorclient.NewMockClientInterface(gomockCtrl) - - if testQueueInformers == nil { - testQueueInformers = []*TestQueueInformer{} - } - queueInformers := []*QueueInformer{} - for _, informer := range testQueueInformers { - queueInformers = append(queueInformers, &informer.QueueInformer) - } - operator := &MockOperator{ - Operator: Operator{ - queueInformers: queueInformers, - OpClient: mockClient, - }, - testQueueInformers: testQueueInformers, - MockClient: mockClient, - } - return operator -} - -// RegisterQueueInformer adds a QueueInformer to this operator -func (o *MockOperator) RegisterQueueInformer(queueInformer *QueueInformer) { - o.testQueueInformers = append(o.testQueueInformers, &TestQueueInformer{*queueInformer}) - o.Operator.queueInformers = append(o.queueInformers, queueInformer) -} diff --git a/pkg/lib/queueinformer/queueinformer_operator.go b/pkg/lib/queueinformer/queueinformer_operator.go index 50f622596d..51e0511db3 100644 --- a/pkg/lib/queueinformer/queueinformer_operator.go +++ b/pkg/lib/queueinformer/queueinformer_operator.go @@ -30,6 +30,17 @@ func NewOperator(kubeconfig string, queueInformers ...*QueueInformer) (*Operator return operator, nil } +func NewOperatorFromClient(opClient operatorclient.ClientInterface, queueInformers ...*QueueInformer) (*Operator, error) { + if queueInformers == nil { + queueInformers = []*QueueInformer{} + } + operator := &Operator{ + OpClient: opClient, + queueInformers: queueInformers, + } + return operator, nil +} + // RegisterQueueInformer adds a QueueInformer to this operator func (o *Operator) RegisterQueueInformer(queueInformer *QueueInformer) { if o.queueInformers == nil { diff --git a/pkg/lib/queueinformer/testqueueinformer.go b/pkg/lib/queueinformer/testqueueinformer.go deleted file mode 100644 index 948b1170a8..0000000000 --- a/pkg/lib/queueinformer/testqueueinformer.go +++ /dev/null @@ -1,33 +0,0 @@ -package queueinformer - -import ( - "k8s.io/client-go/tools/cache" - "k8s.io/client-go/util/workqueue" -) - -// TestQueueInformer wraps a normal queueinformer with knobs for injecting data for testing -type TestQueueInformer struct { - QueueInformer -} - -func (q *TestQueueInformer) Enqueue(obj interface{}) { - q.QueueInformer.enqueue(obj) -} - -func NewTestQueueInformer(queue workqueue.RateLimitingInterface, informer cache.SharedIndexInformer, handler SyncHandler, funcs *cache.ResourceEventHandlerFuncs) *TestQueueInformer { - queueInformer := &TestQueueInformer{ - QueueInformer{ - queue: queue, - informer: informer, - syncHandler: handler, - }, - } - if funcs == nil { - queueInformer.resourceEventHandlerFuncs = queueInformer.defaultResourceEventHandlerFuncs() - } else { - queueInformer.resourceEventHandlerFuncs = funcs - } - queueInformer.informer.AddEventHandler(queueInformer.resourceEventHandlerFuncs) - - return queueInformer -} diff --git a/scripts/run_e2e_local.sh b/scripts/run_e2e_local.sh index 318f113fd6..e915bfc38f 100755 --- a/scripts/run_e2e_local.sh +++ b/scripts/run_e2e_local.sh @@ -25,9 +25,9 @@ function cleanup { function cleanupAndExit { exitCode=$? if [ "$exitCode" -ne "0" ]; then - echo "error running tests, printing pod logs: "; - kubectl -n ${namespace} logs -l app=alm-operator; - kubectl -n ${namespace} logs -l app=catalog-operator; + echo "error running tests. logs written to olm.log and catalog.log"; + kubectl -n ${namespace} logs -l app=alm-operator > olm.log; + kubectl -n ${namespace} logs -l app=catalog-operator > catalog.log; fi cleanup exit $exitCode diff --git a/test/e2e/csv_e2e_test.go b/test/e2e/csv_e2e_test.go index e5897e0b4f..8b79d3e0bb 100644 --- a/test/e2e/csv_e2e_test.go +++ b/test/e2e/csv_e2e_test.go @@ -145,10 +145,11 @@ func waitForCSVToDelete(t *testing.T, c versioned.Interface, name string) error var err error err = wait.Poll(pollInterval, pollDuration, func() (bool, error) { - _, err = c.OperatorsV1alpha1().ClusterServiceVersions(testNamespace).Get(name, metav1.GetOptions{}) + fetched, err := c.OperatorsV1alpha1().ClusterServiceVersions(testNamespace).Get(name, metav1.GetOptions{}) if errors.IsNotFound(err) { return true, nil } + t.Logf("%s (%s): %s", fetched.Status.Phase, fetched.Status.Reason, fetched.Status.Message) if err != nil { return false, err } @@ -396,6 +397,12 @@ func TestUpdateCSVSameDeploymentName(t *testing.T) { require.NoError(t, err) require.NotNil(t, dep) + // should have csv-sa and old-csv-sa + _, err = c.GetServiceAccount(testNamespace, "csv-sa") + require.NoError(t, err) + _, err = c.GetServiceAccount(testNamespace, "old-csv-sa") + require.NoError(t, err) + // Create "updated" CSV strategyNew := install.StrategyDetailsDeployment{ Permissions: []install.StrategyDeploymentPermissions{ @@ -468,19 +475,13 @@ func TestUpdateCSVSameDeploymentName(t *testing.T) { fetchedCSV, err := fetchCSV(t, crc, csvNew.Name, csvSucceededChecker) require.NoError(t, err) - // should have csv-sa and old-csv-sa - _, err = c.GetServiceAccount(testNamespace, "csv-sa") - require.NoError(t, err) - _, err = c.GetServiceAccount(testNamespace, "old-csv-sa") - require.NoError(t, err) - // Should have updated existing deployment depUpdated, err := c.GetDeployment(testNamespace, strategyNew.DeploymentSpecs[0].Name) require.NoError(t, err) require.NotNil(t, depUpdated) require.Equal(t, depUpdated.Spec.Template.Spec.Containers[0].Name, strategyNew.DeploymentSpecs[0].Spec.Template.Spec.Containers[0].Name) - // should have csv-sa and old-csv-sa + // should have csv-sa and new-csv-sa _, err = c.GetServiceAccount(testNamespace, "csv-sa") require.NoError(t, err) _, err = c.GetServiceAccount(testNamespace, "new-csv-sa") @@ -673,11 +674,11 @@ func TestUpdateCSVMultipleIntermediates(t *testing.T) { CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ Owned: []v1alpha1.CRDDescription{ { - Name: "ins2.cluster.com", + Name: "ins3.cluster.com", Version: "v1alpha1", - Kind: "InCluster2", - DisplayName: "Ins2", - Description: "In the cluster2", + Kind: "InCluster3", + DisplayName: "Ins3", + Description: "In the cluster3", }, }, }, @@ -687,7 +688,7 @@ func TestUpdateCSVMultipleIntermediates(t *testing.T) { // Create dependency first (CRD) cleanupCRD, err := createCRD(c, extv1beta1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ - Name: "ins2.cluster.com", + Name: "ins3.cluster.com", }, Spec: extv1beta1.CustomResourceDefinitionSpec{ Group: "cluster.com", @@ -699,10 +700,10 @@ func TestUpdateCSVMultipleIntermediates(t *testing.T) { }, }, Names: extv1beta1.CustomResourceDefinitionNames{ - Plural: "ins2", - Singular: "in2", - Kind: "InCluster2", - ListKind: "InClusterList2", + Plural: "ins3", + Singular: "in3", + Kind: "InCluster3", + ListKind: "InClusterList3", }, Scope: "Namespaced", }, @@ -752,11 +753,11 @@ func TestUpdateCSVMultipleIntermediates(t *testing.T) { CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ Owned: []v1alpha1.CRDDescription{ { - Name: "ins2.cluster.com", + Name: "ins3.cluster.com", Version: "v1alpha1", - Kind: "InCluster2", - DisplayName: "Ins2", - Description: "In the cluster2", + Kind: "InCluster3", + DisplayName: "Ins3", + Description: "In the cluster3", }, }, }, diff --git a/test/e2e/installplan_e2e_test.go b/test/e2e/installplan_e2e_test.go index 972a86a74f..75260deb04 100644 --- a/test/e2e/installplan_e2e_test.go +++ b/test/e2e/installplan_e2e_test.go @@ -193,46 +193,6 @@ func TestCreateInstallPlanManualApproval(t *testing.T) { } -func TestCreateInstallPlanFromInvalidClusterServiceVersionNameExistingBehavior(t *testing.T) { - crc := newCRClient(t) - - installPlan := v1alpha1.InstallPlan{ - TypeMeta: metav1.TypeMeta{ - Kind: v1alpha1.InstallPlanKind, - APIVersion: v1alpha1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "install-bitcoin-miner", - Namespace: testNamespace, - }, - Spec: v1alpha1.InstallPlanSpec{ - ClusterServiceVersionNames: []string{"Bitcoin-miner-0.1"}, - Approval: v1alpha1.ApprovalAutomatic, - }, - } - - _, err := crc.OperatorsV1alpha1().InstallPlans(testNamespace).Create(&installPlan) - require.NoError(t, err) - - fetchedInstallPlan, err := fetchInstallPlan(t, crc, installPlan.GetName(), func(fip *v1alpha1.InstallPlan) bool { - return fip.Status.Phase == v1alpha1.InstallPlanPhasePlanning && - fip.Status.Conditions[0].Type == v1alpha1.InstallPlanResolved && - fip.Status.Conditions[0].Reason == v1alpha1.InstallPlanReasonDependencyConflict - }) - - // Fetch installplan again to check for unnecessary control loops - _, err = fetchInstallPlan(t, crc, installPlan.GetName(), func(fip *v1alpha1.InstallPlan) bool { - compareResources(t, fetchedInstallPlan, fip) - return true - }) - require.NoError(t, err) - - require.Equal(t, v1alpha1.InstallPlanPhaseFailed, fetchedInstallPlan.Status.Phase) - require.Equal(t, v1alpha1.InstallPlanResolved, fetchedInstallPlan.Status.Conditions[0].Type) - require.Equal(t, corev1.ConditionFalse, fetchedInstallPlan.Status.Conditions[0].Status) - require.Equal(t, v1alpha1.InstallPlanReasonInstallCheckFailed, fetchedInstallPlan.Status.Conditions[0].Reason) -} - // As an infra owner, creating an installplan with a clusterServiceVersionName that does not exist in the catalog should result in a “Failed” status func TestCreateInstallPlanFromInvalidClusterServiceVersionName(t *testing.T) { crc := newCRClient(t) From e151d6bd4ba53dab567a40559f4b0b88d37b743d Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Sun, 5 Aug 2018 17:59:45 -0400 Subject: [PATCH 6/9] feat(olm): set up watches on deployments so that csv status can be updated more frequently --- pkg/controller/operators/olm/operator.go | 52 ++++++++++++++++++++++-- pkg/lib/ownerutil/util.go | 18 ++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/pkg/controller/operators/olm/operator.go b/pkg/controller/operators/olm/operator.go index 4404eece91..e71a61b43a 100644 --- a/pkg/controller/operators/olm/operator.go +++ b/pkg/controller/operators/olm/operator.go @@ -18,7 +18,9 @@ import ( "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/annotator" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" + "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/queueinformer" + "k8s.io/api/apps/v1" ) var ErrRequirementsNotMet = errors.New("requirements were not met") @@ -97,9 +99,48 @@ func NewOperator(crClient versioned.Interface, opClient operatorclient.ClientInt op.RegisterQueueInformer(informer) } op.csvQueue = csvQueue + + // set up watch on deployments + depInformers := []cache.SharedIndexInformer{} + for _, namespace := range namespaces { + log.Debugf("watching deployments in namespace %s", namespace) + informer := informers.NewSharedInformerFactory(opClient.KubernetesInterface(), wakeupInterval).Apps().V1().Deployments().Informer() + depInformers = append(depInformers, informer) + } + + depQueue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "csv-deployments") + depQueueInformers := queueinformer.New( + depQueue, + depInformers, + op.syncDeployment, + nil, + ) + for _, informer := range depQueueInformers { + op.RegisterQueueInformer(informer) + } return op, nil } +func (a *Operator) requeueCSV(name, namespace string) { + // we can build the key directly, will need to change if queue uses different key scheme + key := fmt.Sprintf("%s/%s", namespace, name) + a.csvQueue.AddRateLimited(key) + return +} + +func (a *Operator) syncDeployment(obj interface{}) (syncError error) { + deployment, ok := obj.(*v1.Deployment) + if !ok { + log.Debugf("wrong type: %#v", obj) + return fmt.Errorf("casting Deployment failed") + } + if ownerutil.IsOwnedByKind(deployment, v1alpha1.ClusterServiceVersionKind) { + oref := ownerutil.GetOwnerByKind(deployment, v1alpha1.ClusterServiceVersionKind) + a.requeueCSV(oref.Name, deployment.GetNamespace()) + } + 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) @@ -110,6 +151,7 @@ func (a *Operator) syncClusterServiceVersion(obj interface{}) (syncError error) logger := log.WithFields(log.Fields{ "csv": clusterServiceVersion.GetName(), "namespace": clusterServiceVersion.GetNamespace(), + "phase": clusterServiceVersion.Status.Phase, }) logger.Info("syncing") @@ -146,6 +188,7 @@ func (a *Operator) transitionCSVState(in v1alpha1.ClusterServiceVersion) (out *v // check if the current CSV is being replaced, return with replacing status if so if err := a.checkReplacementsAndUpdateStatus(out); err != nil { + logger.WithField("err", err).Info("replacement check") return } @@ -224,10 +267,11 @@ func (a *Operator) transitionCSVState(in v1alpha1.ClusterServiceVersion) (out *v csv.SetPhase(v1alpha1.CSVPhaseDeleting, v1alpha1.CSVReasonReplaced, "has been replaced by a newer ClusterServiceVersion that has successfully installed.") // ignore errors and success here; this step is just an optimization to speed up GC a.client.OperatorsV1alpha1().ClusterServiceVersions(csv.GetNamespace()).UpdateStatus(csv) + a.requeueCSV(csv.GetName(), csv.GetNamespace()) } // if there's no newer version, requeue for processing (likely will be GCable before resync) - //a.requeueCSV(out) + a.requeueCSV(out.GetName(), out.GetNamespace()) case v1alpha1.CSVPhaseDeleting: foreground := metav1.DeletePropagationForeground syncError = a.client.OperatorsV1alpha1().ClusterServiceVersions(out.GetNamespace()).Delete(out.GetName(), &metav1.DeleteOptions{PropagationPolicy: &foreground}) @@ -274,7 +318,7 @@ func (a *Operator) csvsInNamespace(namespace string) map[string]*v1alpha1.Cluste } csvs := make(map[string]*v1alpha1.ClusterServiceVersion, len(csvsInNamespace.Items)) for _, csv := range csvsInNamespace.Items { - csvs[csv.Name] = &csv + csvs[csv.Name] = csv.DeepCopy() } return csvs } @@ -284,7 +328,6 @@ func (a *Operator) checkReplacementsAndUpdateStatus(csv *v1alpha1.ClusterService if csv.Status.Phase == v1alpha1.CSVPhaseReplacing || csv.Status.Phase == v1alpha1.CSVPhaseDeleting { return nil } - if replacement := a.isBeingReplaced(csv, a.csvsInNamespace(csv.GetNamespace())); replacement != nil { log.Infof("newer ClusterServiceVersion replacing %s, no-op", csv.SelfLink) msg := fmt.Sprintf("being replaced by csv: %s", replacement.SelfLink) @@ -331,6 +374,7 @@ func (a *Operator) parseStrategiesAndUpdateStatus(csv *v1alpha1.ClusterServiceVe previousCSV := a.isReplacing(csv) var previousStrategy install.Strategy if previousCSV != nil { + a.requeueCSV(previousCSV.Name, previousCSV.Namespace) previousStrategy, err = a.resolver.UnmarshalStrategy(previousCSV.Spec.InstallStrategy) if err != nil { previousStrategy = nil @@ -403,7 +447,9 @@ func (a *Operator) annotateNamespace(obj interface{}) (syncError error) { func (a *Operator) isBeingReplaced(in *v1alpha1.ClusterServiceVersion, csvsInNamespace map[string]*v1alpha1.ClusterServiceVersion) (replacedBy *v1alpha1.ClusterServiceVersion) { for _, csv := range csvsInNamespace { + log.Infof("checking %s", csv.GetName()) if csv.Spec.Replaces == in.GetName() { + log.Infof("%s replaced by %s", in.GetName(), csv.GetName()) replacedBy = csv return } diff --git a/pkg/lib/ownerutil/util.go b/pkg/lib/ownerutil/util.go index 60a59fa24a..63e403c2a5 100644 --- a/pkg/lib/ownerutil/util.go +++ b/pkg/lib/ownerutil/util.go @@ -22,6 +22,24 @@ func IsOwnedBy(object metav1.Object, owner Owner) bool { return false } +func IsOwnedByKind(object metav1.Object, ownerKind string) bool { + for _, oref := range object.GetOwnerReferences() { + if oref.Kind == ownerKind { + return true + } + } + return false +} + +func GetOwnerByKind(object metav1.Object, ownerKind string) metav1.OwnerReference { + for _, oref := range object.GetOwnerReferences() { + if oref.Kind == ownerKind { + return oref + } + } + return metav1.OwnerReference{} +} + // AddNonBlockingOwner adds a nonblocking owner to the ownerref list. func AddNonBlockingOwner(object metav1.Object, owner Owner) { // Most of the time we won't have TypeMeta on the object, so we infer it for types we know about From 765212b78e157232f534e93e3ad0aae9eb5839a9 Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Sun, 5 Aug 2018 18:25:23 -0400 Subject: [PATCH 7/9] fix(subscriptions): don't update subscription when there are no status changes --- pkg/controller/operators/catalog/operator.go | 2 +- .../operators/catalog/subscriptions.go | 98 +++++++++---------- scripts/run_e2e_docker.sh | 6 +- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/pkg/controller/operators/catalog/operator.go b/pkg/controller/operators/catalog/operator.go index 974f7c1b24..240c326500 100644 --- a/pkg/controller/operators/catalog/operator.go +++ b/pkg/controller/operators/catalog/operator.go @@ -169,7 +169,7 @@ func (o *Operator) syncSubscriptions(obj interface{}) (syncError error) { var updatedSub *v1alpha1.Subscription updatedSub, syncError = o.syncSubscription(sub) - if updatedSub == nil { + if updatedSub == nil || updatedSub.Status.State == sub.Status.State { return } if syncError != nil { diff --git a/pkg/controller/operators/catalog/subscriptions.go b/pkg/controller/operators/catalog/subscriptions.go index 685281b5c7..79c2e22d78 100644 --- a/pkg/controller/operators/catalog/subscriptions.go +++ b/pkg/controller/operators/catalog/subscriptions.go @@ -23,116 +23,116 @@ const ( ) // FIXME(alecmerdler): Rewrite this whole block to be more clear -func (o *Operator) syncSubscription(sub *v1alpha1.Subscription) (*v1alpha1.Subscription, error) { - if sub == nil || sub.Spec == nil { +func (o *Operator) syncSubscription(in *v1alpha1.Subscription) (*v1alpha1.Subscription, error) { + if in == nil || in.Spec == nil { return nil, ErrNilSubscription } - - sub = ensureLabels(sub) + out := in.DeepCopy() + out = ensureLabels(out) // Only sync if catalog has been updated since last sync time - if o.sourcesLastUpdate.Before(&sub.Status.LastUpdated) && sub.Status.State == v1alpha1.SubscriptionStateAtLatest { + if o.sourcesLastUpdate.Before(&out.Status.LastUpdated) && out.Status.State == v1alpha1.SubscriptionStateAtLatest { log.Infof("skipping sync: no new updates to catalog since last sync at %s", - sub.Status.LastUpdated.String()) + out.Status.LastUpdated.String()) return nil, nil } o.sourcesLock.Lock() defer o.sourcesLock.Unlock() - catalogNamespace := sub.Spec.CatalogSourceNamespace + catalogNamespace := out.Spec.CatalogSourceNamespace if catalogNamespace == "" { catalogNamespace = o.namespace } - catalog, ok := o.sources[registry.SourceKey{Name: sub.Spec.CatalogSource, Namespace: catalogNamespace}] + catalog, ok := o.sources[registry.SourceKey{Name: out.Spec.CatalogSource, Namespace: catalogNamespace}] if !ok { - return sub, fmt.Errorf("unknown catalog source %s in namespace %s", sub.Spec.CatalogSource, catalogNamespace) + return out, fmt.Errorf("unknown catalog source %s in namespace %s", out.Spec.CatalogSource, catalogNamespace) } // Find latest CSV if no CSVs are installed already - if sub.Status.CurrentCSV == "" { - if sub.Spec.StartingCSV != "" { - sub.Status.CurrentCSV = sub.Spec.StartingCSV + if out.Status.CurrentCSV == "" { + if out.Spec.StartingCSV != "" { + out.Status.CurrentCSV = out.Spec.StartingCSV } else { - csv, err := catalog.FindCSVForPackageNameUnderChannel(sub.Spec.Package, sub.Spec.Channel) + csv, err := catalog.FindCSVForPackageNameUnderChannel(out.Spec.Package, out.Spec.Channel) if err != nil { - return sub, fmt.Errorf("failed to find CSV for package %s in channel %s: %v", sub.Spec.Package, sub.Spec.Channel, err) + return out, fmt.Errorf("failed to find CSV for package %s in channel %s: %v", out.Spec.Package, out.Spec.Channel, err) } if csv == nil { - return sub, fmt.Errorf("failed to find CSV for package %s in channel %s: nil CSV", sub.Spec.Package, sub.Spec.Channel) + return out, fmt.Errorf("failed to find CSV for package %s in channel %s: nil CSV", out.Spec.Package, out.Spec.Channel) } - sub.Status.CurrentCSV = csv.GetName() + out.Status.CurrentCSV = csv.GetName() } - sub.Status.State = v1alpha1.SubscriptionStateUpgradeAvailable - return sub, nil + out.Status.State = v1alpha1.SubscriptionStateUpgradeAvailable + return out, nil } // Check that desired CSV has been installed - csv, err := o.client.OperatorsV1alpha1().ClusterServiceVersions(sub.GetNamespace()).Get(sub.Status.CurrentCSV, metav1.GetOptions{}) + csv, err := o.client.OperatorsV1alpha1().ClusterServiceVersions(out.GetNamespace()).Get(out.Status.CurrentCSV, metav1.GetOptions{}) if err != nil || csv == nil { - log.Infof("error fetching CSV %s via k8s api: %v", sub.Status.CurrentCSV, err) - if sub.Status.Install != nil && sub.Status.Install.Name != "" { - ip, err := o.client.OperatorsV1alpha1().InstallPlans(sub.GetNamespace()).Get(sub.Status.Install.Name, metav1.GetOptions{}) + log.Infof("error fetching CSV %s via k8s api: %v", out.Status.CurrentCSV, err) + if out.Status.Install != nil && out.Status.Install.Name != "" { + ip, err := o.client.OperatorsV1alpha1().InstallPlans(out.GetNamespace()).Get(out.Status.Install.Name, metav1.GetOptions{}) if err != nil { - log.Errorf("get installplan %s error: %v", sub.Status.Install.Name, err) + log.Errorf("get installplan %s error: %v", out.Status.Install.Name, err) } if err == nil && ip != nil { - log.Infof("installplan for %s already exists", sub.Status.CurrentCSV) - return sub, nil + log.Infof("installplan for %s already exists", out.Status.CurrentCSV) + return out, nil } - log.Infof("installplan %s not found: creating new plan", sub.Status.Install.Name) - sub.Status.Install = nil + log.Infof("installplan %s not found: creating new plan", out.Status.Install.Name) + out.Status.Install = nil } // Install CSV if doesn't exist - sub.Status.State = v1alpha1.SubscriptionStateUpgradePending + out.Status.State = v1alpha1.SubscriptionStateUpgradePending ip := &v1alpha1.InstallPlan{ ObjectMeta: metav1.ObjectMeta{}, Spec: v1alpha1.InstallPlanSpec{ - ClusterServiceVersionNames: []string{sub.Status.CurrentCSV}, - Approval: sub.GetInstallPlanApproval(), + ClusterServiceVersionNames: []string{out.Status.CurrentCSV}, + Approval: out.GetInstallPlanApproval(), }, } - ownerutil.AddNonBlockingOwner(ip, sub) - ip.SetGenerateName(fmt.Sprintf("install-%s-", sub.Status.CurrentCSV)) - ip.SetNamespace(sub.GetNamespace()) + ownerutil.AddNonBlockingOwner(ip, out) + ip.SetGenerateName(fmt.Sprintf("install-%s-", out.Status.CurrentCSV)) + ip.SetNamespace(out.GetNamespace()) // Inherit the subscription's catalog source - ip.Spec.CatalogSource = sub.Spec.CatalogSource - ip.Spec.CatalogSourceNamespace = sub.Spec.CatalogSourceNamespace + ip.Spec.CatalogSource = out.Spec.CatalogSource + ip.Spec.CatalogSourceNamespace = out.Spec.CatalogSourceNamespace - res, err := o.client.OperatorsV1alpha1().InstallPlans(sub.GetNamespace()).Create(ip) + res, err := o.client.OperatorsV1alpha1().InstallPlans(out.GetNamespace()).Create(ip) if err != nil { - return sub, fmt.Errorf("failed to ensure current CSV %s installed: %v", sub.Status.CurrentCSV, err) + return out, fmt.Errorf("failed to ensure current CSV %s installed: %v", out.Status.CurrentCSV, err) } if res == nil { - return sub, errors.New("unexpected installplan returned by k8s api on create: ") + return out, errors.New("unexpected installplan returned by k8s api on create: ") } - sub.Status.Install = &v1alpha1.InstallPlanReference{ + out.Status.Install = &v1alpha1.InstallPlanReference{ UID: res.GetUID(), Name: res.GetName(), APIVersion: v1alpha1.SchemeGroupVersion.String(), Kind: v1alpha1.InstallPlanKind, } - return sub, nil + return out, nil } // Poll catalog for an update - repl, err := catalog.FindReplacementCSVForPackageNameUnderChannel(sub.Spec.Package, sub.Spec.Channel, sub.Status.CurrentCSV) + repl, err := catalog.FindReplacementCSVForPackageNameUnderChannel(out.Spec.Package, out.Spec.Channel, out.Status.CurrentCSV) if err != nil { - sub.Status.State = v1alpha1.SubscriptionStateAtLatest - return sub, fmt.Errorf("failed to lookup replacement CSV for %s: %v", sub.Status.CurrentCSV, err) + out.Status.State = v1alpha1.SubscriptionStateAtLatest + return out, fmt.Errorf("failed to lookup replacement CSV for %s: %v", out.Status.CurrentCSV, err) } if repl == nil { - sub.Status.State = v1alpha1.SubscriptionStateAtLatest - return sub, fmt.Errorf("nil replacement CSV for %s returned from catalog", sub.Status.CurrentCSV) + out.Status.State = v1alpha1.SubscriptionStateAtLatest + return out, fmt.Errorf("nil replacement CSV for %s returned from catalog", out.Status.CurrentCSV) } // Update subscription with new latest - sub.Status.CurrentCSV = repl.GetName() - sub.Status.Install = nil - sub.Status.State = v1alpha1.SubscriptionStateUpgradeAvailable - return sub, nil + out.Status.CurrentCSV = repl.GetName() + out.Status.Install = nil + out.Status.State = v1alpha1.SubscriptionStateUpgradeAvailable + return out, nil } func ensureLabels(sub *v1alpha1.Subscription) *v1alpha1.Subscription { diff --git a/scripts/run_e2e_docker.sh b/scripts/run_e2e_docker.sh index 803e0e9ee2..4a8ed3080d 100755 --- a/scripts/run_e2e_docker.sh +++ b/scripts/run_e2e_docker.sh @@ -25,9 +25,9 @@ function cleanup { function cleanupAndExit { exitCode=$? if [ "$exitCode" -ne "0" ]; then - echo "error running tests, printing pod logs: "; - kubectl -n ${namespace} logs -l app=alm-operator; - kubectl -n ${namespace} logs -l app=catalog-operator; + echo "error running tests. logs written to olm.log and catalog.log"; + kubectl -n ${namespace} logs -l app=alm-operator > olm.log; + kubectl -n ${namespace} logs -l app=catalog-operator > catalog.log; fi cleanup exit $exitCode From 47601e2475eb5120399534e3974266dba3ed6faa Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Sun, 5 Aug 2018 18:59:32 -0400 Subject: [PATCH 8/9] chore(api): update apps/v1beta2 to apps/v1 --- pkg/controller/operators/olm/operator_test.go | 16 +++---- .../registry/directory_loader_test.go | 2 +- pkg/lib/operatorclient/fake/deployment.go | 46 +++++++++---------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/pkg/controller/operators/olm/operator_test.go b/pkg/controller/operators/olm/operator_test.go index 4629e864ea..87a9404803 100644 --- a/pkg/controller/operators/olm/operator_test.go +++ b/pkg/controller/operators/olm/operator_test.go @@ -8,6 +8,8 @@ import ( log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/api/core/v1" "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" @@ -19,8 +21,6 @@ import ( "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned/fake" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" opFake "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient/fake" - "k8s.io/api/apps/v1beta2" - "k8s.io/api/core/v1" ) // Fakes @@ -71,14 +71,14 @@ func (o *Operator) GetClient() versioned.Interface { // Tests -func deployment(deploymentName, namespace string) *v1beta2.Deployment { +func deployment(deploymentName, namespace string) *appsv1.Deployment { var singleInstance = int32(1) - return &v1beta2.Deployment{ + return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: deploymentName, Namespace: namespace, }, - Spec: v1beta2.DeploymentSpec{ + Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": deploymentName, @@ -106,7 +106,7 @@ func deployment(deploymentName, namespace string) *v1beta2.Deployment { }, }, }, - Status: v1beta2.DeploymentStatus{ + Status: appsv1.DeploymentStatus{ Replicas: singleInstance, ReadyReplicas: singleInstance, AvailableReplicas: singleInstance, @@ -121,7 +121,7 @@ func installStrategy(deploymentName string) v1alpha1.NamedInstallStrategy { DeploymentSpecs: []install.StrategyDeploymentSpec{ { Name: deploymentName, - Spec: v1beta2.DeploymentSpec{ + Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": deploymentName, @@ -613,7 +613,7 @@ func TestTransitionCSV(t *testing.T) { expected: expected{ csvStates: map[string]csvState{ "csv1": {exists: false, phase: v1alpha1.CSVPhaseNone}, - "csv2": {exists: true, phase: v1alpha1.CSVPhaseReplacing}, + "csv2": {exists: true, phase: v1alpha1.CSVPhaseDeleting}, "csv3": {exists: true, phase: v1alpha1.CSVPhaseSucceeded}, }, }, diff --git a/pkg/controller/registry/directory_loader_test.go b/pkg/controller/registry/directory_loader_test.go index 2716376ed1..6fd90563c1 100644 --- a/pkg/controller/registry/directory_loader_test.go +++ b/pkg/controller/registry/directory_loader_test.go @@ -18,7 +18,7 @@ func TestDirectoryLoader(t *testing.T) { require.Contains(t, catalog.packages, "etcd") require.Contains(t, catalog.packages, "vault") require.Contains(t, catalog.packages, "prometheus") - require.Len(t, catalog.packages, 2) + require.Len(t, catalog.packages, 3) } func TestDirectoryLoaderHiddenDirs(t *testing.T) { diff --git a/pkg/lib/operatorclient/fake/deployment.go b/pkg/lib/operatorclient/fake/deployment.go index 341b508767..ca1d22102f 100644 --- a/pkg/lib/operatorclient/fake/deployment.go +++ b/pkg/lib/operatorclient/fake/deployment.go @@ -7,7 +7,7 @@ import ( "github.com/golang/glog" "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient" - appsv1beta2 "k8s.io/api/apps/v1beta2" + appsv1 "k8s.io/api/apps/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -20,28 +20,28 @@ const ( ) // GetDeployment returns the Deployment object for the given namespace and name. -func (c *Client) GetDeployment(namespace, name string) (*appsv1beta2.Deployment, error) { +func (c *Client) GetDeployment(namespace, name string) (*appsv1.Deployment, error) { glog.V(4).Infof("[GET Deployment]: %s:%s", namespace, name) - return c.AppsV1beta2().Deployments(namespace).Get(name, metav1.GetOptions{}) + return c.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{}) } // CreateDeployment creates the Deployment object. -func (c *Client) CreateDeployment(dep *appsv1beta2.Deployment) (*appsv1beta2.Deployment, error) { +func (c *Client) CreateDeployment(dep *appsv1.Deployment) (*appsv1.Deployment, error) { glog.V(4).Infof("[CREATE Deployment]: %s:%s", dep.Namespace, dep.Name) - return c.AppsV1beta2().Deployments(dep.Namespace).Create(dep) + return c.AppsV1().Deployments(dep.Namespace).Create(dep) } // DeleteDeployment deletes the Deployment object. func (c *Client) DeleteDeployment(namespace, name string, options *metav1.DeleteOptions) error { glog.V(4).Infof("[DELETE Deployment]: %s:%s", namespace, name) - return c.AppsV1beta2().Deployments(namespace).Delete(name, options) + return c.AppsV1().Deployments(namespace).Delete(name, options) } // UpdateDeployment updates a Deployment object by performing a 2-way patch between the existing // Deployment and the result of the UpdateFunction. // // Returns the latest Deployment and true if it was updated, or an error. -func (c *Client) UpdateDeployment(dep *appsv1beta2.Deployment) (*appsv1beta2.Deployment, bool, error) { +func (c *Client) UpdateDeployment(dep *appsv1.Deployment) (*appsv1.Deployment, bool, error) { return c.PatchDeployment(nil, dep) } @@ -49,11 +49,11 @@ func (c *Client) UpdateDeployment(dep *appsv1beta2.Deployment) (*appsv1beta2.Dep // Deployment and `original` and `modified` manifests. // // Returns the latest Deployment and true if it was updated, or an error. -func (c *Client) PatchDeployment(original, modified *appsv1beta2.Deployment) (*appsv1beta2.Deployment, bool, error) { +func (c *Client) PatchDeployment(original, modified *appsv1.Deployment) (*appsv1.Deployment, bool, error) { namespace, name := modified.Namespace, modified.Name glog.V(4).Infof("[PATCH Deployment]: %s:%s", namespace, name) - current, err := c.AppsV1beta2().Deployments(namespace).Get(name, metav1.GetOptions{}) + current, err := c.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{}) if err != nil { return nil, false, err } @@ -68,7 +68,7 @@ func (c *Client) PatchDeployment(original, modified *appsv1beta2.Deployment) (*a if err != nil { return nil, false, err } - updated, err := c.AppsV1beta2().Deployments(namespace).Patch(name, types.StrategicMergePatchType, patchBytes) + updated, err := c.AppsV1().Deployments(namespace).Patch(name, types.StrategicMergePatchType, patchBytes) if err != nil { return nil, false, err } @@ -77,7 +77,7 @@ func (c *Client) PatchDeployment(original, modified *appsv1beta2.Deployment) (*a // RollingUpdateDeployment performs a rolling update on the given Deployment. It requires that the // Deployment uses the RollingUpdateDeploymentStrategyType update strategy. -func (c *Client) RollingUpdateDeployment(dep *appsv1beta2.Deployment) (*appsv1beta2.Deployment, bool, error) { +func (c *Client) RollingUpdateDeployment(dep *appsv1.Deployment) (*appsv1.Deployment, bool, error) { return c.RollingUpdateDeploymentMigrations(dep.Namespace, dep.Name, operatorclient.Update(dep)) } @@ -86,7 +86,7 @@ func (c *Client) RollingUpdateDeployment(dep *appsv1beta2.Deployment) (*appsv1be // // RollingUpdateDeploymentMigrations will run any before / during / after migrations that have been // specified in the upgrade options. -func (c *Client) RollingUpdateDeploymentMigrations(namespace, name string, f operatorclient.UpdateFunction) (*appsv1beta2.Deployment, bool, error) { +func (c *Client) RollingUpdateDeploymentMigrations(namespace, name string, f operatorclient.UpdateFunction) (*appsv1.Deployment, bool, error) { glog.V(4).Infof("[ROLLING UPDATE Deployment]: %s:%s", namespace, name) return c.RollingPatchDeploymentMigrations(namespace, name, updateToPatch(f)) } @@ -97,7 +97,7 @@ func (c *Client) RollingUpdateDeploymentMigrations(namespace, name string, f ope // // RollingPatchDeployment will run any before / after migrations that have been specified in the // upgrade options. -func (c *Client) RollingPatchDeployment(original, modified *appsv1beta2.Deployment) (*appsv1beta2.Deployment, bool, error) { +func (c *Client) RollingPatchDeployment(original, modified *appsv1.Deployment) (*appsv1.Deployment, bool, error) { return c.RollingPatchDeploymentMigrations(modified.Namespace, modified.Name, patch(original, modified)) } @@ -107,10 +107,10 @@ func (c *Client) RollingPatchDeployment(original, modified *appsv1beta2.Deployme // // RollingPatchDeploymentMigrations will run any before / after migrations that have been specified // in the upgrade options. -func (c *Client) RollingPatchDeploymentMigrations(namespace, name string, f operatorclient.PatchFunction) (*appsv1beta2.Deployment, bool, error) { +func (c *Client) RollingPatchDeploymentMigrations(namespace, name string, f operatorclient.PatchFunction) (*appsv1.Deployment, bool, error) { glog.V(4).Infof("[ROLLING PATCH Deployment]: %s:%s", namespace, name) - current, err := c.AppsV1beta2().Deployments(namespace).Get(name, metav1.GetOptions{}) + current, err := c.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{}) if err != nil { return nil, false, err } @@ -129,7 +129,7 @@ func (c *Client) RollingPatchDeploymentMigrations(namespace, name string, f oper if originalObj == nil { originalObj = current // Emulate 2-way merge. } - original, modified := originalObj.(*appsv1beta2.Deployment), modifiedObj.(*appsv1beta2.Deployment) + original, modified := originalObj.(*appsv1.Deployment), modifiedObj.(*appsv1.Deployment) // Check for nil pointers. if modified == nil { return nil, false, errors.New("modified cannot be nil") @@ -142,7 +142,7 @@ func (c *Client) RollingPatchDeploymentMigrations(namespace, name string, f oper if err != nil { return nil, false, err } - updated, err := c.AppsV1beta2().Deployments(namespace).Patch(name, types.StrategicMergePatchType, patchBytes) + updated, err := c.AppsV1().Deployments(namespace).Patch(name, types.StrategicMergePatchType, patchBytes) if err != nil { return nil, false, err } @@ -153,15 +153,15 @@ func (c *Client) RollingPatchDeploymentMigrations(namespace, name string, f oper return updated, current.GetResourceVersion() != updated.GetResourceVersion(), nil } -func checkDeploymentRollingUpdateEnabled(dep *appsv1beta2.Deployment) error { - enabled := dep.Spec.Strategy.Type == appsv1beta2.RollingUpdateDeploymentStrategyType || dep.Spec.Strategy.Type == "" // Deployments rolling update by default +func checkDeploymentRollingUpdateEnabled(dep *appsv1.Deployment) error { + enabled := dep.Spec.Strategy.Type == appsv1.RollingUpdateDeploymentStrategyType || dep.Spec.Strategy.Type == "" // Deployments rolling update by default if !enabled { return fmt.Errorf("Deployment %s/%s does not have rolling update strategy enabled", dep.GetNamespace(), dep.GetName()) } return nil } -func (c *Client) waitForDeploymentRollout(dep *appsv1beta2.Deployment) error { +func (c *Client) waitForDeploymentRollout(dep *appsv1.Deployment) error { return wait.PollInfinite(deploymentRolloutPollInterval, func() (bool, error) { d, err := c.GetDeployment(dep.Namespace, dep.Name) if err != nil { @@ -180,7 +180,7 @@ func (c *Client) waitForDeploymentRollout(dep *appsv1beta2.Deployment) error { // CreateOrRollingUpdateDeployment creates the Deployment if it doesn't exist. If the Deployment // already exists, it will update the Deployment and wait for it to rollout. Returns true if the // Deployment was created or updated, false if there was no update. -func (c *Client) CreateOrRollingUpdateDeployment(dep *appsv1beta2.Deployment) (*appsv1beta2.Deployment, bool, error) { +func (c *Client) CreateOrRollingUpdateDeployment(dep *appsv1.Deployment) (*appsv1.Deployment, bool, error) { glog.V(4).Infof("[CREATE OR ROLLING UPDATE Deployment]: %s:%s", dep.Namespace, dep.Name) _, err := c.GetDeployment(dep.Namespace, dep.Name) @@ -199,9 +199,9 @@ func (c *Client) CreateOrRollingUpdateDeployment(dep *appsv1beta2.Deployment) (* // ListDeploymentsWithLabels returns a list of deployments that matches the label selector. // An empty list will be returned if no such deployments is found. -func (c *Client) ListDeploymentsWithLabels(namespace string, labels labels.Set) (*appsv1beta2.DeploymentList, error) { +func (c *Client) ListDeploymentsWithLabels(namespace string, labels labels.Set) (*appsv1.DeploymentList, error) { glog.V(4).Infof("[LIST Deployments] in %s, labels: %v", namespace, labels) opts := metav1.ListOptions{LabelSelector: labels.String()} - return c.AppsV1beta2().Deployments(namespace).List(opts) + return c.AppsV1().Deployments(namespace).List(opts) } From 3e7cf1fcd4a9a2a3011625a20f0c92873f48ebc3 Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Mon, 6 Aug 2018 10:23:24 -0400 Subject: [PATCH 9/9] wip(olm): fix leaks --- pkg/controller/operators/olm/operator.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/controller/operators/olm/operator.go b/pkg/controller/operators/olm/operator.go index e71a61b43a..38b81a7681 100644 --- a/pkg/controller/operators/olm/operator.go +++ b/pkg/controller/operators/olm/operator.go @@ -287,6 +287,8 @@ func (a *Operator) transitionCSVState(in v1alpha1.ClusterServiceVersion) (out *v func (a *Operator) findIntermediatesForDeletion(csv *v1alpha1.ClusterServiceVersion) (csvs []*v1alpha1.ClusterServiceVersion) { csvsInNamespace := a.csvsInNamespace(csv.GetNamespace()) current := csv + + // isBeingReplaced returns a copy next := a.isBeingReplaced(current, csvsInNamespace) for next != nil { csvs = append(csvs, current) @@ -450,7 +452,7 @@ func (a *Operator) isBeingReplaced(in *v1alpha1.ClusterServiceVersion, csvsInNam log.Infof("checking %s", csv.GetName()) if csv.Spec.Replaces == in.GetName() { log.Infof("%s replaced by %s", in.GetName(), csv.GetName()) - replacedBy = csv + replacedBy = csv.DeepCopy() return } }