Skip to content

Commit

Permalink
Update webapi to use new ListResources (node/app/db/kube)
Browse files Browse the repository at this point in the history
  • Loading branch information
kimlisa committed Mar 10, 2022
1 parent 3fc479c commit 6d6f2b0
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 39 deletions.
16 changes: 12 additions & 4 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1822,13 +1822,21 @@ func (h *Handler) clusterNodesGet(w http.ResponseWriter, r *http.Request, p http
if err != nil {
return nil, trace.Wrap(err)
}
servers, err := clt.GetNodes(r.Context(), apidefaults.Namespace)

resp, err := listResources(clt, r, types.KindNode)
if err != nil {
return nil, trace.Wrap(err)
}

servers, err := types.ResourcesWithLabels(resp.Resources).AsServers()
if err != nil {
return nil, trace.Wrap(err)
}

return listResourcesGetResponse{
Items: ui.MakeServers(site.GetName(), servers),
Items: ui.MakeServers(site.GetName(), servers),
StartKey: resp.NextKey,
TotalCount: resp.TotalCount,
}, nil
}

Expand All @@ -1850,8 +1858,8 @@ func (h *Handler) siteNodeConnect(
r *http.Request,
p httprouter.Params,
ctx *SessionContext,
site reversetunnel.RemoteSite) (interface{}, error) {

site reversetunnel.RemoteSite,
) (interface{}, error) {
q := r.URL.Query()
params := q.Get("params")
if params == "" {
Expand Down
77 changes: 71 additions & 6 deletions lib/web/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ func (s *WebSuite) TestSAMLSuccess(c *C) {

err = s.server.Auth().CreateSAMLConnector(connector)
c.Assert(err, IsNil)
s.server.Auth().SetClock(clockwork.NewFakeClockAt(time.Date(2017, 05, 10, 18, 53, 0, 0, time.UTC)))
s.server.Auth().SetClock(clockwork.NewFakeClockAt(time.Date(2017, 0o5, 10, 18, 53, 0, 0, time.UTC)))
clt := s.clientNoRedirects()

csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"
Expand Down Expand Up @@ -772,6 +772,7 @@ func TestClusterNodesGet(t *testing.T) {
nodes := clusterNodesGetResponse{}
require.NoError(t, json.Unmarshal(re.Bytes(), &nodes))
require.Len(t, nodes.Items, 1)
require.Equal(t, nodes.TotalCount, 1)

// Get nodes using shortcut.
re, err = pack.clt.Get(context.Background(), pack.clt.Endpoint("webapi", "sites", currentSiteShortcut, "nodes"), url.Values{})
Expand Down Expand Up @@ -1971,7 +1972,8 @@ func TestClusterDatabasesGet(t *testing.T) {
require.NoError(t, err)

type testResponse struct {
Items []ui.Database `json:"items"`
Items []ui.Database `json:"items"`
TotalCount int `json:"totalCount"`
}

// No db registered.
Expand Down Expand Up @@ -2001,6 +2003,7 @@ func TestClusterDatabasesGet(t *testing.T) {
resp = testResponse{}
require.NoError(t, json.Unmarshal(re.Bytes(), &resp))
require.Len(t, resp.Items, 1)
require.Equal(t, resp.TotalCount, 1)
require.EqualValues(t, ui.Database{
Name: "test-db-name",
Desc: "test-description",
Expand All @@ -2021,7 +2024,8 @@ func TestClusterKubesGet(t *testing.T) {
require.NoError(t, err)

type testResponse struct {
Items []ui.Kube `json:"items"`
Items []ui.KubeCluster `json:"items"`
TotalCount int `json:"totalCount"`
}

// No kube registered.
Expand Down Expand Up @@ -2056,12 +2060,73 @@ func TestClusterKubesGet(t *testing.T) {
resp = testResponse{}
require.NoError(t, json.Unmarshal(re.Bytes(), &resp))
require.Len(t, resp.Items, 1)
require.EqualValues(t, ui.Kube{
require.Equal(t, resp.TotalCount, 1)
require.EqualValues(t, ui.KubeCluster{
Name: "test-kube-name",
Labels: []ui.Label{{Name: "test-field", Value: "test-value"}},
}, resp.Items[0])
}

func TestClusterAppsGet(t *testing.T) {
env := newWebPack(t, 1)

proxy := env.proxies[0]
pack := proxy.authPack(t, "[email protected]")

endpoint := pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "apps")
re, err := pack.clt.Get(context.Background(), endpoint, url.Values{})
require.NoError(t, err)

type testResponse struct {
Items []ui.App `json:"items"`
TotalCount int `json:"totalCount"`
}

resource := &types.AppServerV3{
Metadata: types.Metadata{Name: "test-app"},
Kind: types.KindAppServer,
Version: types.V2,
Spec: types.AppServerSpecV3{
HostID: "hostid",
App: &types.AppV3{
Metadata: types.Metadata{
Name: "name",
Description: "description",
Labels: map[string]string{"test-field": "test-value"},
},
Spec: types.AppSpecV3{
URI: "https://console.aws.amazon.com", // sets field awsConsole to true
PublicAddr: "publicaddrs",
},
},
},
}

// Register a app service.
_, err = env.server.Auth().UpsertApplicationServer(context.Background(), resource)
require.NoError(t, err)

// Make the call.
re, err = pack.clt.Get(context.Background(), endpoint, url.Values{})
require.NoError(t, err)

// Test correct response.
resp := testResponse{}
require.NoError(t, json.Unmarshal(re.Bytes(), &resp))
require.Len(t, resp.Items, 1)
require.Equal(t, resp.TotalCount, 1)
require.EqualValues(t, ui.App{
Name: resource.Spec.App.GetName(),
Description: resource.Spec.App.GetDescription(),
URI: resource.Spec.App.GetURI(),
PublicAddr: resource.Spec.App.GetPublicAddr(),
Labels: []ui.Label{{Name: "test-field", Value: "test-value"}},
FQDN: resource.Spec.App.GetPublicAddr(),
ClusterID: env.server.ClusterName(),
AWSConsole: true,
}, resp.Items[0])
}

// TestApplicationAccessDisabled makes sure application access can be disabled
// via modules.
func TestApplicationAccessDisabled(t *testing.T) {
Expand Down Expand Up @@ -3242,8 +3307,8 @@ func newWebPack(t *testing.T, numProxies int) *webPack {
}

func createProxy(ctx context.Context, t *testing.T, proxyID string, node *regular.Server, authServer *auth.TestTLSServer,
hostSigners []ssh.Signer, clock clockwork.FakeClock) *proxy {

hostSigners []ssh.Signer, clock clockwork.FakeClock,
) *proxy {
// create reverse tunnel service:
client, err := authServer.NewClient(auth.TestIdentity{
I: auth.BuiltinRole{
Expand Down
9 changes: 8 additions & 1 deletion lib/web/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ func (h *Handler) clusterAppsGet(w http.ResponseWriter, r *http.Request, p httpr
return nil, trace.Wrap(err)
}

appServers, err := clt.GetApplicationServers(r.Context(), apidefaults.Namespace)
resp, err := listResources(clt, r, types.KindAppServer)
if err != nil {
return nil, trace.Wrap(err)
}

appServers, err := types.ResourcesWithLabels(resp.Resources).AsAppServers()
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -71,6 +76,8 @@ func (h *Handler) clusterAppsGet(w http.ResponseWriter, r *http.Request, p httpr
Identity: identity,
Apps: types.DeduplicateApps(apps),
}),
StartKey: resp.NextKey,
TotalCount: resp.TotalCount,
}, nil
}

Expand Down
43 changes: 43 additions & 0 deletions lib/web/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import (
"net/http"
"strings"

"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/web/ui"
Expand Down Expand Up @@ -288,6 +291,44 @@ func ExtractResourceAndValidate(yaml string) (*services.UnknownResource, error)
return &unknownRes, nil
}

// listResources gets a list of resources depending on the type of resource.
func listResources(clt resourcesAPIGetter, r *http.Request, resourceKind string) (*types.ListResourcesResponse, error) {
values := r.URL.Query()

limit, err := queryLimit(values, "limit", defaults.MaxIterationLimit)
if err != nil {
return nil, trace.Wrap(err)
}

// Sort is expected in format `<fieldName>:<asc|desc>` where
// index 0 is fieldName and index 1 is direction.
// If a direction is not set, or is not recognized, it defaults to ASC.
sortBy := types.SortBy{}
sortParam := values.Get("sort")
if sortParam != "" {
vals := strings.Split(sortParam, ":")
if vals[0] != "" {
sortBy.Field = vals[0]
if len(vals) > 1 && vals[1] == "desc" {
sortBy.IsDesc = true
}
}
}

startKey := values.Get("startKey")
req := proto.ListResourcesRequest{
ResourceType: resourceKind,
Limit: int32(limit),
StartKey: startKey,
NeedTotalCount: startKey == "",
SortBy: sortBy,
PredicateExpression: values.Get("query"),
SearchKeywords: client.ParseSearchKeywords(values.Get("search"), ' '),
}

return clt.ListResources(r.Context(), req)
}

type listResourcesGetResponse struct {
// Items is a list of resources retrieved.
Items interface{} `json:"items"`
Expand Down Expand Up @@ -321,4 +362,6 @@ type resourcesAPIGetter interface {
GetTrustedClusters(ctx context.Context) ([]types.TrustedCluster, error)
// DeleteTrustedCluster removes a TrustedCluster from the backend by name.
DeleteTrustedCluster(ctx context.Context, name string) error
// ListResoures returns a paginated list of resources.
ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error)
}
110 changes: 110 additions & 0 deletions lib/web/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ package web

import (
"context"
"net/http"
"testing"

"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/web/ui"

"github.com/gravitational/trace"
Expand Down Expand Up @@ -331,6 +334,104 @@ version: v2`
require.Contains(t, tc.Content, "name: test-goodcontent")
}

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

// Test parsing query params.
testCases := []struct {
name, url string
wantBadParamErr bool
expected proto.ListResourcesRequest
}{
{
name: "decode complex query correctly",
url: "https://dev:3080/login?query=(labels%5B%60%22test%22%60%5D%20%3D%3D%20%22%2B%3A'%2C%23*~%25%5E%22%20%26%26%20!exists(labels.tier))%20%7C%7C%20resource.spec.description%20!%3D%20%22weird%20example%20https%3A%2F%2Ffoo.dev%3A3080%3Fbar%3Da%2Cb%26baz%3Dbanana%22",
expected: proto.ListResourcesRequest{
ResourceType: types.KindNode,
Limit: defaults.MaxIterationLimit,
NeedTotalCount: true,
SearchKeywords: []string{},
PredicateExpression: "(labels[`\"test\"`] == \"+:',#*~%^\" && !exists(labels.tier)) || resource.spec.description != \"weird example https://foo.dev:3080?bar=a,b&baz=banana\"",
},
},
{
name: "all param defined and set",
url: `https://dev:3080/login?query=labels.env%20%3D%3D%20%22prod%22&limit=50&startKey=banana&sort=foo:desc&search=foo%2Bbar+baz+foo%2Cbar+%22some%20phrase%22`,
expected: proto.ListResourcesRequest{
ResourceType: types.KindNode,
Limit: 50,
StartKey: "banana",
SearchKeywords: []string{"foo+bar", "baz", "foo,bar", "some phrase"},
PredicateExpression: `labels.env == "prod"`,
SortBy: types.SortBy{Field: "foo", IsDesc: true},
},
},
{
name: "all query param defined but empty",
url: `https://dev:3080/login?query=&startKey=&search=&sort=&limit=&startKey=`,
expected: proto.ListResourcesRequest{
ResourceType: types.KindNode,
Limit: defaults.MaxIterationLimit,
SearchKeywords: []string{},
NeedTotalCount: true,
},
},
{
name: "sort partially defined: fieldName",
url: `https://dev:3080/login?sort=foo`,
expected: proto.ListResourcesRequest{
ResourceType: types.KindNode,
Limit: defaults.MaxIterationLimit,
SearchKeywords: []string{},
SortBy: types.SortBy{Field: "foo", IsDesc: false},
NeedTotalCount: true,
},
},
{
name: "sort partially defined: fieldName with colon",
url: `https://dev:3080/login?sort=foo:`,
expected: proto.ListResourcesRequest{
ResourceType: types.KindNode,
Limit: defaults.MaxIterationLimit,
SearchKeywords: []string{},
SortBy: types.SortBy{Field: "foo", IsDesc: false},
NeedTotalCount: true,
},
},
{
name: "invalid limit value",
wantBadParamErr: true,
url: `https://dev:3080/login?limit=12invalid`,
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

httpReq, err := http.NewRequest("", tc.url, nil)
require.NoError(t, err)

m := &mockedResourceAPIGetter{}
m.mockListResources = func(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) {
if !tc.wantBadParamErr {
require.Equal(t, tc.expected, req)
}
return nil, nil
}

_, err = listResources(m, httpReq, types.KindNode)
switch tc.wantBadParamErr {
case true:
require.True(t, trace.IsBadParameter(err))
default:
require.NoError(t, err)
}
})
}
}

type mockedResourceAPIGetter struct {
mockGetRole func(ctx context.Context, name string) (types.Role, error)
mockGetRoles func(ctx context.Context) ([]types.Role, error)
Expand All @@ -343,6 +444,7 @@ type mockedResourceAPIGetter struct {
mockGetTrustedCluster func(ctx context.Context, name string) (types.TrustedCluster, error)
mockGetTrustedClusters func(ctx context.Context) ([]types.TrustedCluster, error)
mockDeleteTrustedCluster func(ctx context.Context, name string) error
mockListResources func(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error)
}

func (m *mockedResourceAPIGetter) GetRole(ctx context.Context, name string) (types.Role, error) {
Expand Down Expand Up @@ -430,3 +532,11 @@ func (m *mockedResourceAPIGetter) DeleteTrustedCluster(ctx context.Context, name

return trace.NotImplemented("mockDeleteTrustedCluster not implemented")
}

func (m *mockedResourceAPIGetter) ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) {
if m.mockListResources != nil {
return m.mockListResources(ctx, req)
}

return nil, trace.NotImplemented("mockListResources not implemented")
}
Loading

0 comments on commit 6d6f2b0

Please sign in to comment.