mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 13:29:41 +02:00
fix(app/kubernetes): Fix listing of secrets and configmaps with same name [r8s-288] (#897)
This commit is contained in:
parent
383bcc4113
commit
4e4c5ffdb6
4 changed files with 1316 additions and 25 deletions
|
@ -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: () => <span data-cy="asterisk-icon" />,
|
||||||
|
File: () => <span data-cy="file-icon" />,
|
||||||
|
FileCode: () => <span data-cy="file-code-icon" />,
|
||||||
|
Key: () => <span data-cy="key-icon" />,
|
||||||
|
Lock: () => <span data-cy="lock-icon" />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock UI components
|
||||||
|
vi.mock('@@/Icon', () => ({
|
||||||
|
Icon: ({
|
||||||
|
icon: IconComponent,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
icon: React.ComponentType;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}) => <IconComponent {...props} />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@@/Tip/TextTip', () => ({
|
||||||
|
TextTip: ({
|
||||||
|
children,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
color?: string;
|
||||||
|
}) => (
|
||||||
|
<div data-cy="text-tip" data-color={color}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 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<string, string>;
|
||||||
|
'data-cy'?: string;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
mockLink({ children, to, params, 'data-cy': dataCy, className });
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-cy={dataCy}
|
||||||
|
data-testid={dataCy}
|
||||||
|
role="link"
|
||||||
|
data-to={to}
|
||||||
|
data-params={JSON.stringify(params)}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={undefined} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
// 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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationEnvVarsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -84,8 +84,8 @@ export function ApplicationEnvVarsTable({ namespace, app }: Props) {
|
||||||
))}
|
))}
|
||||||
</td>
|
</td>
|
||||||
<td data-cy="k8sAppDetail-configName">
|
<td data-cy="k8sAppDetail-configName">
|
||||||
{!envVar.resourseName && <span>-</span>}
|
{!envVar.resourceName && <span>-</span>}
|
||||||
{envVar.resourseName && (
|
{envVar.resourceName && (
|
||||||
<span>
|
<span>
|
||||||
<Link
|
<Link
|
||||||
to={
|
to={
|
||||||
|
@ -94,17 +94,17 @@ export function ApplicationEnvVarsTable({ namespace, app }: Props) {
|
||||||
: 'kubernetes.secrets.secret'
|
: 'kubernetes.secrets.secret'
|
||||||
}
|
}
|
||||||
params={{
|
params={{
|
||||||
name: envVar.resourseName,
|
name: envVar.resourceName,
|
||||||
namespace,
|
namespace,
|
||||||
}}
|
}}
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
data-cy={`configmap-link-${envVar.resourseName}`}
|
data-cy={`configmap-link-${envVar.resourceName}`}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={envVar.type === 'configMap' ? FileCode : Lock}
|
icon={envVar.type === 'configMap' ? FileCode : Lock}
|
||||||
className="!mr-1"
|
className="!mr-1"
|
||||||
/>
|
/>
|
||||||
{envVar.resourseName}
|
{envVar.resourceName}
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
@ -126,7 +126,7 @@ interface ContainerEnvVar {
|
||||||
containerName: string;
|
containerName: string;
|
||||||
isInitContainer: boolean;
|
isInitContainer: boolean;
|
||||||
type: EnvVarType;
|
type: EnvVarType;
|
||||||
resourseName: string;
|
resourceName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getApplicationEnvironmentVariables(
|
function getApplicationEnvironmentVariables(
|
||||||
|
@ -159,7 +159,7 @@ function getApplicationEnvironmentVariables(
|
||||||
containerName: container.name,
|
containerName: container.name,
|
||||||
isInitContainer: false,
|
isInitContainer: false,
|
||||||
type: envtype,
|
type: envtype,
|
||||||
resourseName:
|
resourceName:
|
||||||
envVar?.valueFrom?.configMapKeyRef?.name ||
|
envVar?.valueFrom?.configMapKeyRef?.name ||
|
||||||
envVar?.valueFrom?.secretKeyRef?.name ||
|
envVar?.valueFrom?.secretKeyRef?.name ||
|
||||||
'',
|
'',
|
||||||
|
@ -170,7 +170,7 @@ function getApplicationEnvironmentVariables(
|
||||||
const containerEnvFroms: ContainerEnvVar[] =
|
const containerEnvFroms: ContainerEnvVar[] =
|
||||||
container?.envFrom?.map((envFrom) => ({
|
container?.envFrom?.map((envFrom) => ({
|
||||||
name: '',
|
name: '',
|
||||||
resourseName:
|
resourceName:
|
||||||
envFrom?.configMapRef?.name || envFrom?.secretRef?.name || '',
|
envFrom?.configMapRef?.name || envFrom?.secretRef?.name || '',
|
||||||
containerName: container.name,
|
containerName: container.name,
|
||||||
isInitContainer: false,
|
isInitContainer: false,
|
||||||
|
@ -196,7 +196,7 @@ function getApplicationEnvironmentVariables(
|
||||||
containerName: container.name,
|
containerName: container.name,
|
||||||
isInitContainer: true,
|
isInitContainer: true,
|
||||||
type: envtype,
|
type: envtype,
|
||||||
resourseName:
|
resourceName:
|
||||||
envVar?.valueFrom?.configMapKeyRef?.name ||
|
envVar?.valueFrom?.configMapKeyRef?.name ||
|
||||||
envVar?.valueFrom?.secretKeyRef?.name ||
|
envVar?.valueFrom?.secretKeyRef?.name ||
|
||||||
'',
|
'',
|
||||||
|
@ -207,7 +207,7 @@ function getApplicationEnvironmentVariables(
|
||||||
const containerEnvFroms: ContainerEnvVar[] =
|
const containerEnvFroms: ContainerEnvVar[] =
|
||||||
container?.envFrom?.map((envFrom) => ({
|
container?.envFrom?.map((envFrom) => ({
|
||||||
name: '',
|
name: '',
|
||||||
resourseName:
|
resourceName:
|
||||||
envFrom?.configMapRef?.name || envFrom?.secretRef?.name || '',
|
envFrom?.configMapRef?.name || envFrom?.secretRef?.name || '',
|
||||||
containerName: container.name,
|
containerName: container.name,
|
||||||
isInitContainer: true,
|
isInitContainer: true,
|
||||||
|
|
|
@ -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: () => <span data-cy="asterisk-icon" />,
|
||||||
|
Plus: () => <span data-cy="plus-icon" />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock UI components
|
||||||
|
vi.mock('@@/Icon', () => ({
|
||||||
|
Icon: ({
|
||||||
|
icon: IconComponent,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
icon: React.ComponentType;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}) => <IconComponent {...props} />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 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<string, string>;
|
||||||
|
'data-cy'?: string;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
mockLink({ children, to, params, 'data-cy': dataCy, className });
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-cy={dataCy}
|
||||||
|
data-testid={dataCy}
|
||||||
|
role="link"
|
||||||
|
data-to={to}
|
||||||
|
data-params={JSON.stringify(params)}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
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(
|
||||||
|
<ApplicationVolumeConfigsTable namespace="default" app={app} />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render nothing when app is undefined', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<ApplicationVolumeConfigsTable namespace="default" app={undefined} />
|
||||||
|
);
|
||||||
|
|
||||||
|
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(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
// 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(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<ApplicationVolumeConfigsTable namespace="default" app={app} />
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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(<ApplicationVolumeConfigsTable namespace="default" app={app} />);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 { Asterisk, Plus } from 'lucide-react';
|
||||||
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
|
||||||
import { useK8sSecrets } from '@/react/kubernetes/configs/queries/useK8sSecrets';
|
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
import { Application } from '../../types';
|
import { Application } from '../../types';
|
||||||
import { applicationIsKind } from '../../utils';
|
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 = {
|
type Props = {
|
||||||
namespace: string;
|
namespace: string;
|
||||||
app?: Application;
|
app?: Application;
|
||||||
|
@ -18,8 +26,6 @@ type Props = {
|
||||||
export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
|
export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
|
||||||
const containerVolumeConfigs = getApplicationVolumeConfigs(app);
|
const containerVolumeConfigs = getApplicationVolumeConfigs(app);
|
||||||
|
|
||||||
const { data: secrets } = useK8sSecrets(useEnvironmentId(), namespace);
|
|
||||||
|
|
||||||
if (containerVolumeConfigs.length === 0) {
|
if (containerVolumeConfigs.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -41,6 +47,7 @@ export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
|
||||||
containerName,
|
containerName,
|
||||||
item,
|
item,
|
||||||
volumeConfigName,
|
volumeConfigName,
|
||||||
|
type,
|
||||||
},
|
},
|
||||||
index
|
index
|
||||||
) => (
|
) => (
|
||||||
|
@ -76,7 +83,7 @@ export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
|
||||||
{!item.key && '-'}
|
{!item.key && '-'}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{isVolumeConfigNameFromSecret(secrets, volumeConfigName) ? (
|
{type === 'secret' ? (
|
||||||
<Link
|
<Link
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
to="kubernetes.secrets.secret"
|
to="kubernetes.secrets.secret"
|
||||||
|
@ -107,15 +114,8 @@ export function ApplicationVolumeConfigsTable({ namespace, app }: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// 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) {
|
if (!app) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -142,6 +142,10 @@ function getApplicationVolumeConfigs(app?: Application) {
|
||||||
const containerVolumeMount = container.volumeMounts?.find(
|
const containerVolumeMount = container.volumeMounts?.find(
|
||||||
(volumeMount) => volumeMount.name === volume.name
|
(volumeMount) => volumeMount.name === volume.name
|
||||||
);
|
);
|
||||||
|
const type: VolumeConfigType = volume.configMap
|
||||||
|
? 'configMap'
|
||||||
|
: 'secret';
|
||||||
|
|
||||||
if (volConfigMapItems.length === 0) {
|
if (volConfigMapItems.length === 0) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -150,6 +154,7 @@ function getApplicationVolumeConfigs(app?: Application) {
|
||||||
containerName: container.name,
|
containerName: container.name,
|
||||||
isInitContainer: appInitContainers.includes(container),
|
isInitContainer: appInitContainers.includes(container),
|
||||||
item: {} as KeyToPath,
|
item: {} as KeyToPath,
|
||||||
|
type,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -160,6 +165,7 @@ function getApplicationVolumeConfigs(app?: Application) {
|
||||||
containerName: container.name,
|
containerName: container.name,
|
||||||
isInitContainer: appInitContainers.includes(container),
|
isInitContainer: appInitContainers.includes(container),
|
||||||
item,
|
item,
|
||||||
|
type,
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
// only return the app volumes where the container volumeMounts include the volume name (from map step above)
|
// only return the app volumes where the container volumeMounts include the volume name (from map step above)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue