-
Notifications
You must be signed in to change notification settings - Fork 65
⚠ Introduce ClusterExtensionRevision API #2051
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 |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/* | ||
Copyright 2024. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package v1 | ||
|
||
import ( | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
) | ||
|
||
// ClusterExtensionRevisionSpec defines the desired state of ClusterExtensionRevision | ||
type ClusterExtensionRevisionSpec struct { | ||
// clusterExtensionRef is a required reference to the ClusterExtension | ||
// that this revision represents an available upgrade for. | ||
// | ||
// +kubebuilder:validation:Required | ||
ClusterExtensionRef ClusterExtensionReference `json:"clusterExtensionRef"` | ||
|
||
// version is a required field that specifies the exact version of the bundle | ||
// that represents this available upgrade. | ||
// | ||
// version follows the semantic versioning standard as defined in https://semver.org/. | ||
// | ||
// +kubebuilder:validation:Required | ||
// +kubebuilder:validation:XValidation:rule="self.matches(\"^([0-9]+)(\\\\.[0-9]+)?(\\\\.[0-9]+)?(-([-0-9A-Za-z]+(\\\\.[-0-9A-Za-z]+)*))?(\\\\+([-0-9A-Za-z]+(-\\\\.[-0-9A-Za-z]+)*))?\")",message="version must be well-formed semver" | ||
Version string `json:"version"` | ||
// bundleMetadata contains the complete metadata for the bundle that represents | ||
// this available upgrade. | ||
// | ||
// +kubebuilder:validation:Required | ||
BundleMetadata BundleMetadata `json:"bundleMetadata"` | ||
|
||
// availableSince indicates when this upgrade revision was first detected | ||
// as being available. This helps track how long an upgrade has been pending. | ||
// | ||
// +kubebuilder:validation:Required | ||
AvailableSince metav1.Time `json:"availableSince"` | ||
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. Kubernetes API conventions generally encourage time based naming to be of the format |
||
|
||
// approved indicates whether this upgrade revision has been approved for execution. | ||
// When set to true, the controller will automatically update the corresponding | ||
// ClusterExtension to trigger the upgrade to this version. | ||
// | ||
// +optional | ||
Approved bool `json:"approved,omitempty"` | ||
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. Don't use a boolean. It is strongly encouraged to avoid the use of booleans because they often evolve beyond the true/false value. Use an enum instead that allows the empty string for the unset case. |
||
|
||
// approvedAt indicates when this upgrade revision was approved for execution. | ||
// This field is set automatically when the approved field changes from false to true. | ||
// | ||
// +optional | ||
ApprovedAt *metav1.Time `json:"approvedAt,omitempty"` | ||
Comment on lines
+58
to
+62
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. Better suited for a status field/condition? I'd suggest going the direction of a status condition instead of a field because you would automatically get the transition time when you flip the |
||
} | ||
|
||
// ClusterExtensionReference identifies a ClusterExtension | ||
type ClusterExtensionReference struct { | ||
// name is the name of the ClusterExtension | ||
// | ||
// +kubebuilder:validation:Required | ||
// +kubebuilder:validation:MaxLength:=253 | ||
// +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$\")",message="name must be a valid DNS1123 subdomain" | ||
Comment on lines
+69
to
+71
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. Just a heads up that users can't see these validations in the generated documentation so it is strongly encouraged to explicitly include them in plain english sentences in the GoDoc. |
||
Name string `json:"name"` | ||
} | ||
|
||
// ClusterExtensionRevisionStatus defines the observed state of ClusterExtensionRevision | ||
type ClusterExtensionRevisionStatus struct { | ||
// conditions represent the latest available observations of the ClusterExtensionRevision's current state. | ||
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. If you know the condition types you'll be using, it can be helpful for users to include them in the GoDoc so that they can use tools like |
||
// | ||
// +patchMergeKey=type | ||
// +patchStrategy=merge | ||
// +listType=map | ||
// +listMapKey=type | ||
// +optional | ||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` | ||
} | ||
|
||
// +kubebuilder:object:root=true | ||
// +kubebuilder:subresource:status | ||
// +kubebuilder:resource:scope=Cluster | ||
// +kubebuilder:printcolumn:name="Extension",type=string,JSONPath=".spec.clusterExtensionRef.name" | ||
// +kubebuilder:printcolumn:name="Version",type=string,JSONPath=".spec.version" | ||
// +kubebuilder:printcolumn:name="Approved",type=boolean,JSONPath=".spec.approved" | ||
// +kubebuilder:printcolumn:name="Approved At",type=date,JSONPath=".spec.approvedAt" | ||
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" | ||
|
||
// ClusterExtensionRevision represents an available upgrade for a ClusterExtension. | ||
// It is created automatically by the operator-controller when new versions become | ||
// available in catalogs that represent valid upgrade paths for installed ClusterExtensions. | ||
type ClusterExtensionRevision struct { | ||
metav1.TypeMeta `json:",inline"` | ||
metav1.ObjectMeta `json:"metadata,omitempty"` | ||
|
||
// spec defines the available upgrade revision details. | ||
// | ||
// +kubebuilder:validation:Required | ||
Spec ClusterExtensionRevisionSpec `json:"spec"` | ||
|
||
// status represents the current status of this ClusterExtensionRevision. | ||
// | ||
// +optional | ||
Status ClusterExtensionRevisionStatus `json:"status,omitempty"` | ||
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 be a pointer |
||
} | ||
|
||
// +kubebuilder:object:root=true | ||
|
||
// ClusterExtensionRevisionList contains a list of ClusterExtensionRevision | ||
type ClusterExtensionRevisionList struct { | ||
metav1.TypeMeta `json:",inline"` | ||
metav1.ListMeta `json:"metadata,omitempty"` | ||
Items []ClusterExtensionRevision `json:"items"` | ||
} | ||
|
||
func init() { | ||
SchemeBuilder.Register(&ClusterExtensionRevision{}, &ClusterExtensionRevisionList{}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
Copyright 2024. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package v1 | ||
|
||
import ( | ||
"testing" | ||
|
||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
) | ||
|
||
func TestClusterExtensionRevisionTypes(t *testing.T) { | ||
// Test that we can create a ClusterExtensionRevision with all required fields | ||
revision := &ClusterExtensionRevision{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: "test-extension-1.2.0", | ||
}, | ||
Spec: ClusterExtensionRevisionSpec{ | ||
ClusterExtensionRef: ClusterExtensionReference{ | ||
Name: "test-extension", | ||
}, | ||
Version: "1.2.0", | ||
BundleMetadata: BundleMetadata{ | ||
Name: "test-operator.v1.2.0", | ||
Version: "1.2.0", | ||
}, | ||
AvailableSince: metav1.Now(), | ||
Approved: false, | ||
}, | ||
Status: ClusterExtensionRevisionStatus{ | ||
Conditions: []metav1.Condition{ | ||
{ | ||
Type: "Available", | ||
Status: metav1.ConditionTrue, | ||
Reason: "UpgradeDetected", | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
// Verify the spec fields are accessible | ||
if revision.Spec.ClusterExtensionRef.Name != "test-extension" { | ||
t.Errorf("expected ClusterExtensionRef.Name to be 'test-extension', got %q", revision.Spec.ClusterExtensionRef.Name) | ||
} | ||
|
||
if revision.Spec.Version != "1.2.0" { | ||
t.Errorf("expected Version to be '1.2.0', got %q", revision.Spec.Version) | ||
} | ||
|
||
} | ||
|
||
func TestClusterExtensionRevisionList(t *testing.T) { | ||
// Test that we can create a ClusterExtensionRevisionList | ||
revisionList := &ClusterExtensionRevisionList{ | ||
Items: []ClusterExtensionRevision{ | ||
{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: "test-extension-1.2.0", | ||
}, | ||
Spec: ClusterExtensionRevisionSpec{ | ||
ClusterExtensionRef: ClusterExtensionReference{ | ||
Name: "test-extension", | ||
}, | ||
Version: "1.2.0", | ||
BundleMetadata: BundleMetadata{ | ||
Name: "test-operator.v1.2.0", | ||
Version: "1.2.0", | ||
}, | ||
AvailableSince: metav1.Now(), | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
if len(revisionList.Items) != 1 { | ||
t.Errorf("expected 1 item in list, got %d", len(revisionList.Items)) | ||
} | ||
} |
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.
Adding a CEL validation means you need to add a maximum length or you risk easily exceeding your cost budget as it calculates it using the worst-case scenario (which in this case would be the longest possible string value).
Additionally, I suspect the empty string is not a valid value here - meaning you probably want a minimum length to prevent the value being able to be set to the empty string.