Skip to content

Commit

Permalink
Add sorting for kube cluster (#10702) (#10921)
Browse files Browse the repository at this point in the history
Part of RFD 55
  • Loading branch information
kimlisa authored Mar 7, 2022
1 parent dcfc9cc commit 0abd136
Show file tree
Hide file tree
Showing 13 changed files with 1,061 additions and 612 deletions.
2 changes: 2 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2411,6 +2411,8 @@ func (c *Client) ListResources(ctx context.Context, req proto.ListResourcesReque
resources[i] = respResource.GetKubeService()
case types.KindWindowsDesktop:
resources[i] = respResource.GetWindowsDesktop()
case types.KindKubernetesCluster:
resources[i] = respResource.GetKubeCluster()
default:
return nil, trace.NotImplemented("resource type %s does not support pagination", req.ResourceType)
}
Expand Down
1,282 changes: 682 additions & 600 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions api/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,9 @@ message PaginatedResource {
// WindowsDesktop represents a WindowsDesktop resource.
types.WindowsDesktopV3 WindowsDesktop = 5
[ (gogoproto.jsontag) = "windows_desktop,omitempty" ];
// KubeCluster represents a KubeCluster resource.
types.KubernetesClusterV3 KubeCluster = 6
[ (gogoproto.jsontag) = "kube_cluster,omitempty" ];
}
}

