Skip to content

Commit

Permalink
feat: multi-arch image features
Browse files Browse the repository at this point in the history
Signed-off-by: Raul Kele <[email protected]>
  • Loading branch information
raulkele committed Mar 9, 2023
1 parent 4db0c2e commit 9029b97
Show file tree
Hide file tree
Showing 13 changed files with 281 additions and 110 deletions.
81 changes: 53 additions & 28 deletions src/__tests__/RepoPage/Tags.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,49 @@ jest.mock('react-router-dom', () => ({

const mockedTagsData = [
{
Digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
Tag: 'latest',
LastUpdated: '2022-07-19T18:06:18.818788283Z',
Vendor: 'test1',
Size: '569130088',
Platform: {
Os: 'linux',
Arch: 'amd64'
}
tag: 'latest',
vendor: 'test1',
manifests: [
{
lastUpdated: '2022-07-19T18:06:18.818788283Z',
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
size: '569130088',
platform: {
Os: 'linux',
Arch: 'amd64'
}
}
]
},
{
Digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
Tag: 'bullseye',
LastUpdated: '2022-07-19T18:06:18.818788283Z',
Vendor: 'test1',
Size: '569130088',
Platform: {
Os: 'linux',
Arch: 'amd64'
}
tag: 'bullseye',
vendor: 'test1',
manifests: [
{
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
lastUpdated: '2022-07-19T18:06:18.818788283Z',
size: '569130088',
platform: {
Os: 'linux',
Arch: 'amd64'
}
}
]
},
{
Digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
Tag: '1.5.2',
LastUpdated: '2022-07-19T18:06:18.818788283Z',
Vendor: 'test1',
Size: '569130088',
Platform: {
Os: 'linux',
Arch: 'amd64'
}
tag: '1.5.2',
vendor: 'test1',
manifests: [
{
lastUpdated: '2022-07-19T18:06:18.818788283Z',
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
size: '569130088',
platform: {
Os: 'linux',
Arch: 'amd64'
}
}
]
}
];

Expand All @@ -60,7 +72,20 @@ describe('Tags component', () => {
const tagLink = await screen.findByText('latest');
fireEvent.click(tagLink);
await waitFor(() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith('tag/latest');
expect(mockedUsedNavigate).toHaveBeenCalledWith('tag/latest', { state: { digest: null } });
});
});

it('should navigate to specific manifest when clicking the digest', async () => {
render(<Tags tags={mockedTagsData} />);
const openBtn = screen.getAllByText(/digest/i);
await fireEvent.click(openBtn[0]);
const tagLink = await screen.findByText(/sha256:adca4/i);
fireEvent.click(tagLink);
await waitFor(() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith('tag/latest', {
state: { digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559' }
});
});
});

Expand Down
119 changes: 117 additions & 2 deletions src/__tests__/TagPage/TagDetails.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event';
import { api } from 'api';
import TagDetails from 'components/Tag/TagDetails';
import MockThemeProvier from '__mocks__/MockThemeProvider';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';

const TagDetailsThemeWrapper = () => {
return (
Expand Down Expand Up @@ -72,6 +72,102 @@ const mockImage = {
}
}
]
},
{
Digest: 'sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf45etertdfg973e29',
LastUpdated: '2020-12-08T00:22:52.526672082Z',
Size: '75183423',
ConfigDigest: 'sha256:8dd57e171a61368ffcfde38045ddb6ed74a32950c271c1da93eaddfb66a77e78',
Platform: {
Os: 'windows',
Arch: 'amd64'
},
History: [
{
Layer: {
Size: '75181999',
Digest: 'sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621',
Score: null
},
HistoryDescription: {
Created: '2020-12-08T00:22:52.526672082Z',
CreatedBy:
'/bin/sh -c #(nop) ADD file:bd7a2aed6ede423b719ceb2f723e4ecdfa662b28639c8429731c878e86fb138b in / ',
Author: '',
Comment: '',
EmptyLayer: false
}
},
{
Layer: null,
HistoryDescription: {
Created: '2020-12-08T00:22:52.895811646Z',
CreatedBy:
'/bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20201204',
Author: '',
Comment: '',
EmptyLayer: true
}
},
{
Layer: null,
HistoryDescription: {
Created: '2020-12-08T00:22:53.076477777Z',
CreatedBy: '/bin/sh -c #(nop) CMD ["/bin/bash"]',
Author: '',
Comment: '',
EmptyLayer: true
}
}
]
},
{
Digest: 'sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e25',
LastUpdated: '2020-12-08T00:22:52.526672082Z',
Size: '75183423',
ConfigDigest: 'sha256:8dd57e171a61368ffcfde38045ddb6ed74a32950c271c1da93eaddfb66a77e78',
Platform: {
Os: 'linux',
Arch: 'arm'
},
History: [
{
Layer: {
Size: '75181999',
Digest: 'sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621',
Score: null
},
HistoryDescription: {
Created: '2020-12-08T00:22:52.526672082Z',
CreatedBy:
'/bin/sh -c #(nop) ADD file:bd7a2aed6ede423b719ceb2f723e4ecdfa662b28639c8429731c878e86fb138b in / ',
Author: '',
Comment: '',
EmptyLayer: false
}
},
{
Layer: null,
HistoryDescription: {
Created: '2020-12-08T00:22:52.895811646Z',
CreatedBy:
'/bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20201204',
Author: '',
Comment: '',
EmptyLayer: true
}
},
{
Layer: null,
HistoryDescription: {
Created: '2020-12-08T00:22:53.076477777Z',
CreatedBy: '/bin/sh -c #(nop) CMD ["/bin/bash"]',
Author: '',
Comment: '',
EmptyLayer: true
}
}
]
}
],
Vulnerabilities: {
Expand Down Expand Up @@ -285,7 +381,8 @@ jest.mock('react-router-dom', () => ({
useParams: () => {
return { name: 'test', tag: '1.0.1' };
},
useNavigate: () => mockUseNavigate
useNavigate: () => mockUseNavigate,
useLocation: jest.fn()
}));

jest.mock('../../host', () => ({
Expand Down Expand Up @@ -328,6 +425,24 @@ describe('Tags details', () => {
await waitFor(() => expect(error).toBeCalledTimes(1));
});

it('should show the data of the different manifests when switching between them', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImage } });
render(<TagDetailsThemeWrapper />);
const manifestSelect = await screen.findByText(/linux\/amd64/i);
await userEvent.click(manifestSelect);
await userEvent.click(await screen.findByText(/windows\/amd64/i));
expect(await screen.findByText(/windows\/amd64/i)).toBeInTheDocument();
});

