From 5e2dc1fe59382bda863411b4fda713572a2f6078 Mon Sep 17 00:00:00 2001 From: "Okuniewska, Julia" Date: Thu, 10 Apr 2025 15:21:39 +0000 Subject: [PATCH 1/5] add tests for query over conditions --- internal/rest/getv2clusters_test.go | 120 ++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/internal/rest/getv2clusters_test.go b/internal/rest/getv2clusters_test.go index 185671af..bcef0d16 100644 --- a/internal/rest/getv2clusters_test.go +++ b/internal/rest/getv2clusters_test.go @@ -13,6 +13,7 @@ import ( openapi_types "github.com/oapi-codegen/runtime/types" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" capi "sigs.k8s.io/cluster-api/api/v1beta1" @@ -23,6 +24,42 @@ import ( "github.com/open-edge-platform/cluster-manager/v2/pkg/api" ) +var clusterStatusReady = capi.ClusterStatus{ + Phase: string(capi.ClusterPhaseProvisioned), + Conditions: []capi.Condition{ + { + Type: capi.ReadyCondition, + Status: corev1.ConditionTrue, + }, + { + Type: capi.ControlPlaneReadyCondition, + Status: corev1.ConditionTrue, + }, + { + Type: capi.InfrastructureReadyCondition, + Status: corev1.ConditionTrue, + }, + }, +} + +var clusterStatusInProgressControlPlane = capi.ClusterStatus{ + Phase: string(capi.ClusterPhaseProvisioned), + Conditions: []capi.Condition{ + { + Type: capi.ReadyCondition, + Status: corev1.ConditionTrue, + }, + { + Type: capi.ControlPlaneReadyCondition, + Status: corev1.ConditionFalse, + }, + { + Type: capi.InfrastructureReadyCondition, + Status: corev1.ConditionTrue, + }, + }, +} + func createMockServer(t *testing.T, clusters []capi.Cluster, projectID string, options ...bool) *Server { unstructuredClusters := make([]unstructured.Unstructured, len(clusters)) for i, cluster := range clusters { @@ -77,6 +114,22 @@ func generateCluster(name *string, version *string) capi.Cluster { } } +func generateClusterWithStatus(name, version *string, status capi.ClusterStatus) capi.Cluster { + clusterName := "" + if name != nil { + clusterName = *name + } + clusterVersion := "" + if version != nil { + clusterVersion = *version + } + return capi.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: clusterName}, + Spec: capi.ClusterSpec{Topology: &capi.Topology{Version: clusterVersion}}, + Status: status, + } +} + func generateClusterInfo(name, version string, lifecycleIndicator api.StatusIndicator, lifecycleMessage string) api.ClusterInfo { return api.ClusterInfo{ Name: ptr(name), @@ -450,6 +503,73 @@ func TestGetV2Clusters200(t *testing.T) { } require.Equal(t, expectedResponse, actualResponse, "GetV2Clusters() response = %v, want %v", actualResponse, expectedResponse) }) + + t.Run("filtered clusters by conditions", func(t *testing.T) { + tests := []struct { + name string + clusters []capi.Cluster + filter string + expectedResult api.GetV2Clusters200JSONResponse + }{ + { + name: "filtered clusters by providerStatus", + clusters: []capi.Cluster{ + generateClusterWithStatus(ptr("example-cluster-1"), ptr("v1.30.6+rke2r1"), clusterStatusReady), + generateClusterWithStatus(ptr("example-cluster-2"), ptr("v1.20.4+rke2r1"), clusterStatusInProgressControlPlane), + }, + filter: "providerStatus=ready", + expectedResult: api.GetV2Clusters200JSONResponse{ + Clusters: &[]api.ClusterInfo{ + generateClusterInfo("example-cluster-1", "v1.30.6+rke2r1", api.STATUSINDICATIONIDLE, "active"), + }, + TotalElements: 1, + }, + }, + { + name: "filtered clusters by lifecyclePhase", + clusters: []capi.Cluster{ + generateClusterWithStatus(ptr("example-cluster-1"), ptr("v1.30.6+rke2r1"), clusterStatusReady), + generateClusterWithStatus(ptr("example-cluster-2"), ptr("v1.20.4+rke2r1"), clusterStatusInProgressControlPlane), + }, + filter: "lifecyclePhase=active", + expectedResult: api.GetV2Clusters200JSONResponse{ + Clusters: &[]api.ClusterInfo{ + generateClusterInfo("example-cluster-1", "v1.30.6+rke2r1", api.STATUSINDICATIONIDLE, "active"), + }, + TotalElements: 1, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := createMockServer(t, tt.clusters, expectedActiveProjectID) + require.NotNil(t, server, "NewServer() returned nil, want not nil") + + // Create a new request & response recorder + req := httptest.NewRequest("GET", "/v2/clusters?filter="+tt.filter, nil) + req.Header.Set("Activeprojectid", expectedActiveProjectID) + rr := httptest.NewRecorder() + + // create a handler with middleware to serve the request + handler, err := server.ConfigureHandler() + require.Nil(t, err) + handler.ServeHTTP(rr, req) + + // Parse the response body + var actualResponse api.GetV2Clusters200JSONResponse + err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) + require.NoError(t, err, "Failed to unmarshal response body") + + // Check the response status + require.Equal(t, http.StatusOK, rr.Code, "ServeHTTP() status = %v, want %v", rr.Code, 200) + + // Check the response content + require.Equal(t, tt.expectedResult, actualResponse, "GetV2Clusters() response = %v, want %v", actualResponse, tt.expectedResult) + }) + } + }) + t.Run("no clusters after filter criteria", func(t *testing.T) { clusters := []capi.Cluster{ generateCluster(ptr("example-cluster-1"), ptr("v1.30.6+rke2r1")), From 9933b491be79c9e71781f3b10feb302c115b1723 Mon Sep 17 00:00:00 2001 From: "Okuniewska, Julia" Date: Thu, 10 Apr 2025 15:21:53 +0000 Subject: [PATCH 2/5] add field to supported filters --- internal/pagination/pagination.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/pagination/pagination.go b/internal/pagination/pagination.go index 677134c8..92ea3d25 100644 --- a/internal/pagination/pagination.go +++ b/internal/pagination/pagination.go @@ -266,6 +266,7 @@ func ValidateParams(params any) (pageSize, offset *int, orderBy, filter *string, "name": true, "kubernetesVersion": true, "providerStatus": true, + "lifecyclePhase": true, } orderByParts := strings.Split(*orderBy, " ") if len(orderByParts) == 1 { @@ -287,6 +288,7 @@ func ValidateParams(params any) (pageSize, offset *int, orderBy, filter *string, "name": true, "kubernetesVersion": true, "providerStatus": true, + "lifecyclePhase": true, "version": true, } filterParts := strings.FieldsFunc(*filter, func(r rune) bool { From c819039ee61f239903154edb6ab458b11d560c2a Mon Sep 17 00:00:00 2001 From: Eoghan Lawless Date: Fri, 11 Apr 2025 05:14:12 -0700 Subject: [PATCH 3/5] fix: failing test cases Signed-off-by: Eoghan Lawless --- internal/pagination/pagination.go | 20 +++----------------- internal/rest/getv2clusters.go | 9 +++++++++ internal/rest/getv2clusters_test.go | 26 +++++++++++++++++++++----- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/internal/pagination/pagination.go b/internal/pagination/pagination.go index 92ea3d25..2b756384 100644 --- a/internal/pagination/pagination.go +++ b/internal/pagination/pagination.go @@ -171,17 +171,6 @@ func FilterItems[T any](items []T, filter string, filterFunc func(T, *Filter) bo } var filteredItems []T - for _, item := range items { - if len(applyFilters([]T{item}, filters, useAnd, filterFunc)) > 0 { - filteredItems = append(filteredItems, item) - } - } - - return filteredItems, nil -} - -func applyFilters[T any](items []T, filters []*Filter, useAnd bool, filterFunc filterFunc[T]) []T { - filteredItems := make([]T, 0, len(items)) for _, item := range items { if useAnd { // all required filters should match @@ -197,19 +186,16 @@ func applyFilters[T any](items []T, filters []*Filter, useAnd bool, filterFunc f } } else { // at least one filter match - matchesAny := false for _, filter := range filters { if filterFunc(item, filter) { - matchesAny = true + filteredItems = append(filteredItems, item) break } } - if matchesAny { - filteredItems = append(filteredItems, item) - } } } - return filteredItems + + return filteredItems, nil } func OrderItems[T any](items []T, orderBy string, orderFunc func(T, T, *OrderBy) bool) ([]T, error) { diff --git a/internal/rest/getv2clusters.go b/internal/rest/getv2clusters.go index fc11d6da..10785d4d 100644 --- a/internal/rest/getv2clusters.go +++ b/internal/rest/getv2clusters.go @@ -164,6 +164,10 @@ func filterClusters(cluster api.ClusterInfo, filter *Filter) bool { if cluster.ProviderStatus != nil { return MatchWildcard(cluster.ProviderStatus.Message, filter.Value) } + case "lifecyclePhase": + if cluster.LifecyclePhase != nil { + return MatchWildcard(cluster.LifecyclePhase.Message, filter.Value) + } default: return false } @@ -187,6 +191,11 @@ func orderClustersBy(cluster1, cluster2 api.ClusterInfo, orderBy *OrderBy) bool return *cluster1.ProviderStatus.Message > *cluster2.ProviderStatus.Message } return *cluster1.ProviderStatus.Message < *cluster2.ProviderStatus.Message + case "lifecyclePhase": + if orderBy.IsDesc { + return *cluster1.LifecyclePhase.Message > *cluster2.LifecyclePhase.Message + } + return *cluster1.LifecyclePhase.Message < *cluster2.LifecyclePhase.Message default: return false } diff --git a/internal/rest/getv2clusters_test.go b/internal/rest/getv2clusters_test.go index bcef0d16..7fe6b97a 100644 --- a/internal/rest/getv2clusters_test.go +++ b/internal/rest/getv2clusters_test.go @@ -11,6 +11,7 @@ import ( "testing" openapi_types "github.com/oapi-codegen/runtime/types" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -43,11 +44,11 @@ var clusterStatusReady = capi.ClusterStatus{ } var clusterStatusInProgressControlPlane = capi.ClusterStatus{ - Phase: string(capi.ClusterPhaseProvisioned), + Phase: string(capi.ClusterPhaseProvisioning), Conditions: []capi.Condition{ { Type: capi.ReadyCondition, - Status: corev1.ConditionTrue, + Status: corev1.ConditionFalse, }, { Type: capi.ControlPlaneReadyCondition, @@ -559,13 +560,28 @@ func TestGetV2Clusters200(t *testing.T) { // Parse the response body var actualResponse api.GetV2Clusters200JSONResponse err = json.Unmarshal(rr.Body.Bytes(), &actualResponse) - require.NoError(t, err, "Failed to unmarshal response body") + assert.NoError(t, err, "Failed to unmarshal response body") // Check the response status - require.Equal(t, http.StatusOK, rr.Code, "ServeHTTP() status = %v, want %v", rr.Code, 200) + assert.Equal(t, http.StatusOK, rr.Code, "ServeHTTP() status = %v, want %v", rr.Code, 200) + + for _, cluster := range *actualResponse.Clusters { + cluster.ControlPlaneReady.Message = ptr("condition not found") + cluster.ControlPlaneReady.Timestamp = ptr(uint64(0)) + + cluster.InfrastructureReady.Message = ptr("condition not found") + cluster.InfrastructureReady.Timestamp = ptr(uint64(0)) + + cluster.ProviderStatus.Indicator = statusIndicatorPtr(api.STATUSINDICATIONUNSPECIFIED) + cluster.ProviderStatus.Message = ptr("condition not found") + cluster.ProviderStatus.Timestamp = ptr(uint64(0)) + + cluster.NodeHealth.Timestamp = ptr(uint64(0)) + cluster.LifecyclePhase.Timestamp = ptr(uint64(0)) + } // Check the response content - require.Equal(t, tt.expectedResult, actualResponse, "GetV2Clusters() response = %v, want %v", actualResponse, tt.expectedResult) + assert.Equal(t, tt.expectedResult, actualResponse, "GetV2Clusters() response = %v, want %v", actualResponse, tt.expectedResult) }) } }) From c54e5c682be5cdcd67df63cbdbe3802820f5dd7f Mon Sep 17 00:00:00 2001 From: Eoghan Lawless Date: Fri, 11 Apr 2025 07:40:40 -0700 Subject: [PATCH 4/5] version: 2.0.6-dev Signed-off-by: Eoghan Lawless --- VERSION | 2 +- deployment/charts/cluster-manager/Chart.yaml | 4 ++-- deployment/charts/cluster-template-crd/Chart.yaml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index e0102586..54109376 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.5 +2.0.6-dev diff --git a/deployment/charts/cluster-manager/Chart.yaml b/deployment/charts/cluster-manager/Chart.yaml index 9c977efb..8665d7a7 100644 --- a/deployment/charts/cluster-manager/Chart.yaml +++ b/deployment/charts/cluster-manager/Chart.yaml @@ -16,6 +16,6 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 2.0.5 -appVersion: 2.0.5 +version: 2.0.6-dev +appVersion: 2.0.6-dev annotations: {} diff --git a/deployment/charts/cluster-template-crd/Chart.yaml b/deployment/charts/cluster-template-crd/Chart.yaml index fdf9b20d..32c382f4 100644 --- a/deployment/charts/cluster-template-crd/Chart.yaml +++ b/deployment/charts/cluster-template-crd/Chart.yaml @@ -6,6 +6,6 @@ apiVersion: v2 name: cluster-template-crd description: A Helm chart for the ClusterTemplate CRD type: application -version: 2.0.5 -appVersion: 2.0.5 +version: 2.0.6-dev +appVersion: 2.0.6-dev annotations: {} From 895ada18cb0bd1bb2cd8c0b8ff99b48440084142 Mon Sep 17 00:00:00 2001 From: Eoghan Lawless Date: Fri, 11 Apr 2025 07:57:20 -0700 Subject: [PATCH 5/5] lint: remove unused function definition Signed-off-by: Eoghan Lawless --- Makefile | 2 +- internal/pagination/pagination.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile b/Makefile index de98989f..58229227 100644 --- a/Makefile +++ b/Makefile @@ -366,7 +366,7 @@ CONTROLLER_TOOLS_VERSION ?= v0.17.0 ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') -GOLANGCI_LINT_VERSION ?= v1.62.2 +GOLANGCI_LINT_VERSION ?= v1.64.7 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. diff --git a/internal/pagination/pagination.go b/internal/pagination/pagination.go index 2b756384..3a45a9b8 100644 --- a/internal/pagination/pagination.go +++ b/internal/pagination/pagination.go @@ -25,8 +25,6 @@ type OrderBy struct { IsDesc bool } -type filterFunc[T any] func(item T, filter *Filter) bool - type orderFunc[T any] func(item1, item2 T, orderBy *OrderBy) bool var normalizeEqualsRe = regexp.MustCompile(`[ \t]*=[ \t]*`)