3 Commits

Author SHA1 Message Date
2f94cc30ae patch: referrers from image query
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-03-22 14:12:15 +02:00
ddf1d9224b feat: cve list filtering
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-03-21 12:51:16 +02:00
7471fb58a8 ci(tests): some updates to the scripts to make them work better on macos (#324)
Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-03-20 16:51:50 +02:00
10 changed files with 247 additions and 131 deletions

View File

@ -1,12 +1,9 @@
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { api } from 'api';
import ReferredBy from 'components/Tag/Tabs/ReferredBy'; import ReferredBy from 'components/Tag/Tabs/ReferredBy';
import React from 'react'; import React from 'react';
const mockReferrersList = { const mockReferrersList = [
data: {
Referrers: [
{ {
MediaType: 'application/vnd.oci.artifact.manifest.v1+json', MediaType: 'application/vnd.oci.artifact.manifest.v1+json',
ArtifactType: 'application/vnd.example.icecream.v1', ArtifactType: 'application/vnd.example.icecream.v1',
@ -39,9 +36,7 @@ const mockReferrersList = {
} }
] ]
} }
] ];
}
};
// useNavigate mock // useNavigate mock
const mockedUsedNavigate = jest.fn(); const mockedUsedNavigate = jest.fn();
@ -57,27 +52,17 @@ afterEach(() => {
describe('Referred by tab', () => { describe('Referred by tab', () => {
it('should render referrers if there are any', async () => { it('should render referrers if there are any', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockReferrersList }); render(<ReferredBy referrers={mockReferrersList} />);
render(<ReferredBy repoName="golang" digest="test" />);
expect(await screen.findAllByText('Media type: application/vnd.oci.artifact.manifest.v1+json')).toHaveLength(2); expect(await screen.findAllByText('Media type: application/vnd.oci.artifact.manifest.v1+json')).toHaveLength(2);
}); });
it("renders no referrers if there aren't any", async () => { it("renders no referrers if there aren't any", async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: { Referrers: [] } } }); render(<ReferredBy referrers={[]} />);
render(<ReferredBy repoName="golang" digest="test" />);
expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument(); expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument();
}); });
it('should log an error if the request fails', async () => {
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<ReferredBy repoName="golang" digest="test" />);
await waitFor(() => expect(error).toBeCalledTimes(1));
});
it('should display the digest when clicking the dropdowns', async () => { it('should display the digest when clicking the dropdowns', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockReferrersList }); render(<ReferredBy referrers={mockReferrersList} />);
render(<ReferredBy repoName="golang" digest="test" />);
const firstDigest = (await screen.findAllByText(/digest/i))[0]; const firstDigest = (await screen.findAllByText(/digest/i))[0];
expect(firstDigest).toBeInTheDocument(); expect(firstDigest).toBeInTheDocument();
await userEvent.click(firstDigest); await userEvent.click(firstDigest);
@ -91,8 +76,7 @@ describe('Referred by tab', () => {
}); });
it('should display the annotations when clicking the dropdown', async () => { it('should display the annotations when clicking the dropdown', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockReferrersList }); render(<ReferredBy referrers={mockReferrersList} />);
render(<ReferredBy repoName="golang" digest="test" />);
const firstAnnotations = (await screen.findAllByText(/ANNOTATIONS/i))[0]; const firstAnnotations = (await screen.findAllByText(/ANNOTATIONS/i))[0];
expect(firstAnnotations).toBeInTheDocument(); expect(firstAnnotations).toBeInTheDocument();
await userEvent.click(firstAnnotations); await userEvent.click(firstAnnotations);

View File

@ -1,4 +1,5 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { api } from 'api'; import { api } from 'api';
import VulnerabilitiesDetails from 'components/Tag/Tabs/VulnerabilitiesDetails'; import VulnerabilitiesDetails from 'components/Tag/Tabs/VulnerabilitiesDetails';
import React from 'react'; import React from 'react';
@ -432,6 +433,14 @@ const mockCVEList = {
} }
}; };
const mockCVEListFiltered = {
CVEListForImage: {
Tag: '',
Page: { ItemCount: 20, TotalCount: 20 },
CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => e.Id.includes('2022'))
}
};
const mockCVEFixed = { const mockCVEFixed = {
pageOne: { pageOne: {
ImageListWithCVEFixed: { ImageListWithCVEFixed: {
@ -488,6 +497,16 @@ describe('Vulnerabilties page', () => {
await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20)); await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20));
}); });
it('sends filtered query if user types in the search bar', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
render(<StateVulnerabilitiesWrapper />);
const cveSearchInput = screen.getByPlaceholderText(/search for/i);
jest.spyOn(api, 'get').mockRejectedValueOnce({ status: 200, data: { data: mockCVEListFiltered } });
await userEvent.type(cveSearchInput, '2022');
expect((await screen.queryAllByText(/2023/i).length) === 0);
expect((await screen.findAllByText(/2022/i)).length === 6);
});
it('renders no vulnerabilities if there are not any', async () => { it('renders no vulnerabilities if there are not any', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ jest.spyOn(api, 'get').mockResolvedValue({
status: 200, status: 200,

View File

@ -78,11 +78,16 @@ const endpoints = {
detailedRepoInfo: (name) => detailedRepoInfo: (name) =>
`/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}}}}`, `/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) => 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 }}`, `/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }) => vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '') => {
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${ let query = `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
(pageNumber - 1) * pageSize (pageNumber - 1) * pageSize
}}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`, }}`;
if (!isEmpty(searchTerm)) {
query += `, searchedCVE: "${searchTerm}"`;
}
return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`;
},
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }) => imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }) =>
`/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}", requestedPage: {limit:${pageSize} offset:${ `/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}", requestedPage: {limit:${pageSize} offset:${
(pageNumber - 1) * pageSize (pageNumber - 1) * pageSize

View File

@ -269,7 +269,7 @@ function Explore({ searchInputValue }) {
if (!isLoading && !isEndOfList) { if (!isLoading && !isEndOfList) {
return <div ref={listBottom} />; return <div ref={listBottom} />;
} }
return ''; return;
}; };
return ( return (

View File

@ -1,11 +1,9 @@
import React, { useEffect, useState, useMemo } from 'react'; import React, { useEffect, useState } from 'react';
import { makeStyles } from '@mui/styles'; import { makeStyles } from '@mui/styles';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { Divider, Typography, Stack } from '@mui/material'; import { Divider, Typography, Stack } from '@mui/material';
import ReferrerCard from '../../Shared/ReferrerCard'; import ReferrerCard from '../../Shared/ReferrerCard';
import Loading from '../../Shared/Loading'; import Loading from '../../Shared/Loading';
import { api, endpoints } from 'api';
import { host } from '../../../host';
import { mapReferrer } from 'utilities/objectModels'; import { mapReferrer } from 'utilities/objectModels';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
@ -17,36 +15,24 @@ const useStyles = makeStyles(() => ({
})); }));
function ReferredBy(props) { function ReferredBy(props) {
const { repoName, digest } = props; const { referrers } = props;
const [referrers, setReferrers] = useState([]); const [referrersData, setReferrersData] = useState([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const classes = useStyles(); const classes = useStyles();
const abortController = useMemo(() => new AbortController(), []);
useEffect(() => { useEffect(() => {
api if (!isEmpty(referrers)) {
.get(`${host()}${endpoints.referrers({ repo: repoName, digest })}`, abortController.signal) const mappedReferrersData = referrers.map((referrer) => mapReferrer(referrer));
.then((response) => { setReferrersData(mappedReferrersData);
if (response.data && response.data.data) { } else {
let referrersData = response.data.data.Referrers?.map((referrer) => mapReferrer(referrer)); setReferrersData([]);
if (!isEmpty(referrersData)) {
setReferrers(referrersData);
}
} }
setIsLoading(false); setIsLoading(false);
})
.catch((e) => {
console.error(e);
setIsLoading(false);
});
return () => {
abortController.abort();
};
}, []); }, []);
const renderReferrers = () => { const renderReferrers = () => {
return !isEmpty(referrers) ? ( return !isEmpty(referrersData) ? (
referrers.map((referrer, index) => { referrersData.map((referrer, index) => {
return ( return (
<ReferrerCard <ReferrerCard
key={index} key={index}
@ -63,16 +49,6 @@ function ReferredBy(props) {
); );
}; };
const renderListBottom = () => {
if (isLoading) {
return <Loading />;
}
if (!isLoading) {
return <div />;
}
return;
};
return ( return (
<div data-testid="referred-by-container"> <div data-testid="referred-by-container">
<Typography <Typography
@ -95,8 +71,7 @@ function ReferredBy(props) {
/> />
<Stack direction="column" spacing={2}> <Stack direction="column" spacing={2}>
<Stack direction="column" spacing={2}> <Stack direction="column" spacing={2}>
{renderReferrers()} {isLoading ? <Loading /> : renderReferrers()}
{renderListBottom()}
</Stack> </Stack>
</Stack> </Stack>
</div> </div>

View File

@ -5,16 +5,17 @@ import { api, endpoints } from '../../../api';
// components // components
import Collapse from '@mui/material/Collapse'; import Collapse from '@mui/material/Collapse';
import { Box, Card, CardContent, Divider, Stack, Typography } from '@mui/material'; import { Box, Card, CardContent, Divider, Stack, Typography, InputBase } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles'; import makeStyles from '@mui/styles/makeStyles';
import { host } from '../../../host'; import { host } from '../../../host';
import { isEmpty } from 'lodash'; import { debounce, isEmpty } from 'lodash';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Loading from '../../Shared/Loading'; import Loading from '../../Shared/Loading';
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material'; import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
import { VulnerabilityChipCheck } from 'utilities/vulnerabilityAndSignatureCheck'; import { VulnerabilityChipCheck } from 'utilities/vulnerabilityAndSignatureCheck';
import { mapCVEInfo } from 'utilities/objectModels'; import { mapCVEInfo } from 'utilities/objectModels';
import { CVE_FIXEDIN_PAGE_SIZE, EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants'; import { CVE_FIXEDIN_PAGE_SIZE, EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
import SearchIcon from '@mui/icons-material/Search';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
card: { card: {
@ -81,6 +82,25 @@ const useStyles = makeStyles(() => ({
fontWeight: '600', fontWeight: '600',
cursor: 'pointer', cursor: 'pointer',
textAlign: 'center' textAlign: 'center'
},
search: {
position: 'relative',
minWidth: '100%',
flexDirection: 'row',
marginBottom: '1.7rem',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
border: '0.125rem solid #E7E7E7',
borderRadius: '1rem',
zIndex: 1155
},
searchIcon: {
color: '#52637A',
paddingRight: '3%'
},
input: {
color: '#464141',
marginLeft: 1,
width: '90%'
} }
})); }));
@ -236,27 +256,27 @@ function VulnerabilitiesDetails(props) {
const { name, tag } = props; const { name, tag } = props;
// pagination props // pagination props
const [cveFilter, setCveFilter] = useState('');
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1);
const [isEndOfList, setIsEndOfList] = useState(false); const [isEndOfList, setIsEndOfList] = useState(false);
const listBottom = useRef(null); const listBottom = useRef(null);
const getPaginatedCVEs = () => { const getPaginatedCVEs = () => {
setIsLoading(true);
api api
.get( .get(
`${host()}${endpoints.vulnerabilitiesForRepo(`${name}:${tag}`, { pageNumber, pageSize: EXPLORE_PAGE_SIZE })}`, `${host()}${endpoints.vulnerabilitiesForRepo(
`${name}:${tag}`,
{ pageNumber, pageSize: EXPLORE_PAGE_SIZE },
cveFilter
)}`,
abortController.signal abortController.signal
) )
.then((response) => { .then((response) => {
if (response.data && response.data.data) { if (response.data && response.data.data) {
let cveInfo = response.data.data.CVEListForImage?.CVEList; let cveInfo = response.data.data.CVEListForImage?.CVEList;
let cveListData = mapCVEInfo(cveInfo); let cveListData = mapCVEInfo(cveInfo);
const newCVEList = [...cveData, ...cveListData]; setCveData((previousState) => (pageNumber === 1 ? cveListData : [...previousState, ...cveListData]));
setCveData(newCVEList); setIsEndOfList(response.data.data.CVEListForImage.Page?.ItemCount < EXPLORE_PAGE_SIZE);
setIsEndOfList(
response.data.data.CVEListForImage.Page?.ItemCount < EXPLORE_PAGE_SIZE ||
newCVEList.length >= response.data.data.CVEListForImage?.Page?.TotalCount
);
} else if (response.data.errors) { } else if (response.data.errors) {
setIsEndOfList(true); setIsEndOfList(true);
} }
@ -270,11 +290,25 @@ function VulnerabilitiesDetails(props) {
}); });
}; };
const resetPagination = () => {
setIsLoading(true);
setIsEndOfList(false);
if (pageNumber !== 1) {
setPageNumber(1);
} else {
getPaginatedCVEs();
}
};
const handleCveFilterChange = (e) => {
const { value } = e.target;
setCveFilter(value);
};
const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300));
useEffect(() => { useEffect(() => {
getPaginatedCVEs(); getPaginatedCVEs();
return () => {
abortController.abort();
};
}, [pageNumber]); }, [pageNumber]);
// setup intersection obeserver for infinite scroll // setup intersection obeserver for infinite scroll
@ -302,6 +336,18 @@ function VulnerabilitiesDetails(props) {
}; };
}, [isLoading, isEndOfList]); }, [isLoading, isEndOfList]);
useEffect(() => {
if (isLoading) return;
resetPagination();
}, [cveFilter]);
useEffect(() => {
return () => {
abortController.abort();
debouncedChangeHandler.cancel();
};
}, []);
const renderCVEs = () => { const renderCVEs = () => {
return !isEmpty(cveData) ? ( return !isEmpty(cveData) ? (
cveData.map((cve, index) => { cveData.map((cve, index) => {
@ -347,6 +393,17 @@ function VulnerabilitiesDetails(props) {
width: '100%' width: '100%'
}} }}
/> />
<Stack className={classes.search} direction="row" alignItems="center" justifyContent="space-between" spacing={2}>
<InputBase
style={{ paddingLeft: 10, height: 46, color: 'rgba(0, 0, 0, 0.6)' }}
placeholder={'Search for Vulnerabilities...'}
className={classes.input}
onChange={debouncedChangeHandler}
/>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
</Stack>
<Stack direction="column" spacing={2}> <Stack direction="column" spacing={2}>
<Stack direction="column" spacing={2}> <Stack direction="column" spacing={2}>
{renderCVEs()} {renderCVEs()}

View File

@ -580,7 +580,7 @@ function TagDetails() {
<VulnerabilitiesDetails name={reponame} tag={tag} /> <VulnerabilitiesDetails name={reponame} tag={tag} />
</TabPanel> </TabPanel>
<TabPanel value="ReferredBy" className={classes.tabPanel}> <TabPanel value="ReferredBy" className={classes.tabPanel}>
<ReferredBy repoName={reponame} digest={selectedManifest?.digest} /> <ReferredBy referrers={imageDetailData.referrers} />
</TabPanel> </TabPanel>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -44,6 +44,7 @@ const mapToImage = (responseImage) => {
repoName: responseImage.RepoName, repoName: responseImage.RepoName,
tag: responseImage.Tag, tag: responseImage.Tag,
manifests: responseImage.Manifests?.map((manifest) => mapToManifest(manifest)) || [], manifests: responseImage.Manifests?.map((manifest) => mapToManifest(manifest)) || [],
referrers: responseImage.Referrers,
size: responseImage.Size, size: responseImage.Size,
downloadCount: responseImage.DownloadCount, downloadCount: responseImage.DownloadCount,
lastUpdated: responseImage.LastUpdated, lastUpdated: responseImage.LastUpdated,

View File

@ -60,8 +60,19 @@ def pull_modify_push_image(logger, registry, image_name, tag, cosign_password,
metafile = '{}_{}_metadata.json'.format(image_name, tag) metafile = '{}_{}_metadata.json'.format(image_name, tag)
metafile = os.path.join(meta_dir_name, metafile) metafile = os.path.join(meta_dir_name, metafile)
cmd = [image_update_script_path, "-r", registry, "-i", image_name, "-t", tag, "-c", cosign_password, cmd = [image_update_script_path, "-r", registry, "-i", image_name, "-t", tag, "-f", metafile]
"-f", metafile, "-m", multiarch, "-u", username, "-p", password, "--data-dir", data_dir]
if data_dir:
cmd.extend(["--data-dir", data_dir])
if username:
cmd.extend(["-u", username, "-p", password])
if cosign_password:
cmd.extend(["-c", cosign_password])
if multiarch:
cmd.extend(["-m", multiarch])
if debug: if debug:
cmd.append("-d") cmd.append("-d")

View File

@ -11,27 +11,91 @@ username=""
debug=0 debug=0
data_dir=$(pwd) data_dir=$(pwd)
options=$(getopt -o dr:i:t:u:p:c:m:f: -l debug,registry:,image:,tag:,username:,password:,cosign-password:,multiarch:,file:,data-dir: -- "$@") while (( "$#" )); do
if [ $? -ne 0 ]; then case $1 in
usage $0 -r|--registry)
exit 0 if [ -z "$2" ]; then
fi echo "Option registry requires an argument"
exit 1
eval set -- "$options" fi
while :; do registry=$2;
case "$1" in shift 2
-r|--registry) registry=$2; shift 2;; ;;
-i|--image) image=$2; shift 2;; -i|--image)
-t|--tag) tag=$2; shift 2;; if [ -z "$2" ]; then
-u|--username) username=$2; shift 2;; echo "Option image requires an argument"
-p|--password) username=$2; shift 2;; exit 1
-c|--cosign-password) cosign_password=$2; shift 2;; fi
-m|--multiarch) multiarch=$2; shift 2;; image=$2
-f|--file) metafile=$2; shift 2;; shift 2
--data-dir) data_dir=$2; shift 2;; ;;
-d|--debug) debug=1; shift 1;; -t|--tag)
--) shift 1; break;; if [ -z "$2" ]; then
*) usage $0; exit 1;; echo "Option tag requires an argument"
exit 1
fi
tag=$2
shift 2
;;
-u|--username)
if [ -z "$2" ]; then
echo "Option username requires an argument"
exit 1
fi
username=$2
shift 2
;;
-p|--password)
if [ -z "$2" ]; then
echo "Option password requires an argument"
exit 1
fi
password=$2
shift 2
;;
-c|--cosign-password)
if [ -z "$2" ]; then
echo "Option cosign-password requires an argument"
exit 1
fi
cosign_password=$2
shift 2
;;
-m|--multiarch)
if [ -z "$2" ]; then
echo "Option multiarch requires an argument"
exit 1
fi
multiarch=$2
shift 2
;;
-f|--file)
if [ -z "$2" ]; then
echo "Option metafile requires an argument"
exit 1
fi
metafile=$2
shift 2
;;
--data-dir)
if [ -z "$2" ]; then
echo "Option data-dir requires an argument"
exit 1
fi
data_dir=$2
shift 2
;;
-d|--debug)
debug=1
shift 1
;;
--)
shift 1
break
;;
*)
break
;;
esac esac
done done
@ -90,8 +154,8 @@ license="$(cat ${docker_docs_dir}/${image}/license.md)"
vendor="$(cat ${docker_docs_dir}/${image}/maintainer.md)" vendor="$(cat ${docker_docs_dir}/${image}/maintainer.md)"
logo=$(base64 -w 0 ${docker_docs_dir}/${image}/logo.png) logo=$(base64 -w 0 ${docker_docs_dir}/${image}/logo.png)
echo ${repo} echo ${repo}
sed -i "s|%%GITHUB-REPO%%|${repo}|g" ${docker_docs_dir}/${image}/maintainer.md sed -i.bak "s|%%GITHUB-REPO%%|${repo}|g" ${docker_docs_dir}/${image}/maintainer.md; rm ${docker_docs_dir}/${image}/maintainer.md.bak
sed -i "s|%%IMAGE%%|${image}|g" ${docker_docs_dir}/${image}/content.md sed -i.bak "s|%%IMAGE%%|${image}|g" ${docker_docs_dir}/${image}/content.md; rm ${docker_docs_dir}/${image}/content.md.bak
doc=$(cat ${docker_docs_dir}/${image}/content.md) doc=$(cat ${docker_docs_dir}/${image}/content.md)
local_image_ref_skopeo=oci:${images_dir}:${image}-${tag} local_image_ref_skopeo=oci:${images_dir}:${image}-${tag}