Skip to content

Commit

Permalink
Web: Define fetch kube resource web api endpoint (#46741)
Browse files Browse the repository at this point in the history
* Define fetch kube resource web api endpoint

* Remove unused endpoints and funcs
  • Loading branch information
kimlisa authored Oct 4, 2024
1 parent e1e563f commit a6c3742
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 51 deletions.
2 changes: 1 addition & 1 deletion lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,7 @@ func (h *Handler) bindDefaultEndpoints() {

// Kube access handlers.
h.GET("/webapi/sites/:site/kubernetes", h.WithClusterAuth(h.clusterKubesGet))
h.GET("/webapi/sites/:site/pods", h.WithClusterAuth(h.clusterKubePodsGet))
h.GET("/webapi/sites/:site/kubernetes/resources", h.WithClusterAuth(h.clusterKubeResourcesGet))

// Github connector handlers
h.GET("/webapi/github/login/web", h.WithRedirect(h.githubLoginWeb))
Expand Down
145 changes: 107 additions & 38 deletions lib/web/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4259,7 +4259,7 @@ func TestClusterKubesGet(t *testing.T) {
}
}

func TestClusterKubePodsGet(t *testing.T) {
func TestClusterKubeResourcesGet(t *testing.T) {
t.Parallel()
kubeClusterName := "kube_cluster"

Expand All @@ -4278,6 +4278,10 @@ func TestClusterKubePodsGet(t *testing.T) {
Namespace: types.Wildcard,
Name: types.Wildcard,
},
{
Kind: types.KindKubeNamespace,
Name: types.Wildcard,
},
},
},
})
Expand All @@ -4296,11 +4300,15 @@ func TestClusterKubePodsGet(t *testing.T) {
tt := []struct {
name string
user string
kind string
kubeCluster string
expectedResponse []ui.KubeResource
wantErr bool
}{
{
name: "get pods from gRPC server",
user: "test-user@example.com",
name: "get pods from gRPC server",
kind: types.KindKubePod,
kubeCluster: kubeClusterName,
expectedResponse: []ui.KubeResource{
{
Kind: types.KindKubePod,
Expand All @@ -4318,6 +4326,38 @@ func TestClusterKubePodsGet(t *testing.T) {
},
},
},
{
name: "get namespaces",
kind: types.KindKubeNamespace,
kubeCluster: kubeClusterName,
expectedResponse: []ui.KubeResource{
{
Kind: types.KindKubeNamespace,
Name: "default",
Namespace: "",
Labels: []ui.Label{{Name: "app", Value: "test"}},
KubeCluster: kubeClusterName,
},
},
},
{
name: "missing kind",
kind: "",
kubeCluster: kubeClusterName,
wantErr: true,
},
{
name: "invalid kind",
kind: "invalid-kind",
kubeCluster: kubeClusterName,
wantErr: true,
},
{
name: "missing kube cluster",
kind: types.KindKubeNamespace,
kubeCluster: "",
wantErr: true,
},
}
proxy := env.proxies[0]
listener, err := net.Listen("tcp", "127.0.0.1:0")
Expand All @@ -4327,22 +4367,27 @@ func TestClusterKubePodsGet(t *testing.T) {
addr := utils.MustParseAddr(listener.Addr().String())
proxy.handler.handler.cfg.ProxyWebAddr = *addr

user := "test-user@example.com"
pack := proxy.authPack(t, user, roleWithFullAccess(user))

for _, tc := range tt {
tc := tc
t.Run(tc.name, func(t *testing.T) {
pack := proxy.authPack(t, tc.user, roleWithFullAccess(tc.user))

endpoint := pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "pods")
endpoint := pack.clt.Endpoint("webapi", "sites", env.server.ClusterName(), "kubernetes", "resources")
params := url.Values{}
params.Add("kubeCluster", kubeClusterName)
params.Add("kubeCluster", tc.kubeCluster)
params.Add("kind", tc.kind)
re, err := pack.clt.Get(context.Background(), endpoint, params)
require.NoError(t, err)

resp := testResponse{}
require.NoError(t, json.Unmarshal(re.Bytes(), &resp))
require.Len(t, resp.Items, 2)
require.Equal(t, 2, resp.TotalCount)
require.ElementsMatch(t, tc.expectedResponse, resp.Items)
if tc.wantErr {
require.True(t, trace.IsBadParameter(err))
} else {
require.NoError(t, err)
resp := testResponse{}
require.NoError(t, json.Unmarshal(re.Bytes(), &resp))
require.ElementsMatch(t, tc.expectedResponse, resp.Items)
}

})
}
}
Expand Down Expand Up @@ -9536,35 +9581,59 @@ type fakeKubeService struct {
}

func (s *fakeKubeService) ListKubernetesResources(ctx context.Context, req *kubeproto.ListKubernetesResourcesRequest) (*kubeproto.ListKubernetesResourcesResponse, error) {
return &kubeproto.ListKubernetesResourcesResponse{
Resources: []*types.KubernetesResourceV1{
{
Kind: types.KindKubePod,
Metadata: types.Metadata{
Name: "test-pod",
Labels: map[string]string{
"app": "test",
switch req.GetResourceType() {
case types.KindKubePod:
{
return &kubeproto.ListKubernetesResourcesResponse{
Resources: []*types.KubernetesResourceV1{
{
Kind: types.KindKubePod,
Metadata: types.Metadata{
Name: "test-pod",
Labels: map[string]string{
"app": "test",
},
},
Spec: types.KubernetesResourceSpecV1{
Namespace: "default",
},
},
},
Spec: types.KubernetesResourceSpecV1{
Namespace: "default",
},
},
{
Kind: types.KindKubePod,
Metadata: types.Metadata{
Name: "test-pod2",
Labels: map[string]string{
"app": "test2",
{
Kind: types.KindKubePod,
Metadata: types.Metadata{
Name: "test-pod2",
Labels: map[string]string{
"app": "test2",
},
},
Spec: types.KubernetesResourceSpecV1{
Namespace: "default",
},
},
},
Spec: types.KubernetesResourceSpecV1{
Namespace: "default",
TotalCount: 2,
}, nil
}
case types.KindKubeNamespace:
{
return &kubeproto.ListKubernetesResourcesResponse{
Resources: []*types.KubernetesResourceV1{
{
Kind: types.KindNamespace,
Metadata: types.Metadata{
Name: "default",
Labels: map[string]string{
"app": "test",
},
},
},
},
},
},
TotalCount: 2,
}, nil
TotalCount: 1,
}, nil
}
default:
return nil, trace.BadParameter("kubernetes resource kind %q is not mocked", req.GetResourceType())
}
}

func TestWebSocketAuthenticateRequest(t *testing.T) {
Expand Down
25 changes: 20 additions & 5 deletions lib/web/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package web

import (
"net/http"
"slices"

"github.com/gravitational/trace"
"github.com/julienschmidt/httprouter"
Expand Down Expand Up @@ -61,21 +62,35 @@ func (h *Handler) clusterKubesGet(w http.ResponseWriter, r *http.Request, p http
}, nil
}

// clusterKubePodsGet returns a list of Kubernetes Pods in a form the
// UI can present.
func (h *Handler) clusterKubePodsGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
// clusterKubeResourcesGet returns supported requested kubernetes subresources eg: pods, namespaces, secrets etc.
func (h *Handler) clusterKubeResourcesGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnelclient.RemoteSite) (interface{}, error) {
kind := r.URL.Query().Get("kind")
kubeCluster := r.URL.Query().Get("kubeCluster")

if kubeCluster == "" {
return nil, trace.BadParameter("missing param %q", "kubeCluster")
}

if kind == "" {
return nil, trace.BadParameter("missing param %q", "kind")
}

if !slices.Contains(types.KubernetesResourcesKinds, kind) {
return nil, trace.BadParameter("kind is not valid, valid kinds %v", types.KubernetesResourcesKinds)
}

clt, err := sctx.NewKubernetesServiceClient(r.Context(), h.cfg.ProxyWebAddr.Addr)
if err != nil {
return nil, trace.Wrap(err)
}

resp, err := listKubeResources(r.Context(), clt, r.URL.Query(), site.GetName(), types.KindKubePod)
resp, err := listKubeResources(r.Context(), clt, r.URL.Query(), site.GetName(), kind)
if err != nil {
return nil, trace.Wrap(err)
}

return listResourcesGetResponse{
Items: ui.MakeKubeResources(resp.Resources, r.URL.Query().Get("kubeCluster")),
Items: ui.MakeKubeResources(resp.Resources, kubeCluster),
StartKey: resp.NextKey,
TotalCount: int(resp.TotalCount),
}, nil
Expand Down
22 changes: 22 additions & 0 deletions web/packages/teleport/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type { WebauthnAssertionResponse } from './services/auth';
import type { PluginKind, Regions } from './services/integrations';
import type { ParticipantMode } from 'teleport/services/session';
import type { YamlSupportedResourceKind } from './services/yaml/types';
import type { KubeResourceKind } from './services/kube/types';

const cfg = {
/** @deprecated Use cfg.edition instead. */
Expand Down Expand Up @@ -249,6 +250,8 @@ const cfg = {

kubernetesPath:
'/v1/webapi/sites/:clusterId/kubernetes?searchAsRoles=:searchAsRoles?&limit=:limit?&startKey=:startKey?&query=:query?&search=:search?&sort=:sort?',
kubernetesResourcesPath:
'/v1/webapi/sites/:clusterId/kubernetes/resources?searchAsRoles=:searchAsRoles?&limit=:limit?&startKey=:startKey?&query=:query?&search=:search?&sort=:sort?&kubeCluster=:kubeCluster?&kubeNamespace=:kubeNamespace?&kind=:kind?',

usersPath: '/v1/webapi/users',
userWithUsernamePath: '/v1/webapi/users/:username',
Expand Down Expand Up @@ -880,6 +883,13 @@ const cfg = {
});
},

getKubernetesResourcesUrl(clusterId: string, params: UrlKubeResourcesParams) {
return generateResourcePath(cfg.api.kubernetesResourcesPath, {
clusterId,
...params,
});
},

getAuthnChallengeWithTokenUrl(tokenId: string) {
return generatePath(cfg.api.mfaAuthnChallengeWithTokenPath, {
tokenId,
Expand Down Expand Up @@ -1232,6 +1242,18 @@ export interface UrlResourcesParams {
kinds?: string[];
}

export interface UrlKubeResourcesParams {
query?: string;
search?: string;
sort?: SortType;
limit?: number;
startKey?: string;
searchAsRoles?: 'yes' | '';
kubeNamespace?: string;
kubeCluster: string;
kind: KubeResourceKind;
}

export interface UrlDeployServiceIamConfigureScriptParams {
integrationName: string;
region: Regions;
Expand Down
18 changes: 17 additions & 1 deletion web/packages/teleport/src/generateResourcePath.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import cfg, { UrlResourcesParams } from './config';
import cfg, { UrlKubeResourcesParams, UrlResourcesParams } from './config';
import generateResourcePath from './generateResourcePath';

test('undefined params are set to empty string', () => {
Expand Down Expand Up @@ -66,3 +66,19 @@ test('defined params but set to empty values are set to empty string', () => {
'/v1/webapi/sites/cluster/resources?searchAsRoles=&limit=&startKey=&kinds=&query=&search=&sort=&pinnedOnly=&includedResourceMode='
);
});

test('defined kube related params are set', () => {
const params: UrlKubeResourcesParams = {
kind: 'namespace',
kubeCluster: 'kubecluster',
kubeNamespace: 'kubenamespace',
};
expect(
generateResourcePath(cfg.api.kubernetesResourcesPath, {
clusterId: 'cluster',
...params,
})
).toStrictEqual(
'/v1/webapi/sites/cluster/kubernetes/resources?searchAsRoles=&limit=&startKey=&query=&search=&sort=&kubeCluster=kubecluster&kubeNamespace=kubenamespace&kind=namespace'
);
});
3 changes: 3 additions & 0 deletions web/packages/teleport/src/generateResourcePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ export default function generateResourcePath(
.replace(':search?', processedParams.search || '')
.replace(':searchAsRoles?', processedParams.searchAsRoles || '')
.replace(':sort?', processedParams.sort || '')
.replace(':kind?', processedParams.kind || '')
.replace(':kinds?', processedParams.kinds || '')
.replace(':kubeCluster?', processedParams.kubeCluster || '')
.replace(':kubeNamespace?', processedParams.kubeNamespace || '')
.replace(':pinnedOnly?', processedParams.pinnedOnly || '')
.replace(
':includedResourceMode?',
Expand Down
27 changes: 24 additions & 3 deletions web/packages/teleport/src/services/kube/kube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
*/

import api from 'teleport/services/api';
import cfg, { UrlResourcesParams } from 'teleport/config';
import cfg, {
UrlKubeResourcesParams,
UrlResourcesParams,
} from 'teleport/config';
import { ResourcesResponse } from 'teleport/services/agents';

import { Kube } from './types';
import makeKube from './makeKube';
import { Kube, KubeResourceResponse } from './types';
import { makeKube, makeKubeResource } from './makeKube';

class KubeService {
fetchKubernetes(
Expand All @@ -41,6 +44,24 @@ class KubeService {
};
});
}

fetchKubernetesResources(
clusterId,
params: UrlKubeResourcesParams,
signal?: AbortSignal
): Promise<KubeResourceResponse> {
return api
.get(cfg.getKubernetesResourcesUrl(clusterId, params), signal)
.then(json => {
const items = json?.items || [];

return {
items: items.map(makeKubeResource),
startKey: json?.startKey,
totalCount: json?.totalCount,
};
});
}
}

export default KubeService;
Loading

0 comments on commit a6c3742

Please sign in to comment.