portainer/portainer

View on GitHub
app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationVolumeConfigsTable.tsx

Summary

Maintainability
C
1 day
Test Coverage
import { KeyToPath, Pod, Secret } from 'kubernetes-types/core/v1';
import { Asterisk, Plus } from 'lucide-react';

import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useSecrets } from '@/react/kubernetes/configs/secret.service';

import { Icon } from '@@/Icon';
import { Link } from '@@/Link';

import { Application } from '../../types';
import { applicationIsKind } from '../../utils';

type Props = {
  namespace: string;
  app?: Application;
};

export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
  const containerVolumeConfigs = getApplicationVolumeConfigs(app);

  const { data: secrets } = useSecrets(useEnvironmentId(), namespace);

  if (containerVolumeConfigs.length === 0) {
    return null;
  }

  return (
    <table className="table">
      <tbody>
        <tr className="text-muted">
          <td className="w-1/4">Container</td>
          <td className="w-1/4">Configuration path</td>
          <td className="w-1/4">Value</td>
          <td className="w-1/4">Configuration</td>
        </tr>
        {containerVolumeConfigs.map(
          (
            {
              containerVolumeMount,
              isInitContainer,
              containerName,
              item,
              volumeConfigName,
            },
            index
          ) => (
            <tr key={index}>
              <td>
                {containerName}
                {isInitContainer && (
                  <span>
                    <Icon icon={Asterisk} />(
                    <a
                      href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/"
                      target="_blank"
                      rel="noopener noreferrer"
                    >
                      init container
                    </a>
                    )
                  </span>
                )}
              </td>
              <td>
                {item.path
                  ? `${containerVolumeMount?.mountPath}/${item.path}`
                  : `${containerVolumeMount?.mountPath}`}
              </td>
              <td>
                {item.key && (
                  <div className="flex items-center">
                    <Icon icon={Plus} className="!mr-1" />
                    {item.key}
                  </div>
                )}
                {!item.key && '-'}
              </td>
              <td>
                {isVolumeConfigNameFromSecret(secrets, volumeConfigName) ? (
                  <Link
                    className="flex items-center"
                    to="kubernetes.secrets.secret"
                    params={{ name: volumeConfigName, namespace }}
                    data-cy={`secret-link-${volumeConfigName}`}
                  >
                    <Icon icon={Plus} className="!mr-1" />
                    {volumeConfigName}
                  </Link>
                ) : (
                  <Link
                    className="flex items-center"
                    to="kubernetes.configmaps.configmap"
                    params={{ name: volumeConfigName, namespace }}
                    data-cy={`config-link-${volumeConfigName}`}
                  >
                    <Icon icon={Plus} className="!mr-1" />
                    {volumeConfigName}
                  </Link>
                )}
                {!volumeConfigName && '-'}
              </td>
            </tr>
          )
        )}
      </tbody>
    </table>
  );
}

function isVolumeConfigNameFromSecret(
  secrets?: Secret[],
  volumeConfigName?: string
) {
  return secrets?.some((secret) => secret.metadata?.name === volumeConfigName);
}

// getApplicationVolumeConfigs returns a list of volume configs / secrets for each container and each item within the matching volume
function getApplicationVolumeConfigs(app?: Application) {
  if (!app) {
    return [];
  }

  const podSpec = applicationIsKind<Pod>('Pod', app)
    ? app.spec
    : app.spec?.template?.spec;
  const appContainers = podSpec?.containers || [];
  const appInitContainers = podSpec?.initContainers || [];
  const appVolumes = podSpec?.volumes || [];
  const allContainers = [...appContainers, ...appInitContainers];

  const appVolumeConfigs = allContainers.flatMap((container) => {
    // for each container, get the volume mount paths
    const matchingVolumes = appVolumes
      // filter app volumes by config map or secret
      .filter((volume) => volume.configMap || volume.secret)
      .flatMap((volume) => {
        // flatten by volume items if there are any
        const volConfigMapItems =
          volume.configMap?.items || volume.secret?.items || [];
        const volumeConfigName =
          volume.configMap?.name || volume.secret?.secretName;
        const containerVolumeMount = container.volumeMounts?.find(
          (volumeMount) => volumeMount.name === volume.name
        );
        if (volConfigMapItems.length === 0) {
          return [
            {
              volumeConfigName,
              containerVolumeMount,
              containerName: container.name,
              isInitContainer: appInitContainers.includes(container),
              item: {} as KeyToPath,
            },
          ];
        }
        // if there are items, return a volume config for each item
        return volConfigMapItems.map((item) => ({
          volumeConfigName,
          containerVolumeMount,
          containerName: container.name,
          isInitContainer: appInitContainers.includes(container),
          item,
        }));
      })
      // only return the app volumes where the container volumeMounts include the volume name (from map step above)
      .filter((volume) => volume.containerVolumeMount);
    return matchingVolumes;
  });

  return appVolumeConfigs;
}