import { useMemo } from 'react'; import { Asterisk, Box, Boxes, Database } from 'lucide-react'; import { Container, Pod, Volume } from 'kubernetes-types/core/v1'; import { StatefulSet } from 'kubernetes-types/apps/v1'; import { EnvironmentId } from '@/react/portainer/environments/types'; import { Icon } from '@@/Icon'; import { TextTip } from '@@/Tip/TextTip'; import { Tooltip } from '@@/Tip/Tooltip'; import { Link } from '@@/Link'; import { Application } from '../../types'; import { applicationIsKind } from '../../utils'; import { useApplicationPods } from '../../application.queries'; type Props = { environmentId: EnvironmentId; namespace: string; appName: string; app?: Application; }; export function ApplicationPersistentDataTable({ namespace, app, environmentId, appName, }: Props) { const { data: pods } = useApplicationPods( environmentId, namespace, appName, app ); const persistedFolders = useMemo( () => getPersistedFolders(app, pods), [app, pods] ); const dataAccessPolicy = getDataAccessPolicy(app); return ( <>
Data persistence
{!persistedFolders.length && ( This application has no persisted folders. )} {persistedFolders.length > 0 && ( <>
Data access policy: {dataAccessPolicy === 'isolated' && ( <> Isolated )} {dataAccessPolicy === 'shared' && ( <> Shared )}
{dataAccessPolicy === 'isolated' && ( {persistedFolders.map((persistedFolder, index) => ( ))}
Container name Pod name Persisted folder Persistence
{persistedFolder.volumeMount.container.name} {persistedFolder.isContainerInit && ( ( init container ) )} {persistedFolder.volumeMount?.pod?.metadata?.name} {persistedFolder.volumeMount.mountPath} {persistedFolder.volume.persistentVolumeClaim && ( {`${persistedFolder.volume.persistentVolumeClaim.claimName}-${persistedFolder.volumeMount?.pod?.metadata?.name}`} )} {persistedFolder.volume.hostPath && `${persistedFolder.volume.hostPath.path} on host filesystem`}
)} {dataAccessPolicy === 'shared' && ( {persistedFolders.map((persistedFolder, index) => ( ))}
Persisted folder Persistence
{persistedFolder.volumeMount.mountPath} {persistedFolder.volume.persistentVolumeClaim && ( { persistedFolder.volume.persistentVolumeClaim .claimName } )} {persistedFolder.volume.hostPath && `${persistedFolder.volume.hostPath.path} on host filesystem`}
)} )} ); } function getDataAccessPolicy(app?: Application) { if (!app || applicationIsKind('Pod', app)) { return 'none'; } if (applicationIsKind('StatefulSet', app)) { return 'isolated'; } return 'shared'; } function getPodsMatchingContainer(pods: Pod[], container: Container) { const matchingPods = pods.filter((pod) => { const podContainers = pod.spec?.containers || []; const podInitContainers = pod.spec?.initContainers || []; const podAllContainers = [...podContainers, ...podInitContainers]; return podAllContainers.some( (podContainer) => podContainer.name === container.name && podContainer.image === container.image ); }); return matchingPods; } function getPersistedFolders(app?: Application, pods?: Pod[]) { if (!app || !pods) { return []; } const podSpec = applicationIsKind('Pod', app) ? app.spec : app.spec?.template?.spec; const appVolumes = podSpec?.volumes || []; const appVolumeClaimVolumes = getVolumeClaimTemplates(app, appVolumes); const appAllVolumes = [...appVolumes, ...appVolumeClaimVolumes]; const appContainers = podSpec?.containers || []; const appInitContainers = podSpec?.initContainers || []; const appAllContainers = [...appContainers, ...appInitContainers]; // for each volume, find the volumeMounts that match it const persistedFolders = appAllVolumes.flatMap((volume) => { if (volume.persistentVolumeClaim || volume.hostPath) { const volumeMounts = appAllContainers.flatMap((container) => { const matchingPods = getPodsMatchingContainer(pods, container); return ( container.volumeMounts?.flatMap( (containerVolumeMount) => matchingPods.map((pod) => ({ ...containerVolumeMount, container, pod, })) || [] ) || [] ); }); const uniqueMatchingVolumeMounts = volumeMounts.filter( (volumeMount, index, self) => self.indexOf(volumeMount) === index && // remove volumeMounts with duplicate names volumeMount.name === volume.name // remove volumeMounts that don't match the volume ); return uniqueMatchingVolumeMounts.map((volumeMount) => ({ volume, volumeMount, isContainerInit: appInitContainers.some( (container) => container.name === volumeMount.container.name ), })); } return []; }); return persistedFolders; } function getVolumeClaimTemplates(app: Application, volumes: Volume[]) { if ( applicationIsKind('StatefulSet', app) && app.spec?.volumeClaimTemplates ) { const volumeClaimTemplates: Volume[] = app.spec.volumeClaimTemplates.map( (vc) => ({ name: vc.metadata?.name || '', persistentVolumeClaim: { claimName: vc.metadata?.name || '' }, }) ); const newPVC = volumeClaimTemplates.filter( (vc) => !volumes.find( (v) => v.persistentVolumeClaim?.claimName === vc.persistentVolumeClaim?.claimName ) ); return newPVC; } return []; }