(
+ const objList = generateK8sResourceList(
{
apiVersion: 'v1',
kind: 'ResourceQuota',
diff --git a/frontend/src/components/runtimeClass/List.stories.tsx b/frontend/src/components/runtimeClass/List.stories.tsx
index c3bdba0be13..e46057cb620 100644
--- a/frontend/src/components/runtimeClass/List.stories.tsx
+++ b/frontend/src/components/runtimeClass/List.stories.tsx
@@ -1,12 +1,11 @@
import { Meta, StoryFn } from '@storybook/react';
-import { KubeObject } from '../../lib/k8s/cluster';
import { RuntimeClass } from '../../lib/k8s/runtime';
import { TestContext } from '../../test';
import { RuntimeClassList } from './List';
import { RUNTIME_CLASS_DUMMY_DATA } from './storyHelper';
RuntimeClass.useList = () => {
- const objList = RUNTIME_CLASS_DUMMY_DATA.map((data: KubeObject) => new RuntimeClass(data));
+ const objList = RUNTIME_CLASS_DUMMY_DATA.map(data => new RuntimeClass(data));
return [objList, null, () => {}, () => {}] as any;
};
diff --git a/frontend/src/components/secret/List.stories.tsx b/frontend/src/components/secret/List.stories.tsx
index c93ae63a3fe..c1f83e84b0c 100644
--- a/frontend/src/components/secret/List.stories.tsx
+++ b/frontend/src/components/secret/List.stories.tsx
@@ -1,12 +1,11 @@
import { Meta, StoryFn } from '@storybook/react';
-import { KubeObject } from '../../lib/k8s/cluster';
import Secret from '../../lib/k8s/secret';
import { TestContext } from '../../test';
import ListView from './List';
import { BASE_EMPTY_SECRET, BASE_SECRET } from './storyHelper';
Secret.useList = () => {
- const objList = [BASE_EMPTY_SECRET, BASE_SECRET].map((data: KubeObject) => new Secret(data));
+ const objList = [BASE_EMPTY_SECRET, BASE_SECRET].map(data => new Secret(data));
return [objList, null, () => {}, () => {}] as any;
};
diff --git a/frontend/src/components/service/Details.tsx b/frontend/src/components/service/Details.tsx
index 2c260aa8ad1..ea568c7c328 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 f89f65716da..5b1ebf4c760 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 46ae649b291..5dc95c8b092 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 d4552414b25..86512f2e779 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.stories.tsx b/frontend/src/components/storage/ClassList.stories.tsx
index f084870815a..1dcf2d1c544 100644
--- a/frontend/src/components/storage/ClassList.stories.tsx
+++ b/frontend/src/components/storage/ClassList.stories.tsx
@@ -1,12 +1,11 @@
import { Meta, Story } from '@storybook/react';
-import { KubeObject } from '../../lib/k8s/cluster';
import StorageClass from '../../lib/k8s/storageClass';
import { TestContext } from '../../test';
import ListView from './ClassList';
import { BASE_SC } from './storyHelper';
StorageClass.useList = () => {
- const objList = [BASE_SC].map((data: KubeObject) => new StorageClass(data));
+ const objList = [BASE_SC].map(data => new StorageClass(data));
return [objList, null, () => {}, () => {}] as any;
};
diff --git a/frontend/src/components/storage/ClassList.tsx b/frontend/src/components/storage/ClassList.tsx
index d9c07df2ce2..d12a9301214 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/VolumeList.stories.tsx b/frontend/src/components/storage/VolumeList.stories.tsx
index 58b15a53a1b..7e454dbd31f 100644
--- a/frontend/src/components/storage/VolumeList.stories.tsx
+++ b/frontend/src/components/storage/VolumeList.stories.tsx
@@ -1,12 +1,11 @@
import { Meta, Story } from '@storybook/react';
-import { KubeObject } from '../../lib/k8s/cluster';
import PersistentVolume from '../../lib/k8s/persistentVolume';
import { TestContext } from '../../test';
import ListView from './ClassList';
import { BASE_PV } from './storyHelper';
PersistentVolume.useList = () => {
- const objList = [BASE_PV].map((data: KubeObject) => new PersistentVolume(data));
+ const objList = [BASE_PV].map(data => new PersistentVolume(data));
return [objList, null, () => {}, () => {}] as any;
};
diff --git a/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot b/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot
index 743b2029abe..fd827b4150f 100644
--- a/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot
+++ b/frontend/src/components/storage/__snapshots__/ClassList.Items.stories.storyshot
@@ -667,7 +667,9 @@
|
+ >
+ true
+
|
+ >
+ true
+
Promise.resolve(true);
VPA.useList = () => {
- const objList = generateK8sResourceList(
+ const objList = generateK8sResourceList(
{
apiVersion: 'autoscaling.k8s.io/v1',
kind: 'VerticalPodAutoscaler',
diff --git a/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx b/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx
index e618b9fab92..519595866a4 100644
--- a/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx
+++ b/frontend/src/components/webhookconfiguration/MutatingWebhookConfigDetails.stories.tsx
@@ -1,10 +1,10 @@
import { Meta, Story } from '@storybook/react';
-import MWC, { KubeMutatingWebhookConfiguration } from '../../lib/k8s/mutatingWebhookConfiguration';
+import MWC from '../../lib/k8s/mutatingWebhookConfiguration';
import { TestContext } from '../../test';
import MutatingWebhookConfigDetails from './MutatingWebhookConfigDetails';
import { createMWC } from './storyHelper';
-const usePhonyGet: KubeMutatingWebhookConfiguration['useGet'] = (withService: boolean) => {
+const usePhonyGet = (withService: boolean) => {
return [new MWC(createMWC(withService)), null, () => {}, () => {}] as any;
};
diff --git a/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx b/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx
index da6a6bbba5b..6457c49aa20 100644
--- a/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx
+++ b/frontend/src/components/webhookconfiguration/ValidatingWebhookConfigDetails.stories.tsx
@@ -1,12 +1,10 @@
import { Meta, Story } from '@storybook/react';
-import VWC, {
- KubeValidatingWebhookConfiguration,
-} from '../../lib/k8s/validatingWebhookConfiguration';
+import VWC from '../../lib/k8s/validatingWebhookConfiguration';
import { TestContext } from '../../test';
import { createVWC } from './storyHelper';
import ValidatingWebhookConfigDetails from './ValidatingWebhookConfigDetails';
-const usePhonyGet: KubeValidatingWebhookConfiguration['useGet'] = (withService: boolean) => {
+const usePhonyGet = (withService: boolean) => {
return [new VWC(createVWC(withService)), null, () => {}, () => {}] as any;
};
diff --git a/frontend/src/components/workload/Details.tsx b/frontend/src/components/workload/Details.tsx
index 103a7781dd1..2ebe656ebce 100644
--- a/frontend/src/components/workload/Details.tsx
+++ b/frontend/src/components/workload/Details.tsx
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
-import { KubeObject, Workload } from '../../lib/k8s/cluster';
+import { Workload, WorkloadClass } from '../../lib/k8s/cluster';
import {
ConditionsSection,
ContainersSection,
@@ -9,11 +9,11 @@ import {
OwnedPodsSection,
} from '../common/Resource';
-interface WorkloadDetailsProps {
- workloadKind: KubeObject;
+interface WorkloadDetailsProps {
+ 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 84bcefd1c7e..d045b124a53 100644
--- a/frontend/src/components/workload/Overview.tsx
+++ b/frontend/src/components/workload/Overview.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { useCluster } from '../../lib/k8s';
import { ApiError } from '../../lib/k8s/apiProxy';
-import { KubeObject, Workload } from '../../lib/k8s/cluster';
+import { Workload, WorkloadClass } from '../../lib/k8s/cluster';
import CronJob from '../../lib/k8s/cronJob';
import DaemonSet from '../../lib/k8s/daemonSet';
import Deployment from '../../lib/k8s/deployment';
@@ -72,7 +72,7 @@ export default function Overview() {
return joint;
}
- const workloads: KubeObject[] = [
+ const workloads: WorkloadClass[] = [
Pod,
Deployment,
StatefulSet,
@@ -82,9 +82,9 @@ export default function Overview() {
CronJob,
];
- workloads.forEach((workloadClass: KubeObject) => {
+ workloads.forEach(workloadClass => {
workloadClass.useApiList(
- (items: InstanceType[]) => {
+ (items: Workload[]) => {
setWorkloads({ [workloadClass.className]: items });
},
(err: ApiError) => {
@@ -94,7 +94,7 @@ export default function Overview() {
);
});
- function ChartLink(workload: KubeObject) {
+ function ChartLink(workload: WorkloadClass) {
const linkName = workload.pluralName;
return {linkName};
}
@@ -103,7 +103,7 @@ export default function Overview() {
- {workloads.map(workload => (
+ {workloads.map((workload: WorkloadClass) => (
item.metadata.name,
- render: item => (
-
- ),
+ render: item => ,
},
'namespace',
{
diff --git a/frontend/src/lib/k8s/KubeObject.ts b/frontend/src/lib/k8s/KubeObject.ts
new file mode 100644
index 00000000000..1532a5b6b9f
--- /dev/null
+++ b/frontend/src/lib/k8s/KubeObject.ts
@@ -0,0 +1,574 @@
+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, useErrorState } from '../util';
+import { useCluster, useConnectApi } from '.';
+import { ApiError, apiFactory, apiFactoryWithNamespace, post, QueryParameters } from './apiProxy';
+import {
+ ApiListOptions,
+ ApiListSingleNamespaceOptions,
+ AuthRequestResourceAttrs,
+ KubeMetadata,
+ KubeObjectClass,
+ KubeObjectInterface,
+} from './cluster';
+import { KubeEvent } from './event';
+
+function getAllowedNamespaces() {
+ const cluster = getCluster();
+ if (!cluster) {
+ return [];
+ }
+
+ const clusterSettings = helpers.loadClusterSettings(cluster);
+ return clusterSettings.allowedNamespaces || [];
+}
+
+export class KubeObject {
+ static apiEndpoint: ReturnType;
+ static readOnlyFields: string[] = [];
+ static objectName: string;
+
+ jsonData: T;
+ readonly _clusterName: string;
+
+ constructor(json: T) {
+ this.jsonData = json;
+ this._clusterName = getCluster() || '';
+ }
+
+ static get className(): string {
+ return this.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 as Record)![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({
+ 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: U,
+ onList: (arg: InstanceType[]) => void,
+ onError?: (err: ApiError) => 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: U,
+ onList: (...arg: any[]) => any,
+ onError?: (err: ApiError) => void,
+ opts?: ApiListOptions
+ ) {
+ const [objs, setObjs] = React.useState<{ [key: string]: U[] }>({});
+ const listCallback = onList as (arg: any[]) => 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?.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,
+ opts?: ApiListOptions
+ ): [
+ InstanceType[] | null,
+ ApiError | null,
+ (items: InstanceType[]) => void,
+ (err: ApiError | null) => void
+ ] {
+ const [objList, setObjList] = React.useState[] | null>(null);
+ const [error, setError] = useErrorState(setObjList);
+ const currentCluster = useCluster();
+ const cluster = opts?.cluster || currentCluster;
+
+ // Reset the list and error when the cluster changes.
+ React.useEffect(() => {
+ setObjList(null);
+ setError(null);
+ }, [cluster]);
+
+ function setList(items: InstanceType[] | null) {
+ setObjList(items);
+ if (items !== null) {
+ setError(null);
+ }
+ }
+
+ this.useApiList(setList, setError, opts);
+
+ // Return getters and then the setters as the getters are more likely to be used with
+ // this function.
+ return [objList, error, setObjList, setError];
+ }
+
+ static create>(this: T, item: ConstructorParameters[0]) {
+ return new this(item) as InstanceType;
+ }
+
+ static apiGet(
+ this: U,
+ onGet: (...args: any) => void,
+ name: string,
+ namespace?: string,
+ onError?: (err: ApiError | null) => 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: U,
+ onGet: (item: InstanceType | null) => any,
+ name: string,
+ namespace?: string,
+ onError?: (err: ApiError | null) => 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));
+ }
+
+ static useGet(
+ this: U,
+ name: string,
+ namespace?: string,
+ opts?: {
+ queryParams?: QueryParameters;
+ cluster?: string;
+ }
+ ): [
+ InstanceType | null,
+ ApiError | null,
+ (items: InstanceType) => void,
+ (err: ApiError | null) => void
+ ] {
+ const [obj, setObj] = React.useState | null>(null);
+ const [error, setError] = useErrorState(setObj);
+
+ function onGet(item: InstanceType | null) {
+ // Only set the object if we have we have a different one.
+ if (!!obj && !!item && obj.metadata.resourceVersion === item.metadata.resourceVersion) {
+ return;
+ }
+
+ setObj(item);
+ if (item !== null) {
+ setError(null);
+ }
+ }
+
+ function onError(err: ApiError | null) {
+ if (
+ error === err ||
+ (!!error && !!err && error.message === err.message && error.status === err.status)
+ ) {
+ return;
+ }
+
+ setError(err);
+ }
+
+ this.useApiGet(onGet, name, namespace, onError, opts);
+
+ // Return getters and then the setters as the getters are more likely to be used with
+ // this function.
+ return [obj, error, setObj, setError];
+ }
+
+ _class() {
+ return this.constructor as KubeObjectClass;
+ }
+
+ delete() {
+ const args: string[] = [this.getName()];
+ if (this.isNamespaced) {
+ args.unshift(this.getNamespace()!);
+ }
+
+ 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: Parameters = [body];
+
+ if (this.isNamespaced) {
+ args.push(this.getNamespace());
+ }
+
+ args.push(this.getName());
+ 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';
+ }
+ }
+}
diff --git a/frontend/src/lib/k8s/cluster.ts b/frontend/src/lib/k8s/cluster.ts
index 5c776a92004..fb7e5607693 100644
--- a/frontend/src/lib/k8s/cluster.ts
+++ b/frontend/src/lib/k8s/cluster.ts
@@ -1,32 +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, useErrorState } from '../util';
-import { useCluster, useConnectApi } from '.';
-import { ApiError, apiFactory, apiFactoryWithNamespace, post, QueryParameters } from './apiProxy';
+import { QueryParameters } from './apiProxy';
import CronJob from './cronJob';
import DaemonSet from './daemonSet';
import Deployment from './deployment';
import { KubeEvent } from './event';
import Job from './job';
+import { KubeObject } from './KubeObject';
+import Pod from './pod';
import ReplicaSet from './replicaSet';
import StatefulSet from './statefulSet';
+export { KubeObject } from './KubeObject';
export const HEADLAMP_ALLOWED_NAMESPACES = 'headlamp.allowed-namespaces';
-function getAllowedNamespaces() {
- const cluster = getCluster();
- if (!cluster) {
- return [];
- }
-
- const clusterSettings = helpers.loadClusterSettings(cluster);
- return clusterSettings.allowedNamespaces || [];
-}
-
export interface Cluster {
name: string;
useToken?: boolean;
@@ -55,7 +40,13 @@ export interface KubeObjectInterface {
kind: string;
apiVersion?: string;
metadata: KubeMetadata;
- [otherProps: string]: any;
+ spec?: any;
+ status?: any;
+ items?: any[];
+ actionType?: any;
+ lastTimestamp?: string;
+ key?: any;
+ // [otherProps: string]: any;
}
export interface StringDict {
@@ -192,6 +183,7 @@ export interface KubeMetadata {
* @see {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids | UIDs docs} for more details.
*/
uid: string;
+ apiVersion?: any;
}
export interface KubeOwnerReference {
@@ -288,38 +280,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) => void,
- opts?: ApiListSingleNamespaceOptions
- ) => any;
- useApiList: (
- onList: (arg: InstanceType>[]) => void,
- onError?: (err: ApiError) => void,
- opts?: ApiListOptions
- ) => any;
- useApiGet: (
- onGet: (...args: any) => void,
- name: string,
- namespace?: string,
- onError?: (err: ApiError) => void
- ) => void;
- useList: (
- opts?: ApiListOptions
- ) => [any[], ApiError | null, (items: any[]) => void, (err: ApiError | null) => void];
- useGet: (
- name: string,
- namespace?: string
- ) => [any, ApiError | null, (item: any) => void, (err: ApiError | null) => void];
- getErrorMessage: (err?: ApiError | null) => string | null;
- new (json: T): any;
- className: string;
- [prop: string]: any;
- getAuthorization?: (arg: string, resourceAttrs?: AuthRequestResourceAttrs) => any;
-}
+/**
+ * @deprecated For backwards compatibility, please use KubeObject
+ */
+export type KubeObjectIface = any;
export interface AuthRequestResourceAttrs {
name?: string;
@@ -330,554 +294,25 @@ export interface AuthRequestResourceAttrs {
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.
/**
+ * @deprecated For backwards compatibility, please extend KubeObject
+ *
* @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(
- objectName: string
-): KubeObjectIface {
- class KubeObject {
- static apiEndpoint: ReturnType;
- jsonData: T | null = null;
- public static readOnlyFields: JsonPath[];
- private readonly _clusterName: string;
-
- constructor(json: T) {
- this.jsonData = json;
- this._clusterName = getCluster() || '';
- }
-
- 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) => void,
- opts?: ApiListSingleNamespaceOptions
- ) {
- const createInstance = (item: T) => this.create(item) 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) => 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?.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(
- opts?: ApiListOptions
- ): [U[] | null, ApiError | null, (items: U[]) => void, (err: ApiError | null) => void] {
- const [objList, setObjList] = React.useState(null);
- const [error, setError] = useErrorState(setObjList);
- const currentCluster = useCluster();
- const cluster = opts?.cluster || currentCluster;
-
- // Reset the list and error when the cluster changes.
- React.useEffect(() => {
- setObjList(null);
- setError(null);
- }, [cluster]);
-
- function setList(items: U[] | null) {
- setObjList(items);
- if (items !== null) {
- setError(null);
- }
- }
-
- this.useApiList(setList, setError, opts);
-
- // Return getters and then the setters as the getters are more likely to be used with
- // this function.
- return [objList, error, setObjList, setError];
- }
-
- static create(this: new (arg: T) => U, item: T): U {
- return new this(item) as U;
- }
-
- static apiGet(
- onGet: (...args: any) => void,
- name: string,
- namespace?: string,
- onError?: (err: ApiError | null) => 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) => 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));
- }
-
- static useGet(
- name: string,
- namespace?: string,
- opts?: {
- queryParams?: QueryParameters;
- cluster?: string;
- }
- ): [U | null, ApiError | null, (items: U) => void, (err: ApiError | null) => void] {
- const [obj, setObj] = React.useState(null);
- const [error, setError] = useErrorState(setObj);
-
- function onGet(item: U | null) {
- // Only set the object if we have we have a different one.
- if (!!obj && !!item && obj.metadata.resourceVersion === item.metadata.resourceVersion) {
- return;
- }
-
- setObj(item);
- if (item !== null) {
- setError(null);
- }
- }
-
- function onError(err: ApiError | null) {
- if (
- error === err ||
- (!!error && !!err && error.message === err.message && error.status === err.status)
- ) {
- return;
- }
-
- setError(err);
- }
-
- this.useApiGet(onGet, name, namespace, onError, opts);
-
- // Return getters and then the setters as the getters are more likely to be used with
- // this function.
- return [obj, error, setObj, setError];
- }
-
- private _class() {
- return this.constructor as typeof KubeObject;
- }
-
- delete() {
- const args: string[] = [this.getName()];
- if (this.isNamespaced) {
- args.unshift(this.getNamespace()!);
- }
-
- 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: Parameters = [body];
-
- if (this.isNamespaced) {
- args.push(this.getNamespace());
- }
-
- args.push(this.getName());
- 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';
- }
- }
+export function makeKubeObject(objectName: string) {
+ class KubeObjectInternal extends KubeObject {
+ static objectName = objectName;
}
-
- return KubeObject as KubeObjectIface;
+ return KubeObjectInternal;
}
-export type KubeObjectClass = ReturnType;
-export type KubeObject = InstanceType;
+/**
+ * This type refers to the *class* of a KubeObject.
+ */
+export type KubeObjectClass = typeof KubeObject;
export type Time = number | string | null;
@@ -1282,4 +717,13 @@ export interface KubeContainerStatus {
started?: boolean;
}
-export type Workload = DaemonSet | ReplicaSet | StatefulSet | Job | CronJob | Deployment;
+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/configMap.ts b/frontend/src/lib/k8s/configMap.ts
index 9ac7f7906dd..e5e77e75284 100644
--- a/frontend/src/lib/k8s/configMap.ts
+++ b/frontend/src/lib/k8s/configMap.ts
@@ -1,15 +1,16 @@
import { apiFactoryWithNamespace } from './apiProxy';
-import { KubeObjectInterface, makeKubeObject, StringDict } from './cluster';
+import { KubeObject, KubeObjectInterface, StringDict } from './cluster';
export interface KubeConfigMap extends KubeObjectInterface {
data: StringDict;
}
-class ConfigMap extends makeKubeObject('configMap') {
+class ConfigMap extends KubeObject {
+ static objectName = 'configMap';
static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'configmaps');
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 91f7763e4ee..bda6737f9dc 100644
--- a/frontend/src/lib/k8s/crd.ts
+++ b/frontend/src/lib/k8s/crd.ts
@@ -1,6 +1,6 @@
import { ResourceClasses } from '.';
import { apiFactory, apiFactoryWithNamespace } from './apiProxy';
-import { KubeObjectClass, KubeObjectInterface, makeKubeObject } from './cluster';
+import { KubeObject, KubeObjectClass, KubeObjectInterface } from './cluster';
export interface KubeCRD extends KubeObjectInterface {
spec: {
@@ -47,7 +47,8 @@ export interface KubeCRD extends KubeObjectInterface {
};
}
-class CustomResourceDefinition extends makeKubeObject('crd') {
+class CustomResourceDefinition extends KubeObject {
+ static objectName = 'crd';
static apiEndpoint = apiFactory(
['apiextensions.k8s.io', 'v1', 'customresourcedefinitions'],
['apiextensions.k8s.io', 'v1beta1', 'customresourcedefinitions']
@@ -63,11 +64,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 +99,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 +131,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 +147,7 @@ export function makeCustomResourceClass(
// Used for tests
if (import.meta.env.UNDER_TEST === 'true') {
- const knownClass = ResourceClasses[apiInfoArgs[0][2]];
+ const knownClass = (ResourceClasses as Record)[apiInfoArgs[0][2]];
if (!!knownClass) {
return knownClass;
}
@@ -159,7 +160,8 @@ export function makeCustomResourceClass(
};
const apiFunc = !!objArgs.isNamespaced ? apiFactoryWithNamespace : apiFactory;
- return class CRClass extends makeKubeObject(objArgs.singleName) {
+ return class CRClass extends KubeObject {
+ static objectName = objArgs.singleName;
static apiEndpoint = apiFunc(...apiInfoArgs);
};
}
diff --git a/frontend/src/lib/k8s/cronJob.ts b/frontend/src/lib/k8s/cronJob.ts
index 25e2a189ebf..d1c397c6c79 100644
--- a/frontend/src/lib/k8s/cronJob.ts
+++ b/frontend/src/lib/k8s/cronJob.ts
@@ -1,5 +1,5 @@
import { apiFactoryWithNamespace } from './apiProxy';
-import { KubeContainer, KubeMetadata, KubeObjectInterface, makeKubeObject } from './cluster';
+import { KubeContainer, KubeMetadata, KubeObject, KubeObjectInterface } from './cluster';
/**
* CronJob structure returned by the k8s API.
@@ -34,7 +34,8 @@ export interface KubeCronJob extends KubeObjectInterface {
};
}
-class CronJob extends makeKubeObject('CronJob') {
+class CronJob extends KubeObject {
+ static objectName = 'CronJob';
static apiEndpoint = apiFactoryWithNamespace(
['batch', 'v1', 'cronjobs'],
['batch', 'v1beta1', 'cronjobs']
diff --git a/frontend/src/lib/k8s/daemonSet.ts b/frontend/src/lib/k8s/daemonSet.ts
index 82cfbcf6a85..c264a1e9718 100644
--- a/frontend/src/lib/k8s/daemonSet.ts
+++ b/frontend/src/lib/k8s/daemonSet.ts
@@ -2,9 +2,9 @@ import { apiFactoryWithNamespace } from './apiProxy';
import {
KubeContainer,
KubeMetadata,
+ KubeObject,
KubeObjectInterface,
LabelSelector,
- makeKubeObject,
} from './cluster';
import { KubePodSpec } from './pod';
@@ -28,15 +28,16 @@ export interface KubeDaemonSet extends KubeObjectInterface {
};
}
-class DaemonSet extends makeKubeObject('DaemonSet') {
+class DaemonSet extends KubeObject {
+ static objectName = 'DaemonSet';
static apiEndpoint = apiFactoryWithNamespace('apps', 'v1', 'daemonsets');
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 b02bb34e4ec..0350f54462b 100644
--- a/frontend/src/lib/k8s/deployment.ts
+++ b/frontend/src/lib/k8s/deployment.ts
@@ -2,9 +2,9 @@ import { apiFactoryWithNamespace } from './apiProxy';
import {
KubeContainer,
KubeMetadata,
+ KubeObject,
KubeObjectInterface,
LabelSelector,
- makeKubeObject,
} from './cluster';
import { KubePodSpec } from './pod';
@@ -26,7 +26,8 @@ export interface KubeDeployment extends KubeObjectInterface {
};
}
-class Deployment extends makeKubeObject('Deployment') {
+class Deployment extends KubeObject {
+ static objectName = 'Deployment';
static apiEndpoint = apiFactoryWithNamespace('apps', 'v1', 'deployments', true);
get spec() {
diff --git a/frontend/src/lib/k8s/endpoints.ts b/frontend/src/lib/k8s/endpoints.ts
index 90d4e731e64..dc73d101307 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, KubeObject, KubeObjectInterface } from './cluster';
export interface KubeEndpointPort {
name?: string;
@@ -28,19 +28,20 @@ export interface KubeEndpoint extends KubeObjectInterface {
subsets: KubeEndpointSubset[];
}
-class Endpoints extends makeKubeObject('endpoint') {
+class Endpoints extends KubeObject {
+ static objectName = 'endpoint';
static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'endpoints');
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 5ede3b043ad..02b8487be42 100644
--- a/frontend/src/lib/k8s/event.ts
+++ b/frontend/src/lib/k8s/event.ts
@@ -2,7 +2,7 @@ import React from 'react';
import { CancellablePromise, ResourceClasses } from '.';
import { ApiError, apiFactoryWithNamespace, QueryParameters } from './apiProxy';
import { request } from './apiProxy';
-import { KubeMetadata, KubeObject, makeKubeObject } from './cluster';
+import { KubeMetadata, KubeObject, KubeObjectClass } from './cluster';
export interface KubeEvent {
type: string;
@@ -21,7 +21,8 @@ export interface KubeEvent {
[otherProps: string]: any;
}
-class Event extends makeKubeObject('Event') {
+class Event extends KubeObject {
+ static objectName = 'Event';
static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'events');
// Max number of events to fetch from the API
@@ -105,7 +106,7 @@ class Event extends makeKubeObject('Event') {
return eventTime;
}
- const firstTimestamp = this.firstTimestamp;
+ const firstTimestamp = this.getValue('firstTimestamp');
if (!!firstTimestamp) {
return firstTimestamp;
}
@@ -149,7 +150,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 +160,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 b68ffe0695a..9e1cfb7756f 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, KubeObject, KubeObjectClass, KubeObjectInterface } from './cluster';
export interface CrossVersionObjectReference {
apiVersion: string;
kind: string;
@@ -166,15 +166,16 @@ interface HPAMetrics {
shortValue: string;
}
-class HPA extends makeKubeObject('horizontalPodAutoscaler') {
+class HPA extends KubeObject {
+ static objectName = 'horizontalPodAutoscaler';
static apiEndpoint = apiFactoryWithNamespace('autoscaling', 'v2', 'horizontalpodautoscalers');
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 +335,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 +348,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 9022e5ec88a..6f1505c7165 100644
--- a/frontend/src/lib/k8s/index.test.ts
+++ b/frontend/src/lib/k8s/index.test.ts
@@ -244,7 +244,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 3991862a185..f0175ba80e0 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, 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,53 +39,40 @@ import ServiceAccount from './serviceAccount';
import StatefulSet from './statefulSet';
import StorageClass from './storageClass';
-const classList = [
- ClusterRole,
- ClusterRoleBinding,
- ConfigMap,
- CustomResourceDefinition,
- CronJob,
- DaemonSet,
- Deployment,
- Endpoints,
- LimitRange,
- Lease,
- ResourceQuota,
- HPA,
- PodDisruptionBudget,
- PriorityClass,
- Ingress,
- IngressClass,
- Job,
- Namespace,
- NetworkPolicy,
- Node,
- PersistentVolume,
- PersistentVolumeClaim,
- Pod,
- ReplicaSet,
- Role,
- RoleBinding,
- RuntimeClass,
- Secret,
- Service,
- 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;
+export const ResourceClasses = {
+ ClusterRole: ClusterRole,
+ ClusterRoleBinding: ClusterRoleBinding,
+ ConfigMap: ConfigMap,
+ CustomResourceDefinition: CustomResourceDefinition,
+ CronJob: CronJob,
+ DaemonSet: DaemonSet,
+ Deployment: Deployment,
+ Endpoint: Endpoints,
+ LimitRange: LimitRange,
+ Lease: Lease,
+ ResourceQuota: ResourceQuota,
+ HorizontalPodAutoscaler: HPA,
+ PodDisruptionBudget: PodDisruptionBudget,
+ PriorityClass: PriorityClass,
+ Ingress: Ingress,
+ IngressClass: IngressClass,
+ Job: Job,
+ Namespace: Namespace,
+ NetworkPolicy: NetworkPolicy,
+ Node: Node,
+ PersistentVolume: PersistentVolume,
+ PersistentVolumeClaim: PersistentVolumeClaim,
+ Pod: Pod,
+ ReplicaSet: ReplicaSet,
+ Role: Role,
+ RoleBinding: RoleBinding,
+ RuntimeClass: RuntimeClass,
+ Secret: Secret,
+ Service: Service,
+ ServiceAccount: ServiceAccount,
+ StatefulSet: StatefulSet,
+ StorageClass: StorageClass,
+};
/** 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 b546e581f21..6f4610d2bd6 100644
--- a/frontend/src/lib/k8s/ingress.ts
+++ b/frontend/src/lib/k8s/ingress.ts
@@ -1,5 +1,5 @@
import { apiFactoryWithNamespace } from './apiProxy';
-import { KubeObjectInterface, makeKubeObject } from './cluster';
+import { KubeObject, KubeObjectInterface } from './cluster';
interface LegacyIngressRule {
host: string;
@@ -68,7 +68,8 @@ export interface KubeIngress extends KubeObjectInterface {
};
}
-class Ingress extends makeKubeObject('ingress') {
+class Ingress extends KubeObject {
+ static objectName = 'ingress';
static apiEndpoint = apiFactoryWithNamespace(
['networking.k8s.io', 'v1', 'ingresses'],
['extensions', 'v1beta1', 'ingresses']
@@ -77,7 +78,7 @@ class Ingress extends makeKubeObject('ingress') {
private cachedRules: IngressRule[] = [];
get spec(): KubeIngress['spec'] {
- return this.jsonData!.spec;
+ return this.jsonData.spec;
}
getHosts() {
diff --git a/frontend/src/lib/k8s/ingressClass.ts b/frontend/src/lib/k8s/ingressClass.ts
index 79db73b4f77..43ead5fa2e2 100644
--- a/frontend/src/lib/k8s/ingressClass.ts
+++ b/frontend/src/lib/k8s/ingressClass.ts
@@ -1,5 +1,5 @@
import { apiFactory } from './apiProxy';
-import { KubeObjectInterface, makeKubeObject } from './cluster';
+import { KubeObject, KubeObjectInterface } from './cluster';
export interface KubeIngressClass extends KubeObjectInterface {
spec: {
@@ -8,15 +8,16 @@ export interface KubeIngressClass extends KubeObjectInterface {
};
}
-class IngressClass extends makeKubeObject('ingressClass') {
+class IngressClass extends KubeObject {
+ static objectName = 'ingressClass';
static apiEndpoint = apiFactory(['networking.k8s.io', 'v1', 'ingressclasses']);
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 c2ec2963d15..991b741ba1a 100644
--- a/frontend/src/lib/k8s/job.ts
+++ b/frontend/src/lib/k8s/job.ts
@@ -2,9 +2,9 @@ import { apiFactoryWithNamespace } from './apiProxy';
import {
KubeContainer,
KubeMetadata,
+ KubeObject,
KubeObjectInterface,
LabelSelector,
- makeKubeObject,
} from './cluster';
import { KubePodSpec } from './pod';
@@ -22,15 +22,16 @@ export interface KubeJob extends KubeObjectInterface {
};
}
-class Job extends makeKubeObject('Job') {
+class Job extends KubeObject {
+ static objectName = 'Job';
static apiEndpoint = apiFactoryWithNamespace('batch', 'v1', 'jobs');
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 8df4801b404..c26e83e9807 100644
--- a/frontend/src/lib/k8s/lease.ts
+++ b/frontend/src/lib/k8s/lease.ts
@@ -1,5 +1,5 @@
import { apiFactoryWithNamespace } from './apiProxy';
-import { KubeObjectInterface, makeKubeObject } from './cluster';
+import { KubeObject, KubeObjectInterface } from './cluster';
export interface LeaseSpec {
holderIdentity: string;
@@ -12,10 +12,11 @@ export interface KubeLease extends KubeObjectInterface {
spec: LeaseSpec;
}
-export class Lease extends makeKubeObject('Lease') {
+export class Lease extends KubeObject {
+ static objectName = 'Lease';
static apiEndpoint = apiFactoryWithNamespace('coordination.k8s.io', 'v1', 'leases');
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 d2007a14081..d9f0976ea41 100644
--- a/frontend/src/lib/k8s/limitRange.tsx
+++ b/frontend/src/lib/k8s/limitRange.tsx
@@ -1,5 +1,5 @@
import { apiFactoryWithNamespace } from './apiProxy';
-import { KubeObjectInterface, makeKubeObject } from './cluster';
+import { KubeObject, KubeObjectInterface } from './cluster';
export interface LimitRangeSpec {
limits: {
@@ -27,10 +27,11 @@ export interface KubeLimitRange extends KubeObjectInterface {
spec: LimitRangeSpec;
}
-export class LimitRange extends makeKubeObject('LimitRange') {
+export class LimitRange extends KubeObject {
+ static objectName = 'LimitRange';
static apiEndpoint = apiFactoryWithNamespace('', 'v1', 'limitranges');
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 c51aba26927..6a26892f57e 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 { KubeObject, KubeObjectInterface, LabelSelector } from './cluster';
export interface KubeRuleWithOperations {
apiGroups: string[];
@@ -42,9 +42,8 @@ export interface KubeMutatingWebhookConfiguration extends KubeObjectInterface {
}[];
}
-class MutatingWebhookConfiguration extends makeKubeObject |