Skip to content

Commit

Permalink
Define fetch kube resource web api endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
kimlisa committed Sep 18, 2024
1 parent c86f46d commit d18358f
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 32 deletions.
1 change: 1 addition & 0 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,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
181 changes: 156 additions & 25 deletions lib/web/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4259,6 +4259,113 @@ func TestClusterKubesGet(t *testing.T) {
}
}

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

roleWithFullAccess := func(username string) []types.Role {
ret, err := types.NewRole(services.RoleNameForUser(username), types.RoleSpecV6{
Allow: types.RoleConditions{
Namespaces: []string{apidefaults.Namespace},
KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
Rules: []types.Rule{
types.NewRule(types.KindConnectionDiagnostic, services.RW()),
},
KubeGroups: []string{"groups"},
KubernetesResources: []types.KubernetesResource{
{
Kind: types.KindKubePod,
Namespace: types.Wildcard,
Name: types.Wildcard,
},
{
Kind: types.KindKubeNamespace,
Name: types.Wildcard,
},
},
},
})
require.NoError(t, err)
return []types.Role{ret}
}
require.NotNil(t, roleWithFullAccess)

env := newWebPack(t, 1)

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

tt := []struct {
name string
user string
kind string
expectedResponse []ui.KubeResource
}{
{
name: "get pods from gRPC server",
user: "test-user@example.com",
kind: types.KindKubePod,
expectedResponse: []ui.KubeResource{
{
Kind: types.KindKubePod,
Name: "test-pod",
Namespace: "default",
Labels: []ui.Label{{Name: "app", Value: "test"}},
KubeCluster: kubeClusterName,
},
{
Kind: types.KindKubePod,
Name: "test-pod2",
Namespace: "default",
Labels: []ui.Label{{Name: "app", Value: "test2"}},
KubeCluster: kubeClusterName,
},
},
},
{
name: "get namespaces",
user: "test-user2@example.com",
kind: types.KindKubeNamespace,
expectedResponse: []ui.KubeResource{
{
Kind: types.KindKubeNamespace,
Name: "default",
Namespace: "",
Labels: []ui.Label{{Name: "app", Value: "test"}},
KubeCluster: kubeClusterName,
},
},
},
}
proxy := env.proxies[0]
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
// Init fake gRPC Kube service.
initGRPCServer(t, env, listener)
addr := utils.MustParseAddr(listener.Addr().String())
proxy.handler.handler.cfg.ProxyWebAddr = *addr

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(), "kubernetes", "resources")
params := url.Values{}
params.Add("kubeCluster", kubeClusterName)
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.ElementsMatch(t, tc.expectedResponse, resp.Items)
})
}
}

func TestClusterKubePodsGet(t *testing.T) {
t.Parallel()
kubeClusterName := "kube_cluster"
Expand Down Expand Up @@ -9513,35 +9620,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
23 changes: 23 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 @@ -247,8 +248,11 @@ const cfg = {

// TODO(zmb3): remove this when Assist is no longer using it
sshPlaybackPrefix: '/v1/webapi/sites/:clusterId/sessions/:sid', // prefix because this is eventually concatenated with "/stream" or "/events"

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 +884,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 +1243,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;
17 changes: 15 additions & 2 deletions web/packages/teleport/src/services/kube/makeKube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { Kube } from './types';
import { Kube, KubeResource } from './types';

export default function makeKube(json): Kube {
export function makeKube(json): Kube {
const { name, requiresRequest } = json;
const labels = json.labels || [];

Expand All @@ -31,3 +31,16 @@ export default function makeKube(json): Kube {
requiresRequest,
};
}

export function makeKubeResource(json): KubeResource {
const { kind, name, namespace, cluster } = json;
const labels = json.labels || [];

return {
kind,
name,
namespace,
labels,
cluster,
};
}
Loading

0 comments on commit d18358f

Please sign in to comment.