1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fully support displaying all supporter PodVolume types

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-10-25 10:37:37 -04:00
parent a76fc6df84
commit 2b2395d4dd
14 changed files with 1276 additions and 220 deletions

View File

@ -141,8 +141,11 @@ export function isMetricsEmpty(metrics: Record<string, IMetrics>) {
return Object.values(metrics).every(metric => !metric?.data?.result?.length); return Object.values(metrics).every(metric => !metric?.data?.result?.length);
} }
export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> | void { export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> | undefined {
if (!metrics) return; if (!metrics) {
return undefined;
}
const itemMetrics = { ...metrics }; const itemMetrics = { ...metrics };
for (const metric in metrics) { for (const metric in metrics) {

View File

@ -31,17 +31,25 @@ export interface IPvcMetrics<T = IMetrics> {
diskCapacity: T; diskCapacity: T;
} }
export interface PersistentVolumeClaim { export interface PersistentVolumeClaimSpec {
spec: { accessModes: string[];
accessModes: string[]; selector: LabelSelector;
storageClassName: string; resources: {
selector: LabelSelector; requests?: Record<string, string>;
resources: { limits?: Record<string, string>;
requests: {
storage: string; // 8Gi
};
};
}; };
volumeName?: string;
storageClassName?: string;
volumeMode?: string;
dataSource?: {
apiGroup: string;
kind: string;
name: string;
};
}
export interface PersistentVolumeClaim {
spec: PersistentVolumeClaimSpec;
status: { status: {
phase: string; // Pending phase: string; // Pending
}; };

View File

@ -9,6 +9,10 @@ import { IMetrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { RequireExactlyOne } from "type-fest";
import type { KubeObjectMetadata, LocalObjectReference } from "../kube-object";
import type { SecretReference } from "./secret.api";
import type { PersistentVolumeClaimSpec } from "./persistent-volume-claims.api";
export class PodsApi extends KubeApi<Pod> { export class PodsApi extends KubeApi<Pod> {
getLogs = async (params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> => { getLogs = async (params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> => {
@ -112,12 +116,8 @@ export interface IPodContainer extends Partial<Record<PodContainerProbe, IContai
}; };
}[]; }[];
envFrom?: { envFrom?: {
configMapRef?: { configMapRef?: LocalObjectReference;
name: string; secretRef?: LocalObjectReference;
};
secretRef?: {
name: string;
};
}[]; }[];
volumeMounts?: { volumeMounts?: {
name: string; name: string;
@ -195,6 +195,444 @@ export interface IPodContainerStatus {
started?: boolean; started?: boolean;
} }
export interface AwsElasticBlockStoreSource {
volumeID: string;
fsType: string;
}
export interface AzureDiskSource {
/**
* The name of the VHD blob object OR the name of an Azure managed data disk if `kind` is `"Managed"`.
*/
diskName: string;
/**
* The URI of the vhd blob object OR the `resourceID` of an Azure managed data disk if `kind` is `"Managed"`.
*/
diskURI: string;
/**
* Kind of disk
* @default "Shared"
*/
kind?: "Shared" | "Dedicated" | "Managed";
/**
* Disk caching mode.
* @default "None"
*/
cachingMode?: "None" | "ReadOnly" | "ReadWrite";
/**
* The filesystem type to mount.
* @default "ext4"
*/
fsType?: string;
/**
* Whether the filesystem is used as readOnly.
* @default false
*/
readonly?: boolean;
}
export interface AzureFileSource {
/**
* The name of the secret that contains both Azure storage account name and key.
*/
secretName: string;
/**
* The share name to be used.
*/
shareName: string;
/**
* In case the secret is stored in a different namespace.
* @default "default"
*/
secretNamespace?: string;
/**
* Whether the filesystem is used as readOnly.
*/
readOnly: boolean;
}
export interface CephfsSource {
/**
* List of Ceph monitors
*/
monitors: string[];
/**
* Used as the mounted root, rather than the full Ceph tree.
* @default "/"
*/
path?: string;
/**
* The RADOS user name.
* @default "admin"
*/
user?: string;
/**
* The path to the keyring file.
* @default "/etc/ceph/user.secret"
*/
secretFile?: string;
/**
* Reference to Ceph authentication secrets. If provided, then the secret overrides `secretFile`
*/
secretRef?: SecretReference;
/**
* Whether the filesystem is used as readOnly.
*/
readOnly: boolean;
}
export interface CinderSource {
volumeID: string;
fsType: string;
/**
* @default false
*/
readOnly?: boolean;
secretRef?: SecretReference;
}
export interface ConfigMapSource {
name: string;
items: {
key: string;
path: string;
}[];
}
export interface DownwardApiSource {
items: {
path: string;
fieldRef: {
fieldPath: string;
};
}[];
}
export interface EphemeralSource {
volumeClaimTemplate: {
/**
* All the rest of the fields are ignored and rejected during validation
*/
metadata?: Pick<KubeObjectMetadata, "labels" | "annotations">;
spec: PersistentVolumeClaimSpec;
};
}
export interface EmptyDirSource {
medium?: string;
sizeLimit?: string;
}
export interface FiberChannelSource {
/**
* A list of World Wide Names
*/
targetWWNs: string[];
/**
* Logical Unit number
*/
lun: number;
/**
* The type of filesystem
* @default "ext4"
*/
fsType?: string;
readOnly: boolean;
}
export interface FlokerSource {
datasetName: string;
}
export interface FlexVolumeSource {
driver: string;
fsType?: string;
secretRef?: LocalObjectReference;
/**
* @default false
*/
readOnly?: boolean;
options?: Record<string, string>;
}
export interface GcePersistentDiskSource {
pdName: string;
fsType: string;
}
export interface GitRepoSource {
repository: string;
revision: string;
}
export interface GlusterFsSource {
/**
* The name of the Endpoints object that represents a Gluster cluster configuration.
*/
endpoints: string;
/**
* The Glusterfs volume name.
*/
path: string;
/**
* The boolean that sets the mountpoint readOnly or readWrite.
*/
readOnly: boolean;
}
export interface HostPathSource {
path: string;
/**
* Determines the sorts of checks that will be done
* @default ""
*/
type?: "" | "DirectoryOrCreate" | "Directory" | "FileOrCreate" | "File" | "Socket" | "CharDevice" | "BlockDevice";
}
export interface IscsiSource {
targetPortal: string;
iqn: string;
lun: number;
fsType: string;
readOnly: boolean;
chapAuthDiscovery?: boolean;
chapAuthSession?: boolean;
secretRef?: SecretReference;
}
export interface LocalSource {
path: string;
}
export interface NetworkFsSource {
server: string;
path: string;
readOnly?: boolean;
}
export interface PVCSource {
claimName: string;
}
export interface PhotonPersistentDiskSource {
pdID: string;
/**
* @default "ext4"
*/
fsType?: string;
}
export interface PortworxVolumeSource {
volumeID: string;
fsType?: string;
readOnly?: boolean;
}
export interface ProjectedSource {
sources: {
secret?: {
name: string;
items?: {
key: string;
path: string;
mode?: number;
}[];
};
downwardAPI?: {
items?: {
path: string;
fieldRef?: {
fieldPath: string;
apiVersion?: string;
};
resourceFieldRef?: {
resource: string;
containerName?: string;
};
mode?: number;
}[];
};
configMap?: {
name: string;
items?: {
key: string;
path: string;
mode?: number;
}[];
optional?: boolean;
};
serviceAccountToken?: {
audience?: string;
expirationSeconds?: number;
path: string;
};
}[];
defaultMode: number;
}
export interface QuobyteSource {
registry: string;
volume: string;
/**
* @default false
*/
readOnly?: boolean;
/**
* @default "serivceaccount"
*/
user?: string;
group?: string;
tenant?: string;
}
export interface RbdSource {
monitors: string[];
image: string;
/**
* @default "ext4"
*/
fsType?: string;
/**
* @default "rbd"
*/
pool?: string;
/**
* @default "admin"
*/
user?: string;
/**
* @default "/etc/ceph/keyring"
*/
keyring?: string;
secretRef?: SecretReference;
/**
* @default false
*/
readOnly?: boolean;
}
export interface ScaleIoSource {
gateway: string;
system: string;
secretRef?: LocalObjectReference;
/**
* @default false
*/
sslEnabled?: boolean;
protectionDomain?: string;
storagePool?: string;
/**
* @default "ThinProvisioned"
*/
storageMode?: "ThickProvisioned" | "ThinProvisioned";
volumeName: string;
/**
* @default "xfs"
*/
fsType?: string;
/**
* @default false
*/
readOnly?: boolean;
}
export interface SecretSource {
secretName: string;
items?: {
key: string;
path: string;
mode?: number;
}[];
defaultMode?: number;
optional?: boolean;
}
export interface StorageOsSource {
volumeName: string;
/**
* @default Pod.metadata.namespace
*/
volumeNamespace?: string;
/**
* @default "ext4"
*/
fsType?: string;
/**
* @default false
*/
readOnly?: boolean;
secretRef?: LocalObjectReference;
}
export interface VsphereVolumeSource {
volumePath: string;
/**
* @default "ext4"
*/
fsType?: string;
storagePolicyName?: string;
storagePolicyID?: string;
}
export interface ContainerStorageInterfaceSource {
driver: string;
/**
* @default false
*/
readOnly?: boolean;
/**
* @default "ext4"
*/
fsType?: string;
volumeAttributes?: Record<string, string>;
controllerPublishSecretRef?: SecretReference;
nodeStageSecretRef?: SecretReference;
nodePublishSecretRef?: SecretReference;
controllerExpandSecretRef?: SecretReference;
}
export interface PodVolumeVariants {
awsElasticBlockStore: AwsElasticBlockStoreSource;
azureDisk: AzureDiskSource;
azureFile: AzureFileSource;
cephfs: CephfsSource;
cinder: CinderSource;
configMap: ConfigMapSource;
downwardAPI: DownwardApiSource;
ephemeral: EphemeralSource;
emptyDir: EmptyDirSource;
fc: FiberChannelSource;
flocker: FlokerSource;
flexVolume: FlexVolumeSource;
gcePersistentDisk: GcePersistentDiskSource;
gitRepo: GitRepoSource;
glusterfs: GlusterFsSource;
hostPath: HostPathSource;
iscsi: IscsiSource;
local: LocalSource;
nfs: NetworkFsSource;
persistentVolumeClaim: PVCSource;
photonPersistentDisk: PhotonPersistentDiskSource;
portworxVolume: PortworxVolumeSource;
projected: ProjectedSource;
quobyte: QuobyteSource;
rbd: RbdSource;
scaleIO: ScaleIoSource;
secret: SecretSource;
storageos: StorageOsSource;
vsphereVolume: VsphereVolumeSource;
csi: ContainerStorageInterfaceSource;
}
/**
* The valid kinds of volume
*/
export type PodVolumeKind = keyof PodVolumeVariants;
export type PodVolume = RequireExactlyOne<PodVolumeVariants> & {
name: string;
};
export class Pod extends WorkloadKubeObject { export class Pod extends WorkloadKubeObject {
static kind = "Pod"; static kind = "Pod";
static namespaced = true; static namespaced = true;
@ -206,23 +644,7 @@ export class Pod extends WorkloadKubeObject {
} }
declare spec?: { declare spec?: {
volumes?: { volumes?: PodVolume[];
name: string;
persistentVolumeClaim: {
claimName: string;
};
emptyDir: {
medium?: string;
sizeLimit?: string;
};
configMap: {
name: string;
};
secret: {
secretName: string;
defaultMode: number;
};
}[];
initContainers: IPodContainer[]; initContainers: IPodContainer[];
containers: IPodContainer[]; containers: IPodContainer[];
restartPolicy?: string; restartPolicy?: string;
@ -239,9 +661,7 @@ export class Pod extends WorkloadKubeObject {
[selector: string]: string; [selector: string]: string;
}; };
securityContext?: {}; securityContext?: {};
imagePullSecrets?: { imagePullSecrets?: LocalObjectReference[];
name: string;
}[];
hostNetwork?: boolean; hostNetwork?: boolean;
hostPID?: boolean; hostPID?: boolean;
hostIPC?: boolean; hostIPC?: boolean;

View File

@ -25,6 +25,11 @@ export interface ISecretRef {
name: string; name: string;
} }
export interface SecretReference {
name: string;
namespace?: string;
}
export interface SecretData extends KubeJsonApiData { export interface SecretData extends KubeJsonApiData {
type: SecretType; type: SecretType;
data?: Record<string, string>; data?: Record<string, string>;

View File

@ -21,6 +21,13 @@ export type KubeObjectConstructor<K extends KubeObject> = (new (data: KubeJsonAp
apiBase?: string; apiBase?: string;
}; };
/**
* A reference to an object in the same namespace
*/
export interface LocalObjectReference {
name: string;
}
export interface KubeObjectMetadata { export interface KubeObjectMetadata {
uid: string; uid: string;
name: string; name: string;

View File

@ -5,10 +5,10 @@
import "./pod-details-secrets.scss"; import "./pod-details-secrets.scss";
import React, { Component } from "react"; import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { autorun, observable, makeObservable } from "mobx"; import { autorun, observable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { observer } from "mobx-react";
import { Pod, Secret, secretsApi } from "../../../common/k8s-api/endpoints"; import { Pod, Secret, secretsApi } from "../../../common/k8s-api/endpoints";
import { getDetailsUrl } from "../kube-detail-params"; import { getDetailsUrl } from "../kube-detail-params";
@ -16,59 +16,38 @@ export interface PodDetailsSecretsProps {
pod: Pod; pod: Pod;
} }
@observer export const PodDetailsSecrets = observer(({ pod }: PodDetailsSecretsProps) => {
export class PodDetailsSecrets extends Component<PodDetailsSecretsProps> { const [secrets] = useState(observable.map<string, Secret>());
@observable secrets: Map<string, Secret> = observable.map<string, Secret>();
componentDidMount(): void { useEffect(() => autorun(async () => {
disposeOnUnmount(this, [ const podSecrets = await Promise.all(
autorun(async () => { pod.getSecrets().map(secretName => secretsApi.get({
const { pod } = this.props; name: secretName,
namespace: pod.getNs(),
const secrets = await Promise.all( })),
pod.getSecrets().map(secretName => secretsApi.get({
name: secretName,
namespace: pod.getNs(),
})),
);
secrets.forEach(secret => secret && this.secrets.set(secret.getName(), secret));
}),
]);
}
constructor(props: PodDetailsSecretsProps) {
super(props);
makeObservable(this);
}
render() {
const { pod } = this.props;
return (
<div className="PodDetailsSecrets">
{
pod.getSecrets().map(secretName => {
const secret = this.secrets.get(secretName);
if (secret) {
return this.renderSecretLink(secret);
} else {
return (
<span key={secretName}>{secretName}</span>
);
}
})
}
</div>
); );
}
protected renderSecretLink(secret: Secret) { secrets.replace(podSecrets.filter(Boolean).map(secret => [secret.getName(), secret]));
}), []);
const renderSecret = (name: string) => {
const secret = secrets.get(name);
if (!secret) {
return <span key={name}>{name}</span>;
}
return ( return (
<Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}> <Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}>
{secret.getName()} {secret.getName()}
</Link> </Link>
); );
} };
}
return (
<div className="PodDetailsSecrets">
{pod.getSecrets().map(renderSecret)}
</div>
);
});

View File

@ -0,0 +1,647 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import jsyaml from "js-yaml";
import { observer } from "mobx-react";
import React from "react";
import { Link } from "react-router-dom";
import { configMapApi, Pod, PodVolume, PodVolumeKind, PodVolumeVariants, pvcApi, SecretReference, secretsApi } from "../../../common/k8s-api/endpoints";
import type { KubeApi } from "../../../common/k8s-api/kube-api";
import type { KubeObject, LocalObjectReference } from "../../../common/k8s-api/kube-object";
import { DrawerItem, DrawerItemLabels, DrawerSection, DrawerSubSection } from "../drawer";
import { Icon } from "../icon";
import { getDetailsUrl } from "../kube-detail-params";
/**
* Additional parameters for each volume variant
*/
export interface VariantOptions {
pod: Pod;
volumeName: string;
}
function renderLocalRef(pod: Pod, title: string, ref: LocalObjectReference | SecretReference, typeApi: KubeApi<KubeObject>) {
return (
<DrawerItem name={title}>
<Link to={getDetailsUrl(typeApi.getUrl({ namespace: pod.getNs(), ...ref }))}>
{ref.name}
</Link>
</DrawerItem>
);
}
/**
*
*/
type PodVolumeVariantRenderers = {
[Key in keyof PodVolumeVariants]: (variant: PodVolumeVariants[Key], opts: VariantOptions) => React.ReactNode;
};
const volumeRenderers: PodVolumeVariantRenderers = {
awsElasticBlockStore: ({ volumeID, fsType = "ext4" }) => (
<>
<DrawerItem name="Volume ID">
{volumeID}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
</>
),
azureDisk: ({ diskName, diskURI, kind = "Shared", cachingMode = "None", fsType = "ext4", readonly = false }) => (
<>
<DrawerItem name={kind === "Managed" ? "Disk Name" : "VHD blob Name"}>
{diskName}
</DrawerItem>
<DrawerItem name={kind === "Managed" ? "Resource ID" : "Disk URI"}>
{diskURI}
</DrawerItem>
<DrawerItem name="Kind">
{kind}
</DrawerItem>
<DrawerItem name="Caching Mode">
{cachingMode}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readonly.toString()}
</DrawerItem>
</>
),
azureFile: ({ readOnly = false, secretName, shareName, secretNamespace = "default" }) => (
<>
<DrawerItem name="Secret Name">
{secretName}
</DrawerItem>
<DrawerItem name="Share Name">
{shareName}
</DrawerItem>
<DrawerItem name="Namespace of Secret">
{secretNamespace}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
),
ephemeral: ({ volumeClaimTemplate: { metadata, spec }}, { pod, volumeName }) => (
<>
<DrawerItem name="PVC Template Name">
{pod.getName()}-{volumeName}
</DrawerItem>
<DrawerItemLabels
name="Template Labels"
labels={metadata.labels}
/>
<DrawerItemLabels
name="Template Annotations"
labels={metadata.annotations}
/>
<DrawerItem name="Template PVC Spec">
{jsyaml.dump(spec)}
</DrawerItem>
</>
),
emptyDir: ({ medium, sizeLimit }) => (
<>
<DrawerItem name="Medium" hidden={!medium}>
{medium}
</DrawerItem>
<DrawerItem name="Size Limit" hidden={!sizeLimit}>
{sizeLimit}
</DrawerItem>
</>
),
cephfs: ({ monitors, path = "/", user = "admin", secretFile = "/etc/ceph/user.secret", secretRef, readOnly }, { pod }) => (
<>
<DrawerItem name="Monitors">
<ul>
{monitors.map(monitor => <li key={monitor}>{monitor}</li>)}
</ul>
</DrawerItem>
<DrawerItem name="Mount Path">
{path}
</DrawerItem>
<DrawerItem name="Username">
{user}
</DrawerItem>
{
secretRef
? renderLocalRef(pod, "Secret", secretRef, secretsApi)
: (
<DrawerItem name="Secret Filepath">
{secretFile}
</DrawerItem>
)
}
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
),
cinder: ({ volumeID, fsType = "ext4" }) => (
<>
<DrawerItem name="Volume ID">
{volumeID}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
</>
),
configMap: ({ name }, { pod }) => renderLocalRef(pod, "Name", { name }, configMapApi),
downwardAPI: ({ items }) => (
<>
<DrawerItem name="Items">
<ul>
{items.map(item => <li key={item.path}>{item.path}</li>)}
</ul>
</DrawerItem>
</>
),
fc: ({ targetWWNs, lun, fsType = "ext4", readOnly = false }) => (
<>
<DrawerItem name="Target World Wide Names">
<ul>
{targetWWNs.map(targetWWN => <li key={targetWWN}>{targetWWN}</li>)}
</ul>
</DrawerItem>
<DrawerItem name="Logical Unit Number">
{lun.toString()}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
),
flexVolume: ({ driver, fsType, secretRef, readOnly = false, options }, { pod }) => (
<>
<DrawerItem name="Driver">
{driver}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType || "-- system default --"}
</DrawerItem>
{secretRef && renderLocalRef(pod, "Secret", secretRef, secretsApi)}
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
{
...Object.entries(options)
.map(([key, value]) => (
<DrawerItem key={key} name={`Option: ${key}`}>
{value}
</DrawerItem>
))
}
</>
),
flocker: ({ datasetName }) => (
<>
<DrawerItem name="Dataset Name">
{datasetName}
</DrawerItem>
</>
),
gcePersistentDisk: ({ pdName, fsType = "ext4" }) => (
<>
<DrawerItem name="Persistent Disk Name">
{pdName}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
</>
),
gitRepo: ({ repository, revision }) => (
<>
<DrawerItem name="Repository URL">
{repository}
</DrawerItem>
<DrawerItem name="Commit Hash">
{revision}
</DrawerItem>
</>
),
glusterfs: ({ endpoints, path, readOnly = false }) => (
<>
<DrawerItem name="Endpoints object name">
{endpoints}
</DrawerItem>
<DrawerItem name="Glusterfs volume name">
{path}
</DrawerItem>
<DrawerItem name="Readonly Mountpoint">
{readOnly.toString()}
</DrawerItem>
</>
),
hostPath: ({ path, type }) => (
<>
<DrawerItem name="Node's Host Filesystem Path">
{path}
</DrawerItem>
<DrawerItem name="Check Behaviour">
{type || "-- none --"}
</DrawerItem>
</>
),
iscsi: ({ targetPortal, iqn, lun, fsType = "ext4", readOnly = false, chapAuthDiscovery, chapAuthSession, secretRef }) => (
<>
<DrawerItem name="Target Address">
{targetPortal}
</DrawerItem>
<DrawerItem name="iSCSI qualified name">
{iqn}
</DrawerItem>
<DrawerItem name="Logical Unit Number">
{lun.toString()}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
{chapAuthDiscovery && (
<DrawerItem name="CHAP Discovery Authentication">
{chapAuthDiscovery.toString()}
</DrawerItem>
)}
{chapAuthSession && (
<DrawerItem name="CHAP Session Authentication">
{chapAuthSession.toString()}
</DrawerItem>
)}
{ secretRef && (
<DrawerItem name="CHAP Secret">
{secretRef.name}
</DrawerItem>
)}
</>
),
local: ({ path }) => (
<>
<DrawerItem name="Path">
{path}
</DrawerItem>
</>
),
nfs: ({ server, path, readOnly = false }) => (
<>
<DrawerItem name="Server">
{server}
</DrawerItem>
<DrawerItem name="Path">
{path}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
),
persistentVolumeClaim: ({ claimName }, { pod }) => renderLocalRef(pod, "Name", { name: claimName }, pvcApi),
photonPersistentDisk: ({ pdID, fsType = "ext4" }) => (
<>
<DrawerItem name="Persistent Disk ID">
{pdID}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
</>
),
portworxVolume: ({ volumeID, fsType = "ext4", readOnly = false }) => (
<>
<DrawerItem name="Volume ID">
{volumeID}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
),
projected: ({ sources, defaultMode }) => (
<>
<DrawerItem name="Default Mount Mode">
0o{defaultMode.toString(8)}
</DrawerItem>
<DrawerItem name="Sources">
{
sources.map(({ secret, downwardAPI, configMap, serviceAccountToken }, index) => (
<React.Fragment key={index}>
{secret && (
<DrawerSubSection title="Secret">
<DrawerItem name="Name">
{secret.name}
</DrawerItem>
{secret.items && (
<DrawerItem name="Items">
<ul>
{secret.items.map(({ key }) => <li key={key}>{key}</li>)}
</ul>
</DrawerItem>
)}
</DrawerSubSection>
)}
{downwardAPI && (
<DrawerSubSection title="Downward API">
{downwardAPI.items && (
<DrawerItem name="Items">
<ul>
{downwardAPI.items.map(({ path }) => <li key={path}>{path}</li>)}
</ul>
</DrawerItem>
)}
</DrawerSubSection>
)}
{configMap && (
<DrawerSubSection title="Config Map">
<DrawerItem name="Name">
{configMap.name}
</DrawerItem>
{configMap.items && (
<DrawerItem name="Items">
<ul>
{configMap.items.map(({ path }) => <li key={path}>{path}</li>)}
</ul>
</DrawerItem>
)}
</DrawerSubSection>
)}
{serviceAccountToken && (
<DrawerSubSection title="Service Account Token">
<DrawerItem name="Audience" hidden={!serviceAccountToken.audience}>
{serviceAccountToken.audience}
</DrawerItem>
<DrawerItem name="Expiration" hidden={!serviceAccountToken.expirationSeconds}>
{serviceAccountToken.expirationSeconds}s
</DrawerItem>
<DrawerItem name="Path">
{serviceAccountToken.path}
</DrawerItem>
</DrawerSubSection>
)}
</React.Fragment>
))
}
</DrawerItem>
</>
),
quobyte: ({ registry, volume, readOnly = false, user = "serviceaccount", group, tenant }) => (
<>
<DrawerItem name="Registry">
{registry}
</DrawerItem>
<DrawerItem name="Volume">
{volume}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
<DrawerItem name="User">
{user}
</DrawerItem>
<DrawerItem name="Group">
{group ?? "-- no group --"}
</DrawerItem>
<DrawerItem name="Tenant" hidden={!tenant}>
{tenant}
</DrawerItem>
</>
),
rbd: ({ monitors, image, fsType = "ext4", pool = "rbd", user = "admin", keyring = "/etc/ceph/keyright", secretRef, readOnly = false }, { pod }) => (
<>
<DrawerItem name="Ceph Monitors">
<ul>
{monitors.map(monitor => <li key={monitor}>{monitor}</li>)}
</ul>
</DrawerItem>
<DrawerItem name="Image">
{image}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Pool">
{pool}
</DrawerItem>
<DrawerItem name="User">
{user}
</DrawerItem>
{
secretRef
? renderLocalRef(pod, "Authentication Secret", secretRef, secretsApi)
: (
<DrawerItem name="Keyright Path">
{keyring}
</DrawerItem>
)
}
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
),
scaleIO: ({ gateway, system, secretRef, sslEnabled = false, protectionDomain, storagePool, storageMode = "ThinProvisioned", volumeName, fsType = "xfs", readOnly = false }, { pod }) => (
<>
<DrawerItem name="Gateway">
{gateway}
</DrawerItem>
<DrawerItem name="System">
{system}
</DrawerItem>
{renderLocalRef(pod, "Name", secretRef, secretsApi)}
<DrawerItem name="SSL Enabled">
{sslEnabled.toString()}
</DrawerItem>
<DrawerItem name="SSL Enabled">
{sslEnabled.toString()}
</DrawerItem>
<DrawerItem name="Protection Domain Name" hidden={!protectionDomain}>
{protectionDomain}
</DrawerItem>
<DrawerItem name="Storage Pool" hidden={!storagePool}>
{storagePool}
</DrawerItem>
<DrawerItem name="Storage Mode" hidden={!storageMode}>
{storageMode}
</DrawerItem>
<DrawerItem name="Volume Name">
{volumeName}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
</>
),
secret: ({ secretName, items = [], defaultMode = 0o644, optional = false }, { pod }) => (
<>
{renderLocalRef(pod, "Name", { name: secretName }, secretsApi)}
<DrawerItem name="Items" hidden={items.length === 0}>
<ul>
{items.map(({ key }) => <li key={key}>{key}</li>)}
</ul>
</DrawerItem>
<DrawerItem name="Default File Mode">
0o{defaultMode.toString(8)}
</DrawerItem>
<DrawerItem name="Optional">
{optional.toString()}
</DrawerItem>
</>
),
storageos: ({ volumeName, volumeNamespace, fsType = "ext4", readOnly = false, secretRef }, { pod }) => (
<>
<DrawerItem name="Volume Name">
{volumeName}
</DrawerItem>
<DrawerItem name="Volume Namespace" hidden={volumeNamespace === "default"}>
{
volumeNamespace === volumeName
? "- no default behaviour -"
: volumeNamespace || pod.getNs()
}
</DrawerItem>
<DrawerItem name="Filesystem type">
{fsType}
</DrawerItem>
<DrawerItem name="Readonly">
{readOnly.toString()}
</DrawerItem>
{secretRef && renderLocalRef(pod, "Secret", secretRef, secretsApi)}
</>
),
vsphereVolume: ({ volumePath, fsType = "ext4", storagePolicyName, storagePolicyID }) => (
<>
<DrawerItem name="Virtual Machine Disk Volume">
{volumePath}
</DrawerItem>
<DrawerItem name="Filesystem type">
{fsType}
</DrawerItem>
<DrawerItem name="Storage Policy Based Management Profile Name" hidden={!storagePolicyName}>
{storagePolicyName}
</DrawerItem>
<DrawerItem name="Storage Policy Based Management Profile ID" hidden={!storagePolicyID}>
{storagePolicyID}
</DrawerItem>
</>
),
csi: ({ driver, readOnly = false, fsType = "ext4", volumeAttributes = {}, nodePublishSecretRef, controllerPublishSecretRef, nodeStageSecretRef, controllerExpandSecretRef }, { pod }) => (
<>
<DrawerItem name="Driver">
{driver}
</DrawerItem>
<DrawerItem name="ReadOnly">
{readOnly.toString()}
</DrawerItem>
<DrawerItem name="Filesystem Type">
{fsType}
</DrawerItem>
{controllerPublishSecretRef && renderLocalRef(pod, "Controller Publish Secret", controllerPublishSecretRef, secretsApi)}
{controllerExpandSecretRef && renderLocalRef(pod, "Controller Expand Secret", controllerExpandSecretRef, secretsApi)}
{nodePublishSecretRef && renderLocalRef(pod, "Node Publish Secret", nodePublishSecretRef, secretsApi)}
{nodeStageSecretRef && renderLocalRef(pod, "Node Stage Secret", nodeStageSecretRef, secretsApi)}
{
...Object.entries(volumeAttributes)
.map(([key, value]) => (
<DrawerItem key={key} name={key}>
{value}
</DrawerItem>
))
}
</>
),
};
export interface PodVolumesProps {
pod: Pod;
}
const deprecatedVolumeTypes = new Set<PodVolumeKind>([
"flocker",
"gitRepo",
"quobyte",
"storageos",
]);
function getVolumeType(volume: PodVolume): PodVolumeKind | undefined {
const keys = new Set(Object.keys(volume));
keys.delete("name"); // This key is not a kind field
for (const key of keys) {
// skip other random keys
if (key in volumeRenderers) {
const kind = key as PodVolumeKind;
if (volume[kind] && typeof volume[kind] === "object") {
return kind;
}
}
}
return undefined;
}
export const PodVolumes = observer(({ pod }: PodVolumesProps) => {
if (!pod) {
return null;
}
const renderVolume = (volume: PodVolume) => {
const type = getVolumeType(volume);
const isDeprecated = deprecatedVolumeTypes.has(type);
const renderVolume = volumeRenderers[type];
return (
<div key={volume.name} className="volume">
<div className="title flex gaps">
<Icon small material="storage" />
<span>{volume.name}</span>
</div>
{
renderVolume
? (
<>
<DrawerItem name="Type">
{type}
{isDeprecated && <Icon title="Deprecated" material="warning_amber" />}
</DrawerItem>
{renderVolume(volume[type] as any, { pod, volumeName: volume.name })}
</>
)
: type
? (
<DrawerItem name="Type">
{type}
</DrawerItem>
)
: <p>Error! Unknown pod volume kind</p>
}
</div>
);
};
const volumes = pod.getVolumes();
return (
<DrawerSection title="Volumes" hidden={volumes.length === 0}>
{volumes.map(renderVolume)}
</DrawerSection>
);
});

View File

@ -10,14 +10,13 @@ import kebabCase from "lodash/kebabCase";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { observable, reaction, makeObservable } from "mobx"; import { observable, reaction, makeObservable } from "mobx";
import { type IPodMetrics, nodesApi, Pod, pvcApi, configMapApi, getMetricsForPods } from "../../../common/k8s-api/endpoints"; import { type IPodMetrics, nodesApi, Pod, getMetricsForPods } from "../../../common/k8s-api/endpoints";
import { DrawerItem, DrawerTitle } from "../drawer"; import { DrawerItem, DrawerSection } from "../drawer";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { boundMethod, cssNames, toJS } from "../../utils"; import { boundMethod, cssNames, toJS } from "../../utils";
import { PodDetailsContainer } from "./pod-details-container"; import { PodDetailsContainer } from "./pod-details-container";
import { PodDetailsAffinities } from "./pod-details-affinities"; import { PodDetailsAffinities } from "./pod-details-affinities";
import { PodDetailsTolerations } from "./pod-details-tolerations"; import { PodDetailsTolerations } from "./pod-details-tolerations";
import { Icon } from "../icon";
import { PodDetailsSecrets } from "./pod-details-secrets"; import { PodDetailsSecrets } from "./pod-details-secrets";
import { ResourceMetrics } from "../resource-metrics"; import { ResourceMetrics } from "../resource-metrics";
import type { KubeObjectDetailsProps } from "../kube-object-details"; import type { KubeObjectDetailsProps } from "../kube-object-details";
@ -28,6 +27,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { getDetailsUrl } from "../kube-detail-params"; import { getDetailsUrl } from "../kube-detail-params";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { PodVolumes } from "./pod-details-volumes";
export interface PodDetailsProps extends KubeObjectDetailsProps<Pod> { export interface PodDetailsProps extends KubeObjectDetailsProps<Pod> {
} }
@ -73,12 +73,12 @@ export class PodDetails extends React.Component<PodDetailsProps> {
} }
const { status, spec } = pod; const { status, spec } = pod;
const { conditions, podIP } = status; const { conditions = [], podIP } = status ?? {};
const podIPs = pod.getIPs(); const podIPs = pod.getIPs();
const { nodeName } = spec; const { nodeName } = spec ?? {};
const nodeSelector = pod.getNodeSelectors(); const nodeSelector = pod.getNodeSelectors();
const volumes = pod.getVolumes();
const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Pod); const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Pod);
const initContainers = pod.getInitContainers();
return ( return (
<div className="PodDetails"> <div className="PodDetails">
@ -90,26 +90,22 @@ export class PodDetails extends React.Component<PodDetailsProps> {
<PodCharts/> <PodCharts/>
</ResourceMetrics> </ResourceMetrics>
)} )}
<KubeObjectMeta object={pod}/> <KubeObjectMeta object={pod}/>
<DrawerItem name="Status"> <DrawerItem name="Status">
<span className={cssNames("status", kebabCase(pod.getStatusMessage()))}>{pod.getStatusMessage()}</span> <span className={cssNames("status", kebabCase(pod.getStatusMessage()))}>{pod.getStatusMessage()}</span>
</DrawerItem> </DrawerItem>
<DrawerItem name="Node"> <DrawerItem name="Node" hidden={!nodeName}>
{nodeName && ( <Link to={getDetailsUrl(nodesApi.getUrl({ name: nodeName }))}>
<Link to={getDetailsUrl(nodesApi.getUrl({ name: nodeName }))}> {nodeName}
{nodeName} </Link>
</Link>
)}
</DrawerItem> </DrawerItem>
<DrawerItem name="Pod IP"> <DrawerItem name="Pod IP">
{podIP} {podIP}
</DrawerItem> </DrawerItem>
<DrawerItem name="Pod IPs" hidden={!podIPs.length} labelsOnly> <DrawerItem name="Pod IPs" hidden={podIPs.length === 0} labelsOnly>
{ {podIPs.map(label => <Badge key={label} label={label} />)}
podIPs.map(label => (
<Badge key={label} label={label}/>
))
}
</DrawerItem> </DrawerItem>
<DrawerItem name="Priority Class"> <DrawerItem name="Priority Class">
{pod.getPriorityClassName()} {pod.getPriorityClassName()}
@ -117,129 +113,49 @@ export class PodDetails extends React.Component<PodDetailsProps> {
<DrawerItem name="QoS Class"> <DrawerItem name="QoS Class">
{pod.getQosClass()} {pod.getQosClass()}
</DrawerItem> </DrawerItem>
{conditions &&
<DrawerItem name="Conditions" className="conditions" labelsOnly>
{
conditions.map(condition => {
const { type, status, lastTransitionTime } = condition;
return ( <DrawerItem name="Conditions" className="conditions" hidden={conditions.length === 0} labelsOnly>
<Badge
key={type}
label={type}
disabled={status === "False"}
tooltip={`Last transition time: ${lastTransitionTime}`}
/>
);
})
}
</DrawerItem>
}
{nodeSelector.length > 0 &&
<DrawerItem name="Node Selector">
{ {
nodeSelector.map(label => ( conditions.map(({ type, status, lastTransitionTime }) => (
<Badge key={label} label={label}/> <Badge
key={type}
label={type}
disabled={status === "False"}
tooltip={`Last transition time: ${lastTransitionTime}`}
/>
)) ))
} }
</DrawerItem> </DrawerItem>
}
<DrawerItem name="Node Selector" hidden={nodeSelector.length === 0}>
{nodeSelector.map(label => <Badge key={label} label={label} />)}
</DrawerItem>
<PodDetailsTolerations workload={pod}/> <PodDetailsTolerations workload={pod}/>
<PodDetailsAffinities workload={pod}/> <PodDetailsAffinities workload={pod}/>
{pod.getSecrets().length > 0 && ( <DrawerItem name="Secrets" hidden={pod.getSecrets().length === 0}>
<DrawerItem name="Secrets"> <PodDetailsSecrets pod={pod}/>
<PodDetailsSecrets pod={pod}/> </DrawerItem>
</DrawerItem>
)}
{pod.getInitContainers() && pod.getInitContainers().length > 0 && <DrawerSection title="Init Containers" hidden={initContainers.length === 0}>
<DrawerTitle title="Init Containers"/> {initContainers.map(c => <PodDetailsContainer key={c.name} pod={pod} container={c} />)}
} </DrawerSection>
{
pod.getInitContainers() && pod.getInitContainers().map(container => {
return <PodDetailsContainer key={container.name} pod={pod} container={container}/>;
})
}
<DrawerTitle title="Containers"/>
{
pod.getContainers().map(container => {
const { name } = container;
const metrics = getItemMetrics(toJS(this.containerMetrics), name);
return ( <DrawerSection title="Containers">
{
pod.getContainers().map(container => (
<PodDetailsContainer <PodDetailsContainer
key={name} key={container.name}
pod={pod} pod={pod}
container={container} container={container}
metrics={metrics || null} metrics={getItemMetrics(toJS(this.containerMetrics), container.name)}
/> />
); ))
}) }
} </DrawerSection>
{volumes.length > 0 && ( <PodVolumes pod={pod} />
<>
<DrawerTitle title="Volumes"/>
{volumes.map(volume => {
const claimName = volume.persistentVolumeClaim ? volume.persistentVolumeClaim.claimName : null;
const configMap = volume.configMap ? volume.configMap.name : null;
const type = Object.keys(volume)[1];
return (
<div key={volume.name} className="volume">
<div className="title flex gaps">
<Icon small material="storage"/>
<span>{volume.name}</span>
</div>
<DrawerItem name="Type">
{type}
</DrawerItem>
{ type == "configMap" && (
<div>
{configMap && (
<DrawerItem name="Name">
<Link
to={getDetailsUrl(configMapApi.getUrl({
name: configMap,
namespace: pod.getNs(),
}))}>{configMap}
</Link>
</DrawerItem>
)}
</div>
)}
{ type === "emptyDir" && (
<div>
{ volume.emptyDir.medium && (
<DrawerItem name="Medium">
{volume.emptyDir.medium}
</DrawerItem>
)}
{ volume.emptyDir.sizeLimit && (
<DrawerItem name="Size Limit">
{volume.emptyDir.sizeLimit}
</DrawerItem>
)}
</div>
)}
{claimName && (
<DrawerItem name="Claim Name">
<Link
to={getDetailsUrl(pvcApi.getUrl({
name: claimName,
namespace: pod.getNs(),
}))}
>{claimName}
</Link>
</DrawerItem>
)}
</div>
);
})}
</>
)}
</div> </div>
); );
} }

View File

@ -6,21 +6,30 @@
import React from "react"; import React from "react";
import { DrawerItem, DrawerItemProps } from "./drawer-item"; import { DrawerItem, DrawerItemProps } from "./drawer-item";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { KubeObject } from "../../../common/k8s-api/kube-object";
export interface DrawerItemLabelsProps extends DrawerItemProps { export interface DrawerItemLabelsProps extends DrawerItemProps {
labels: string[]; labels: string[] | Record<string, string>;
} }
export function DrawerItemLabels(props: DrawerItemLabelsProps) { export function DrawerItemLabels(props: DrawerItemLabelsProps) {
const { labels, ...itemProps } = props; const { labels, ...itemProps } = props;
if (!labels || !labels.length) { if (!labels || typeof labels !== "object") {
return null;
}
const labelStrings = Array.isArray(labels)
? labels
: KubeObject.stringifyLabels(labels);
if (labelStrings.length === 0) {
return null; return null;
} }
return ( return (
<DrawerItem {...itemProps} labelsOnly> <DrawerItem {...itemProps} labelsOnly>
{labels.map(label => <Badge key={label} label={label} title={label}/>)} {labelStrings.map(label => <Badge key={label} label={label} title={label}/>)}
</DrawerItem> </DrawerItem>
); );
} }

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { DrawerTitle, DrawerSubTitle } from "./drawer-title";
export interface DrawerSectionProps {
className?: string;
title?: React.ReactNode;
hidden?: boolean;
}
export class DrawerSection extends React.Component<DrawerSectionProps> {
render() {
const { title, children, className, hidden } = this.props;
if (hidden) {
return null;
}
return (
<>
<DrawerTitle className={className} title={title} />
{children}
</>
);
}
}
export class DrawerSubSection extends React.Component<DrawerSectionProps> {
render() {
const { title, children, className, hidden } = this.props;
if (hidden) {
return null;
}
return (
<>
<DrawerSubTitle className={className} title={title} />
{children}
</>
);
}
}

View File

@ -8,3 +8,8 @@
margin: $margin * 3 (-$margin * 3); margin: $margin * 3 (-$margin * 3);
background: var(--drawerSubtitleBackground); background: var(--drawerSubtitleBackground);
} }
.DrawerSubTitle {
padding: $padding $padding * 2;
background: var(--drawerSubtitleBackground);
}

View File

@ -23,3 +23,14 @@ export class DrawerTitle extends React.Component<DrawerTitleProps> {
); );
} }
} }
export class DrawerSubTitle extends React.Component<DrawerTitleProps> {
render() {
const { title, children, className } = this.props;
return (
<div className={cssNames("DrawerSubTitle", className)}>
{title || children}
</div>
);
}
}

View File

@ -5,6 +5,7 @@
export * from "./drawer"; export * from "./drawer";
export * from "./drawer-title"; export * from "./drawer-title";
export * from "./drawer-section";
export * from "./drawer-item"; export * from "./drawer-item";
export * from "./drawer-item-labels"; export * from "./drawer-item-labels";
export * from "./drawer-param-toggler"; export * from "./drawer-param-toggler";

View File

@ -17,8 +17,6 @@ interface TabsContextValue<D = any> {
onChange?(value: D): void; onChange?(value: D): void;
} }
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export interface TabsProps<D = any> extends TabsContextValue<D>, Omit<DOMAttributes<HTMLElement>, "onChange"> { export interface TabsProps<D = any> extends TabsContextValue<D>, Omit<DOMAttributes<HTMLElement>, "onChange"> {
className?: string; className?: string;
center?: boolean; center?: boolean;