Compare commits
	
		
			3 Commits
		
	
	
		
			commit-60c
			...
			commit-747
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7471fb58a8 | ||
|  | 2b3058fb14 | ||
|  | ecff33fe01 | 
							
								
								
									
										110
									
								
								.github/workflows/end-to-end-test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								.github/workflows/end-to-end-test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|   release: | ||||
|     types: | ||||
|       - published | ||||
| name: end-to-end-test | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   build-and-test: | ||||
|     name: Test zui/zot integration | ||||
|     env: | ||||
|       CI: "" | ||||
|       REGISTRY_HOST: "localhost" | ||||
|       REGISTRY_PORT: "8080" | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     steps: | ||||
|     - name: Checkout zui repository | ||||
|       uses: actions/checkout@v3 | ||||
|       with: | ||||
|         fetch-depth: 2 | ||||
|  | ||||
|     - name: Set up Node.js 16.x | ||||
|       uses: actions/setup-node@v3 | ||||
|       with: | ||||
|         node-version: 16.x | ||||
|         cache: 'npm' | ||||
|  | ||||
|     - name: Build zui | ||||
|       run:  | | ||||
|         cd $GITHUB_WORKSPACE | ||||
|         make install | ||||
|         make build | ||||
|  | ||||
|     - name: Install container image tooling | ||||
|       run: | | ||||
|         cd $GITHUB_WORKSPACE | ||||
|         sudo apt-get update | ||||
|         sudo apt-get install libgpgme-dev libassuan-dev libbtrfs-dev libdevmapper-dev pkg-config rpm snapd jq | ||||
|         git clone https://github.com/containers/skopeo -b v1.9.0 $GITHUB_WORKSPACE/src/github.com/containers/skopeo | ||||
|         cd $GITHUB_WORKSPACE/src/github.com/containers/skopeo && make bin/skopeo | ||||
|         chmod +x bin/skopeo | ||||
|         sudo mv bin/skopeo /usr/local/bin/skopeo | ||||
|         which skopeo | ||||
|         skopeo -v | ||||
|         curl -L https://github.com/regclient/regclient/releases/download/v0.4.7/regctl-linux-amd64 -o regctl | ||||
|         chmod +x regctl | ||||
|         sudo mv regctl /usr/local/bin/regctl | ||||
|         which regctl | ||||
|         regctl version | ||||
|         curl -L https://github.com/sigstore/cosign/releases/download/v1.13.0/cosign-linux-amd64 -o cosign | ||||
|         chmod +x cosign | ||||
|         sudo mv cosign /usr/local/bin/cosign | ||||
|         which cosign | ||||
|         cosign version | ||||
|         cd $GITHUB_WORKSPACE | ||||
|  | ||||
|     - name: Install go | ||||
|       uses: actions/setup-go@v3 | ||||
|       with: | ||||
|         go-version: 1.19.x | ||||
|  | ||||
|     - name: Checkout zot repo | ||||
|       uses: actions/checkout@v3 | ||||
|       with: | ||||
|         fetch-depth: 2 | ||||
|         repository: project-zot/zot | ||||
|         ref: main | ||||
|         path: zot | ||||
|  | ||||
|     - name: Build zot | ||||
|       run: | | ||||
|         cd $GITHUB_WORKSPACE/zot | ||||
|         make binary | ||||
|         ls -l bin/ | ||||
|  | ||||
|     - name: Bringup zot server | ||||
|       run: | | ||||
|         cd $GITHUB_WORKSPACE/zot | ||||
|         mkdir /tmp/zot | ||||
|         ./bin/zot-linux-amd64 serve examples/config-ui.json & | ||||
|         while true; do x=0; curl -f http://$REGISTRY_HOST:$REGISTRY_PORT/v2/ || x=1; if [ $x -eq 0 ]; then break; fi; sleep 1; done | ||||
|  | ||||
|     - name: Load image test data from cache into a local folder | ||||
|       id: restore-cache | ||||
|       uses: actions/cache@v3 | ||||
|       with: | ||||
|         path: tests/data/images | ||||
|         key: image-config-${{ hashFiles('**/tests/data/config.yaml') }} | ||||
|         restore-keys: | | ||||
|           image-config- | ||||
|  | ||||
|     - name: Load image test data into zot server | ||||
|       run: | | ||||
|         cd $GITHUB_WORKSPACE | ||||
|         regctl registry set --tls disabled $REGISTRY_HOST:$REGISTRY_PORT | ||||
|         make test-data REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT | ||||
|  | ||||
|     - name: Run integration tests | ||||
|       run: | | ||||
|         cd $GITHUB_WORKSPACE | ||||
|         make integration-tests REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -8,6 +8,7 @@ | ||||
|  | ||||
| # testing | ||||
| /coverage | ||||
| /tests/data/ | ||||
|  | ||||
| # production | ||||
| /build | ||||
|   | ||||
							
								
								
									
										15
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								Makefile
									
									
									
									
									
								
							| @@ -1,3 +1,6 @@ | ||||