Expand Down
2 changes: 1 addition & 1 deletion api/client/proto/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,5 @@ func (req *ListResourcesRequest) CheckAndSetDefaults() error {
// RequiresFakePagination checks if we need to fallback to GetXXX calls
// that retrieves entire resources upfront rather than working with subsets.
func (req *ListResourcesRequest) RequiresFakePagination() bool {
return req.SortBy.Field != "" || req.NeedTotalCount
return req.SortBy.Field != "" || req.NeedTotalCount || req.ResourceType == types.KindKubernetesCluster
}
98 changes: 98 additions & 0 deletions api/types/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,38 @@ package types

import (
"fmt"
"sort"
"time"

"github.com/gogo/protobuf/proto"
"github.com/gravitational/teleport/api/utils"
"github.com/gravitational/trace"
)

// KubeCluster represents a kubernetes cluster.
type KubeCluster interface {
// ResourceWithLabels provides common resource methods.
ResourceWithLabels
// GetNamespace returns the kube cluster namespace.
GetNamespace() string
// GetStaticLabels returns the kube cluster static labels.
GetStaticLabels() map[string]string
// SetStaticLabels sets the kube cluster static labels.
SetStaticLabels(map[string]string)
// GetDynamicLabels returns the kube cluster dynamic labels.
GetDynamicLabels() map[string]CommandLabel
// SetDynamicLabels sets the kube cluster dynamic labels.
SetDynamicLabels(map[string]CommandLabel)
// LabelsString returns all labels as a string.
LabelsString() string
// String returns string representation of the kube cluster.
String() string
// GetDescription returns the kube cluster description.
GetDescription() string
// Copy returns a copy of this kube cluster resource.
Copy() *KubernetesClusterV3
}

// NewKubernetesClusterV3FromLegacyCluster creates a new Kubernetes cluster resource
// from the legacy type.
func NewKubernetesClusterV3FromLegacyCluster(namespace string, cluster *KubernetesCluster) (*KubernetesClusterV3, error) {
Expand Down Expand Up @@ -192,3 +217,76 @@ func (k *KubernetesClusterV3) CheckAndSetDefaults() error {

return nil
}

// KubeClusters represents a list of kube clusters.
type KubeClusters []KubeCluster

// Len returns the slice length.
func (s KubeClusters) Len() int { return len(s) }

// Less compares kube clusters by name.
func (s KubeClusters) Less(i, j int) bool {
return s[i].GetName() < s[j].GetName()
}

// Swap swaps two kube clusters.
func (s KubeClusters) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

// SortByCustom custom sorts by given sort criteria.
func (s KubeClusters) SortByCustom(sortBy SortBy) error {
if sortBy.Field == "" {
return nil
}

isDesc := sortBy.IsDesc
switch sortBy.Field {
case ResourceMetadataName:
sort.SliceStable(s, func(i, j int) bool {
return stringCompare(s[i].GetName(), s[j].GetName(), isDesc)
})
default:
return trace.NotImplemented("sorting by field %q for resource %q is not supported", sortBy.Field, KindKubernetesCluster)
}

return nil
}

// AsResources returns as type resources with labels.
func (s KubeClusters) AsResources() []ResourceWithLabels {
resources := make([]ResourceWithLabels, 0, len(s))
for _, cluster := range s {
resources = append(resources, ResourceWithLabels(cluster))
}
return resources
}

// GetFieldVals returns list of select field values.
func (s KubeClusters) GetFieldVals(field string) ([]string, error) {
vals := make([]string, 0, len(s))
switch field {
case ResourceMetadataName:
for _, server := range s {
vals = append(vals, server.GetName())
}
default:
return nil, trace.NotImplemented("getting field %q for resource %q is not supported", field, KindKubernetesCluster)
}

return vals, nil
}

// DeduplicateKubeClusters deduplicates kube clusters by name.
func DeduplicateKubeClusters(kubeclusters []KubeCluster) []KubeCluster {
seen := make(map[string]struct{})
result := make([]KubeCluster, 0, len(kubeclusters))

for _, cluster := range kubeclusters {
if _, ok := seen[cluster.GetName()]; ok {
continue
}
seen[cluster.GetName()] = struct{}{}
result = append(result, cluster)
}

return result
}
84 changes: 84 additions & 0 deletions api/types/kubernetes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
Copyright 2022 Gravitational, Inc.
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 types

import (
"testing"

"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
)

func TestKubeClustersSorter(t *testing.T) {
t.Parallel()

makeClusters := func(testVals []string, testField string) []KubeCluster {
servers := make([]KubeCluster, len(testVals))
for i := 0; i < len(testVals); i++ {
var err error
servers[i], err = NewKubernetesClusterV3FromLegacyCluster("_", &KubernetesCluster{
Name: testVals[i],
})
require.NoError(t, err)
}
return servers
}

testValsUnordered := []string{"d", "b", "a", "c"}

// Test descending.
sortBy := SortBy{Field: ResourceMetadataName, IsDesc: true}
clusters := KubeClusters(makeClusters(testValsUnordered, ResourceMetadataName))
require.NoError(t, clusters.SortByCustom(sortBy))
targetVals, err := clusters.GetFieldVals(ResourceMetadataName)
require.NoError(t, err)
require.IsDecreasing(t, targetVals)

// Test ascending.
sortBy = SortBy{Field: ResourceMetadataName}
clusters = KubeClusters(makeClusters(testValsUnordered, ResourceMetadataName))
require.NoError(t, clusters.SortByCustom(sortBy))
targetVals, err = clusters.GetFieldVals(ResourceMetadataName)
require.NoError(t, err)
require.IsIncreasing(t, targetVals)

// Test error.
sortBy = SortBy{Field: "unsupported"}
clusters = KubeClusters(makeClusters(testValsUnordered, ResourceMetadataName))
require.True(t, trace.IsNotImplemented(clusters.SortByCustom(sortBy)))
}

func TestDeduplicateKubeClusters(t *testing.T) {
t.Parallel()

expected := []KubeCluster{
&KubernetesClusterV3{Metadata: Metadata{Name: "a"}},
&KubernetesClusterV3{Metadata: Metadata{Name: "b"}},
&KubernetesClusterV3{Metadata: Metadata{Name: "c"}},
}

extra := []KubeCluster{
&KubernetesClusterV3{Metadata: Metadata{Name: "a"}},
&KubernetesClusterV3{Metadata: Metadata{Name: "a"}},
&KubernetesClusterV3{Metadata: Metadata{Name: "b"}},
}

clusters := append(expected, extra...)

result := DeduplicateKubeClusters(clusters)
require.ElementsMatch(t, result, expected)
}
13 changes: 13 additions & 0 deletions api/types/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,19 @@ func (r ResourcesWithLabels) AsWindowsDesktops() ([]WindowsDesktop, error) {
return desktops, nil
}

// AsKubeClusters converts each resource into type KubeCluster.
func (r ResourcesWithLabels) AsKubeClusters() ([]KubeCluster, error) {
clusters := make([]KubeCluster, 0, len(r))
for _, resource := range r {
cluster, ok := resource.(KubeCluster)
if !ok {
return nil, trace.BadParameter("expected types.KubeCluster, got: %T", resource)
}
clusters = append(clusters, cluster)
}
return clusters, nil
}

// GetVersion returns resource version
func (h *ResourceHeader) GetVersion() string {
return h.Version
Expand Down
28 changes: 26 additions & 2 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,30 @@ func (a *ServerWithRoles) listResourcesWithSort(ctx context.Context, req proto.L
}
resources = servers.AsResources()

case types.KindKubernetesCluster:
kubeservices, err := a.GetKubeServices(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

// Extract kube clusters into its own list.
clusters := []types.KubeCluster{}
for _, svc := range kubeservices {
for _, legacyCluster := range svc.GetKubernetesClusters() {
cluster, err := types.NewKubernetesClusterV3FromLegacyCluster(svc.GetNamespace(), legacyCluster)
if err != nil {
return nil, trace.Wrap(err)
}
clusters = append(clusters, cluster)
}
}

sortedClusters := types.KubeClusters(types.DeduplicateKubeClusters(clusters))
if err := sortedClusters.SortByCustom(req.SortBy); err != nil {
return nil, trace.Wrap(err)
}
resources = sortedClusters.AsResources()

case types.KindWindowsDesktop:
windowsdesktops, err := a.GetWindowsDesktops(ctx, req.GetWindowsDesktopFilter())
if err != nil {
Expand Down Expand Up @@ -1741,8 +1765,8 @@ func (a *ServerWithRoles) GenerateKeyPair(pass string) ([]byte, []byte, error) {
}

func (a *ServerWithRoles) GenerateHostCert(
key []byte, hostID, nodeName string, principals []string, clusterName string, role types.SystemRole, ttl time.Duration) ([]byte, error) {

key []byte, hostID, nodeName string, principals []string, clusterName string, role types.SystemRole, ttl time.Duration,
) ([]byte, error) {
if err := a.action(apidefaults.Namespace, types.KindHostCert, types.VerbCreate); err != nil {
return nil, trace.Wrap(err)
}
Expand Down
105 changes: 105 additions & 0 deletions lib/auth/auth_with_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2296,3 +2296,108 @@ func TestGetAndList_WindowsDesktops(t *testing.T) {
require.Len(t, resp.Resources, 0)
require.Empty(t, cmp.Diff([]types.ResourceWithLabels{}, resp.Resources))
}

func TestListResources_KindKubernetesCluster(t *testing.T) {
t.Parallel()
ctx := context.Background()
srv, err := NewTestAuthServer(TestAuthServerConfig{Dir: t.TempDir()})
require.NoError(t, err)

authContext, err := srv.Authorizer.Authorize(context.WithValue(ctx, ContextUser, TestBuiltin(types.RoleProxy).I))
require.NoError(t, err)

s := &ServerWithRoles{
authServer: srv.AuthServer,
sessions: srv.SessionServer,
alog: srv.AuditLog,
context: *authContext,
}

testNames := []string{"a", "b", "c", "d"}

// Add some kube services.
kubeService, err := types.NewServer("bar", types.KindKubeService, types.ServerSpecV2{
KubernetesClusters: []*types.KubernetesCluster{{Name: "d"}, {Name: "b"}, {Name: "a"}},
})
require.NoError(t, err)
_, err = s.UpsertKubeServiceV2(ctx, kubeService)
require.NoError(t, err)

// Include a duplicate cluster name to test deduplicate.
kubeService, err = types.NewServer("foo", types.KindKubeService, types.ServerSpecV2{
KubernetesClusters: []*types.KubernetesCluster{{Name: "a"}, {Name: "c"}},
})
require.NoError(t, err)
_, err = s.UpsertKubeServiceV2(ctx, kubeService)
require.NoError(t, err)

// Test upsert.
kubeservices, err := s.GetKubeServices(ctx)
require.NoError(t, err)
require.Len(t, kubeservices, 2)

t.Run("fetch all", func(t *testing.T) {
t.Parallel()

res, err := s.ListResources(ctx, proto.ListResourcesRequest{
ResourceType: types.KindKubernetesCluster,
Limit: 10,
})
require.NoError(t, err)
require.Len(t, res.Resources, len(testNames))
require.Empty(t, res.NextKey)
require.Empty(t, res.TotalCount)

clusters, err := types.ResourcesWithLabels(res.Resources).AsKubeClusters()
require.NoError(t, err)
names, err := types.KubeClusters(clusters).GetFieldVals(types.ResourceMetadataName)
require.NoError(t, err)
require.ElementsMatch(t, names, testNames)
})

t.Run("start keys", func(t *testing.T) {
t.Parallel()

// First fetch.
res, err := s.ListResources(ctx, proto.ListResourcesRequest{
ResourceType: types.KindKubernetesCluster,
Limit: 1,
})
require.NoError(t, err)
require.Len(t, res.Resources, 1)
require.Equal(t, kubeservices[0].GetKubernetesClusters()[1].Name, res.NextKey)

// Second fetch.
res, err = s.ListResources(ctx, proto.ListResourcesRequest{
ResourceType: types.KindKubernetesCluster,
Limit: 1,
StartKey: res.NextKey,
})
require.NoError(t, err)
require.Len(t, res.Resources, 1)
require.Equal(t, kubeservices[0].GetKubernetesClusters()[2].Name, res.NextKey)
})

t.Run("fetch with sort and total count", func(t *testing.T) {
t.Parallel()
res, err := s.ListResources(ctx, proto.ListResourcesRequest{
ResourceType: types.KindKubernetesCluster,
Limit: 10,
SortBy: types.SortBy{
IsDesc: true,
Field: types.ResourceMetadataName,
},
NeedTotalCount: true,
})
require.NoError(t, err)
require.Empty(t, res.NextKey)
require.Len(t, res.Resources, len(testNames))
require.Equal(t, res.TotalCount, len(testNames))

clusters, err := types.ResourcesWithLabels(res.Resources).AsKubeClusters()
require.NoError(t, err)
names, err := types.KubeClusters(clusters).GetFieldVals(types.ResourceMetadataName)
require.NoError(t, err)
require.IsDecreasing(t, names)
})
}
Loading

0 comments on commit 0abd136

Please sign in to comment.