Skip to content

Commit

Permalink
feat(pci.kubernetes): add cluster ETCD component and tests
Browse files Browse the repository at this point in the history
ref: TAPC-23
Signed-off-by: Pierre-Philippe <[email protected]>
  • Loading branch information
Pierre-Philippe committed Nov 15, 2024
1 parent 28c2fbe commit b086a5d
Show file tree
Hide file tree
Showing 15 changed files with 398 additions and 83 deletions.
1 change: 1 addition & 0 deletions packages/manager/apps/pci-kubernetes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"i18next-http-backend": "^2.5.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.1.2",
"react-hook-form": "^7.52.1",
"react-i18next": "^14.1.2",
"react-router-dom": "^6.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"kube_service": "Service",
"kube_service_error": "An error has occurred loading the information: {{message}}",
"kube_service_name": "Name",
"kube_service_cluster_etcd_quota": "Utilisation de l'ETCD",
"kube_service_description_information": "Below, you will find information about your Kubernetes service, and how to access it using the <a class=\"oui-link oui-link_icon\" href=\"{{ url }}\" target=\"_blank\" rel=\"noopener\">kubectl tool.<span class=\"oui-icon oui-icon-external-link\" aria-hidden=\"true\"></span></a>",
"kube_service_description_reset": "You can reset your Kubernetes cluster's configuration at any time.",
"kube_service_offer_beta": "Managed Kubernetes Service (free)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
"kube_service_cluster_network_vrack_default_gateway": "Passerelle par défaut du vRACK (DHCP)",
"kube_service_cluster_network_vrack_customer_gateway": "Passerelle : {{ vRackGatewayIp }}",
"kubernetes_add_private_network": "Configurer un réseau",

"kube_service_network_edit": "Modifier les paramètres du réseau"
"kube_service_network_edit": "Modifier les paramètres du réseau",
"kube_service_cluster_etcd_quota": "Utilisation du quota ETCD",
"kube_service_cluster_etcd_quota_info": "Le quota Etcd représente l'espace alloué à la base de donnée ETCD de votre cluster managé. Veuillez consulter notre guide pour en savoir plus sur le quota ETCD.  <a href=\"{{link}}\" rel=\"noopener\" target=\"_blank\">{{link}}</a>",
"kube_service_etcd_quota_error": "Ce cluster utilise plus de 80% de l'espace ETCD attribué. Consultez cette page pour éviter tout impact sur votre service: <a href=\"{{link}}\" rel=\"noopener\" target=\"_blank\">{{link}}</a>"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, it, vi } from 'vitest';
import * as ApiKubernetesModule from '@/api/data/kubernetes';
import {
useAllKube,
useGetClusterEtcdUsage,
useKubernetesCluster,
useKubes,
useRenameKubernetesCluster,
Expand Down Expand Up @@ -176,3 +177,37 @@ describe('useUpdateKubePolicy', () => {
expect(mockError).not.toHaveBeenCalled();
});
});