| REGISTRY_HOST ?= localhost | ||||
| REGISTRY_PORT ?= 8080 | ||||
|  | ||||
| .PHONY: all | ||||
| all: install audit build | ||||
|  | ||||
| @@ -20,3 +23,15 @@ audit: | ||||
| .PHONY: run | ||||
| run: | ||||
| 	npm start | ||||
|  | ||||
| .PHONY: test-data | ||||
| test-data: | ||||
| 	./tests/scripts/load_test_data.py \ | ||||
| 		--registry $(REGISTRY_HOST):$(REGISTRY_PORT) \ | ||||
| 		--data-dir tests/data \ | ||||
| 		--config-file tests/data/config.yaml \ | ||||
| 		--metadata-file tests/data/image_metadata.json | ||||
|  | ||||
| .PHONY: integration-tests | ||||
| integration-tests: # Triggering the tests TBD | ||||
| 	cat tests/data/image_metadata.json | jq | ||||
|   | ||||
| @@ -13,8 +13,9 @@ jest.mock('react-router-dom', () => ({ | ||||
| })); | ||||
|  | ||||
| const mockImageList = { | ||||
|   RepoListWithNewestImage: { | ||||
|     Results: [ | ||||
|   GlobalSearch: { | ||||
|     Page: { TotalCount: 6, ItemCount: 3 }, | ||||
|     Repos: [ | ||||
|       { | ||||
|         Name: 'alpine', | ||||
|         Size: '2806985', | ||||
| @@ -65,28 +66,36 @@ const mockImageList = { | ||||
|             Count: 10 | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       } | ||||
|     ] | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const mockImageListRecent = { | ||||
|   GlobalSearch: { | ||||
|     Page: { TotalCount: 6, ItemCount: 2 }, | ||||
|     Repos: [ | ||||
|       { | ||||
|         Name: 'centos', | ||||
|         Size: '369311301', | ||||
|         LastUpdated: '2022-08-23T00:20:40.144281895Z', | ||||
|         Name: 'alpine', | ||||
|         Size: '2806985', | ||||
|         LastUpdated: '2022-08-09T17:19:53.274069586Z', | ||||
|         NewestImage: { | ||||
|           Tag: 'latest', | ||||
|           Description: '', | ||||
|           IsSigned: true, | ||||
|           Description: 'w', | ||||
|           IsSigned: false, | ||||
|           Licenses: '', | ||||
|           Vendor: '', | ||||
|           Labels: '', | ||||
|           Vulnerabilities: { | ||||
|             MaxSeverity: 'NONE', | ||||
|             Count: 10 | ||||
|             MaxSeverity: 'LOW', | ||||
|             Count: 7 | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         Name: 'debian', | ||||
|         Size: '369311301', | ||||
|         LastUpdated: '2022-08-23T00:20:40.144281895Z', | ||||
|         Name: 'mongo', | ||||
|         Size: '231383863', | ||||
|         LastUpdated: '2022-08-02T01:30:49.193203152Z', | ||||
|         NewestImage: { | ||||
|           Tag: 'latest', | ||||
|           Description: '', | ||||
| @@ -95,25 +104,8 @@ const mockImageList = { | ||||
|           Vendor: '', | ||||
|           Labels: '', | ||||
|           Vulnerabilities: { | ||||
|             MaxSeverity: 'MEDIUM', | ||||
|             Count: 10 | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         Name: 'mysql', | ||||
|         Size: '369311301', | ||||
|         LastUpdated: '2022-08-23T00:20:40.144281895Z', | ||||
|         NewestImage: { | ||||
|           Tag: 'latest', | ||||
|           Description: '', | ||||
|           IsSigned: true, | ||||
|           Licenses: '', | ||||
|           Vendor: '', | ||||
|           Labels: '', | ||||
|           Vulnerabilities: { | ||||
|             MaxSeverity: 'UNKNOWN', | ||||
|             Count: 10 | ||||
|             MaxSeverity: 'HIGH', | ||||
|             Count: 2 | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| @@ -132,7 +124,8 @@ afterEach(() => { | ||||
|  | ||||
| describe('Home component', () => { | ||||
|   it('fetches image data and renders popular, bookmarks and recently updated', async () => { | ||||
|     jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } }); | ||||
|     jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } }); | ||||
|     jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } }); | ||||
|     render(<Home />); | ||||
|     await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(2)); | ||||
|     await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(2)); | ||||
| @@ -140,14 +133,16 @@ describe('Home component', () => { | ||||
|   }); | ||||
|  | ||||
|   it('renders signature icons', async () => { | ||||
|     jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } }); | ||||
|     jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } }); | ||||
|     jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } }); | ||||
|     render(<Home />); | ||||
|     expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(2); | ||||
|     expect(await screen.findAllByTestId('verified-icon')).toHaveLength(3); | ||||
|   }); | ||||
|  | ||||
|   it('renders vulnerability icons', async () => { | ||||
|     jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } }); | ||||
|     jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } }); | ||||
|     jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } }); | ||||
|     render(<Home />); | ||||
|     expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(2); | ||||
|     expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(2); | ||||
| @@ -158,11 +153,12 @@ describe('Home component', () => { | ||||
|     jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} }); | ||||
|     const error = jest.spyOn(console, 'error').mockImplementation(() => {}); | ||||
|     render(<Home />); | ||||
|     await waitFor(() => expect(error).toBeCalledTimes(1)); | ||||
|     await waitFor(() => expect(error).toBeCalledTimes(2)); | ||||
|   }); | ||||
|  | ||||
|   it('should redirect to explore page when clicking view all popular', async () => { | ||||
|     jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } }); | ||||
|     jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } }); | ||||
|     jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } }); | ||||
|     render(<Home />); | ||||
|     const viewAllButtons = await screen.findAllByText(/view all/i); | ||||
|     expect(viewAllButtons).toHaveLength(2); | ||||
|   | ||||
| @@ -99,7 +99,7 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) { | ||||
|   const [searchQuery, setSearchQuery] = useState(''); | ||||
|   const [suggestionData, setSuggestionData] = useState([]); | ||||
|   const [queryParams] = useSearchParams(); | ||||
|   const search = queryParams.get('search'); | ||||
|   const search = queryParams.get('search') || ''; | ||||
|   const [isLoading, setIsLoading] = useState(false); | ||||
|   const [isFailedSearch, setIsFailedSearch] = useState(false); | ||||
|   const navigate = useNavigate(); | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import { mapToRepo } from 'utilities/objectModels'; | ||||
| import Loading from '../Shared/Loading'; | ||||
| import { useNavigate, createSearchParams } from 'react-router-dom'; | ||||
| import { sortByCriteria } from 'utilities/sortCriteria'; | ||||
| import { HOME_POPULAR_PAGE_SIZE, HOME_RECENT_PAGE_SIZE } from 'utilities/paginationConstants'; | ||||
|  | ||||
| const useStyles = makeStyles(() => ({ | ||||
|   gridWrapper: { | ||||
| @@ -60,29 +61,70 @@ const useStyles = makeStyles(() => ({ | ||||
|  | ||||
| function Home() { | ||||
|   const [isLoading, setIsLoading] = useState(true); | ||||
|   const [homeData, setHomeData] = useState([]); | ||||
|   const [popularData, setPopularData] = useState([]); | ||||
|   const [recentData, setRecentData] = useState([]); | ||||
|   const navigate = useNavigate(); | ||||
|   const abortController = useMemo(() => new AbortController(), []); | ||||
|   const classes = useStyles(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     window.scrollTo(0, 0); | ||||
|   const getPopularData = () => { | ||||
|     setIsLoading(true); | ||||
|     api | ||||
|       .get(`${host()}${endpoints.repoList()}`, abortController.signal) | ||||
|       .get( | ||||
|         `${host()}${endpoints.globalSearch({ | ||||
|           searchQuery: '', | ||||
|           pageNumber: 1, | ||||
|           pageSize: HOME_POPULAR_PAGE_SIZE, | ||||
|           sortBy: sortByCriteria.downloads?.value | ||||
|         })}`, | ||||
|         abortController.signal | ||||
|       ) | ||||
|       .then((response) => { | ||||
|         if (response.data && response.data.data) { | ||||
|           let repoList = response.data.data.RepoListWithNewestImage.Results; | ||||
|           let repoList = response.data.data.GlobalSearch.Repos; | ||||
|           let repoData = repoList.map((responseRepo) => { | ||||
|             return mapToRepo(responseRepo); | ||||
|           }); | ||||
|           setHomeData(repoData); | ||||
|           setPopularData(repoData); | ||||
|           setIsLoading(false); | ||||
|         } | ||||
|       }) | ||||
|       .catch((e) => { | ||||
|         console.error(e); | ||||
|       }); | ||||
|   }; | ||||
|  | ||||
|   const getRecentData = () => { | ||||
|     setIsLoading(true); | ||||
|     api | ||||
|       .get( | ||||
|         `${host()}${endpoints.globalSearch({ | ||||
|           searchQuery: '', | ||||
|           pageNumber: 1, | ||||
|           pageSize: HOME_RECENT_PAGE_SIZE, | ||||
|           sortBy: sortByCriteria.updateTime?.value | ||||
|         })}`, | ||||
|         abortController.signal | ||||
|       ) | ||||
|       .then((response) => { | ||||
|         if (response.data && response.data.data) { | ||||
|           let repoList = response.data.data.GlobalSearch.Repos; | ||||
|           let repoData = repoList.map((responseRepo) => { | ||||
|             return mapToRepo(responseRepo); | ||||
|           }); | ||||
|           setRecentData(repoData); | ||||
|           setIsLoading(false); | ||||
|         } | ||||
|       }) | ||||
|       .catch((e) => { | ||||
|         console.error(e); | ||||
|       }); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     window.scrollTo(0, 0); | ||||
|     getPopularData(); | ||||
|     getRecentData(); | ||||
|     return () => { | ||||
|       abortController.abort(); | ||||
|     }; | ||||
| @@ -94,8 +136,8 @@ function Home() { | ||||
|  | ||||
|   const renderMostPopular = () => { | ||||
|     return ( | ||||
|       homeData && | ||||
|       homeData.slice(0, 3).map((item, index) => { | ||||
|       popularData && | ||||
|       popularData.map((item, index) => { | ||||
|         return ( | ||||
|           <RepoCard | ||||
|             name={item.name} | ||||
| @@ -120,8 +162,8 @@ function Home() { | ||||
|  | ||||
|   const renderRecentlyUpdated = () => { | ||||
|     return ( | ||||
|       homeData && | ||||
|       homeData.slice(0, 2).map((item, index) => { | ||||
|       recentData && | ||||
|       recentData.map((item, index) => { | ||||
|         return ( | ||||
|           <RepoCard | ||||
|             name={item.name} | ||||
|   | ||||
| @@ -349,7 +349,7 @@ function TagDetails() { | ||||
|                       {/* <BookmarkIcon sx={{color:"#52637A"}}/> */} | ||||
|                     </Stack> | ||||
|  | ||||
|                     <Stack> | ||||
|                     <Stack sx={{ width: { xs: '100%', md: 'auto' } }}> | ||||
|                       <FormControl sx={{ m: '1', minWidth: '4.6875rem' }} className={classes.sortForm} size="small"> | ||||
|                         <InputLabel>OS/Arch</InputLabel> | ||||
|                         {!isEmpty(selectedManifest) && ( | ||||
|   | ||||
| @@ -1,6 +1,15 @@ | ||||
| const HEADER_SEARCH_PAGE_SIZE = 9; | ||||
| const EXPLORE_PAGE_SIZE = 10; | ||||
| const HOME_PAGE_SIZE = 10; | ||||
| const HOME_POPULAR_PAGE_SIZE = 3; | ||||
| const HOME_RECENT_PAGE_SIZE = 2; | ||||
| const CVE_FIXEDIN_PAGE_SIZE = 5; | ||||
|  | ||||
| export { HEADER_SEARCH_PAGE_SIZE, EXPLORE_PAGE_SIZE, HOME_PAGE_SIZE, CVE_FIXEDIN_PAGE_SIZE }; | ||||
| export { | ||||
|   HEADER_SEARCH_PAGE_SIZE, | ||||
|   EXPLORE_PAGE_SIZE, | ||||
|   HOME_PAGE_SIZE, | ||||
|   CVE_FIXEDIN_PAGE_SIZE, | ||||
|   HOME_POPULAR_PAGE_SIZE, | ||||
|   HOME_RECENT_PAGE_SIZE | ||||
| }; | ||||
|   | ||||
							
								
								
									
										116
									
								
								tests/data/config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								tests/data/config.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| images: | ||||
|   - name: alpine | ||||
|     tags: | ||||
|       - "3.17" | ||||
|       - "3.17.2" | ||||
|       - "3.17.1" | ||||
|       - "3.16" | ||||
|       - "3.16.4" | ||||
|       - "3.16.3" | ||||
|       - "3.16.2" | ||||
|       - "3.16.1" | ||||
|       - "3.15" | ||||
|       - "3.15.7" | ||||
|       - "3.15.6" | ||||
|       - "3.15.5" | ||||
|       - "3.15.4" | ||||
|       - "3.15.3" | ||||
|       - "3.15.2" | ||||
|       - "3.15.1" | ||||
|       - "3.14" | ||||
|       - "3.14.9" | ||||
|     multiarch: "all" | ||||
|   - name: ubuntu | ||||
|     tags: | ||||
|       - "18.04" | ||||
|       - "bionic-20230301" | ||||
|       - "bionic" | ||||
|       - "22.04" | ||||
|       - "jammy-20230301" | ||||
|       - "jammy" | ||||
|       - "latest" | ||||
|     multiarch: "" | ||||
|   - name: debian | ||||
|     tags: | ||||
|       - "bullseye-slim" | ||||
|       - "bullseye-20230227-slim" | ||||
|       - "11.6-slim" | ||||
|       - "11-slim" | ||||
|     multiarch: "" | ||||
|   - name: centos # Not supported since 2020 | ||||
|     tags: | ||||
|       - "centos7" | ||||
|       - "7" | ||||
|       - "centos7.9.2009" | ||||
|       - "7.9.2009" | ||||
|     multiarch: "" | ||||
|   - name: node | ||||
|     tags: | ||||
|       - "18-alpine3.16" # Alpine here an below | ||||
|       - "18.15-alpine3.16" | ||||
|       - "18.15.0-alpine3.16" | ||||
|       - "lts-alpine3.16" | ||||
|       - "18-alpine" | ||||
|       - "18-alpine3.17" | ||||
|       - "18.15-alpine" | ||||
|       - "18.15-alpine3.17" | ||||
|       - "18.15.0-alpine3.17" | ||||
|       - "lts-alpine3.17" | ||||
|       - "18-bullseye-slim" # Debian here and below | ||||
|       - "18-slim" | ||||
|       - "18.15-bullseye-slim" | ||||
|       - "18.15.0-bullseye-slim" | ||||
|       - "18.15.0-slim" | ||||
|     multiarch: "all" | ||||
|   - name: nginx | ||||
|     tags: | ||||
|       - "1.23.3" # debian:bullseye-slim based here and below | ||||
|       - "mainline" | ||||
|       - "1.23" | ||||
|       - "1.23.3-alpine" # Depends on alpine slim tags | ||||
|       - "mainline-alpine" | ||||
|       - "1.23-alpine" | ||||
|       - "1.23.3-alpine-slim" # Based on alpine | ||||
|       - "mainline-alpine-slim" | ||||
|       - "1.23-alpine-slim" | ||||
|     multiarch: "" | ||||
|   - name: python | ||||
|     tags: | ||||
|       - "3.8.16-alpine3.17" | ||||
|       - "3.8.16-alpine3.16" | ||||
|       - "3.8.16-bullseye" | ||||
|     multiarch: "" | ||||
|   - name: golang | ||||
|     tags: | ||||
|       - "1.20.2-bullseye" | ||||
|       - "1.20.2-alpine3.17" | ||||
|     multiarch: "" | ||||
|   - name: perl | ||||
|     tags: | ||||
|       - "5.36.0-slim" # debian:bullseye-slim based | ||||
|     multiarch: "" | ||||
|   - name: ruby | ||||
|     tags: | ||||
|       - "3.2.1-slim-bullseye" # debian:bullseye-slim based | ||||
|     multiarch: "" | ||||
|   - name: busybox | ||||
|     tags: | ||||
|       - "1.36.0" # From scratch | ||||
|       - "1.35.0" | ||||
|     multiarch: "" | ||||
|   - name: httpd | ||||
|     tags: | ||||
|       - "2.4.56-alpine3.17" | ||||
|     multiarch: "" | ||||
|   - name: hello-world # From scratch | ||||
|     tags: | ||||
|       - "linux" | ||||
|     multiarch: "" | ||||
|   - name: bash | ||||
|     tags: | ||||
|       - "5.2.15-alpine3.16" | ||||
|     multiarch: "" | ||||
|   - name: rust | ||||
|     tags: | ||||
|       - "1.68-slim-bullseye" | ||||
|     multiarch: "" | ||||
							
								
								
									
										148
									
								
								tests/scripts/load_test_data.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										148
									
								
								tests/scripts/load_test_data.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,148 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import argparse | ||||
| import json | ||||
| import logging | ||||
| import os | ||||
| import subprocess | ||||
| import sys | ||||
| import tempfile | ||||
| import yaml | ||||
|  | ||||
| def init_logger(debug=False): | ||||
|     logger = logging.getLogger(__name__) | ||||
|     if debug: | ||||
|         logger.setLevel(logging.DEBUG) | ||||
|     else: | ||||
|         logger.setLevel(logging.INFO) | ||||
|     # log format | ||||
|     log_fmt = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" | ||||
|     date_fmt = "%Y-%m-%d %H:%M:%S" | ||||
|     formatter = logging.Formatter(log_fmt, date_fmt) | ||||
|     # create streamHandler and set log fmt | ||||
|     stream_handler = logging.StreamHandler() | ||||
|     stream_handler.setFormatter(formatter) | ||||
|     # add the streamHandler to logger | ||||
|     logger.addHandler(stream_handler) | ||||
|     return logger | ||||
|  | ||||
| def parse_args(): | ||||
|     p = argparse.ArgumentParser() | ||||
|     p.add_argument('-r', '--registry', default='localhost:8080', help='Registry address') | ||||
|     p.add_argument('-u', '--username', default="", help='registry username') | ||||
|     p.add_argument('-p', '--password', default="", help='registry password') | ||||
|     p.add_argument('-c', '--cosign-password', default="", help='cosign key password') | ||||
|     p.add_argument('-d', '--debug', action='store_true', help='enable debug logs') | ||||
|     p.add_argument('-f', '--config-file', default="config.yaml", help='config file containing information about images to upload') | ||||
|     p.add_argument('-m', '--metadata-file', default="image_metadata.json", help='file containing metadata on uploaded images') | ||||
|     p.add_argument('--data-dir', default="", help='location where to store image related data') | ||||
|  | ||||
|     return p.parse_args() | ||||
|  | ||||
| def fetch_tags(logger, image_name): | ||||
|     cmd = "skopeo list-tags docker://docker.io/{}".format(image_name) | ||||
|     logger.info("running command: '{}'".format(cmd)) | ||||
|  | ||||
|     result = subprocess.run(cmd, capture_output=True, shell=True) | ||||
|     if result.returncode != 0: | ||||
|         logger.error("running command `{}` exited with code: {}".format(cmd, str(result.returncode))) | ||||
|         logger.error(result.stderr) | ||||
|         sys.exit(1) | ||||
|  | ||||
|     return json.loads(result.stdout)["Tags"] | ||||
|  | ||||
| def pull_modify_push_image(logger, registry, image_name, tag, cosign_password, | ||||
|                            multiarch, username, password, debug, data_dir): | ||||
|     logger.info("image '{}:{}' will be processed and pushed".format(image_name, tag)) | ||||
|  | ||||
|     image_update_script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "pull_update_push_image.sh") | ||||
|  | ||||
|     with tempfile.TemporaryDirectory() as meta_dir_name: | ||||
|         metafile = '{}_{}_metadata.json'.format(image_name, tag) | ||||
|         metafile = os.path.join(meta_dir_name, metafile) | ||||
|  | ||||
|         cmd = [image_update_script_path, "-r", registry, "-i", image_name, "-t", tag, "-f", metafile] | ||||
|  | ||||
|         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: | ||||
|             cmd.append("-d") | ||||
|  | ||||
|         logger.info("running command: '{}'".format(" ".join(cmd))) | ||||
|         result = subprocess.run(cmd, stderr=sys.stderr, stdout=sys.stdout) | ||||
|         if result.returncode != 0: | ||||
|             logger.error("pushing image `{}:{}` exited with code: ".format(image_name, tag) + str(result.returncode)) | ||||
|             sys.exit(1) | ||||
|  | ||||
|         with open(metafile) as f: | ||||
|             image_metadata = json.load(f) | ||||
|             image_metadata[image_name][tag]["multiarch"] = multiarch | ||||
|  | ||||
|     return image_metadata | ||||
|  | ||||
| def main(): | ||||
|     args = parse_args() | ||||
|  | ||||
|     registry = args.registry | ||||
|     username = args.username | ||||
|     password = args.password | ||||
|     cosign_password = args.cosign_password | ||||
|     config_file = args.config_file | ||||
|     debug = args.debug | ||||
|     metadata_file = args.metadata_file | ||||
|     data_dir= args.data_dir | ||||
|  | ||||
|     logger = init_logger(debug) | ||||
|  | ||||
|     with open(config_file, "r") as f: | ||||
|         config = yaml.load(f, Loader=yaml.SafeLoader) | ||||
|   | ||||
|     metadata = {} | ||||
|  | ||||
|     for image in config["images"]: | ||||
|         image_name = image["name"] | ||||
|  | ||||
|         multiarch = image["multiarch"] | ||||
|         if not multiarch: | ||||
|             multiarch = "" | ||||
|  | ||||
|         actual_tags = fetch_tags(logger, image_name) | ||||
|         expected_tags = image["tags"] | ||||
|  | ||||
|         logger.debug("image '{}' has the following tags specified in the config file: '{}'".format(image_name, ",".join(expected_tags))) | ||||
|         logger.debug("image '{}' has the following tags on source registry: '{}'".format(image_name, ",".join(actual_tags))) | ||||
|  | ||||
|         for tag in expected_tags: | ||||
|             found = False | ||||
|  | ||||
|             for actual_tag in actual_tags: | ||||
|                 if actual_tag == tag: | ||||
|                     found = True | ||||
|                     break | ||||
|  | ||||
|             if not found: | ||||
|                 logger.error("image '{}:{}' not found".format(image_name, tag)) | ||||
|                 sys.exit(1) | ||||
|  | ||||
|         for tag in expected_tags: | ||||
|             image_metadata = pull_modify_push_image(logger, registry, image_name, tag, cosign_password, multiarch, username, password, debug, data_dir) | ||||
|  | ||||
|             metadata.setdefault(image_name, {}) | ||||
|             metadata[image_name][tag] = image_metadata[image_name][tag] | ||||
|  | ||||
|     with open(metadata_file, "w") as f: | ||||
|         json.dump(metadata, f, indent=2) | ||||
|  | ||||
|     logger.info("Done loading images, see more details in '{}'".format(metadata_file)) | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
							
								
								
									
										229
									
								
								tests/scripts/pull_update_push_image.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										229
									
								
								tests/scripts/pull_update_push_image.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,229 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| registry="" | ||||
| image="" | ||||
| tag="" | ||||
| cosign_password="" | ||||
| metafile="" | ||||
| multiarch="" | ||||
| username="" | ||||
| username="" | ||||
| debug=0 | ||||
| data_dir=$(pwd) | ||||
|  | ||||
| while (( "$#" )); do | ||||
|     case $1 in | ||||
|         -r|--registry) | ||||
|             if [ -z "$2" ]; then | ||||
|                 echo "Option registry requires an argument" | ||||
|                 exit 1 | ||||
|             fi | ||||
|             registry=$2; | ||||
|             shift 2 | ||||
|             ;; | ||||
|         -i|--image) | ||||
|             if [ -z "$2" ]; then | ||||
|                 echo "Option image requires an argument" | ||||
|                 exit 1 | ||||
|             fi | ||||
|             image=$2 | ||||
|             shift 2 | ||||
|             ;; | ||||
|         -t|--tag) | ||||
|             if [ -z "$2" ]; then | ||||
|                 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 | ||||
| done | ||||
|  | ||||
| if [ ${debug} -eq 1 ]; then | ||||
|     set -x | ||||
| fi | ||||
|  | ||||
| images_dir=${data_dir}/images | ||||
| docker_docs_dir=${data_dir}/docs | ||||
| cosign_key_path=${data_dir}/cosign.key | ||||
|  | ||||
| function verify_prerequisites { | ||||
|     mkdir -p ${data_dir} | ||||
|  | ||||
|     if [ ! command -v regctl ] &>/dev/null; then | ||||
|         echo "you need to install regctl as a prerequisite" >&3 | ||||
|         return 1 | ||||
|     fi | ||||
|  | ||||
|     if [ ! command -v skopeo ] &>/dev/null; then | ||||
|         echo "you need to install skopeo as a prerequisite" >&3 | ||||
|         return 1 | ||||
|     fi | ||||
|  | ||||
|     if [ ! command -v cosign ] &>/dev/null; then | ||||
|         echo "you need to install cosign as a prerequisite" >&3 | ||||
|         return 1 | ||||
|     fi | ||||
|  | ||||
|     if [ ! command -v jq ] &>/dev/null; then | ||||
|         echo "you need to install jq as a prerequisite" >&3 | ||||
|         return 1 | ||||
|     fi | ||||
|  | ||||
|     if [ ! -f "${cosign_key_path}" ]; then | ||||
|         COSIGN_PASSWORD=${cosign_password} cosign generate-key-pair | ||||
|         key_dir=$(dirname ${cosign_key_path}) | ||||
|         mv cosign.key ${cosign_key_path} | ||||
|         mv cosign.pub ${key_dir} | ||||
|     fi | ||||
|  | ||||
|     # pull docker docs repo | ||||
|     if [ ! -d ${docker_docs_dir} ] | ||||
|     then | ||||
|         git clone https://github.com/docker-library/docs.git ${docker_docs_dir} | ||||
|     fi | ||||
|  | ||||
|     return 0 | ||||
| } | ||||
|  | ||||
| verify_prerequisites | ||||
|  | ||||
| repo=$(cat ${docker_docs_dir}/${image}/github-repo) | ||||
| description="$(cat ${docker_docs_dir}/${image}/README-short.txt)" | ||||
| license="$(cat ${docker_docs_dir}/${image}/license.md)" | ||||
| vendor="$(cat ${docker_docs_dir}/${image}/maintainer.md)" | ||||
| logo=$(base64 -w 0 ${docker_docs_dir}/${image}/logo.png) | ||||
| echo ${repo} | ||||
| sed -i.bak "s|%%GITHUB-REPO%%|${repo}|g" ${docker_docs_dir}/${image}/maintainer.md; rm ${docker_docs_dir}/${image}/maintainer.md.bak | ||||
| 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) | ||||
|  | ||||
| local_image_ref_skopeo=oci:${images_dir}:${image}-${tag} | ||||
| local_image_ref_regtl=ocidir://${images_dir}:${image}-${tag} | ||||
| remote_src_image_ref=docker://${image}:${tag} | ||||
| remote_dest_image_ref=${registry}/${image}:${tag} | ||||
|  | ||||
| multiarch_arg="" | ||||
| if [ ! -z "${multiarch}" ]; then | ||||
|     multiarch_arg="--multi-arch=${multiarch}" | ||||
| fi | ||||
|  | ||||
| # Verify if image is already present in local oci layout | ||||
| skopeo inspect ${local_image_ref_skopeo} | ||||
| if [ $? -eq 0 ]; then | ||||
|     echo "Image ${local_image_ref_skopeo} found locally" | ||||
| else | ||||
|     echo "Image ${local_image_ref_skopeo} will be copied" | ||||
|     skopeo --insecure-policy copy --format=oci ${multiarch_arg} ${remote_src_image_ref} ${local_image_ref_skopeo} | ||||
|     if [ $? -ne 0 ]; then | ||||
|         exit 1 | ||||
|     fi | ||||
| fi | ||||
|  | ||||
| # Mofify image in local oci layout and update the old reference to point to the new index | ||||
| regctl image mod --replace --annotation org.opencontainers.image.title=${image} ${local_image_ref_regtl} | ||||
| regctl image mod --replace --annotation org.opencontainers.image.description="${description}" ${local_image_ref_regtl} | ||||
| regctl image mod --replace --annotation org.opencontainers.image.url=${repo} ${local_image_ref_regtl} | ||||
| regctl image mod --replace --annotation org.opencontainers.image.source=${repo} ${local_image_ref_regtl} | ||||
| regctl image mod --replace --annotation org.opencontainers.image.licenses="${license}" ${local_image_ref_regtl} | ||||
| regctl image mod --replace --annotation org.opencontainers.image.vendor="${vendor}" ${local_image_ref_regtl} | ||||
| regctl image mod --replace --annotation org.opencontainers.image.documentation="${description}" ${local_image_ref_regtl} | ||||
|  | ||||
| credentials_args="" | ||||
| if [ ! -z "${username}" ]; then | ||||
|     credentials_args="--dest-creds ${username}:${username}" | ||||
| fi | ||||
|  | ||||
| # Upload image to target registry | ||||
| skopeo copy --dest-tls-verify=false ${multiarch_arg} ${credentials_args} ${local_image_ref_skopeo} docker://${remote_dest_image_ref} | ||||
| if [ $? -ne 0 ]; then | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| # Upload image logo as image media type | ||||
| regctl artifact put --annotation artifact.type=com.zot.logo.image --annotation format=oci \ | ||||
|     --artifact-type "application/vnd.zot.logo.v1" --subject ${remote_dest_image_ref} ${remote_dest_image_ref}-logo-image << EOF | ||||
| ${logo} | ||||
| EOF | ||||
| if [ $? -ne 0 ]; then | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| # Sign new updated image | ||||
| COSIGN_PASSWORD=${cosign_password} cosign sign ${remote_dest_image_ref} --key ${cosign_key_path} --allow-insecure-registry | ||||
| if [ $? -ne 0 ]; then | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| details=$(jq -n \ | ||||
|     --arg org.opencontainers.image.title "${image}" \ | ||||
|     --arg org.opencontainers.image.description " $description" \ | ||||
|     --arg org.opencontainers.image.url "${repo}" \ | ||||
|     --arg org.opencontainers.image.source "${repo}" \ | ||||
|     --arg org.opencontainers.image.licenses "${license}" \ | ||||
|     --arg org.opencontainers.image.vendor "${vendor}" \ | ||||
|     --arg org.opencontainers.image.documentation "${description}" \ | ||||
|     '$ARGS.named' | ||||
| ) | ||||
|  | ||||
| jq -n --arg image "${image}" --arg tag "${tag}"  --argjson details "${details}" '.[$image][$tag]=$details' > ${metafile} | ||||
		Reference in New Issue
	
	Block a user