-
Notifications
You must be signed in to change notification settings - Fork 85
✨ implementation of a registry+v1 direct bundle installer #2907
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,7 +99,6 @@ type ClusterExtensionSpec struct { | |
| // source is required and selects the installation source of content for this ClusterExtension. | ||
| // Set the sourceType field to perform the selection. | ||
| // | ||
| // Catalog is currently the only implemented sourceType. | ||
| // Setting sourceType to "Catalog" requires the catalog field to also be defined. | ||
| // | ||
| // Below is a minimal example of a source definition (in yaml): | ||
|
|
@@ -142,31 +141,110 @@ type ClusterExtensionSpec struct { | |
| ProgressDeadlineMinutes int32 `json:"progressDeadlineMinutes,omitempty"` | ||
| } | ||
|
|
||
| const SourceTypeCatalog = "Catalog" | ||
| const ( | ||
| SourceTypeCatalog = "Catalog" | ||
| SourceTypeOCIImage = "OCIImage" | ||
| ) | ||
|
|
||
| // SourceConfig is a discriminated union which selects the installation source. | ||
| // | ||
| // +union | ||
| // +kubebuilder:validation:XValidation:rule="has(self.sourceType) && self.sourceType == 'Catalog' ? has(self.catalog) : !has(self.catalog)",message="catalog is required when sourceType is Catalog, and forbidden otherwise" | ||
| // +kubebuilder:validation:XValidation:rule="has(self.sourceType) && self.sourceType == 'Catalog' ? self.catalog.size() != 0 : self.catalog.size() == 0",message="catalog is required when sourceType is Catalog, and forbidden otherwise" | ||
| // <opcon:experimental:validation:XValidation:rule="has(self.sourceType) && self.sourceType == 'OCIImage' ? self.ociImage.size() != 0 : self.ociImage.size() == 0",message="ociImage is required when sourceType is OCIImage, and forbidden otherwise"> | ||
| type SourceConfig struct { | ||
| // sourceType is required and specifies the type of install source. | ||
| // | ||
| // The only allowed value is "Catalog". | ||
| // <opcon:standard:description> | ||
| // The allowed value is "Catalog". | ||
| // | ||
| // When set to "Catalog", information for determining the appropriate bundle of content to install | ||
| // is fetched from ClusterCatalog resources on the cluster. | ||
| // When using the Catalog sourceType, the catalog field must also be set. | ||
| // </opcon:standard:description> | ||
| // | ||
| // <opcon:experimental:description> | ||
| // The allowed values are "Catalog" and "OCIImage". | ||
| // | ||
| // When set to "OCIImage", the bundle image is used directly. Direct sources do not perform | ||
| // dependency resolution and are only supported by the Boxcutter runtime. | ||
| // | ||
| // When set to "Catalog", information for determining the appropriate bundle of content to install | ||
| // is fetched from ClusterCatalog resources on the cluster. | ||
| // When using the Catalog sourceType, the catalog field must also be set. | ||
| // </opcon:experimental:description> | ||
| // | ||
| // +unionDiscriminator | ||
| // +kubebuilder:validation:Enum:="Catalog" | ||
| // <opcon:experimental:validation:Enum=Catalog;OCIImage> | ||
| // +required | ||
| SourceType string `json:"sourceType"` | ||
|
|
||
| // catalog configures how information is sourced from a catalog. | ||
| // It is required when sourceType is "Catalog", and forbidden otherwise. | ||
| // | ||
| // +optional | ||
| Catalog *CatalogFilter `json:"catalog,omitempty"` | ||
| Catalog CatalogFilter `json:"catalog,omitzero"` | ||
|
|
||
| // ociImage configures a bundle image to install directly. | ||
| // <opcon:experimental:description> | ||
| // They do not provide catalog dependency resolution or upgrade safety. | ||
| // </opcon:experimental:description> | ||
| // <opcon:experimental> | ||
| // +optional | ||
| OCIImage OCIImageSource `json:"ociImage,omitzero"` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we make this a pointer with I know Another thing I think we are free to do is change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I originally did, and coderabbit flagged for omitzero instead.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ehh, if we have API level validation that says "if sourceType is OCIImage, then Also, I'd go out on a (short?) limb and say "coderabbit is wrong" to suggest that we follow a different type pattern among different union members. I feel like the only right answers are:
I think we'd see go-apidiff fail but no mention in crddiff (I don't think the CRD schema would actually change, but I might be wrong).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I prefer to use instances with omitzero for cases where the difference between nil & non-initialized isn't meaningful. |
||
| } | ||
|
|
||
| // OCIImageSource identifies a bundle image to install directly from an OCI registry. | ||
| // +kubebuilder:validation:MinProperties:=1 | ||
| type OCIImageSource struct { | ||
| // ref is a required field that defines the reference to a container image for a registry+v1 bundle. | ||
| // It cannot be more than 1000 characters. | ||
| // | ||
| // A reference has 3 parts: the domain, name, and identifier. | ||
| // | ||
| // The domain is typically the registry where an image is located. | ||
| // It must be alphanumeric characters (lowercase and uppercase) separated by the "." character. | ||
| // Hyphenation is allowed, but the domain must start and end with alphanumeric characters. | ||
| // Specifying a port to use is also allowed by adding the ":" character followed by numeric values. | ||
| // The port must be the last value in the domain. | ||
| // Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080". | ||
| // | ||
| // The name is typically the repository in the registry where an image is located. | ||
| // It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters. | ||
| // Multiple names can be concatenated with the "/" character. | ||
| // The domain and name are combined using the "/" character. | ||
| // Some examples of valid name values are "operatorhubio/catalog", "catalog", "my-catalog.prod". | ||
| // An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/catalog". | ||
| // | ||
| // The identifier is typically the tag or digest for an image reference and is present at the end of the reference. | ||
| // It starts with a separator character used to distinguish the end of the name and beginning of the identifier. | ||
| // For a digest-based reference, the "@" character is the separator. | ||
| // For a tag-based reference, the ":" character is the separator. | ||
| // An identifier is required in the reference. | ||
| // | ||
| // Digest-based references must contain an algorithm reference immediately after the "@" separator. | ||
| // The algorithm reference must be followed by the ":" character and an encoded string. | ||
| // The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters. | ||
| // Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58". | ||
| // The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters. | ||
| // | ||
| // Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters. | ||
| // The tag must not be longer than 127 characters. | ||
| // | ||
| // An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05" | ||
| // An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" | ||
| // | ||
| // +required | ||
| // +kubebuilder:validation:MaxLength:=1000 | ||
| // +kubebuilder:validation:XValidation:rule="self.matches('^([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]+)?\\\\b')",message="must start with a valid domain. valid domains must be alphanumeric characters (lowercase and uppercase) separated by the \".\" character." | ||
| // +kubebuilder:validation:XValidation:rule="self.find('(\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') != \"\"",message="a valid name is required. valid names must contain lowercase alphanumeric characters separated only by the \".\", \"_\", \"__\", \"-\" characters." | ||
| // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != \"\" || self.find(':.*$') != \"\"",message="must end with a digest or a tag" | ||
| // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') == \"\" ? (self.find(':.*$') != \"\" ? self.find(':.*$').substring(1).size() <= 127 : true) : true",message="tag is invalid. the tag must not be more than 127 characters" | ||
| // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') == \"\" ? (self.find(':.*$') != \"\" ? self.find(':.*$').matches(':[\\\\w][\\\\w.-]*$') : true) : true",message="tag is invalid. valid tags must begin with a word character (alphanumeric + \"_\") followed by word characters or \".\", and \"-\" characters" | ||
| // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != \"\" ? self.find('(@.*:)').matches('(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])') : true",message="digest algorithm is not valid. valid algorithms must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the \"-\", \"_\", \"+\", and \".\" characters." | ||
| // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != \"\" ? self.find(':.*$').substring(1).size() >= 32 : true",message="digest is not valid. the encoded string must be at least 32 characters" | ||
| // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != \"\" ? self.find(':.*$').matches(':[0-9A-Fa-f]*$') : true",message="digest is not valid. the encoded string must only contain hex characters (A-F, a-f, 0-9)" | ||
|
Comment on lines
+241
to
+246
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Parse the tag and digest after the repository path, not from the first The tag and digest rules extract the identifier with
Both port cases in The name rule on Line 240 has a separate gap. It uses an unanchored Derive the identifier after the last 🤖 Prompt for AI Agents |
||
| Ref string `json:"ref"` | ||
| } | ||
|
|
||
| // ClusterExtensionInstallConfig is a union which selects the clusterExtension installation config. | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Guard both union rules with
has()before callingsize().CatalogandOCIImageare value fields tagged withomitzero. When a field is zero, the client omits it from the request. In CEL,self.catalog.size()on an absent field fails with a "no such key" error, so the rule rejects the object.OCIImagesource, the request has nocatalogfield. The Line 152 rule therefore rejects every validOCIImageClusterExtension, including every case inTestClusterExtensionOCIImageSourceConfig.helm/.../experimental/...clusterextensions.yamlLines 586-590) contains only the catalog rule. TheociImagerule was not emitted. As a result,sourceType: OCIImagewithoutociImagepasses admission, and only the reconcile-time check catches it.Fix both rules, then run
make generate manifests crd-ref-docs. Confirm that the experimental CRD contains theociImagerule.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents