Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DNM] (review-only) WIP: OLM v1 API review #2067

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

everettraven
Copy link

@everettraven everettraven commented Oct 17, 2024

The Operator Framework group has been working on a re-vamp of the Operator Lifecycle Manager project that we have dubbed "OLM v1". Our upstream project has adopted the OpenShift API conventions and since we intend for it to go GA in OpenShift 4.18 we wanted to get the APIs reviewed before GA.

This PR adds a new olm directory containing the Go types for the two APIs that would be introduced:

  • ClusterExtension
  • ClusterCatalog

Each API is under a separate sub-directory in this PR because while they are fairly coupled, the APIs live in separate upstream projects:

Also of note for reviewers, there is a portion of the ClusterCatalog API that is still a work-in-progress, but was included in this PR with what we anticipate it to look like. I will leave PR comments on the exact locations with references to the design document we have in place for the API change and functionality intended.

Signed-off-by: everettraven <everettraven@gmail.com>
@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Oct 17, 2024
Copy link
Contributor

openshift-ci bot commented Oct 17, 2024

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

Copy link
Contributor

openshift-ci bot commented Oct 17, 2024

Hello @everettraven! Some important instructions when contributing to openshift/api:
API design plays an important part in the user experience of OpenShift and as such API PRs are subject to a high level of scrutiny to ensure they follow our best practices. If you haven't already done so, please review the OpenShift API Conventions and ensure that your proposed changes are compliant. Following these conventions will help expedite the api review process for your PR.

@openshift-ci openshift-ci bot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Oct 17, 2024
Copy link
Contributor

openshift-ci bot commented Oct 17, 2024

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: everettraven
Once this PR has been reviewed and has the lgtm label, please assign joelspeed for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Comment on lines 143 to 146
type CatalogURLs struct {
// base is a required cluster-internal URL from which on-cluster components can access the API endpoint for this catalog
Base string `json:"base"`
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This particular type is still a work in progress. For now we intend to only provide a base URL in which a user can build on top of to perform requests against the supported REST API for accessing this ClusterCatalog's content.

For more information, see https://docs.google.com/document/d/1XvQPwk-Ws1rVcBpNsAaDoYKlmlws-hiQ19Ex9D4zAok/edit?usp=sharing

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relevant types changes here merged in operator-framework/catalogd#429 and released in v0.34.0 of catalogd.

Copy link
Contributor

@JoelSpeed JoelSpeed Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a shame, as you haven't added much validation to the field.

You would have benefited from adding CEL to validate that it was a valid URL, and imposing a maximum length to the field. I suppose we can add that for v1, though conversion might be tricky

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC our current thinking is that, even though it isn't following best practices for API versioning, we can do a hard cut over to the v1 versions of the API when we release v1.0.0 of the upstream projects and GA in OpenShift 4.18 (although @joelanford please correct me if this is an incorrect understanding).

With that in mind, we can still add some validations without having to worry about conversion complexity.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section has been updated to reflect the most recent changes to the types that were merged for this API upstream. Leaving this thread unresolved in case there is more discussion required re: conversion/API versioning

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are you expecting the transitional path to be for users of the existing v1alpha1 APIs? A hard cut does not sound like there is necessarily a migration path?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great question :) - my current understanding is that there is none. My general understanding is that doing a hard cut:

  • Has no impact to supported OpenShift customers since these APIs only exist when TechPreviewNoUpgrade is enabled meaning there is nothing to migrate when going GA in 4.18
  • May have impact to users of the upstream project, but we seem to be OK with not providing a migration path there because, following SemVer, users should expect a breaking change going from a v0.Y.Z release to a v1.Y.Z release.

I don't think this approach is set in stone, but I'll have another chat with @joelanford later this afternoon and see if there is any expectations that we provide that transitional path and that we are OK with the consequences of not providing that path.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May have impact to users of the upstream project, but we seem to be OK with not providing a migration path there because, following SemVer, users should expect a breaking change going from a v0.Y.Z release to a v1.Y.Z release.

If the cluster tries to upgrade to a new OLM, that is installing the v1 CRD, without the v1alpha1 version included, this would in theory mean that the data stored in etcd already, now has no representation in the v1 API, so, I think you'd lose all data there.

Also, any controllers running during this change, would break until they are upgraded to the new version of the API.

I'm not even sure if Kube will allow you to make that change to a CRD once its installed

I think you'll need to try this out if you want to say that v0 users aren't going to be provided with a migration path, it could mean they have to completely uninstall the operator, rather than upgrade, which in turn, means perhaps uninstalling the other operators installed by OLM right? The UX of that could be pretty bad

}

// ClusterCatalogSpec defines the desired state of ClusterCatalog
// +kubebuilder:validation:XValidation:rule="!has(self.source.image.pollInterval) || (self.source.image.ref.find('@sha256:') == \"\")",message="cannot specify PollInterval while using digest-based image"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this here rather than at the image level?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No good reason as far as I can tell. We should move it.

