diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationEnvVarsTable.test.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationEnvVarsTable.test.tsx
new file mode 100644
index 000000000..22baba628
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationEnvVarsTable.test.tsx
@@ -0,0 +1,702 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { vi, beforeEach, afterEach } from 'vitest';
+
+import { Application } from '../../types';
+
+import { ApplicationEnvVarsTable } from './ApplicationEnvVarsTable';
+
+// Mock icon components
+vi.mock('lucide-react', () => ({
+ Asterisk: () => ,
+ File: () => ,
+ FileCode: () => ,
+ Key: () => ,
+ Lock: () => ,
+}));
+
+// Mock UI components
+vi.mock('@@/Icon', () => ({
+ Icon: ({
+ icon: IconComponent,
+ ...props
+ }: {
+ icon: React.ComponentType;
+ [key: string]: unknown;
+ }) => ,
+}));
+
+vi.mock('@@/Tip/TextTip', () => ({
+ TextTip: ({
+ children,
+ color,
+ }: {
+ children: React.ReactNode;
+ color?: string;
+ }) => (
+
;
+ 'data-cy'?: string;
+ className?: string;
+ }) => {
+ mockLink({ children, to, params, 'data-cy': dataCy, className });
+ return (
+
+ {children}
+
+ );
+ },
+}));
+
+describe('ApplicationEnvVarsTable', () => {
+ beforeEach(() => {
+ mockLink.mockClear();
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should render helpful tip when there are no environment variables', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(
+ screen.getByText('Environment variables, ConfigMaps or Secrets')
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('text-tip')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'This application is not using any environment variable, ConfigMap or Secret.'
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('should render nothing when app is undefined', () => {
+ render();
+
+ expect(
+ screen.getByText('Environment variables, ConfigMaps or Secrets')
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('text-tip')).toBeInTheDocument();
+ });
+
+ it('should render regular environment variables with direct values', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ env: [
+ {
+ name: 'ENV_VAR',
+ value: 'test-value',
+ },
+ ],
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getByText('ENV_VAR')).toBeInTheDocument();
+ expect(screen.getByText('test-value')).toBeInTheDocument();
+ expect(screen.getByText('-')).toBeInTheDocument(); // No configuration resource
+ });
+
+ it('should render configmap environment variables with correct routing', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ env: [
+ {
+ name: 'CONFIG_VAR',
+ valueFrom: {
+ configMapKeyRef: {
+ name: 'test-configmap',
+ key: 'config-key',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getAllByText('CONFIG_VAR')).toHaveLength(2); // Appears in name and value columns
+ // Note: config-key is not displayed in UI - the component shows the env var name
+ expect(screen.getByText('test-configmap')).toBeInTheDocument();
+ expect(screen.getByTestId('key-icon')).toBeInTheDocument();
+ expect(screen.getByTestId('file-code-icon')).toBeInTheDocument();
+
+ // Verify the Link component was called with correct routing parameters
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.configmaps.configmap',
+ params: {
+ name: 'test-configmap',
+ namespace: 'default',
+ },
+ 'data-cy': 'configmap-link-test-configmap',
+ })
+ );
+ });
+
+ it('should render secret environment variables with correct routing', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ env: [
+ {
+ name: 'SECRET_VAR',
+ valueFrom: {
+ secretKeyRef: {
+ name: 'test-secret',
+ key: 'secret-key',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getAllByText('SECRET_VAR')).toHaveLength(2); // Appears in name and value columns
+ // Note: secret-key is not displayed in UI - the component shows the env var name
+ expect(screen.getByText('test-secret')).toBeInTheDocument();
+ expect(screen.getByTestId('key-icon')).toBeInTheDocument();
+ expect(screen.getByTestId('lock-icon')).toBeInTheDocument();
+
+ // Verify the Link component was called with correct routing parameters for secret
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.secrets.secret',
+ params: {
+ name: 'test-secret',
+ namespace: 'default',
+ },
+ 'data-cy': 'configmap-link-test-secret',
+ })
+ );
+ });
+
+ it('should render downward API field references', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ env: [
+ {
+ name: 'POD_NAME',
+ valueFrom: {
+ fieldRef: {
+ fieldPath: 'metadata.name',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getByText('POD_NAME')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ (content, element) =>
+ content.includes('metadata.name') && element?.tagName === 'SPAN'
+ )
+ ).toBeInTheDocument();
+ expect(screen.getByText('downward API')).toBeInTheDocument();
+ expect(screen.getByTestId('asterisk-icon')).toBeInTheDocument();
+ expect(screen.getByText('-')).toBeInTheDocument(); // No configuration resource
+ });
+
+ it('should render envFrom configmap (entire configmap import)', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ envFrom: [
+ {
+ configMapRef: {
+ name: 'entire-configmap',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getAllByText('-')).toHaveLength(2); // EnvFrom doesn't have a specific key name or value
+ expect(screen.getByText('entire-configmap')).toBeInTheDocument();
+ expect(screen.getByTestId('file-code-icon')).toBeInTheDocument();
+
+ // Verify configmap routing
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.configmaps.configmap',
+ params: {
+ name: 'entire-configmap',
+ namespace: 'default',
+ },
+ 'data-cy': 'configmap-link-entire-configmap',
+ })
+ );
+ });
+
+ it('should render envFrom secret (entire secret import)', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ envFrom: [
+ {
+ secretRef: {
+ name: 'entire-secret',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getAllByText('-')).toHaveLength(2); // EnvFrom doesn't have a specific key name or value
+ expect(screen.getByText('entire-secret')).toBeInTheDocument();
+ expect(screen.getByTestId('lock-icon')).toBeInTheDocument();
+
+ // Verify secret routing
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.secrets.secret',
+ params: {
+ name: 'entire-secret',
+ namespace: 'default',
+ },
+ 'data-cy': 'configmap-link-entire-secret',
+ })
+ );
+ });
+
+ it('should render init containers with asterisk indicator', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'main-container',
+ image: 'main-image',
+ env: [
+ {
+ name: 'MAIN_VAR',
+ value: 'main-value',
+ },
+ ],
+ },
+ ],
+ initContainers: [
+ {
+ name: 'init-container',
+ image: 'init-image',
+ env: [
+ {
+ name: 'INIT_VAR',
+ value: 'init-value',
+ },
+ ],
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ // Check main container
+ expect(screen.getByText('main-container')).toBeInTheDocument();
+ expect(screen.getByText('MAIN_VAR')).toBeInTheDocument();
+ expect(screen.getByText('main-value')).toBeInTheDocument();
+
+ // Check init container
+ expect(screen.getByText('init-container')).toBeInTheDocument();
+ expect(screen.getByText('INIT_VAR')).toBeInTheDocument();
+ expect(screen.getByText('init-value')).toBeInTheDocument();
+ expect(screen.getByText('init container')).toBeInTheDocument();
+ expect(screen.getByTestId('asterisk-icon')).toBeInTheDocument();
+ });
+
+ it('should handle mixed environment variable types correctly', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ env: [
+ {
+ name: 'REGULAR_VAR',
+ value: 'regular-value',
+ },
+ {
+ name: 'CONFIG_VAR',
+ valueFrom: {
+ configMapKeyRef: {
+ name: 'test-configmap',
+ key: 'config-key',
+ },
+ },
+ },
+ {
+ name: 'SECRET_VAR',
+ valueFrom: {
+ secretKeyRef: {
+ name: 'test-secret',
+ key: 'secret-key',
+ },
+ },
+ },
+ ],
+ envFrom: [
+ {
+ configMapRef: {
+ name: 'entire-configmap',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getAllByText('test-container')).toHaveLength(4); // Should appear 4 times - once for each env var
+ expect(screen.getByText('REGULAR_VAR')).toBeInTheDocument();
+ expect(screen.getByText('regular-value')).toBeInTheDocument();
+ expect(screen.getAllByText('CONFIG_VAR')).toHaveLength(2); // Appears in name and value columns
+ // Note: config-key is not displayed in UI - the component shows the env var name
+ expect(screen.getAllByText('SECRET_VAR')).toHaveLength(2); // Appears in name and value columns
+ // Note: secret-key is not displayed in UI - the component shows the env var name
+ expect(screen.getByText('test-configmap')).toBeInTheDocument();
+ expect(screen.getByText('test-secret')).toBeInTheDocument();
+ expect(screen.getByText('entire-configmap')).toBeInTheDocument();
+
+ // Should have made multiple Link calls
+ expect(mockLink).toHaveBeenCalledTimes(3);
+
+ // Verify different routing calls
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.configmaps.configmap',
+ })
+ );
+
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.secrets.secret',
+ })
+ );
+ });
+
+ it('should handle Deployment kind applications', () => {
+ const app: Application = {
+ metadata: { name: 'test-deployment', namespace: 'default' },
+ spec: {
+ selector: {
+ matchLabels: {
+ app: 'test-app',
+ },
+ },
+ template: {
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ env: [
+ {
+ name: 'ENV_VAR',
+ value: 'test-value',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ kind: 'Deployment',
+ apiVersion: 'apps/v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getByText('ENV_VAR')).toBeInTheDocument();
+ expect(screen.getByText('test-value')).toBeInTheDocument();
+ });
+
+ it('should handle missing resource names gracefully', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ env: [
+ {
+ name: 'CONFIG_VAR',
+ valueFrom: {
+ configMapKeyRef: {
+ // name is undefined
+ key: 'config-key',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getAllByText('CONFIG_VAR')).toHaveLength(2); // Appears in name and value columns
+ // Note: config-key is not displayed in UI - the component shows the env var name
+
+ // Should show dash for missing resource name
+ const dashElements = screen.getAllByText('-');
+ expect(dashElements.length).toBeGreaterThan(0);
+ });
+
+ it('should handle containers without environment variables', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ // No env or envFrom
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(
+ screen.getByText('Environment variables, ConfigMaps or Secrets')
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('text-tip')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'This application is not using any environment variable, ConfigMap or Secret.'
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('should handle environment variables without keys', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ env: [
+ {
+ name: '', // Empty name to test this edge case
+ value: 'test-value',
+ },
+ ],
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getByText('test-value')).toBeInTheDocument();
+
+ // Should show dash for missing env var name
+ const dashElements = screen.getAllByText('-');
+ expect(dashElements.length).toBeGreaterThan(0);
+ });
+
+ it('should render multiple containers with different environment variable types', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'container-1',
+ image: 'image-1',
+ env: [
+ {
+ name: 'CONFIG_VAR',
+ valueFrom: {
+ configMapKeyRef: {
+ name: 'shared-config',
+ key: 'config-key',
+ },
+ },
+ },
+ ],
+ },
+ {
+ name: 'container-2',
+ image: 'image-2',
+ env: [
+ {
+ name: 'SECRET_VAR',
+ valueFrom: {
+ secretKeyRef: {
+ name: 'shared-config', // Same name but different type
+ key: 'secret-key',
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('container-1')).toBeInTheDocument();
+ expect(screen.getByText('container-2')).toBeInTheDocument();
+ expect(screen.getAllByText('CONFIG_VAR')).toHaveLength(2); // Appears in name and value columns
+ expect(screen.getAllByText('SECRET_VAR')).toHaveLength(2); // Appears in name and value columns
+ expect(screen.getAllByText('shared-config')).toHaveLength(2);
+
+ // Should have made two Link calls - one for configmap, one for secret
+ expect(mockLink).toHaveBeenCalledTimes(2);
+
+ // Verify configmap routing
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.configmaps.configmap',
+ params: {
+ name: 'shared-config',
+ namespace: 'default',
+ },
+ })
+ );
+
+ // Verify secret routing
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.secrets.secret',
+ params: {
+ name: 'shared-config',
+ namespace: 'default',
+ },
+ })
+ );
+ });
+});
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationEnvVarsTable.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationEnvVarsTable.tsx
index c1b7435f2..bbfefef0d 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationEnvVarsTable.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationEnvVarsTable.tsx
@@ -84,8 +84,8 @@ export function ApplicationEnvVarsTable({ namespace, app }: Props) {
))}
- {!envVar.resourseName && -}
- {envVar.resourseName && (
+ {!envVar.resourceName && -}
+ {envVar.resourceName && (
- {envVar.resourseName}
+ {envVar.resourceName}
)}
@@ -126,7 +126,7 @@ interface ContainerEnvVar {
containerName: string;
isInitContainer: boolean;
type: EnvVarType;
- resourseName: string;
+ resourceName: string;
}
function getApplicationEnvironmentVariables(
@@ -159,7 +159,7 @@ function getApplicationEnvironmentVariables(
containerName: container.name,
isInitContainer: false,
type: envtype,
- resourseName:
+ resourceName:
envVar?.valueFrom?.configMapKeyRef?.name ||
envVar?.valueFrom?.secretKeyRef?.name ||
'',
@@ -170,7 +170,7 @@ function getApplicationEnvironmentVariables(
const containerEnvFroms: ContainerEnvVar[] =
container?.envFrom?.map((envFrom) => ({
name: '',
- resourseName:
+ resourceName:
envFrom?.configMapRef?.name || envFrom?.secretRef?.name || '',
containerName: container.name,
isInitContainer: false,
@@ -196,7 +196,7 @@ function getApplicationEnvironmentVariables(
containerName: container.name,
isInitContainer: true,
type: envtype,
- resourseName:
+ resourceName:
envVar?.valueFrom?.configMapKeyRef?.name ||
envVar?.valueFrom?.secretKeyRef?.name ||
'',
@@ -207,7 +207,7 @@ function getApplicationEnvironmentVariables(
const containerEnvFroms: ContainerEnvVar[] =
container?.envFrom?.map((envFrom) => ({
name: '',
- resourseName:
+ resourceName:
envFrom?.configMapRef?.name || envFrom?.secretRef?.name || '',
containerName: container.name,
isInitContainer: true,
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationVolumeConfigsTable.test.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationVolumeConfigsTable.test.tsx
new file mode 100644
index 000000000..2fce798ea
--- /dev/null
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationVolumeConfigsTable.test.tsx
@@ -0,0 +1,583 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { vi, beforeEach, afterEach } from 'vitest';
+
+import { Application } from '../../types';
+
+import { ApplicationVolumeConfigsTable } from './ApplicationVolumeConfigsTable';
+
+// Mock icon components
+vi.mock('lucide-react', () => ({
+ Asterisk: () => ,
+ Plus: () => ,
+}));
+
+// Mock UI components
+vi.mock('@@/Icon', () => ({
+ Icon: ({
+ icon: IconComponent,
+ ...props
+ }: {
+ icon: React.ComponentType;
+ [key: string]: unknown;
+ }) => ,
+}));
+
+// Mock the Link component to capture routing props
+const mockLink = vi.fn();
+vi.mock('@@/Link', () => ({
+ Link: ({
+ children,
+ to,
+ params,
+ 'data-cy': dataCy,
+ className,
+ }: {
+ children: React.ReactNode;
+ to: string;
+ params: Record;
+ 'data-cy'?: string;
+ className?: string;
+ }) => {
+ mockLink({ children, to, params, 'data-cy': dataCy, className });
+ return (
+
+ {children}
+
+ );
+ },
+}));
+
+describe('ApplicationVolumeConfigsTable', () => {
+ beforeEach(() => {
+ mockLink.mockClear();
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should render nothing when there are no volume configurations', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ const { container } = render(
+
+ );
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it('should render nothing when app is undefined', () => {
+ const { container } = render(
+
+ );
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it('should render volume configurations from configmaps with items', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ volumeMounts: [
+ {
+ name: 'config-volume',
+ mountPath: '/etc/config',
+ },
+ ],
+ },
+ ],
+ volumes: [
+ {
+ name: 'config-volume',
+ configMap: {
+ name: 'test-configmap',
+ items: [
+ {
+ key: 'config-key',
+ path: 'config.yaml',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getByText('/etc/config/config.yaml')).toBeInTheDocument();
+ expect(screen.getByText('config-key')).toBeInTheDocument();
+ expect(screen.getAllByTestId('plus-icon')).toHaveLength(2); // One for value, one for link
+ expect(screen.getByText('test-configmap')).toBeInTheDocument();
+
+ // Verify the Link component was called with correct routing parameters
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.configmaps.configmap',
+ params: {
+ name: 'test-configmap',
+ namespace: 'default',
+ },
+ 'data-cy': 'config-link-test-configmap',
+ })
+ );
+ });
+
+ it('should render volume configurations from secrets with items', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ volumeMounts: [
+ {
+ name: 'secret-volume',
+ mountPath: '/etc/secrets',
+ },
+ ],
+ },
+ ],
+ volumes: [
+ {
+ name: 'secret-volume',
+ secret: {
+ secretName: 'test-secret',
+ items: [
+ {
+ key: 'secret-key',
+ path: 'secret.txt',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getByText('/etc/secrets/secret.txt')).toBeInTheDocument();
+ expect(screen.getByText('secret-key')).toBeInTheDocument();
+ expect(screen.getAllByTestId('plus-icon')).toHaveLength(2); // One for value, one for link
+ expect(screen.getByText('test-secret')).toBeInTheDocument();
+
+ // Verify the Link component was called with correct routing parameters for secret
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.secrets.secret',
+ params: {
+ name: 'test-secret',
+ namespace: 'default',
+ },
+ 'data-cy': 'secret-link-test-secret',
+ })
+ );
+ });
+
+ it('should render init containers with asterisk indicator', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'main-container',
+ image: 'main-image',
+ volumeMounts: [
+ {
+ name: 'config-volume',
+ mountPath: '/etc/config',
+ },
+ ],
+ },
+ ],
+ initContainers: [
+ {
+ name: 'init-container',
+ image: 'init-image',
+ volumeMounts: [
+ {
+ name: 'config-volume',
+ mountPath: '/etc/init-config',
+ },
+ ],
+ },
+ ],
+ volumes: [
+ {
+ name: 'config-volume',
+ configMap: {
+ name: 'shared-config',
+ },
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ // Check main container
+ expect(screen.getByText('main-container')).toBeInTheDocument();
+ expect(screen.getByText('/etc/config')).toBeInTheDocument();
+
+ // Check init container
+ expect(screen.getByText('init-container')).toBeInTheDocument();
+ expect(screen.getByText('/etc/init-config')).toBeInTheDocument();
+ expect(screen.getByTestId('asterisk-icon')).toBeInTheDocument();
+ expect(screen.getByText('init container')).toBeInTheDocument();
+ });
+
+ it('should render secret volume configurations correctly based on type', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ volumeMounts: [
+ {
+ name: 'secret-volume',
+ mountPath: '/etc/secrets',
+ },
+ ],
+ },
+ ],
+ volumes: [
+ {
+ name: 'secret-volume',
+ secret: {
+ secretName: 'test-secret',
+ },
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getByText('test-secret')).toBeInTheDocument();
+
+ // Should route to secret page because type is 'secret'
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.secrets.secret',
+ params: {
+ name: 'test-secret',
+ namespace: 'default',
+ },
+ 'data-cy': 'secret-link-test-secret',
+ })
+ );
+ });
+
+ it('should render configmap volume configurations correctly based on type', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ volumeMounts: [
+ {
+ name: 'config-volume',
+ mountPath: '/etc/config',
+ },
+ ],
+ },
+ ],
+ volumes: [
+ {
+ name: 'config-volume',
+ configMap: {
+ name: 'test-configmap',
+ },
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getByText('test-configmap')).toBeInTheDocument();
+
+ // Should route to configmap page because type is 'configMap'
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.configmaps.configmap',
+ params: {
+ name: 'test-configmap',
+ namespace: 'default',
+ },
+ 'data-cy': 'config-link-test-configmap',
+ })
+ );
+ });
+
+ it('should handle volumes without items (entire volume mount)', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ volumeMounts: [
+ {
+ name: 'config-volume',
+ mountPath: '/etc/config',
+ },
+ ],
+ },
+ ],
+ volumes: [
+ {
+ name: 'config-volume',
+ configMap: {
+ name: 'test-configmap',
+ // No items - entire configmap is mounted
+ },
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getByText('/etc/config')).toBeInTheDocument();
+ expect(screen.getByText('-')).toBeInTheDocument(); // No specific key
+ expect(screen.getByText('test-configmap')).toBeInTheDocument();
+ });
+
+ it('should handle multiple volumes with different types correctly', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ volumeMounts: [
+ {
+ name: 'secret-volume',
+ mountPath: '/etc/secrets',
+ },
+ {
+ name: 'config-volume',
+ mountPath: '/etc/config',
+ },
+ ],
+ },
+ ],
+ volumes: [
+ {
+ name: 'secret-volume',
+ secret: {
+ secretName: 'test-secret',
+ },
+ },
+ {
+ name: 'config-volume',
+ configMap: {
+ name: 'test-configmap',
+ },
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getAllByText('test-container')).toHaveLength(2); // Should appear twice - once for each volume
+ expect(screen.getByText('test-secret')).toBeInTheDocument();
+ expect(screen.getByText('test-configmap')).toBeInTheDocument();
+
+ // Should have made two Link calls - one for secret, one for configmap
+ expect(mockLink).toHaveBeenCalledTimes(2);
+
+ // Verify secret link
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.secrets.secret',
+ params: {
+ name: 'test-secret',
+ namespace: 'default',
+ },
+ 'data-cy': 'secret-link-test-secret',
+ })
+ );
+
+ // Verify configmap link
+ expect(mockLink).toHaveBeenCalledWith(
+ expect.objectContaining({
+ to: 'kubernetes.configmaps.configmap',
+ params: {
+ name: 'test-configmap',
+ namespace: 'default',
+ },
+ 'data-cy': 'config-link-test-configmap',
+ })
+ );
+ });
+
+ it('should handle containers without volume mounts', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ // No volumeMounts
+ },
+ ],
+ volumes: [
+ {
+ name: 'config-volume',
+ configMap: {
+ name: 'test-configmap',
+ },
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ const { container } = render(
+
+ );
+
+ // Should render nothing because there are no matching volume mounts
+ expect(container.firstChild).toBeNull();
+ });
+
+ it('should handle Deployment kind applications', () => {
+ const app: Application = {
+ metadata: { name: 'test-deployment', namespace: 'default' },
+ spec: {
+ selector: {
+ matchLabels: {
+ app: 'test-app',
+ },
+ },
+ template: {
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ volumeMounts: [
+ {
+ name: 'config-volume',
+ mountPath: '/etc/config',
+ },
+ ],
+ },
+ ],
+ volumes: [
+ {
+ name: 'config-volume',
+ configMap: {
+ name: 'test-configmap',
+ },
+ },
+ ],
+ },
+ },
+ },
+ kind: 'Deployment',
+ apiVersion: 'apps/v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getByText('/etc/config')).toBeInTheDocument();
+ expect(screen.getByText('test-configmap')).toBeInTheDocument();
+ });
+
+ it('should handle missing volume config names', () => {
+ const app: Application = {
+ metadata: { name: 'test-pod', namespace: 'default' },
+ spec: {
+ containers: [
+ {
+ name: 'test-container',
+ image: 'test-image',
+ volumeMounts: [
+ {
+ name: 'config-volume',
+ mountPath: '/etc/config',
+ },
+ ],
+ },
+ ],
+ volumes: [
+ {
+ name: 'config-volume',
+ configMap: {
+ // name is undefined
+ },
+ },
+ ],
+ },
+ kind: 'Pod',
+ apiVersion: 'v1',
+ };
+
+ render();
+
+ expect(screen.getByText('test-container')).toBeInTheDocument();
+ expect(screen.getByText('/etc/config')).toBeInTheDocument();
+
+ // Should show dash for missing volume config name
+ const dashElements = screen.getAllByText('-');
+ expect(dashElements.length).toBeGreaterThan(0);
+ });
+});
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationVolumeConfigsTable.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationVolumeConfigsTable.tsx
index b3cff0f19..d3b418386 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationVolumeConfigsTable.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationDetailsWidget/ApplicationVolumeConfigsTable.tsx
@@ -1,15 +1,23 @@
-import { KeyToPath, Pod, Secret } from 'kubernetes-types/core/v1';
+import { KeyToPath, Pod, VolumeMount } from 'kubernetes-types/core/v1';
import { Asterisk, Plus } from 'lucide-react';
-import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
-import { useK8sSecrets } from '@/react/kubernetes/configs/queries/useK8sSecrets';
-
import { Icon } from '@@/Icon';
import { Link } from '@@/Link';
import { Application } from '../../types';
import { applicationIsKind } from '../../utils';
+type VolumeConfigType = 'configMap' | 'secret';
+
+type AppVolumeConfig = {
+ volumeConfigName: string | undefined;
+ containerVolumeMount: VolumeMount | undefined;
+ containerName: string;
+ isInitContainer: boolean;
+ item: KeyToPath;
+ type: VolumeConfigType;
+};
+
type Props = {
namespace: string;
app?: Application;
@@ -18,8 +26,6 @@ type Props = {
export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
const containerVolumeConfigs = getApplicationVolumeConfigs(app);
- const { data: secrets } = useK8sSecrets(useEnvironmentId(), namespace);
-
if (containerVolumeConfigs.length === 0) {
return null;
}
@@ -41,6 +47,7 @@ export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
containerName,
item,
volumeConfigName,
+ type,
},
index
) => (
@@ -76,7 +83,7 @@ export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
{!item.key && '-'}
|
- {isVolumeConfigNameFromSecret(secrets, volumeConfigName) ? (
+ {type === '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) {
+function getApplicationVolumeConfigs(app?: Application): AppVolumeConfig[] {
if (!app) {
return [];
}
@@ -142,6 +142,10 @@ function getApplicationVolumeConfigs(app?: Application) {
const containerVolumeMount = container.volumeMounts?.find(
(volumeMount) => volumeMount.name === volume.name
);
+ const type: VolumeConfigType = volume.configMap
+ ? 'configMap'
+ : 'secret';
+
if (volConfigMapItems.length === 0) {
return [
{
@@ -150,6 +154,7 @@ function getApplicationVolumeConfigs(app?: Application) {
containerName: container.name,
isInitContainer: appInitContainers.includes(container),
item: {} as KeyToPath,
+ type,
},
];
}
@@ -160,6 +165,7 @@ function getApplicationVolumeConfigs(app?: Application) {
containerName: container.name,
isInitContainer: appInitContainers.includes(container),
item,
+ type,
}));
})
// only return the app volumes where the container volumeMounts include the volume name (from map step above)
|