Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(quay): fix infinite progress bar when scan is unsupported #1031

Merged
merged 1 commit into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,28 @@ describe('QuayRepository', () => {
expect(queryByText(/Quay repository/i)).toBeInTheDocument();
expect(queryByTestId('quay-repo-security-scan-progress')).not.toBeNull();
});

it('should show table if loaded and data is present but shows unsupported if security scan is not supported', () => {
(useTags as jest.Mock).mockReturnValue({
loading: false,
data: [
{
name: 'latest',
manifest_digest: undefined,
securityStatus: 'unsupported',
size: null,
last_modified: 'Wed, 15 Mar 2023 18:22:18 -0000',
},
],
});
const { queryByTestId, queryByText } = render(
<BrowserRouter>
<QuayRepository />
</BrowserRouter>,
);
expect(queryByTestId('quay-repo-table')).not.toBeNull();
expect(queryByTestId('quay-repo-table-empty')).toBeNull();
expect(queryByText(/Quay repository/i)).toBeInTheDocument();
expect(queryByTestId('quay-repo-security-scan-unsupported')).not.toBeNull();
});
});
14 changes: 13 additions & 1 deletion plugins/quay/src/components/QuayRepository/tableHeading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';

import { Link, Progress, TableColumn } from '@backstage/core-components';

import { Tooltip } from '@material-ui/core';
rohitkrai03 marked this conversation as resolved.
Show resolved Hide resolved
import makeStyles from '@material-ui/core/styles/makeStyles';

import type { Layer } from '../../types';
Expand Down Expand Up @@ -41,13 +42,24 @@ export const columns: TableColumn[] = [
title: 'Security Scan',
field: 'securityScan',
render: (rowData: any): React.ReactNode => {
if (!rowData.securityDetails) {
if (!rowData.securityStatus && !rowData.securityDetails) {
return (
<span data-testid="quay-repo-security-scan-progress">
<Progress />
</span>
);
}

if (rowData.securityStatus === 'unsupported') {
return (
<Tooltip title="The manifest for this tag has an operating system or package manager unsupported by Quay Security Scanner">
<span data-testid="quay-repo-security-scan-unsupported">
Unsupported
</span>
</Tooltip>
);
}

const tagManifest = rowData.manifest_digest_raw;
const retStr = vulnerabilitySummary(rowData.securityDetails as Layer);
return <Link to={`tag/${tagManifest}`}>{retStr}</Link>;
Expand Down
22 changes: 16 additions & 6 deletions plugins/quay/src/hooks/quay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export const useTags = (organization: string, repository: string) => {
const [tagManifestLayers, setTagManifestLayers] = React.useState<
Record<string, Layer>
>({});
const [tagManifestStatuses, setTagManifestStatuses] = React.useState<
Record<string, string>
>({});
const localClasses = useLocalStyles();

const fetchSecurityDetails = async (tag: Tag) => {
Expand All @@ -46,13 +49,19 @@ export const useTags = (organization: string, repository: string) => {
tagsResponse.tags.map(async tag => {
const securityDetails = await fetchSecurityDetails(tag);
const securityData = securityDetails.data;
if (!securityData) {
return;
}
setTagManifestLayers(prevState => ({
const securityStatus = securityDetails.status;

setTagManifestStatuses(prevState => ({
...prevState,
[tag.manifest_digest]: securityData.Layer,
[tag.manifest_digest]: securityStatus,
}));

if (securityData) {
setTagManifestLayers(prevState => ({
...prevState,
[tag.manifest_digest]: securityData.Layer,
}));
}
}),
);
setTags(prevTags => [...prevTags, ...tagsResponse.tags]);
Expand All @@ -76,6 +85,7 @@ export const useTags = (organization: string, repository: string) => {
),
expiration: tag.expiration,
securityDetails: tagManifestLayers[tag.manifest_digest],
securityStatus: tagManifestStatuses[tag.manifest_digest],
manifest_digest_raw: tag.manifest_digest,
// is_manifest_list: tag.is_manifest_list,
// reversion: tag.reversion,
Expand All @@ -84,7 +94,7 @@ export const useTags = (organization: string, repository: string) => {
// manifest_list: tag.manifest_list,
};
});
}, [tags, tagManifestLayers, localClasses.chip]);
}, [tags, localClasses.chip, tagManifestLayers, tagManifestStatuses]);

return { loading, data };
};
Expand Down
38 changes: 38 additions & 0 deletions plugins/quay/src/hooks/useTags.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useApi } from '@backstage/core-plugin-api';

import { waitFor } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';

Expand All @@ -21,4 +23,40 @@ describe('useTags', () => {
expect(result.current.data).toHaveLength(1);
});
});

it('should return security status for tags', async () => {
(useApi as jest.Mock).mockReturnValue({
getSecurityDetails: jest
.fn()
.mockReturnValue({ data: null, status: 'unsupported' }),
getTags: jest.fn().mockReturnValue({
tags: [{ name: 'tag1', manifest_digest: 'manifestDigest' }],
}),
});
const { result } = renderHook(() => useTags('foo', 'bar'));
await waitFor(() => {
expect(result.current.loading).toBeFalsy();
expect(result.current.data).toHaveLength(1);
expect(result.current.data[0].securityStatus).toBe('unsupported');
expect(result.current.data[0].securityDetails).toBeUndefined();
});
});

it('should return tag layers as security details for tags', async () => {
(useApi as jest.Mock).mockReturnValue({
getSecurityDetails: jest
.fn()
.mockReturnValue({ data: { Layer: {} }, status: 'scanned' }),
getTags: jest.fn().mockReturnValue({
tags: [{ name: 'tag1', manifest_digest: 'manifestDigest' }],
}),
});
const { result } = renderHook(() => useTags('foo', 'bar'));
await waitFor(() => {
expect(result.current.loading).toBeFalsy();
expect(result.current.data).toHaveLength(1);
expect(result.current.data[0].securityStatus).toBe('scanned');
expect(result.current.data[0].securityDetails).toEqual({});
});
});
});