diff --git a/frontend/src/components/App/Home/index.tsx b/frontend/src/components/App/Home/index.tsx index fc41d1b357..802d3fdfda 100644 --- a/frontend/src/components/App/Home/index.tsx +++ b/frontend/src/components/App/Home/index.tsx @@ -213,7 +213,7 @@ function HomeComponent(props: HomeComponentProps) { /> } > - defaultSortingColumn={{ id: 'name', desc: false }} columns={[ { diff --git a/frontend/src/components/App/RouteSwitcher.tsx b/frontend/src/components/App/RouteSwitcher.tsx index a809199600..a03fb5d5a3 100644 --- a/frontend/src/components/App/RouteSwitcher.tsx +++ b/frontend/src/components/App/RouteSwitcher.tsx @@ -93,7 +93,7 @@ function PageTitle({ } interface AuthRouteProps { - children: React.ReactNode | JSX.Element; + children: React.ReactNode; sidebar: RouteType['sidebar']; requiresAuth: boolean; requiresCluster: boolean; diff --git a/frontend/src/components/DetailsViewSection/DetailsViewSection.stories.tsx b/frontend/src/components/DetailsViewSection/DetailsViewSection.stories.tsx index d028a86d42..e25163fa05 100644 --- a/frontend/src/components/DetailsViewSection/DetailsViewSection.stories.tsx +++ b/frontend/src/components/DetailsViewSection/DetailsViewSection.stories.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { useDispatch } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; +import { makeMockKubeObject } from '../../test/mocker'; import { SectionBox } from '../common'; import DetailsViewSection, { DetailsViewSectionProps } from './DetailsViewSection'; import { setDetailsView } from './detailsViewSectionSlice'; @@ -58,10 +59,10 @@ const Template: StoryFn = args => { export const MatchRenderIt = Template.bind({}); MatchRenderIt.args = { - resource: { kind: 'Node' }, + resource: makeMockKubeObject({ kind: 'Node' }), }; export const NoMatchNoRender = Template.bind({}); NoMatchNoRender.args = { - resource: { kind: 'DoesNotExist' }, + resource: makeMockKubeObject({ kind: 'DoesNotExist' }), }; diff --git a/frontend/src/components/DetailsViewSection/DetailsViewSection.tsx b/frontend/src/components/DetailsViewSection/DetailsViewSection.tsx index e9a518956e..cdfc1e1d00 100644 --- a/frontend/src/components/DetailsViewSection/DetailsViewSection.tsx +++ b/frontend/src/components/DetailsViewSection/DetailsViewSection.tsx @@ -1,5 +1,5 @@ import React, { isValidElement, ReactElement, ReactNode, useMemo } from 'react'; -import { KubeObject } from '../../lib/k8s/cluster'; +import { KubeObject } from '../../lib/k8s/KubeObject'; import { HeadlampEventType, useEventCallback } from '../../redux/headlampEventSlice'; import { useTypedSelector } from '../../redux/reducers/reducers'; import ErrorBoundary from '../common/ErrorBoundary'; @@ -8,7 +8,7 @@ export interface DetailsViewSectionProps { resource: KubeObject; } export type DetailsViewSectionType = - | ((...args: any[]) => JSX.Element | null | ReactNode) + | ((...args: any[]) => ReactNode) | null | ReactElement | ReactNode; diff --git a/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.test.ts b/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.test.ts index da30ac1244..497a23adbf 100644 --- a/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.test.ts +++ b/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.test.ts @@ -3,7 +3,6 @@ import detailsViewSectionReducer, { addDetailsViewSectionsProcessor, DefaultDetailsViewSection, DetailsViewSection, - DetailsViewSectionProcessorType, DetailsViewsSectionProcessor, setDetailsView, setDetailsViewSection, @@ -67,7 +66,7 @@ describe('detailsViewSectionSlice', () => { it('should add a new details view sections processor when provided as an object', () => { const processor: DetailsViewsSectionProcessor = { id: 'test-processor', - processor: info => info.actions, + processor: () => [{ id: 'test-section' }], }; store.dispatch(addDetailsViewSectionsProcessor(processor)); expect(store.getState().detailsViewSection.detailsViewSectionsProcessors).toEqual([ @@ -76,7 +75,7 @@ describe('detailsViewSectionSlice', () => { }); it('should add a new details view sections processor when provided as a function', () => { - const processorFunc: DetailsViewSectionProcessorType = info => info.actions; + const processorFunc = () => [{ id: 'test-section' }]; store.dispatch(addDetailsViewSectionsProcessor(processorFunc)); const processors = store.getState().detailsViewSection.detailsViewSectionsProcessors; expect(processors).toHaveLength(1); diff --git a/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.ts b/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.ts index e36d678637..a72440036d 100644 --- a/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.ts +++ b/frontend/src/components/DetailsViewSection/detailsViewSectionSlice.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { get, set } from 'lodash'; -import { KubeObject } from '../../lib/k8s/cluster'; +import { ReactNode } from 'react'; +import { KubeObject } from '../../lib/k8s/KubeObject'; import { DetailsViewSectionType } from './DetailsViewSection'; export type DetailsViewSection = { @@ -20,7 +21,7 @@ export enum DefaultDetailsViewSection { type HeaderActionFuncType = ( resource: KubeObject | null, - sections: DetailsViewSection[] + sections: (DetailsViewSection | ReactNode)[] ) => DetailsViewSection[]; export type DetailsViewsSectionProcessor = { diff --git a/frontend/src/components/cluster/Charts.tsx b/frontend/src/components/cluster/Charts.tsx index 104518ff80..e455ff41f2 100644 --- a/frontend/src/components/cluster/Charts.tsx +++ b/frontend/src/components/cluster/Charts.tsx @@ -1,7 +1,7 @@ import '../../i18n/config'; import { useTheme } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; -import { KubeObject } from '../../lib/k8s/cluster'; +import { KubeMetrics } from '../../lib/k8s/cluster'; import Node from '../../lib/k8s/node'; import Pod from '../../lib/k8s/pod'; import { parseCpu, parseRam, TO_GB, TO_ONE_CPU } from '../../lib/units'; @@ -14,11 +14,11 @@ export function MemoryCircularChart(props: ResourceCircularChartProps) { const { noMetrics } = props; const { t } = useTranslation(['translation', 'glossary']); - function memoryUsedGetter(item: KubeObject) { + function memoryUsedGetter(item: KubeMetrics) { return parseRam(item.usage.memory) / TO_GB; } - function memoryAvailableGetter(item: KubeObject) { + function memoryAvailableGetter(item: Node | Pod) { return parseRam(item.status!.capacity.memory) / TO_GB; } @@ -50,11 +50,11 @@ export function CpuCircularChart(props: ResourceCircularChartProps) { const { noMetrics } = props; const { t } = useTranslation(['translation', 'glossary']); - function cpuUsedGetter(item: KubeObject) { + function cpuUsedGetter(item: KubeMetrics) { return parseCpu(item.usage.cpu) / TO_ONE_CPU; } - function cpuAvailableGetter(item: KubeObject) { + function cpuAvailableGetter(item: Node | Pod) { return parseCpu(item.status!.capacity.cpu) / TO_ONE_CPU; } @@ -82,7 +82,7 @@ export function CpuCircularChart(props: ResourceCircularChartProps) { ); } -export function PodsStatusCircleChart(props: Pick) { +export function PodsStatusCircleChart(props: { items: Pod[] | null }) { const theme = useTheme(); const { items } = props; const { t } = useTranslation(['translation', 'glossary']); @@ -143,7 +143,7 @@ export function PodsStatusCircleChart(props: Pick) { +export function NodesStatusCircleChart(props: { items: Node[] | null }) { const theme = useTheme(); const { items } = props; const { t } = useTranslation(['translation', 'glossary']); diff --git a/frontend/src/components/common/Chart.tsx b/frontend/src/components/common/Chart.tsx index 1cbc40884e..0cbbfdc62f 100644 --- a/frontend/src/components/common/Chart.tsx +++ b/frontend/src/components/common/Chart.tsx @@ -3,7 +3,7 @@ import Paper from '@mui/material/Paper'; import { useTheme } from '@mui/material/styles'; import { styled } from '@mui/material/styles'; import Typography from '@mui/material/Typography'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { Bar, BarChart, @@ -28,8 +28,8 @@ export interface PercentageCircleProps { size?: number; dataKey?: string; label?: string | null; - title?: string | JSX.Element | null; - legend?: string | null; + title?: ReactNode; + legend?: ReactNode; total?: number; totalProps?: { [propName: string]: any; @@ -170,7 +170,7 @@ const StyledBarChart = styled(BarChart)(({ theme }) => ({ export interface PercentageBarProps { data: ChartDataPoint[]; total?: number; - tooltipFunc?: ((data: any) => JSX.Element | string) | null; + tooltipFunc?: ((data: any) => ReactNode) | null; } export function PercentageBar(props: PercentageBarProps) { diff --git a/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx b/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx index 12d51ec2c5..3c7e8357cb 100644 --- a/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx +++ b/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx @@ -3,8 +3,8 @@ import { eventAction, HeadlampEventType } from '../../../redux/headlampEventSlic import store from '../../../redux/stores/store'; export interface ErrorBoundaryProps { - fallback?: ComponentType<{ error: Error }> | ReactElement | null; - children: ReactNode; + fallback?: ReactElement<{ error: Error }> | ComponentType<{ error: Error }>; + children?: ReactNode; } interface State { @@ -50,13 +50,10 @@ export default class ErrorBoundary extends Component if (!error) { return this.props.children; } - if (isValidElement(this.props.fallback)) { - return this.props.fallback; + const FallbackComponent = this.props.fallback; + if (isValidElement(FallbackComponent)) { + return FallbackComponent; } - const FallbackComponent = this.props.fallback as - | ComponentType<{ error: Error }> - | undefined - | null; return FallbackComponent ? : null; } } diff --git a/frontend/src/components/common/LabelListItem.tsx b/frontend/src/components/common/LabelListItem.tsx index f191310805..a7946c78bd 100644 --- a/frontend/src/components/common/LabelListItem.tsx +++ b/frontend/src/components/common/LabelListItem.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { LightTooltip } from './Tooltip'; export interface LabelListItemProps { - labels: React.ReactNode[]; + labels?: React.ReactNode[]; } export default function LabelListItem(props: LabelListItemProps) { diff --git a/frontend/src/components/common/Link.tsx b/frontend/src/components/common/Link.tsx index ecbe7248e6..0551ee387e 100644 --- a/frontend/src/components/common/Link.tsx +++ b/frontend/src/components/common/Link.tsx @@ -1,7 +1,7 @@ import MuiLink from '@mui/material/Link'; import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; -import { makeKubeObject } from '../../lib/k8s/cluster'; +import { KubeObject } from '../../lib/k8s/KubeObject'; import { createRouteURL, RouteURLProps } from '../../lib/router'; import { LightTooltip } from './Tooltip'; @@ -24,7 +24,7 @@ export interface LinkProps extends LinkBaseProps { } export interface LinkObjectProps extends LinkBaseProps { - kubeObject: InstanceType>; + kubeObject?: KubeObject | null; [prop: string]: any; } @@ -32,8 +32,8 @@ function PureLink(props: React.PropsWithChildren) { if ((props as LinkObjectProps).kubeObject) { const { kubeObject, ...otherProps } = props as LinkObjectProps; return ( - - {props.children || kubeObject.getName()} + + {props.children || kubeObject!.getName()} ); } diff --git a/frontend/src/components/common/LogViewer.tsx b/frontend/src/components/common/LogViewer.tsx index 6213952b4c..b2a4d9a002 100644 --- a/frontend/src/components/common/LogViewer.tsx +++ b/frontend/src/components/common/LogViewer.tsx @@ -3,7 +3,7 @@ import { FitAddon } from '@xterm/addon-fit'; import { ISearchOptions, SearchAddon } from '@xterm/addon-search'; import { Terminal as XTerminal } from '@xterm/xterm'; import _ from 'lodash'; -import React, { useEffect } from 'react'; +import React, { ReactNode, useEffect } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import ActionButton from './ActionButton'; @@ -14,7 +14,7 @@ export interface LogViewerProps extends DialogProps { title?: string; downloadName?: string; onClose: () => void; - topActions?: JSX.Element[]; + topActions?: ReactNode[]; open: boolean; xtermRef?: React.MutableRefObject; /** diff --git a/frontend/src/components/common/NameValueTable/NameValueTable.tsx b/frontend/src/components/common/NameValueTable/NameValueTable.tsx index 946a0fa518..22c25d0da3 100644 --- a/frontend/src/components/common/NameValueTable/NameValueTable.tsx +++ b/frontend/src/components/common/NameValueTable/NameValueTable.tsx @@ -1,12 +1,13 @@ import { Grid, GridProps } from '@mui/material'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { ValueLabel } from '../Label'; +// TODO: use ReactNode after migration to react 18 export interface NameValueTableRow { /** The name (key) for this row */ - name: string | JSX.Element; + name: ReactNode; /** The value for this row */ - value?: string | JSX.Element | JSX.Element[]; + value?: ReactNode; /** Whether this row should be hidden (can be a boolean or a function that will take the * @param value and return a boolean) */ hide?: boolean | ((value: NameValueTableRow['value']) => boolean); @@ -21,11 +22,7 @@ export interface NameValueTableProps { valueCellProps?: GridProps; } -function Value({ - value, -}: { - value: string | JSX.Element | JSX.Element[] | undefined; -}): JSX.Element | null { +function Value({ value }: { value: ReactNode }): ReactNode { if (typeof value === 'undefined') { return null; } else if (typeof value === 'string') { diff --git a/frontend/src/components/common/ObjectEventList.tsx b/frontend/src/components/common/ObjectEventList.tsx index 1256a9bc4f..16cfa3a4e3 100644 --- a/frontend/src/components/common/ObjectEventList.tsx +++ b/frontend/src/components/common/ObjectEventList.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { KubeObject } from '../../lib/k8s/cluster'; import Event, { KubeEvent } from '../../lib/k8s/event'; +import { KubeObject } from '../../lib/k8s/KubeObject'; import { localeDate, timeAgo } from '../../lib/util'; import { HeadlampEventType, useEventCallback } from '../../redux/headlampEventSlice'; import { HoverInfoLabel, SectionBox, SimpleTable } from '../common'; diff --git a/frontend/src/components/common/Resource/AuthVisible.tsx b/frontend/src/components/common/Resource/AuthVisible.tsx index 170b4e4a47..2c8e191c4b 100644 --- a/frontend/src/components/common/Resource/AuthVisible.tsx +++ b/frontend/src/components/common/Resource/AuthVisible.tsx @@ -1,10 +1,11 @@ import { useQuery } from '@tanstack/react-query'; import React, { useEffect } from 'react'; -import { KubeObject } from '../../../lib/k8s/cluster'; +import { KubeObject } from '../../../lib/k8s/KubeObject'; +import { KubeObjectClass } from '../../../lib/k8s/KubeObject'; export interface AuthVisibleProps extends React.PropsWithChildren<{}> { /** The item for which auth will be checked or a resource class (e.g. Job). */ - item: KubeObject; + item: KubeObject | KubeObjectClass | null; /** The verb associated with the permissions being verifying. See https://kubernetes.io/docs/reference/access-authn-authz/authorization/#determine-the-request-verb . */ authVerb: string; /** The subresource for which the permissions are being verifyied (e.g. "log" when checking for a pod's log). */ @@ -31,7 +32,7 @@ export default function AuthVisible(props: AuthVisibleProps) { queryKey: ['authVisible', item, authVerb, subresource, namespace], queryFn: async () => { try { - const res = await item.getAuthorization(authVerb, { subresource, namespace }); + const res = await item!.getAuthorization(authVerb, { subresource, namespace }); return res; } catch (e: any) { onError?.(e); diff --git a/frontend/src/components/common/Resource/CircularChart.tsx b/frontend/src/components/common/Resource/CircularChart.tsx index eb2022a061..b13b338f2a 100644 --- a/frontend/src/components/common/Resource/CircularChart.tsx +++ b/frontend/src/components/common/Resource/CircularChart.tsx @@ -1,21 +1,24 @@ import '../../../i18n/config'; -import _ from 'lodash'; +import _, { List } from 'lodash'; import { useTranslation } from 'react-i18next'; -import { KubeMetrics, KubeObject } from '../../../lib/k8s/cluster'; +import { KubeMetrics } from '../../../lib/k8s/cluster'; +import { KubeObject } from '../../../lib/k8s/KubeObject'; +import Node from '../../../lib/k8s/node'; +import Pod from '../../../lib/k8s/pod'; import { PercentageCircleProps } from '../Chart'; import TileChart from '../TileChart'; export interface CircularChartProps extends Omit { /** Items to display in the chart (should have a corresponding value in @param itemsMetrics) */ - items: KubeObject[] | null; + items: Node[] | Pod[] | null; /** Metrics to display in the chart (for items in @param items) */ itemsMetrics: KubeMetrics[] | null; /** Whether no metrics are available. If true, then instead of a chart, a message will be displayed */ noMetrics?: boolean; /** Function to get the "used" value for the metrics in question */ - resourceUsedGetter?: (node: KubeObject) => number; + resourceUsedGetter?: (node: KubeMetrics) => number; /** Function to get the "available" value for the metrics in question */ - resourceAvailableGetter?: (node: KubeObject) => number; + resourceAvailableGetter?: (node: Node | Pod) => number; /** Function to create a legend for the data */ getLegend?: (used: number, available: number) => string; /** Tooltip to display when hovering over the chart */ @@ -56,7 +59,7 @@ export function CircularChart(props: CircularChartProps) { const nodeMetrics = filterMetrics(items, itemsMetrics); const usedValue = _.sumBy(nodeMetrics, resourceUsedGetter); - const availableValue = _.sumBy(items, resourceAvailableGetter); + const availableValue = _.sumBy(items as List, resourceAvailableGetter); return [usedValue, availableValue]; } diff --git a/frontend/src/components/common/Resource/CreateButton.tsx b/frontend/src/components/common/Resource/CreateButton.tsx index 2f54a265cc..4bd579fdb7 100644 --- a/frontend/src/components/common/Resource/CreateButton.tsx +++ b/frontend/src/components/common/Resource/CreateButton.tsx @@ -10,7 +10,7 @@ import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { useClusterGroup } from '../../../lib/k8s'; import { apply } from '../../../lib/k8s/apiProxy'; -import { KubeObjectInterface } from '../../../lib/k8s/cluster'; +import { KubeObjectInterface } from '../../../lib/k8s/KubeObject'; import { clusterAction } from '../../../redux/clusterActionSlice'; import { EventStatus, @@ -83,7 +83,7 @@ export default function CreateButton(props: CreateButtonProps) { if (massagedNewItemDefs[i].kind === 'List') { // flatten this List kind with the items that it has which is a list of valid k8s resources const deletedItem = massagedNewItemDefs.splice(i, 1); - massagedNewItemDefs = massagedNewItemDefs.concat(deletedItem[0].items); + massagedNewItemDefs = massagedNewItemDefs.concat(deletedItem[0].items!); } if (!massagedNewItemDefs[i].metadata?.name) { setErrorMessage( diff --git a/frontend/src/components/common/Resource/DeleteButton.tsx b/frontend/src/components/common/Resource/DeleteButton.tsx index a29e9b009f..e6d92f649e 100644 --- a/frontend/src/components/common/Resource/DeleteButton.tsx +++ b/frontend/src/components/common/Resource/DeleteButton.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { KubeObject } from '../../../lib/k8s/cluster'; +import { KubeObject } from '../../../lib/k8s/KubeObject'; import { CallbackActionOptions, clusterAction } from '../../../redux/clusterActionSlice'; import { EventStatus, diff --git a/frontend/src/components/common/Resource/EditButton.tsx b/frontend/src/components/common/Resource/EditButton.tsx index e838eadef0..79e6fe195b 100644 --- a/frontend/src/components/common/Resource/EditButton.tsx +++ b/frontend/src/components/common/Resource/EditButton.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { KubeObject, KubeObjectInterface } from '../../../lib/k8s/cluster'; +import { KubeObject } from '../../../lib/k8s/KubeObject'; +import { KubeObjectInterface } from '../../../lib/k8s/KubeObject'; import { CallbackActionOptions, clusterAction } from '../../../redux/clusterActionSlice'; import { EventStatus, diff --git a/frontend/src/components/common/Resource/EditorDialog.tsx b/frontend/src/components/common/Resource/EditorDialog.tsx index 7644723a4a..58bd07b2e6 100644 --- a/frontend/src/components/common/Resource/EditorDialog.tsx +++ b/frontend/src/components/common/Resource/EditorDialog.tsx @@ -18,7 +18,7 @@ import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { KubeObjectInterface } from '../../../lib/k8s/cluster'; +import { KubeObjectInterface } from '../../../lib/k8s/KubeObject'; import { getThemeName } from '../../../lib/themes'; import { useId } from '../../../lib/util'; import ConfirmButton from '../ConfirmButton'; diff --git a/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.tsx b/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.tsx index 9529c6a893..e479c44007 100644 --- a/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.tsx +++ b/frontend/src/components/common/Resource/MainInfoSection/MainInfoSection.tsx @@ -2,7 +2,7 @@ import Paper from '@mui/material/Paper'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import { KubeObject } from '../../../../lib/k8s/cluster'; +import { KubeObject } from '../../../../lib/k8s/KubeObject'; import { createRouteURL } from '../../../../lib/router'; import { HeaderAction } from '../../../../redux/actionButtonsSlice'; import Loader from '../../../common/Loader'; @@ -13,16 +13,13 @@ import SectionBox from '../../SectionBox'; import { MetadataDisplay } from '../MetadataDisplay'; import { MainInfoHeader } from './MainInfoSectionHeader'; -export interface MainInfoSectionProps { - resource: KubeObject | null; - headerSection?: ((resource: KubeObject | null) => React.ReactNode) | React.ReactNode; +export interface MainInfoSectionProps { + resource: T | null; + headerSection?: ((resource: T | null) => React.ReactNode) | React.ReactNode; title?: string; - extraInfo?: - | ((resource: KubeObject | null) => NameValueTableRow[] | null) - | NameValueTableRow[] - | null; + extraInfo?: ((resource: T | null) => NameValueTableRow[] | null) | NameValueTableRow[] | null; actions?: - | ((resource: KubeObject | null) => React.ReactNode[] | HeaderAction[] | null) + | ((resource: T | null) => React.ReactNode[] | HeaderAction[] | null) | React.ReactNode[] | null | HeaderAction[]; @@ -33,7 +30,7 @@ export interface MainInfoSectionProps { error?: string | Error | null; } -export function MainInfoSection(props: MainInfoSectionProps) { +export function MainInfoSection(props: MainInfoSectionProps) { const { resource, headerSection, diff --git a/frontend/src/components/common/Resource/MainInfoSection/MainInfoSectionHeader.tsx b/frontend/src/components/common/Resource/MainInfoSection/MainInfoSectionHeader.tsx index b61cb4b47d..3a7783d083 100644 --- a/frontend/src/components/common/Resource/MainInfoSection/MainInfoSectionHeader.tsx +++ b/frontend/src/components/common/Resource/MainInfoSection/MainInfoSectionHeader.tsx @@ -1,7 +1,7 @@ import { has } from 'lodash'; import React, { isValidElement } from 'react'; import { useLocation } from 'react-router-dom'; -import { KubeObject } from '../../../../lib/k8s/cluster'; +import { KubeObject } from '../../../../lib/k8s/KubeObject'; import { DefaultHeaderAction, HeaderAction, @@ -15,12 +15,12 @@ import EditButton from '../EditButton'; import { RestartButton } from '../RestartButton'; import ScaleButton from '../ScaleButton'; -export interface MainInfoHeaderProps { - resource: KubeObject | null; - headerSection?: ((resource: KubeObject | null) => React.ReactNode) | React.ReactNode; +export interface MainInfoHeaderProps { + resource: T | null; + headerSection?: ((resource: T | null) => React.ReactNode) | React.ReactNode; title?: string; actions?: - | ((resource: KubeObject | null) => React.ReactNode[] | HeaderAction[] | null) + | ((resource: T | null) => React.ReactNode[] | HeaderAction[] | null) | React.ReactNode[] | null | HeaderAction[]; @@ -30,7 +30,7 @@ export interface MainInfoHeaderProps { backLink?: string | ReturnType | null; } -export function MainInfoHeader(props: MainInfoHeaderProps) { +export function MainInfoHeader(props: MainInfoHeaderProps) { const { resource, title, actions = [], headerStyle = 'main', noDefaultActions = false } = props; const headerActions = useTypedSelector(state => state.actionButtons.headerActions); const headerActionsProcessors = useTypedSelector( diff --git a/frontend/src/components/common/Resource/MetadataDisplay.stories.tsx b/frontend/src/components/common/Resource/MetadataDisplay.stories.tsx index 69f33aa229..d99c909677 100644 --- a/frontend/src/components/common/Resource/MetadataDisplay.stories.tsx +++ b/frontend/src/components/common/Resource/MetadataDisplay.stories.tsx @@ -1,5 +1,5 @@ import { Meta, StoryFn } from '@storybook/react'; -import { KubeObjectInterface } from '../../../lib/k8s/cluster'; +import { KubeObjectInterface } from '../../../lib/k8s/KubeObject'; import { TestContext } from '../../../test'; import { MetadataDisplay as MetadataDisplayComponent, @@ -18,7 +18,7 @@ export default { ], } as Meta; -const Template: StoryFn = args => ; +const Template: StoryFn> = args => ; const mockResource: KubeObjectInterface = { kind: 'MyKind', diff --git a/frontend/src/components/common/Resource/MetadataDisplay.tsx b/frontend/src/components/common/Resource/MetadataDisplay.tsx index e7b11f6e7c..40df4fe9f7 100644 --- a/frontend/src/components/common/Resource/MetadataDisplay.tsx +++ b/frontend/src/components/common/Resource/MetadataDisplay.tsx @@ -6,14 +6,15 @@ import Typography, { TypographyProps } from '@mui/material/Typography'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { ResourceClasses } from '../../../lib/k8s'; -import { KubeObject, KubeObjectInterface, KubeOwnerReference } from '../../../lib/k8s/cluster'; +import { KubeOwnerReference } from '../../../lib/k8s/cluster'; +import { KubeObject } from '../../../lib/k8s/KubeObject'; import Theme from '../../../lib/themes'; import { localeDate } from '../../../lib/util'; import { NameValueTable, NameValueTableRow } from '../../common/SimpleTable'; import Link from '../Link'; import { LightTooltip } from '../Tooltip'; -type ExtraRowsFunc = (resource: KubeObjectInterface) => NameValueTableRow[] | null; +type ExtraRowsFunc = (resource: T) => NameValueTableRow[] | null; export const metadataStyles = (theme: typeof Theme.light) => ({ color: theme.palette.text.primary, @@ -29,15 +30,15 @@ export const metadataStyles = (theme: typeof Theme.light) => ({ textOverflow: 'ellipsis', }); -export interface MetadataDisplayProps { - resource: KubeObject; - extraRows?: ExtraRowsFunc | NameValueTableRow[] | null; +export interface MetadataDisplayProps { + resource: T; + extraRows?: ExtraRowsFunc | NameValueTableRow[] | null; } -export function MetadataDisplay(props: MetadataDisplayProps) { +export function MetadataDisplay(props: MetadataDisplayProps) { const { resource, extraRows } = props; const { t } = useTranslation(); - let makeExtraRows: ExtraRowsFunc; + let makeExtraRows: ExtraRowsFunc; function makeOwnerReferences(ownerReferences: KubeOwnerReference[]) { if (!resource || ownerReferences === undefined) { @@ -54,7 +55,7 @@ export function MetadataDisplay(props: MetadataDisplayProps) { if (ownerRef.kind in ResourceClasses) { let routeName; try { - routeName = new ResourceClasses[ownerRef.kind]().detailsRoute; + routeName = ResourceClasses[ownerRef.kind as keyof typeof ResourceClasses].detailsRoute; } catch (e) { console.error(`Error getting routeName for {ownerRef.kind}`, e); return null; diff --git a/frontend/src/components/common/Resource/PortForward.tsx b/frontend/src/components/common/Resource/PortForward.tsx index dc641c8ab7..cd3595f5a4 100644 --- a/frontend/src/components/common/Resource/PortForward.tsx +++ b/frontend/src/components/common/Resource/PortForward.tsx @@ -12,7 +12,8 @@ import { startPortForward, stopOrDeletePortForward, } from '../../../lib/k8s/apiProxy'; -import { KubeContainer, KubeObject } from '../../../lib/k8s/cluster'; +import { KubeContainer } from '../../../lib/k8s/cluster'; +import { KubeObjectInterface } from '../../../lib/k8s/KubeObject'; import Pod from '../../../lib/k8s/pod'; import Service from '../../../lib/k8s/service'; import { getCluster } from '../../../lib/util'; @@ -21,7 +22,7 @@ export { type PortForward as PortForwardState } from '../../../lib/k8s/api/v1/po interface PortForwardProps { containerPort: number | string; - resource?: KubeObject; + resource?: KubeObjectInterface; } export const PORT_FORWARDS_STORAGE_KEY = 'portforwards'; @@ -149,14 +150,14 @@ function PortForwardContent(props: PortForwardProps) { } function handlePortForward() { - if (!namespace || !cluster) { + if (!namespace || !cluster || !pods) { return; } setError(null); const resourceName = name || ''; - const podNamespace = isPod ? namespace : pods![0].metadata.namespace; + const podNamespace = isPod ? namespace : pods[0].metadata.namespace!; const serviceNamespace = namespace; const serviceName = !isPod ? resourceName : ''; const podName = isPod ? resourceName : pods![0].metadata.name; @@ -259,7 +260,7 @@ function PortForwardContent(props: PortForwardProps) { }); } - if (isPod && (!resource || resource.status.phase === 'Failed')) { + if (isPod && (!resource || (resource as Pod).status.phase === 'Failed')) { return null; } diff --git a/frontend/src/components/common/Resource/Resource.tsx b/frontend/src/components/common/Resource/Resource.tsx index 05d653212e..26777726d0 100644 --- a/frontend/src/components/common/Resource/Resource.tsx +++ b/frontend/src/components/common/Resource/Resource.tsx @@ -13,19 +13,17 @@ import { useTheme } from '@mui/system'; import { Location } from 'history'; import { Base64 } from 'js-base64'; import _, { has } from 'lodash'; -import React, { PropsWithChildren } from 'react'; +import React, { PropsWithChildren, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath, NavLinkProps, useLocation } from 'react-router-dom'; import YAML from 'yaml'; import { labelSelectorToQuery, ResourceClasses } from '../../../lib/k8s'; import { ApiError } from '../../../lib/k8s/apiProxy'; -import { - KubeCondition, - KubeContainer, - KubeContainerStatus, - KubeObject, - KubeObjectInterface, -} from '../../../lib/k8s/cluster'; +import { KubeCondition, KubeContainer, KubeContainerStatus } from '../../../lib/k8s/cluster'; +import { KubeEvent } from '../../../lib/k8s/event'; +import { KubeObject } from '../../../lib/k8s/KubeObject'; +import { KubeObjectInterface } from '../../../lib/k8s/KubeObject'; +import { KubeObjectClass } from '../../../lib/k8s/KubeObject'; import Pod, { KubePod, KubeVolume } from '../../../lib/k8s/pod'; import { createRouteURL, RouteURLProps } from '../../../lib/router'; import { getThemeName } from '../../../lib/themes'; @@ -60,7 +58,7 @@ export interface ResourceLinkProps extends Omit; } export function ResourceLink(props: ResourceLinkProps) { @@ -83,31 +81,31 @@ export function ResourceLink(props: ResourceLinkProps) { ); } -export interface DetailsGridProps - extends PropsWithChildren> { +export interface DetailsGridProps + extends PropsWithChildren>, 'resource'>> { /** Resource type to fetch (from the ResourceClasses). */ - resourceType: KubeObject; + resourceType: T; /** Name of the resource. */ name: string; /** Namespace of the resource. If not provided, it's assumed the resource is not namespaced. */ namespace?: string; /** Sections to show in the details grid (besides the default ones). */ extraSections?: - | ((item: KubeObject) => boolean | DetailsViewSection[]) + | ((item: InstanceType) => boolean | DetailsViewSection[] | ReactNode[]) | boolean | DetailsViewSection[]; /** @deprecated Use extraSections instead. */ - sectionsFunc?: (item: KubeObject) => React.ReactNode | DetailsViewSection[]; + sectionsFunc?: (item: InstanceType) => React.ReactNode | DetailsViewSection[]; /** If true, will show the events section. */ withEvents?: boolean; /** Called when the resource instance is created/updated, or there is an error. */ - onResourceUpdate?: (resource: KubeObject, error: ApiError) => void; + onResourceUpdate?: (resource: InstanceType, error: ApiError) => void; } /** Renders the different parts that constibute an actual resource's details view. * Those are: the back link, the header, the main info section, the extra sections, and the events section. */ -export function DetailsGrid(props: DetailsGridProps) { +export function DetailsGrid(props: DetailsGridProps) { const { sectionsFunc, resourceType, @@ -133,8 +131,11 @@ export function DetailsGrid(props: DetailsGridProps) { const { extraInfo, actions, noDefaultActions, headerStyle, backLink, title, headerSection } = otherMainInfoSectionProps; - const [item, error] = resourceType.useGet(name, namespace); - const prevItemRef = React.useRef<{ uid?: string; version?: string; error?: ApiError }>({}); + const [item, error] = resourceType.useGet(name, namespace) as [ + InstanceType | null, + ApiError | null + ]; + const prevItemRef = React.useRef<{ uid?: string; version?: string; error?: ApiError | null }>({}); React.useEffect(() => { if (item) { @@ -154,7 +155,7 @@ export function DetailsGrid(props: DetailsGridProps) { // infinite loops. const prevItem = prevItemRef.current; if ( - prevItem?.uid === item?.metatada?.uid && + prevItem?.uid === item?.metadata?.uid && prevItem?.version === item?.metadata?.resourceVersion && error === prevItem.error ) { @@ -162,11 +163,11 @@ export function DetailsGrid(props: DetailsGridProps) { } prevItemRef.current = { - uid: item?.metatada?.uid, + uid: item?.metadata?.uid, version: item?.metadata?.resourceVersion, error, }; - onResourceUpdate?.(item, error); + onResourceUpdate?.(item as InstanceType, error!); }, [item, error]); const actualBackLink: string | Location | undefined = React.useMemo(() => { @@ -190,7 +191,7 @@ export function DetailsGrid(props: DetailsGridProps) { route = item.listRoute; } else { try { - route = new resourceType().listRoute; + route = new resourceType({} as any).listRoute; } catch (err) { console.error( `Error creating route for details grid (resource type=${resourceType}): ${err}` @@ -204,7 +205,7 @@ export function DetailsGrid(props: DetailsGridProps) { return createRouteURL(route); }, [item]); - const sections: DetailsViewSection[] = []; + const sections: (DetailsViewSection | ReactNode)[] = []; // Back link if (!!actualBackLink || actualBackLink === '') { @@ -268,16 +269,16 @@ export function DetailsGrid(props: DetailsGridProps) { ); sections.push({ id: 'LEGACY_SECTIONS_FUNC', - section: sectionsFunc(item) as any, + section: sectionsFunc(item!) as any, }); } if (!!extraSections) { - let actualExtraSections: DetailsViewSection[] = []; + let actualExtraSections: (DetailsViewSection | ReactNode)[] = []; if (Array.isArray(extraSections)) { actualExtraSections = extraSections; } else if (typeof extraSections === 'function') { - const extraSectionsResult = extraSections(item) || []; + const extraSectionsResult = extraSections(item!) || []; if (Array.isArray(extraSectionsResult)) { actualExtraSections = extraSectionsResult; } @@ -498,7 +499,7 @@ export function SecretField(props: InputProps) { } export interface ConditionsTableProps { - resource: KubeObjectInterface | null; + resource: KubeObjectInterface | KubeEvent | null; showLastUpdate?: boolean; } @@ -641,7 +642,7 @@ export function LivenessProbes(props: { liveness: KubeContainer['livenessProbe'] export interface ContainerInfoProps { container: KubeContainer; - resource?: KubeObjectInterface | null; + resource: KubeObjectInterface | null; status?: Omit; } @@ -1146,7 +1147,7 @@ export function VolumeSection(props: VolumeSectionProps) { function PrintVolumeLink(props: PrintVolumeLinkProps) { const { volumeName, volumeKind, volume } = props; const resourceClasses = ResourceClasses; - const classList = Object.keys(resourceClasses); + const classList = Object.keys(resourceClasses) as Array; for (const kind of classList) { if (kind.toLowerCase() === volumeKind.toLowerCase()) { diff --git a/frontend/src/components/common/Resource/ResourceListView.tsx b/frontend/src/components/common/Resource/ResourceListView.tsx index 08cc7f5c2e..a28ab5b036 100644 --- a/frontend/src/components/common/Resource/ResourceListView.tsx +++ b/frontend/src/components/common/Resource/ResourceListView.tsx @@ -1,28 +1,35 @@ -import React, { PropsWithChildren } from 'react'; -import { KubeObject } from '../../../lib/k8s/cluster'; +import React, { PropsWithChildren, ReactElement, ReactNode } from 'react'; +import { KubeObject } from '../../../lib/k8s/KubeObject'; +import { KubeObjectClass } from '../../../lib/k8s/KubeObject'; import SectionBox from '../SectionBox'; import SectionFilterHeader, { SectionFilterHeaderProps } from '../SectionFilterHeader'; import ResourceTable, { ResourceTableProps } from './ResourceTable'; -export interface ResourceListViewProps - extends PropsWithChildren> { - title: string | JSX.Element; +export interface ResourceListViewProps + extends PropsWithChildren, 'data'>> { + title: ReactNode; headerProps?: Omit; + data: Item[] | null; } -type Class = new (...args: any[]) => T; - -export interface ResourceListViewWithResourceClassProps - extends Omit, 'data'> { - resourceClass: Class; +export interface ResourceListViewWithResourceClassProps + extends PropsWithChildren>, 'data'>> { + title: ReactNode; + headerProps?: Omit; + resourceClass: ItemClass; } -export default function ResourceListView( - props: ResourceListViewProps | ResourceListViewWithResourceClassProps +export default function ResourceListView( + props: ResourceListViewWithResourceClassProps +): ReactElement; +export default function ResourceListView>( + props: ResourceListViewProps +): ReactElement; +export default function ResourceListView( + props: ResourceListViewProps | ResourceListViewWithResourceClassProps ) { const { title, children, headerProps, ...tableProps } = props; - const withNamespaceFilter = - 'resourceClass' in props && (props.resourceClass as KubeObject)?.isNamespaced; + const withNamespaceFilter = 'resourceClass' in props && props.resourceClass?.isNamespaced; return ( ; + resourceTableArgs: ResourceTableFromResourceClassProps; namespaces: string[]; search: string; }> = args => { @@ -115,12 +115,12 @@ class MyPod extends Pod { ); } -const podData: ResourceTableFromResourceClassProps = { +const podData: ResourceTableFromResourceClassProps = { columns: ['name', 'namespace', 'age'], resourceClass: MyPod, }; -const withHiddenCols: ResourceTableFromResourceClassProps = { +const withHiddenCols: ResourceTableFromResourceClassProps = { columns: [ 'name', 'namespace', diff --git a/frontend/src/components/common/Resource/ResourceTable.tsx b/frontend/src/components/common/Resource/ResourceTable.tsx index cf5d1296a7..f9675d5afa 100644 --- a/frontend/src/components/common/Resource/ResourceTable.tsx +++ b/frontend/src/components/common/Resource/ResourceTable.tsx @@ -6,7 +6,8 @@ import { useTranslation } from 'react-i18next'; import helpers from '../../../helpers'; import { useClusterGroup } from '../../../lib/k8s'; import { ApiError } from '../../../lib/k8s/apiProxy'; -import { KubeObject } from '../../../lib/k8s/cluster'; +import { KubeObject } from '../../../lib/k8s/KubeObject'; +import { KubeObjectClass } from '../../../lib/k8s/KubeObject'; import { useFilterFunc } from '../../../lib/util'; import { HeadlampEventType, useEventCallback } from '../../../redux/headlampEventSlice'; import { useTypedSelector } from '../../../redux/reducers/reducers'; @@ -101,30 +102,35 @@ export interface ResourceTableProps { clusterErrors?: { [cluster: string]: ApiError | null } | null; } -export interface ResourceTableFromResourceClassProps - extends Omit, 'data'> { - resourceClass: KubeObject; +export interface ResourceTableFromResourceClassProps + extends Omit>, 'data'> { + resourceClass: KubeClass; } -export default function ResourceTable( - props: ResourceTableFromResourceClassProps | ResourceTableProps +export default function ResourceTable( + props: + | ResourceTableFromResourceClassProps + | ResourceTableProps> ) { const { clusterErrors } = props; - if (!!(props as ResourceTableFromResourceClassProps).resourceClass) { - const { resourceClass, ...otherProps } = props as ResourceTableFromResourceClassProps; + if (!!(props as ResourceTableFromResourceClassProps).resourceClass) { + const { resourceClass, ...otherProps } = + props as ResourceTableFromResourceClassProps; return ; } return ( <> - )} /> + >)} /> ); } -function TableFromResourceClass(props: ResourceTableFromResourceClassProps) { +function TableFromResourceClass( + props: ResourceTableFromResourceClassProps +) { const { resourceClass, id, ...otherProps } = props; const { items, error, clusterErrors } = resourceClass.useList(); @@ -134,7 +140,7 @@ function TableFromResourceClass(props: ResourceTableFromResourceClassPr useEffect(() => { dispatchHeadlampEvent({ - resources: items, + resources: items!, resourceKind: resourceClass.className, error: error || undefined, }); @@ -223,7 +229,7 @@ export function useThrottle(value: any, interval = 1000): any { return throttledValue; } -function ResourceTableContent(props: ResourceTableProps) { +function ResourceTableContent(props: ResourceTableProps) { const { columns, defaultSortingColumn, @@ -265,7 +271,7 @@ function ResourceTableContent(props: ResourceTableProps) { } const allColumns = removeClusterColIfNeeded(processedColumns) - .map((col, index): TableColumn => { + .map((col, index): TableColumn => { const indexId = String(index); if (typeof col !== 'string') { @@ -273,7 +279,7 @@ function ResourceTableContent(props: ResourceTableProps) { const sort = column.sort ?? true; - const mrtColumn: TableColumn = { + const mrtColumn: TableColumn = { id: column.id ?? indexId, header: column.label, filterVariant: column.filterVariant, @@ -295,7 +301,7 @@ function ResourceTableContent(props: ResourceTableProps) { } else if ('getter' in column) { mrtColumn.accessorFn = column.getter; } else { - mrtColumn.accessorFn = (item: KubeObject) => item[column.datum]; + mrtColumn.accessorFn = (item: RowItem) => item[column.datum]; } if ('render' in column) { mrtColumn.Cell = ({ row }: { row: MRT_Row }) => @@ -314,7 +320,7 @@ function ResourceTableContent(props: ResourceTableProps) { id: 'name', header: t('translation|Name'), gridTemplate: 1.5, - accessorFn: (item: KubeObject) => item.metadata.name, + accessorFn: (item: RowItem) => item.metadata.name, Cell: ({ row }: { row: MRT_Row }) => row.original && , }; @@ -323,13 +329,12 @@ function ResourceTableContent(props: ResourceTableProps) { id: 'age', header: t('translation|Age'), gridTemplate: 'min-content', - accessorFn: (item: KubeObject) => - -new Date(item.metadata.creationTimestamp).getTime(), + accessorFn: (item: RowItem) => -new Date(item.metadata.creationTimestamp).getTime(), enableColumnFilter: false, muiTableBodyCellProps: { align: 'right', }, - Cell: ({ row }: { row: MRT_Row }) => + Cell: ({ row }: { row: MRT_Row }) => row.original && ( (props: ResourceTableProps) { return { id: 'namespace', header: t('glossary|Namespace'), - accessorFn: (item: KubeObject) => item.getNamespace(), + accessorFn: (item: RowItem) => item.getNamespace(), filterVariant: 'multi-select', - Cell: ({ row }: { row: MRT_Row }) => + Cell: ({ row }: { row: MRT_Row }) => row.original?.getNamespace() ? ( {row.original.getNamespace()} @@ -364,7 +369,7 @@ function ResourceTableContent(props: ResourceTableProps) { return { id: 'kind', header: t('translation|Type'), - accessorFn: (resource: KubeObject) => String(resource?.kind), + accessorFn: (resource: RowItem) => String(resource?.kind), filterVariant: 'multi-select', }; default: @@ -372,7 +377,7 @@ function ResourceTableContent(props: ResourceTableProps) { } }) .filter(col => !hideColumns?.includes(col.id ?? '')) as Array< - TableColumn & { gridTemplate?: string | number } + TableColumn & { gridTemplate?: string | number } >; let sort = undefined; diff --git a/frontend/src/components/common/Resource/RestartButton.tsx b/frontend/src/components/common/Resource/RestartButton.tsx index 35e0d36653..33397390d3 100644 --- a/frontend/src/components/common/Resource/RestartButton.tsx +++ b/frontend/src/components/common/Resource/RestartButton.tsx @@ -14,7 +14,10 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { apply } from '../../../lib/k8s/apiProxy'; -import { KubeObject } from '../../../lib/k8s/cluster'; +import Deployment from '../../../lib/k8s/deployment'; +import { KubeObject } from '../../../lib/k8s/KubeObject'; +import ReplicaSet from '../../../lib/k8s/replicaSet'; +import StatefulSet from '../../../lib/k8s/statefulSet'; import { clusterAction } from '../../../redux/clusterActionSlice'; import { EventStatus, @@ -25,7 +28,7 @@ import { AppDispatch } from '../../../redux/stores/store'; import AuthVisible from './AuthVisible'; interface RestartButtonProps { - item: KubeObject; + item: Deployment | StatefulSet | ReplicaSet; } export function RestartButton(props: RestartButtonProps) { diff --git a/frontend/src/components/common/Resource/ScaleButton.tsx b/frontend/src/components/common/Resource/ScaleButton.tsx index 98a636ca20..9e1642f643 100644 --- a/frontend/src/components/common/Resource/ScaleButton.tsx +++ b/frontend/src/components/common/Resource/ScaleButton.tsx @@ -15,7 +15,9 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { KubeObject } from '../../../lib/k8s/cluster'; +import Deployment from '../../../lib/k8s/deployment'; +import ReplicaSet from '../../../lib/k8s/replicaSet'; +import StatefulSet from '../../../lib/k8s/statefulSet'; import { CallbackActionOptions, clusterAction } from '../../../redux/clusterActionSlice'; import { EventStatus, @@ -27,7 +29,7 @@ import { LightTooltip } from '../Tooltip'; import AuthVisible from './AuthVisible'; interface ScaleButtonProps { - item: KubeObject; + item: Deployment | StatefulSet | ReplicaSet; options?: CallbackActionOptions; } @@ -101,8 +103,8 @@ export default function ScaleButton(props: ScaleButtonProps) { ); } -interface ScaleDialogProps extends DialogProps { - resource: KubeObject; +interface ScaleDialogProps extends Omit { + resource: Deployment | StatefulSet | ReplicaSet; onSave: (numReplicas: number) => void; onClose: () => void; errorMessage?: string; @@ -129,7 +131,7 @@ function ScaleDialog(props: ScaleDialogProps) { const dispatchHeadlampEvent = useEventCallback(HeadlampEventType.SCALE_RESOURCE); function getNumReplicas() { - if (!resource?.spec) { + if (!('spec' in resource)) { return -1; } diff --git a/frontend/src/components/common/Resource/ViewButton.stories.tsx b/frontend/src/components/common/Resource/ViewButton.stories.tsx index e002a6face..9f4867efcc 100644 --- a/frontend/src/components/common/Resource/ViewButton.stories.tsx +++ b/frontend/src/components/common/Resource/ViewButton.stories.tsx @@ -1,6 +1,7 @@ import '../../../i18n/config'; import { Meta, StoryFn } from '@storybook/react'; import React from 'react'; +import { KubeObject } from '../../../lib/k8s/KubeObject'; import ViewButton from './ViewButton'; import { ViewButtonProps } from './ViewButton'; @@ -16,13 +17,13 @@ export const View = Template.bind({}); View.args = { item: { jsonData: {}, - }, + } as KubeObject, }; export const ViewOpen = Template.bind({}); ViewOpen.args = { item: { jsonData: {}, - }, + } as KubeObject, initialToggle: true, }; diff --git a/frontend/src/components/common/Resource/ViewButton.tsx b/frontend/src/components/common/Resource/ViewButton.tsx index a9c67acb57..f71c644659 100644 --- a/frontend/src/components/common/Resource/ViewButton.tsx +++ b/frontend/src/components/common/Resource/ViewButton.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { KubeObject } from '../../../lib/k8s/cluster'; +import { KubeObject } from '../../../lib/k8s/KubeObject'; import ActionButton from '../ActionButton'; import EditorDialog from './EditorDialog'; diff --git a/frontend/src/components/common/SectionBox.tsx b/frontend/src/components/common/SectionBox.tsx index c48fe34835..ae92902327 100644 --- a/frontend/src/components/common/SectionBox.tsx +++ b/frontend/src/components/common/SectionBox.tsx @@ -28,7 +28,7 @@ export function SectionBox(props: SectionBoxProps) { if (typeof title === 'string') { titleElem = ; } else { - titleElem = title as JSX.Element; + titleElem = title; } return ( diff --git a/frontend/src/components/common/SimpleTable.stories.tsx b/frontend/src/components/common/SimpleTable.stories.tsx index 38a1fc01ce..2ec4dd8c85 100644 --- a/frontend/src/components/common/SimpleTable.stories.tsx +++ b/frontend/src/components/common/SimpleTable.stories.tsx @@ -2,7 +2,7 @@ import { Box, Typography } from '@mui/material'; import { configureStore } from '@reduxjs/toolkit'; import { Meta, StoryFn } from '@storybook/react'; import { useLocation } from 'react-router-dom'; -import { KubeObjectInterface } from '../../lib/k8s/cluster'; +import { KubeObjectInterface } from '../../lib/k8s/KubeObject'; import { useFilterFunc } from '../../lib/util'; import { TestContext, TestContextProps } from '../../test'; import SectionFilterHeader from './SectionFilterHeader'; diff --git a/frontend/src/components/common/SimpleTable.tsx b/frontend/src/components/common/SimpleTable.tsx index e43d50eb5d..b3b415b9e0 100644 --- a/frontend/src/components/common/SimpleTable.tsx +++ b/frontend/src/components/common/SimpleTable.tsx @@ -43,7 +43,8 @@ export interface SimpleTableProps { [dataProp: string]: any; [dataProp: number]: any; }[] - | null; + | null + | undefined; filterFunction?: ((...args: any[]) => boolean) | null; rowsPerPage?: number[]; emptyMessage?: string; @@ -283,7 +284,7 @@ export default function SimpleTable(props: SimpleTableProps) { function getPagedRows() { const startIndex = page * rowsPerPage; - return filteredData.slice(startIndex, startIndex + rowsPerPage); + return filteredData!.slice(startIndex, startIndex + rowsPerPage); } if (displayData === null) { @@ -296,10 +297,13 @@ export default function SimpleTable(props: SimpleTableProps) { let filteredData = displayData; if (filterFunction) { - filteredData = displayData.filter(filterFunction); + filteredData = displayData?.filter(filterFunction); } - if ((filteredData.length === 0 || filteredData.length < page * rowsPerPage) && page !== 0) { + if ( + (filteredData?.length === 0 || (filteredData?.length ?? 0) < page * rowsPerPage) && + page !== 0 + ) { setPage(0); } @@ -415,7 +419,7 @@ export default function SimpleTable(props: SimpleTableProps) { )} - {filteredData.length > 0 ? ( + {filteredData!.length > 0 ? ( getPagedRows().map((row: any, i: number) => ( {columns.map((col, i) => { @@ -443,11 +447,11 @@ export default function SimpleTable(props: SimpleTableProps) { )} - {filteredData.length > rowsPerPageOptions[0] && showPagination && ( + {filteredData!.length > rowsPerPageOptions[0] && showPagination && ( & { matchCriteria?: string[] }; function TableWithFilter(props: TableWithFilterProps) { const { matchCriteria, ...otherProps } = props; const filterFunc = useFilterFunc(matchCriteria); - return ; + return filterFunction={filterFunc} {...otherProps} />; } const TemplateWithFilter: StoryFn<{ diff --git a/frontend/src/components/common/Tabs.tsx b/frontend/src/components/common/Tabs.tsx index 7b268fd96d..d04792aeaa 100644 --- a/frontend/src/components/common/Tabs.tsx +++ b/frontend/src/components/common/Tabs.tsx @@ -3,12 +3,12 @@ import MuiTab from '@mui/material/Tab'; import MuiTabs from '@mui/material/Tabs'; import Typography, { TypographyProps } from '@mui/material/Typography'; import { SxProps } from '@mui/system'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { useId } from '../../lib/util'; export interface Tab { label: string; - component: JSX.Element | JSX.Element[]; + component: ReactNode; } export interface TabsProps { diff --git a/frontend/src/components/crd/CustomResourceDetails.tsx b/frontend/src/components/crd/CustomResourceDetails.tsx index a7ea28a355..fb580ff1e6 100644 --- a/frontend/src/components/crd/CustomResourceDetails.tsx +++ b/frontend/src/components/crd/CustomResourceDetails.tsx @@ -5,6 +5,7 @@ import { useParams } from 'react-router-dom'; import { ResourceClasses } from '../../lib/k8s'; import { ApiError } from '../../lib/k8s/apiProxy'; import CustomResourceDefinition, { KubeCRD } from '../../lib/k8s/crd'; +import { KubeObject } from '../../lib/k8s/KubeObject'; import { localeDate } from '../../lib/util'; import { HoverInfoLabel, Link, NameValueTableRow, ObjectEventList, SectionBox } from '../common'; import Empty from '../common/EmptyContent'; @@ -32,7 +33,7 @@ export function CustomResourceDetails(props: CustomResourceDetailsProps) { const { t } = useTranslation('glossary'); const namespace = ns === '-' ? undefined : ns; - const CRD = ResourceClasses.CustomResourceDefinition as CustomResourceDefinition; + const CRD = ResourceClasses.CustomResourceDefinition; CRD.useApiGet(setCRD, crdName, undefined, setError); @@ -109,7 +110,7 @@ export interface CustomResourceDetailsRendererProps { function CustomResourceDetailsRenderer(props: CustomResourceDetailsRendererProps) { const { crd, crName, namespace } = props; - const [item, setItem] = React.useState(null); + const [item, setItem] = React.useState(null); const [error, setError] = React.useState(null); const { t } = useTranslation('glossary'); @@ -152,7 +153,7 @@ function CustomResourceDetailsRenderer(props: CustomResourceDetailsRendererProps ), }, - ...getExtraInfo(extraColumns, item!.jsonData), + ...getExtraInfo(extraColumns, item!.jsonData as KubeCRD), ]} backLink="" /> diff --git a/frontend/src/components/crd/CustomResourceInstancesList.tsx b/frontend/src/components/crd/CustomResourceInstancesList.tsx index 1923a6fd4b..01646c08c7 100644 --- a/frontend/src/components/crd/CustomResourceInstancesList.tsx +++ b/frontend/src/components/crd/CustomResourceInstancesList.tsx @@ -1,8 +1,8 @@ import { Alert, AlertTitle } from '@mui/material'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { KubeObject } from '../../lib/k8s/cluster'; import CRD from '../../lib/k8s/crd'; +import { KubeObject } from '../../lib/k8s/KubeObject'; import { Link, Loader, SectionBox, ShowHideLabel } from '../common/'; import Empty from '../common/EmptyContent'; import { ResourceListView } from '../common/Resource'; @@ -115,7 +115,7 @@ function CrInstancesView({ crds }: { crds: CRD[]; key: string }) { { label: 'Categories', getValue: cr => { - const categories = getCRDForCR(cr).jsonData!.status.acceptedNames.categories; + const categories = getCRDForCR(cr).jsonData.status?.acceptedNames?.categories; return categories !== undefined ? categories.toString().split(',').join(', ') : ''; }, }, diff --git a/frontend/src/components/crd/CustomResourceList.stories.tsx b/frontend/src/components/crd/CustomResourceList.stories.tsx index a046c77c98..b7ac59ce7c 100644 --- a/frontend/src/components/crd/CustomResourceList.stories.tsx +++ b/frontend/src/components/crd/CustomResourceList.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryFn } from '@storybook/react'; import { http, HttpResponse } from 'msw'; -import { KubeObjectClass } from '../../lib/k8s/cluster'; +import { KubeObjectClass } from '../../lib/k8s/KubeObject'; import { TestContext, TestContextProps } from '../../test'; import CustomResourceList from './CustomResourceList'; import { mockCRD, mockCRList } from './storyHelper'; diff --git a/frontend/src/components/crd/CustomResourceList.tsx b/frontend/src/components/crd/CustomResourceList.tsx index f63a64f78f..5dd4a88c6d 100644 --- a/frontend/src/components/crd/CustomResourceList.tsx +++ b/frontend/src/components/crd/CustomResourceList.tsx @@ -3,8 +3,8 @@ import { JSONPath } from 'jsonpath-plus'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import { KubeObject } from '../../lib/k8s/cluster'; import CRD, { KubeCRD } from '../../lib/k8s/crd'; +import { KubeObject } from '../../lib/k8s/KubeObject'; import { localeDate } from '../../lib/util'; import { Link, Loader, PageGrid, SectionHeader } from '../common'; import BackLink from '../common/BackLink'; @@ -35,10 +35,14 @@ export default function CustomResourceList() { ); } - return ; + return ; } -function CustomResourceLink(props: { resource: KubeCRD; crd: CRD; [otherProps: string]: any }) { +function CustomResourceLink(props: { + resource: KubeObject; + crd: CRD; + [otherProps: string]: any; +}) { const { resource, crd, ...otherProps } = props; return ( @@ -83,7 +87,7 @@ function CustomResourceListRenderer(props: CustomResourceListProps) { ); } -function getValueWithJSONPath(item: KubeCRD, jsonPath: string): string { +function getValueWithJSONPath(item: { jsonData: object }, jsonPath: string): string { let value: string | undefined; try { // Extract the value from the json item @@ -110,7 +114,7 @@ export function CustomResourceListTable(props: CustomResourceTableProps) { return crd.getMainAPIGroup(); }, [crd]); - const CRClass = React.useMemo(() => { + const CRClass: typeof KubeObject = React.useMemo(() => { return crd.makeCRClass(); }, [crd]); @@ -124,7 +128,7 @@ export function CustomResourceListTable(props: CustomResourceTableProps) { crd.jsonData.spec.versions.find( (version: KubeCRD['spec']['versions'][number]) => version.name === currentVersion )?.additionalPrinterColumns || []; - const cols: ResourceTableColumn[] = []; + const cols: ResourceTableColumn>[] = []; for (let i = 0; i < colsFromSpec.length; i++) { const idx = i; const colSpec = colsFromSpec[idx]; @@ -150,15 +154,15 @@ export function CustomResourceListTable(props: CustomResourceTableProps) { }, [crd, apiGroup]); const cols = React.useMemo(() => { - const colsToDisplay: ResourceTableProps['columns'] = [ + const colsToDisplay = [ { label: t('translation|Name'), getValue: resource => resource.metadata.name, - render: (resource: KubeObject) => , + render: resource => , }, ...additionalPrinterCols, 'age', - ]; + ] as ResourceTableProps>['columns']; if (crd.isNamespacedScope) { colsToDisplay.splice(1, 0, 'namespace'); diff --git a/frontend/src/components/cronjob/Details.tsx b/frontend/src/components/cronjob/Details.tsx index c21cad0139..177e81ff0e 100644 --- a/frontend/src/components/cronjob/Details.tsx +++ b/frontend/src/components/cronjob/Details.tsx @@ -15,9 +15,9 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useParams } from 'react-router-dom'; import { apply } from '../../lib/k8s/apiProxy'; -import { KubeObjectInterface } from '../../lib/k8s/cluster'; import CronJob from '../../lib/k8s/cronJob'; import Job from '../../lib/k8s/job'; +import { KubeObjectInterface } from '../../lib/k8s/KubeObject'; import { clusterAction } from '../../redux/clusterActionSlice'; import { AppDispatch } from '../../redux/stores/store'; import { ActionButton } from '../common'; diff --git a/frontend/src/components/endpoints/Details.tsx b/frontend/src/components/endpoints/Details.tsx index 030ded0bc9..7a19ad5421 100644 --- a/frontend/src/components/endpoints/Details.tsx +++ b/frontend/src/components/endpoints/Details.tsx @@ -55,7 +55,9 @@ export default function EndpointDetails() { label: t('Target'), getter: address => { const targetRefClass = !!address.targetRef?.kind - ? ResourceClasses[address.targetRef?.kind] + ? ResourceClasses[ + address.targetRef?.kind as keyof typeof ResourceClasses + ] : null; if (!!targetRefClass) { return ( diff --git a/frontend/src/components/horizontalPodAutoscaler/List.tsx b/frontend/src/components/horizontalPodAutoscaler/List.tsx index 336ef42c4a..fc39b5ee8b 100644 --- a/frontend/src/components/horizontalPodAutoscaler/List.tsx +++ b/frontend/src/components/horizontalPodAutoscaler/List.tsx @@ -1,5 +1,6 @@ import { Chip } from '@mui/material'; import { styled } from '@mui/system'; +import { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import HPA from '../../lib/k8s/hpa'; import { Link } from '../common'; @@ -49,7 +50,7 @@ export default function HpaList() { .map(it => it.shortValue) .join(', '), render: (hpa: HPA) => { - const value: JSX.Element[] = []; + const value: ReactNode[] = []; const metrics = hpa.metrics(t); if (metrics.length) { value.push( diff --git a/frontend/src/components/ingress/Details.tsx b/frontend/src/components/ingress/Details.tsx index 249116c55f..92325d0128 100644 --- a/frontend/src/components/ingress/Details.tsx +++ b/frontend/src/components/ingress/Details.tsx @@ -13,7 +13,7 @@ import SimpleTable from '../common/SimpleTable'; * Is https used in the ingress item */ function isHttpsUsed(item: Ingress, url: String) { - const hostList: string[] = item.jsonData.spec?.tls?.map(({ ...hosts }) => `${hosts.hosts}`); + const hostList = item.jsonData.spec?.tls?.map(({ ...hosts }) => `${hosts.hosts}`) ?? []; const isHttps = hostList.includes(`${url}`); return isHttps; diff --git a/frontend/src/components/job/storyHelper.ts b/frontend/src/components/job/storyHelper.ts index d09ae12612..6f1c55fd15 100644 --- a/frontend/src/components/job/storyHelper.ts +++ b/frontend/src/components/job/storyHelper.ts @@ -1,8 +1,11 @@ -export const jobs = [ +import { KubeObjectInterface } from '../../lib/k8s/KubeObject'; + +export const jobs: KubeObjectInterface[] = [ { apiVersion: 'batch/v1', kind: 'Job', metadata: { + name: '', creationTimestamp: '2023-07-28T08:00:00Z', generation: 1, labels: { diff --git a/frontend/src/components/namespace/List.tsx b/frontend/src/components/namespace/List.tsx index 5975503dbb..4fbb8d6f9d 100644 --- a/frontend/src/components/namespace/List.tsx +++ b/frontend/src/components/namespace/List.tsx @@ -39,8 +39,8 @@ export default function NamespacesList() { } const resourceTableProps: - | ResourceTableFromResourceClassProps - | ResourceTableProps = React.useMemo(() => { + | ResourceTableProps + | ResourceTableFromResourceClassProps = React.useMemo(() => { if (allowedNamespaces.length > 0) { return { columns: [ @@ -72,7 +72,7 @@ export default function NamespacesList() { }, ], data: allowedNamespaces as unknown as Namespace[], - }; + } satisfies ResourceTableProps; } return { resourceClass: Namespace, @@ -87,7 +87,7 @@ export default function NamespacesList() { }, 'age', ], - }; + } satisfies ResourceTableFromResourceClassProps; }, [allowedNamespaces]); return ( @@ -97,7 +97,7 @@ export default function NamespacesList() { titleSideActions: [], noNamespaceFilter: true, }} - {...resourceTableProps} + {...(resourceTableProps as ResourceTableProps)} /> ); } diff --git a/frontend/src/components/node/Details.tsx b/frontend/src/components/node/Details.tsx index d7ebb422dd..4a1ca48324 100644 --- a/frontend/src/components/node/Details.tsx +++ b/frontend/src/components/node/Details.tsx @@ -114,7 +114,7 @@ export default function NodeDetails() { } const cloneNode = _.cloneDeep(node); - cloneNode.spec.unschedulable = !node.spec.unschedulable; + cloneNode!.spec.unschedulable = !node!.spec.unschedulable; setNode(cloneNode); }) .catch(error => { @@ -168,7 +168,7 @@ export default function NodeDetails() { })} onConfirm={() => { setDrainDialogOpen(false); - handleNodeDrain(node); + handleNodeDrain(node!); }} handleClose={() => setDrainDialogOpen(false)} open={drainDialogOpen} @@ -200,7 +200,7 @@ export default function NodeDetails() { handleNodeScheduleState(item, cordon)} + onClick={() => handleNodeScheduleState(item!, cordon)} iconButtonProps={{ disabled: isupdatingNodeScheduleProperty, }} diff --git a/frontend/src/components/node/List.tsx b/frontend/src/components/node/List.tsx index 2b92d072e7..b04e4dabb9 100644 --- a/frontend/src/components/node/List.tsx +++ b/frontend/src/components/node/List.tsx @@ -78,7 +78,7 @@ export default function NodeList() { label: t('Roles'), gridTemplate: 'minmax(150px, .5fr)', getValue: node => { - return Object.keys(node.metadata.labels) + return Object.keys(node.metadata.labels ?? {}) .filter((t: String) => t.startsWith('node-role.kubernetes.io/')) .map(t => t.replace('node-role.kubernetes.io/', '')) .join(','); diff --git a/frontend/src/components/node/utils.tsx b/frontend/src/components/node/utils.tsx index 618fef9833..97c033c518 100644 --- a/frontend/src/components/node/utils.tsx +++ b/frontend/src/components/node/utils.tsx @@ -1,5 +1,6 @@ import { Box, Chip, Tooltip } from '@mui/material'; import { styled } from '@mui/system'; +import { ReactNode } from 'react'; import Node from '../../lib/k8s/node'; const WrappingBox = styled(Box)(({ theme }) => ({ @@ -21,7 +22,7 @@ export function NodeTaintsLabel(props: { node: Node }) { if (node.spec?.taints === undefined) { return ; } - const limits: JSX.Element[] = []; + const limits: ReactNode[] = []; node.spec.taints.forEach(taint => { limits.push( diff --git a/frontend/src/components/pod/List.tsx b/frontend/src/components/pod/List.tsx index b0a82b0f2f..a067e19f1f 100644 --- a/frontend/src/components/pod/List.tsx +++ b/frontend/src/components/pod/List.tsx @@ -92,7 +92,7 @@ export function PodListRenderer(props: PodListProps) { 'cluster', { label: t('Restarts'), - getValue: (pod: Pod) => { + getValue: pod => { const { restarts, lastRestartDate } = pod.getDetailedStatus(); return lastRestartDate.getTime() !== 0 ? t('{{ restarts }} ({{ abbrevTime }} ago)', { @@ -105,7 +105,7 @@ export function PodListRenderer(props: PodListProps) { { id: 'ready', label: t('translation|Ready'), - getValue: (pod: Pod) => { + getValue: pod => { const podRow = pod.getDetailedStatus(); return `${podRow.readyContainers}/${podRow.totalContainers}`; }, @@ -119,7 +119,7 @@ export function PodListRenderer(props: PodListProps) { { id: 'ip', label: t('glossary|IP'), - getValue: (pod: Pod) => pod.status?.podIP ?? '', + getValue: pod => pod.status?.podIP ?? '', }, { id: 'node', @@ -161,7 +161,7 @@ export function PodListRenderer(props: PodListProps) { return statusTrueCount; }, - render: (pod: Pod) => { + render: pod => { const readinessGatesStatus = getReadinessGatesStatus(pod); const total = Object.keys(readinessGatesStatus).length; diff --git a/frontend/src/components/pod/podDetailsVolumeSection.stories.tsx b/frontend/src/components/pod/podDetailsVolumeSection.stories.tsx index 054529dc44..2fe2b0e1e9 100644 --- a/frontend/src/components/pod/podDetailsVolumeSection.stories.tsx +++ b/frontend/src/components/pod/podDetailsVolumeSection.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryFn } from '@storybook/react'; import React from 'react'; -import { KubeObjectInterface } from '../../lib/k8s/cluster'; +import { KubeObjectInterface } from '../../lib/k8s/KubeObject'; import { TestContext } from '../../test'; import { VolumeSection, VolumeSectionProps } from '../common'; diff --git a/frontend/src/components/pod/storyHelper.ts b/frontend/src/components/pod/storyHelper.ts index 085d949fdd..2f62f51151 100644 --- a/frontend/src/components/pod/storyHelper.ts +++ b/frontend/src/components/pod/storyHelper.ts @@ -748,7 +748,7 @@ const readinessGate = { }; // Exporting so these can be used for details views -export const podList = [ +export const podList: KubePod[] = [ imgPullBackOff, successful, initializing, diff --git a/frontend/src/components/podDisruptionBudget/Details.tsx b/frontend/src/components/podDisruptionBudget/Details.tsx index fad84fae38..fc2ecaa12e 100644 --- a/frontend/src/components/podDisruptionBudget/Details.tsx +++ b/frontend/src/components/podDisruptionBudget/Details.tsx @@ -1,3 +1,4 @@ +import { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import PDB from '../../lib/k8s/podDisruptionBudget'; @@ -7,7 +8,7 @@ export default function PDBDetails() { const { namespace, name } = useParams<{ namespace: string; name: string }>(); function selectorsToJSX(selectors: string[]) { - const values: JSX.Element[] = []; + const values: ReactNode[] = []; selectors.forEach((selector: string) => { values.push( diff --git a/frontend/src/components/resourceQuota/List.tsx b/frontend/src/components/resourceQuota/List.tsx index 50a5bd88a2..2bb64e1837 100644 --- a/frontend/src/components/resourceQuota/List.tsx +++ b/frontend/src/components/resourceQuota/List.tsx @@ -1,5 +1,6 @@ import { Box, Chip } from '@mui/material'; import { styled } from '@mui/system'; +import { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { ApiError } from '../../lib/k8s/apiProxy'; import ResourceQuota from '../../lib/k8s/resourceQuota'; @@ -50,7 +51,7 @@ export function ResourceQuotaRenderer(props: ResourceQuotaProps) { label: t('translation|Request'), getValue: item => item.requests.join(', '), render: item => { - const requests: JSX.Element[] = []; + const requests: ReactNode[] = []; item.requests.forEach((request: string) => { requests.push(); }); @@ -62,7 +63,7 @@ export function ResourceQuotaRenderer(props: ResourceQuotaProps) { label: t('translation|Limit'), getValue: item => item?.limits?.join(', '), render: item => { - const limits: JSX.Element[] = []; + const limits: ReactNode[] = []; item.limits.forEach((limit: string) => { limits.push(); }); diff --git a/frontend/src/components/service/Details.tsx b/frontend/src/components/service/Details.tsx index 9d190253e2..80e3e3744c 100644 --- a/frontend/src/components/service/Details.tsx +++ b/frontend/src/components/service/Details.tsx @@ -4,7 +4,7 @@ import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import Endpoints from '../../lib/k8s/endpoints'; +import Endpoint from '../../lib/k8s/endpoints'; import Service from '../../lib/k8s/service'; import { Link } from '../common'; import Empty from '../common/EmptyContent'; @@ -18,7 +18,7 @@ export default function ServiceDetails() { const { namespace, name } = useParams<{ namespace: string; name: string }>(); const { t } = useTranslation(['glossary', 'translation']); - const [endpoints, endpointsError] = Endpoints.useList({ namespace }); + const [endpoints, endpointsError] = Endpoint.useList({ namespace }); function getOwnedEndpoints(item: Service) { return item ? endpoints?.filter(endpoint => endpoint.getName() === item.getName()) : null; diff --git a/frontend/src/components/serviceaccount/Details.tsx b/frontend/src/components/serviceaccount/Details.tsx index f89f65716d..5b1ebf4c76 100644 --- a/frontend/src/components/serviceaccount/Details.tsx +++ b/frontend/src/components/serviceaccount/Details.tsx @@ -15,7 +15,7 @@ export default function ServiceAccountDetails() { name={name} namespace={namespace} withEvents - extraInfo={(item: ServiceAccount) => + extraInfo={item => item && [ { name: t('Secrets'), diff --git a/frontend/src/components/storage/ClaimDetails.tsx b/frontend/src/components/storage/ClaimDetails.tsx index 46ae649b29..5dc95c8b09 100644 --- a/frontend/src/components/storage/ClaimDetails.tsx +++ b/frontend/src/components/storage/ClaimDetails.tsx @@ -7,7 +7,7 @@ import { StatusLabelByPhase } from './utils'; export function makePVCStatusLabel(item: PersistentVolumeClaim) { const status = item.status!.phase; - return StatusLabelByPhase(status); + return StatusLabelByPhase(status!); } export default function VolumeClaimDetails() { @@ -28,11 +28,11 @@ export default function VolumeClaimDetails() { }, { name: t('Capacity'), - value: item.spec!.resources.requests.storage, + value: item.spec!.resources!.requests.storage, }, { name: t('Access Modes'), - value: item.spec!.accessModes.join(', '), + value: item.spec!.accessModes!.join(', '), }, { name: t('Volume Mode'), diff --git a/frontend/src/components/storage/ClaimList.tsx b/frontend/src/components/storage/ClaimList.tsx index d4552414b2..86512f2e77 100644 --- a/frontend/src/components/storage/ClaimList.tsx +++ b/frontend/src/components/storage/ClaimList.tsx @@ -18,9 +18,9 @@ export default function VolumeClaimList() { { id: 'className', label: t('Class Name'), - getValue: volumeClaim => volumeClaim.spec.storageClassName, + getValue: volumeClaim => volumeClaim.spec?.storageClassName, render: volumeClaim => { - const name = volumeClaim.spec.storageClassName; + const name = volumeClaim.spec?.storageClassName; if (!name) { return ''; } @@ -34,26 +34,26 @@ export default function VolumeClaimList() { { id: 'capacity', label: t('Capacity'), - getValue: volumeClaim => volumeClaim.status.capacity?.storage, + getValue: volumeClaim => volumeClaim.status?.capacity?.storage, gridTemplate: 0.8, }, { id: 'accessModes', label: t('Access Modes'), - getValue: volumeClaim => volumeClaim.spec.accessModes.join(', '), - render: volumeClaim => , + getValue: volumeClaim => volumeClaim.spec?.accessModes?.join(', '), + render: volumeClaim => , }, { id: 'volumeMode', label: t('Volume Mode'), - getValue: volumeClaim => volumeClaim.spec.volumeMode, + getValue: volumeClaim => volumeClaim.spec?.volumeMode, }, { id: 'volume', label: t('Volume'), - getValue: volumeClaim => volumeClaim.spec.volumeName, + getValue: volumeClaim => volumeClaim.spec?.volumeName, render: volumeClaim => { - const name = volumeClaim.spec.volumeName; + const name = volumeClaim.spec?.volumeName; if (!name) { return ''; } @@ -67,7 +67,7 @@ export default function VolumeClaimList() { { id: 'status', label: t('translation|Status'), - getValue: volume => volume.status.phase, + getValue: volume => volume.status?.phase, render: volume => makePVCStatusLabel(volume), gridTemplate: 0.3, }, diff --git a/frontend/src/components/storage/ClassList.tsx b/frontend/src/components/storage/ClassList.tsx index d9c07df2ce..d12a930121 100644 --- a/frontend/src/components/storage/ClassList.tsx +++ b/frontend/src/components/storage/ClassList.tsx @@ -32,7 +32,7 @@ export default function ClassList() { { id: 'allowVolumeExpansion', label: t('Allow Volume Expansion'), - getValue: storageClass => storageClass.allowVolumeExpansion, + getValue: storageClass => String(storageClass.allowVolumeExpansion), }, 'age', ]} diff --git a/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot b/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot index a18966319b..707b47ef9b 100644 --- a/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot +++ b/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot @@ -681,7 +681,9 @@
+ > + true + + > + undefined + { + workloadKind: T; } -export default function WorkloadDetails(props: WorkloadDetailsProps) { +export default function WorkloadDetails(props: WorkloadDetailsProps) { const { namespace, name } = useParams<{ namespace: string; name: string }>(); const { workloadKind } = props; const { t } = useTranslation(['glossary', 'translation']); diff --git a/frontend/src/components/workload/Overview.tsx b/frontend/src/components/workload/Overview.tsx index f83ae0eec7..c18de8e17a 100644 --- a/frontend/src/components/workload/Overview.tsx +++ b/frontend/src/components/workload/Overview.tsx @@ -2,7 +2,6 @@ import Grid from '@mui/material/Grid'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import { KubeObject, Workload } from '../../lib/k8s/cluster'; import CronJob from '../../lib/k8s/cronJob'; import DaemonSet from '../../lib/k8s/daemonSet'; import Deployment from '../../lib/k8s/deployment'; @@ -10,6 +9,8 @@ import Job from '../../lib/k8s/job'; import Pod from '../../lib/k8s/pod'; import ReplicaSet from '../../lib/k8s/replicaSet'; import StatefulSet from '../../lib/k8s/statefulSet'; +import { WorkloadClass } from '../../lib/k8s/Workload'; +import { Workload } from '../../lib/k8s/Workload'; import { getReadyReplicas, getTotalReplicas } from '../../lib/util'; import Link from '../common/Link'; import { PageGrid, ResourceLink } from '../common/Resource'; @@ -81,7 +82,7 @@ export default function Overview() { return joint; }, [workloadsData]); - const workloads: KubeObject[] = [ + const workloads: WorkloadClass[] = [ Pod, Deployment, StatefulSet, @@ -101,7 +102,7 @@ export default function Overview() { [CronJob.className]: t('glossary|Cron Jobs'), }; - function ChartLink({ workload }: { workload: KubeObject }) { + function ChartLink({ workload }: { workload: WorkloadClass }) { return {workloadLabel[workload.className]}; } @@ -129,9 +130,7 @@ export default function Overview() { id: 'name', label: t('translation|Name'), getValue: item => item.metadata.name, - render: item => ( - - ), + render: item => , }, 'namespace', { diff --git a/frontend/src/lib/k8s/KubeMetadata.ts b/frontend/src/lib/k8s/KubeMetadata.ts new file mode 100644 index 0000000000..6b0441d8ed --- /dev/null +++ b/frontend/src/lib/k8s/KubeMetadata.ts @@ -0,0 +1,135 @@ +import { KubeManagedFieldsEntry, KubeOwnerReference, StringDict } from './cluster'; + +/** + * KubeMetadata contains the metadata that is common to all Kubernetes objects. + * + * @see {@link https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#metadata | Metadata} for more details. + */ + +export interface KubeMetadata { + /** + * A map of string keys and values that can be used by external tooling to store and + * retrieve arbitrary metadata about this object + * @see {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ | annotations docs} for more details. + */ + annotations?: StringDict; + /** + * An RFC 3339 date of the date and time an object was created + */ + creationTimestamp: string; + /** + * Number of seconds allowed for this object to gracefully terminate before it + * will be removed from the system. Only set when deletionTimestamp is also set. + * May only be shortened. + * Read-only. + */ + deletionGracePeriodSeconds?: number; + /** + * An RFC 3339 date of the date and time after which this resource will be deleted. + * This field is set by the server when a graceful deletion is requested by the + * user, and is not directly settable by a client. The resource will be deleted + * (no longer visible from resource lists, and not reachable by name) after the + * time in this field except when the object has a finalizer set. In case the + * finalizer is set the deletion of the object is postponed at least until the + * finalizer is removed. Once the deletionTimestamp is set, this value may not + * be unset or be set further into the future, although it may be shortened or + * the resource may be deleted prior to this time. + */ + deletionTimestamp?: string; + /** + * Must be empty before the object is deleted from the registry. Each entry is + * an identifier for the responsible component that will remove the entry from + * the list. If the deletionTimestamp of the object is non-nil, entries in this + * list can only be removed. Finalizers may be processed and removed in any order. + * Order is NOT enforced because it introduces significant risk of stuck finalizers. + * finalizers is a shared field, any actor with permission can reorder it. + * If the finalizer list is processed in order, then this can lead to a situation + * in which the component responsible for the first finalizer in the list is + * waiting for a signal (field value, external system, or other) produced by a + * component responsible for a finalizer later in the list, resulting in a deadlock. + * Without enforced ordering finalizers are free to order amongst themselves and + * are not vulnerable to ordering changes in the list. + * + * patch strategy: merge + */ + finalizers?: string[]; + /** + * GenerateName is an optional prefix, used by the server, to generate a unique + * name ONLY IF the Name field has not been provided. If this field is used, + * the name returned to the client will be different than the name passed. + * This value will also be combined with a unique suffix. The provided value + * has the same validation rules as the Name field, and may be truncated by + * the length of the suffix required to make the value unique on the server. + * If this field is specified and the generated name exists, the server will + * return a 409. Applied only if Name is not specified. + * + * @see {@link https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency | more info} + */ + generateName?: string; + /** + * A sequence number representing a specific generation of the desired state. + * Populated by the system. + * Read-only. + */ + generation?: number; + /** + * A map of string keys and values that can be used to organize and categorize objects + * + * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + */ + labels?: StringDict; + /** + * Maps workflow-id and version to the set of fields that are managed by that workflow. + * This is mostly for internal housekeeping, and users typically shouldn't need to set + * or understand this field. A workflow can be the user's name, a controller's name, or + * the name of a specific apply path like "ci-cd". The set of fields is always in the + * version that the workflow used when modifying the object. + */ + managedFields?: KubeManagedFieldsEntry[]; + /** + * Uniquely identifies this object within the current namespace (see the identifiers docs). + * This value is used in the path when retrieving an individual object. + * + * @see {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ | Names docs} for more details. + */ + name: string; + /** + * Namespace defines the space within which each name must be unique. An empty namespace is + * equivalent to the "default" namespace, but "default" is the canonical representation. + * Not all objects are required to be scoped to a namespace - the value of this field for + * those objects will be empty. Must be a DNS_LABEL. Cannot be updated. + * + * @see {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ | Namespaces docs} for more details. + */ + namespace?: string; + /** + * List of objects depended by this object. If ALL objects in the list have been deleted, + * this object will be garbage collected. If this object is managed by a controller, + * then an entry in this list will point to this controller, with the controller field + * set to true. There cannot be more than one managing controller. + */ + ownerReferences?: KubeOwnerReference[]; + /** + * Identifies the internal version of this object that can be used by clients to + * determine when objects have changed. This value MUST be treated as opaque by + * clients and passed unmodified back to the server. Clients should not assume + * that the resource version has meaning across namespaces, different kinds of + * resources, or different servers. + * + * @see {@link https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency | concurrency control docs} for more details + */ + resourceVersion?: string; + /** + * Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. + */ + selfLink?: string; + /** + * UID is the unique in time and space value for this object. It is typically generated by + * the server on successful creation of a resource and is not allowed to change on PUT + * operations. Populated by the system. Read-only. + * + * @see {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids | UIDs docs} for more details. + */ + uid: string; + apiVersion?: any; +} diff --git a/frontend/src/lib/k8s/KubeObject.ts b/frontend/src/lib/k8s/KubeObject.ts new file mode 100644 index 0000000000..ee7559a5d5 --- /dev/null +++ b/frontend/src/lib/k8s/KubeObject.ts @@ -0,0 +1,640 @@ +import { OpPatch } from 'json-patch'; +import { JSONPath } from 'jsonpath-plus'; +import { cloneDeep, unset } from 'lodash'; +import React from 'react'; +import helpers from '../../helpers'; +import { getCluster } from '../cluster'; +import { createRouteURL } from '../router'; +import { timeAgo } from '../util'; +import { useConnectApi } from '.'; +import { useKubeObject, useKubeObjectList } from './api/v2/hooks'; +import { ApiError, apiFactory, apiFactoryWithNamespace, post, QueryParameters } from './apiProxy'; +import { KubeEvent } from './event'; +import { KubeMetadata } from './KubeMetadata'; + +function getAllowedNamespaces() { + const cluster = getCluster(); + if (!cluster) { + return []; + } + + const clusterSettings = helpers.loadClusterSettings(cluster); + return clusterSettings.allowedNamespaces || []; +} + +export class KubeObject { + jsonData: T; + /** Readonly field defined as JSONPath paths */ + static readOnlyFields: string[] = []; + _clusterName: string; + + /** The kind of the object. Corresponding to the resource kind in Kubernetes. */ + static readonly kind: string; + + /** Name of the resource, plural, used in API */ + static readonly apiName: string; + + /** Group and version of the resource formatted as "GROUP/VERSION", e.g. "policy.k8s.io/v1". */ + static readonly apiVersion: string | string[]; + + /** Whether the object is namespaced. */ + static readonly isNamespaced: boolean; + + static _internalApiEndpoint?: ReturnType; + + static get apiEndpoint() { + if (this._internalApiEndpoint) return this._internalApiEndpoint; + + const factory = this.isNamespaced ? apiFactoryWithNamespace : apiFactory; + const versions = Array.isArray(this.apiVersion) ? this.apiVersion : [this.apiVersion]; + + const factoryArguments = versions.map(apiVersion => { + const [group, version] = apiVersion.includes('/') ? apiVersion.split('/') : ['', apiVersion]; + const includeScaleApi = ['Deployment', 'ReplicaSet', 'StatefulSet'].includes(this.kind); + + return [group, version, this.apiName, includeScaleApi]; + }); + + const endpoint = factory(...(factoryArguments as any)); + this._internalApiEndpoint = endpoint; + + return endpoint; + } + static set apiEndpoint(endpoint: ReturnType) { + this._internalApiEndpoint = endpoint; + } + + constructor(json: T, cluster?: string) { + this.jsonData = json; + this._clusterName = cluster || getCluster() || ''; + } + + get cluster(): string { + return this._clusterName; + } + + set cluster(cluster: string) { + this._clusterName = cluster; + } + + static get className(): string { + return this.kind; + } + + get detailsRoute(): string { + return this._class().detailsRoute; + } + + static get detailsRoute(): string { + return this.kind; + } + + static get pluralName(): string { + // This is a naive way to get the plural name of the object by default. It will + // work in most cases, but for exceptions (like Ingress), we must override this. + return this.apiName; + } + + get pluralName(): string { + // In case we need to override the plural name in instances. + return this._class().pluralName; + } + + get listRoute(): string { + return this._class().listRoute; + } + + static get listRoute(): string { + return this.apiName; + } + + get kind() { + return this.jsonData.kind; + } + + getDetailsLink() { + const params = { + namespace: this.getNamespace(), + name: this.getName(), + }; + const link = createRouteURL(this.detailsRoute, params); + return link; + } + + getListLink() { + return createRouteURL(this.listRoute); + } + + getName() { + return this.metadata.name; + } + + getNamespace() { + return this.metadata.namespace; + } + + getCreationTs() { + return this.metadata.creationTimestamp; + } + + getAge() { + return timeAgo(this.getCreationTs()); + } + + getValue(prop: string) { + return (this.jsonData as Record)![prop]; + } + + get metadata() { + return this.jsonData.metadata; + } + + get isNamespaced() { + return this._class().isNamespaced; + } + + getEditableObject() { + const fieldsToRemove = this._class().readOnlyFields; + const code = this.jsonData ? cloneDeep(this.jsonData) : {}; + + fieldsToRemove?.forEach(path => { + JSONPath({ + path, + json: code, + callback: (result, type, fullPayload) => { + if (fullPayload.parent && fullPayload.parentProperty) { + delete fullPayload.parent[fullPayload.parentProperty]; + } + }, + resultType: 'all', + }); + }); + + return code; + } + + // @todo: apiList has 'any' return type. + /** + * Returns the API endpoint for this object. + * + * @param onList - Callback function to be called when the list is retrieved. + * @param onError - Callback function to be called when an error occurs. + * @param opts - Options to be passed to the API endpoint. + * + * @returns The API endpoint for this object. + */ + static apiList( + this: (new (...args: any) => K) & typeof KubeObject, + onList: (arg: K[]) => void, + onError?: (err: ApiError, cluster?: string) => void, + opts?: ApiListSingleNamespaceOptions + ) { + const createInstance = (item: any): any => this.create(item); + + const args: any[] = [(list: any[]) => onList(list.map((item: any) => createInstance(item)))]; + + if (this.apiEndpoint.isNamespaced) { + args.unshift(opts?.namespace || null); + } + + args.push(onError); + + const queryParams: QueryParameters = {}; + if (opts?.queryParams?.labelSelector) { + queryParams['labelSelector'] = opts.queryParams.labelSelector; + } + if (opts?.queryParams?.fieldSelector) { + queryParams['fieldSelector'] = opts.queryParams.fieldSelector; + } + if (opts?.queryParams?.limit) { + queryParams['limit'] = opts.queryParams.limit; + } + args.push(queryParams); + + args.push(opts?.cluster); + + return this.apiEndpoint.list.bind(null, ...args); + } + + static useApiList( + this: (new (...args: any) => K) & typeof KubeObject, + onList: (...arg: any[]) => any, + onError?: (err: ApiError, cluster?: string) => void, + opts?: ApiListOptions + ) { + const [objs, setObjs] = React.useState<{ [key: string]: K[] }>({}); + const listCallback = onList as (arg: any[]) => void; + + function onObjs(namespace: string, objList: K[]) { + let newObjs: typeof objs = {}; + // Set the objects so we have them for the next API response... + setObjs(previousObjs => { + newObjs = { ...previousObjs, [namespace || '']: objList }; + return newObjs; + }); + + let allObjs: K[] = []; + Object.values(newObjs).map(currentObjs => { + allObjs = allObjs.concat(currentObjs); + }); + + listCallback(allObjs); + } + + const listCalls = []; + const queryParams = cloneDeep(opts); + let namespaces: string[] = []; + unset(queryParams, 'namespace'); + + const cluster = opts?.cluster; + + if (!!opts?.namespace) { + if (typeof opts.namespace === 'string') { + namespaces = [opts.namespace]; + } else if (Array.isArray(opts.namespace)) { + namespaces = opts.namespace as string[]; + } else { + throw Error('namespace should be a string or array of strings'); + } + } + + // If the request itself has no namespaces set, we check whether to apply the + // allowed namespaces. + if (namespaces.length === 0 && this.isNamespaced) { + namespaces = getAllowedNamespaces(); + } + + if (namespaces.length > 0) { + // If we have a namespace set, then we have to make an API call for each + // namespace and then set the objects once we have all of the responses. + for (const namespace of namespaces) { + listCalls.push( + this.apiList(objList => onObjs(namespace, objList as K[]), onError, { + namespace, + queryParams, + cluster, + }) + ); + } + } else { + // If we don't have a namespace set, then we only have one API call + // response to set and we return it right away. + listCalls.push(this.apiList(listCallback, onError, { queryParams, cluster })); + } + + useConnectApi(...listCalls); + } + + static useList( + this: new (...args: any) => K, + { + cluster, + namespace, + ...queryParams + }: { cluster?: string; namespace?: string } & QueryParameters = {} + ) { + return useKubeObjectList({ + queryParams: queryParams, + kubeObjectClass: this as (new (...args: any) => K) & typeof KubeObject, + cluster: cluster, + namespace: namespace, + }); + } + + static useGet( + this: new (...args: any) => K, + name: string, + namespace?: string, + opts?: { + queryParams?: QueryParameters; + cluster?: string; + } + ) { + return useKubeObject({ + kubeObjectClass: this as (new (...args: any) => K) & typeof KubeObject, + name: name, + namespace: namespace, + cluster: opts?.cluster, + queryParams: opts?.queryParams, + }); + } + + static create( + this: new (...args: Args) => T, + ...item: Args + ) { + return new this(...item) as T; + } + + static apiGet( + this: (new (...args: any) => K) & typeof KubeObject, + onGet: (...args: any) => void, + name: string, + namespace?: string, + onError?: (err: ApiError | null, cluster?: string) => void, + opts?: { + queryParams?: QueryParameters; + cluster?: string; + } + ) { + const createInstance = (item: any) => this.create(item); + const args: any[] = [name, (obj: any) => onGet(createInstance(obj))]; + + if (this.apiEndpoint.isNamespaced) { + args.unshift(namespace); + } + + args.push(onError); + args.push(opts?.queryParams); + args.push(opts?.cluster); + + return this.apiEndpoint.get.bind(null, ...args); + } + + static useApiGet( + this: (new (...args: any) => K) & typeof KubeObject, + onGet: (item: K | null) => any, + name: string, + namespace?: string, + onError?: (err: ApiError | null, cluster?: string) => void, + opts?: { + queryParams?: QueryParameters; + cluster?: string; + } + ) { + // We do the type conversion here because we want to be able to use hooks that may not have + // the exact signature as get callbacks. + const getCallback = onGet as (item: K) => void; + useConnectApi(this.apiGet(getCallback, name, namespace, onError, opts)); + } + + _class() { + return this.constructor as KubeObjectClass; + } + + delete() { + const args: string[] = [this.getName()]; + if (this.isNamespaced) { + args.unshift(this.getNamespace()!); + } + + // @ts-ignore + return this._class().apiEndpoint.delete(...args, {}, this._clusterName); + } + + update(data: KubeObjectInterface) { + return this._class().apiEndpoint.put(data, {}, this._clusterName); + } + + static put(data: KubeObjectInterface) { + return this.apiEndpoint.put(data); + } + + scale(numReplicas: number) { + const hasScaleApi = Object.keys(this._class().apiEndpoint).includes('scale'); + if (!hasScaleApi) { + throw new Error(`This class has no scale API: ${this._class().className}`); + } + + const spec = { + replicas: numReplicas, + }; + + type ApiEndpointWithScale = { + scale: { + patch: ( + body: { spec: { replicas: number } }, + metadata: KubeMetadata, + clusterName?: string + ) => Promise; + }; + }; + + return (this._class().apiEndpoint as ApiEndpointWithScale).scale.patch( + { + spec, + }, + this.metadata, + this._clusterName + ); + } + + patch(body: OpPatch[]) { + const args: any[] = [body]; + + if (this.isNamespaced) { + args.push(this.getNamespace()); + } + + args.push(this.getName()); + + // @ts-ignore + return this._class().apiEndpoint.patch(...args, {}, this._clusterName); + } + + /** Performs a request to check if the user has the given permission. + * @param reResourceAttrs The attributes describing this access request. See https://kubernetes.io/docs/reference/kubernetes-api/authorization-resources/self-subject-access-review-v1/#SelfSubjectAccessReviewSpec . + * @returns The result of the access request. + */ + static async fetchAuthorization(reqResourseAttrs?: AuthRequestResourceAttrs) { + // @todo: We should get the API info from the API endpoint. + const authApiVersions = ['v1', 'v1beta1']; + for (let j = 0; j < authApiVersions.length; j++) { + const authVersion = authApiVersions[j]; + + try { + return await post( + `/apis/authorization.k8s.io/${authVersion}/selfsubjectaccessreviews`, + { + kind: 'SelfSubjectAccessReview', + apiVersion: `authorization.k8s.io/${authVersion}`, + spec: { + resourceAttributes: reqResourseAttrs, + }, + }, + false + ); + } catch (err) { + // If this is the last attempt or the error is not 404, let it throw. + if ((err as ApiError).status !== 404 || j === authApiVersions.length - 1) { + throw err; + } + } + } + } + + static async getAuthorization(verb: string, reqResourseAttrs?: AuthRequestResourceAttrs) { + const resourceAttrs: AuthRequestResourceAttrs = { + verb, + ...reqResourseAttrs, + }; + + if (!resourceAttrs.resource) { + resourceAttrs['resource'] = this.pluralName; + } + + // @todo: We should get the API info from the API endpoint. + + // If we already have the group, version, and resource, then we can make the request + // without trying the API info, which may have several versions and thus be less optimal. + if (!!resourceAttrs.group && !!resourceAttrs.version && !!resourceAttrs.resource) { + return this.fetchAuthorization(resourceAttrs); + } + + // If we don't have the group, version, and resource, then we have to try all of the + // API info versions until we find one that works. + const apiInfo = this.apiEndpoint.apiInfo; + for (let i = 0; i < apiInfo.length; i++) { + const { group, version, resource } = apiInfo[i]; + // We only take from the details from the apiInfo if they're missing from the resourceAttrs. + // The idea is that, since this function may also be called from the instance's getAuthorization, + // it may already have the details from the instance's API version. + const attrs = { ...resourceAttrs }; + + if (!!attrs.resource) { + attrs.resource = resource; + } + if (!!attrs.group) { + attrs.group = group; + } + if (!!attrs.version) { + attrs.version = version; + } + + let authResult; + + try { + authResult = await this.fetchAuthorization(attrs); + } catch (err) { + // If this is the last attempt or the error is not 404, let it throw. + if ((err as ApiError).status !== 404 || i === apiInfo.length - 1) { + throw err; + } + } + + if (!!authResult) { + return authResult; + } + } + } + + async getAuthorization(verb: string, reqResourseAttrs?: AuthRequestResourceAttrs) { + const resourceAttrs: AuthRequestResourceAttrs = { + name: this.getName(), + verb, + ...reqResourseAttrs, + }; + + const namespace = this.getNamespace(); + if (!resourceAttrs.namespace && !!namespace) { + resourceAttrs['namespace'] = namespace; + } + + // Set up the group and version from the object's API version. + let [group, version] = this.jsonData?.apiVersion?.split('/') ?? []; + if (!version) { + version = group; + group = ''; + } + + if (!!group) { + resourceAttrs['group'] = group; + } + if (!!version) { + resourceAttrs['version'] = version; + } + + return this._class().getAuthorization(verb, resourceAttrs); + } + + static getErrorMessage(err: ApiError | null) { + if (!err) { + return null; + } + + switch (err.status) { + case 404: + return 'Error: Not found'; + case 403: + return 'Error: No permissions'; + default: + return 'Error'; + } + } +} + +/** + * @deprecated This function is no longer recommended, it's kept for backwards compatibility. + * Please extend KubeObject instead + * + * @returns A KubeObject implementation for the given object name. + * + * @param objectName The name of the object to create a KubeObject implementation for. + */ + +export function makeKubeObject() { + class KubeObjectInternal extends KubeObject {} + return KubeObjectInternal; +} +/** + * This type refers to the *class* of a KubeObject. + */ + +export type KubeObjectClass = typeof KubeObject; +/** + * This is the base interface for all Kubernetes resources, i.e. it contains fields + * that all Kubernetes resources have. + */ + +export interface KubeObjectInterface { + /** + * Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * + * In CamelCase. + * + * Cannot be updated. + * + * @see {@link https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | more info} + */ + kind: string; + apiVersion?: string; + metadata: KubeMetadata; + spec?: any; + status?: any; + items?: any[]; + actionType?: any; + lastTimestamp?: string; + key?: any; + [otherProps: string]: any; +} +export interface ApiListOptions extends QueryParameters { + /** + * The clusters to list objects from. By default uses the current clusters being viewed. + */ + clusters?: string[]; + /** The namespace to list objects from. */ + namespace?: string | string[]; + /** + * The cluster to list objects from. By default uses the current cluster being viewed. + * If clusters is set, then we use that and "cluster" is ignored. + */ + cluster?: string; +} +export interface ApiListSingleNamespaceOptions { + /** The namespace to get the object from. */ + namespace?: string; + /** The parameters to be passed to the API endpoint. */ + queryParams?: QueryParameters; + /** The cluster to get the object from. By default uses the current cluster being viewed. */ + cluster?: string; +} +export interface AuthRequestResourceAttrs { + name?: string; + resource?: string; + subresource?: string; + namespace?: string; + version?: string; + group?: string; + verb?: string; +} diff --git a/frontend/src/lib/k8s/Workload.ts b/frontend/src/lib/k8s/Workload.ts new file mode 100644 index 0000000000..0b8454e013 --- /dev/null +++ b/frontend/src/lib/k8s/Workload.ts @@ -0,0 +1,17 @@ +import CronJob from './cronJob'; +import DaemonSet from './daemonSet'; +import Deployment from './deployment'; +import Job from './job'; +import Pod from './pod'; +import ReplicaSet from './replicaSet'; +import StatefulSet from './statefulSet'; + +export type Workload = Pod | DaemonSet | ReplicaSet | StatefulSet | Job | CronJob | Deployment; +export type WorkloadClass = + | typeof Pod + | typeof DaemonSet + | typeof ReplicaSet + | typeof StatefulSet + | typeof Job + | typeof CronJob + | typeof Deployment; diff --git a/frontend/src/lib/k8s/api/v1/apply.ts b/frontend/src/lib/k8s/api/v1/apply.ts index d473af9cdf..55dcdec05c 100644 --- a/frontend/src/lib/k8s/api/v1/apply.ts +++ b/frontend/src/lib/k8s/api/v1/apply.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { getCluster } from '../../../cluster'; -import { KubeObjectInterface } from '../../cluster'; +import { KubeObjectInterface } from '../../KubeObject'; import { getClusterDefaultNamespace } from './clusterApi'; import { ApiError } from './clusterRequests'; import { resourceDefToApiFactory } from './factories'; diff --git a/frontend/src/lib/k8s/api/v1/clusterRequests.ts b/frontend/src/lib/k8s/api/v1/clusterRequests.ts index a284221041..13972cd526 100644 --- a/frontend/src/lib/k8s/api/v1/clusterRequests.ts +++ b/frontend/src/lib/k8s/api/v1/clusterRequests.ts @@ -5,7 +5,7 @@ import store from '../../../../redux/stores/store'; import { findKubeconfigByClusterName, getUserIdFromLocalStorage } from '../../../../stateless'; import { getToken, logout, setToken } from '../../../auth'; import { getCluster } from '../../../cluster'; -import { KubeObjectInterface } from '../../cluster'; +import { KubeObjectInterface } from '../../KubeObject'; import { BASE_HTTP_URL, CLUSTERS_PREFIX, DEFAULT_TIMEOUT, JSON_HEADERS } from './constants'; import { asQuery, combinePath } from './formatUrl'; import { QueryParameters } from './queryParameters'; diff --git a/frontend/src/lib/k8s/api/v1/factories.ts b/frontend/src/lib/k8s/api/v1/factories.ts index 1af272b02d..e02cda0bee 100644 --- a/frontend/src/lib/k8s/api/v1/factories.ts +++ b/frontend/src/lib/k8s/api/v1/factories.ts @@ -4,7 +4,7 @@ import { OpPatch } from 'json-patch'; import { isDebugVerbose } from '../../../../helpers'; import { getCluster } from '../../../cluster'; -import { KubeObjectInterface } from '../../cluster'; +import { KubeObjectInterface } from '../../KubeObject'; import { ApiError, clusterRequest, patch, post, put, remove } from './clusterRequests'; import { asQuery, getApiRoot } from './formatUrl'; import { QueryParameters } from './queryParameters'; diff --git a/frontend/src/lib/k8s/api/v1/scaleApi.ts b/frontend/src/lib/k8s/api/v1/scaleApi.ts index 86c23e813a..2ee4797d11 100644 --- a/frontend/src/lib/k8s/api/v1/scaleApi.ts +++ b/frontend/src/lib/k8s/api/v1/scaleApi.ts @@ -1,5 +1,5 @@ import { getCluster } from '../../../cluster'; -import { KubeMetadata } from '../../cluster'; +import { KubeMetadata } from '../../KubeMetadata'; import { clusterRequest, patch, put } from './clusterRequests'; export interface ScaleApi { diff --git a/frontend/src/lib/k8s/api/v1/streamingApi.ts b/frontend/src/lib/k8s/api/v1/streamingApi.ts index de38e46979..2d875fb46a 100644 --- a/frontend/src/lib/k8s/api/v1/streamingApi.ts +++ b/frontend/src/lib/k8s/api/v1/streamingApi.ts @@ -2,7 +2,7 @@ import { isDebugVerbose } from '../../../../helpers'; import { findKubeconfigByClusterName, getUserIdFromLocalStorage } from '../../../../stateless'; import { getToken } from '../../../auth'; import { getCluster } from '../../../cluster'; -import { KubeObjectInterface } from '../../cluster'; +import { KubeObjectInterface } from '../../KubeObject'; import { ApiError, clusterRequest } from './clusterRequests'; import { BASE_HTTP_URL, CLUSTERS_PREFIX } from './constants'; import { asQuery, combinePath } from './formatUrl'; diff --git a/frontend/src/lib/k8s/api/v2/KubeList.test.ts b/frontend/src/lib/k8s/api/v2/KubeList.test.ts index c65e4eab62..e51cce5897 100644 --- a/frontend/src/lib/k8s/api/v2/KubeList.test.ts +++ b/frontend/src/lib/k8s/api/v2/KubeList.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { KubeObjectClass, KubeObjectInterface } from '../../cluster'; +import { KubeObjectClass, KubeObjectInterface } from '../../KubeObject'; import { KubeList, KubeListUpdateEvent } from './KubeList'; class MockKubeObject implements KubeObjectInterface { @@ -17,7 +17,7 @@ class MockKubeObject implements KubeObjectInterface { describe('KubeList.applyUpdate', () => { const itemClass = MockKubeObject as unknown as KubeObjectClass; - const initialList = { + const initialList: KubeList = { kind: 'MockKubeList', apiVersion: 'v1', items: [ diff --git a/frontend/src/lib/k8s/api/v2/KubeList.ts b/frontend/src/lib/k8s/api/v2/KubeList.ts index e15f31dab4..9b6ecb746e 100644 --- a/frontend/src/lib/k8s/api/v2/KubeList.ts +++ b/frontend/src/lib/k8s/api/v2/KubeList.ts @@ -1,4 +1,4 @@ -import { KubeObjectClass, KubeObjectInterface } from '../../cluster'; +import { KubeObject, KubeObjectInterface } from '../../KubeObject'; export interface KubeList { kind: string; @@ -23,11 +23,14 @@ export const KubeList = { * @param itemClass - Class of an item in the list. Used to instantiate each item * @returns New list with the updated values */ - applyUpdate( - list: KubeList, - update: KubeListUpdateEvent, - itemClass: KubeObjectClass - ): KubeList { + applyUpdate< + ObjectInterface extends KubeObjectInterface, + ObjectClass extends typeof KubeObject + >( + list: KubeList>, + update: KubeListUpdateEvent, + itemClass: ObjectClass + ): KubeList> { const newItems = [...list.items]; const index = newItems.findIndex(item => item.metadata.uid === update.object.metadata.uid); @@ -35,9 +38,9 @@ export const KubeList = { case 'ADDED': case 'MODIFIED': if (index !== -1) { - newItems[index] = new itemClass(update.object) as T; + newItems[index] = new itemClass(update.object); } else { - newItems.push(new itemClass(update.object) as T); + newItems.push(new itemClass(update.object)); } break; case 'DELETED': diff --git a/frontend/src/lib/k8s/api/v2/hooks.ts b/frontend/src/lib/k8s/api/v2/hooks.ts index 6e227f86ff..2b39ac0806 100644 --- a/frontend/src/lib/k8s/api/v2/hooks.ts +++ b/frontend/src/lib/k8s/api/v2/hooks.ts @@ -2,7 +2,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useMemo } from 'react'; import { getCluster } from '../../../cluster'; import { ApiError, QueryParameters } from '../../apiProxy'; -import { KubeObjectClass, KubeObjectInterface } from '../../cluster'; +import { KubeObject, KubeObjectInterface } from '../../KubeObject'; import { clusterFetch } from './fetch'; import { KubeList, KubeListUpdateEvent } from './KubeList'; import { KubeObjectEndpoint } from './KubeObjectEndpoint'; @@ -68,7 +68,7 @@ export interface QueryListResponse /** * Returns a single KubeObject. */ -export function useKubeObject({ +export function useKubeObject({ kubeObjectClass, namespace, name, @@ -76,7 +76,7 @@ export function useKubeObject({ queryParams, }: { /** Class to instantiate the object with */ - kubeObjectClass: T; + kubeObjectClass: (new (...args: any) => K) & typeof KubeObject; /** Object namespace */ namespace?: string; /** Object name */ @@ -84,8 +84,8 @@ export function useKubeObject({ /** Cluster name */ cluster?: string; queryParams?: QueryParameters; -}): [InstanceType | null, ApiError | null] & QueryResponse, ApiError> { - type Instance = InstanceType; +}): [K | null, ApiError | null] & QueryResponse { + type Instance = K; const endpoint = useEndpoints(kubeObjectClass.apiEndpoint.apiInfo); const cluster = maybeCluster ?? getCluster() ?? ''; @@ -105,15 +105,14 @@ export function useKubeObject({ staleTime: 5000, queryKey, queryFn: async () => { - if (!endpoint) return; const url = makeUrl( - [KubeObjectEndpoint.toUrl(endpoint, namespace), name], + [KubeObjectEndpoint.toUrl(endpoint!, namespace), name], cleanedUpQueryParams ); const obj: KubeObjectInterface = await clusterFetch(url, { cluster, }).then(it => it.json()); - return new kubeObjectClass(obj); + return new kubeObjectClass(obj) as Instance; }, }); @@ -144,7 +143,7 @@ export function useKubeObject({ isFetching: query.isFetching, isSuccess: query.isSuccess, status: query.status, - *[Symbol.iterator](): ArrayIterator | null> { + *[Symbol.iterator](): ArrayIterator { yield data; yield query.error; }, @@ -200,21 +199,20 @@ const useEndpoints = (endpoints: KubeObjectEndpoint[]) => { * * @private please use useKubeObjectList. */ -function _useKubeObjectList({ +function _useKubeObjectList({ kubeObjectClass, namespace, cluster: maybeCluster, queryParams, }: { /** Class to instantiate the object with */ - kubeObjectClass: T; + kubeObjectClass: (new (...args: any) => K) & typeof KubeObject; /** Object list namespace */ namespace?: string; /** Object list cluster */ cluster?: string; queryParams?: QueryParameters; -}): [Array> | null, ApiError | null] & - QueryListResponse>, InstanceType, ApiError> { +}): [Array | null, ApiError | null] & QueryListResponse, K, ApiError> { const endpoint = useEndpoints(kubeObjectClass.apiEndpoint.apiInfo); const cleanedUpQueryParams = Object.fromEntries( @@ -251,10 +249,10 @@ function _useKubeObjectList({ }, }); - const items: Array> | null = query.error ? null : query.data?.items ?? null; - const data: KubeList> | null = query.error ? null : query.data ?? null; + const items: Array | null = query.error ? null : query.data?.items ?? null; + const data: KubeList | null = query.error ? null : query.data ?? null; - useWebSocket>>({ + useWebSocket>({ url: () => makeUrl([KubeObjectEndpoint.toUrl(endpoint!)], { ...cleanedUpQueryParams, @@ -281,7 +279,7 @@ function _useKubeObjectList({ isFetching: query.isFetching, isSuccess: query.isSuccess, status: query.status, - *[Symbol.iterator](): ArrayIterator[] | null> { + *[Symbol.iterator](): ArrayIterator { yield items; yield query.error; }, @@ -291,7 +289,7 @@ function _useKubeObjectList({ /** * Returns a combined list of Kubernetes objects and watches for changes from the clusters given. */ -export function useKubeObjectList({ +export function useKubeObjectList({ kubeObjectClass, namespace, cluster, @@ -299,15 +297,14 @@ export function useKubeObjectList({ queryParams, }: { /** Class to instantiate the object with */ - kubeObjectClass: T; + kubeObjectClass: (new (...args: any) => K) & typeof KubeObject; /** Object list namespace */ namespace?: string; cluster?: string; /** Object list clusters */ clusters?: string[]; queryParams?: QueryParameters; -}): [Array> | null, ApiError | null] & - QueryListResponse>, InstanceType, ApiError> { +}): [Array | null, ApiError | null] & QueryListResponse, K, ApiError> { if (clusters && clusters.length > 0) { return _useKubeObjectLists({ kubeObjectClass, @@ -316,7 +313,7 @@ export function useKubeObjectList({ queryParams, }); } else { - return _useKubeObjectList({ + return _useKubeObjectList({ kubeObjectClass, namespace, cluster: cluster, @@ -330,22 +327,21 @@ export function useKubeObjectList({ * * @private please use useKubeObjectList */ -function _useKubeObjectLists({ +function _useKubeObjectLists({ kubeObjectClass, namespace, clusters, queryParams, }: { /** Class to instantiate the object with */ - kubeObjectClass: T; + kubeObjectClass: (new (...args: any) => K) & typeof KubeObject; /** Object list namespace */ namespace?: string; /** Object list clusters */ clusters: string[]; queryParams?: QueryParameters; -}): [Array> | null, ApiError | null] & - QueryListResponse>, InstanceType, ApiError> { - const clusterResults: Record> = {}; +}): [Array | null, ApiError | null] & QueryListResponse, K, ApiError> { + const clusterResults: Record>> = {}; for (const cluster of clusters) { clusterResults[cluster] = useKubeObjectList({ @@ -361,7 +357,7 @@ function _useKubeObjectLists({ if (items === null) { items = clusterResults[cluster].items; } else { - items = items.concat(clusterResults[cluster].items); + items = items.concat(clusterResults[cluster].items!); } } @@ -398,7 +394,7 @@ function _useKubeObjectLists({ isFetching, isSuccess, status, - *[Symbol.iterator](): ArrayIterator[] | null> { + *[Symbol.iterator](): ArrayIterator { yield items; yield error; }, diff --git a/frontend/src/lib/k8s/cluster.ts b/frontend/src/lib/k8s/cluster.ts index 610be01cdd..8190a38023 100644 --- a/frontend/src/lib/k8s/cluster.ts +++ b/frontend/src/lib/k8s/cluster.ts @@ -1,21 +1,17 @@ -import { OpPatch } from 'json-patch'; -import { JSONPath } from 'jsonpath-plus'; -import { cloneDeep, unset } from 'lodash'; -import React from 'react'; import helpers from '../../helpers'; -import { createRouteURL } from '../router'; -import { getCluster, timeAgo } from '../util'; -import { useConnectApi } from '.'; -import { useCluster } from './'; -import { useKubeObject, useKubeObjectList } from './api/v2/hooks'; -import { ApiError, apiFactory, apiFactoryWithNamespace, post, QueryParameters } from './apiProxy'; -import CronJob from './cronJob'; -import DaemonSet from './daemonSet'; -import Deployment from './deployment'; -import { KubeEvent } from './event'; -import Job from './job'; -import ReplicaSet from './replicaSet'; -import StatefulSet from './statefulSet'; +import { getCluster } from '../util'; +import { KubeMetadata } from './KubeMetadata'; +export { + KubeObject, + makeKubeObject, + type KubeObjectClass, + type KubeObjectInterface, + type ApiListOptions, + type ApiListSingleNamespaceOptions, + type AuthRequestResourceAttrs, +} from './KubeObject'; +export { type KubeMetadata } from './KubeMetadata'; +export { type Workload } from './Workload'; export const HEADLAMP_ALLOWED_NAMESPACES = 'headlamp.allowed-namespaces'; @@ -55,163 +51,10 @@ export interface Cluster { [propName: string]: any; } -/** - * This is the base interface for all Kubernetes resources, i.e. it contains fields - * that all Kubernetes resources have. - */ -export interface KubeObjectInterface { - /** - * Kind is a string value representing the REST resource this object represents. - * Servers may infer this from the endpoint the client submits requests to. - * - * In CamelCase. - * - * Cannot be updated. - * - * @see {@link https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds | more info} - */ - kind: string; - apiVersion?: string; - metadata: KubeMetadata; - [otherProps: string]: any; -} - export interface StringDict { [key: string]: string; } -/** - * KubeMetadata contains the metadata that is common to all Kubernetes objects. - * - * @see {@link https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#metadata | Metadata} for more details. - */ -export interface KubeMetadata { - /** - * A map of string keys and values that can be used by external tooling to store and - * retrieve arbitrary metadata about this object - * @see {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ | annotations docs} for more details. - */ - annotations?: StringDict; - /** - * An RFC 3339 date of the date and time an object was created - */ - creationTimestamp: string; - /** - * Number of seconds allowed for this object to gracefully terminate before it - * will be removed from the system. Only set when deletionTimestamp is also set. - * May only be shortened. - * Read-only. - */ - deletionGracePeriodSeconds?: number; - /** - * An RFC 3339 date of the date and time after which this resource will be deleted. - * This field is set by the server when a graceful deletion is requested by the - * user, and is not directly settable by a client. The resource will be deleted - * (no longer visible from resource lists, and not reachable by name) after the - * time in this field except when the object has a finalizer set. In case the - * finalizer is set the deletion of the object is postponed at least until the - * finalizer is removed. Once the deletionTimestamp is set, this value may not - * be unset or be set further into the future, although it may be shortened or - * the resource may be deleted prior to this time. - */ - deletionTimestamp?: string; - /** - * Must be empty before the object is deleted from the registry. Each entry is - * an identifier for the responsible component that will remove the entry from - * the list. If the deletionTimestamp of the object is non-nil, entries in this - * list can only be removed. Finalizers may be processed and removed in any order. - * Order is NOT enforced because it introduces significant risk of stuck finalizers. - * finalizers is a shared field, any actor with permission can reorder it. - * If the finalizer list is processed in order, then this can lead to a situation - * in which the component responsible for the first finalizer in the list is - * waiting for a signal (field value, external system, or other) produced by a - * component responsible for a finalizer later in the list, resulting in a deadlock. - * Without enforced ordering finalizers are free to order amongst themselves and - * are not vulnerable to ordering changes in the list. - * - * patch strategy: merge - */ - finalizers?: string[]; - /** - * GenerateName is an optional prefix, used by the server, to generate a unique - * name ONLY IF the Name field has not been provided. If this field is used, - * the name returned to the client will be different than the name passed. - * This value will also be combined with a unique suffix. The provided value - * has the same validation rules as the Name field, and may be truncated by - * the length of the suffix required to make the value unique on the server. - * If this field is specified and the generated name exists, the server will - * return a 409. Applied only if Name is not specified. - * - * @see {@link https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency | more info} - */ - generateName?: string; - /** - * A sequence number representing a specific generation of the desired state. - * Populated by the system. - * Read-only. - */ - generation?: number; - /** - * A map of string keys and values that can be used to organize and categorize objects - * - * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ - */ - labels?: StringDict; - /** - * Maps workflow-id and version to the set of fields that are managed by that workflow. - * This is mostly for internal housekeeping, and users typically shouldn't need to set - * or understand this field. A workflow can be the user's name, a controller's name, or - * the name of a specific apply path like "ci-cd". The set of fields is always in the - * version that the workflow used when modifying the object. - */ - managedFields?: KubeManagedFieldsEntry[]; - /** - * Uniquely identifies this object within the current namespace (see the identifiers docs). - * This value is used in the path when retrieving an individual object. - * - * @see {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ | Names docs} for more details. - */ - name: string; - /** - * Namespace defines the space within which each name must be unique. An empty namespace is - * equivalent to the "default" namespace, but "default" is the canonical representation. - * Not all objects are required to be scoped to a namespace - the value of this field for - * those objects will be empty. Must be a DNS_LABEL. Cannot be updated. - * - * @see {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ | Namespaces docs} for more details. - */ - namespace?: string; - /** - * List of objects depended by this object. If ALL objects in the list have been deleted, - * this object will be garbage collected. If this object is managed by a controller, - * then an entry in this list will point to this controller, with the controller field - * set to true. There cannot be more than one managing controller. - */ - ownerReferences?: KubeOwnerReference[]; - /** - * Identifies the internal version of this object that can be used by clients to - * determine when objects have changed. This value MUST be treated as opaque by - * clients and passed unmodified back to the server. Clients should not assume - * that the resource version has meaning across namespaces, different kinds of - * resources, or different servers. - * - * @see {@link https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency | concurrency control docs} for more details - */ - resourceVersion?: string; - /** - * Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. - */ - selfLink?: string; - /** - * UID is the unique in time and space value for this object. It is typically generated by - * the server on successful creation of a resource and is not allowed to change on PUT - * operations. Populated by the system. Read-only. - * - * @see {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids | UIDs docs} for more details. - */ - uid: string; -} - export interface KubeOwnerReference { /** API version of the referent. */ apiVersion: string; @@ -237,29 +80,6 @@ export interface KubeOwnerReference { uid: string; } -export interface ApiListOptions extends QueryParameters { - /** - * The clusters to list objects from. By default uses the current clusters being viewed. - */ - clusters?: string[]; - /** The namespace to list objects from. */ - namespace?: string | string[]; - /** - * The cluster to list objects from. By default uses the current cluster being viewed. - * If clusters is set, then we use that and "cluster" is ignored. - */ - cluster?: string; -} - -export interface ApiListSingleNamespaceOptions { - /** The namespace to get the object from. */ - namespace?: string; - /** The parameters to be passed to the API endpoint. */ - queryParams?: QueryParameters; - /** The cluster to get the object from. By default uses the current cluster being viewed. */ - cluster?: string; -} - /** * ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the * resource that the fieldset applies to. @@ -313,575 +133,10 @@ export interface KubeManagedFieldsEntry { */ export interface KubeManagedFields extends KubeManagedFieldsEntry {} -// We have to define a KubeObject implementation here because the KubeObject -// class is defined within the function and therefore not inferable. -export interface KubeObjectIface { - apiList: ( - onList: (arg: InstanceType>[]) => void, - onError?: (err: ApiError, cluster?: string) => void, - opts?: ApiListSingleNamespaceOptions - ) => any; - useApiList: ( - onList: (arg: InstanceType>[]) => void, - onError?: (err: ApiError, cluster?: string) => void, - opts?: ApiListOptions - ) => any; - useApiGet: ( - onGet: (...args: any) => void, - name: string, - namespace?: string, - onError?: (err: ApiError, cluster?: string) => void - ) => void; - useList(options?: ApiListOptions): ReturnType; - useGet: ( - name: string, - namespace?: string, - opts?: { - queryParams?: QueryParameters; - cluster?: string; - } - ) => ReturnType; - getErrorMessage: (err?: ApiError | null) => string | null; - new (json: T): any; - className: string; - [prop: string]: any; - getAuthorization?: (arg: string, resourceAttrs?: AuthRequestResourceAttrs) => any; -} - -export interface AuthRequestResourceAttrs { - name?: string; - resource?: string; - subresource?: string; - namespace?: string; - version?: string; - group?: string; - verb?: string; -} -type JsonPath = T extends object - ? { - [K in keyof T]: K extends string ? `${K}` | `${K}.${JsonPath}` : never; - }[keyof T] - : never; - -// @todo: uses of makeKubeObject somehow end up in an 'any' type. - /** - * @returns A KubeObject implementation for the given object name. - * - * @param objectName The name of the object to create a KubeObject implementation for. + * @deprecated For backwards compatibility, please use KubeObject */ -export function makeKubeObject( - objectName: string -): KubeObjectIface { - class KubeObject { - static apiEndpoint: ReturnType; - jsonData: T | null = null; - public static readOnlyFields: JsonPath[]; - private _clusterName: string; - - constructor(json: T, cluster?: string) { - this.jsonData = json; - this._clusterName = cluster || getCluster() || ''; - } - - get cluster() { - return this._clusterName; - } - - set cluster(cluster: string) { - this._clusterName = cluster; - } - - static get className(): string { - return objectName; - } - - get detailsRoute(): string { - return this._class().detailsRoute; - } - - static get detailsRoute(): string { - return this.className; - } - - static get pluralName(): string { - // This is a naive way to get the plural name of the object by default. It will - // work in most cases, but for exceptions (like Ingress), we must override this. - return this.className.toLowerCase() + 's'; - } - - get pluralName(): string { - // In case we need to override the plural name in instances. - return this._class().pluralName; - } - - get listRoute(): string { - return this._class().listRoute; - } - - static get listRoute(): string { - return this.detailsRoute + 's'; - } - - getDetailsLink() { - const params = { - namespace: this.getNamespace(), - name: this.getName(), - }; - const link = createRouteURL(this.detailsRoute, params); - return link; - } - - getListLink() { - return createRouteURL(this.listRoute); - } - - getName() { - return this.metadata.name; - } - - getNamespace() { - return this.metadata.namespace; - } - - getCreationTs() { - return this.metadata.creationTimestamp; - } - - getAge() { - return timeAgo(this.getCreationTs()); - } - - getValue(prop: string) { - return this.jsonData![prop]; - } - - get metadata() { - return this.jsonData!.metadata; - } - - get kind() { - return this.jsonData!.kind; - } - - get isNamespaced() { - return this._class().isNamespaced; - } - - static get isNamespaced() { - return this.apiEndpoint.isNamespaced; - } - - getEditableObject() { - const fieldsToRemove = this._class().readOnlyFields; - const code = this.jsonData ? cloneDeep(this.jsonData) : {}; - - fieldsToRemove?.forEach((path: JsonPath) => { - JSONPath({ - path, - json: code, - callback: (result, type, fullPayload) => { - if (fullPayload.parent && fullPayload.parentProperty) { - delete fullPayload.parent[fullPayload.parentProperty]; - } - }, - resultType: 'all', - }); - }); - - return code; - } - - // @todo: apiList has 'any' return type. - /** - * Returns the API endpoint for this object. - * - * @param onList - Callback function to be called when the list is retrieved. - * @param onError - Callback function to be called when an error occurs. - * @param opts - Options to be passed to the API endpoint. - * - * @returns The API endpoint for this object. - */ - static apiList( - onList: (arg: U[]) => void, - onError?: (err: ApiError, cluster?: string) => void, - opts?: ApiListSingleNamespaceOptions - ) { - const createInstance = (item: T) => this.create(item, opts?.cluster) as U; - - const args: any[] = [(list: T[]) => onList(list.map((item: T) => createInstance(item) as U))]; - - if (this.apiEndpoint.isNamespaced) { - args.unshift(opts?.namespace || null); - } - - args.push(onError); - - const queryParams: QueryParameters = {}; - if (opts?.queryParams?.labelSelector) { - queryParams['labelSelector'] = opts.queryParams.labelSelector; - } - if (opts?.queryParams?.fieldSelector) { - queryParams['fieldSelector'] = opts.queryParams.fieldSelector; - } - if (opts?.queryParams?.limit) { - queryParams['limit'] = opts.queryParams.limit; - } - args.push(queryParams); - - args.push(opts?.cluster); - - return this.apiEndpoint.list.bind(null, ...args); - } - - static useApiList( - onList: (...arg: any[]) => any, - onError?: (err: ApiError, cluster?: string) => void, - opts?: ApiListOptions - ) { - const [objs, setObjs] = React.useState<{ [key: string]: U[] }>({}); - const listCallback = onList as (arg: U[]) => void; - - function onObjs(namespace: string, objList: U[]) { - let newObjs: typeof objs = {}; - // Set the objects so we have them for the next API response... - setObjs(previousObjs => { - newObjs = { ...previousObjs, [namespace || '']: objList }; - return newObjs; - }); - - let allObjs: U[] = []; - Object.values(newObjs).map(currentObjs => { - allObjs = allObjs.concat(currentObjs); - }); - - listCallback(allObjs); - } - - const listCalls = []; - const queryParams = cloneDeep(opts); - let namespaces: string[] = []; - unset(queryParams, 'namespace'); - - const cluster = opts?.clusters?.[0] || opts?.cluster; - - if (!!opts?.namespace) { - if (typeof opts.namespace === 'string') { - namespaces = [opts.namespace]; - } else if (Array.isArray(opts.namespace)) { - namespaces = opts.namespace as string[]; - } else { - throw Error('namespace should be a string or array of strings'); - } - } - - // If the request itself has no namespaces set, we check whether to apply the - // allowed namespaces. - if (namespaces.length === 0 && this.isNamespaced) { - namespaces = getAllowedNamespaces(); - } - - if (namespaces.length > 0) { - // If we have a namespace set, then we have to make an API call for each - // namespace and then set the objects once we have all of the responses. - for (const namespace of namespaces) { - listCalls.push( - this.apiList(objList => onObjs(namespace, objList as U[]), onError, { - namespace, - queryParams, - cluster, - }) - ); - } - } else { - // If we don't have a namespace set, then we only have one API call - // response to set and we return it right away. - listCalls.push(this.apiList(listCallback, onError, { queryParams, cluster })); - } - - useConnectApi(...listCalls); - } - - static useList( - this: U, - { cluster, namespace, clusters, ...queryParams }: ApiListOptions & QueryParameters = {} - ) { - const currentCluster = useCluster(); - - return useKubeObjectList({ - queryParams: queryParams, - kubeObjectClass: this, - cluster: clusters?.[0] || cluster || currentCluster || undefined, - namespace: Array.isArray(namespace) ? namespace[0] : namespace, - }); - } - - static useGet( - this: U, - name: string, - namespace?: string, - opts?: { - queryParams?: QueryParameters; - cluster?: string; - } - ) { - return useKubeObject({ - kubeObjectClass: this, - name: name, - namespace: namespace, - cluster: opts?.cluster, - queryParams: opts?.queryParams, - }); - } - - static create( - this: new (arg: T, cluster?: string) => U, - item: T, - cluster?: string - ): U { - return new this(item, cluster) as U; - } - - static apiGet( - onGet: (...args: any) => void, - name: string, - namespace?: string, - onError?: (err: ApiError | null, cluster?: string) => void, - opts?: { - queryParams?: QueryParameters; - cluster?: string; - } - ) { - const createInstance = (item: T) => this.create(item) as U; - const args: any[] = [name, (obj: T) => onGet(createInstance(obj))]; - - if (this.apiEndpoint.isNamespaced) { - args.unshift(namespace); - } - - args.push(onError); - args.push(opts?.queryParams); - args.push(opts?.cluster); - - return this.apiEndpoint.get.bind(null, ...args); - } - - static useApiGet( - onGet: (...args: any) => any, - name: string, - namespace?: string, - onError?: (err: ApiError | null, cluster?: string) => void, - opts?: { - queryParams?: QueryParameters; - cluster?: string; - } - ) { - // We do the type conversion here because we want to be able to use hooks that may not have - // the exact signature as get callbacks. - const getCallback = onGet as (item: U) => void; - useConnectApi(this.apiGet(getCallback, name, namespace, onError, opts)); - } - - private _class() { - return this.constructor as typeof KubeObject; - } - - delete() { - const args: string[] = [this.getName()]; - if (this.isNamespaced) { - args.unshift(this.getNamespace()!); - } - - // @ts-ignore - return this._class().apiEndpoint.delete(...args, {}, this._clusterName); - } - - update(data: KubeObjectInterface) { - return this._class().apiEndpoint.put(data, {}, this._clusterName); - } - - static put(data: KubeObjectInterface) { - return this.apiEndpoint.put(data); - } - - scale(numReplicas: number) { - const hasScaleApi = Object.keys(this._class().apiEndpoint).includes('scale'); - if (!hasScaleApi) { - throw new Error(`This class has no scale API: ${this._class().className}`); - } - - const spec = { - replicas: numReplicas, - }; - - type ApiEndpointWithScale = { - scale: { - patch: ( - body: { spec: { replicas: number } }, - metadata: KubeMetadata, - clusterName?: string - ) => Promise; - }; - }; - - return (this._class().apiEndpoint as ApiEndpointWithScale).scale.patch( - { - spec, - }, - this.metadata, - this._clusterName - ); - } - - patch(body: OpPatch[]) { - const patchMethod = this._class().apiEndpoint.patch; - const args: Partial> = [body]; - - if (this.isNamespaced) { - args.push(this.getNamespace()); - } - - args.push(this.getName()); - // @ts-ignore - return this._class().apiEndpoint.patch(...args, {}, this._clusterName); - } - - /** Performs a request to check if the user has the given permission. - * @param reResourceAttrs The attributes describing this access request. See https://kubernetes.io/docs/reference/kubernetes-api/authorization-resources/self-subject-access-review-v1/#SelfSubjectAccessReviewSpec . - * @returns The result of the access request. - */ - private static async fetchAuthorization(reqResourseAttrs?: AuthRequestResourceAttrs) { - // @todo: We should get the API info from the API endpoint. - const authApiVersions = ['v1', 'v1beta1']; - for (let j = 0; j < authApiVersions.length; j++) { - const authVersion = authApiVersions[j]; - - try { - return await post( - `/apis/authorization.k8s.io/${authVersion}/selfsubjectaccessreviews`, - { - kind: 'SelfSubjectAccessReview', - apiVersion: `authorization.k8s.io/${authVersion}`, - spec: { - resourceAttributes: reqResourseAttrs, - }, - }, - false - ); - } catch (err) { - // If this is the last attempt or the error is not 404, let it throw. - if ((err as ApiError).status !== 404 || j === authApiVersions.length - 1) { - throw err; - } - } - } - } - - static async getAuthorization(verb: string, reqResourseAttrs?: AuthRequestResourceAttrs) { - const resourceAttrs: AuthRequestResourceAttrs = { - verb, - ...reqResourseAttrs, - }; - - if (!resourceAttrs.resource) { - resourceAttrs['resource'] = this.pluralName; - } - - // @todo: We should get the API info from the API endpoint. - - // If we already have the group, version, and resource, then we can make the request - // without trying the API info, which may have several versions and thus be less optimal. - if (!!resourceAttrs.group && !!resourceAttrs.version && !!resourceAttrs.resource) { - return this.fetchAuthorization(resourceAttrs); - } - - // If we don't have the group, version, and resource, then we have to try all of the - // API info versions until we find one that works. - const apiInfo = this.apiEndpoint.apiInfo; - for (let i = 0; i < apiInfo.length; i++) { - const { group, version, resource } = apiInfo[i]; - // We only take from the details from the apiInfo if they're missing from the resourceAttrs. - // The idea is that, since this function may also be called from the instance's getAuthorization, - // it may already have the details from the instance's API version. - const attrs = { ...resourceAttrs }; - - if (!!attrs.resource) { - attrs.resource = resource; - } - if (!!attrs.group) { - attrs.group = group; - } - if (!!attrs.version) { - attrs.version = version; - } - - let authResult; - - try { - authResult = await this.fetchAuthorization(attrs); - } catch (err) { - // If this is the last attempt or the error is not 404, let it throw. - if ((err as ApiError).status !== 404 || i === apiInfo.length - 1) { - throw err; - } - } - - if (!!authResult) { - return authResult; - } - } - } - - async getAuthorization(verb: string, reqResourseAttrs?: AuthRequestResourceAttrs) { - const resourceAttrs: AuthRequestResourceAttrs = { - name: this.getName(), - verb, - ...reqResourseAttrs, - }; - - const namespace = this.getNamespace(); - if (!resourceAttrs.namespace && !!namespace) { - resourceAttrs['namespace'] = namespace; - } - - // Set up the group and version from the object's API version. - let [group, version] = this.jsonData?.apiVersion?.split('/') ?? []; - if (!version) { - version = group; - group = ''; - } - - if (!!group) { - resourceAttrs['group'] = group; - } - if (!!version) { - resourceAttrs['version'] = version; - } - - return this._class().getAuthorization(verb, resourceAttrs); - } - - static getErrorMessage(err: ApiError | null) { - if (!err) { - return null; - } - - switch (err.status) { - case 404: - return 'Error: Not found'; - case 403: - return 'Error: No permissions'; - default: - return 'Error'; - } - } - } - - return KubeObject as KubeObjectIface; -} - -export type KubeObjectClass = ReturnType; -export type KubeObject = InstanceType; +export type KubeObjectIface = any; export type Time = number | string | null; @@ -1285,5 +540,3 @@ export interface KubeContainerStatus { state: Partial; started?: boolean; } - -export type Workload = DaemonSet | ReplicaSet | StatefulSet | Job | CronJob | Deployment; diff --git a/frontend/src/lib/k8s/clusterRole.ts b/frontend/src/lib/k8s/clusterRole.ts index b49b5a272a..f9f4e040fa 100644 --- a/frontend/src/lib/k8s/clusterRole.ts +++ b/frontend/src/lib/k8s/clusterRole.ts @@ -1,17 +1,11 @@ -import { apiFactory } from './apiProxy'; -import { makeKubeObject } from './cluster'; +import { makeKubeObject } from './KubeObject'; import { KubeRole } from './role'; -class ClusterRole extends makeKubeObject('role') { - static apiEndpoint = apiFactory('rbac.authorization.k8s.io', 'v1', 'clusterroles'); - - static get className() { - return 'ClusterRole'; - } - - get detailsRoute() { - return 'clusterRole'; - } +class ClusterRole extends makeKubeObject() { + static kind = 'ClusterRole'; + static apiName = 'clusterroles'; + static apiVersion = 'rbac.authorization.k8s.io/v1'; + static isNamespaced = false; get rules() { return this.jsonData!.rules; diff --git a/frontend/src/lib/k8s/clusterRoleBinding.ts b/frontend/src/lib/k8s/clusterRoleBinding.ts index 354c08fa3c..a1411cd52a 100644 --- a/frontend/src/lib/k8s/clusterRoleBinding.ts +++ b/frontend/src/lib/k8s/clusterRoleBinding.ts @@ -1,17 +1,11 @@ -import { apiFactory } from './apiProxy'; -import { makeKubeObject } from './cluster'; +import { makeKubeObject } from './KubeObject'; import { KubeRoleBinding } from './roleBinding'; -class ClusterRoleBinding extends makeKubeObject('roleBinding') { - static apiEndpoint = apiFactory('rbac.authorization.k8s.io', 'v1', 'clusterrolebindings'); - - static get className(): string { - return 'ClusterRoleBinding'; - } - - get detailsRoute() { - return 'clusterRoleBinding'; - } +class ClusterRoleBinding extends makeKubeObject() { + static kind = 'ClusterRoleBinding'; + static apiName = 'clusterrolebindings'; + static apiVersion = 'rbac.authorization.k8s.io/v1'; + static isNamespaced = false; get roleRef() { return this.jsonData!.roleRef; diff --git a/frontend/src/lib/k8s/configMap.ts b/frontend/src/lib/k8s/configMap.ts index 9ac7f7906d..5609fd98cf 100644 --- a/frontend/src/lib/k8s/configMap.ts +++ b/frontend/src/lib/k8s/configMap.ts @@ -1,15 +1,18 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject, StringDict } from './cluster'; +import { StringDict } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeConfigMap extends KubeObjectInterface { data: StringDict; } -class ConfigMap extends makeKubeObject('configMap') { - static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'configmaps'); +class ConfigMap extends KubeObject { + static kind = 'ConfigMap'; + static apiName = 'configmaps'; + static apiVersion = 'v1'; + static isNamespaced = true; get data() { - return this.jsonData?.data; + return this.jsonData.data; } } diff --git a/frontend/src/lib/k8s/crd.ts b/frontend/src/lib/k8s/crd.ts index 65ca1ddbee..1df833b571 100644 --- a/frontend/src/lib/k8s/crd.ts +++ b/frontend/src/lib/k8s/crd.ts @@ -1,6 +1,8 @@ import { ResourceClasses } from '.'; import { apiFactory, apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectClass, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject } from './KubeObject'; +import { KubeObjectInterface } from './KubeObject'; +import { KubeObjectClass } from './KubeObject'; export interface KubeCRD extends KubeObjectInterface { spec: { @@ -47,15 +49,16 @@ export interface KubeCRD extends KubeObjectInterface { }; } -class CustomResourceDefinition extends makeKubeObject('crd') { - static apiEndpoint = apiFactory( - ['apiextensions.k8s.io', 'v1', 'customresourcedefinitions'], - ['apiextensions.k8s.io', 'v1beta1', 'customresourcedefinitions'] - ); +class CustomResourceDefinition extends KubeObject { + static kind = 'CustomResourceDefinition'; + static apiName = 'customresourcedefinitions'; + static apiVersion = ['apiextensions.k8s.io/v1', 'apiextensions.k8s.io/v1beta1']; + static isNamespaced = false; + static readOnlyFields = ['metadata.managedFields']; - static get className(): string { - return 'CustomResourceDefinition'; + static get listRoute(): string { + return 'crds'; } static get detailsRoute(): string { @@ -63,11 +66,11 @@ class CustomResourceDefinition extends makeKubeObject('crd') { } get spec(): KubeCRD['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): KubeCRD['status'] { - return this.jsonData!.status; + return this.jsonData.status; } get plural(): string { @@ -98,7 +101,7 @@ class CustomResourceDefinition extends makeKubeObject('crd') { return this.spec.scope === 'Namespaced'; } - makeCRClass(): KubeObjectClass { + makeCRClass(): typeof KubeObject { const apiInfo: CRClassArgs['apiInfo'] = (this.jsonData as KubeCRD).spec.versions.map( versionInfo => ({ group: this.spec.group, version: versionInfo.name }) ); @@ -130,12 +133,12 @@ export interface CRClassArgs { export function makeCustomResourceClass( args: [group: string, version: string, pluralName: string][], isNamespaced: boolean -): ReturnType; -export function makeCustomResourceClass(args: CRClassArgs): ReturnType; +): KubeObjectClass; +export function makeCustomResourceClass(args: CRClassArgs): KubeObjectClass; export function makeCustomResourceClass( args: [group: string, version: string, pluralName: string][] | CRClassArgs, isNamespaced?: boolean -): ReturnType { +): KubeObjectClass { let apiInfoArgs: [group: string, version: string, pluralName: string][] = []; if (Array.isArray(args)) { @@ -146,7 +149,7 @@ export function makeCustomResourceClass( // Used for tests if (import.meta.env.UNDER_TEST || import.meta.env.STORYBOOK) { - const knownClass = ResourceClasses[apiInfoArgs[0][2]]; + const knownClass = (ResourceClasses as Record)[apiInfoArgs[0][2]]; if (!!knownClass) { return knownClass; } @@ -159,7 +162,12 @@ export function makeCustomResourceClass( }; const apiFunc = !!objArgs.isNamespaced ? apiFactoryWithNamespace : apiFactory; - return class CRClass extends makeKubeObject(objArgs.singleName) { + return class CRClass extends KubeObject { + static kind = objArgs.singleName; + static apiName = crClassArgs.pluralName; + static apiVersion = apiInfoArgs.map(([group, version]) => + group ? `${group}/${version}` : version + ); static apiEndpoint = apiFunc(...apiInfoArgs); }; } diff --git a/frontend/src/lib/k8s/cronJob.ts b/frontend/src/lib/k8s/cronJob.ts index 25e2a189eb..36a108cd22 100644 --- a/frontend/src/lib/k8s/cronJob.ts +++ b/frontend/src/lib/k8s/cronJob.ts @@ -1,5 +1,6 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeContainer, KubeMetadata, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeContainer } from './cluster'; +import { KubeMetadata } from './KubeMetadata'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; /** * CronJob structure returned by the k8s API. @@ -34,11 +35,11 @@ export interface KubeCronJob extends KubeObjectInterface { }; } -class CronJob extends makeKubeObject('CronJob') { - static apiEndpoint = apiFactoryWithNamespace( - ['batch', 'v1', 'cronjobs'], - ['batch', 'v1beta1', 'cronjobs'] - ); +class CronJob extends KubeObject { + static kind = 'CronJob'; + static apiName = 'cronjobs'; + static apiVersion = ['batch/v1', 'batch/v1beta1']; + static isNamespaced = true; get spec() { return this.getValue('spec'); diff --git a/frontend/src/lib/k8s/daemonSet.ts b/frontend/src/lib/k8s/daemonSet.ts index 82cfbcf6a8..da4088da3c 100644 --- a/frontend/src/lib/k8s/daemonSet.ts +++ b/frontend/src/lib/k8s/daemonSet.ts @@ -1,11 +1,6 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { - KubeContainer, - KubeMetadata, - KubeObjectInterface, - LabelSelector, - makeKubeObject, -} from './cluster'; +import { KubeContainer, LabelSelector } from './cluster'; +import { KubeMetadata } from './KubeMetadata'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; import { KubePodSpec } from './pod'; export interface KubeDaemonSet extends KubeObjectInterface { @@ -28,15 +23,18 @@ export interface KubeDaemonSet extends KubeObjectInterface { }; } -class DaemonSet extends makeKubeObject('DaemonSet') { - static apiEndpoint = apiFactoryWithNamespace('apps', 'v1', 'daemonsets'); +class DaemonSet extends KubeObject { + static kind = 'DaemonSet'; + static apiName = 'daemonsets'; + static apiVersion = 'apps/v1'; + static isNamespaced = true; get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } get status() { - return this.jsonData!.status; + return this.jsonData.status; } getContainers(): KubeContainer[] { diff --git a/frontend/src/lib/k8s/deployment.ts b/frontend/src/lib/k8s/deployment.ts index b02bb34e4e..4632392f15 100644 --- a/frontend/src/lib/k8s/deployment.ts +++ b/frontend/src/lib/k8s/deployment.ts @@ -1,11 +1,6 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { - KubeContainer, - KubeMetadata, - KubeObjectInterface, - LabelSelector, - makeKubeObject, -} from './cluster'; +import { KubeContainer, LabelSelector } from './cluster'; +import { KubeMetadata } from './KubeMetadata'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; import { KubePodSpec } from './pod'; export interface KubeDeployment extends KubeObjectInterface { @@ -26,8 +21,11 @@ export interface KubeDeployment extends KubeObjectInterface { }; } -class Deployment extends makeKubeObject('Deployment') { - static apiEndpoint = apiFactoryWithNamespace('apps', 'v1', 'deployments', true); +class Deployment extends KubeObject { + static kind = 'Deployment'; + static apiName = 'deployments'; + static apiVersion = 'apps/v1'; + static isNamespaced = true; get spec() { return this.getValue('spec'); diff --git a/frontend/src/lib/k8s/endpoints.ts b/frontend/src/lib/k8s/endpoints.ts index 90d4e731e6..90237b605d 100644 --- a/frontend/src/lib/k8s/endpoints.ts +++ b/frontend/src/lib/k8s/endpoints.ts @@ -1,5 +1,5 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeMetadata, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeMetadata } from './KubeMetadata'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeEndpointPort { name?: string; @@ -28,19 +28,32 @@ export interface KubeEndpoint extends KubeObjectInterface { subsets: KubeEndpointSubset[]; } -class Endpoints extends makeKubeObject('endpoint') { - static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'endpoints'); +class Endpoints extends KubeObject { + static kind = 'Endpoints'; + static apiName = 'endpoints'; + static apiVersion = 'v1'; + static isNamespaced = true; + + // @todo Remove this when we can break backward compatibility. + static get detailsRoute() { + return 'Endpoint'; + } + + // @todo Remove this when we can break backward compatibility. + static get className() { + return 'Endpoint'; + } get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } get status() { - return this.jsonData!.status; + return this.jsonData.status; } get subsets() { - return this.jsonData!.subsets; + return this.jsonData.subsets; } getAddressesText() { diff --git a/frontend/src/lib/k8s/event.ts b/frontend/src/lib/k8s/event.ts index 3792e69b61..2867035760 100644 --- a/frontend/src/lib/k8s/event.ts +++ b/frontend/src/lib/k8s/event.ts @@ -1,8 +1,10 @@ import { useMemo } from 'react'; import { ResourceClasses } from '.'; -import { ApiError, apiFactoryWithNamespace, QueryParameters } from './apiProxy'; +import { ApiError, QueryParameters } from './apiProxy'; import { request } from './apiProxy'; -import { KubeMetadata, KubeObject, makeKubeObject } from './cluster'; +import { KubeMetadata } from './KubeMetadata'; +import { KubeObject } from './KubeObject'; +import { KubeObjectClass } from './KubeObject'; export interface KubeEvent { type: string; @@ -21,8 +23,12 @@ export interface KubeEvent { [otherProps: string]: any; } -class Event extends makeKubeObject('Event') { - static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'events'); +class Event extends KubeObject { + static kind = 'Event'; + static apiName = 'events'; + static apiVersion = 'v1'; + + static isNamespaced = true; // Max number of events to fetch from the API private static maxEventsLimit = 2000; @@ -105,7 +111,7 @@ class Event extends makeKubeObject('Event') { return eventTime; } - const firstTimestamp = this.firstTimestamp; + const firstTimestamp = this.getValue('firstTimestamp'); if (!!firstTimestamp) { return firstTimestamp; } @@ -149,7 +155,9 @@ class Event extends makeKubeObject('Event') { return null; } - const InvolvedObjectClass = ResourceClasses[this.involvedObject.kind]; + const InvolvedObjectClass = (ResourceClasses as Record)[ + this.involvedObject.kind + ]; let objInstance: KubeObject | null = null; if (!!InvolvedObjectClass) { objInstance = new InvolvedObjectClass({ @@ -157,7 +165,7 @@ class Event extends makeKubeObject('Event') { metadata: { name: this.involvedObject.name, namespace: this.involvedObject.namespace, - }, + } as KubeMetadata, }); } diff --git a/frontend/src/lib/k8s/hpa.ts b/frontend/src/lib/k8s/hpa.ts index b68ffe0695..95cc5518b7 100644 --- a/frontend/src/lib/k8s/hpa.ts +++ b/frontend/src/lib/k8s/hpa.ts @@ -1,6 +1,6 @@ import { ResourceClasses } from '.'; -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObject, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeMetadata } from './KubeMetadata'; +import { KubeObject, KubeObjectClass, KubeObjectInterface } from './KubeObject'; export interface CrossVersionObjectReference { apiVersion: string; kind: string; @@ -166,15 +166,18 @@ interface HPAMetrics { shortValue: string; } -class HPA extends makeKubeObject('horizontalPodAutoscaler') { - static apiEndpoint = apiFactoryWithNamespace('autoscaling', 'v2', 'horizontalpodautoscalers'); +class HPA extends KubeObject { + static kind = 'HorizontalPodAutoscaler'; + static apiName = 'horizontalpodautoscalers'; + static apiVersion = 'autoscaling/v2'; + static isNamespaced = true; get spec(): HpaSpec { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): HpaStatus { - return this.jsonData!.status; + return this.jsonData.status; } metrics(t: Function): HPAMetrics[] { @@ -334,12 +337,12 @@ class HPA extends makeKubeObject('horizontalPodAutoscaler') { } get referenceObject(): KubeObject | null { - const target = this.jsonData?.spec?.scaleTargetRef; + const target = this.jsonData.spec?.scaleTargetRef; if (!target) { return null; } - const TargetObjectClass = ResourceClasses[target.kind]; + const TargetObjectClass = (ResourceClasses as Record)[target.kind]; let objInstance: KubeObject | null = null; if (!!TargetObjectClass) { objInstance = new TargetObjectClass({ @@ -347,7 +350,7 @@ class HPA extends makeKubeObject('horizontalPodAutoscaler') { metadata: { name: target.name, namespace: this.getNamespace(), - }, + } as KubeMetadata, }); } diff --git a/frontend/src/lib/k8s/index.test.ts b/frontend/src/lib/k8s/index.test.ts index 9022e5ec88..a14c0adb4d 100644 --- a/frontend/src/lib/k8s/index.test.ts +++ b/frontend/src/lib/k8s/index.test.ts @@ -1,6 +1,7 @@ import { createRouteURL } from '../router'; import { labelSelectorToQuery, ResourceClasses } from '.'; -import { KubeObjectClass, LabelSelector } from './cluster'; +import { LabelSelector } from './cluster'; +import { KubeObjectClass } from './KubeObject'; import Namespace from './namespace'; // Remove NetworkPolicy since we don't use it. @@ -224,6 +225,7 @@ const namespacedClasses = [ 'DaemonSet', 'Deployment', 'Endpoint', + 'Endpoints', 'HorizontalPodAutoscaler', 'Ingress', 'Job', @@ -244,7 +246,7 @@ const namespacedClasses = [ ]; describe('Test class namespaces', () => { - const classCopy = { ...ResourceClasses }; + const classCopy: Record = { ...ResourceClasses }; namespacedClasses.forEach(cls => { test(`Check namespaced ${cls}`, () => { expect(classCopy[cls]).toBeDefined(); diff --git a/frontend/src/lib/k8s/index.ts b/frontend/src/lib/k8s/index.ts index 151fad2da6..2651039035 100644 --- a/frontend/src/lib/k8s/index.ts +++ b/frontend/src/lib/k8s/index.ts @@ -5,7 +5,7 @@ import { ConfigState } from '../../redux/configSlice'; import { useTypedSelector } from '../../redux/reducers/reducers'; import { getCluster, getClusterGroup, getClusterPrefixedPath } from '../util'; import { ApiError, clusterRequest } from './apiProxy'; -import { Cluster, KubeObject, LabelSelector, StringDict } from './cluster'; +import { Cluster, LabelSelector, StringDict } from './cluster'; import ClusterRole from './clusterRole'; import ClusterRoleBinding from './clusterRoleBinding'; import ConfigMap from './configMap'; @@ -39,7 +39,7 @@ import ServiceAccount from './serviceAccount'; import StatefulSet from './statefulSet'; import StorageClass from './storageClass'; -const classList = [ +export const ResourceClasses = { ClusterRole, ClusterRoleBinding, ConfigMap, @@ -47,11 +47,12 @@ const classList = [ CronJob, DaemonSet, Deployment, + Endpoint: Endpoints, Endpoints, LimitRange, Lease, ResourceQuota, - HPA, + HorizontalPodAutoscaler: HPA, PodDisruptionBudget, PriorityClass, Ingress, @@ -72,20 +73,7 @@ const classList = [ ServiceAccount, StatefulSet, StorageClass, -]; - -const resourceClassesDict: { - [className: string]: KubeObject; -} = {}; - -classList.forEach(cls => { - // Ideally this should just be the class name, but until we ensure the class name is consistent - // (in what comes to the capitalization), we use this lazy approach. - const className: string = cls.className.charAt(0).toUpperCase() + cls.className.slice(1); - resourceClassesDict[className] = cls; -}); - -export const ResourceClasses = resourceClassesDict; +}; /** Hook for getting or fetching the clusters configuration. * This gets the clusters from the redux store. The redux store is updated diff --git a/frontend/src/lib/k8s/ingress.ts b/frontend/src/lib/k8s/ingress.ts index a2f6ac3b34..ca4c913982 100644 --- a/frontend/src/lib/k8s/ingress.ts +++ b/frontend/src/lib/k8s/ingress.ts @@ -1,5 +1,4 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; interface LegacyIngressRule { host: string; @@ -68,16 +67,17 @@ export interface KubeIngress extends KubeObjectInterface { }; } -class Ingress extends makeKubeObject('ingress') { - static apiEndpoint = apiFactoryWithNamespace( - ['networking.k8s.io', 'v1', 'ingresses'], - ['extensions', 'v1beta1', 'ingresses'] - ); +class Ingress extends KubeObject { + static kind = 'Ingress'; + static apiName = 'ingresses'; + static apiVersion = ['networking.k8s.io/v1', 'extensions/v1beta1']; + static isNamespaced = true; + // Normalized, cached rules. private cachedRules: IngressRule[] = []; get spec(): KubeIngress['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } getHosts() { @@ -129,14 +129,6 @@ class Ingress extends makeKubeObject('ingress') { this.cachedRules = rules; return rules; } - - static get listRoute() { - return 'ingresses'; - } - - static get pluralName() { - return 'ingresses'; - } } export default Ingress; diff --git a/frontend/src/lib/k8s/ingressClass.ts b/frontend/src/lib/k8s/ingressClass.ts index 79db73b4f7..bb16021d2a 100644 --- a/frontend/src/lib/k8s/ingressClass.ts +++ b/frontend/src/lib/k8s/ingressClass.ts @@ -1,5 +1,4 @@ -import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeIngressClass extends KubeObjectInterface { spec: { @@ -8,15 +7,18 @@ export interface KubeIngressClass extends KubeObjectInterface { }; } -class IngressClass extends makeKubeObject('ingressClass') { - static apiEndpoint = apiFactory(['networking.k8s.io', 'v1', 'ingressclasses']); +class IngressClass extends KubeObject { + static kind = 'IngressClass'; + static apiName = 'ingressclasses'; + static apiVersion = 'networking.k8s.io/v1'; + static isNamespaced = false; get spec(): KubeIngressClass['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get isDefault(): boolean { - const annotations = this.jsonData!.metadata?.annotations; + const annotations = this.jsonData.metadata?.annotations; if (annotations !== undefined) { return annotations['ingressclass.kubernetes.io/is-default-class'] === 'true'; } diff --git a/frontend/src/lib/k8s/job.ts b/frontend/src/lib/k8s/job.ts index c2ec2963d1..1d71a2226c 100644 --- a/frontend/src/lib/k8s/job.ts +++ b/frontend/src/lib/k8s/job.ts @@ -1,11 +1,6 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { - KubeContainer, - KubeMetadata, - KubeObjectInterface, - LabelSelector, - makeKubeObject, -} from './cluster'; +import { KubeContainer, LabelSelector } from './cluster'; +import { KubeMetadata } from './KubeMetadata'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; import { KubePodSpec } from './pod'; export interface KubeJob extends KubeObjectInterface { @@ -22,15 +17,18 @@ export interface KubeJob extends KubeObjectInterface { }; } -class Job extends makeKubeObject('Job') { - static apiEndpoint = apiFactoryWithNamespace('batch', 'v1', 'jobs'); +class Job extends KubeObject { + static kind = 'Job'; + static apiName = 'jobs'; + static apiVersion = 'batch/v1'; + static isNamespaced = true; get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } get status() { - return this.jsonData!.status; + return this.jsonData.status; } getContainers(): KubeContainer[] { diff --git a/frontend/src/lib/k8s/lease.ts b/frontend/src/lib/k8s/lease.ts index 8df4801b40..954f290f6f 100644 --- a/frontend/src/lib/k8s/lease.ts +++ b/frontend/src/lib/k8s/lease.ts @@ -1,5 +1,4 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface LeaseSpec { holderIdentity: string; @@ -12,10 +11,13 @@ export interface KubeLease extends KubeObjectInterface { spec: LeaseSpec; } -export class Lease extends makeKubeObject('Lease') { - static apiEndpoint = apiFactoryWithNamespace('coordination.k8s.io', 'v1', 'leases'); +export class Lease extends KubeObject { + static kind = 'Lease'; + static apiName = 'leases'; + static apiVersion = 'coordination.k8s.io/v1'; + static isNamespaced = true; get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } } diff --git a/frontend/src/lib/k8s/limitRange.tsx b/frontend/src/lib/k8s/limitRange.tsx index d2007a1408..1380374c19 100644 --- a/frontend/src/lib/k8s/limitRange.tsx +++ b/frontend/src/lib/k8s/limitRange.tsx @@ -1,5 +1,4 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface LimitRangeSpec { limits: { @@ -27,10 +26,13 @@ export interface KubeLimitRange extends KubeObjectInterface { spec: LimitRangeSpec; } -export class LimitRange extends makeKubeObject('LimitRange') { - static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'limitranges'); +export class LimitRange extends KubeObject { + static kind = 'LimitRange'; + static apiName = 'limitranges'; + static apiVersion = 'v1'; + static isNamespaced = true; get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } } diff --git a/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts b/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts index c51aba2692..d939d7782b 100644 --- a/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts +++ b/frontend/src/lib/k8s/mutatingWebhookConfiguration.ts @@ -1,5 +1,5 @@ -import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, LabelSelector, makeKubeObject } from './cluster'; +import { LabelSelector } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeRuleWithOperations { apiGroups: string[]; @@ -42,17 +42,14 @@ export interface KubeMutatingWebhookConfiguration extends KubeObjectInterface { }[]; } -class MutatingWebhookConfiguration extends makeKubeObject( - 'MutatingWebhookConfiguration' -) { - static apiEndpoint = apiFactory( - 'admissionregistration.k8s.io', - 'v1', - 'mutatingwebhookconfigurations' - ); +class MutatingWebhookConfiguration extends KubeObject { + static kind = 'MutatingWebhookConfiguration'; + static apiName = 'mutatingwebhookconfigurations'; + static apiVersion = 'admissionregistration.k8s.io/v1'; + static isNamespaced = false; get webhooks(): KubeMutatingWebhookConfiguration['webhooks'] { - return this.jsonData!.webhooks; + return this.jsonData.webhooks; } } diff --git a/frontend/src/lib/k8s/namespace.ts b/frontend/src/lib/k8s/namespace.ts index c838ee3e69..699cfd6fdc 100644 --- a/frontend/src/lib/k8s/namespace.ts +++ b/frontend/src/lib/k8s/namespace.ts @@ -1,17 +1,21 @@ -import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeCondition } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeNamespace extends KubeObjectInterface { status: { phase: string; + conditions?: KubeCondition[]; }; } -class Namespace extends makeKubeObject('namespace') { - static apiEndpoint = apiFactory('', 'v1', 'namespaces'); +class Namespace extends KubeObject { + static kind = 'Namespace'; + static apiName = 'namespaces'; + static apiVersion = 'v1'; + static isNamespaced = false; get status() { - return this.jsonData!.status; + return this.jsonData.status; } /** diff --git a/frontend/src/lib/k8s/networkpolicy.tsx b/frontend/src/lib/k8s/networkpolicy.tsx index 0096e80cd6..b061a8fc94 100644 --- a/frontend/src/lib/k8s/networkpolicy.tsx +++ b/frontend/src/lib/k8s/networkpolicy.tsx @@ -1,5 +1,5 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, LabelSelector, makeKubeObject } from './cluster'; +import { LabelSelector } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface NetworkPolicyPort { port?: string | number; @@ -35,8 +35,11 @@ export interface KubeNetworkPolicy extends KubeObjectInterface { policyTypes: string[]; } -class NetworkPolicy extends makeKubeObject('NetworkPolicy') { - static apiEndpoint = apiFactoryWithNamespace('networking.k8s.io', 'v1', 'networkpolicies'); +class NetworkPolicy extends KubeObject { + static kind = 'NetworkPolicy'; + static apiName = 'networkpolicies'; + static apiVersion = 'networking.k8s.io/v1'; + static isNamespaced = true; static get pluralName() { return 'networkpolicies'; diff --git a/frontend/src/lib/k8s/node.ts b/frontend/src/lib/k8s/node.ts index 45d8e4dc55..e993390f9f 100644 --- a/frontend/src/lib/k8s/node.ts +++ b/frontend/src/lib/k8s/node.ts @@ -1,8 +1,9 @@ import React from 'react'; import { useErrorState } from '../util'; import { useConnectApi } from '.'; -import { ApiError, apiFactory, metrics } from './apiProxy'; -import { KubeCondition, KubeMetrics, KubeObjectInterface, makeKubeObject } from './cluster'; +import { ApiError, metrics } from './apiProxy'; +import { KubeCondition, KubeMetrics } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeNode extends KubeObjectInterface { status: { @@ -52,15 +53,18 @@ export interface KubeNode extends KubeObjectInterface { }; } -class Node extends makeKubeObject('node') { - static apiEndpoint = apiFactory('', 'v1', 'nodes'); +class Node extends KubeObject { + static kind = 'Node'; + static apiName = 'nodes'; + static apiVersion = 'v1'; + static isNamespaced = false; get status(): KubeNode['status'] { - return this.jsonData!.status; + return this.jsonData.status; } get spec(): KubeNode['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } static useMetrics(): [KubeMetrics[] | null, ApiError | null] { diff --git a/frontend/src/lib/k8s/persistentVolume.ts b/frontend/src/lib/k8s/persistentVolume.ts index bfaeb0ed9d..e442bde0f9 100644 --- a/frontend/src/lib/k8s/persistentVolume.ts +++ b/frontend/src/lib/k8s/persistentVolume.ts @@ -1,5 +1,4 @@ -import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubePersistentVolume extends KubeObjectInterface { spec: { @@ -15,15 +14,18 @@ export interface KubePersistentVolume extends KubeObjectInterface { }; } -class PersistentVolume extends makeKubeObject('persistentVolume') { - static apiEndpoint = apiFactory('', 'v1', 'persistentvolumes'); +class PersistentVolume extends KubeObject { + static kind = 'PersistentVolume'; + static apiName = 'persistentvolumes'; + static apiVersion = 'v1'; + static isNamespaced = false; get spec() { - return this.jsonData?.spec; + return this.jsonData.spec; } get status() { - return this.jsonData?.status; + return this.jsonData.status; } } diff --git a/frontend/src/lib/k8s/persistentVolumeClaim.ts b/frontend/src/lib/k8s/persistentVolumeClaim.ts index 9c4ec82a46..9a366729db 100644 --- a/frontend/src/lib/k8s/persistentVolumeClaim.ts +++ b/frontend/src/lib/k8s/persistentVolumeClaim.ts @@ -1,5 +1,4 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubePersistentVolumeClaim extends KubeObjectInterface { spec?: { @@ -26,17 +25,18 @@ export interface KubePersistentVolumeClaim extends KubeObjectInterface { }; } -class PersistentVolumeClaim extends makeKubeObject( - 'persistentVolumeClaim' -) { - static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'persistentvolumeclaims'); +class PersistentVolumeClaim extends KubeObject { + static kind = 'PersistentVolumeClaim'; + static apiName = 'persistentvolumeclaims'; + static apiVersion = 'v1'; + static isNamespaced = true; get spec() { - return this.jsonData?.spec; + return this.jsonData.spec; } get status() { - return this.jsonData?.status; + return this.jsonData.status; } } diff --git a/frontend/src/lib/k8s/pod.ts b/frontend/src/lib/k8s/pod.ts index dadb7c00a2..84f1c7fc98 100644 --- a/frontend/src/lib/k8s/pod.ts +++ b/frontend/src/lib/k8s/pod.ts @@ -1,13 +1,7 @@ import { Base64 } from 'js-base64'; -import { apiFactoryWithNamespace, stream, StreamArgs, StreamResultsCb } from './apiProxy'; -import { - KubeCondition, - KubeContainer, - KubeContainerStatus, - KubeObjectInterface, - makeKubeObject, - Time, -} from './cluster'; +import { stream, StreamArgs, StreamResultsCb } from './apiProxy'; +import { KubeCondition, KubeContainer, KubeContainerStatus, Time } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeVolume { name: string; @@ -26,6 +20,10 @@ export interface KubePodSpec { conditionType: string; }[]; volumes?: KubeVolume[]; + serviceAccountName?: string; + serviceAccount?: string; + priority?: string; + tolerations?: any[]; } export interface KubePod extends KubeObjectInterface { @@ -86,8 +84,12 @@ type PodDetailedStatus = { lastRestartDate: Date; }; -class Pod extends makeKubeObject('Pod') { - static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'pods'); +class Pod extends KubeObject { + static kind = 'Pod'; + static apiName = 'pods'; + static apiVersion = 'v1'; + static isNamespaced = true; + protected detailedStatusCache: Partial<{ resourceVersion: string; details: PodDetailedStatus }>; constructor(jsonData: KubePod) { @@ -96,11 +98,11 @@ class Pod extends makeKubeObject('Pod') { } get spec(): KubePod['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): KubePod['status'] { - return this.jsonData!.status; + return this.jsonData.status; } getLogs(...args: Parameters): () => void { diff --git a/frontend/src/lib/k8s/podDisruptionBudget.ts b/frontend/src/lib/k8s/podDisruptionBudget.ts index ad2611f24d..e88b222899 100644 --- a/frontend/src/lib/k8s/podDisruptionBudget.ts +++ b/frontend/src/lib/k8s/podDisruptionBudget.ts @@ -1,5 +1,4 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubePDB extends KubeObjectInterface { spec: { @@ -36,15 +35,18 @@ export interface KubePDB extends KubeObjectInterface { }; } -class PDB extends makeKubeObject('podDisruptionBudget') { - static apiEndpoint = apiFactoryWithNamespace(['policy', 'v1', 'poddisruptionbudgets']); +class PDB extends KubeObject { + static kind = 'PodDisruptionBudget'; + static apiName = 'poddisruptionbudgets'; + static apiVersion = 'policy/v1'; + static isNamespaced = true; get spec(): KubePDB['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): KubePDB['status'] { - return this.jsonData!.status; + return this.jsonData.status; } get selectors(): string[] { diff --git a/frontend/src/lib/k8s/priorityClass.ts b/frontend/src/lib/k8s/priorityClass.ts index d4c0f7c9ed..03a417f775 100644 --- a/frontend/src/lib/k8s/priorityClass.ts +++ b/frontend/src/lib/k8s/priorityClass.ts @@ -1,38 +1,32 @@ -import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubePriorityClass extends KubeObjectInterface { value: number; preemptionPolicy: string; - globalDefault?: boolean; + globalDefault?: boolean | null; description: string; } -class PriorityClass extends makeKubeObject('priorityClass') { - static apiEndpoint = apiFactory('scheduling.k8s.io', 'v1', 'priorityclasses'); +class PriorityClass extends KubeObject { + static kind = 'PriorityClass'; + static apiName = 'priorityclasses'; + static apiVersion = 'scheduling.k8s.io/v1'; + static isNamespaced = false; - static get pluralName(): string { - return 'priorityclasses'; - } - - static get listRoute() { - return 'priorityclasses'; - } - - get value(): string { + get value(): number { return this.jsonData!.value; } get globalDefault(): boolean | null { - return this.jsonData.globalDefault; + return this.jsonData.globalDefault!; } get description(): string { - return this.jsonData!.description; + return this.jsonData.description; } get preemptionPolicy(): string { - return this.jsonData!.preemptionPolicy; + return this.jsonData.preemptionPolicy; } } diff --git a/frontend/src/lib/k8s/replicaSet.ts b/frontend/src/lib/k8s/replicaSet.ts index 2e4b80adab..a20fc2b524 100644 --- a/frontend/src/lib/k8s/replicaSet.ts +++ b/frontend/src/lib/k8s/replicaSet.ts @@ -1,12 +1,6 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { - KubeCondition, - KubeContainer, - KubeMetadata, - KubeObjectInterface, - LabelSelector, - makeKubeObject, -} from './cluster'; +import { KubeCondition, KubeContainer, LabelSelector } from './cluster'; +import { KubeMetadata } from './KubeMetadata'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; import { KubePodSpec } from './pod'; export interface KubeReplicaSet extends KubeObjectInterface { @@ -30,15 +24,18 @@ export interface KubeReplicaSet extends KubeObjectInterface { }; } -class ReplicaSet extends makeKubeObject('ReplicaSet') { - static apiEndpoint = apiFactoryWithNamespace('apps', 'v1', 'replicasets', true); +class ReplicaSet extends KubeObject { + static kind = 'ReplicaSet'; + static apiName = 'replicasets'; + static apiVersion = 'apps/v1'; + static isNamespaced = true; get spec(): KubeReplicaSet['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): KubeReplicaSet['status'] { - return this.jsonData!.status; + return this.jsonData.status; } getContainers(): KubeContainer[] { diff --git a/frontend/src/lib/k8s/resourceQuota.ts b/frontend/src/lib/k8s/resourceQuota.ts index 2bd2b22bcf..553f88fe88 100644 --- a/frontend/src/lib/k8s/resourceQuota.ts +++ b/frontend/src/lib/k8s/resourceQuota.ts @@ -1,6 +1,5 @@ import { normalizeUnit } from '../util'; -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; interface spec { hard: { @@ -30,15 +29,18 @@ export interface KubeResourceQuota extends KubeObjectInterface { status: status; } -class ResourceQuota extends makeKubeObject('resourceQuota') { - static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'resourcequotas'); +class ResourceQuota extends KubeObject { + static kind = 'ResourceQuota'; + static apiName = 'resourcequotas'; + static apiVersion = 'v1'; + static isNamespaced = true; get spec(): spec { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): status { - return this.jsonData!.status; + return this.jsonData.status; } get requests(): string[] { diff --git a/frontend/src/lib/k8s/role.ts b/frontend/src/lib/k8s/role.ts index 50deb3d3fa..1b187068cb 100644 --- a/frontend/src/lib/k8s/role.ts +++ b/frontend/src/lib/k8s/role.ts @@ -1,5 +1,4 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeRole extends KubeObjectInterface { rules: { @@ -8,14 +7,17 @@ export interface KubeRole extends KubeObjectInterface { resourceNames: string[]; resources: string[]; verbs: string[]; - }; + }[]; } -class Role extends makeKubeObject('role') { - static apiEndpoint = apiFactoryWithNamespace('rbac.authorization.k8s.io', 'v1', 'roles'); +class Role extends KubeObject { + static kind = 'Role'; + static apiName = 'roles'; + static apiVersion = 'rbac.authorization.k8s.io/v1'; + static isNamespaced = true; get rules() { - return this.jsonData!.rules; + return this.jsonData.rules; } } diff --git a/frontend/src/lib/k8s/roleBinding.ts b/frontend/src/lib/k8s/roleBinding.ts index 87f927fd4a..87feef388f 100644 --- a/frontend/src/lib/k8s/roleBinding.ts +++ b/frontend/src/lib/k8s/roleBinding.ts @@ -1,5 +1,4 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeRoleBinding extends KubeObjectInterface { roleRef: { @@ -15,15 +14,18 @@ export interface KubeRoleBinding extends KubeObjectInterface { }[]; } -class RoleBinding extends makeKubeObject('roleBinding') { - static apiEndpoint = apiFactoryWithNamespace('rbac.authorization.k8s.io', 'v1', 'rolebindings'); +class RoleBinding extends KubeObject { + static kind = 'RoleBinding'; + static apiName = 'rolebindings'; + static apiVersion = 'rbac.authorization.k8s.io/v1'; + static isNamespaced = true; get roleRef() { - return this.jsonData!.roleRef; + return this.jsonData.roleRef; } get subjects(): KubeRoleBinding['subjects'] { - return this.jsonData!.subjects; + return this.jsonData.subjects; } } diff --git a/frontend/src/lib/k8s/runtime.ts b/frontend/src/lib/k8s/runtime.ts index 3c821fe770..9d97aeec25 100644 --- a/frontend/src/lib/k8s/runtime.ts +++ b/frontend/src/lib/k8s/runtime.ts @@ -1,22 +1,18 @@ -import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeRuntimeClass extends KubeObjectInterface { handler: string; + overhead?: any; + scheduling?: any; } -export class RuntimeClass extends makeKubeObject('RuntimeClass') { - static apiEndpoint = apiFactory('node.k8s.io', 'v1', 'runtimeclasses'); +export class RuntimeClass extends KubeObject { + static kind = 'RuntimeClass'; + static apiName = 'runtimeclasses'; + static apiVersion = 'node.k8s.io/v1'; + static isNamespaced = false; get spec() { - return this.jsonData!.spec; - } - - static get pluralName() { - return 'runtimeclasses'; - } - - static get listRoute() { - return this.pluralName; + return this.jsonData.spec; } } diff --git a/frontend/src/lib/k8s/secret.ts b/frontend/src/lib/k8s/secret.ts index 6cac08ea22..628b225599 100644 --- a/frontend/src/lib/k8s/secret.ts +++ b/frontend/src/lib/k8s/secret.ts @@ -1,20 +1,22 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject, StringDict } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeSecret extends KubeObjectInterface { - data: StringDict; + data: Record; type: string; } -class Secret extends makeKubeObject('secret') { - static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'secrets'); +class Secret extends KubeObject { + static kind = 'Secret'; + static apiName = 'secrets'; + static apiVersion = 'v1'; + static isNamespaced = true; get data() { - return this.jsonData!.data; + return this.jsonData.data; } get type() { - return this.jsonData!.type; + return this.jsonData.type; } } diff --git a/frontend/src/lib/k8s/service.ts b/frontend/src/lib/k8s/service.ts index 89d474cfa0..ac46d152db 100644 --- a/frontend/src/lib/k8s/service.ts +++ b/frontend/src/lib/k8s/service.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeCondition, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeCondition } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubePortStatus { error?: string; @@ -39,15 +39,18 @@ export interface KubeService extends KubeObjectInterface { }; } -class Service extends makeKubeObject('service') { - static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'services'); +class Service extends KubeObject { + static kind = 'Service'; + static apiName = 'services'; + static apiVersion = 'v1'; + static isNamespaced = true; get spec(): KubeService['spec'] { - return this.jsonData!.spec; + return this.jsonData.spec; } get status() { - return this.jsonData!.status; + return this.jsonData.status; } getExternalAddresses() { diff --git a/frontend/src/lib/k8s/serviceAccount.ts b/frontend/src/lib/k8s/serviceAccount.ts index fcfccf8969..f0623fd6b7 100644 --- a/frontend/src/lib/k8s/serviceAccount.ts +++ b/frontend/src/lib/k8s/serviceAccount.ts @@ -1,5 +1,4 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeServiceAccount extends KubeObjectInterface { secrets: { @@ -12,11 +11,14 @@ export interface KubeServiceAccount extends KubeObjectInterface { }[]; } -class ServiceAccount extends makeKubeObject('serviceAccount') { - static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'serviceaccounts'); +class ServiceAccount extends KubeObject { + static kind = 'ServiceAccount'; + static apiName = 'serviceaccounts'; + static apiVersion = 'v1'; + static isNamespaced = true; get secrets(): KubeServiceAccount['secrets'] { - return this.jsonData!.secrets; + return this.jsonData.secrets; } } diff --git a/frontend/src/lib/k8s/statefulSet.ts b/frontend/src/lib/k8s/statefulSet.ts index 926b2227d3..5859c7215b 100644 --- a/frontend/src/lib/k8s/statefulSet.ts +++ b/frontend/src/lib/k8s/statefulSet.ts @@ -1,11 +1,6 @@ -import { apiFactoryWithNamespace } from './apiProxy'; -import { - KubeContainer, - KubeMetadata, - KubeObjectInterface, - LabelSelector, - makeKubeObject, -} from './cluster'; +import { KubeContainer, LabelSelector } from './cluster'; +import { KubeMetadata } from './KubeMetadata'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; import { KubePodSpec } from './pod'; export interface KubeStatefulSet extends KubeObjectInterface { @@ -28,15 +23,18 @@ export interface KubeStatefulSet extends KubeObjectInterface { }; } -class StatefulSet extends makeKubeObject('StatefulSet') { - static apiEndpoint = apiFactoryWithNamespace('apps', 'v1', 'statefulsets', true); +class StatefulSet extends KubeObject { + static kind = 'StatefulSet'; + static apiName = 'statefulsets'; + static apiVersion = 'apps/v1'; + static isNamespaced = true; get spec() { - return this.jsonData!.spec; + return this.jsonData.spec; } get status() { - return this.jsonData!.status; + return this.jsonData.status; } getContainers(): KubeContainer[] { diff --git a/frontend/src/lib/k8s/storageClass.ts b/frontend/src/lib/k8s/storageClass.ts index 6f643f5cba..a13cb6c7ba 100644 --- a/frontend/src/lib/k8s/storageClass.ts +++ b/frontend/src/lib/k8s/storageClass.ts @@ -1,25 +1,32 @@ -import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; export interface KubeStorageClass extends KubeObjectInterface { provisioner: string; reclaimPolicy: string; volumeBindingMode: string; + allowVolumeExpansion?: boolean; } -class StorageClass extends makeKubeObject('storageClass') { - static apiEndpoint = apiFactory('storage.k8s.io', 'v1', 'storageclasses'); +class StorageClass extends KubeObject { + static kind = 'StorageClass'; + static apiName = 'storageclasses'; + static apiVersion = 'storage.k8s.io/v1'; + static isNamespaced = false; get provisioner() { - return this.jsonData?.provisioner; + return this.jsonData.provisioner; } get reclaimPolicy() { - return this.jsonData?.reclaimPolicy; + return this.jsonData.reclaimPolicy; } get volumeBindingMode() { - return this.jsonData?.volumeBindingMode; + return this.jsonData.volumeBindingMode; + } + + get allowVolumeExpansion() { + return this.jsonData.allowVolumeExpansion; } static get listRoute() { diff --git a/frontend/src/lib/k8s/token.ts b/frontend/src/lib/k8s/token.ts index 788c75a518..26473b02d9 100644 --- a/frontend/src/lib/k8s/token.ts +++ b/frontend/src/lib/k8s/token.ts @@ -1,4 +1,4 @@ -import { KubeObjectInterface } from './cluster'; +import { KubeObjectInterface } from './KubeObject'; export interface KubeToken extends KubeObjectInterface { status: { diff --git a/frontend/src/lib/k8s/validatingWebhookConfiguration.ts b/frontend/src/lib/k8s/validatingWebhookConfiguration.ts index 576f0da99e..fc97a1dbd3 100644 --- a/frontend/src/lib/k8s/validatingWebhookConfiguration.ts +++ b/frontend/src/lib/k8s/validatingWebhookConfiguration.ts @@ -1,5 +1,5 @@ -import { apiFactory } from './apiProxy'; -import { KubeObjectInterface, LabelSelector, makeKubeObject } from './cluster'; +import { LabelSelector } from './cluster'; +import { KubeObject, KubeObjectInterface } from './KubeObject'; import { KubeRuleWithOperations, KubeWebhookClientConfig } from './mutatingWebhookConfiguration'; export interface KubeValidatingWebhookConfiguration extends KubeObjectInterface { @@ -23,17 +23,14 @@ export interface KubeValidatingWebhookConfiguration extends KubeObjectInterface }[]; } -class ValidatingWebhookConfiguration extends makeKubeObject( - 'ValidatingWebhookConfiguration' -) { - static apiEndpoint = apiFactory( - 'admissionregistration.k8s.io', - 'v1', - 'validatingwebhookconfigurations' - ); +class ValidatingWebhookConfiguration extends KubeObject { + static kind = 'ValidatingWebhookConfiguration'; + static apiName = 'validatingwebhookconfigurations'; + static apiVersion = 'admissionregistration.k8s.io/v1'; + static isNamespaced = false; get webhooks(): KubeValidatingWebhookConfiguration['webhooks'] { - return this.jsonData!.webhooks; + return this.jsonData.webhooks; } } diff --git a/frontend/src/lib/k8s/vpa.ts b/frontend/src/lib/k8s/vpa.ts index 2f3a619849..239c23e8d6 100644 --- a/frontend/src/lib/k8s/vpa.ts +++ b/frontend/src/lib/k8s/vpa.ts @@ -1,7 +1,8 @@ import { ResourceClasses } from '.'; -import { apiFactoryWithNamespace } from './apiProxy'; import { request } from './apiProxy'; -import { KubeObject, KubeObjectInterface, makeKubeObject } from './cluster'; +import { KubeObject } from './KubeObject'; +import { KubeObjectInterface } from './KubeObject'; +import { KubeObjectClass } from './KubeObject'; type ResourceName = 'cpu' | 'memory' | 'storage' | 'ephemeral-storage'; @@ -76,12 +77,11 @@ export interface KubeVPA extends KubeObjectInterface { status: VpaStatus; } -class VPA extends makeKubeObject('verticalPodAutoscaler') { - static apiEndpoint = apiFactoryWithNamespace( - 'autoscaling.k8s.io', - 'v1', - 'verticalpodautoscalers' - ); +class VPA extends KubeObject { + static kind = 'VerticalPodAutoscaler'; + static apiName = 'verticalpodautoscalers'; + static apiVersion = 'autoscaling.k8s.io/v1'; + static isNamespaced = true; static async isEnabled(): Promise { let res; @@ -102,11 +102,11 @@ class VPA extends makeKubeObject('verticalPodAutoscaler') { } get spec(): VpaSpec { - return this.jsonData!.spec; + return this.jsonData.spec; } get status(): VpaStatus { - return this.jsonData!.status; + return this.jsonData.status; } get referenceObject(): KubeObject | null { @@ -115,7 +115,7 @@ class VPA extends makeKubeObject('verticalPodAutoscaler') { return null; } - const TargetObjectClass = ResourceClasses[target.kind]; + const TargetObjectClass = (ResourceClasses as Record)[target.kind]; let objInstance: KubeObject | null = null; if (!!TargetObjectClass) { objInstance = new TargetObjectClass({ @@ -124,7 +124,7 @@ class VPA extends makeKubeObject('verticalPodAutoscaler') { namespace: target.namespace || this.metadata.namespace, }, kind: target.kind, - }); + } as KubeObjectInterface); } return objInstance; } diff --git a/frontend/src/lib/router.tsx b/frontend/src/lib/router.tsx index 828f705a87..bc2b98ba3d 100644 --- a/frontend/src/lib/router.tsx +++ b/frontend/src/lib/router.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { generatePath, useHistory } from 'react-router'; import NotFoundComponent from '../components/404'; import AuthToken from '../components/account/Auth'; @@ -114,7 +114,7 @@ export interface Route { /** The sidebar entry this Route should enable, or null if it shouldn't enable any. If an object is passed with item and sidebar, it will try to enable the given sidebar and the given item. */ sidebar: string | null | { item: string | null; sidebar: string | DefaultSidebars }; /** Shown component for this route. */ - component: () => JSX.Element; + component: () => ReactNode; /** Hide the appbar at the top. */ hideAppBar?: boolean; /** Whether the route should be disabled (not registered). */ diff --git a/frontend/src/lib/util.ts b/frontend/src/lib/util.ts index 0b30bb3bff..35ab634bd6 100644 --- a/frontend/src/lib/util.ts +++ b/frontend/src/lib/util.ts @@ -7,9 +7,11 @@ import { useTypedSelector } from '../redux/reducers/reducers'; import store from '../redux/stores/store'; import { getCluster, getClusterPrefixedPath } from './cluster'; import { ApiError } from './k8s/apiProxy'; -import { KubeMetrics, KubeObjectInterface, Workload } from './k8s/cluster'; +import { KubeMetrics } from './k8s/cluster'; import { KubeEvent } from './k8s/event'; +import { KubeObjectInterface } from './k8s/KubeObject'; import Node from './k8s/node'; +import { Workload } from './k8s/Workload'; import { parseCpu, parseRam, unparseCpu, unparseRam } from './units'; // Exported to keep compatibility for plugins that may have used them. @@ -191,8 +193,7 @@ export function useErrorState(dependentSetter?: (...args: any) => void) { [error] ); - // Adding "as any" here because it was getting difficult to validate the setter type. - return [error, setError as any]; + return [error, setError] as const; } /** diff --git a/frontend/src/plugin/__snapshots__/pluginLib.snapshot b/frontend/src/plugin/__snapshots__/pluginLib.snapshot index c7532c9c08..c286d20f07 100644 --- a/frontend/src/plugin/__snapshots__/pluginLib.snapshot +++ b/frontend/src/plugin/__snapshots__/pluginLib.snapshot @@ -252,6 +252,7 @@ "DaemonSet": [Function], "Deployment": [Function], "Endpoint": [Function], + "Endpoints": [Function], "HorizontalPodAutoscaler": [Function], "Ingress": [Function], "IngressClass": [Function], @@ -279,6 +280,7 @@ }, "cluster": { "HEADLAMP_ALLOWED_NAMESPACES": "headlamp.allowed-namespaces", + "KubeObject": [Function], "getAllowedNamespaces": [Function], "makeKubeObject": [Function], }, diff --git a/frontend/src/plugin/registry.tsx b/frontend/src/plugin/registry.tsx index b57d93f3c1..82729a794a 100644 --- a/frontend/src/plugin/registry.tsx +++ b/frontend/src/plugin/registry.tsx @@ -1,5 +1,5 @@ import { has } from 'lodash'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { AppLogoProps, AppLogoType } from '../components/App/AppLogo'; import { PluginManager } from '../components/App/pluginManager'; import { runCommand } from '../components/App/runCommand'; @@ -14,13 +14,13 @@ import { DetailsViewSectionProps, DetailsViewSectionType } from '../components/D import { addDetailsViewSectionsProcessor, DefaultDetailsViewSection, - DetailsViewSectionsProcessor, + DetailsViewsSectionProcessor, setDetailsViewSection, } from '../components/DetailsViewSection/detailsViewSectionSlice'; import { DefaultSidebars, SidebarEntryProps } from '../components/Sidebar'; import { setSidebarItem, setSidebarItemFilter } from '../components/Sidebar/sidebarSlice'; import { getHeadlampAPIHeaders } from '../helpers'; -import { KubeObject } from '../lib/k8s/cluster'; +import { KubeObject } from '../lib/k8s/KubeObject'; import { Route } from '../lib/router'; import { addDetailsViewHeaderActionsProcessor, @@ -67,7 +67,7 @@ import { export interface SectionFuncProps { title: string; - component: (props: { resource: any }) => JSX.Element | null; + component: (props: { resource: any }) => ReactNode; } export type { @@ -161,7 +161,7 @@ export default class Registry { /** * @deprecated Registry.registerAppBarAction is deprecated. Please use registerAppBarAction. */ - registerAppBarAction(actionName: string, actionFunc: (...args: any[]) => JSX.Element | null) { + registerAppBarAction(actionName: string, actionFunc: (...args: any[]) => ReactNode) { console.warn('Registry.registerAppBarAction is deprecated. Please use registerAppBarAction.'); return registerAppBarAction(actionFunc); } @@ -185,7 +185,7 @@ export default class Registry { */ registerDetailsViewSection( sectionName: string, - sectionFunc: (props: { resource: any }) => SectionFuncProps | null + sectionFunc: (resource: KubeObject) => SectionFuncProps | null ) { console.warn( 'Registry.registerDetailsViewSection is deprecated. Please use registerDetailsViewSection.' @@ -520,7 +520,7 @@ export function registerDetailsViewSection(viewSection: DetailsViewSectionType) * ``` */ export function registerDetailsViewSectionsProcessor( - processor: DetailsViewSectionsProcessor | DetailsViewSectionsProcessor['processor'] + processor: DetailsViewsSectionProcessor | DetailsViewsSectionProcessor['processor'] ) { store.dispatch(addDetailsViewSectionsProcessor(processor)); } diff --git a/frontend/src/redux/actionButtonsSlice.ts b/frontend/src/redux/actionButtonsSlice.ts index 6605029023..8a1258df75 100644 --- a/frontend/src/redux/actionButtonsSlice.ts +++ b/frontend/src/redux/actionButtonsSlice.ts @@ -1,20 +1,12 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { get, set } from 'lodash'; import { ReactElement, ReactNode } from 'react'; -import { KubeObject } from '../lib/k8s/cluster'; +import { KubeObject } from '../lib/k8s/KubeObject'; -export type HeaderActionType = - | ((...args: any[]) => JSX.Element | null | ReactNode) - | null - | ReactElement - | ReactNode; +export type HeaderActionType = ((...args: any[]) => ReactNode) | null | ReactElement | ReactNode; export type DetailsViewFunc = HeaderActionType; -export type AppBarActionType = - | ((...args: any[]) => JSX.Element | null | ReactNode) - | null - | ReactElement - | ReactNode; +export type AppBarActionType = ((...args: any[]) => ReactNode) | null | ReactElement | ReactNode; export type HeaderAction = { id: string; diff --git a/frontend/src/redux/filterSlice.ts b/frontend/src/redux/filterSlice.ts index 4497054de6..abd6b05655 100644 --- a/frontend/src/redux/filterSlice.ts +++ b/frontend/src/redux/filterSlice.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { JSONPath } from 'jsonpath-plus'; -import { KubeObjectInterface } from '../lib/k8s/cluster'; import { KubeEvent } from '../lib/k8s/event'; +import { KubeObjectInterface } from '../lib/k8s/KubeObject'; export interface FilterState { /** The namespaces to filter on. */ diff --git a/frontend/src/redux/headlampEventSlice.ts b/frontend/src/redux/headlampEventSlice.ts index 938f71f394..a8b703bd20 100644 --- a/frontend/src/redux/headlampEventSlice.ts +++ b/frontend/src/redux/headlampEventSlice.ts @@ -5,8 +5,8 @@ import { PayloadAction, } from '@reduxjs/toolkit'; import { useDispatch } from 'react-redux'; -import { KubeObject } from '../lib/k8s/cluster'; import Event from '../lib/k8s/event'; +import { KubeObject } from '../lib/k8s/KubeObject'; import Pod from '../lib/k8s/pod'; import { Plugin } from '../plugin/lib'; import { RootState } from './reducers/reducers'; diff --git a/frontend/src/test/index.tsx b/frontend/src/test/index.tsx index 0971283bde..7c11b2c80d 100644 --- a/frontend/src/test/index.tsx +++ b/frontend/src/test/index.tsx @@ -2,7 +2,6 @@ import { configureStore } from '@reduxjs/toolkit'; import { PropsWithChildren } from 'react'; import { Provider } from 'react-redux'; import { MemoryRouter, Route } from 'react-router-dom'; -import { KubeObject } from '../lib/k8s/cluster'; import defaultStore from '../redux/stores/store'; export type TestContextProps = PropsWithChildren<{ @@ -44,12 +43,10 @@ export function TestContext(props: TestContextProps) { ); } -export function overrideKubeObject( - kubeObject: KubeObject, - propsToOverride: { [method: keyof KubeObject]: KubeObject[keyof KubeObject] | undefined } -) { +export function overrideKubeObject(kubeObject: U, propsToOverride: Partial) { for (const [key, value] of Object.entries(propsToOverride)) { if (value !== undefined) { + // @ts-ignore kubeObject[key] = value; } } diff --git a/frontend/src/test/mocker.ts b/frontend/src/test/mocker.ts index 659763bc84..0ffa33968e 100644 --- a/frontend/src/test/mocker.ts +++ b/frontend/src/test/mocker.ts @@ -1,23 +1,37 @@ import _ from 'lodash'; -import { KubeObjectIface, KubeObjectInterface } from '../lib/k8s/cluster'; +import { KubeMetadata } from '../lib/k8s/KubeMetadata'; +import { KubeObject } from '../lib/k8s/KubeObject'; +import { KubeObjectInterface } from '../lib/k8s/KubeObject'; +import { KubeObjectClass } from '../lib/k8s/KubeObject'; -interface K8sResourceListGeneratorOptions { +interface K8sResourceListGeneratorOptions { numResults?: number; - instantiateAs?: KubeObjectIface; uidPrefix?: string; + instantiateAs?: T; } -export function generateK8sResourceList( - baseJson: Omit, - options: K8sResourceListGeneratorOptions = {} -) { +export function generateK8sResourceList< + C extends typeof KubeObject, + T extends KubeObjectInterface +>( + baseJson: Partial, + options?: { numResults?: number; instantiateAs: C; uidPrefix?: string } +): InstanceType[]; +export function generateK8sResourceList( + baseJson: Partial, + options?: { numResults?: number; uidPrefix?: string } +): T[]; +export function generateK8sResourceList< + T extends KubeObjectInterface, + C extends typeof KubeObject +>(baseJson: Partial, options: K8sResourceListGeneratorOptions = {}) { const { numResults = 5, instantiateAs } = options; const list = []; for (let i = 0; i < numResults; i++) { const json = { metadata: { name: '', - }, + } as KubeMetadata, ..._.cloneDeep(baseJson), } as T; @@ -50,3 +64,17 @@ export function generateK8sResourceList) => + new KubeObject({ + apiVersion: 'v1', + kind: 'Pod', + metadata: { + name: 'my-pod', + namespace: 'default', + uid: 'abcde', + creationTimestamp: new Date('2020-01-01').toISOString(), + ...partial.metadata, + }, + ...partial, + });