// CatalogSource is a discriminated union of possible sources for a Catalog.
// CatalogSource contains the sourcing information for a Catalog
// +union
// +kubebuilder:validation:XValidation:rule="self.type == 'Image' && has(self.image)",message="source type 'Image' requires image field"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This forces the value of type to required, which is fine, but, would need to be changed if you ever added a new value

We typically use

Suggested change
// +kubebuilder:validation:XValidation:rule="self.type == 'Image' && has(self.image)",message="source type 'Image' requires image field"
// +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Image' ?has(self.image) : !has(self.image)",message="image is required when type is Image, and forbidden otherwise"

You can then copy/paste this for new fields in the union without changing this one

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thanks for this suggestion. That will be helpful for having a reasonable "message" when we add new types.

Comment on lines +200 to +201
// ref: quay.io/operatorhubio/catalog:latest # image reference
// ref: quay.io/operatorhubio/catalog@sha256:c7392b4be033da629f9d665fec30f6901de51ce3adebeff0af579f311ee5cf1b # image reference with sha256 digest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhere in o/api there is an api that validates valid references, we should find that and copy their validation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one in o/api is part of the catalog mirroring effort, it validates something slightly different

There are some constraints we can validate on here though right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some further investigation and the containers/image library that we use for parsing these references uses this regex:

