Skip to content

Commit

Permalink
Address CR: store namespaces with kube cluster request
Browse files Browse the repository at this point in the history
  • Loading branch information
kimlisa committed Oct 18, 2024
1 parent 2e2afc8 commit 2042699
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function KubeNamespaceSelector({
namespaceRequired,
}: {
kubeClusterItem: PendingListItem;
fetchKubeNamespaces(p: KubeNamespaceRequest): Promise<Option[]>;
fetchKubeNamespaces(p: KubeNamespaceRequest): Promise<string[]>;
savedResourceItems: PendingListItem[];
toggleResource: (resource: PendingListItem) => void;
bulkToggleKubeResources: (
Expand Down Expand Up @@ -129,12 +129,16 @@ export function KubeNamespaceSelector({
};

async function handleLoadOptions(input: string) {
const options = await fetchKubeNamespaces({
const namespaces = await fetchKubeNamespaces({
kubeCluster: kubeClusterItem.id,
search: input,
});

return options;
return namespaces.map(namespace => ({
kind: 'namespace',
value: namespace,
label: namespace,
}));
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,10 @@ export const Success = () => (

const baseProps: RequestCheckoutWithSliderProps = {
fetchKubeNamespaces: async () => [
{ value: 'namespace1', label: 'namespace1' },
{ value: 'namespace2', label: 'namespace2' },
{ value: 'namespace3', label: 'namespace3' },
{ value: 'namespace4', label: 'namespace4' },
'namespace1',
'namespace2',
'namespace3',
'namespace4',
],
bulkToggleKubeResources: () => null,
createAttempt: { status: '' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,7 @@ export type RequestCheckoutProps<T extends PendingListItem = PendingListItem> =
Header?: () => JSX.Element;
startTime: Date;
onStartTimeChange(t?: Date): void;
fetchKubeNamespaces(p: KubeNamespaceRequest): Promise<Option[]>;
fetchKubeNamespaces(p: KubeNamespaceRequest): Promise<string[]>;
bulkToggleKubeResources(
kubeResources: PendingKubeResourceItem[],
kubeCluster: T
Expand Down
14 changes: 0 additions & 14 deletions web/packages/shared/components/AccessRequests/NewRequest/kube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import { Attempt } from 'shared/hooks/useAttemptNext';

import { PendingListItem } from './RequestCheckout';
import { RequestableResourceKind } from './resource';

export type KubeNamespaceRequest = {
kubeCluster: string;
Expand Down Expand Up @@ -93,16 +92,3 @@ export function checkForUnsupportedKubeRequestModes(
requiresNamespaceSelect,
};
}

export function requiresKubeResourceSelection({
dryRun,
requestMode,
kind,
}: {
dryRun: boolean;
requestMode: KubeResourceKind[];
kind: RequestableResourceKind;
}) {
const requiresKubeResourceSelection = requestMode.length > 0;
return dryRun && kind === 'kube_cluster' && requiresKubeResourceSelection;
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ export function AccessRequestCheckout() {
setShowCheckout(false);
}

const filteredData = data?.filter(d =>
const filteredData = data.filter(d =>
excludeKubeClusterWithNamespaces(d, data)
);

const numAddedResources = filteredData?.length;
const numAddedResources = filteredData.length;

// We should rather detect how much space we have,
// but for simplicity we only count items.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { useState, useEffect } from 'react';
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';

import useAttempt from 'shared/hooks/useAttemptNext';
import { Option } from 'shared/components/Select';

import {
getDryRunMaxDuration,
Expand All @@ -32,10 +31,8 @@ import { CreateRequest } from 'shared/components/AccessRequests/Shared/types';
import {
excludeKubeClusterWithNamespaces,
KubeNamespaceRequest,
requiresKubeResourceSelection,
} from 'shared/components/AccessRequests/NewRequest/kube';
import { PendingKubeResourceItem } from 'shared/components/AccessRequests/NewRequest/RequestCheckout/RequestCheckout';
import { KubeResourceKind } from 'teleport/services/kube';

import { useAppContext } from 'teleterm/ui/appContextProvider';
import {
Expand Down Expand Up @@ -64,13 +61,6 @@ export default function useAccessRequestCheckout() {
ctx.workspacesService?.getActiveWorkspace()?.localClusterUri;
const rootClusterUri = ctx.workspacesService?.getRootClusterUri();

const loggedInUser =
ctx.clustersService.findCluster(rootClusterUri)?.loggedInUser;
const allowedKubeSubresourceKinds =
loggedInUser?.requestMode?.kubernetesResources?.map(
r => r.kind as KubeResourceKind
) || [];

const {
selectedReviewers,
setSelectedReviewers,
Expand Down Expand Up @@ -122,7 +112,10 @@ export default function useAccessRequestCheckout() {
return;
}

const data = getPendingAccessRequestsPerResource(pendingAccessRequest);
const data = getPendingAccessRequestsPerResource({
pendingRequest: pendingAccessRequest,
excludeSubResourceParentResource: true,
});
runFetchResourceRoles(() =>
retryWithRelogin(ctx, clusterUri, async () => {
const { response } = await ctx.tshd.getRequestableRoles({
Expand Down Expand Up @@ -161,9 +154,21 @@ export default function useAccessRequestCheckout() {
}
}, [showCheckout, hasExited, createRequestAttempt.status]);

function getPendingAccessRequestsPerResource(
pendingRequest: PendingAccessRequest
): PendingListItemWithOriginalItem[] {
/**
*
* @param pendingRequest holds a list or map of resources to process
* @param excludeSubResourceParentResource when true, resources that have
* subresources, will be excluded from the returned list. eg:
* if a kube_cluster resource has a list of namespaces (subresources),
* then if this flag is true, kube_cluster will be excluded from the result.
*/
function getPendingAccessRequestsPerResource({
pendingRequest,
excludeSubResourceParentResource = false,
}: {
pendingRequest: PendingAccessRequest;
excludeSubResourceParentResource?: boolean;
}): PendingListItemWithOriginalItem[] {
const data: PendingListItemWithOriginalItem[] = [];
if (!workspaceAccessRequest) {
return data;
Expand All @@ -185,6 +190,34 @@ export default function useAccessRequestCheckout() {
}
case 'resource': {
pendingRequest.resources.forEach(resourceRequest => {
// If this request is a kube cluster and has namespaces
// extract each as own request.
if (
resourceRequest.kind === 'kube' &&
resourceRequest.resource.namespaces?.size > 0
) {
// Process each namespace.
resourceRequest.resource.namespaces.forEach(namespaceRequest => {
const { kind, id, name } =
extractResourceRequestProperties(namespaceRequest);

const item = {
kind,
id,
name,
subResourceName: name,
originalItem: namespaceRequest,
clusterName: ctx.clustersService.findClusterByResource(
namespaceRequest.resource.uri
)?.name,
};
data.push(item);
});
if (excludeSubResourceParentResource) {
return;
}
}

const { kind, id, name } =
extractResourceRequestProperties(resourceRequest);
const item: PendingListItemWithOriginalItem = {
Expand All @@ -196,10 +229,6 @@ export default function useAccessRequestCheckout() {
resourceRequest.resource.uri
)?.name,
};

if (kind === 'namespace') {
item.subResourceName = name;
}
data.push(item);
});
}
Expand All @@ -225,42 +254,6 @@ export default function useAccessRequestCheckout() {
await workspaceAccessRequest.addOrRemoveResource(
pendingListItem.originalItem
);

if (pendingListItem.kind === 'kube_cluster') {
deleteKubeClustersNamespaces({
kubeClusterUri: pendingListItem.originalItem.resource.uri,
kubeClusterId: pendingListItem.id,
});
}
}

async function deleteKubeClustersNamespaces({
kubeClusterUri,
kubeClusterId,
}: {
kubeClusterUri: string;
kubeClusterId: string;
}) {
const pending = workspaceAccessRequest.getPendingAccessRequest();
if (pending.kind === 'role') return;
const hasInsertedItem = pending.resources.has(kubeClusterUri);

if (!hasInsertedItem) {
const namespacesToDelete: ResourceRequest[] = [];
pending.resources.forEach(value => {
if (value.kind === 'namespace') {
const { kubeId } = routing.parseKubeResourceNamespaceUri(
value.resource.uri
).params;
if (kubeId === kubeClusterId) {
namespacesToDelete.push(value);
}
}
});
if (namespacesToDelete.length) {
await workspaceAccessRequest.addOrRemoveResources(namespacesToDelete);
}
}
}

async function bulkToggleKubeResources(
Expand Down Expand Up @@ -294,7 +287,10 @@ export default function useAccessRequestCheckout() {
* Shared logic used both during dry runs and regular access request creation.
*/
function prepareAndCreateRequest(req: CreateRequest) {
const data = getPendingAccessRequestsPerResource(pendingAccessRequest);
const data = getPendingAccessRequestsPerResource({
pendingRequest: pendingAccessRequest,
excludeSubResourceParentResource: true,
});

const params: CreateAccessRequestRequest = {
rootClusterUri,
Expand All @@ -304,17 +300,6 @@ export default function useAccessRequestCheckout() {
resourceIds: data
.filter(d => d.kind !== 'role')
.filter(d => excludeKubeClusterWithNamespaces(d, data))
// Skip dry running with kube_cluster that requires
// subresource selection. Otherwise the user will see
// an error saying they can't make kube_cluster requests.
.filter(
d =>
!requiresKubeResourceSelection({
dryRun: req.dryRun,
kind: d.kind,
requestMode: allowedKubeSubresourceKinds,
})
)
.map(d => {
if (d.kind === 'namespace') {
return {
Expand Down Expand Up @@ -374,6 +359,7 @@ export default function useAccessRequestCheckout() {
});
teletermAccessRequest = accessRequest;
} catch {
setCreateRequestAttempt({ status: '' });
return;
}

Expand Down Expand Up @@ -435,7 +421,7 @@ export default function useAccessRequestCheckout() {
async function fetchKubeNamespaces({
kubeCluster,
search,
}: KubeNamespaceRequest): Promise<Option[]> {
}: KubeNamespaceRequest): Promise<string[]> {
const { response } = await ctx.tshd.listKubernetesResources({
searchKeywords: search,
limit: 50,
Expand All @@ -447,13 +433,7 @@ export default function useAccessRequestCheckout() {
kubernetesCluster: kubeCluster,
kubernetesNamespace: '',
});
return response.resources.map(i => {
return {
kind: 'namespace',
value: i.name,
label: i.name,
};
});
return response.resources.map(i => i.name);
}

const shouldShowClusterNameColumn =
Expand All @@ -467,7 +447,9 @@ export default function useAccessRequestCheckout() {
isCollapsed,
assumedRequests: getAssumedRequests(),
toggleResource,
data: getPendingAccessRequestsPerResource(pendingAccessRequest),
data: getPendingAccessRequestsPerResource({
pendingRequest: pendingAccessRequest,
}),
shouldShowClusterNameColumn,
createRequest,
reset,
Expand Down Expand Up @@ -497,7 +479,6 @@ export default function useAccessRequestCheckout() {
onStartTimeChange,
fetchKubeNamespaces,
bulkToggleKubeResources,
allowedKubeSubresourceKinds,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export class AccessRequestsService {
const { resources } = draftState.pending;

requestedResources.forEach(request => {
if (request.kind === 'namespace') {
this.addOrRemoveKubeNamespace(request, resources);
return;
}
if (resources.has(request.resource.uri)) {
resources.delete(request.resource.uri);
} else {
Expand All @@ -127,6 +131,32 @@ export class AccessRequestsService {
});
}

async addOrRemoveKubeNamespace(
namespaceResourceRequest: ResourceRequest,
resources: Map<ResourceUri, ResourceRequest>
) {
const { uri: resourceUri } = namespaceResourceRequest.resource;

const requestedResource = resources.get(
routing.getKubeUri(
routing.parseKubeResourceNamespaceUri(resourceUri).params
)
);
if (!requestedResource || requestedResource.kind !== 'kube') {
throw new Error('Cannot add a kube namespace to a non-kube resource');
}
const kubeResource = requestedResource.resource;

if (!kubeResource.namespaces) {
kubeResource.namespaces = new Map();
}
if (kubeResource.namespaces.has(resourceUri)) {
kubeResource.namespaces.delete(resourceUri);
} else {
kubeResource.namespaces.set(resourceUri, namespaceResourceRequest);
}
}

/**
* Removes all requested resources, if all the requested resources were already added
* or adds all requested resources, if not all requested resources were added.
Expand Down Expand Up @@ -299,7 +329,7 @@ export type ResourceRequest =
kind: 'kube';
resource: {
uri: KubeUri;
namespaces?: KubeResourceNamespaceUri[];
namespaces?: Map<ResourceUri, ResourceRequest>;
};
}
| {
Expand Down

0 comments on commit 2042699

Please sign in to comment.