describe('useGetClusterEtcdUsage', () => {
afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
});
it('fetches etcd usage successfully', async () => {
const mockData = { usage: 500, quota: 1024 };
vi.spyOn(ApiKubernetesModule, 'getKubeEtcdUsage').mockResolvedValueOnce(
mockData,
);

const { result } = renderHook(
() => useGetClusterEtcdUsage('project-valid', 'kube1'),
{ wrapper },
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual(mockData);
});

it('handles error when fetching etcd usage', async () => {
vi.spyOn(ApiKubernetesModule, 'getKubeEtcdUsage').mockRejectedValueOnce(
new Error('Network Error'),
);

const { result } = renderHook(
() => useGetClusterEtcdUsage('project-error', 'kube1'),
{ wrapper },
);

expect(result.current).toBe(null);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { applyFilters, Filter } from '@ovh-ux/manager-core-api';
import { PaginationState } from '@ovh-ux/manager-react-components';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { TKube } from '@/types';
Expand Down Expand Up @@ -612,17 +612,11 @@ export const useCreateKubernetesCluster = ({

export const useGetClusterEtcdUsage = (projectId, kubeId) => {
const queryKey = ['project', projectId, 'kube', kubeId, 'etcd', 'usage'];
const query = useQuery({
return useSuspenseQuery({
queryKey,
queryFn: async () => {
const data = await getKubeEtcdUsage(projectId, kubeId);
return data;
},
enabled: Boolean(projectId),
suspense: true,
});

return {
...query,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { render, waitFor } from '@testing-library/react';
import * as manager from '@ovh-ux/manager-react-components';
import { vi } from 'vitest';
import { UseSuspenseQueryResult } from '@tanstack/react-query';
import ClusterEtcd from './ClusterETCD.component';
import { wrapper } from '@/wrapperRenders';
import * as useKubernetesModule from '@/api/hooks/useKubernetes';
import { formatBytes, getColorByPercentage } from '@/helpers';

describe('ClusterEtcd', () => {
it('renders progress bar and usage text correctly', async () => {
const mockUsage = 500;
const mockQuota = 1000;
const mockPercentage = (mockUsage / mockQuota) * 100;

vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({
data: { usage: mockUsage, quota: mockQuota },
isPending: false,
} as unknown) as UseSuspenseQueryResult<{ usage: number; quota: number }>);

const { getByText, container } = render(<ClusterEtcd />, { wrapper });

await waitFor(() => {
const progressBar = container.querySelector('osds-progress-bar');
const progressBarColor = getColorByPercentage(mockPercentage);

expect(progressBar).toBeInTheDocument();
expect(progressBar?.getAttribute('color')).toBe(progressBarColor);
expect(progressBar?.getAttribute('value')).toBe(
mockPercentage.toString(),
);
expect(
getByText(`${formatBytes(mockUsage)} / ${formatBytes(mockQuota)}`),
).toBeInTheDocument();
});
});

it('applies correct styles based on progress percentage', async () => {
const mockUsage = 700;
const mockQuota = 1000;
const mockPercentage = (mockUsage / mockQuota) * 100;

vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({
data: { usage: mockUsage, quota: mockQuota },
isPending: false,
} as unknown) as UseSuspenseQueryResult<{ usage: number; quota: number }>);

const { container } = render(<ClusterEtcd />, { wrapper });

await waitFor(() => {
const progressBar = container.querySelector('osds-progress-bar');
const progressBarStyle = getColorByPercentage(mockPercentage);

expect(progressBar).toBeInTheDocument();
expect(progressBar).toHaveProperty('color', progressBarStyle);
});
});

it('renders correct usage and quota information', async () => {
const mockUsage = 300;
const mockQuota = 600;

vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({
data: { usage: mockUsage, quota: mockQuota },
isPending: false,
} as unknown) as UseSuspenseQueryResult<{ usage: number; quota: number }>);

const { getByText } = render(<ClusterEtcd />, { wrapper });

await waitFor(() => {
expect(
getByText(`${formatBytes(mockUsage)} / ${formatBytes(mockQuota)}`),
).toBeInTheDocument();
});
});
it('should call addError only once when percentage is over 80%', async () => {
const mockUsage = 550;
const mockQuota = 600;
const addWarning = vi.fn();
vi.spyOn(manager, 'useNotifications').mockReturnValue({
addWarning,
});

// Mock useGetClusterEtcdUsage to return usage leading to percentage > 80%
vi.spyOn(useKubernetesModule, 'useGetClusterEtcdUsage').mockReturnValue(({
data: { usage: mockUsage, quota: mockQuota },
isPending: false,
} as unknown) as UseSuspenseQueryResult<{ usage: number; quota: number }>);

// Render the component
render(<ClusterEtcd />);
await waitFor(() => expect(addWarning).toHaveBeenCalledTimes(1));
// Check that addWarning is called only once
});
});
Empty file.
Original file line number Diff line number Diff line change
@@ -1,22 +1,83 @@
import { OsdsProgressBar } from '@ovhcloud/ods-components/react';
import { useEffect, useMemo } from 'react';
import {
ODS_TEXT_COLOR_INTENT,
ODS_TEXT_LEVEL,
ODS_TEXT_SIZE,
} from '@ovhcloud/ods-components';
import { Trans, useTranslation } from 'react-i18next';
import { useNotifications } from '@ovh-ux/manager-react-components';
import { OsdsProgressBar, OsdsText } from '@ovhcloud/ods-components/react';
import { useParams } from 'react-router-dom';
import { useGetClusterEtcdUsage } from '@/api/hooks/useKubernetes';
import { formatBytes } from '@/helpers';
import { formatBytes, getColorByPercentage, QUOTA_ERROR_URL } from '@/helpers';

// TODO rajouter le seuil
const getProgressBarStyle = (color: string) => `
progress[value] {
--progress: calc(var(--w) * (attr(value) / 100)); /* Largeur de la progression en fonction du pourcentage */
--color: ${color};
--background: lightgrey; /* Couleur de fond */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
background: var(--background);
}
progress[value]::-webkit-progress-bar {
background: var(--background);
}
progress[value]::-webkit-progress-value {
background: var(--color);
}
progress[value]::-moz-progress-bar {
background: var(--color);
}
`;

function ClusterEtcd() {
const { projectId, kubeId } = useParams();
const {
data: { usage: used, quota: total },
} = useGetClusterEtcdUsage(projectId, kubeId);
const percentage = (used / total) * 100;
const percentage = useMemo(() => (used / total) * 100, []);
const { t } = useTranslation(['service']);
const { addWarning } = useNotifications();

useEffect(() => {
if (percentage > 80) {
addWarning(
<Trans components={{ a: <a></a> }}>
{t('kube_service_etcd_quota_error', {
link: QUOTA_ERROR_URL,
})}
</Trans>,
true,
);
}
}, [percentage]);

useEffect(() => {
const progressBarElement = document.querySelector('osds-progress-bar');
const { shadowRoot } = progressBarElement;
const style = document.createElement('style');
style.textContent = getProgressBarStyle(getColorByPercentage(percentage));
shadowRoot.appendChild(style);
}, []);

return (
<div className="w-full p-3 my-4">
<OsdsProgressBar color="error" value={percentage} max={100} />
<div style={{ textAlign: 'right', marginTop: '4px' }}>
<OsdsProgressBar
color={getColorByPercentage(percentage)}
value={percentage}
max={100}
/>
<OsdsText
size={ODS_TEXT_SIZE._400}
level={ODS_TEXT_LEVEL.body}
color={ODS_TEXT_COLOR_INTENT.text}
className="mt-4 float-right"
>
{formatBytes(used)} / {formatBytes(total)}
</div>
</OsdsText>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { render, screen, waitFor, act } from '@testing-library/react';
import * as manager from '@ovh-ux/manager-react-components';
import { describe, it, expect, vi } from 'vitest';
import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming';
import ClusterInformation from '@/components/service/ClusterInformation.component';
import { wrapper } from '@/wrapperRenders';
import { TKube } from '@/types';
import * as ApiKube from '@/api/data/kubernetes';

const renderClusterInformation = (kubeDetail) =>
render(<ClusterInformation kubeDetail={kubeDetail} />);
render(<ClusterInformation kubeDetail={kubeDetail} />, { wrapper });

describe('ClusterInformation', () => {
const mockData = { usage: 500, quota: 1024 };
vi.spyOn(ApiKube, 'getKubeEtcdUsage').mockResolvedValueOnce(mockData);

const kubeDetail = {
id: '1',
name: 'Cluster1',
Expand Down Expand Up @@ -35,25 +41,41 @@ describe('ClusterInformation', () => {
],
} as TKube;

it('renders cluster information correctly', () => {
it.skip('calls clearNotifications on unmount', async () => {
const { unmount } = renderClusterInformation(kubeDetail);
const mockClearNotifications = vi.fn();

vi.spyOn(manager, 'useNotifications').mockReturnValue({
clearNotifications: mockClearNotifications,
});
expect(mockClearNotifications).not.toHaveBeenCalled();
act(() => unmount());
// not working issue
// https://github.com/testing-library/react-hooks-testing-library/issues/847
expect(mockClearNotifications).toHaveBeenCalledTimes(1);
});

it('renders cluster information correctly', async () => {
renderClusterInformation(kubeDetail);

expect(
screen.getByText(/kube_service_cluster_information/i),
).toBeInTheDocument();
expect(screen.getByText('Cluster1')).toBeInTheDocument();
expect(
screen.getByText('kube_service_cluster_status_READY'),
).toBeInTheDocument();
expect(screen.getByText('1.18')).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(/kube_service_cluster_information/i),
).toBeInTheDocument();
expect(screen.getByText('Cluster1')).toBeInTheDocument();
expect(
screen.getByText('kube_service_cluster_status_READY'),
).toBeInTheDocument();
expect(screen.getByText('1.18')).toBeInTheDocument();

expect(
screen.getByTestId('admission-plugin-chip AlwaysPullImages'),
).toHaveProperty('color', ODS_THEME_COLOR_INTENT.success);
expect(
screen.getByTestId('admission-plugin-chip NodeRestriction'),
).toHaveProperty('color', ODS_THEME_COLOR_INTENT.warning);
expect(screen.getByText('Region1')).toBeInTheDocument();
expect(
screen.getByTestId('admission-plugin-chip AlwaysPullImages'),
).toHaveProperty('color', ODS_THEME_COLOR_INTENT.success);
expect(
screen.getByTestId('admission-plugin-chip NodeRestriction'),
).toHaveProperty('color', ODS_THEME_COLOR_INTENT.warning);
expect(screen.getByText('Region1')).toBeInTheDocument();
});
});

it('renders cluster ID with clipboard component', () => {
Expand Down
Loading

0 comments on commit b086a5d

Please sign in to comment.