From 8e559616967789d9a88d0e3fa0bff86cb1c46af6 Mon Sep 17 00:00:00 2001 From: Josh Manning <19478595+jsm84@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:42:48 -0400 Subject: [PATCH] Implement API changes according to RFC spec resolves #1088 Summary: * v1 API now uses a `SourceConfig` discriminated union which will allow modularity for future install sources (bundles, charts, etc). * `SourceConfig` uses CEL validation to ensure only valid field names & values are utilized (`sourceType: Catalog` ensures that the `catalog` field is also set in `SourceConfig`). * Added new `clusterextension_admission` unit test for `SourceConfig` objects. The test covers both valid and invalid cases. * Fixed `clusterextension_controller` test where an unset `ClusterExtension` spec caused a null pointer deref. * Moved `ClusterSelector` from `ClusterExtension.Spec` to `ClusterExtension.Source.Catalog` and renamed to `Selector`. * Updated GoDocs to reflect the new API spec and included post-review changes * Fixed all definitions of `kind: ClusterExtension` in docs and scripts to reflect the API changes. Signed-off-by: Josh Manning <19478595+jsm84@users.noreply.github.com> --- api/v1alpha1/clusterextension_types.go | 143 +++-- api/v1alpha1/zz_generated.deepcopy.go | 38 +- ...peratorframework.io_clusterextensions.yaml | 493 ++++++++++-------- .../olm_v1alpha1_clusterextension.yaml | 7 +- docs/Tasks/installing-an-extension.md | 9 +- docs/drafts/upgrade-support.md | 9 +- docs/refs/crd-upgrade-safety.md | 11 +- hack/test/pre-upgrade-setup.sh | 7 +- .../clusterextension_admission_test.go | 111 +++- .../clusterextension_controller.go | 2 +- .../clusterextension_controller_test.go | 170 ++++-- internal/resolve/catalog.go | 10 +- internal/resolve/catalog_test.go | 59 ++- test/e2e/cluster_extension_install_test.go | 121 +++-- .../extension_developer_test.go | 7 +- test/upgrade-e2e/post_upgrade_test.go | 2 +- 16 files changed, 796 insertions(+), 403 deletions(-) diff --git a/api/v1alpha1/clusterextension_types.go b/api/v1alpha1/clusterextension_types.go index 55f13ab66..5ea68948c 100644 --- a/api/v1alpha1/clusterextension_types.go +++ b/api/v1alpha1/clusterextension_types.go @@ -46,6 +46,97 @@ const ( // ClusterExtensionSpec defines the desired state of ClusterExtension type ClusterExtensionSpec struct { + // source is a required field which selects the installation source of content + // for this ClusterExtension. Selection is performed by setting the sourceType. + // + // Catalog is currently the only implemented sourceType, and setting the + // sourcetype to "Catalog" requires the catalog field to also be defined. + // + // Below is a minimal example of a source definition (in yaml): + // + // source: + // sourceType: Catalog + // catalog: + // packageName: example-package + // + Source SourceConfig `json:"source"` + + // installNamespace is a reference to the Namespace in which the bundle of + // content for the package referenced in the packageName field will be applied. + // The bundle may contain cluster-scoped resources or resources that are + // applied to other Namespaces. This Namespace is expected to exist. + // + // installNamespace is required, immutable, and follows the DNS label standard + // as defined in RFC 1123. This means that valid values: + // - Contain no more than 63 characters + // - Contain only lowercase alphanumeric characters or '-' + // - Start with an alphanumeric character + // - End with an alphanumeric character + // + // Some examples of valid values are: + // - some-namespace + // - 123-namespace + // - 1-namespace-2 + // - somenamespace + // + // Some examples of invalid values are: + // - -some-namespace + // - some-namespace- + // - thisisareallylongnamespacenamethatisgreaterthanthemaximumlength + // - some.namespace + // + //+kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + //+kubebuilder:validation:MaxLength:=63 + //+kubebuilder:validation:XValidation:rule="self == oldSelf",message="installNamespace is immutable" + InstallNamespace string `json:"installNamespace"` + + // preflight is an optional field that can be used to configure the preflight checks run before installation or upgrade of the content for the package specified in the packageName field. + // + // When specified, it overrides the default configuration of the preflight checks that are required to execute successfully during an install/upgrade operation. + // + // When not specified, the default configuration for each preflight check will be used. + // + //+optional + Preflight *PreflightConfig `json:"preflight,omitempty"` + + // serviceAccount is a required reference to a ServiceAccount that exists + // in the installNamespace. The provided ServiceAccount is used to install and + // manage the content for the package specified in the packageName field. + // + // In order to successfully install and manage the content for the package, + // the ServiceAccount provided via this field should be configured with the + // appropriate permissions to perform the necessary operations on all the + // resources that are included in the bundle of content being applied. + ServiceAccount ServiceAccountReference `json:"serviceAccount"` +} + +const SourceTypeCatalog = "Catalog" + +// SourceConfig is a discriminated union which selects the installation source. +// +union +// +kubebuilder:validation:XValidation:rule="self.sourceType == 'Catalog' && has(self.catalog)",message="sourceType Catalog requires catalog field" +type SourceConfig struct { + // sourceType is a required reference to the type of install source. + // + // Allowed values are ["Catalog"] + // + // When this field is set to "Catalog", information for determining the appropriate + // bundle of content to install will be fetched from ClusterCatalog resources existing + // on the cluster. When using the Catalog sourceType, the catalog field must also be set. + // + // +unionDiscriminator + // +kubebuilder:validation:Enum:="Catalog" + SourceType string `json:"sourceType"` + + // catalog is used to configure how information is sourced from a catalog. This field must be defined when sourceType is set to "Catalog", + // and must be the only field defined for this sourceType. + // + // +optional. + Catalog *CatalogSource `json:"catalog,omitempty"` +} + +// CatalogSource defines the required fields for catalog source. +type CatalogSource struct { // packageName is a reference to the name of the package to be installed // and is used to filter the content from catalogs. // @@ -196,7 +287,7 @@ type ClusterExtensionSpec struct { //+optional Channel string `json:"channel,omitempty"` - // catalogSelector is an optional field that can be used + // selector is an optional field that can be used // to filter the set of ClusterCatalogs used in the bundle // selection process. // @@ -204,7 +295,7 @@ type ClusterExtensionSpec struct { // the bundle selection process. // //+optional - CatalogSelector metav1.LabelSelector `json:"catalogSelector,omitempty"` + Selector metav1.LabelSelector `json:"selector,omitempty"` // upgradeConstraintPolicy is an optional field that controls whether // the upgrade path(s) defined in the catalog are enforced for the package @@ -228,54 +319,6 @@ type ClusterExtensionSpec struct { //+kubebuilder:default:=Enforce //+optional UpgradeConstraintPolicy UpgradeConstraintPolicy `json:"upgradeConstraintPolicy,omitempty"` - - // installNamespace is a reference to the Namespace in which the bundle of - // content for the package referenced in the packageName field will be applied. - // The bundle may contain cluster-scoped resources or resources that are - // applied to other Namespaces. This Namespace is expected to exist. - // - // installNamespace is required, immutable, and follows the DNS label standard - // as defined in RFC 1123. This means that valid values: - // - Contain no more than 63 characters - // - Contain only lowercase alphanumeric characters or '-' - // - Start with an alphanumeric character - // - End with an alphanumeric character - // - // Some examples of valid values are: - // - some-namespace - // - 123-namespace - // - 1-namespace-2 - // - somenamespace - // - // Some examples of invalid values are: - // - -some-namespace - // - some-namespace- - // - thisisareallylongnamespacenamethatisgreaterthanthemaximumlength - // - some.namespace - // - //+kubebuilder:validation:Pattern:=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - //+kubebuilder:validation:MaxLength:=63 - //+kubebuilder:validation:XValidation:rule="self == oldSelf",message="installNamespace is immutable" - InstallNamespace string `json:"installNamespace"` - - // preflight is an optional field that can be used to configure the preflight checks run before installation or upgrade of the content for the package specified in the packageName field. - // - // When specified, it overrides the default configuration of the preflight checks that are required to execute successfully during an install/upgrade operation. - // - // When not specified, the default configuration for each preflight check will be used. - // - //+optional - Preflight *PreflightConfig `json:"preflight,omitempty"` - - // serviceAccount is a required reference to a ServiceAccount that exists - // in the installNamespace. The provided ServiceAccount is used to install and - // manage the content for the package specified in the packageName field. - // - // In order to successfully install and manage the content for the package, - // the ServiceAccount provided via this field should be configured with the - // appropriate permissions to perform the necessary operations on all the - // resources that are included in the bundle of content being applied. - ServiceAccount ServiceAccountReference `json:"serviceAccount"` } // ServiceAccountReference references a serviceAccount. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 6b40fb2df..8e86e1dba 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -55,6 +55,22 @@ func (in *CRDUpgradeSafetyPreflightConfig) DeepCopy() *CRDUpgradeSafetyPreflight return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CatalogSource) DeepCopyInto(out *CatalogSource) { + *out = *in + in.Selector.DeepCopyInto(&out.Selector) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CatalogSource. +func (in *CatalogSource) DeepCopy() *CatalogSource { + if in == nil { + return nil + } + out := new(CatalogSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterExtension) DeepCopyInto(out *ClusterExtension) { *out = *in @@ -117,7 +133,7 @@ func (in *ClusterExtensionList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterExtensionSpec) DeepCopyInto(out *ClusterExtensionSpec) { *out = *in - in.CatalogSelector.DeepCopyInto(&out.CatalogSelector) + in.Source.DeepCopyInto(&out.Source) if in.Preflight != nil { in, out := &in.Preflight, &out.Preflight *out = new(PreflightConfig) @@ -202,3 +218,23 @@ func (in *ServiceAccountReference) DeepCopy() *ServiceAccountReference { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SourceConfig) DeepCopyInto(out *SourceConfig) { + *out = *in + if in.Catalog != nil { + in, out := &in.Catalog, &out.Catalog + *out = new(CatalogSource) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceConfig. +func (in *SourceConfig) DeepCopy() *SourceConfig { + if in == nil { + return nil + } + out := new(SourceConfig) + in.DeepCopyInto(out) + return out +} diff --git a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml index fee64aa6a..651e1b11e 100644 --- a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml +++ b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml @@ -39,100 +39,6 @@ spec: spec: description: ClusterExtensionSpec defines the desired state of ClusterExtension properties: - catalogSelector: - description: |- - catalogSelector is an optional field that can be used - to filter the set of ClusterCatalogs used in the bundle - selection process. - - When unspecified, all ClusterCatalogs will be used in - the bundle selection process. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. - The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies - to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - channel: - description: |- - channel is an optional reference to a channel belonging to - the package specified in the packageName field. - - A "channel" is a package author defined stream of updates for an extension. - - When specified, it is used to constrain the set of installable bundles and - the automated upgrade path. This constraint is an AND operation with the - version field. For example: - - Given channel is set to "foo" - - Given version is set to ">=1.0.0, <1.5.0" - - Only bundles that exist in channel "foo" AND satisfy the version range comparison will be considered installable - - Automatic upgrades will be constrained to upgrade edges defined by the selected channel - - When unspecified, upgrade edges across all channels will be used to identify valid automatic upgrade paths. - - This field follows the DNS subdomain name standard as defined in RFC - 1123, with a deviation in the maximum length being no more than 48 - characters. This means that valid values: - - Contain no more than 48 characters - - Contain only lowercase alphanumeric characters, '-', or '.' - - Start with an alphanumeric character - - End with an alphanumeric character - - Some examples of valid values are: - - 1.1.x - - alpha - - stable - - stable-v1 - - v1-stable - - dev-preview - - preview - - community - - Some examples of invalid values are: - - -some-channel - - some-channel- - - thisisareallylongchannelnamethatisgreaterthanthemaximumlength - maxLength: 48 - pattern: ^[a-z0-9]+([\.-][a-z0-9]+)*$ - type: string installNamespace: description: |- installNamespace is a reference to the Namespace in which the bundle of @@ -164,36 +70,6 @@ spec: x-kubernetes-validations: - message: installNamespace is immutable rule: self == oldSelf - packageName: - description: |- - packageName is a reference to the name of the package to be installed - and is used to filter the content from catalogs. - - This field is required, immutable and follows the DNS label standard as defined in RFC - 1123, with a deviation in the maximum length being no more than 48 - characters. This means that valid values: - - Contain no more than 48 characters - - Contain only lowercase alphanumeric characters or '-' - - Start with an alphanumeric character - - End with an alphanumeric character - - Some examples of valid values are: - - some-package - - 123-package - - 1-package-2 - - somepackage - - Some examples of invalid values are: - - -some-package - - some-package- - - thisisareallylongpackagenamethatisgreaterthanthemaximumlength - - some.package - maxLength: 48 - pattern: ^[a-z0-9]+(-[a-z0-9]+)*$ - type: string - x-kubernetes-validations: - - message: packageName is immutable - rule: self == oldSelf preflight: description: |- preflight is an optional field that can be used to configure the preflight checks run before installation or upgrade of the content for the package specified in the packageName field. @@ -283,112 +159,277 @@ spec: required: - name type: object - upgradeConstraintPolicy: - default: Enforce - description: |- - upgradeConstraintPolicy is an optional field that controls whether - the upgrade path(s) defined in the catalog are enforced for the package - referenced in the packageName field. - - Allowed values are: ["Enforce", "Ignore"]. - - When this field is set to "Enforce", automatic upgrades will only occur - when upgrade constraints specified by the package author are met. - - When this field is set to "Ignore", the upgrade constraints specified by - the package author are ignored. This allows for upgrades and downgrades to - any version of the package. This is considered a dangerous operation as it - can lead to unknown and potentially disastrous outcomes, such as data - loss. It is assumed that users have independently verified changes when - using this option. - - If unspecified, the default value is "Enforce". - enum: - - Enforce - - Ignore - type: string - version: + source: description: |- - version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed. - - Acceptable version ranges are no longer than 64 characters. - Version ranges are composed of comma- or space-delimited values and one or - more comparison operators, known as comparison strings. Additional - comparison strings can be added using the OR operator (||). - - # Range Comparisons - - To specify a version range, you can use a comparison string like ">=3.0, - <3.6". When specifying a range, automatic updates will occur within that - range. The example comparison string means "install any version greater than - or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any - upgrades are available within the version range after initial installation, - those upgrades should be automatically performed. - - # Pinned Versions - - To specify an exact version to install you can use a version range that - "pins" to a specific version. When pinning to a specific version, no - automatic updates will occur. An example of a pinned version range is - "0.6.0", which means "only install version 0.6.0 and never - upgrade from this version". - - # Basic Comparison Operators - - The basic comparison operators and their meanings are: - - "=", equal (not aliased to an operator) - - "!=", not equal - - "<", less than - - ">", greater than - - ">=", greater than OR equal to - - "<=", less than OR equal to - - # Wildcard Comparisons - - You can use the "x", "X", and "*" characters as wildcard characters in all - comparison operations. Some examples of using the wildcard characters: - - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0" - - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0" - - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3" - - "x", "X", and "*" is equivalent to ">= 0.0.0" - - # Patch Release Comparisons - - When you want to specify a minor version up to the next major version you - can use the "~" character to perform patch comparisons. Some examples: - - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0" - - "~1" and "~1.x" is equivalent to ">=1, <2" - - "~2.3" is equivalent to ">=2.3, <2.4" - - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0" - - # Major Release Comparisons - - You can use the "^" character to make major release comparisons after a - stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples: - - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0" - - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0" - - "^2.3" is equivalent to ">=2.3, <3" - - "^2.x" is equivalent to ">=2.0.0, <3" - - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0" - - "^0.2" is equivalent to ">=0.2.0, <0.3.0" - - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4" - - "^0.0" is equivalent to ">=0.0.0, <0.1.0" - - "^0" is equivalent to ">=0.0.0, <1.0.0" - - # OR Comparisons - You can use the "||" character to represent an OR operation in the version - range. Some examples: - - ">=1.2.3, <2.0.0 || >3.0.0" - - "^0 || ^3 || ^5" - - For more information on semver, please see https://semver.org/ - maxLength: 64 - pattern: ^(\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\^)\s*(v?(0|[1-9]\d*|[x|X|\*])(\.(0|[1-9]\d*|x|X|\*]))?(\.(0|[1-9]\d*|x|X|\*))?(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?)\s*)((?:\s+|,\s*|\s*\|\|\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\^)\s*(v?(0|[1-9]\d*|x|X|\*])(\.(0|[1-9]\d*|x|X|\*))?(\.(0|[1-9]\d*|x|X|\*]))?(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?)\s*)*$ - type: string + source is a required field which selects the installation source of content + for this ClusterExtension. Selection is performed by setting the sourceType. + + Catalog is currently the only implemented sourceType, and setting the + sourcetype to "Catalog" requires the catalog field to also be defined. + + Below is a minimal example of a source definition (in yaml): + + source: + sourceType: Catalog + catalog: + packageName: example-package + properties: + catalog: + description: |- + catalog is used to configure how information is sourced from a catalog. This field must be defined when sourceType is set to "Catalog", + and must be the only field defined for this sourceType. + properties: + channel: + description: |- + channel is an optional reference to a channel belonging to + the package specified in the packageName field. + + A "channel" is a package author defined stream of updates for an extension. + + When specified, it is used to constrain the set of installable bundles and + the automated upgrade path. This constraint is an AND operation with the + version field. For example: + - Given channel is set to "foo" + - Given version is set to ">=1.0.0, <1.5.0" + - Only bundles that exist in channel "foo" AND satisfy the version range comparison will be considered installable + - Automatic upgrades will be constrained to upgrade edges defined by the selected channel + + When unspecified, upgrade edges across all channels will be used to identify valid automatic upgrade paths. + + This field follows the DNS subdomain name standard as defined in RFC + 1123, with a deviation in the maximum length being no more than 48 + characters. This means that valid values: + - Contain no more than 48 characters + - Contain only lowercase alphanumeric characters, '-', or '.' + - Start with an alphanumeric character + - End with an alphanumeric character + + Some examples of valid values are: + - 1.1.x + - alpha + - stable + - stable-v1 + - v1-stable + - dev-preview + - preview + - community + + Some examples of invalid values are: + - -some-channel + - some-channel- + - thisisareallylongchannelnamethatisgreaterthanthemaximumlength + maxLength: 48 + pattern: ^[a-z0-9]+([\.-][a-z0-9]+)*$ + type: string + packageName: + description: |- + packageName is a reference to the name of the package to be installed + and is used to filter the content from catalogs. + + This field is required, immutable and follows the DNS label standard as defined in RFC + 1123, with a deviation in the maximum length being no more than 48 + characters. This means that valid values: + - Contain no more than 48 characters + - Contain only lowercase alphanumeric characters or '-' + - Start with an alphanumeric character + - End with an alphanumeric character + + Some examples of valid values are: + - some-package + - 123-package + - 1-package-2 + - somepackage + + Some examples of invalid values are: + - -some-package + - some-package- + - thisisareallylongpackagenamethatisgreaterthanthemaximumlength + - some.package + maxLength: 48 + pattern: ^[a-z0-9]+(-[a-z0-9]+)*$ + type: string + x-kubernetes-validations: + - message: packageName is immutable + rule: self == oldSelf + selector: + description: |- + selector is an optional field that can be used + to filter the set of ClusterCatalogs used in the bundle + selection process. + + When unspecified, all ClusterCatalogs will be used in + the bundle selection process. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + upgradeConstraintPolicy: + default: Enforce + description: |- + upgradeConstraintPolicy is an optional field that controls whether + the upgrade path(s) defined in the catalog are enforced for the package + referenced in the packageName field. + + Allowed values are: ["Enforce", "Ignore"]. + + When this field is set to "Enforce", automatic upgrades will only occur + when upgrade constraints specified by the package author are met. + + When this field is set to "Ignore", the upgrade constraints specified by + the package author are ignored. This allows for upgrades and downgrades to + any version of the package. This is considered a dangerous operation as it + can lead to unknown and potentially disastrous outcomes, such as data + loss. It is assumed that users have independently verified changes when + using this option. + + If unspecified, the default value is "Enforce". + enum: + - Enforce + - Ignore + type: string + version: + description: |- + version is an optional semver constraint (a specific version or range of versions). When unspecified, the latest version available will be installed. + + Acceptable version ranges are no longer than 64 characters. + Version ranges are composed of comma- or space-delimited values and one or + more comparison operators, known as comparison strings. Additional + comparison strings can be added using the OR operator (||). + + # Range Comparisons + + To specify a version range, you can use a comparison string like ">=3.0, + <3.6". When specifying a range, automatic updates will occur within that + range. The example comparison string means "install any version greater than + or equal to 3.0.0 but less than 3.6.0.". It also states intent that if any + upgrades are available within the version range after initial installation, + those upgrades should be automatically performed. + + # Pinned Versions + + To specify an exact version to install you can use a version range that + "pins" to a specific version. When pinning to a specific version, no + automatic updates will occur. An example of a pinned version range is + "0.6.0", which means "only install version 0.6.0 and never + upgrade from this version". + + # Basic Comparison Operators + + The basic comparison operators and their meanings are: + - "=", equal (not aliased to an operator) + - "!=", not equal + - "<", less than + - ">", greater than + - ">=", greater than OR equal to + - "<=", less than OR equal to + + # Wildcard Comparisons + + You can use the "x", "X", and "*" characters as wildcard characters in all + comparison operations. Some examples of using the wildcard characters: + - "1.2.x", "1.2.X", and "1.2.*" is equivalent to ">=1.2.0, < 1.3.0" + - ">= 1.2.x", ">= 1.2.X", and ">= 1.2.*" is equivalent to ">= 1.2.0" + - "<= 2.x", "<= 2.X", and "<= 2.*" is equivalent to "< 3" + - "x", "X", and "*" is equivalent to ">= 0.0.0" + + # Patch Release Comparisons + + When you want to specify a minor version up to the next major version you + can use the "~" character to perform patch comparisons. Some examples: + - "~1.2.3" is equivalent to ">=1.2.3, <1.3.0" + - "~1" and "~1.x" is equivalent to ">=1, <2" + - "~2.3" is equivalent to ">=2.3, <2.4" + - "~1.2.x" is equivalent to ">=1.2.0, <1.3.0" + + # Major Release Comparisons + + You can use the "^" character to make major release comparisons after a + stable 1.0.0 version is published. If there is no stable version published, // minor versions define the stability level. Some examples: + - "^1.2.3" is equivalent to ">=1.2.3, <2.0.0" + - "^1.2.x" is equivalent to ">=1.2.0, <2.0.0" + - "^2.3" is equivalent to ">=2.3, <3" + - "^2.x" is equivalent to ">=2.0.0, <3" + - "^0.2.3" is equivalent to ">=0.2.3, <0.3.0" + - "^0.2" is equivalent to ">=0.2.0, <0.3.0" + - "^0.0.3" is equvalent to ">=0.0.3, <0.0.4" + - "^0.0" is equivalent to ">=0.0.0, <0.1.0" + - "^0" is equivalent to ">=0.0.0, <1.0.0" + + # OR Comparisons + You can use the "||" character to represent an OR operation in the version + range. Some examples: + - ">=1.2.3, <2.0.0 || >3.0.0" + - "^0 || ^3 || ^5" + + For more information on semver, please see https://semver.org/ + maxLength: 64 + pattern: ^(\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\^)\s*(v?(0|[1-9]\d*|[x|X|\*])(\.(0|[1-9]\d*|x|X|\*]))?(\.(0|[1-9]\d*|x|X|\*))?(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?)\s*)((?:\s+|,\s*|\s*\|\|\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\^)\s*(v?(0|[1-9]\d*|x|X|\*])(\.(0|[1-9]\d*|x|X|\*))?(\.(0|[1-9]\d*|x|X|\*]))?(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?)\s*)*$ + type: string + required: + - packageName + type: object + sourceType: + description: |- + sourceType is a required reference to the type of install source. + + Allowed values are ["Catalog"] + + When this field is set to "Catalog", information for determining the appropriate + bundle of content to install will be fetched from ClusterCatalog resources existing + on the cluster. When using the Catalog sourceType, the catalog field must also be set. + enum: + - Catalog + type: string + required: + - sourceType + type: object + x-kubernetes-validations: + - message: sourceType Catalog requires catalog field + rule: self.sourceType == 'Catalog' && has(self.catalog) required: - installNamespace - - packageName - serviceAccount + - source type: object status: description: ClusterExtensionStatus defines the observed state of ClusterExtension. diff --git a/config/samples/olm_v1alpha1_clusterextension.yaml b/config/samples/olm_v1alpha1_clusterextension.yaml index 2dc1ca00d..46a9eb555 100644 --- a/config/samples/olm_v1alpha1_clusterextension.yaml +++ b/config/samples/olm_v1alpha1_clusterextension.yaml @@ -273,7 +273,10 @@ metadata: name: argocd spec: installNamespace: argocd - packageName: argocd-operator - version: 0.6.0 + source: + sourceType: Catalog + catalog: + packageName: argocd-operator + version: 0.6.0 serviceAccount: name: argocd-installer diff --git a/docs/Tasks/installing-an-extension.md b/docs/Tasks/installing-an-extension.md index 3380ecf91..8fef2bc35 100644 --- a/docs/Tasks/installing-an-extension.md +++ b/docs/Tasks/installing-an-extension.md @@ -24,9 +24,12 @@ After you add a catalog to your cluster, you can install an extension by creatin metadata: name: spec: - packageName: - channel: - version: "" + source: + sourceType: Catalog + catalog: + packageName: + channel: + version: "" ``` `extension_name` diff --git a/docs/drafts/upgrade-support.md b/docs/drafts/upgrade-support.md index dac8de5a6..4f938a8fd 100644 --- a/docs/drafts/upgrade-support.md +++ b/docs/drafts/upgrade-support.md @@ -61,7 +61,10 @@ kind: ClusterExtension metadata: name: extension-sample spec: - packageName: argocd-operator - version: 0.6.0 - upgradeConstraintPolicy: Ignore + source: + sourceType: Catalog + catalog: + packageName: argocd-operator + version: 0.6.0 + upgradeConstraintPolicy: Ignore ``` diff --git a/docs/refs/crd-upgrade-safety.md b/docs/refs/crd-upgrade-safety.md index ab790211e..2421550d9 100644 --- a/docs/refs/crd-upgrade-safety.md +++ b/docs/refs/crd-upgrade-safety.md @@ -62,8 +62,11 @@ metadata: name: clusterextension-sample spec: installNamespace: default - packageName: argocd-operator - version: 0.6.0 + source: + sourceType: Catalog + catalog: + packageName: argocd-operator + version: 0.6.0 preflight: crdUpgradeSafety: disabled: true @@ -176,9 +179,9 @@ In this example, the existing stored version, `v1alpha1`, has been removed: ``` validating upgrade for CRD "test.example.com" failed: CustomResourceDefinition test.example.com failed upgrade safety validation. "NoStoredVersionRemoved" validation failed: stored version "v1alpha1" removed ``` - + ### Removing an existing field - + In this example, the `pollInterval` field has been removed from `v1alpha1`: ??? example diff --git a/hack/test/pre-upgrade-setup.sh b/hack/test/pre-upgrade-setup.sh index 66b0a64c0..c5072c60d 100755 --- a/hack/test/pre-upgrade-setup.sh +++ b/hack/test/pre-upgrade-setup.sh @@ -134,8 +134,11 @@ metadata: name: ${TEST_CLUSTER_EXTENSION_NAME} spec: installNamespace: default - packageName: prometheus - version: 1.0.0 + source: + sourceType: Catalog + catalog: + packageName: prometheus + version: 1.0.0 serviceAccount: name: upgrade-e2e EOF diff --git a/internal/controllers/clusterextension_admission_test.go b/internal/controllers/clusterextension_admission_test.go index 10c0bdb25..2394dc548 100644 --- a/internal/controllers/clusterextension_admission_test.go +++ b/internal/controllers/clusterextension_admission_test.go @@ -11,9 +11,69 @@ import ( ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" ) +func TestClusterExtensionSourceConfig(t *testing.T) { + sourceTypeEmptyError := "Invalid value: \"null\"" + sourceTypeMismatchError := "spec.source.sourceType: Unsupported value" + sourceConfigInvalidError := "spec.source: Invalid value" + // unionField represents the required Catalog or (future) Bundle field required by SourceConfig + testCases := []struct { + name string + sourceType string + unionField string + errMsg string + }{ + {"sourceType is null", "", "Catalog", sourceTypeEmptyError}, + {"sourceType is invalid", "Invalid", "Catalog", sourceTypeMismatchError}, + {"catalog field does not exist", "Catalog", "", sourceConfigInvalidError}, + {"sourceConfig has required fields", "Catalog", "Catalog", ""}, + } + + t.Parallel() + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + cl := newClient(t) + var err error + if tc.unionField == "Catalog" { + err = cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ + Source: ocv1alpha1.SourceConfig{ + SourceType: tc.sourceType, + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "test-package", + }, + }, + InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, + })) + } + if tc.unionField == "" { + err = cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ + Source: ocv1alpha1.SourceConfig{ + SourceType: tc.sourceType, + }, + InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{ + Name: "default", + }, + })) + } + + if tc.errMsg == "" { + require.NoError(t, err, "unexpected error for sourceType %q: %w", tc.sourceType, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg) + } + }) + } +} + func TestClusterExtensionAdmissionPackageName(t *testing.T) { - tooLongError := "spec.packageName: Too long: may not be longer than 48" - regexMismatchError := "spec.packageName in body should match" + tooLongError := "spec.source.catalog.packageName: Too long: may not be longer than 48" + regexMismatchError := "spec.source.catalog.packageName in body should match" testCases := []struct { name string @@ -43,7 +103,12 @@ func TestClusterExtensionAdmissionPackageName(t *testing.T) { t.Parallel() cl := newClient(t) err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ - PackageName: tc.pkgName, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: tc.pkgName, + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: "default", @@ -59,8 +124,8 @@ func TestClusterExtensionAdmissionPackageName(t *testing.T) { } } func TestClusterExtensionAdmissionVersion(t *testing.T) { - tooLongError := "spec.version: Too long: may not be longer than 64" - regexMismatchError := "spec.version in body should match" + tooLongError := "spec.source.catalog.version: Too long: may not be longer than 64" + regexMismatchError := "spec.source.catalog.version in body should match" testCases := []struct { name string @@ -134,8 +199,13 @@ func TestClusterExtensionAdmissionVersion(t *testing.T) { t.Parallel() cl := newClient(t) err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ - PackageName: "package", - Version: tc.version, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "package", + Version: tc.version, + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: "default", @@ -152,8 +222,8 @@ func TestClusterExtensionAdmissionVersion(t *testing.T) { } func TestClusterExtensionAdmissionChannel(t *testing.T) { - tooLongError := "spec.channel: Too long: may not be longer than 48" - regexMismatchError := "spec.channel in body should match" + tooLongError := "spec.source.catalog.channel: Too long: may not be longer than 48" + regexMismatchError := "spec.source.catalog.channel in body should match" testCases := []struct { name string @@ -182,8 +252,13 @@ func TestClusterExtensionAdmissionChannel(t *testing.T) { t.Parallel() cl := newClient(t) err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ - PackageName: "package", - Channel: tc.channelName, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "package", + Channel: tc.channelName, + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: "default", @@ -231,7 +306,12 @@ func TestClusterExtensionAdmissionInstallNamespace(t *testing.T) { t.Parallel() cl := newClient(t) err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ - PackageName: "package", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "package", + }, + }, InstallNamespace: tc.installNamespace, ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: "default", @@ -279,7 +359,12 @@ func TestClusterExtensionAdmissionServiceAccount(t *testing.T) { t.Parallel() cl := newClient(t) err := cl.Create(context.Background(), buildClusterExtension(ocv1alpha1.ClusterExtensionSpec{ - PackageName: "package", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "package", + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: tc.serviceAccount, diff --git a/internal/controllers/clusterextension_controller.go b/internal/controllers/clusterextension_controller.go index 7bca96c9d..73debcffa 100644 --- a/internal/controllers/clusterextension_controller.go +++ b/internal/controllers/clusterextension_controller.go @@ -314,7 +314,7 @@ func SetDeprecationStatus(ext *ocv1alpha1.ClusterExtension, bundleName string, d case declcfg.SchemaPackage: deprecations[ocv1alpha1.TypePackageDeprecated] = entry case declcfg.SchemaChannel: - if ext.Spec.Channel != entry.Reference.Name { + if ext.Spec.Source.Catalog.Channel != entry.Reference.Name { continue } deprecations[ocv1alpha1.TypeChannelDeprecated] = entry diff --git a/internal/controllers/clusterextension_controller_test.go b/internal/controllers/clusterextension_controller_test.go index 81293d397..a92ea3c38 100644 --- a/internal/controllers/clusterextension_controller_test.go +++ b/internal/controllers/clusterextension_controller_test.go @@ -53,7 +53,12 @@ func TestClusterExtensionResolutionFails(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: pkgName, + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: "default", @@ -108,9 +113,14 @@ func TestClusterExtensionResolutionSucceeds(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + }, + }, InstallNamespace: installNamespace, ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: serviceAccount, @@ -177,9 +187,14 @@ func TestClusterExtensionUnpackFails(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + }, + }, InstallNamespace: installNamespace, ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: serviceAccount, @@ -248,9 +263,14 @@ func TestClusterExtensionUnpackUnexpectedState(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + }, + }, InstallNamespace: installNamespace, ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: serviceAccount, @@ -320,9 +340,14 @@ func TestClusterExtensionUnpackSucceeds(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + }, + }, InstallNamespace: installNamespace, ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: serviceAccount, @@ -395,9 +420,14 @@ func TestClusterExtensionInstallationFailedApplierFails(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + }, + }, InstallNamespace: installNamespace, ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: serviceAccount, @@ -476,9 +506,14 @@ func TestClusterExtensionInstallationFailedWatcherFailed(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + }, + }, InstallNamespace: installNamespace, ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: serviceAccount, @@ -560,9 +595,14 @@ func TestClusterExtensionInstallationSucceeds(t *testing.T) { clusterExtension := &ocv1alpha1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{Name: extKey.Name}, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: pkgName, - Version: pkgVer, - Channel: pkgChan, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + }, + }, InstallNamespace: installNamespace, ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: serviceAccount, @@ -701,6 +741,14 @@ func TestSetDeprecationStatus(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Generation: 1, }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "", + }, + }, + }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{}, }, @@ -709,6 +757,14 @@ func TestSetDeprecationStatus(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Generation: 1, }, + Spec: ocv1alpha1.ClusterExtensionSpec{ + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "", + }, + }, + }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{ { @@ -755,7 +811,12 @@ func TestSetDeprecationStatus(t *testing.T) { Generation: 1, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - Channel: "nondeprecated", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "nondeprecated", + }, + }, }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{}, @@ -766,7 +827,12 @@ func TestSetDeprecationStatus(t *testing.T) { Generation: 1, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - Channel: "nondeprecated", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "nondeprecated", + }, + }, }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{ @@ -816,7 +882,12 @@ func TestSetDeprecationStatus(t *testing.T) { Generation: 1, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - Channel: "badchannel", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "badchannel", + }, + }, }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{}, @@ -827,7 +898,12 @@ func TestSetDeprecationStatus(t *testing.T) { Generation: 1, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - Channel: "badchannel", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "badchannel", + }, + }, }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{ @@ -878,7 +954,12 @@ func TestSetDeprecationStatus(t *testing.T) { Generation: 1, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - Channel: "badchannel", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "badchannel", + }, + }, }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{}, @@ -889,7 +970,12 @@ func TestSetDeprecationStatus(t *testing.T) { Generation: 1, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - Channel: "badchannel", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "badchannel", + }, + }, }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{ @@ -953,7 +1039,12 @@ func TestSetDeprecationStatus(t *testing.T) { Generation: 1, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - Channel: "badchannel", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "badchannel", + }, + }, }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{}, @@ -964,7 +1055,12 @@ func TestSetDeprecationStatus(t *testing.T) { Generation: 1, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - Channel: "badchannel", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "badchannel", + }, + }, }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{ @@ -1022,7 +1118,12 @@ func TestSetDeprecationStatus(t *testing.T) { Generation: 1, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - Channel: "badchannel", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "badchannel", + }, + }, }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{}, @@ -1033,7 +1134,12 @@ func TestSetDeprecationStatus(t *testing.T) { Generation: 1, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - Channel: "badchannel", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + Channel: "badchannel", + }, + }, }, Status: ocv1alpha1.ClusterExtensionStatus{ Conditions: []metav1.Condition{ diff --git a/internal/resolve/catalog.go b/internal/resolve/catalog.go index 2683dd68c..258207482 100644 --- a/internal/resolve/catalog.go +++ b/internal/resolve/catalog.go @@ -29,11 +29,11 @@ type CatalogResolver struct { // Resolve returns a Bundle from a catalog that needs to get installed on the cluster. func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1alpha1.ClusterExtension, installedBundle *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { - packageName := ext.Spec.PackageName - versionRange := ext.Spec.Version - channelName := ext.Spec.Channel + packageName := ext.Spec.Source.Catalog.PackageName + versionRange := ext.Spec.Source.Catalog.Version + channelName := ext.Spec.Source.Catalog.Channel - selector, err := metav1.LabelSelectorAsSelector(&ext.Spec.CatalogSelector) + selector, err := metav1.LabelSelectorAsSelector(&ext.Spec.Source.Catalog.Selector) if err != nil { return nil, nil, nil, fmt.Errorf("desired catalog selector is invalid: %w", err) } @@ -76,7 +76,7 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1alpha1.ClusterEx predicates = append(predicates, filter.InMastermindsSemverRange(versionRangeConstraints)) } - if ext.Spec.UpgradeConstraintPolicy != ocv1alpha1.UpgradeConstraintPolicyIgnore && installedBundle != nil { + if ext.Spec.Source.Catalog.UpgradeConstraintPolicy != ocv1alpha1.UpgradeConstraintPolicyIgnore && installedBundle != nil { successorPredicate, err := filter.SuccessorsOf(installedBundle, packageFBC.Channels...) if err != nil { return fmt.Errorf("error finding upgrade edges: %w", err) diff --git a/internal/resolve/catalog_test.go b/internal/resolve/catalog_test.go index 25f099e70..f25f31b7f 100644 --- a/internal/resolve/catalog_test.go +++ b/internal/resolve/catalog_test.go @@ -534,12 +534,17 @@ func buildFooClusterExtension(pkg, channel, version string, upgradeConstraintPol Name: pkg, }, Spec: ocv1alpha1.ClusterExtensionSpec{ - InstallNamespace: "default", - ServiceAccount: ocv1alpha1.ServiceAccountReference{Name: "default"}, - PackageName: pkg, - Channel: channel, - Version: version, - UpgradeConstraintPolicy: upgradeConstraintPolicy, + InstallNamespace: "default", + ServiceAccount: ocv1alpha1.ServiceAccountReference{Name: "default"}, + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: pkg, + Channel: channel, + Version: version, + UpgradeConstraintPolicy: upgradeConstraintPolicy, + }, + }, }, } } @@ -641,13 +646,17 @@ func TestInvalidClusterExtensionCatalogMatchExpressions(t *testing.T) { Name: "foo", }, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: "foo", - CatalogSelector: metav1.LabelSelector{ - MatchExpressions: []metav1.LabelSelectorRequirement{ - { - Key: "name", - Operator: metav1.LabelSelectorOperator("bad"), - Values: []string{"value"}, + Source: ocv1alpha1.SourceConfig{ + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "foo", + Selector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "name", + Operator: metav1.LabelSelectorOperator("bad"), + Values: []string{"value"}, + }, + }, }, }, }, @@ -667,9 +676,13 @@ func TestInvalidClusterExtensionCatalogMatchLabelsName(t *testing.T) { Name: "foo", }, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: "foo", - CatalogSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{"": "value"}, + Source: ocv1alpha1.SourceConfig{ + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "foo", + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{"": "value"}, + }, + }, }, }, } @@ -687,9 +700,13 @@ func TestInvalidClusterExtensionCatalogMatchLabelsValue(t *testing.T) { Name: "foo", }, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: "foo", - CatalogSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{"name": "&value"}, + Source: ocv1alpha1.SourceConfig{ + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "foo", + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{"name": "&value"}, + }, + }, }, }, } @@ -706,7 +723,7 @@ func TestClusterExtensionMatchLabel(t *testing.T) { } r := CatalogResolver{WalkCatalogsFunc: w.WalkCatalogs} ce := buildFooClusterExtension(pkgName, "", "", ocv1alpha1.UpgradeConstraintPolicyEnforce) - ce.Spec.CatalogSelector.MatchLabels = map[string]string{"olm.operatorframework.io/name": "b"} + ce.Spec.Source.Catalog.Selector.MatchLabels = map[string]string{"olm.operatorframework.io/name": "b"} _, _, _, err := r.Resolve(context.Background(), ce, nil) require.NoError(t, err) @@ -721,7 +738,7 @@ func TestClusterExtensionNoMatchLabel(t *testing.T) { } r := CatalogResolver{WalkCatalogsFunc: w.WalkCatalogs} ce := buildFooClusterExtension(pkgName, "", "", ocv1alpha1.UpgradeConstraintPolicyEnforce) - ce.Spec.CatalogSelector.MatchLabels = map[string]string{"olm.operatorframework.io/name": "a"} + ce.Spec.Source.Catalog.Selector.MatchLabels = map[string]string{"olm.operatorframework.io/name": "a"} _, _, _, err := r.Resolve(context.Background(), ce, nil) require.Error(t, err) diff --git a/test/e2e/cluster_extension_install_test.go b/test/e2e/cluster_extension_install_test.go index d21c5598a..8e9b61f11 100644 --- a/test/e2e/cluster_extension_install_test.go +++ b/test/e2e/cluster_extension_install_test.go @@ -227,14 +227,19 @@ func TestClusterExtensionInstallRegistry(t *testing.T) { defer getArtifactsOutput(t) clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "prometheus", + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{"olm.operatorframework.io/name": extensionCatalog.Name}, + }, + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: sa.Name, }, - CatalogSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{"olm.operatorframework.io/name": extensionCatalog.Name}, - }, } t.Log("It resolves the specified package with correct bundle path") t.Log("By creating the ClusterExtension resource") @@ -288,7 +293,12 @@ func TestClusterExtensionInstallRegistryMultipleBundles(t *testing.T) { defer getArtifactsOutput(t) clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "prometheus", + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: sa.Name, @@ -323,13 +333,18 @@ func TestClusterExtensionBlockInstallNonSuccessorVersion(t *testing.T) { t.Log("By creating an ClusterExtension at a specified version") clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", - Version: "1.0.0", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "prometheus", + Version: "1.0.0", + // No Selector since this is an exact version match + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: sa.Name, }, - // No CatalogSelector since this is an exact version match } require.NoError(t, c.Create(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful installation") @@ -348,7 +363,7 @@ func TestClusterExtensionBlockInstallNonSuccessorVersion(t *testing.T) { t.Log("It does not allow to upgrade the ClusterExtension to a non-successor version") t.Log("By updating the ClusterExtension resource to a non-successor version") // 1.2.0 does not replace/skip/skipRange 1.0.0. - clusterExtension.Spec.Version = "1.2.0" + clusterExtension.Spec.Source.Catalog.Version = "1.2.0" require.NoError(t, c.Update(context.Background(), clusterExtension)) t.Log("By eventually reporting an unsatisfiable resolution") require.EventuallyWithT(t, func(ct *assert.CollectT) { @@ -373,8 +388,13 @@ func TestClusterExtensionForceInstallNonSuccessorVersion(t *testing.T) { t.Log("By creating an ClusterExtension at a specified version") clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", - Version: "1.0.0", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "prometheus", + Version: "1.0.0", + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: sa.Name, @@ -396,8 +416,8 @@ func TestClusterExtensionForceInstallNonSuccessorVersion(t *testing.T) { t.Log("It allows to upgrade the ClusterExtension to a non-successor version") t.Log("By updating the ClusterExtension resource to a non-successor version") // 1.2.0 does not replace/skip/skipRange 1.0.0. - clusterExtension.Spec.Version = "1.2.0" - clusterExtension.Spec.UpgradeConstraintPolicy = ocv1alpha1.UpgradeConstraintPolicyIgnore + clusterExtension.Spec.Source.Catalog.Version = "1.2.0" + clusterExtension.Spec.Source.Catalog.UpgradeConstraintPolicy = ocv1alpha1.UpgradeConstraintPolicyIgnore require.NoError(t, c.Update(context.Background(), clusterExtension)) t.Log("By eventually reporting a satisfiable resolution") require.EventuallyWithT(t, func(ct *assert.CollectT) { @@ -421,8 +441,13 @@ func TestClusterExtensionInstallSuccessorVersion(t *testing.T) { t.Log("By creating an ClusterExtension at a specified version") clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", - Version: "1.0.0", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "prometheus", + Version: "1.0.0", + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: sa.Name, @@ -444,7 +469,7 @@ func TestClusterExtensionInstallSuccessorVersion(t *testing.T) { t.Log("It does allow to upgrade the ClusterExtension to any of the successor versions within non-zero major version") t.Log("By updating the ClusterExtension resource by skipping versions") // 1.0.1 replaces 1.0.0 in the test catalog - clusterExtension.Spec.Version = "1.0.1" + clusterExtension.Spec.Source.Catalog.Version = "1.0.1" require.NoError(t, c.Update(context.Background(), clusterExtension)) t.Log("By eventually reporting a successful resolution and bundle path") require.EventuallyWithT(t, func(ct *assert.CollectT) { @@ -467,20 +492,25 @@ func TestClusterExtensionInstallReResolvesWhenCatalogIsPatched(t *testing.T) { defer getArtifactsOutput(t) clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "prometheus", + Selector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "olm.operatorframework.io/name", + Operator: metav1.LabelSelectorOpIn, + Values: []string{extensionCatalog.Name}, + }, + }, + }, + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: sa.Name, }, - CatalogSelector: metav1.LabelSelector{ - MatchExpressions: []metav1.LabelSelectorRequirement{ - { - Key: "olm.operatorframework.io/name", - Operator: metav1.LabelSelectorOpIn, - Values: []string{extensionCatalog.Name}, - }, - }, - }, } t.Log("It resolves the specified package with correct bundle path") t.Log("By creating the ClusterExtension resource") @@ -556,14 +586,19 @@ func TestClusterExtensionInstallReResolvesWhenNewCatalog(t *testing.T) { defer getArtifactsOutput(t) clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "prometheus", + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{"olm.operatorframework.io/name": extensionCatalog.Name}, + }, + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: sa.Name, }, - CatalogSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{"olm.operatorframework.io/name": extensionCatalog.Name}, - }, } t.Log("It resolves the specified package with correct bundle path") t.Log("By creating the ClusterExtension resource") @@ -621,14 +656,19 @@ func TestClusterExtensionInstallReResolvesWhenManagedContentChanged(t *testing.T defer getArtifactsOutput(t) clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "prometheus", + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{"olm.operatorframework.io/name": extensionCatalog.Name}, + }, + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: sa.Name, }, - CatalogSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{"olm.operatorframework.io/name": extensionCatalog.Name}, - }, } t.Log("It installs the specified package with correct bundle path") t.Log("By creating the ClusterExtension resource") @@ -681,14 +721,19 @@ func TestClusterExtensionRecoversFromInitialInstallFailedWhenFailureFixed(t *tes defer getArtifactsOutput(t) clusterExtension.Spec = ocv1alpha1.ClusterExtensionSpec{ - PackageName: "prometheus", + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: "prometheus", + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{"olm.operatorframework.io/name": extensionCatalog.Name}, + }, + }, + }, InstallNamespace: "default", ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: sa.Name, }, - CatalogSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{"olm.operatorframework.io/name": extensionCatalog.Name}, - }, } t.Log("It resolves the specified package with correct bundle path") t.Log("By creating the ClusterExtension resource") diff --git a/test/extension-developer-e2e/extension_developer_test.go b/test/extension-developer-e2e/extension_developer_test.go index cf081d562..0a80db17e 100644 --- a/test/extension-developer-e2e/extension_developer_test.go +++ b/test/extension-developer-e2e/extension_developer_test.go @@ -70,7 +70,12 @@ func TestExtensionDeveloper(t *testing.T) { Name: "registryv1", }, Spec: ocv1alpha1.ClusterExtensionSpec{ - PackageName: os.Getenv("REG_PKG_NAME"), + Source: ocv1alpha1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1alpha1.CatalogSource{ + PackageName: os.Getenv("REG_PKG_NAME"), + }, + }, InstallNamespace: installNamespace, ServiceAccount: ocv1alpha1.ServiceAccountReference{ Name: sa.Name, diff --git a/test/upgrade-e2e/post_upgrade_test.go b/test/upgrade-e2e/post_upgrade_test.go index a3b57c6ab..f8675d4bf 100644 --- a/test/upgrade-e2e/post_upgrade_test.go +++ b/test/upgrade-e2e/post_upgrade_test.go @@ -95,7 +95,7 @@ func TestClusterExtensionAfterOLMUpgrade(t *testing.T) { t.Log("Updating the ClusterExtension to change version") // Make sure that after we upgrade OLM itself we can still reconcile old objects if we change them - clusterExtension.Spec.Version = "1.0.1" + clusterExtension.Spec.Source.Catalog.Version = "1.0.1" require.NoError(t, c.Update(ctx, &clusterExtension)) t.Log("Checking that the ClusterExtension installs successfully")