[registry] Add alphabetical sorting to resource lists
Add sorting helper package with generic functions for sorting resources by name (cluster-scoped) or namespace/name (namespace-scoped). Apply sorting to: - TenantNamespace (by name) - TenantSecret (refactored to use helper) - TenantModule (new) - Application (new) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
This commit is contained in:
parent
d7931fdb20
commit
0b16f83eae
9 changed files with 262 additions and 16 deletions
|
|
@ -42,6 +42,7 @@ import (
|
|||
|
||||
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
|
||||
"github.com/cozystack/cozystack/pkg/config"
|
||||
"github.com/cozystack/cozystack/pkg/registry/sorting"
|
||||
internalapiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
|
|
@ -394,6 +395,8 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption
|
|||
appList.SetResourceVersion(hrList.GetResourceVersion())
|
||||
appList.Items = items
|
||||
|
||||
sorting.ByNamespacedName[appsv1alpha1.Application, *appsv1alpha1.Application](appList.Items)
|
||||
|
||||
klog.V(6).Infof("Successfully listed %d Application resources in namespace %s", len(items), namespace)
|
||||
return appList, nil
|
||||
}
|
||||
|
|
|
|||
36
pkg/registry/apps/application/rest_sorting_test.go
Normal file
36
pkg/registry/apps/application/rest_sorting_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
|
||||
"github.com/cozystack/cozystack/pkg/registry/sorting"
|
||||
)
|
||||
|
||||
func TestApplicationListSortsAlphabetically(t *testing.T) {
|
||||
items := []appsv1alpha1.Application{
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-b", Name: "zebra"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-a", Name: "alpha"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-b", Name: "alpha"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-a", Name: "bravo"}},
|
||||
}
|
||||
|
||||
sorting.ByNamespacedName[appsv1alpha1.Application, *appsv1alpha1.Application](items)
|
||||
|
||||
expected := []struct{ ns, name string }{
|
||||
{"ns-a", "alpha"},
|
||||
{"ns-a", "bravo"},
|
||||
{"ns-b", "alpha"},
|
||||
{"ns-b", "zebra"},
|
||||
}
|
||||
|
||||
for i, exp := range expected {
|
||||
if items[i].Namespace != exp.ns || items[i].Name != exp.name {
|
||||
t.Errorf("item %d: expected %s/%s, got %s/%s", i, exp.ns, exp.name, items[i].Namespace, items[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ import (
|
|||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
|
||||
"github.com/cozystack/cozystack/pkg/registry/sorting"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
)
|
||||
|
||||
|
|
@ -288,6 +289,8 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption
|
|||
moduleList.SetResourceVersion(hrList.GetResourceVersion())
|
||||
moduleList.Items = items
|
||||
|
||||
sorting.ByNamespacedName[corev1alpha1.TenantModule, *corev1alpha1.TenantModule](moduleList.Items)
|
||||
|
||||
klog.V(6).Infof("Successfully listed %d TenantModule resources in namespace %s", len(items), namespace)
|
||||
return moduleList, nil
|
||||
}
|
||||
|
|
|
|||
36
pkg/registry/core/tenantmodule/rest_test.go
Normal file
36
pkg/registry/core/tenantmodule/rest_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenantmodule
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
|
||||
"github.com/cozystack/cozystack/pkg/registry/sorting"
|
||||
)
|
||||
|
||||
func TestTenantModuleListSortsAlphabetically(t *testing.T) {
|
||||
items := []corev1alpha1.TenantModule{
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-b", Name: "zebra"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-a", Name: "alpha"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-b", Name: "alpha"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-a", Name: "bravo"}},
|
||||
}
|
||||
|
||||
sorting.ByNamespacedName[corev1alpha1.TenantModule, *corev1alpha1.TenantModule](items)
|
||||
|
||||
expected := []struct{ ns, name string }{
|
||||
{"ns-a", "alpha"},
|
||||
{"ns-a", "bravo"},
|
||||
{"ns-b", "alpha"},
|
||||
{"ns-b", "zebra"},
|
||||
}
|
||||
|
||||
for i, exp := range expected {
|
||||
if items[i].Namespace != exp.ns || items[i].Name != exp.name {
|
||||
t.Errorf("item %d: expected %s/%s, got %s/%s", i, exp.ns, exp.name, items[i].Namespace, items[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
|
||||
"github.com/cozystack/cozystack/pkg/registry/sorting"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -206,10 +207,10 @@ func (r *REST) ConvertToTable(_ context.Context, obj runtime.Object, _ runtime.O
|
|||
for i := range v.Items {
|
||||
tbl.Rows = append(tbl.Rows, row(&v.Items[i]))
|
||||
}
|
||||
tbl.ListMeta.ResourceVersion = v.ListMeta.ResourceVersion
|
||||
tbl.ResourceVersion = v.ResourceVersion
|
||||
case *corev1alpha1.TenantNamespace:
|
||||
tbl.Rows = append(tbl.Rows, row(v))
|
||||
tbl.ListMeta.ResourceVersion = v.ResourceVersion
|
||||
tbl.ResourceVersion = v.ResourceVersion
|
||||
default:
|
||||
return nil, notAcceptable{r.gvr.GroupResource(), fmt.Sprintf("unexpected %T", obj)}
|
||||
}
|
||||
|
|
@ -254,6 +255,9 @@ func (r *REST) makeList(src *corev1.NamespaceList, allowed []string) *corev1alph
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
sorting.ByName[corev1alpha1.TenantNamespace, *corev1alpha1.TenantNamespace](out.Items)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
|
|
|
|||
40
pkg/registry/core/tenantnamespace/rest_test.go
Normal file
40
pkg/registry/core/tenantnamespace/rest_test.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tenantnamespace
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestMakeListSortsAlphabetically(t *testing.T) {
|
||||
r := &REST{}
|
||||
|
||||
// Create namespaces in non-alphabetical order
|
||||
src := &corev1.NamespaceList{
|
||||
Items: []corev1.Namespace{
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "tenant-zebra"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "tenant-alpha"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "tenant-mike"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "tenant-bravo"}},
|
||||
},
|
||||
}
|
||||
|
||||
allowed := []string{"tenant-zebra", "tenant-alpha", "tenant-mike", "tenant-bravo"}
|
||||
|
||||
result := r.makeList(src, allowed)
|
||||
|
||||
expected := []string{"tenant-alpha", "tenant-bravo", "tenant-mike", "tenant-zebra"}
|
||||
|
||||
if len(result.Items) != len(expected) {
|
||||
t.Fatalf("expected %d items, got %d", len(expected), len(result.Items))
|
||||
}
|
||||
|
||||
for i, name := range expected {
|
||||
if result.Items[i].Name != name {
|
||||
t.Errorf("item %d: expected %q, got %q", i, name, result.Items[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import (
|
|||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
|
@ -28,6 +27,7 @@ import (
|
|||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
|
||||
"github.com/cozystack/cozystack/pkg/registry/sorting"
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -278,17 +278,7 @@ func (r *REST) List(ctx context.Context, opts *metainternal.ListOptions) (runtim
|
|||
for i := range list.Items {
|
||||
out.Items = append(out.Items, *secretToTenant(&list.Items[i]))
|
||||
}
|
||||
slices.SortFunc(out.Items, func(a, b corev1alpha1.TenantSecret) int {
|
||||
aKey := fmt.Sprintf("%s/%s", a.Namespace, a.Name)
|
||||
bKey := fmt.Sprintf("%s/%s", b.Namespace, b.Name)
|
||||
switch {
|
||||
case aKey < bKey:
|
||||
return -1
|
||||
case aKey > bKey:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
sorting.ByNamespacedName[corev1alpha1.TenantSecret, *corev1alpha1.TenantSecret](out.Items)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
|
@ -481,10 +471,10 @@ func (r *REST) ConvertToTable(_ context.Context, obj runtime.Object, _ runtime.O
|
|||
for i := range v.Items {
|
||||
tbl.Rows = append(tbl.Rows, row(&v.Items[i]))
|
||||
}
|
||||
tbl.ListMeta.ResourceVersion = v.ListMeta.ResourceVersion
|
||||
tbl.ResourceVersion = v.ResourceVersion
|
||||
case *corev1alpha1.TenantSecret:
|
||||
tbl.Rows = append(tbl.Rows, row(v))
|
||||
tbl.ListMeta.ResourceVersion = v.ResourceVersion
|
||||
tbl.ResourceVersion = v.ResourceVersion
|
||||
default:
|
||||
return nil, notAcceptable{r.gvr.GroupResource(), fmt.Sprintf("unexpected %T", obj)}
|
||||
}
|
||||
|
|
|
|||
74
pkg/registry/sorting/sort.go
Normal file
74
pkg/registry/sorting/sort.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package sorting provides generic sorting utilities for registry resources.
|
||||
package sorting
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// NameGetter is an interface for objects that have a Name.
|
||||
type NameGetter interface {
|
||||
GetName() string
|
||||
}
|
||||
|
||||
// NamespaceGetter is an interface for objects that have both Name and Namespace.
|
||||
type NamespaceGetter interface {
|
||||
NameGetter
|
||||
GetNamespace() string
|
||||
}
|
||||
|
||||
// ByName sorts a slice of objects by their Name in alphabetical order.
|
||||
// Use this for cluster-scoped resources.
|
||||
func ByName[T any, PT interface {
|
||||
*T
|
||||
NameGetter
|
||||
}](items []T) {
|
||||
slices.SortFunc(items, func(a, b T) int {
|
||||
pa, pb := PT(&a), PT(&b)
|
||||
switch {
|
||||
case pa.GetName() < pb.GetName():
|
||||
return -1
|
||||
case pa.GetName() > pb.GetName():
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
// ByNamespacedName sorts a slice of objects by Namespace/Name in alphabetical order.
|
||||
// Use this for namespace-scoped resources.
|
||||
func ByNamespacedName[T any, PT interface {
|
||||
*T
|
||||
NamespaceGetter
|
||||
}](items []T) {
|
||||
slices.SortFunc(items, func(a, b T) int {
|
||||
pa, pb := PT(&a), PT(&b)
|
||||
aKey := pa.GetNamespace() + "/" + pa.GetName()
|
||||
bKey := pb.GetNamespace() + "/" + pb.GetName()
|
||||
switch {
|
||||
case aKey < bKey:
|
||||
return -1
|
||||
case aKey > bKey:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
// ObjectMetaWrapper wraps metav1.ObjectMeta to implement NamespaceGetter.
|
||||
type ObjectMetaWrapper struct {
|
||||
metav1.ObjectMeta
|
||||
}
|
||||
|
||||
// GetName returns the name.
|
||||
func (o *ObjectMetaWrapper) GetName() string {
|
||||
return o.Name
|
||||
}
|
||||
|
||||
// GetNamespace returns the namespace.
|
||||
func (o *ObjectMetaWrapper) GetNamespace() string {
|
||||
return o.Namespace
|
||||
}
|
||||
60
pkg/registry/sorting/sort_test.go
Normal file
60
pkg/registry/sorting/sort_test.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sorting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type testClusterScoped struct {
|
||||
metav1.ObjectMeta
|
||||
}
|
||||
|
||||
type testNamespaceScoped struct {
|
||||
metav1.ObjectMeta
|
||||
}
|
||||
|
||||
func TestByName(t *testing.T) {
|
||||
items := []testClusterScoped{
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "zebra"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "alpha"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "mike"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "bravo"}},
|
||||
}
|
||||
|
||||
ByName[testClusterScoped, *testClusterScoped](items)
|
||||
|
||||
expected := []string{"alpha", "bravo", "mike", "zebra"}
|
||||
|
||||
for i, name := range expected {
|
||||
if items[i].Name != name {
|
||||
t.Errorf("item %d: expected %q, got %q", i, name, items[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestByNamespacedName(t *testing.T) {
|
||||
items := []testNamespaceScoped{
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-b", Name: "zebra"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-a", Name: "alpha"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-b", Name: "alpha"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Namespace: "ns-a", Name: "bravo"}},
|
||||
}
|
||||
|
||||
ByNamespacedName[testNamespaceScoped, *testNamespaceScoped](items)
|
||||
|
||||
expected := []struct{ ns, name string }{
|
||||
{"ns-a", "alpha"},
|
||||
{"ns-a", "bravo"},
|
||||
{"ns-b", "alpha"},
|
||||
{"ns-b", "zebra"},
|
||||
}
|
||||
|
||||
for i, exp := range expected {
|
||||
if items[i].Namespace != exp.ns || items[i].Name != exp.name {
|
||||
t.Errorf("item %d: expected %s/%s, got %s/%s", i, exp.ns, exp.name, items[i].Namespace, items[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue