diff --git a/Documentation/design/building-your-csv.md b/Documentation/design/building-your-csv.md index 1db35edcc9..c888a51a3e 100644 --- a/Documentation/design/building-your-csv.md +++ b/Documentation/design/building-your-csv.md @@ -44,7 +44,7 @@ It’s common for your Operator to use multiple CRDs to link together concepts, **Name**: The full name of your CRD -The next two sections require more explanation. +The next two sections require more explanation. **Resources**: Your CRDs will own one or more types of Kubernetes objects. These are listed in the resources section to inform your end-users of the objects they might need to troubleshoot or how to connect to the application, such as the Service or Ingress rule that exposes a database. @@ -178,7 +178,7 @@ An APIService is uniquely identified by the group-version it provides and can be **Kind**: A kind that the APIService is expected to provide. -**DeploymentName**: +**DeploymentName**: Name of the deployment defined by your CSV that corresponds to your APIService (required for owned APIServices). During the CSV pending phase, the OLM Operator will search your CSV's InstallStrategy for a deployment spec with a matching name, and if not found, will not transition the CSV to the install ready phase. **Resources**: @@ -190,8 +190,8 @@ It’s recommended to only list out the objects that are important to a human, n Essentially the same as for owned CRDs. ### APIService Resource Creation -The Lifecycle Manage is responsible for creating or replacing the Service and APIService resources for each unique owned APIService. -* Service pod selectors are copied from the CSV deployment matching the APIServiceDescription's DeploymentName. +The Lifecycle Manage is responsible for creating or replacing the Service and APIService resources for each unique owned APIService. +* Service pod selectors are copied from the CSV deployment matching the APIServiceDescription's DeploymentName. * A new CA key/cert pair is generated for for each installation and the base64 encoded CA bundle is embedded in the respective APIService resource. ### APIService Serving Certs @@ -232,6 +232,8 @@ The metadata section contains general metadata around the name, version and othe **Description**: A markdown blob that describes the Operator. Important information to include: features, limitations and common use-cases for the Operator. If your Operator manages different types of installs, eg. standalone vs clustered, it is useful to give an overview of how each differs from each other, or which ones are supported for production use. +**MinKubeVersion**: A minimum version of Kubernetes that server is supposed to have so operator(s) can be deployed. + **Labels** (optional): Any key/value pairs used to organize and categorize this CSV object. **Selectors** (optional): A label selector to identify related resources. Set this to select on current labels applied to this CSV object (if applicable). diff --git a/deploy/chart/templates/0000_30_02-clusterserviceversion.crd.yaml b/deploy/chart/templates/0000_30_02-clusterserviceversion.crd.yaml index 22568d41aa..e3142c168b 100644 --- a/deploy/chart/templates/0000_30_02-clusterserviceversion.crd.yaml +++ b/deploy/chart/templates/0000_30_02-clusterserviceversion.crd.yaml @@ -60,6 +60,10 @@ spec: type: string description: Human readable description of what the application does + minKubeVersion: + type: string + description: Minimum kubernetes version requirement on the server to deploy operator + keywords: type: array description: List of keywords which will be used to discover and categorize app types @@ -198,7 +202,7 @@ spec: description: Version of the API resource kind: type: string - description: Kind of the API resource + description: Kind of the API resource apiservicedefinitions: type: object properties: diff --git a/deploy/chart/templates/_packageserver.clusterserviceversion.yaml b/deploy/chart/templates/_packageserver.clusterserviceversion.yaml index ecff591d5e..c321bb9d42 100644 --- a/deploy/chart/templates/_packageserver.clusterserviceversion.yaml +++ b/deploy/chart/templates/_packageserver.clusterserviceversion.yaml @@ -7,6 +7,7 @@ spec: displayName: Package Server description: Represents an Operator package that is available from a given CatalogSource which will resolve to a ClusterServiceVersion. + minKubeVersion: {{ .Values.minKubeVersion }} keywords: ['packagemanifests', 'olm', 'packages'] maintainers: - name: Red Hat diff --git a/deploy/chart/values.yaml b/deploy/chart/values.yaml index 587e512fa2..38aa99f798 100644 --- a/deploy/chart/values.yaml +++ b/deploy/chart/values.yaml @@ -2,6 +2,7 @@ rbacApiVersion: rbac.authorization.k8s.io namespace: operator-lifecycle-manager catalog_namespace: operator-lifecycle-manager operator_namespace: operators +minKubeVersion: 1.11 imagestream: false debug: false olm: diff --git a/manifests/0000_30_02-clusterserviceversion.crd.yaml b/manifests/0000_30_02-clusterserviceversion.crd.yaml index 55ec962996..ff3e6dac0d 100644 --- a/manifests/0000_30_02-clusterserviceversion.crd.yaml +++ b/manifests/0000_30_02-clusterserviceversion.crd.yaml @@ -62,6 +62,10 @@ spec: type: string description: Human readable description of what the application does + minKubeVersion: + type: string + description: Minimum kubernetes version requirement on the server to deploy operator + keywords: type: array description: List of keywords which will be used to discover and categorize app types @@ -200,7 +204,7 @@ spec: description: Version of the API resource kind: type: string - description: Kind of the API resource + description: Kind of the API resource apiservicedefinitions: type: object properties: diff --git a/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go b/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go index 4fd09f6a78..238c80234c 100644 --- a/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go +++ b/pkg/api/apis/operators/v1alpha1/clusterserviceversion_types.go @@ -132,6 +132,7 @@ type ClusterServiceVersionSpec struct { CustomResourceDefinitions CustomResourceDefinitions `json:"customresourcedefinitions,omitempty"` APIServiceDefinitions APIServiceDefinitions `json:"apiservicedefinitions,omitempty"` NativeAPIs []metav1.GroupVersionKind `json:"nativeAPIs,omitempty"` + MinKubeVersion string `json:"minKubeVersion,omitempty"` DisplayName string `json:"displayName"` Description string `json:"description,omitempty"` Keywords []string `json:"keywords,omitempty"` @@ -473,3 +474,4 @@ func (csv ClusterServiceVersion) GetOwnedAPIServiceDescriptions() []APIServiceDe return descs } + diff --git a/pkg/controller/operators/olm/operator_test.go b/pkg/controller/operators/olm/operator_test.go index 1acd52d97d..d27071d429 100644 --- a/pkg/controller/operators/olm/operator_test.go +++ b/pkg/controller/operators/olm/operator_test.go @@ -454,7 +454,7 @@ func installStrategy(deploymentName string, permissions []install.StrategyDeploy } func csv( - name, namespace, replaces string, + name, namespace, minKubeVersion, replaces string, installStrategy v1alpha1.NamedInstallStrategy, owned, required []*v1beta1.CustomResourceDefinition, phase v1alpha1.ClusterServiceVersionPhase, @@ -479,6 +479,7 @@ func csv( Namespace: namespace, }, Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: minKubeVersion, Replaces: replaces, InstallStrategy: installStrategy, InstallModes: []v1alpha1.InstallMode{ @@ -719,6 +720,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -739,6 +741,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{}, @@ -759,6 +762,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", v1alpha1.NamedInstallStrategy{"deployment", json.RawMessage{}}, []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -782,6 +786,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, @@ -826,6 +831,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -850,6 +856,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{}, @@ -873,6 +880,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{}, @@ -897,6 +905,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{}, @@ -921,6 +930,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("b1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -947,6 +957,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -955,6 +966,7 @@ func TestTransitionCSV(t *testing.T) { ), csv("csv2", namespace, + "0.0", "", installStrategy("csv2-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -985,6 +997,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withCertInfo(withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{}, @@ -993,6 +1006,7 @@ func TestTransitionCSV(t *testing.T) { ), apis("a1.v1.a1Kind"), nil), metav1.NewTime(time.Now().Add(24*time.Hour)), metav1.NewTime(time.Now())), withAPIServices(csv("csv2", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{}, @@ -1059,6 +1073,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1082,6 +1097,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1105,6 +1121,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", v1alpha1.NamedInstallStrategy{"deployment", json.RawMessage{}}, []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1128,6 +1145,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1151,6 +1169,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{}, @@ -1172,6 +1191,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1195,6 +1215,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1218,6 +1239,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withCertInfo(withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1285,6 +1307,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withCertInfo(withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1352,6 +1375,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withCertInfo(withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1419,6 +1443,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withCertInfo(withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1486,6 +1511,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withCertInfo(withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1553,6 +1579,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withCertInfo(withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1620,6 +1647,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withCertInfo(withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1687,6 +1715,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withCertInfo(withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1754,6 +1783,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withConditionReason(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1782,6 +1812,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withInstallModes(withConditionReason(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1817,6 +1848,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", v1alpha1.NamedInstallStrategy{"deployment", json.RawMessage{}}, []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1840,6 +1872,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1866,6 +1899,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ withAPIServices(csv("csv1", namespace, + "0.0", "", installStrategy("a1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1886,6 +1920,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1894,6 +1929,7 @@ func TestTransitionCSV(t *testing.T) { ), csv("csv2", namespace, + "0.0", "csv1", installStrategy("csv2-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1921,6 +1957,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1929,6 +1966,7 @@ func TestTransitionCSV(t *testing.T) { ), csv("csv2", namespace, + "0.0", "csv1", installStrategy("csv2-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1957,6 +1995,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1965,6 +2004,7 @@ func TestTransitionCSV(t *testing.T) { ), csv("csv2", namespace, + "0.0", "csv1", installStrategy("csv2-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -1994,6 +2034,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv3", namespace, + "0.0", "csv2", installStrategy("csv3-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -2002,6 +2043,7 @@ func TestTransitionCSV(t *testing.T) { ), csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -2010,6 +2052,7 @@ func TestTransitionCSV(t *testing.T) { ), csv("csv2", namespace, + "0.0", "csv1", installStrategy("csv2-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -2040,6 +2083,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv3", namespace, + "0.0", "csv2", installStrategy("csv3-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -2048,6 +2092,7 @@ func TestTransitionCSV(t *testing.T) { ), csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -2056,6 +2101,7 @@ func TestTransitionCSV(t *testing.T) { ), csv("csv2", namespace, + "0.0", "csv1", installStrategy("csv2-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -2086,6 +2132,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv2", namespace, + "0.0", "csv1", installStrategy("csv2-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -2094,6 +2141,7 @@ func TestTransitionCSV(t *testing.T) { ), csv("csv3", namespace, + "0.0", "csv2", installStrategy("csv3-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -2123,6 +2171,7 @@ func TestTransitionCSV(t *testing.T) { csvs: []runtime.Object{ csv("csv2", namespace, + "0.0", "csv1", installStrategy("csv2-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -2131,6 +2180,7 @@ func TestTransitionCSV(t *testing.T) { ), csv("csv3", namespace, + "0.0", "csv2", installStrategy("csv3-dep1", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v1")}, @@ -2255,6 +2305,7 @@ func TestSyncOperatorGroups(t *testing.T) { crd := crd("c1.fake.api.group", "v1") operatorCSV := csv("csv1", operatorNamespace, + "0.0", "", installStrategy("csv1-dep1", permissions, nil), []*v1beta1.CustomResourceDefinition{crd}, @@ -2781,7 +2832,7 @@ func TestIsReplacing(t *testing.T) { }{ { name: "QueryErr", - in: csv("name", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + in: csv("name", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), initial: initial{ csvs: []runtime.Object{}, }, @@ -2789,30 +2840,30 @@ func TestIsReplacing(t *testing.T) { }, { name: "CSVInCluster/NotReplacing", - in: csv("csv1", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + in: csv("csv1", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), initial: initial{ csvs: []runtime.Object{ - csv("csv1", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + csv("csv1", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), }, }, expected: nil, }, { name: "CSVInCluster/Replacing", - in: csv("csv2", namespace, "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + in: csv("csv2", namespace, "0.0", "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), initial: initial{ csvs: []runtime.Object{ - csv("csv1", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + csv("csv1", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), }, }, - expected: csv("csv1", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + expected: csv("csv1", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), }, { name: "CSVInCluster/ReplacingNotFound", - in: csv("csv2", namespace, "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + in: csv("csv2", namespace, "0.0", "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), initial: initial{ csvs: []runtime.Object{ - csv("csv3", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + csv("csv3", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), }, }, expected: nil, @@ -2845,28 +2896,28 @@ func TestIsBeingReplaced(t *testing.T) { }{ { name: "QueryErr", - in: csv("name", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + in: csv("name", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), expected: nil, }, { name: "CSVInCluster/NotReplacing", - in: csv("csv1", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + in: csv("csv1", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), initial: initial{ csvs: map[string]*v1alpha1.ClusterServiceVersion{ - "csv2": csv("csv2", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + "csv2": csv("csv2", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), }, }, expected: nil, }, { name: "CSVInCluster/Replacing", - in: csv("csv1", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + in: csv("csv1", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), initial: initial{ csvs: map[string]*v1alpha1.ClusterServiceVersion{ - "csv2": csv("csv2", namespace, "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + "csv2": csv("csv2", namespace, "0.0", "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), }, }, - expected: csv("csv2", namespace, "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + expected: csv("csv2", namespace, "0.0", "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), }, } for _, tt := range tests { @@ -2893,28 +2944,28 @@ func TestCheckReplacement(t *testing.T) { }{ { name: "QueryErr", - in: csv("name", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + in: csv("name", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), expected: nil, }, { name: "CSVInCluster/NotReplacing", - in: csv("csv1", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + in: csv("csv1", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), initial: initial{ csvs: map[string]*v1alpha1.ClusterServiceVersion{ - "csv2": csv("csv2", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + "csv2": csv("csv2", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), }, }, expected: nil, }, { name: "CSVInCluster/Replacing", - in: csv("csv1", namespace, "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + in: csv("csv1", namespace, "0.0", "", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), initial: initial{ csvs: map[string]*v1alpha1.ClusterServiceVersion{ - "csv2": csv("csv2", namespace, "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + "csv2": csv("csv2", namespace, "0.0", "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), }, }, - expected: csv("csv2", namespace, "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), + expected: csv("csv2", namespace, "0.0", "csv1", installStrategy("dep", nil, nil), nil, nil, v1alpha1.CSVPhaseSucceeded), }, } for _, tt := range tests { diff --git a/pkg/controller/operators/olm/requirements.go b/pkg/controller/operators/olm/requirements.go index a4e7b982a1..99988b7908 100644 --- a/pkg/controller/operators/olm/requirements.go +++ b/pkg/controller/operators/olm/requirements.go @@ -3,9 +3,11 @@ package olm import ( "encoding/json" "fmt" + "strconv" "github.com/sirupsen/logrus" + "github.com/coreos/go-semver/semver" "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1" olmErrors "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/errors" "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install" @@ -13,6 +15,58 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func (a *Operator) minKubeVersionStatus(name string, minKubeVersion string) (met bool, statuses []v1alpha1.RequirementStatus) { + status := v1alpha1.RequirementStatus{ + Group: "operators.coreos.com", + Version: "v1alpha1", + Kind: "ClusterServiceVersion", + Name: name, + } + + if minKubeVersion == "" { + status.Status = v1alpha1.RequirementStatusReasonNotPresent + status.Message = "CSV missing minimum kube version specification" + met = true + statuses = append(statuses, status) + return + } + + // Retrieve server k8s version + serverVersionInfo, err := a.OpClient.KubernetesInterface().Discovery().ServerVersion() + if err != nil { + status.Status = v1alpha1.RequirementStatusReasonPresentNotSatisfied + status.Message = "Server version discovery error" + met = false + statuses = append(statuses, status) + } + + // copy necessary fields into comparable for semver + majorInt, err := strconv.ParseInt(serverVersionInfo.Major, 10, 64) + minorInt, err := strconv.ParseInt(serverVersionInfo.Minor, 10, 64) + + serverVersionComparable := semver.Version{ + Major: majorInt, + Minor: minorInt, + } + + csvVersionInfo, err := semver.NewVersion(minKubeVersion) + if err != nil { + status.Status = v1alpha1.RequirementStatusReasonPresentNotSatisfied + status.Message = "CSV version parsing error" + met = false + statuses = append(statuses, status) + } + + if csvVersionInfo.Compare(serverVersionComparable) < 0 { + status.Status = v1alpha1.RequirementStatusReasonPresentNotSatisfied + status.Message = "CSV version requirement not met" + met = false + statuses = append(statuses, status) + } + + return +} + func (a *Operator) requirementStatus(strategyDetailsDeployment *install.StrategyDetailsDeployment, crdDescs []v1alpha1.CRDDescription, ownedAPIServiceDescs []v1alpha1.APIServiceDescription, requiredAPIServiceDescs []v1alpha1.APIServiceDescription, requiredNativeAPIs []metav1.GroupVersionKind) (met bool, statuses []v1alpha1.RequirementStatus) { @@ -292,7 +346,10 @@ func (a *Operator) requirementAndPermissionStatus(csv *v1alpha1.ClusterServiceVe return false, nil, fmt.Errorf("could not cast install strategy as type %T", strategyDetailsDeployment) } + // Check kubernetes version requirement between CSV and server + minKubeMet, minKubeStatus := a.minKubeVersionStatus(csv.Spec.DisplayName, csv.Spec.MinKubeVersion) reqMet, reqStatuses := a.requirementStatus(strategyDetailsDeployment, csv.GetAllCRDDescriptions(), csv.GetOwnedAPIServiceDescriptions(), csv.GetRequiredAPIServiceDescriptions(), csv.Spec.NativeAPIs) + allReqStatuses := append(minKubeStatus, reqStatuses...) rbacLister := a.lister.RbacV1() roleLister := rbacLister.RoleLister() @@ -307,10 +364,10 @@ func (a *Operator) requirementAndPermissionStatus(csv *v1alpha1.ClusterServiceVe } // Aggregate requirement and permissions statuses - statuses := append(reqStatuses, permStatuses...) - met := reqMet && permMet + statuses := append(allReqStatuses, permStatuses...) + met := minKubeMet && reqMet && permMet if !met { - a.Log.WithField("reqMet", reqMet).WithField("permMet", permMet).Debug("permissions not met") + a.Log.WithField("minKubeMet", minKubeMet).WithField("reqMet", reqMet).WithField("permMet", permMet).Debug("permissions/requirements not met") } return met, statuses, nil diff --git a/pkg/controller/operators/olm/requirements_test.go b/pkg/controller/operators/olm/requirements_test.go index 135d26cd3e..e397926428 100644 --- a/pkg/controller/operators/olm/requirements_test.go +++ b/pkg/controller/operators/olm/requirements_test.go @@ -38,6 +38,7 @@ func TestRequirementAndPermissionStatus(t *testing.T) { description: "BadInstallStrategy", csv: csv("csv1", namespace, + "0.0", "", v1alpha1.NamedInstallStrategy{"deployment", json.RawMessage{}}, nil, @@ -54,6 +55,7 @@ func TestRequirementAndPermissionStatus(t *testing.T) { description: "AllPermissionsMet", csv: csv("csv1", namespace, + "0.0", "", installStrategy( "csv1-dep", @@ -184,6 +186,7 @@ func TestRequirementAndPermissionStatus(t *testing.T) { description: "OnePermissionNotMet", csv: csv("csv1", namespace, + "0.0", "", installStrategy( "csv1-dep", @@ -314,6 +317,7 @@ func TestRequirementAndPermissionStatus(t *testing.T) { description: "AllRequirementsMet", csv: csv("csv1", namespace, + "0.0", "", installStrategy( "csv1-dep", @@ -431,6 +435,7 @@ func TestRequirementAndPermissionStatus(t *testing.T) { description: "RequirementNotMet/NonServedCRDVersion", csv: csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v2")}, @@ -457,6 +462,7 @@ func TestRequirementAndPermissionStatus(t *testing.T) { description: "RequirementNotMet/NotEstablishedCRDVersion", csv: csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "version-not-found")}, @@ -483,6 +489,7 @@ func TestRequirementAndPermissionStatus(t *testing.T) { description: "RequirementNotMet/NamesConflictedCRD", csv: csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v2")}, @@ -515,6 +522,7 @@ func TestRequirementAndPermissionStatus(t *testing.T) { description: "RequirementNotMet/CRDResourceInactive", csv: csv("csv1", namespace, + "0.0", "", installStrategy("csv1-dep", nil, nil), []*v1beta1.CustomResourceDefinition{crd("c1", "v2")}, diff --git a/test/e2e/csv_e2e_test.go b/test/e2e/csv_e2e_test.go index d4dce434db..4461f0c194 100644 --- a/test/e2e/csv_e2e_test.go +++ b/test/e2e/csv_e2e_test.go @@ -255,6 +255,68 @@ func waitForCSVToDelete(t *testing.T, c versioned.Interface, name string) error return err } +func TestCreateCSVWithUnmetRequirementsMinKubeVersion(t *testing.T) { + defer cleaner.NotifyTestComplete(t, true) + + c := newKubeClient(t) + crc := newCRClient(t) + + depName := genName("dep-") + csv := v1alpha1.ClusterServiceVersion{ + TypeMeta: metav1.TypeMeta{ + Kind: v1alpha1.ClusterServiceVersionKind, + APIVersion: v1alpha1.ClusterServiceVersionAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: genName("csv"), + }, + Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "999.999", + InstallModes: []v1alpha1.InstallMode{ + { + Type: v1alpha1.InstallModeTypeOwnNamespace, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeSingleNamespace, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeMultiNamespace, + Supported: true, + }, + { + Type: v1alpha1.InstallModeTypeAllNamespaces, + Supported: true, + }, + }, + InstallStrategy: newNginxInstallStrategy(depName, nil, nil), + CustomResourceDefinitions: v1alpha1.CustomResourceDefinitions{ + Owned: []v1alpha1.CRDDescription{ + { + DisplayName: "Not In Cluster", + Description: "A CRD that is not currently in the cluster", + Name: "not.in.cluster.com", + Version: "v1alpha1", + Kind: "NotInCluster", + }, + }, + }, + }, + } + + cleanupCSV, err := createCSV(t, c, crc, csv, testNamespace, false, false) + require.NoError(t, err) + defer cleanupCSV() + + _, err = fetchCSV(t, crc, csv.Name, testNamespace, csvPendingChecker) + require.NoError(t, err) + + // Shouldn't create deployment + _, err = c.GetDeployment(testNamespace, depName) + require.Error(t, err) +} + // TODO: same test but missing serviceaccount instead func TestCreateCSVWithUnmetRequirementsCRD(t *testing.T) { defer cleaner.NotifyTestComplete(t, true) @@ -272,6 +334,7 @@ func TestCreateCSVWithUnmetRequirementsCRD(t *testing.T) { Name: genName("csv"), }, Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0", InstallModes: []v1alpha1.InstallMode{ { Type: v1alpha1.InstallModeTypeOwnNamespace, @@ -444,6 +507,7 @@ func TestCreateCSVWithUnmetRequirementsAPIService(t *testing.T) { Name: genName("csv"), }, Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0", InstallModes: []v1alpha1.InstallMode{ { Type: v1alpha1.InstallModeTypeOwnNamespace, @@ -532,6 +596,7 @@ func TestCreateCSVWithUnmetPermissionsAPIService(t *testing.T) { Name: genName("csv"), }, Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0", InstallModes: []v1alpha1.InstallMode{ { Type: v1alpha1.InstallModeTypeOwnNamespace, @@ -594,6 +659,7 @@ func TestCreateCSVWithUnmetRequirementsNativeAPI(t *testing.T) { Name: genName("csv"), }, Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0", InstallModes: []v1alpha1.InstallMode{ { Type: v1alpha1.InstallModeTypeOwnNamespace, @@ -684,6 +750,7 @@ func TestCreateCSVRequirementsMetCRD(t *testing.T) { Name: genName("csv"), }, Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0", InstallModes: []v1alpha1.InstallMode{ { Type: v1alpha1.InstallModeTypeOwnNamespace, @@ -939,6 +1006,7 @@ func TestCreateCSVRequirementsMetAPIService(t *testing.T) { Name: genName("csv"), }, Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0", InstallModes: []v1alpha1.InstallMode{ { Type: v1alpha1.InstallModeTypeOwnNamespace, @@ -1094,6 +1162,7 @@ func TestCreateCSVWithOwnedAPIService(t *testing.T) { csv := v1alpha1.ClusterServiceVersion{ Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0", InstallModes: []v1alpha1.InstallMode{ { Type: v1alpha1.InstallModeTypeOwnNamespace, @@ -1268,6 +1337,7 @@ func TestUpdateCSVSameDeploymentName(t *testing.T) { Name: genName("csv"), }, Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0", InstallModes: []v1alpha1.InstallMode{ { Type: v1alpha1.InstallModeTypeOwnNamespace, @@ -1450,6 +1520,7 @@ func TestUpdateCSVDifferentDeploymentName(t *testing.T) { Name: genName("csv"), }, Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0", InstallModes: []v1alpha1.InstallMode{ { Type: v1alpha1.InstallModeTypeOwnNamespace, @@ -1637,6 +1708,7 @@ func TestUpdateCSVMultipleIntermediates(t *testing.T) { Name: genName("csv"), }, Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0", InstallModes: []v1alpha1.InstallMode{ { Type: v1alpha1.InstallModeTypeOwnNamespace, @@ -1830,6 +1902,7 @@ func TestUpdateCSVMultipleVersionCRD(t *testing.T) { Name: genName("csv"), }, Spec: v1alpha1.ClusterServiceVersionSpec{ + MinKubeVersion: "0.0", InstallModes: []v1alpha1.InstallMode{ { Type: v1alpha1.InstallModeTypeOwnNamespace,