Skip to content

⚠ 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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ manifests: $(CONTROLLER_GEN) $(KUSTOMIZE) #EXHELP Generate WebhookConfiguration,
mkdir $(CRD_WORKING_DIR)
$(CONTROLLER_GEN) --load-build-tags=$(GO_BUILD_TAGS) crd paths="./api/v1/..." output:crd:artifacts:config=$(CRD_WORKING_DIR)
mv $(CRD_WORKING_DIR)/olm.operatorframework.io_clusterextensions.yaml $(KUSTOMIZE_OPCON_CRDS_DIR)
mv $(CRD_WORKING_DIR)/olm.operatorframework.io_clusterextensionrevisions.yaml $(KUSTOMIZE_OPCON_CRDS_DIR)
mv $(CRD_WORKING_DIR)/olm.operatorframework.io_clustercatalogs.yaml $(KUSTOMIZE_CATD_CRDS_DIR)
rmdir $(CRD_WORKING_DIR)
# Generate the remaining operator-controller manifests
Expand Down
125 changes: 125 additions & 0 deletions api/v1/clusterextensionrevision_types.go
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"`
Copy link
Contributor

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.

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

Choose a reason for hiding this comment

The 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 {thing}Time. In this case it would be something like AvailableTime as opposed to AvailableSince.

https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#naming-conventions


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

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 Approved status condition to the True state.

}

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

Choose a reason for hiding this comment

The 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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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 kubectl explain ... and get that information included.

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

Choose a reason for hiding this comment

The 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{})
}
91 changes: 91 additions & 0 deletions api/v1/clusterextensionrevision_types_test.go
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))
}
}
117 changes: 115 additions & 2 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions cmd/operator-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,16 @@ func run() error {
return err
}

if err = (&controllers.ClusterExtensionRevisionReconciler{
Client: cl,
Scheme: mgr.GetScheme(),
Resolver: resolver,
InstalledBundleGetter: &controllers.DefaultInstalledBundleGetter{ActionClientGetter: acg},
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ClusterExtensionRevision")
return err
}

if err = (&controllers.ClusterCatalogReconciler{
Client: cl,
CatalogCache: catalogClientBackend,
Expand Down
Loading