it('should preselect a manifest if data is received', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImage } });
useLocation.mockImplementation(() => ({
state: { digest: 'sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e25' }
}));
render(<TagDetailsThemeWrapper />);
expect(await screen.findByText(/linux\/arm/i)).toBeInTheDocument();
});

it('should redirect to homepage if it receives invalid data', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: null, errors: ['testerror'] } });
render(<TagDetailsThemeWrapper />);
Expand Down
2 changes: 1 addition & 1 deletion src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const endpoints = {
(pageNumber - 1) * pageSize
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned Documentation Vendor Labels} DownloadCount}}}`,
detailedRepoInfo: (name) =>
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch}} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor Size } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName IsSigned Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Title Documentation DownloadCount Source Description Licenses}}}}`,
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName IsSigned Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Title Documentation DownloadCount Source Description Licenses}}}}`,
detailedImageInfo: (name, tag) =>
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned Vulnerabilities {MaxSeverity Count} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }) =>
Expand Down
7 changes: 5 additions & 2 deletions src/components/Header/SearchSuggestion.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { api, endpoints } from 'api';
import { host } from 'host';
import { mapToImage, mapToRepo } from 'utilities/objectModels';
import { createSearchParams, useNavigate, useSearchParams } from 'react-router-dom';
import { createSearchParams, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { debounce, isEmpty } from 'lodash';
import { useCombobox } from 'downshift';
import { HEADER_SEARCH_PAGE_SIZE } from 'utilities/paginationConstants';
Expand Down Expand Up @@ -104,6 +104,7 @@ function SearchSuggestion() {
const [isLoading, setIsLoading] = useState(false);
const [isFailedSearch, setIsFailedSearch] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const abortController = useMemo(() => new AbortController(), []);

const classes = useStyles();
Expand Down Expand Up @@ -180,7 +181,9 @@ function SearchSuggestion() {
};

const searchCall = (value) => {
setQueryParams((prevState) => createSearchParams({ ...prevState, search: searchQuery }));
if (location.pathname?.includes('explore')) {
setQueryParams((prevState) => createSearchParams({ ...prevState, search: searchQuery }));
}
if (value !== '') {
// if search term inclused the ':' character, search for images, if not, search repos
if (value?.includes(':')) {
Expand Down
22 changes: 7 additions & 15 deletions src/components/Repo/RepoDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { TabContext, TabList, TabPanel } from '@mui/lab';

import RepoDetailsMetadata from './RepoDetailsMetadata';
import Loading from '../Shared/Loading';
import { isEmpty } from 'lodash';
import { isEmpty, uniq } from 'lodash';
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
import { mapToRepoFromRepoInfo } from 'utilities/objectModels';

Expand Down Expand Up @@ -164,6 +164,7 @@ function RepoDetails() {
const classes = useStyles();

useEffect(() => {
setIsLoading(true);
api
.get(`${host()}${endpoints.detailedRepoInfo(name)}`, abortController.signal)
.then((response) => {
Expand Down Expand Up @@ -197,22 +198,13 @@ function RepoDetails() {

const platformChips = () => {
const platforms = repoDetailData?.platforms || [];
const filteredPlatforms = platforms?.flatMap((platform) => [platform.Os, platform.Arch]);

return platforms.map((platform, index) => (
<Stack key={`stack${platform?.Os}${platform?.Arch}`} alignItems="center" direction="row" spacing={2}>
return uniq(filteredPlatforms).map((platform, index) => (
<Stack key={`stack${platform}`} alignItems="center" direction="row" spacing={2}>
<Chip
key={`${name}${platform?.Os}${index}`}
label={platform?.Os}
onClick={handlePlatformChipClick}
sx={{
backgroundColor: '#E0E5EB',
color: '#52637A',
fontSize: '0.8125rem'
}}
/>
<Chip
key={`${name}${platform?.Arch}${index}`}
label={platform?.Arch}
key={`${name}${platform}${index}`}
label={platform}
onClick={handlePlatformChipClick}
sx={{
backgroundColor: '#E0E5EB',
Expand Down
16 changes: 6 additions & 10 deletions src/components/Repo/Tabs/Tags.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// react global
import React, { useState } from 'react';

import { head } from 'lodash';

// components
import Typography from '@mui/material/Typography';
import { Card, CardContent, Divider, Stack, InputBase, FormControl, Select, InputLabel, MenuItem } from '@mui/material';
Expand Down Expand Up @@ -77,7 +75,7 @@ export default function Tags(props) {
const [sortFilter, setSortFilter] = useState(tagsSortByCriteria.updateTimeDesc.value);
const renderTags = (tags) => {
const selectedSort = Object.values(tagsSortByCriteria).find((sc) => sc.value === sortFilter);
const filteredTags = tags.filter((t) => t.Tag?.includes(tagsFilter));
const filteredTags = tags.filter((t) => t.tag?.includes(tagsFilter));
if (selectedSort) {
filteredTags.sort(selectedSort.func);
}
Expand All @@ -86,13 +84,11 @@ export default function Tags(props) {
filteredTags.map((tag) => {
return (
<TagCard
key={tag.Tag}
tag={tag.Tag}
lastUpdated={tag.LastUpdated}
digest={head(tag.Manifests)?.Digest}
vendor={tag.Vendor}
size={tag.Size}
platform={head(tag.Manifests)?.Platform}
key={tag.tag}
tag={tag.tag}
lastUpdated={tag.lastUpdated}
vendor={tag.vendor}
manifests={tag.manifests}
/>
);
})
Expand Down
Loading

0 comments on commit 9029b97

Please sign in to comment.