teleport/lib/services/matchers_test.go
Lisa Kim a61d38a1a7
Add de-duplicating apps, dbs, and desktops when sorting/totalCount is needed (#12685)
Fixes total count errors for the web UI:
- Adds de-duplicating matches for `FakePaginate` func. 
- This change does makes `FakePaginate` less efficient b/c 
  it will always run filter for every resource per fetch versus just 
  running it on the initial fetch to get the total count. 
- Removes inaccurate de-dupping in the web api layer since we 
  do it from the back now
- Adds another criteria when de-duplicating `applications` by checking 
  for its `public_addr` as well as its name.
2022-06-13 19:45:22 +00:00

525 lines
14 KiB
Go

/*
Copyright 2021 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 services
import (
"testing"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/trace"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
// TestMatchResourceLabels tests matching a resource against a selector.
func TestMatchResourceLabels(t *testing.T) {
tests := []struct {
description string
selectors []ResourceMatcher
databaseLabels map[string]string
match bool
}{
{
description: "wildcard selector matches empty labels",
selectors: []ResourceMatcher{
{Labels: types.Labels{types.Wildcard: []string{types.Wildcard}}},
},
databaseLabels: nil,
match: true,
},
{
description: "wildcard selector matches any label",
selectors: []ResourceMatcher{
{Labels: types.Labels{types.Wildcard: []string{types.Wildcard}}},
},
databaseLabels: map[string]string{
uuid.New().String(): uuid.New().String(),
uuid.New().String(): uuid.New().String(),
},
match: true,
},
{
description: "selector doesn't match empty labels",
selectors: []ResourceMatcher{
{Labels: types.Labels{"env": []string{"dev"}}},
},
databaseLabels: nil,
match: false,
},
{
description: "selector matches specific label",
selectors: []ResourceMatcher{
{Labels: types.Labels{"env": []string{"dev"}}},
},
databaseLabels: map[string]string{"env": "dev"},
match: true,
},
{
description: "selector doesn't match label",
selectors: []ResourceMatcher{
{Labels: types.Labels{"env": []string{"dev"}}},
},
databaseLabels: map[string]string{"env": "prod"},
match: false,
},
{
description: "selector matches label",
selectors: []ResourceMatcher{
{Labels: types.Labels{"env": []string{"dev", "prod"}}},
},
databaseLabels: map[string]string{"env": "prod"},
match: true,
},
{
description: "selector doesn't match multiple labels",
selectors: []ResourceMatcher{
{Labels: types.Labels{
"env": []string{"dev"},
"cluster": []string{"root"},
}},
},
databaseLabels: map[string]string{"cluster": "root"},
match: false,
},
{
description: "selector matches multiple labels",
selectors: []ResourceMatcher{
{Labels: types.Labels{
"env": []string{"dev"},
"cluster": []string{"root"},
}},
},
databaseLabels: map[string]string{"cluster": "root", "env": "dev"},
match: true,
},
{
description: "one of multiple selectors matches",
selectors: []ResourceMatcher{
{Labels: types.Labels{"env": []string{"dev"}}},
{Labels: types.Labels{"cluster": []string{"root"}}},
},
databaseLabels: map[string]string{"cluster": "root"},
match: true,
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
database, err := types.NewDatabaseV3(types.Metadata{
Name: "test",
Labels: test.databaseLabels,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
})
require.NoError(t, err)
require.Equal(t, test.match, MatchResourceLabels(test.selectors, database))
})
}
}
func TestMatchResourceByFilters_Helper(t *testing.T) {
t.Parallel()
server, err := types.NewServerWithLabels("banana", types.KindNode, types.ServerSpecV2{
Hostname: "foo",
Addr: "bar",
}, map[string]string{"env": "prod", "os": "mac"})
require.NoError(t, err)
resource := types.ResourceWithLabels(server)
testcases := []struct {
name string
filters MatchResourceFilter
assertErr require.ErrorAssertionFunc
assertMatch require.BoolAssertionFunc
}{
{
name: "empty filters",
assertErr: require.NoError,
assertMatch: require.True,
},
{
name: "all match",
filters: MatchResourceFilter{
PredicateExpression: `resource.spec.hostname == "foo"`,
SearchKeywords: []string{"banana"},
Labels: map[string]string{"os": "mac"},
},
assertErr: require.NoError,
assertMatch: require.True,
},
{
name: "no match",
filters: MatchResourceFilter{
PredicateExpression: `labels.env == "no-match"`,
SearchKeywords: []string{"no", "match"},
Labels: map[string]string{"no": "match"},
},
assertErr: require.NoError,
assertMatch: require.False,
},
{
name: "expression match",
filters: MatchResourceFilter{
PredicateExpression: `labels.env == "prod" && exists(labels.os)`,
},
assertErr: require.NoError,
assertMatch: require.True,
},
{
name: "no expression match",
filters: MatchResourceFilter{
PredicateExpression: `labels.env == "no-match"`,
},
assertErr: require.NoError,
assertMatch: require.False,
},
{
name: "error in expr",
filters: MatchResourceFilter{
PredicateExpression: `labels.env == prod`,
},
assertErr: require.Error,
assertMatch: require.False,
},
{
name: "label match",
filters: MatchResourceFilter{
Labels: map[string]string{"os": "mac"},
},
assertErr: require.NoError,
assertMatch: require.True,
},
{
name: "no label match",
filters: MatchResourceFilter{
Labels: map[string]string{"no": "match"},
},
assertErr: require.NoError,
assertMatch: require.False,
},
{
name: "search match",
filters: MatchResourceFilter{
SearchKeywords: []string{"mac", "env"},
},
assertErr: require.NoError,
assertMatch: require.True,
},
{
name: "no search match",
filters: MatchResourceFilter{
SearchKeywords: []string{"no", "match"},
},
assertErr: require.NoError,
assertMatch: require.False,
},
{
name: "partial match is no match: search",
filters: MatchResourceFilter{
PredicateExpression: `resource.spec.hostname == "foo"`,
Labels: map[string]string{"os": "mac"},
SearchKeywords: []string{"no", "match"},
},
assertErr: require.NoError,
assertMatch: require.False,
},
{
name: "partial match is no match: labels",
filters: MatchResourceFilter{
PredicateExpression: `resource.spec.hostname == "foo"`,
Labels: map[string]string{"no": "match"},
SearchKeywords: []string{"mac", "env"},
},
assertErr: require.NoError,
assertMatch: require.False,
},
{
name: "partial match is no match: expression",
filters: MatchResourceFilter{
PredicateExpression: `labels.env == "no-match"`,
Labels: map[string]string{"os": "mac"},
SearchKeywords: []string{"mac", "env"},
},
assertErr: require.NoError,
assertMatch: require.False,
},
}
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
match, err := matchResourceByFilters(resource, tc.filters)
tc.assertErr(t, err)
tc.assertMatch(t, match)
})
}
}
func TestMatchAndFilterKubeClusters(t *testing.T) {
t.Parallel()
getKubeService := func() types.Server {
kubeService, err := types.NewServer("_", types.KindKubeService, types.ServerSpecV2{
KubernetesClusters: []*types.KubernetesCluster{
{
Name: "cluster-1",
StaticLabels: map[string]string{"env": "prod", "os": "mac"},
},
{
Name: "cluster-2",
StaticLabels: map[string]string{"env": "staging", "os": "mac"},
},
{
Name: "cluster-3",
StaticLabels: map[string]string{"env": "prod", "os": "mac"},
},
},
})
require.NoError(t, err)
return kubeService
}
testcases := []struct {
name string
filters MatchResourceFilter
expectedLen int
assertMatch require.BoolAssertionFunc
}{
{
name: "empty values",
expectedLen: 3,
assertMatch: require.True,
},
{
name: "all match",
expectedLen: 3,
filters: MatchResourceFilter{
PredicateExpression: `labels.os == "mac"`,
},
assertMatch: require.True,
},
{
name: "some match",
expectedLen: 2,
filters: MatchResourceFilter{
PredicateExpression: `labels.env == "prod"`,
},
assertMatch: require.True,
},
{
name: "single match",
expectedLen: 1,
filters: MatchResourceFilter{
PredicateExpression: `labels.env == "staging"`,
},
assertMatch: require.True,
},
{
name: "no match",
filters: MatchResourceFilter{
PredicateExpression: `labels.env == "no-match"`,
},
assertMatch: require.False,
},
}
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
kubeService := getKubeService()
match, err := matchAndFilterKubeClusters(types.ResourceWithLabels(kubeService), tc.filters)
require.NoError(t, err)
tc.assertMatch(t, match)
require.Len(t, kubeService.GetKubernetesClusters(), tc.expectedLen)
})
}
}
// TestMatchResourceByFilters tests supported resource kinds and
// if a resource has contained resources, those contained resources
// are filtered instead.
func TestMatchResourceByFilters(t *testing.T) {
t.Parallel()
filterExpression := `resource.metadata.name == "foo"`
testcases := []struct {
name string
isKubeService bool
wantNotImplErr bool
filters MatchResourceFilter
resource func() types.ResourceWithLabels
}{
{
name: "no filter should return true",
resource: func() types.ResourceWithLabels {
server, err := types.NewServer("foo", types.KindNode, types.ServerSpecV2{})
require.NoError(t, err)
return server
},
filters: MatchResourceFilter{ResourceKind: types.KindNode},
},
{
name: "unsupported resource kind",
resource: func() types.ResourceWithLabels { return nil },
filters: MatchResourceFilter{
ResourceKind: "unsupported",
SearchKeywords: []string{"nothing"},
},
wantNotImplErr: true,
},
{
name: "app server",
resource: func() types.ResourceWithLabels {
appServer, err := types.NewAppServerV3(types.Metadata{
Name: "_",
}, types.AppServerSpecV3{
HostID: "_",
App: &types.AppV3{
Metadata: types.Metadata{Name: "foo"},
Spec: types.AppSpecV3{URI: "_"},
},
})
require.NoError(t, err)
return appServer
},
filters: MatchResourceFilter{
ResourceKind: types.KindAppServer,
PredicateExpression: filterExpression,
},
},
{
name: "db server",
resource: func() types.ResourceWithLabels {
dbServer, err := types.NewDatabaseServerV3(types.Metadata{
Name: "_",
}, types.DatabaseServerSpecV3{
HostID: "_",
Hostname: "_",
Database: &types.DatabaseV3{
Metadata: types.Metadata{Name: "foo"},
Spec: types.DatabaseSpecV3{
URI: "_",
Protocol: "_",
},
},
})
require.NoError(t, err)
return dbServer
},
filters: MatchResourceFilter{
ResourceKind: types.KindDatabaseServer,
PredicateExpression: filterExpression,
},
},
{
name: "kube service",
isKubeService: true,
resource: func() types.ResourceWithLabels {
dbServer, err := types.NewServer("_", types.KindKubeService, types.ServerSpecV2{
KubernetesClusters: []*types.KubernetesCluster{
{Name: "bar"},
{Name: "foo"},
{Name: "foo"},
},
})
require.NoError(t, err)
return dbServer
},
filters: MatchResourceFilter{
ResourceKind: types.KindKubeService,
PredicateExpression: filterExpression,
},
},
{
name: "kube cluster",
resource: func() types.ResourceWithLabels {
cluster, err := types.NewKubernetesClusterV3FromLegacyCluster("_", &types.KubernetesCluster{
Name: "foo",
})
require.NoError(t, err)
return cluster
},
filters: MatchResourceFilter{
ResourceKind: types.KindKubernetesCluster,
PredicateExpression: filterExpression,
},
},
{
name: "node",
resource: func() types.ResourceWithLabels {
server, err := types.NewServer("foo", types.KindNode, types.ServerSpecV2{})
require.NoError(t, err)
return server
},
filters: MatchResourceFilter{
ResourceKind: types.KindNode,
PredicateExpression: filterExpression,
},
},
{
name: "windows desktop",
resource: func() types.ResourceWithLabels {
desktop, err := types.NewWindowsDesktopV3("foo", nil, types.WindowsDesktopSpecV3{Addr: "_"})
require.NoError(t, err)
return desktop
},
filters: MatchResourceFilter{
ResourceKind: types.KindWindowsDesktop,
PredicateExpression: filterExpression,
},
},
}
for _, tc := range testcases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
resource := tc.resource()
match, err := MatchResourceByFilters(resource, tc.filters, nil)
switch tc.wantNotImplErr {
case true:
require.True(t, trace.IsNotImplemented(err))
require.False(t, match)
default:
require.NoError(t, err)
require.True(t, match)
}
if tc.isKubeService {
server, ok := resource.(types.Server)
require.True(t, ok)
require.Len(t, server.GetKubernetesClusters(), 2)
require.Equal(t, server.GetKubernetesClusters()[0].Name, "foo")
require.Equal(t, server.GetKubernetesClusters()[1].Name, "foo")
}
})
}
}