^((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?/)?[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\w][\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$

It's quite complex, but I'll see if I can break it down into some CEL expressions similar to how you mentioned we could attempt to break down some of our other pattern validations

// pollInterval: 1h30m # poll the image source every 1 hour and 30 minutes
//
// When omitted, the image will not be polled for new content.
// +kubebuilder:validation:Format:=duration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested that this actually works? And that non-duration like fields are rejected at admission time

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are going to change this field to pollIntervalMinutes to avoid any pitfalls of using a duration.

// For more information on semver, please see https://semver.org/
//
//+kubebuilder:validation:MaxLength:=64
//+kubebuilder:validation: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*)*$`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Pattern will print this regex to the end user, and to me this is write once, read never...

If you were to use CEL, you can provide a custom error message that might be a bit more user friendly

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good call out.

For context, CEL has regex capabilities. See https://github.com/google/cel-spec/blob/master/doc/langdef.md#string-functions

We can transplant this pattern into a CEL expression using the CEL matches function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When moving this to CEL, you might also want to break it down into multiple validations rather than one huge regex

@everettraven everettraven changed the title WIP: OLM v1 API review [DNM] (review-only) WIP: OLM v1 API review Oct 18, 2024
Signed-off-by: everettraven <everettraven@gmail.com>
@openshift-ci openshift-ci bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Oct 21, 2024
Signed-off-by: everettraven <everettraven@gmail.com>
// When set to "Enabled", the CRD Upgrade Safety pre-flight check will be run when
// performing an upgrade operation.
//
//+kubebuilder:validation:Enum:="Enabled";"Disabled"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabled/Disabled should be replaced with descriptive levels like

  1. InformationalOnly
  2. Strict
  3. Could later add: CriticalOnly.


MetadataNameLabel = "olm.operatorframework.io/metadata.name"

AvailabilityEnabled = "Enabled"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabled/Disabled needs more dimension, and communicate better to those with less op-con context.
Proposed were something like "AvailableToBeUsed" and its reverse.

// +optional
Priority int32 `json:"priority"`

// Availability is an optional field that allows users to define whether the ClusterCatalog is utilized by the operator-controller.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain without references to a specific client and speak more to the general use-case.

Comment on lines +84 to +96
// Availability is an optional field that allows users to define whether the ClusterCatalog is utilized by the operator-controller.
//
// Allowed values are : ["Enabled", "Disabled"].
// If set to "Enabled", the catalog will be used for updates, serving contents, and package installations.
//
// If set to "Disabled", catalogd will stop serving the catalog and the cached data will be removed.
//
// If unspecified, the default value is "Enabled"
//
// +kubebuilder:validation:Enum="Disabled";"Enabled"
// +kubebuilder:default="Enabled"
// +optional
Availability string `json:"availability,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Available should probably be a value, not a field name. A possibility

Available: Operators in this catalog are useable on the cluster.
Removed: Operators in this catalog are not usable on the cluster.

// ref contains the resolved sha256 image ref containing Catalog contents.
Ref string `json:"ref"`
// lastSuccessfulPollAttempt is the time when the resolved source was last successfully polled for new content.
LastSuccessfulPollAttempt metav1.Time `json:"lastSuccessfulPollAttempt"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fields like this end up at as standing write load. Suggest using conditions instead with a threshold for number of failures before declaring a thing stale.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this. Later, we want possibly to add conditions to capture 'staleness' of catalog contents.

// When omitted, the image will not be polled for new content.
// +kubebuilder:validation:Format:=duration
// +optional
PollInterval *metav1.Duration `json:"pollInterval,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lower bound on the poll interval

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 minute as the lower bound sounds good as discussed.

// pollInterval: 1h30m # poll the image source every 1 hour and 30 minutes
//
// When omitted, the image will not be polled for new content.
// +kubebuilder:validation:Format:=duration
Copy link

@grokspawn grokspawn Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding a lower-bound to this, or adding CEL validation, or ... better yet, change to pollIntervalMinutes which sidesteps the problem.

// When omitted, the image will not be polled for new content.
// +kubebuilder:validation:Format:=duration
// +optional
PollInterval *metav1.Duration `json:"pollInterval,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the resync period for the informers in the controller that will be processing this? Does a full resync of the informer cause this to poll?

I'm wondering if the resync of the informer (which might be 10m, 10h, 24h...) actually sets an upper bound for the poll interval

// When omitted, the image will not be polled for new content.
// +kubebuilder:validation:Format:=duration
// +optional
PollInterval *metav1.Duration `json:"pollInterval,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PollIntervalMinutes

// pollInterval: 1h30m # poll the image source every 1 hour and 30 minutes
//
// When omitted, the image will not be polled for new content.
// +kubebuilder:validation:Format:=duration

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider a force-refresh field, where if user changes the value from the spec we will take an action, which fulfills the user need to trigger an update w/o forcing a polling interval change

Copy link
Contributor

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of places where my comments would/are duplicated, I didn't capture them all as I didn't want to constantly repeat myself.

For the most part I think the structure looks good, though I've left some comments where we might need more thought about structure.

The majority of what I've left is suggestions on the godocs and what I would consider missing validations.

Comment on lines +57 to +64
Availability is an optional field that allows users to define whether the ClusterCatalog is utilized by the operator-controller.

Allowed values are : ["Enabled", "Disabled"].
If set to "Enabled", the catalog will be used for updates, serving contents, and package installations.

If set to "Disabled", catalogd will stop serving the catalog and the cached data will be removed.

If unspecified, the default value is "Enabled"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of interest, have you tried a kubectl explain on fields like this? I'm wondering if the output does render like this, or if it mucks with the whitespace.

Would be good to check that when a user explains some of your fields, that they are rendering as you expect

)

// SourceType defines the type of source used for catalogs.
// +enum
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this tag actually does anything, it may help the openapi generator perhaps

If you are intending to have this be an enum in a CRD, you need to have

// +kubebuilder:validation:Enum:=Image;Foo;Bar;...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll plan to remove the tag. Where this is used in the types we do have the appropriate enum tag.

Comment on lines +29 to +30
AvailabilityEnabled = "Enabled"
AvailabilityDisabled = "Disabled"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these are for enums, use a string type alias as you have done with SourceType

Comment on lines +46 to +47
Spec ClusterCatalogSpec `json:"spec"`
Status ClusterCatalogStatus `json:"status,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these should have godoc

Godoc should start with the json tag version of the name

Both should also have either // +kubeubilder:validation:Required or // +optional. We add an explicit optional vs required as there are ways to accidentally change this if it isn't specified explicitly (you can change the default at the package level, and there are other inferences that decide as well)

metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`

Items []ClusterCatalog `json:"items"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Godoc missing here too

// to be used for installation and management of the content for the package
// specified in the packageName field.
//
// This ServiceAccount is expected to exist in the installNamespace.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be more direct?

Suggested change
// This ServiceAccount is expected to exist in the installNamespace.
// This ServiceAccount must exist in the installNamespace.

// ref: quay.io/operatorhubio/catalog:latest
//
// For more information on FBC, see https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs
Source CatalogSource `json:"source"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would there ever be a use case for having multiple sources, like mirrors of the same catalog?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't envision a use case for this right now, but if they do arise, we think that there would be a reasonable way to implement something like this on a per source level without it being a breaking change. That being said, we haven't done a deep dive into how or what that design would look like. The existing OLM v0 CatalogSource API only supports a single image source per CatalogSource and we have not had any RFEs for being able to specify multiple.

For the mirroring use case in particular, we already respect the standard mirroring configurations when using things like ITMS, IDMS, etc. in OpenShift.

)

// ClusterExtensionSpec defines the desired state of ClusterExtension
type ClusterExtensionSpec struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source and Install could well be interpreted to be quite similar, eg, why is the source not part of the installation configuration, I wonder if changing the names might make this more obvious, or perhaps, source should be within install? Or the fields from install should just be promoted to the spec?

//+kubebuilder:validation:MaxLength:=64
//+kubebuilder:validation: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*)*$`
//+optional
Version string `json:"version,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be easier to reason about, if you created a structure to capture the version constraints, rather than trying to regex match and explain this massive validation?

For example, just splitting this into a list of strings, and saying that a match of any item in the list is considered a match, would remove the need to account for the OR in this.

Thinking about the ~ and ^ usage, is that not effectively a form of pinning to either the major, minor or patch versions of the version?

Could a field that was an enum represent that choice? upgradePinning: Major | Minor represent something equivalent? (I don't think you need patch)

One of my concerns about the validation here at the moment, is that there are many ways to express the same thing

What if this was something like

version:
- base: 1.2.0 # This would be ">=1.2.0, < 1.3.0" or "1.2.x" or "~1.2.0" in the current form
  operator: GreaterOrEqual
  pinnedAt: Minor

// # 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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I wanted to exclude a specific version from being valid, do I then just have to create ORs on a range around the version I want to exclude?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants