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' && (
Container name |
Pod name |
Persisted folder |
Persistence |
{persistedFolders.map((persistedFolder, index) => (
{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' && (
Persisted folder |
Persistence |
{persistedFolders.map((persistedFolder, index) => (
{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 [];
}