mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-05-31 04:30:44 +00:00
Compare commits
133 Commits
v1.6.2
...
93d9d509d5
| Author | SHA1 | Date | |
|---|---|---|---|
| 93d9d509d5 | |||
| 98ed26b470 | |||
| 0babc6b4ab | |||
| 55d2549e1f | |||
| f3e3ff1432 | |||
| aa3861af9c | |||
| 34660d3615 | |||
| d63377a851 | |||
| a20eb6866a | |||
| 809ab47042 | |||
| 0b84009b4a | |||
| 92d7561d49 | |||
| e908531bef | |||
| f8620d10b2 | |||
| 8be72b48a1 | |||
| 5655480e00 | |||
| 3c8defa304 | |||
| 1ed50697ec | |||
| 3b432d42b5 | |||
| 89e3bdfa07 | |||
| f8d2d3cb8d | |||
| 01004e19c0 | |||
| 92bb28149c | |||
| 652210acfa | |||
| 45f3da256b | |||
| 3c5e83366a | |||
| a7a4bb7001 | |||
| 2f7f726d4c | |||
| 43d7284657 | |||
| 02423af931 | |||
| 2a228625e2 | |||
| 16e0302f30 | |||
| 6ecbc9ba90 | |||
| 7276b9621f | |||
| fb7734b5b0 | |||
| 29c37293b9 | |||
| f25ba2e6b0 | |||
| 6a5b9ddacf | |||
| 48355f65ed | |||
| d616977904 | |||
| 3c068febde | |||
| 76adbe49e0 | |||
| f6221a2413 | |||
| 4f46cb04f5 | |||
| 66e814c086 | |||
| b3f5d96490 | |||
| 144265122a | |||
| 4f95d75c37 | |||
| 8db1d2e7f1 | |||
| 4088a811cd | |||
| 2ac4c75fad | |||
| e2ebcbb02a | |||
| 9defe70190 | |||
| 23943d47e9 | |||
| 49f342878a | |||
| 1f29c65a95 | |||
| a65f79eb79 | |||
| c6a9f82358 | |||
| 1702537979 | |||
| 12604b9379 | |||
| 9b523e6bd5 | |||
| aa030e3f36 | |||
| 8e739524b0 | |||
| 4ba4c0cba6 | |||
| 48d02918c1 | |||
| d672bfa317 | |||
| 9a90038dd2 | |||
| 7d23196f76 | |||
| 2c6812934e | |||
| 60f3eb151b | |||
| 0db9797c4e | |||
| a2ffffedc1 | |||
| 19b98c62c1 | |||
| 06fea598e1 | |||
| 9a6f06d23e | |||
| 67f6a0e458 | |||
| a57e2aecd7 | |||
| 1e7c15b69b | |||
| a70f572efd | |||
| 8c183b45c6 | |||
| a8f7f58dab | |||
| 31fe26de5e | |||
| eb1b770dc2 | |||
| abb2ad635f | |||
| a75df0a697 | |||
| ea797f8ebe | |||
| a4cc9211d6 | |||
| 836d9f3b8b | |||
| 61650e5b3b | |||
| 4e457aa570 | |||
| fe70da9c08 | |||
| 4b57e65658 | |||
| bcd81eeae4 | |||
| af483d1165 | |||
| 6b8651fda2 | |||
| de699aebe5 | |||
| 0021cf876b | |||
| 32b601bde6 | |||
| b464e7f80b | |||
| e0f282aca9 | |||
| ba65daf6cb | |||
| b8455f6de9 | |||
| 132c923f25 | |||
| b6d83a4f61 | |||
| 4526d6d831 | |||
| 02d2ba255c | |||
| d94792dd65 | |||
| 66eb75f492 | |||
| 33a2f70d07 | |||
| 10f942c8e0 | |||
| 568a9ce4d5 | |||
| ddf415a359 | |||
| 29ac9c1919 | |||
| d3bed7830c | |||
| c2d5f47643 | |||
| 731e92c8e4 | |||
| 94a600c0c1 | |||
| e1d8ae8a35 | |||
| d3b7186dea | |||
| 3608c137a0 | |||
| 15a3efe758 | |||
| 4b73ae462f | |||
| b49a631e0b | |||
| 12b6b04055 | |||
| a1f659bea0 | |||
| 8ca4cb8dcb | |||
| 8ce8f250d5 | |||
| 3672f6f92f | |||
| 888a6b2caa | |||
| 231039e86c | |||
| dc884e6052 | |||
| 4675589cf6 | |||
| 32f03bfd62 |
@@ -4,6 +4,10 @@ Fixes #
|
||||
|
||||
All new code should be covered with tests, documentation should be updated. CI should pass.
|
||||
|
||||
Also, to speed up things, if you could kindly "Allow edits and access to secrets by maintainers" in the
|
||||
PR settings, as this allows us to rebase the PR on master, fix conflicts, run coverage and help with
|
||||
implementing code and tests.
|
||||
|
||||
## Description of the Change
|
||||
|
||||
<!--
|
||||
@@ -14,6 +18,7 @@ Why this change is important?
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] allow Maintainers to edit PR (rebase, run coverage, help with tests, ...)
|
||||
- [ ] unit-test added (if change is algorithm)
|
||||
- [ ] functional test added/updated (if change is functional)
|
||||
- [ ] man page updated (if applicable)
|
||||
|
||||
+70
-25
@@ -17,8 +17,32 @@ env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: "Unit Tests"
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: false
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# fetch the whole repo for `git describe` to work
|
||||
fetch-depth: 0
|
||||
- name: "Docker Image"
|
||||
run: |
|
||||
make docker-image
|
||||
- name: "Unit Tests"
|
||||
run: |
|
||||
make docker-unit-tests
|
||||
mkdir -p out/coverage
|
||||
mv unit.out out/coverage/
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unit-tests-coverage
|
||||
path: out/
|
||||
|
||||
test:
|
||||
name: "Test (Ubuntu 22.04)"
|
||||
name: "System Test"
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: false
|
||||
timeout-minutes: 30
|
||||
@@ -63,21 +87,10 @@ jobs:
|
||||
with:
|
||||
directory: ${{ runner.temp }}
|
||||
|
||||
- name: "Run Unit Tests"
|
||||
env:
|
||||
RUN_LONG_TESTS: 'yes'
|
||||
AZURE_STORAGE_ENDPOINT: "http://127.0.0.1:10000/devstoreaccount1"
|
||||
AZURE_STORAGE_ACCOUNT: "devstoreaccount1"
|
||||
AZURE_STORAGE_ACCESS_KEY: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
sudo mkdir -p /srv ; sudo chown runner /srv
|
||||
COVERAGE_DIR=${{ runner.temp }} make test
|
||||
|
||||
- name: "Run Benchmark"
|
||||
run: |
|
||||
COVERAGE_DIR=${{ runner.temp }} make bench
|
||||
mkdir -p out/coverage
|
||||
COVERAGE_DIR=$PWD/out/coverage make bench
|
||||
|
||||
- name: "Run System Tests"
|
||||
env:
|
||||
@@ -89,30 +102,63 @@ jobs:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
sudo mkdir -p /srv ; sudo chown runner /srv
|
||||
COVERAGE_DIR=${{ runner.temp }} make system-test
|
||||
mkdir -p out/coverage
|
||||
COVERAGE_DIR=$PWD/out/coverage make system-test
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: system-tests-coverage
|
||||
path: out/
|
||||
|
||||
coverage:
|
||||
name: "Upload Coverage"
|
||||
runs-on: ubuntu-22.04
|
||||
continue-on-error: false
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- unit-test
|
||||
- test
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Download Unit Test Coverage"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: unit-tests-coverage
|
||||
|
||||
- name: "Download System Test Coverage"
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: system-tests-coverage
|
||||
|
||||
- name: "Merge Code Coverage"
|
||||
run: |
|
||||
go install github.com/wadey/gocovmerge@v0.0.0-20160331181800-b5bfa59ec0ad
|
||||
~/go/bin/gocovmerge unit.out ${{ runner.temp }}/*.out > coverage.txt
|
||||
# go install github.com/wadey/gocovmerge@v0.0.0-20160331181800-b5bfa59ec0ad
|
||||
# ~/go/bin/gocovmerge coverage/*.out > coverage.txt
|
||||
awk 'FNR==1 && NR!=1 {next} {print}' coverage/*.out > coverage.txt
|
||||
|
||||
- name: "Upload Code Coverage"
|
||||
uses: codecov/codecov-action@v2
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: coverage.txt
|
||||
fail_ci_if_error: true
|
||||
|
||||
|
||||
ci-debian-build:
|
||||
name: "Build"
|
||||
needs: test
|
||||
needs:
|
||||
- coverage
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name: ["Debian 13/testing", "Debian 12/bookworm", "Debian 11/bullseye", "Debian 10/buster", "Ubuntu 24.04", "Ubuntu 22.04", "Ubuntu 20.04"]
|
||||
name: ["Debian 13/trixie", "Debian 12/bookworm", "Debian 11/bullseye", "Ubuntu 24.04", "Ubuntu 22.04", "Ubuntu 20.04"]
|
||||
arch: ["amd64", "i386" , "arm64" , "armhf"]
|
||||
include:
|
||||
- name: "Debian 13/testing"
|
||||
- name: "Debian 13/trixie"
|
||||
suite: trixie
|
||||
image: debian:trixie-slim
|
||||
- name: "Debian 12/bookworm"
|
||||
@@ -121,9 +167,6 @@ jobs:
|
||||
- name: "Debian 11/bullseye"
|
||||
suite: bullseye
|
||||
image: debian:bullseye-slim
|
||||
- name: "Debian 10/buster"
|
||||
suite: buster
|
||||
image: debian:buster-slim
|
||||
- name: "Ubuntu 24.04"
|
||||
suite: noble
|
||||
image: ubuntu:24.04
|
||||
@@ -135,6 +178,7 @@ jobs:
|
||||
image: ubuntu:20.04
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
options: --user root
|
||||
env:
|
||||
APT_LISTCHANGES_FRONTEND: none
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
@@ -226,7 +270,8 @@ jobs:
|
||||
|
||||
ci-binary-build:
|
||||
name: "Build"
|
||||
needs: test
|
||||
needs:
|
||||
- coverage
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Install and initialize swagger
|
||||
run: |
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
swag init -q --markdownFiles docs
|
||||
swag init -q --propertyStrategy pascalcase --markdownFiles docs
|
||||
shell: sh
|
||||
|
||||
- name: golangci-lint
|
||||
|
||||
@@ -26,6 +26,8 @@ _testmain.go
|
||||
*.test
|
||||
|
||||
coverage.txt
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
*.pyc
|
||||
|
||||
|
||||
@@ -69,3 +69,18 @@ List of contributors, in chronological order:
|
||||
* Leigh London (https://github.com/leighlondon)
|
||||
* Gordian Schoenherr (https://github.com/schoenherrg)
|
||||
* Silke Hofstra (https://github.com/silkeh)
|
||||
* Itay Porezky (https://github.com/itayporezky)
|
||||
* Alejandro Guijarro Monerris (https://github.com/alguimodd)
|
||||
* JupiterRider (https://github.com/JupiterRider)
|
||||
* Agustin Henze (https://github.com/agustinhenze)
|
||||
* Tobias Assarsson (https://github.com/daedaluz)
|
||||
* Yaksh Bariya (https://github.com/thunder-coding)
|
||||
* Juan Calderon-Perez (https://github.com/gaby)
|
||||
* Ato Araki (https://github.com/atotto)
|
||||
* Roman Lebedev (https://github.com/LebedevRI)
|
||||
* Brian Witt (https://github.com/bwitt)
|
||||
* Ales Bregar (https://github.com/abregar)
|
||||
* Tim Foerster (https://github.com/tonobo)
|
||||
* Zhang Xiao (https://github.com/xzhang1)
|
||||
* Tom Nguyen (https://github.com/lecafard)
|
||||
* Philip Cramer (https://github.com/PhilipCramer)
|
||||
|
||||
@@ -7,9 +7,23 @@ COVERAGE_DIR?=$(shell mktemp -d)
|
||||
GOOS=$(shell go env GOHOSTOS)
|
||||
GOARCH=$(shell go env GOHOSTARCH)
|
||||
|
||||
export PODMAN_USERNS = keep-id
|
||||
DOCKER_RUN = docker run --security-opt label=disable --user 0:0 --rm -v ${PWD}:/work/src
|
||||
|
||||
# Setting TZ for certificates
|
||||
export TZ=UTC
|
||||
# Unit Tests and some sysmte tests rely on expired certificates, turn back the time
|
||||
export TEST_FAKETIME := 2025-01-02 03:04:05
|
||||
|
||||
# run with 'COVERAGE_SKIP=1' to skip coverage checks during system tests
|
||||
ifeq ($(COVERAGE_SKIP),1)
|
||||
COVERAGE_ARG_BUILD :=
|
||||
COVERAGE_ARG_TEST := --coverage-skip
|
||||
else
|
||||
COVERAGE_ARG_BUILD := -coverpkg="./..."
|
||||
COVERAGE_ARG_TEST := --coverage-dir $(COVERAGE_DIR)
|
||||
endif
|
||||
|
||||
# export CAPUTRE=1 for regenrating test gold files
|
||||
ifeq ($(CAPTURE),1)
|
||||
CAPTURE_ARG := --capture
|
||||
@@ -63,7 +77,7 @@ azurite-stop:
|
||||
|
||||
swagger: swagger-install
|
||||
# Generate swagger docs
|
||||
@PATH=$(BINPATH)/:$(PATH) swag init --parseDependency --parseInternal --markdownFiles docs --generalInfo docs/swagger.conf
|
||||
@PATH=$(BINPATH)/:$(PATH) swag init --propertyStrategy pascalcase --parseDependency --parseInternal --markdownFiles docs --generalInfo docs/swagger.conf
|
||||
|
||||
etcd-install:
|
||||
# Install etcd
|
||||
@@ -101,13 +115,13 @@ test: prepare swagger etcd-install ## Run unit tests (add TEST=regex to specify
|
||||
|
||||
system-test: prepare swagger etcd-install ## Run system tests
|
||||
# build coverage binary
|
||||
go test -v -coverpkg="./..." -c -tags testruncli
|
||||
go test -v $(COVERAGE_ARG_BUILD) -c -tags testruncli
|
||||
# Download fixture-db, fixture-pool, etcd.db
|
||||
if [ ! -e ~/aptly-fixture-db ]; then git clone https://github.com/aptly-dev/aptly-fixture-db.git ~/aptly-fixture-db/; fi
|
||||
if [ ! -e ~/aptly-fixture-pool ]; then git clone https://github.com/aptly-dev/aptly-fixture-pool.git ~/aptly-fixture-pool/; fi
|
||||
test -f ~/etcd.db || (curl -o ~/etcd.db.xz http://repo.aptly.info/system-tests/etcd.db.xz && xz -d ~/etcd.db.xz)
|
||||
# Run system tests
|
||||
PATH=$(BINPATH)/:$(PATH) FORCE_COLOR=1 $(PYTHON) system/run.py --long --coverage-dir $(COVERAGE_DIR) $(CAPTURE_ARG) $(TEST)
|
||||
PATH=$(BINPATH)/:$(PATH) FORCE_COLOR=1 $(PYTHON) system/run.py --long $(COVERAGE_ARG_TEST) $(CAPTURE_ARG) $(TEST)
|
||||
|
||||
bench:
|
||||
@echo "\e[33m\e[1mRunning benchmark ...\e[0m"
|
||||
@@ -117,7 +131,8 @@ serve: prepare swagger-install ## Run development server (auto recompiling)
|
||||
test -f $(BINPATH)/air || go install github.com/air-verse/air@v1.52.3
|
||||
cp debian/aptly.conf ~/.aptly.conf
|
||||
sed -i /enable_swagger_endpoint/s/false/true/ ~/.aptly.conf
|
||||
PATH=$(BINPATH):$$PATH air -build.pre_cmd 'swag init -q --markdownFiles docs --generalInfo docs/swagger.conf' -build.exclude_dir docs,system,debian,pgp/keyrings,pgp/test-bins,completion.d,man,deb/testdata,console,_man,systemd,obj-x86_64-linux-gnu -- api serve -listen 0.0.0.0:3142
|
||||
sed -i /enable_metrics_endpoint/s/false/true/ ~/.aptly.conf
|
||||
PATH=$(BINPATH):$$PATH air -build.pre_cmd 'swag init -q --propertyStrategy pascalcase --markdownFiles docs --generalInfo docs/swagger.conf' -build.exclude_dir docs,system,debian,pgp/keyrings,pgp/test-bins,completion.d,man,deb/testdata,console,_man,systemd,obj-x86_64-linux-gnu -- api serve -listen 0.0.0.0:3142
|
||||
|
||||
dpkg: prepare swagger ## Build debian packages
|
||||
@test -n "$(DEBARCH)" || (echo "please define DEBARCH"; exit 1)
|
||||
@@ -171,16 +186,16 @@ docker-image-no-cache: ## Build aptly-dev docker image (no cache)
|
||||
@docker build --no-cache -f system/Dockerfile . -t aptly-dev
|
||||
|
||||
docker-build: ## Build aptly in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper build
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper build
|
||||
|
||||
docker-shell: ## Run aptly and other commands in docker container
|
||||
@docker run -it --rm -p 3142:3142 -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper || true
|
||||
@$(DOCKER_RUN) -it -p 3142:3142 aptly-dev /work/src/system/docker-wrapper || true
|
||||
|
||||
docker-deb: ## Build debian packages in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper dpkg DEBARCH=amd64
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper dpkg DEBARCH=amd64
|
||||
|
||||
docker-unit-test: ## Run unit tests in docker container (add TEST=regex to specify which tests to run)
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper \
|
||||
docker-unit-tests: ## Run unit tests in docker container (add TEST=regex to specify which tests to run)
|
||||
$(DOCKER_RUN) -t --tmpfs /smallfs:rw,size=1m aptly-dev /work/src/system/docker-wrapper \
|
||||
azurite-start \
|
||||
AZURE_STORAGE_ENDPOINT=http://127.0.0.1:10000/devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCOUNT=devstoreaccount1 \
|
||||
@@ -189,27 +204,27 @@ docker-unit-test: ## Run unit tests in docker container (add TEST=regex to spec
|
||||
azurite-stop
|
||||
|
||||
docker-system-test: ## Run system tests in docker container (add TEST=t04_mirror or TEST=UpdateMirror26Test to run only specific tests)
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper \
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper \
|
||||
azurite-start \
|
||||
AZURE_STORAGE_ENDPOINT=http://127.0.0.1:10000/devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCOUNT=devstoreaccount1 \
|
||||
AZURE_STORAGE_ACCESS_KEY="Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" \
|
||||
AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) \
|
||||
AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) \
|
||||
system-test TEST=$(TEST) \
|
||||
system-test TEST=$(TEST) CAPTURE=$(CAPTURE) COVERAGE_SKIP=$(COVERAGE_SKIP) \
|
||||
azurite-stop
|
||||
|
||||
docker-serve: ## Run development server (auto recompiling) on http://localhost:3142
|
||||
@docker run -it --rm -p 3142:3142 -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper serve || true
|
||||
@$(DOCKER_RUN) -it -p 3142:3142 -v /tmp/cache-go-aptly:/var/lib/aptly/.cache/go-build aptly-dev /work/src/system/docker-wrapper serve || true
|
||||
|
||||
docker-lint: ## Run golangci-lint in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper lint
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper lint
|
||||
|
||||
docker-binaries: ## Build binary releases (FreeBSD, macOS, Linux generic) in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper binaries
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper binaries
|
||||
|
||||
docker-man: ## Create man page in docker container
|
||||
@docker run -it --rm -v ${PWD}:/work/src aptly-dev /work/src/system/docker-wrapper man
|
||||
@$(DOCKER_RUN) -t aptly-dev /work/src/system/docker-wrapper man
|
||||
|
||||
mem.png: mem.dat mem.gp
|
||||
gnuplot mem.gp
|
||||
@@ -226,4 +241,4 @@ clean: ## remove local build and module cache
|
||||
rm -f unit.out aptly.test VERSION docs/docs.go docs/swagger.json docs/swagger.yaml docs/swagger.conf
|
||||
find system/ -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true
|
||||
|
||||
.PHONY: help man prepare swagger version binaries build docker-release docker-system-test docker-unit-test docker-lint docker-build docker-image docker-man docker-shell docker-serve clean releasetype dpkg serve flake8
|
||||
.PHONY: help man prepare swagger version binaries build docker-release docker-system-tests docker-unit-test docker-lint docker-build docker-image docker-man docker-shell docker-serve clean releasetype dpkg serve flake8
|
||||
|
||||
+4
-6
@@ -63,7 +63,7 @@ Define Release APT sources in ``/etc/apt/sources.list.d/aptly.list``::
|
||||
|
||||
deb [signed-by=/etc/apt/keyrings/aptly.asc] http://repo.aptly.info/release DIST main
|
||||
|
||||
Where DIST is one of: ``buster``, ``bullseye``, ``bookworm``, ``focal``, ``jammy``, ``noble``
|
||||
Where DIST is one of: ``bullseye``, ``bookworm``, ``trixie``, ``focal``, ``jammy``, ``noble``
|
||||
|
||||
Install aptly packages::
|
||||
|
||||
@@ -80,7 +80,7 @@ Define CI APT sources in ``/etc/apt/sources.list.d/aptly-ci.list``::
|
||||
|
||||
deb [signed-by=/etc/apt/keyrings/aptly.asc] http://repo.aptly.info/ci DIST main
|
||||
|
||||
Where DIST is one of: ``buster``, ``bullseye``, ``bookworm``, ``focal``, ``jammy``, ``noble``
|
||||
Where DIST is one of: ``bullseye``, ``bookworm``, ``trixie``, ``focal``, ``jammy``, ``noble``
|
||||
|
||||
Note: same gpg key is used as for the Upstream Debian Packages.
|
||||
|
||||
@@ -111,10 +111,8 @@ With configuration management systems:
|
||||
|
||||
- `Chef cookbook <https://github.com/hw-cookbooks/aptly>`_ by Aaron Baer
|
||||
(Heavy Water Operations, LLC)
|
||||
- `Puppet module <https://github.com/alphagov/puppet-aptly>`_ by
|
||||
Government Digital Services
|
||||
- `Puppet module <https://github.com/tubemogul/puppet-aptly>`_ by
|
||||
TubeMogul
|
||||
- `Puppet module <https://github.com/voxpupuli/puppet-aptly>`_ by
|
||||
Vox Pupuli
|
||||
- `SaltStack Formula <https://github.com/saltstack-formulas/aptly-formula>`_ by
|
||||
Forrest Alvarez and Brian Jackson
|
||||
- `Ansible role <https://github.com/aioue/ansible-role-aptly>`_ by Tom Paine
|
||||
|
||||
@@ -13,5 +13,6 @@ git push origin v$version master
|
||||
- run swagger locally (`make docker-serve`)
|
||||
- copy generated docs/swagger.json to https://github.com/aptly-dev/www.aptly.info/tree/master/static/swagger/aptly_1.x.y.json
|
||||
- add new version to select tag in content/doc/api/swagger.md line 48
|
||||
- update version in content/download.md
|
||||
- push commit to master
|
||||
- create release announcement on https://github.com/aptly-dev/aptly/discussions
|
||||
|
||||
+2
-2
@@ -70,7 +70,7 @@ func apiReady(isReady *atomic.Value) func(*gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
status := aptlyStatus{Status: "Aptly is ready"}
|
||||
status := aptlyStatus{Status: "Aptly is ready"}
|
||||
c.JSON(200, status)
|
||||
}
|
||||
}
|
||||
@@ -178,7 +178,7 @@ func truthy(value interface{}) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
switch v := value.(type) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
switch strings.ToLower(v) {
|
||||
case "n", "no", "f", "false", "0", "off":
|
||||
|
||||
+41
-2
@@ -13,6 +13,10 @@ import (
|
||||
"github.com/saracen/walker"
|
||||
)
|
||||
|
||||
// syncFile is a seam to allow tests to force fsync failures (e.g. ENOSPC).
|
||||
// In production it calls (*os.File).Sync().
|
||||
var syncFile = func(f *os.File) error { return f.Sync() }
|
||||
|
||||
func verifyPath(path string) bool {
|
||||
path = filepath.Clean(path)
|
||||
for _, part := range strings.Split(path, string(filepath.Separator)) {
|
||||
@@ -114,34 +118,69 @@ func apiFilesUpload(c *gin.Context) {
|
||||
}
|
||||
|
||||
stored := []string{}
|
||||
openFiles := []*os.File{}
|
||||
|
||||
// Write all files first
|
||||
for _, files := range c.Request.MultipartForm.File {
|
||||
for _, file := range files {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
// Close any files we've opened
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = src.Close() }()
|
||||
|
||||
destPath := filepath.Join(path, filepath.Base(file.Filename))
|
||||
dst, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
_ = src.Close()
|
||||
// Close any files we've opened
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = dst.Close() }()
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
_ = src.Close()
|
||||
if err != nil {
|
||||
_ = dst.Close()
|
||||
// Close any files we've opened
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Keep file open for batch sync
|
||||
openFiles = append(openFiles, dst)
|
||||
stored = append(stored, filepath.Join(c.Params.ByName("dir"), filepath.Base(file.Filename)))
|
||||
}
|
||||
}
|
||||
|
||||
// Sync all files at once to catch ENOSPC errors
|
||||
for i, dst := range openFiles {
|
||||
err := syncFile(dst)
|
||||
if err != nil {
|
||||
// Close all files
|
||||
for _, f := range openFiles {
|
||||
_ = f.Close()
|
||||
}
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("error syncing file %s: %s", stored[i], err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Close all files
|
||||
for _, dst := range openFiles {
|
||||
_ = dst.Close()
|
||||
}
|
||||
|
||||
apiFilesUploadedCounter.WithLabelValues(c.Params.ByName("dir")).Inc()
|
||||
c.JSON(200, stored)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
ctx "github.com/aptly-dev/aptly/context"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/smira/flag"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type FilesUploadDiskFullSuite struct {
|
||||
aptlyContext *ctx.AptlyContext
|
||||
flags *flag.FlagSet
|
||||
configFile *os.File
|
||||
router http.Handler
|
||||
}
|
||||
|
||||
var _ = Suite(&FilesUploadDiskFullSuite{})
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) SetUpTest(c *C) {
|
||||
aptly.Version = "testVersion"
|
||||
|
||||
file, err := os.CreateTemp("", "aptly")
|
||||
c.Assert(err, IsNil)
|
||||
s.configFile = file
|
||||
|
||||
jsonString, err := json.Marshal(gin.H{
|
||||
"architectures": []string{},
|
||||
"rootDir": c.MkDir(),
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
_, err = file.Write(jsonString)
|
||||
c.Assert(err, IsNil)
|
||||
_ = file.Close()
|
||||
|
||||
flags := flag.NewFlagSet("fakeFlags", flag.ContinueOnError)
|
||||
flags.Bool("no-lock", false, "dummy")
|
||||
flags.Int("db-open-attempts", 3, "dummy")
|
||||
flags.String("config", s.configFile.Name(), "dummy")
|
||||
flags.String("architectures", "", "dummy")
|
||||
s.flags = flags
|
||||
|
||||
aptlyContext, err := ctx.NewContext(s.flags)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.aptlyContext = aptlyContext
|
||||
s.router = Router(aptlyContext)
|
||||
context = aptlyContext
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TearDownTest(c *C) {
|
||||
if s.configFile != nil {
|
||||
_ = os.Remove(s.configFile.Name())
|
||||
}
|
||||
if s.aptlyContext != nil {
|
||||
s.aptlyContext.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadSuccessWithSync(c *C) {
|
||||
testContent := []byte("test file content for upload")
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "testfile.txt")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = part.Write(testContent)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/testdir", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
uploadedFile := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "testdir", "testfile.txt")
|
||||
content, err := os.ReadFile(uploadedFile)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(content, DeepEquals, testContent)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadVerifiesFileIntegrity(c *C) {
|
||||
testContent := bytes.Repeat([]byte("A"), 10000)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "largefile.bin")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = io.Copy(part, bytes.NewReader(testContent))
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/testdir2", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
uploadedFile := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "testdir2", "largefile.bin")
|
||||
content, err := os.ReadFile(uploadedFile)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(len(content), Equals, len(testContent))
|
||||
c.Check(content, DeepEquals, testContent)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadMultipleFilesWithBatchSync(c *C) {
|
||||
testFiles := map[string][]byte{
|
||||
"file1.txt": []byte("content of file 1"),
|
||||
"file2.txt": bytes.Repeat([]byte("B"), 5000),
|
||||
"file3.deb": []byte("debian package content"),
|
||||
}
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
for filename, content := range testFiles {
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write(content)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
err := writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/multitest", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
uploadDir := filepath.Join(s.aptlyContext.Config().GetRootDir(), "upload", "multitest")
|
||||
for filename, expectedContent := range testFiles {
|
||||
uploadedFile := filepath.Join(uploadDir, filename)
|
||||
content, err := os.ReadFile(uploadedFile)
|
||||
c.Assert(err, IsNil, Commentf("Failed to read %s", filename))
|
||||
c.Check(content, DeepEquals, expectedContent, Commentf("Content mismatch for %s", filename))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturnsErrorOnSyncFailure(c *C) {
|
||||
oldSyncFile := syncFile
|
||||
syncFile = func(f *os.File) error {
|
||||
if filepath.Base(f.Name()) == "syncfail.txt" {
|
||||
return syscall.ENOSPC
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer func() { syncFile = oldSyncFile }()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part1, err := writer.CreateFormFile("file", "ok.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part1.Write([]byte("ok"))
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
part2, err := writer.CreateFormFile("file", "syncfail.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part2.Write([]byte("will fail on sync"))
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = writer.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/syncfaildir", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
c.Check(bytes.Contains(w.Body.Bytes(), []byte("error syncing file")), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestVerifyPath(c *C) {
|
||||
c.Check(verifyPath("a/b/c"), Equals, true)
|
||||
c.Check(verifyPath("../x"), Equals, false)
|
||||
c.Check(verifyPath("./x"), Equals, true)
|
||||
c.Check(verifyPath(".."), Equals, false)
|
||||
c.Check(verifyPath("."), Equals, false)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListDirsEmptyWhenUploadMissing(c *C) {
|
||||
_ = os.RemoveAll(s.aptlyContext.UploadPath())
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
c.Check(strings.TrimSpace(w.Body.String()), Equals, "[]")
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListDirsReturnsDirectories(c *C) {
|
||||
uploadRoot := s.aptlyContext.UploadPath()
|
||||
c.Assert(os.MkdirAll(filepath.Join(uploadRoot, "d1"), 0777), IsNil)
|
||||
c.Assert(os.MkdirAll(filepath.Join(uploadRoot, "d2"), 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(uploadRoot, "rootfile"), []byte("x"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
body := w.Body.String()
|
||||
c.Check(strings.Contains(body, "d1"), Equals, true)
|
||||
c.Check(strings.Contains(body, "d2"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListFilesNotFound(c *C) {
|
||||
req, err := http.NewRequest("GET", "/api/files/does-not-exist", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 404)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListFilesReturnsFiles(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "dir")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "b.txt"), []byte("b"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files/dir", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
body := w.Body.String()
|
||||
c.Check(strings.Contains(body, "a.txt"), Equals, true)
|
||||
c.Check(strings.Contains(body, "b.txt"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteDirRemovesDirectory(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "todel")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/todel", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
_, statErr := os.Stat(base)
|
||||
c.Check(os.IsNotExist(statErr), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteFileRemovesFile(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "todel2")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/todel2/a.txt", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
|
||||
_, statErr := os.Stat(filepath.Join(base, "a.txt"))
|
||||
c.Check(os.IsNotExist(statErr), Equals, true)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteFileNotFoundStillOk(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "todel3")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/todel3/nope.txt", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestRejectsInvalidDir(c *C) {
|
||||
req, err := http.NewRequest("DELETE", "/api/files/..", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestRejectsInvalidFileName(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "dirx")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/dirx/..", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListDirsEmptyIfUploadPathIsNotDir(c *C) {
|
||||
_ = os.RemoveAll(s.aptlyContext.UploadPath())
|
||||
c.Assert(os.WriteFile(s.aptlyContext.UploadPath(), []byte("not a dir"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 200)
|
||||
c.Check(strings.TrimSpace(w.Body.String()), Equals, "[]")
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestListFilesReturns500OnPermissionError(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "noperms")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
c.Assert(os.Chmod(base, 0), IsNil)
|
||||
defer func() { _ = os.Chmod(base, 0777) }()
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/files/noperms", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteFileReturns500OnNonNotExistError(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "dirisfile")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
subdir := filepath.Join(base, "subdir")
|
||||
c.Assert(os.MkdirAll(subdir, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(subdir, "x"), []byte("x"), 0644), IsNil)
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/dirisfile/subdir", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadBadMultipartReturns400(c *C) {
|
||||
req, err := http.NewRequest("POST", "/api/files/badmultipart", bytes.NewBufferString("not multipart"))
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", "multipart/form-data; boundary=missing")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadRejectsInvalidDir(c *C) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "a.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write([]byte("x"))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(writer.Close(), IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/..", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 400)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturns500IfUploadRootIsNotDir(c *C) {
|
||||
_ = os.RemoveAll(s.aptlyContext.UploadPath())
|
||||
c.Assert(os.WriteFile(s.aptlyContext.UploadPath(), []byte("not a dir"), 0644), IsNil)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "a.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write([]byte("x"))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(writer.Close(), IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/testdir", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturns500OnFileOpenFailure(c *C) {
|
||||
// Pre-populate MultipartForm to inject a FileHeader that fails on Open().
|
||||
form := &multipart.Form{
|
||||
File: map[string][]*multipart.FileHeader{
|
||||
"file": {{Filename: "broken.bin"}},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/openfaildir", nil)
|
||||
c.Assert(err, IsNil)
|
||||
req.MultipartForm = form
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestUploadReturns500OnCreateFailure(c *C) {
|
||||
base := filepath.Join(s.aptlyContext.UploadPath(), "readonly")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.Chmod(base, 0555), IsNil)
|
||||
defer func() { _ = os.Chmod(base, 0777) }()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", "a.txt")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = part.Write([]byte("x"))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(writer.Close(), IsNil)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/files/readonly", body)
|
||||
c.Assert(err, IsNil)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
|
||||
func (s *FilesUploadDiskFullSuite) TestDeleteDirReturns500OnRemoveFailure(c *C) {
|
||||
parent := s.aptlyContext.UploadPath()
|
||||
base := filepath.Join(parent, "cantremove")
|
||||
c.Assert(os.MkdirAll(base, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(base, "a.txt"), []byte("a"), 0644), IsNil)
|
||||
|
||||
c.Assert(os.Chmod(parent, 0555), IsNil)
|
||||
defer func() { _ = os.Chmod(parent, 0777) }()
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/api/files/cantremove", nil)
|
||||
c.Assert(err, IsNil)
|
||||
w := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(w, req)
|
||||
c.Assert(w.Code, Equals, 500)
|
||||
}
|
||||
+201
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -12,6 +13,23 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type gpgKeyInfo struct {
|
||||
// 16-character key ID (short form)
|
||||
KeyID string `json:"KeyID" example:"8B48AD6246925553"`
|
||||
// Full fingerprint
|
||||
Fingerprint string `json:"Fingerprint" example:"D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0"`
|
||||
// Key validity (u=unknown, f=fulltrust, m=marginal, n=never)
|
||||
Validity string `json:"Validity" example:"u"`
|
||||
// User ID(s) associated with this key
|
||||
UserIDs []string `json:"UserIDs" example:"John Doe <john@example.com>"`
|
||||
// Creation date (Unix timestamp format from gpg)
|
||||
CreatedAt string `json:"CreatedAt" example:"2023-01-15"`
|
||||
}
|
||||
|
||||
type gpgKeyListResponse struct {
|
||||
Keys []gpgKeyInfo `json:"Keys"`
|
||||
}
|
||||
|
||||
type gpgAddKeyParams struct {
|
||||
// Keyring for adding the keys (default: trustedkeys.gpg)
|
||||
Keyring string `json:"Keyring" example:"trustedkeys.gpg"`
|
||||
@@ -25,6 +43,14 @@ type gpgAddKeyParams struct {
|
||||
GpgKeyID string `json:"GpgKeyID" example:"EF0F382A1A7B6500 8B48AD6246925553"`
|
||||
}
|
||||
|
||||
type gpgDeleteKeyParams struct {
|
||||
// Keyring to delete keys from (default: trustedkeys.gpg)
|
||||
Keyring string `json:"Keyring" example:"trustedkeys.gpg"`
|
||||
|
||||
// Key ID or fingerprint to delete
|
||||
GpgKeyID string `json:"GpgKeyID" example:"8B48AD6246925553"`
|
||||
}
|
||||
|
||||
// @Summary Add GPG Keys
|
||||
// @Description **Adds GPG keys to aptly keyring**
|
||||
// @Description
|
||||
@@ -108,3 +134,178 @@ func apiGPGAddKey(c *gin.Context) {
|
||||
|
||||
c.JSON(200, string(out))
|
||||
}
|
||||
|
||||
// @Summary List GPG Keys
|
||||
// @Description **Lists all GPG keys in aptly keyring**
|
||||
// @Description
|
||||
// @Description Returns all public keys currently installed in the aptly GPG keyring.
|
||||
// @Description
|
||||
// @Tags Mirrors
|
||||
// @Param keyring query string false "Keyring file to list keys from (default: trustedkeys.gpg)" example(trustedkeys.gpg)
|
||||
// @Produce json
|
||||
// @Success 200 {object} gpgKeyListResponse "OK"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Router /api/gpg/keys [get]
|
||||
func apiGPGListKeys(c *gin.Context) {
|
||||
keyring := c.DefaultQuery("keyring", "trustedkeys.gpg")
|
||||
keyring = utils.SanitizePath(keyring)
|
||||
|
||||
finder := pgp.GPGDefaultFinder()
|
||||
gpg, _, err := finder.FindGPG()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--no-default-keyring",
|
||||
"--with-colons",
|
||||
"--keyring", keyring,
|
||||
"--list-keys",
|
||||
}
|
||||
|
||||
cmd := exec.Command(gpg, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("failed to list keys: %s", string(out)))
|
||||
return
|
||||
}
|
||||
|
||||
keys := parseGPGOutput(string(out))
|
||||
c.JSON(200, gpgKeyListResponse{Keys: keys})
|
||||
}
|
||||
|
||||
// @Summary Delete GPG Key
|
||||
// @Description **Deletes a GPG key from aptly keyring**
|
||||
// @Description
|
||||
// @Description Removes a public key from the aptly GPG keyring. This is useful for removing
|
||||
// @Description compromised keys or cleaning up obsolete keys.
|
||||
// @Description
|
||||
// @Tags Mirrors
|
||||
// @Consume json
|
||||
// @Param request body gpgDeleteKeyParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} string "OK"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Router /api/gpg/key [delete]
|
||||
func apiGPGDeleteKey(c *gin.Context) {
|
||||
b := gpgDeleteKeyParams{}
|
||||
if c.Bind(&b) != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(b.GpgKeyID)) == 0 {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("GpgKeyID is required"))
|
||||
return
|
||||
}
|
||||
|
||||
b.GpgKeyID = utils.SanitizePath(b.GpgKeyID)
|
||||
// b.Keyring can be an absolute path
|
||||
|
||||
finder := pgp.GPGDefaultFinder()
|
||||
gpg, _, err := finder.FindGPG()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--no-default-keyring",
|
||||
"--allow-non-selfsigned-uid",
|
||||
"--batch",
|
||||
"--yes",
|
||||
}
|
||||
|
||||
keyring := "trustedkeys.gpg"
|
||||
if len(b.Keyring) > 0 {
|
||||
keyring = b.Keyring
|
||||
}
|
||||
|
||||
args = append(args, "--keyring", keyring)
|
||||
args = append(args, "--delete-keys", b.GpgKeyID)
|
||||
|
||||
cmd := exec.Command(gpg, args...)
|
||||
fmt.Printf("running %s %s\n", gpg, strings.Join(args, " "))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("failed to delete key: %s", string(out)))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, string(out))
|
||||
}
|
||||
|
||||
// parseGPGOutput parses the output of `gpg --with-colons --list-keys`
|
||||
// and returns a structured list of keys
|
||||
func parseGPGOutput(output string) []gpgKeyInfo {
|
||||
var keys []gpgKeyInfo
|
||||
var currentKey *gpgKeyInfo
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(output))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) < 10 {
|
||||
continue
|
||||
}
|
||||
|
||||
recordType := parts[0]
|
||||
|
||||
// pub: public key record
|
||||
if recordType == "pub" {
|
||||
// Save previous key if it exists
|
||||
if currentKey != nil && currentKey.KeyID != "" {
|
||||
keys = append(keys, *currentKey)
|
||||
}
|
||||
|
||||
// Create new key entry
|
||||
// Format: pub:trust:length:algo:keyid:created:expires:uidhash:...
|
||||
keyID := parts[4]
|
||||
if len(keyID) >= 16 {
|
||||
keyID = keyID[len(keyID)-16:] // Last 16 chars = short key ID
|
||||
}
|
||||
validity := parts[1]
|
||||
createdAt := parts[5]
|
||||
|
||||
currentKey = &gpgKeyInfo{
|
||||
KeyID: keyID,
|
||||
Validity: validity,
|
||||
CreatedAt: createdAt,
|
||||
UserIDs: []string{},
|
||||
Fingerprint: "",
|
||||
}
|
||||
}
|
||||
|
||||
// uid: user ID record
|
||||
if recordType == "uid" && currentKey != nil {
|
||||
// Format: uid:trust:created:expires:keyid:uidhash:uidtype:validity:userID:...
|
||||
if len(parts) >= 10 {
|
||||
userID := parts[9]
|
||||
if userID != "" {
|
||||
currentKey.UserIDs = append(currentKey.UserIDs, userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fpr: fingerprint record
|
||||
if recordType == "fpr" && currentKey != nil {
|
||||
// Format: fpr:::::::::fingerprint:
|
||||
if len(parts) >= 10 {
|
||||
fingerprint := parts[9]
|
||||
currentKey.Fingerprint = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last key
|
||||
if currentKey != nil && currentKey.KeyID != "" {
|
||||
keys = append(keys, *currentKey)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type GPGSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&GPGSuite{})
|
||||
|
||||
func (s *GPGSuite) withFakeGPG(c *C, scriptBody string, test func(scriptPath string)) {
|
||||
tempDir, err := os.MkdirTemp("", "aptly-fake-gpg")
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
scriptPath := filepath.Join(tempDir, "gpg")
|
||||
err = os.WriteFile(scriptPath, []byte(scriptBody), 0o755)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
oldPath := os.Getenv("PATH")
|
||||
err = os.Setenv("PATH", tempDir+string(os.PathListSeparator)+oldPath)
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = os.Setenv("PATH", oldPath) }()
|
||||
|
||||
test(scriptPath)
|
||||
}
|
||||
|
||||
func (s *GPGSuite) fakeGPGScript(c *C, listOutput string, deleteOutput string, deleteError string) string {
|
||||
return "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then\n" +
|
||||
" echo 'gpg (GnuPG) 2.2.27'\n" +
|
||||
" exit 0\n" +
|
||||
"fi\n" +
|
||||
"args=\"$*\"\n" +
|
||||
"if printf '%s' \"$args\" | grep -q -- '--list-keys'; then\n" +
|
||||
" cat <<'EOF'\n" + listOutput + "\nEOF\n" +
|
||||
" exit 0\n" +
|
||||
"fi\n" +
|
||||
"if printf '%s' \"$args\" | grep -q -- '--delete-keys'; then\n" +
|
||||
" if [ -n \"" + strings.ReplaceAll(deleteError, "\n", "") + "\" ]; then\n" +
|
||||
" echo '" + strings.ReplaceAll(deleteError, "'", "'\\''") + "'\n" +
|
||||
" exit 1\n" +
|
||||
" fi\n" +
|
||||
" cat <<'EOF'\n" + deleteOutput + "\nEOF\n" +
|
||||
" exit 0\n" +
|
||||
"fi\n" +
|
||||
"echo 'unexpected invocation' >&2\n" +
|
||||
"exit 1\n"
|
||||
}
|
||||
|
||||
// TestParseGPGOutputEmpty tests parsing of empty GPG output
|
||||
func (s *GPGSuite) TestParseGPGOutputEmpty(c *C) {
|
||||
output := ""
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 0)
|
||||
}
|
||||
|
||||
// TestParseGPGOutputSingleKeyMinimal tests parsing a single key with minimal fields
|
||||
func (s *GPGSuite) TestParseGPGOutputSingleKeyMinimal(c *C) {
|
||||
// Minimal valid GPG output with one key
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
|
||||
key := keys[0]
|
||||
c.Check(key.KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(key.Validity, Equals, "u")
|
||||
c.Check(key.CreatedAt, Equals, "1611864000")
|
||||
c.Check(key.Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
c.Check(key.UserIDs, DeepEquals, []string{"John Doe <john@example.com>"})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputMultipleKeys tests parsing multiple keys
|
||||
func (s *GPGSuite) TestParseGPGOutputMultipleKeys(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:
|
||||
pub:f:2048:1:A1B2C3D4E5F67890:1580592000:1612128000:uidhash:::scESC:::::::23::0:
|
||||
uid:f::::1580592000::0987654321::Jane Smith <jane@example.com>::::::::::0:
|
||||
fpr:::::::::E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 2)
|
||||
|
||||
// First key
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(keys[0].Validity, Equals, "u")
|
||||
c.Check(keys[0].UserIDs, DeepEquals, []string{"John Doe <john@example.com>"})
|
||||
|
||||
// Second key
|
||||
c.Check(keys[1].KeyID, Equals, "A1B2C3D4E5F67890")
|
||||
c.Check(keys[1].Validity, Equals, "f")
|
||||
c.Check(keys[1].UserIDs, DeepEquals, []string{"Jane Smith <jane@example.com>"})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputMultipleUIDs tests a key with multiple user IDs
|
||||
func (s *GPGSuite) TestParseGPGOutputMultipleUIDs(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
uid:u::::1611864000::1234567891::John Doe <john.doe@company.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
|
||||
key := keys[0]
|
||||
c.Check(key.UserIDs, HasLen, 2)
|
||||
c.Check(key.UserIDs, DeepEquals, []string{
|
||||
"John Doe <john@example.com>",
|
||||
"John Doe <john.doe@company.com>",
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputMalformedLines tests that malformed lines are skipped
|
||||
func (s *GPGSuite) TestParseGPGOutputMalformedLines(c *C) {
|
||||
// Mix of valid and invalid lines (too few fields)
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
invalid:line:with:only:three:fields
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputEmptyLines tests that empty lines are skipped
|
||||
func (s *GPGSuite) TestParseGPGOutputEmptyLines(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputKeyWithoutUID tests a public key without user ID
|
||||
func (s *GPGSuite) TestParseGPGOutputKeyWithoutUID(c *C) {
|
||||
// Key without uid record (should still be included)
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
|
||||
key := keys[0]
|
||||
c.Check(key.KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(key.UserIDs, HasLen, 0)
|
||||
c.Check(key.Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputVariousValidity tests different validity values
|
||||
func (s *GPGSuite) TestParseGPGOutputVariousValidity(c *C) {
|
||||
output := `pub:u:4096:1:KEY1111111111111:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::Key1::::::::::0:
|
||||
fpr:::::::::1111111111111111111111111111111111111111:
|
||||
pub:f:4096:1:KEY2222222222222:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:f::::1611864000::1234567891::Key2::::::::::0:
|
||||
fpr:::::::::2222222222222222222222222222222222222222:
|
||||
pub:m:4096:1:KEY3333333333333:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:m::::1611864000::1234567892::Key3::::::::::0:
|
||||
fpr:::::::::3333333333333333333333333333333333333333:
|
||||
pub:n:4096:1:KEY4444444444444:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:n::::1611864000::1234567893::Key4::::::::::0:
|
||||
fpr:::::::::4444444444444444444444444444444444444444:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 4)
|
||||
|
||||
validities := []string{"u", "f", "m", "n"}
|
||||
for i, validity := range validities {
|
||||
c.Check(keys[i].Validity, Equals, validity)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseGPGOutputShortKeyID tests that key IDs are shortened to 16 chars
|
||||
func (s *GPGSuite) TestParseGPGOutputShortKeyID(c *C) {
|
||||
// 40-character key ID that should be shortened to last 16 chars
|
||||
longKeyID := "0123456789ABCDEF0123456789ABCDEF8B48AD62"
|
||||
output := `pub:u:4096:1:` + longKeyID + `:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should extract the last 16 characters: 89ABCDEF8B48AD62
|
||||
c.Check(keys[0].KeyID, Equals, "89ABCDEF8B48AD62")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputSpecialCharactersInUID tests user IDs with special characters
|
||||
func (s *GPGSuite) TestParseGPGOutputSpecialCharactersInUID(c *C) {
|
||||
// UID with Unicode characters and special formatting
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::J\xc3\xb6hn D\xc3\xb6\xc3\xa9 (D\xc3\xbcss) <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should preserve the encoded special characters
|
||||
c.Check(keys[0].UserIDs, HasLen, 1)
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysDefaultKeyring tests the HTTP endpoint with default keyring
|
||||
func (s *GPGSuite) TestAPIGPGListKeysDefaultKeyring(c *C) {
|
||||
s.withFakeGPG(c, s.fakeGPGScript(c, `pub:u:4096:1:8B48AD6246925553:1611864000:::::
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`, "", ""), func(_ string) {
|
||||
response, err := s.HTTPRequest("GET", "/api/gpg/keys", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var result gpgKeyListResponse
|
||||
err = json.NewDecoder(response.Body).Decode(&result)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(result.Keys, HasLen, 1)
|
||||
c.Check(result.Keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysWithKeyringParam tests the HTTP endpoint with custom keyring parameter
|
||||
func (s *GPGSuite) TestAPIGPGListKeysWithKeyringParam(c *C) {
|
||||
argFile, err := os.CreateTemp("", "aptly-gpg-args")
|
||||
c.Assert(err, IsNil)
|
||||
_ = argFile.Close()
|
||||
defer func() { _ = os.Remove(argFile.Name()) }()
|
||||
|
||||
script := "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then echo 'gpg (GnuPG) 2.2.27'; exit 0; fi\n" +
|
||||
"printf '%s\n' \"$@\" > '" + argFile.Name() + "'\n" +
|
||||
"if printf '%s' \"$*\" | grep -q -- '--list-keys'; then\n" +
|
||||
"cat <<'EOF'\n" +
|
||||
"pub:u:4096:1:8B48AD6246925553:1611864000:::::\n" +
|
||||
"fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:\n" +
|
||||
"EOF\n" +
|
||||
"exit 0\n" +
|
||||
"fi\n" +
|
||||
"exit 1\n"
|
||||
|
||||
s.withFakeGPG(c, script, func(_ string) {
|
||||
response, reqErr := s.HTTPRequest("GET", "/api/gpg/keys?keyring=/custom.gpg", nil)
|
||||
c.Assert(reqErr, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
argBytes, readErr := os.ReadFile(argFile.Name())
|
||||
c.Assert(readErr, IsNil)
|
||||
c.Check(string(argBytes), Matches, `(?s).*--keyring\ncustom\.gpg\n.*`)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysResponseFormat tests that the response has the correct structure
|
||||
func (s *GPGSuite) TestAPIGPGListKeysResponseFormat(c *C) {
|
||||
s.withFakeGPG(c, s.fakeGPGScript(c, `pub:f:4096:1:A1B2C3D4E5F67890:1611864000:::::
|
||||
uid:f::::1611864000::1234567890::Jane Smith <jane@example.com>::::::::::0:
|
||||
fpr:::::::::E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4:`, "", ""), func(_ string) {
|
||||
response, err := s.HTTPRequest("GET", "/api/gpg/keys", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
|
||||
var result gpgKeyListResponse
|
||||
err = json.NewDecoder(response.Body).Decode(&result)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(result.Keys, HasLen, 1)
|
||||
c.Check(result.Keys[0].KeyID, Equals, "A1B2C3D4E5F67890")
|
||||
c.Check(result.Keys[0].Validity, Equals, "f")
|
||||
c.Check(result.Keys[0].CreatedAt, Equals, "1611864000")
|
||||
})
|
||||
}
|
||||
|
||||
// TestParseGPGOutputEdgeCaseUIDWithoutFields tests UID record with missing fields
|
||||
func (s *GPGSuite) TestParseGPGOutputEdgeCaseUIDWithoutFields(c *C) {
|
||||
// UID record with fewer than 10 fields
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should not have user ID since it's in field 9 and this record is too short
|
||||
c.Check(keys[0].UserIDs, HasLen, 0)
|
||||
}
|
||||
|
||||
// TestParseGPGOutputFingerprintWithoutCurrentKey tests FPR record appearing before any PUB
|
||||
func (s *GPGSuite) TestParseGPGOutputFingerprintWithoutCurrentKey(c *C) {
|
||||
// FPR record without a preceding PUB (should be ignored)
|
||||
output := `fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:
|
||||
pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should only have one key with the correct fingerprint
|
||||
c.Check(keys[0].Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputComplexRealWorldExample tests real-world-like GPG output
|
||||
func (s *GPGSuite) TestParseGPGOutputComplexRealWorldExample(c *C) {
|
||||
// Real-world GPG output with multiple keys, UIDs, and other record types (sig, sub)
|
||||
// Note: sub and sig records are skipped as we only care about pub/uid/fpr
|
||||
realWorldOutput := `tru::1:1611864000:0:3:1:5
|
||||
pub:u:4096:1:8B48AD6246925553:1611864000:2023-01-15T00:00:00:::::scESC:::::::23::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:
|
||||
uid:u::::1611864000::1234567890::John Doe <john@example.com>::::::::::0:
|
||||
uid:u::::1611864100::1234567891::John Doe <john@work.com>::::::::::0:
|
||||
pub:f:2048:1:1234567890123456:1580592000:2022-12-31T00:00:00::u:::scESC:::::::23::0:
|
||||
fpr:::::::::F4E3D2C1B0A9F8E7D6C5B4A3F2E1D0C9:
|
||||
uid:f::::1580592000::0987654321::Maintainer Key <maint@example.com>::::::::::0:`
|
||||
|
||||
keys := parseGPGOutput(realWorldOutput)
|
||||
c.Check(keys, HasLen, 2)
|
||||
|
||||
// First key should have 2 UIDs
|
||||
c.Check(keys[0].KeyID, Equals, "8B48AD6246925553")
|
||||
c.Check(keys[0].UserIDs, HasLen, 2)
|
||||
c.Check(keys[0].Fingerprint, Equals, "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0")
|
||||
|
||||
// Second key should have 1 UID
|
||||
c.Check(keys[1].KeyID, Equals, "1234567890123456")
|
||||
c.Check(keys[1].UserIDs, HasLen, 1)
|
||||
c.Check(keys[1].Fingerprint, Equals, "F4E3D2C1B0A9F8E7D6C5B4A3F2E1D0C9")
|
||||
}
|
||||
|
||||
// TestParseGPGOutputConsecutiveEmptyUIDs tests handling of consecutive empty user ID fields
|
||||
func (s *GPGSuite) TestParseGPGOutputConsecutiveEmptyUIDs(c *C) {
|
||||
output := `pub:u:4096:1:8B48AD6246925553:1611864000:1643400000:uidhash:::scESC:::::::23::0:
|
||||
uid:u::::1611864000::1234567890:::::::::::0:
|
||||
uid:u::::1611864000::1234567891::John Doe <john@example.com>::::::::::0:
|
||||
fpr:::::::::D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0:`
|
||||
|
||||
keys := parseGPGOutput(output)
|
||||
c.Check(keys, HasLen, 1)
|
||||
// Should skip empty UID but include the non-empty one
|
||||
c.Check(keys[0].UserIDs, HasLen, 1)
|
||||
c.Check(keys[0].UserIDs[0], Equals, "John Doe <john@example.com>")
|
||||
}
|
||||
|
||||
// TestGPGDeleteKeyParamsValidation tests gpgDeleteKeyParams validation
|
||||
func (s *GPGSuite) TestGPGDeleteKeyParamsValidation(c *C) {
|
||||
// This is a unit test that validates parameter structure (no HTTP needed)
|
||||
params := gpgDeleteKeyParams{
|
||||
Keyring: "custom.gpg",
|
||||
GpgKeyID: "8B48AD6246925553",
|
||||
}
|
||||
c.Check(params.Keyring, Equals, "custom.gpg")
|
||||
c.Check(params.GpgKeyID, Equals, "8B48AD6246925553")
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeyMissingKeyID tests delete with missing key ID parameter
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeyMissingKeyID(c *C) {
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"Keyring": "trustedkeys.gpg",
|
||||
// GpgKeyID is missing
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeyInvalidJSON tests delete with invalid JSON request
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeyInvalidJSON(c *C) {
|
||||
response, err := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader([]byte("invalid json")))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeySuccess tests successful key deletion
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeySuccess(c *C) {
|
||||
argFile, err := os.CreateTemp("", "aptly-gpg-delete-args")
|
||||
c.Assert(err, IsNil)
|
||||
_ = argFile.Close()
|
||||
defer func() { _ = os.Remove(argFile.Name()) }()
|
||||
|
||||
script := "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then echo 'gpg (GnuPG) 2.2.27'; exit 0; fi\n" +
|
||||
"printf '%s\n' \"$@\" > '" + argFile.Name() + "'\n" +
|
||||
"if printf '%s' \"$*\" | grep -q -- '--delete-keys'; then\n" +
|
||||
"echo 'deleted'\n" +
|
||||
"exit 0\n" +
|
||||
"fi\n" +
|
||||
"exit 1\n"
|
||||
|
||||
s.withFakeGPG(c, script, func(_ string) {
|
||||
body, marshalErr := json.Marshal(gpgDeleteKeyParams{
|
||||
Keyring: "/trustedkeys.gpg",
|
||||
GpgKeyID: "8B48AD6246925553",
|
||||
})
|
||||
c.Assert(marshalErr, IsNil)
|
||||
|
||||
response, reqErr := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader(body))
|
||||
c.Assert(reqErr, IsNil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Matches, `"deleted\\n"`)
|
||||
|
||||
argBytes, readErr := os.ReadFile(argFile.Name())
|
||||
c.Assert(readErr, IsNil)
|
||||
argText := string(argBytes)
|
||||
c.Check(argText, Matches, `(?s).*--batch\n--yes\n.*`)
|
||||
c.Check(argText, Matches, `(?s).*--keyring\n/trustedkeys\.gpg\n.*`)
|
||||
c.Check(argText, Matches, `(?s).*--delete-keys\n8B48AD6246925553\n.*`)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGListKeysCommandFailure tests list error propagation from gpg
|
||||
func (s *GPGSuite) TestAPIGPGListKeysCommandFailure(c *C) {
|
||||
script := "#!/bin/sh\n" +
|
||||
"if [ \"$1\" = \"--version\" ]; then echo 'gpg (GnuPG) 2.2.27'; exit 0; fi\n" +
|
||||
"if printf '%s' \"$*\" | grep -q -- '--list-keys'; then\n" +
|
||||
"echo 'keyring missing'\n" +
|
||||
"exit 1\n" +
|
||||
"fi\n" +
|
||||
"exit 1\n"
|
||||
|
||||
s.withFakeGPG(c, script, func(_ string) {
|
||||
response, err := s.HTTPRequest("GET", "/api/gpg/keys", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Matches, `(?s).*failed to list keys: keyring missing.*`)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIGPGDeleteKeyCommandFailure tests delete error propagation from gpg
|
||||
func (s *GPGSuite) TestAPIGPGDeleteKeyCommandFailure(c *C) {
|
||||
s.withFakeGPG(c, s.fakeGPGScript(c, "", "", "delete failed"), func(_ string) {
|
||||
body, err := json.Marshal(gpgDeleteKeyParams{
|
||||
Keyring: "trustedkeys.gpg",
|
||||
GpgKeyID: "8B48AD6246925553",
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, reqErr := s.HTTPRequest("DELETE", "/api/gpg/key", bytes.NewReader(body))
|
||||
c.Assert(reqErr, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Matches, `(?s).*failed to delete key: delete failed.*`)
|
||||
})
|
||||
}
|
||||
+181
-54
@@ -31,22 +31,61 @@ func getVerifier(keyRings []string) (pgp.Verifier, error) {
|
||||
return verifier, nil
|
||||
}
|
||||
|
||||
// stringSlicesEqual compares two string slices for equality (order matters)
|
||||
func stringSlicesEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// uniqueStrings returns a new slice with only unique strings from the input, sorted
|
||||
func uniqueStrings(input []string) []string {
|
||||
if len(input) == 0 {
|
||||
return input
|
||||
}
|
||||
seen := make(map[string]struct{}, len(input))
|
||||
result := make([]string, 0, len(input))
|
||||
for _, s := range input {
|
||||
if _, ok := seen[s]; !ok {
|
||||
seen[s] = struct{}{}
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// @Summary List Mirrors
|
||||
// @Description **Show list of currently available mirrors**
|
||||
// @Description Each mirror is returned as in “show” API.
|
||||
// @Tags Mirrors
|
||||
// @Produce json
|
||||
// @Success 200 {array} deb.RemoteRepo
|
||||
// @Success 200 {array} remoteRepoResponse
|
||||
// @Router /api/mirrors [get]
|
||||
func apiMirrorsList(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
result := []*deb.RemoteRepo{}
|
||||
_ = collection.ForEach(func(repo *deb.RemoteRepo) error {
|
||||
result = append(result, repo)
|
||||
result := []remoteRepoResponse{}
|
||||
err := collection.ForEach(func(repo *deb.RemoteRepo) error {
|
||||
err := collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = append(result, newRemoteRepoResponse(repo))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to show: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
@@ -72,6 +111,8 @@ type mirrorCreateParams struct {
|
||||
DownloadUdebs bool ` json:"DownloadUdebs"`
|
||||
// Set "true" to mirror installer files
|
||||
DownloadInstaller bool ` json:"DownloadInstaller"`
|
||||
// Set "true" to mirror AppStream (DEP-11) metadata
|
||||
DownloadAppStream bool ` json:"DownloadAppStream"`
|
||||
// Set "true" to include dependencies of matching packages when filtering
|
||||
FilterWithDeps bool ` json:"FilterWithDeps"`
|
||||
// Set "true" to skip if the given components are in the Release file
|
||||
@@ -123,7 +164,7 @@ func apiMirrorsCreate(c *gin.Context) {
|
||||
}
|
||||
|
||||
repo, err := deb.NewRemoteRepo(b.Name, b.ArchiveURL, b.Distribution, b.Components, b.Architectures,
|
||||
b.DownloadSources, b.DownloadUdebs, b.DownloadInstaller)
|
||||
b.DownloadSources, b.DownloadUdebs, b.DownloadInstaller, b.DownloadAppStream)
|
||||
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to create mirror: %s", err))
|
||||
@@ -232,6 +273,7 @@ func apiMirrorsShow(c *gin.Context) {
|
||||
err = collection.LoadComplete(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to show: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, repo)
|
||||
@@ -330,29 +372,133 @@ func apiMirrorsPackages(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
type mirrorEditParams struct {
|
||||
// Package query that is applied to mirror packages
|
||||
Filter *string ` json:"Filter" example:"xserver-xorg"`
|
||||
// Set "true" to include dependencies of matching packages when filtering
|
||||
FilterWithDeps *bool ` json:"FilterWithDeps"`
|
||||
// Set "true" to mirror installer files
|
||||
DownloadInstaller *bool `json:"DownloadInstaller"`
|
||||
// Set "true" to mirror source packages
|
||||
DownloadSources *bool ` json:"DownloadSources"`
|
||||
// Set "true" to mirror udeb files
|
||||
DownloadUdebs *bool ` json:"DownloadUdebs"`
|
||||
// URL of the archive to mirror
|
||||
ArchiveURL *string ` json:"ArchiveURL" example:"http://deb.debian.org/debian"`
|
||||
// Comma separated list of architectures
|
||||
Architectures *[]string `json:"Architectures" example:"amd64"`
|
||||
// Gpg keyring(s) for verifying Release file if a mirror update is required.
|
||||
Keyrings []string ` json:"Keyrings" example:"trustedkeys.gpg"`
|
||||
// Set "true" to skip the verification of Release file signatures
|
||||
IgnoreSignatures *bool ` json:"IgnoreSignatures"`
|
||||
}
|
||||
|
||||
// @Summary Edit Mirror
|
||||
// @Description **Edit mirror config**
|
||||
// @Tags Mirrors
|
||||
// @Param name path string true "mirror name to edit"
|
||||
// @Consume json
|
||||
// @Param request body mirrorEditParams true "Parameters"
|
||||
// @Produce json
|
||||
// @Success 200 {object} deb.RemoteRepo "Mirror was edited successfully"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Mirror not found"
|
||||
// @Failure 409 {object} Error "Aptly db locked"
|
||||
// @Failure 500 {object} Error "Internal Error"
|
||||
// @Router /api/mirrors/{name} [post]
|
||||
func apiMirrorsEdit(c *gin.Context) {
|
||||
var (
|
||||
err error
|
||||
b mirrorEditParams
|
||||
repo *deb.RemoteRepo
|
||||
)
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.RemoteRepoCollection()
|
||||
|
||||
name := c.Params.ByName("name")
|
||||
repo, err = collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = repo.CheckLock()
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 409, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if c.Bind(&b) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fetchMirror := false
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
|
||||
if b.Filter != nil {
|
||||
repo.Filter = *b.Filter
|
||||
}
|
||||
if b.FilterWithDeps != nil {
|
||||
repo.FilterWithDeps = *b.FilterWithDeps
|
||||
}
|
||||
if b.DownloadInstaller != nil {
|
||||
repo.DownloadInstaller = *b.DownloadInstaller
|
||||
}
|
||||
if b.DownloadSources != nil {
|
||||
repo.DownloadSources = *b.DownloadSources
|
||||
}
|
||||
if b.DownloadUdebs != nil {
|
||||
repo.DownloadUdebs = *b.DownloadUdebs
|
||||
}
|
||||
if b.ArchiveURL != nil && *b.ArchiveURL != repo.ArchiveRoot {
|
||||
repo.SetArchiveRoot(*b.ArchiveURL)
|
||||
fetchMirror = true
|
||||
}
|
||||
if b.Architectures != nil {
|
||||
uniqueArchitectures := uniqueStrings(*b.Architectures)
|
||||
if !stringSlicesEqual(uniqueArchitectures, uniqueStrings(repo.Architectures)) {
|
||||
repo.Architectures = uniqueArchitectures
|
||||
fetchMirror = true
|
||||
}
|
||||
}
|
||||
if b.IgnoreSignatures != nil {
|
||||
ignoreSignatures = *b.IgnoreSignatures
|
||||
}
|
||||
|
||||
if repo.IsFlat() && repo.DownloadUdebs {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to edit: flat mirrors don't support udebs"))
|
||||
return
|
||||
}
|
||||
|
||||
if fetchMirror {
|
||||
verifier, err := getVerifier(b.Keyrings)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to initialize GPG verifier: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = repo.Fetch(context.Downloader(), verifier, ignoreSignatures)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = collection.Update(repo)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, fmt.Errorf("unable to edit: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, repo)
|
||||
}
|
||||
|
||||
type mirrorUpdateParams struct {
|
||||
// Change mirror name to `Name`
|
||||
Name string ` json:"Name" example:"mirror1"`
|
||||
// Url of the archive to mirror
|
||||
ArchiveURL string ` json:"ArchiveURL" example:"http://deb.debian.org/debian"`
|
||||
// Package query that is applied to mirror packages
|
||||
Filter string ` json:"Filter" example:"xserver-xorg"`
|
||||
// Limit mirror to those architectures, if not specified aptly would fetch all architectures
|
||||
Architectures []string ` json:"Architectures" example:"amd64"`
|
||||
// Components to mirror, if not specified aptly would fetch all components
|
||||
Components []string ` json:"Components" example:"main"`
|
||||
// Gpg keyring(s) for verifing Release file
|
||||
Keyrings []string ` json:"Keyrings" example:"trustedkeys.gpg"`
|
||||
// Set "true" to include dependencies of matching packages when filtering
|
||||
FilterWithDeps bool ` json:"FilterWithDeps"`
|
||||
// Set "true" to mirror source packages
|
||||
DownloadSources bool ` json:"DownloadSources"`
|
||||
// Set "true" to mirror udeb files
|
||||
DownloadUdebs bool ` json:"DownloadUdebs"`
|
||||
// Set "true" to skip checking if the given components are in the Release file
|
||||
SkipComponentCheck bool ` json:"SkipComponentCheck"`
|
||||
// Set "true" to skip checking if the given architectures are in the Release file
|
||||
SkipArchitectureCheck bool ` json:"SkipArchitectureCheck"`
|
||||
// Set "true" to ignore checksum errors
|
||||
IgnoreChecksums bool ` json:"IgnoreChecksums"`
|
||||
// Set "true" to skip the verification of Release file signatures
|
||||
@@ -361,6 +507,8 @@ type mirrorUpdateParams struct {
|
||||
ForceUpdate bool ` json:"ForceUpdate"`
|
||||
// Set "true" to skip downloading already downloaded packages
|
||||
SkipExistingPackages bool ` json:"SkipExistingPackages"`
|
||||
// Set "true" to download only the latest version per package/architecture
|
||||
LatestOnly bool ` json:"LatestOnly"`
|
||||
}
|
||||
|
||||
// @Summary Update Mirror
|
||||
@@ -394,14 +542,6 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
b.Name = remote.Name
|
||||
b.DownloadUdebs = remote.DownloadUdebs
|
||||
b.DownloadSources = remote.DownloadSources
|
||||
b.SkipComponentCheck = remote.SkipComponentCheck
|
||||
b.SkipArchitectureCheck = remote.SkipArchitectureCheck
|
||||
b.FilterWithDeps = remote.FilterWithDeps
|
||||
b.Filter = remote.Filter
|
||||
b.Architectures = remote.Architectures
|
||||
b.Components = remote.Components
|
||||
b.IgnoreSignatures = context.Config().GpgDisableVerify
|
||||
|
||||
log.Info().Msgf("%s: Starting mirror update", b.Name)
|
||||
@@ -418,27 +558,6 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if b.DownloadUdebs != remote.DownloadUdebs {
|
||||
if remote.IsFlat() && b.DownloadUdebs {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to update: flat mirrors don't support udebs"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if b.ArchiveURL != "" {
|
||||
remote.SetArchiveRoot(b.ArchiveURL)
|
||||
}
|
||||
|
||||
remote.Name = b.Name
|
||||
remote.DownloadUdebs = b.DownloadUdebs
|
||||
remote.DownloadSources = b.DownloadSources
|
||||
remote.SkipComponentCheck = b.SkipComponentCheck
|
||||
remote.SkipArchitectureCheck = b.SkipArchitectureCheck
|
||||
remote.FilterWithDeps = b.FilterWithDeps
|
||||
remote.Filter = b.Filter
|
||||
remote.Architectures = b.Architectures
|
||||
remote.Components = b.Components
|
||||
|
||||
verifier, err := getVerifier(b.Keyrings)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 400, fmt.Errorf("unable to initialize GPG verifier: %s", err))
|
||||
@@ -461,11 +580,19 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
err = remote.DownloadPackageIndexes(out, downloader, verifier, collectionFactory, b.IgnoreSignatures, b.SkipComponentCheck)
|
||||
err = remote.DownloadPackageIndexes(out, downloader, verifier, collectionFactory, b.IgnoreSignatures, remote.SkipComponentCheck)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
if remote.DownloadAppStream && !remote.IsFlat() {
|
||||
err = remote.DownloadAppStreamFiles(out, downloader,
|
||||
context.PackagePool(), collectionFactory.ChecksumCollection(nil), b.IgnoreChecksums)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if remote.Filter != "" {
|
||||
var filterQuery deb.PackageQuery
|
||||
|
||||
@@ -481,7 +608,7 @@ func apiMirrorsUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
queue, downloadSize, err := remote.BuildDownloadQueue(context.PackagePool(), collectionFactory.PackageCollection(),
|
||||
collectionFactory.ChecksumCollection(nil), b.SkipExistingPackages)
|
||||
collectionFactory.ChecksumCollection(nil), b.SkipExistingPackages, b.LatestOnly)
|
||||
if err != nil {
|
||||
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
+66
-1
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
@@ -17,7 +18,10 @@ var _ = Suite(&MirrorSuite{})
|
||||
func (s *MirrorSuite) TestGetMirrors(c *C) {
|
||||
response, _ := s.HTTPRequest("GET", "/api/mirrors", nil)
|
||||
c.Check(response.Code, Equals, 200)
|
||||
c.Check(response.Body.String(), Equals, "[]")
|
||||
|
||||
var mirrors []map[string]interface{}
|
||||
err := json.Unmarshal(response.Body.Bytes(), &mirrors)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestDeleteMirrorNonExisting(c *C) {
|
||||
@@ -26,6 +30,21 @@ func (s *MirrorSuite) TestDeleteMirrorNonExisting(c *C) {
|
||||
c.Check(response.Body.String(), Equals, "{\"error\":\"unable to drop: mirror with name does-not-exist not found\"}")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestCreateMirrorFlatWithAppStream(c *C) {
|
||||
body, err := json.Marshal(gin.H{
|
||||
"Name": "test-flat-appstream",
|
||||
"ArchiveURL": "http://example.com/repo/",
|
||||
"Distribution": "./",
|
||||
"DownloadAppStream": true,
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("POST", "/api/mirrors", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Matches, ".*AppStream.*flat.*")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestCreateMirror(c *C) {
|
||||
c.ExpectFailure("Need to mock downloads")
|
||||
body, err := json.Marshal(gin.H{
|
||||
@@ -38,3 +57,49 @@ func (s *MirrorSuite) TestCreateMirror(c *C) {
|
||||
c.Check(response.Code, Equals, 400)
|
||||
c.Check(response.Body.String(), Equals, "")
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestGetMirrorsIncludesNumPackages(c *C) {
|
||||
collection := s.context.NewCollectionFactory().RemoteRepoCollection()
|
||||
|
||||
repo, err := deb.NewRemoteRepo("count-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = collection.Add(repo)
|
||||
c.Assert(err, IsNil)
|
||||
putRawDBValue(c, &s.APISuite, repo.RefKey(), makePackageRefList(c).Encode())
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/mirrors", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 200)
|
||||
|
||||
var mirrors []map[string]interface{}
|
||||
err = json.Unmarshal(response.Body.Bytes(), &mirrors)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
found := false
|
||||
for _, mirror := range mirrors {
|
||||
if mirror["Name"] == "count-mirror" {
|
||||
found = true
|
||||
value, ok := mirror["NumPackages"]
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(value, Equals, float64(2))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(found, Equals, true)
|
||||
}
|
||||
|
||||
func (s *MirrorSuite) TestGetMirrorsReturns500OnCorruptRefList(c *C) {
|
||||
collection := s.context.NewCollectionFactory().RemoteRepoCollection()
|
||||
|
||||
repo, err := deb.NewRemoteRepo("broken-mirror", "http://example.com/debian", "stable", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(collection.Add(repo), IsNil)
|
||||
putRawDBValue(c, &s.APISuite, repo.RefKey(), []byte("not-msgpack"))
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/mirrors", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 500)
|
||||
c.Assert(response.Body.String(), Matches, ".*unable to show:.*")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package api
|
||||
|
||||
import "github.com/aptly-dev/aptly/deb"
|
||||
|
||||
type remoteRepoResponse struct {
|
||||
*deb.RemoteRepo
|
||||
NumPackages int `json:"NumPackages"`
|
||||
}
|
||||
|
||||
type localRepoResponse struct {
|
||||
*deb.LocalRepo
|
||||
NumPackages int `json:"NumPackages"`
|
||||
}
|
||||
|
||||
type snapshotResponse struct {
|
||||
*deb.Snapshot
|
||||
NumPackages int `json:"NumPackages"`
|
||||
}
|
||||
|
||||
func newRemoteRepoResponse(repo *deb.RemoteRepo) remoteRepoResponse {
|
||||
return remoteRepoResponse{RemoteRepo: repo, NumPackages: repo.NumPackages()}
|
||||
}
|
||||
|
||||
func newLocalRepoResponse(repo *deb.LocalRepo) localRepoResponse {
|
||||
return localRepoResponse{LocalRepo: repo, NumPackages: repo.NumPackages()}
|
||||
}
|
||||
|
||||
func newSnapshotResponse(snapshot *deb.Snapshot) snapshotResponse {
|
||||
return snapshotResponse{Snapshot: snapshot, NumPackages: snapshot.NumPackages()}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func makePackageRefList(c *C) *deb.PackageRefList {
|
||||
list := deb.NewPackageList()
|
||||
c.Assert(list.Add(&deb.Package{Name: "libcount", Version: "1.0", Architecture: "amd64"}), IsNil)
|
||||
c.Assert(list.Add(&deb.Package{Name: "appcount", Version: "2.0", Architecture: "all"}), IsNil)
|
||||
return deb.NewPackageRefListFromPackageList(list)
|
||||
}
|
||||
|
||||
func putRawDBValue(c *C, s *APISuite, key []byte, value []byte) {
|
||||
db, err := s.context.Database()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(db.Put(key, value), IsNil)
|
||||
}
|
||||
+77
-4
@@ -16,8 +16,8 @@ import (
|
||||
type signingParams struct {
|
||||
// Don't sign published repository
|
||||
Skip bool ` json:"Skip" example:"false"`
|
||||
// GPG key ID to use when signing the release, if not specified default key is used
|
||||
GpgKey string ` json:"GpgKey" example:"A0546A43624A8331"`
|
||||
// GPG key ID(s) to use when signing the release, separated by comma, and if not specified, default configured key(s) are used
|
||||
GpgKey string ` json:"GpgKey" example:"KEY_ID_a, KEY_ID_b"`
|
||||
// GPG keyring to use (instead of default)
|
||||
Keyring string ` json:"Keyring" example:"trustedkeys.gpg"`
|
||||
// GPG secret keyring to use (instead of default) Note: depreciated with gpg2
|
||||
@@ -41,7 +41,21 @@ func getSigner(options *signingParams) (pgp.Signer, error) {
|
||||
}
|
||||
|
||||
signer := context.GetSigner()
|
||||
signer.SetKey(options.GpgKey)
|
||||
|
||||
var multiGpgKeys []string
|
||||
// REST params have priority over config
|
||||
if options.GpgKey != "" {
|
||||
for _, p := range strings.Split(options.GpgKey, ",") {
|
||||
if t := strings.TrimSpace(p); t != "" {
|
||||
multiGpgKeys = append(multiGpgKeys, t)
|
||||
}
|
||||
}
|
||||
} else if len(context.Config().GpgKeys) > 0 {
|
||||
multiGpgKeys = context.Config().GpgKeys
|
||||
}
|
||||
for _, gpgKey := range multiGpgKeys {
|
||||
signer.SetKey(gpgKey)
|
||||
}
|
||||
signer.SetKeyRing(options.Keyring, options.SecretKeyring)
|
||||
signer.SetPassphrase(options.Passphrase, options.PassphraseFile)
|
||||
|
||||
@@ -168,8 +182,12 @@ type publishedRepoCreateParams struct {
|
||||
SkipBz2 *bool ` json:"SkipBz2" example:"false"`
|
||||
// Provide index files by hash
|
||||
AcquireByHash *bool ` json:"AcquireByHash" example:"false"`
|
||||
// An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file.
|
||||
SignedBy *string ` json:"SignedBy" example:""`
|
||||
// Enable multiple packages with the same filename in different distributions
|
||||
MultiDist *bool ` json:"MultiDist" example:"false"`
|
||||
// Version of the release
|
||||
Version string ` json:"Version" example:""`
|
||||
}
|
||||
|
||||
// @Summary Create Published Repository
|
||||
@@ -341,6 +359,14 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
|
||||
published.AcquireByHash = *b.AcquireByHash
|
||||
}
|
||||
|
||||
if b.SignedBy != nil {
|
||||
published.SignedBy = *b.SignedBy
|
||||
}
|
||||
|
||||
if b.Version != "" {
|
||||
published.Version = b.Version
|
||||
}
|
||||
|
||||
duplicate := collection.CheckDuplicate(published)
|
||||
if duplicate != nil {
|
||||
_ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
@@ -376,8 +402,16 @@ type publishedRepoUpdateSwitchParams struct {
|
||||
Snapshots []sourceParams ` json:"Snapshots"`
|
||||
// Provide index files by hash
|
||||
AcquireByHash *bool ` json:"AcquireByHash" example:"false"`
|
||||
// An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file
|
||||
SignedBy *string ` json:"SignedBy" example:""`
|
||||
// Enable multiple packages with the same filename in different distributions
|
||||
MultiDist *bool ` json:"MultiDist" example:"false"`
|
||||
// Value of Label: field in published repository stanza
|
||||
Label *string ` json:"Label" example:"Debian"`
|
||||
// Value of Origin: field in published repository stanza
|
||||
Origin *string ` json:"Origin" example:"Debian"`
|
||||
// Version of the release: Optional
|
||||
Version *string ` json:"Version" example:"13.3"`
|
||||
}
|
||||
|
||||
// @Summary Update Published Repository
|
||||
@@ -393,7 +427,6 @@ type publishedRepoUpdateSwitchParams struct {
|
||||
// @Description
|
||||
// @Description See also: `aptly publish update` / `aptly publish switch`
|
||||
// @Tags Publish
|
||||
// @Produce json
|
||||
// @Param prefix path string true "publishing prefix"
|
||||
// @Param distribution path string true "distribution name"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
@@ -462,10 +495,26 @@ func apiPublishUpdateSwitch(c *gin.Context) {
|
||||
published.AcquireByHash = *b.AcquireByHash
|
||||
}
|
||||
|
||||
if b.SignedBy != nil {
|
||||
published.SignedBy = *b.SignedBy
|
||||
}
|
||||
|
||||
if b.MultiDist != nil {
|
||||
published.MultiDist = *b.MultiDist
|
||||
}
|
||||
|
||||
if b.Label != nil {
|
||||
published.Label = *b.Label
|
||||
}
|
||||
|
||||
if b.Origin != nil {
|
||||
published.Origin = *b.Origin
|
||||
}
|
||||
|
||||
if b.Version != nil {
|
||||
published.Version = *b.Version
|
||||
}
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
@@ -955,8 +1004,16 @@ type publishedRepoUpdateParams struct {
|
||||
SkipCleanup *bool ` json:"SkipCleanup" example:"false"`
|
||||
// Provide index files by hash
|
||||
AcquireByHash *bool ` json:"AcquireByHash" example:"false"`
|
||||
// An optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file
|
||||
SignedBy *string ` json:"SignedBy" example:""`
|
||||
// Enable multiple packages with the same filename in different distributions
|
||||
MultiDist *bool ` json:"MultiDist" example:"false"`
|
||||
// Value of Label: field in published repository stanza
|
||||
Label *string ` json:"Label" example:"Debian"`
|
||||
// Value of Origin: field in published repository stanza
|
||||
Origin *string ` json:"Origin" example:"Debian"`
|
||||
// Version of the release: Optional
|
||||
Version *string ` json:"Version" example:"13.3"`
|
||||
}
|
||||
|
||||
// @Summary Update Published Repository
|
||||
@@ -1021,10 +1078,26 @@ func apiPublishUpdate(c *gin.Context) {
|
||||
published.AcquireByHash = *b.AcquireByHash
|
||||
}
|
||||
|
||||
if b.SignedBy != nil {
|
||||
published.SignedBy = *b.SignedBy
|
||||
}
|
||||
|
||||
if b.MultiDist != nil {
|
||||
published.MultiDist = *b.MultiDist
|
||||
}
|
||||
|
||||
if b.Label != nil {
|
||||
published.Label = *b.Label
|
||||
}
|
||||
|
||||
if b.Origin != nil {
|
||||
published.Origin = *b.Origin
|
||||
}
|
||||
|
||||
if b.Version != nil {
|
||||
published.Version = *b.Version
|
||||
}
|
||||
|
||||
resources := []string{string(published.Key())}
|
||||
taskName := fmt.Sprintf("Update published %s repository %s/%s", published.SourceKind, published.StoragePrefix(), published.Distribution)
|
||||
maybeRunTaskInBackground(c, taskName, resources, func(out aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
|
||||
|
||||
+24
-13
@@ -24,7 +24,7 @@ import (
|
||||
// @Tags Repos
|
||||
// @Produce html
|
||||
// @Success 200 {object} string "HTML"
|
||||
// @Router /api/repos [get]
|
||||
// @Router /repos [get]
|
||||
func reposListInAPIMode(localRepos map[string]utils.FileSystemPublishRoot) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -49,7 +49,7 @@ func reposListInAPIMode(localRepos map[string]utils.FileSystemPublishRoot) gin.H
|
||||
// @Param pkgPath path string true "Package Path" allowReserved=true
|
||||
// @Produce json
|
||||
// @Success 200 ""
|
||||
// @Router /api/{storage}/{pkgPath} [get]
|
||||
// @Router /repos/{storage}/{pkgPath} [get]
|
||||
func reposServeInAPIMode(c *gin.Context) {
|
||||
pkgpath := c.Param("pkgPath")
|
||||
|
||||
@@ -69,17 +69,26 @@ func reposServeInAPIMode(c *gin.Context) {
|
||||
// @Description Each repo is returned as in “show” API.
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Success 200 {array} deb.LocalRepo
|
||||
// @Success 200 {array} localRepoResponse
|
||||
// @Router /api/repos [get]
|
||||
func apiReposList(c *gin.Context) {
|
||||
result := []*deb.LocalRepo{}
|
||||
result := []localRepoResponse{}
|
||||
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
_ = collection.ForEach(func(r *deb.LocalRepo) error {
|
||||
result = append(result, r)
|
||||
err := collection.ForEach(func(r *deb.LocalRepo) error {
|
||||
err := collection.LoadComplete(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = append(result, newLocalRepoResponse(r))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
@@ -195,17 +204,18 @@ func apiReposEdit(c *gin.Context) {
|
||||
collectionFactory := context.NewCollectionFactory()
|
||||
collection := collectionFactory.LocalRepoCollection()
|
||||
|
||||
repo, err := collection.ByName(c.Params.ByName("name"))
|
||||
name := c.Params.ByName("name")
|
||||
repo, err := collection.ByName(name)
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 404, err)
|
||||
return
|
||||
}
|
||||
|
||||
if b.Name != nil {
|
||||
if b.Name != nil && *b.Name != name {
|
||||
_, err := collection.ByName(*b.Name)
|
||||
if err == nil {
|
||||
// already exists
|
||||
AbortWithJSONError(c, 404, err)
|
||||
AbortWithJSONError(c, 404, fmt.Errorf("local repo with name %q already exists", *b.Name))
|
||||
return
|
||||
}
|
||||
repo.Name = *b.Name
|
||||
@@ -410,6 +420,7 @@ func apiReposPackagesAddDelete(c *gin.Context, taskNamePrefix string, cb func(li
|
||||
// @Description API verifies that packages actually exist in aptly database and checks constraint that conflicting packages can’t be part of the same local repository.
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Consume json
|
||||
// @Param request body reposPackagesAddDeleteParams true "Parameters"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
@@ -455,7 +466,7 @@ func apiReposPackagesDelete(c *gin.Context) {
|
||||
// @Tags Repos
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param dir path string true "Directory of packages"
|
||||
// @Param file path string false "Filename (optional)"
|
||||
// @Param file path string true "Filename"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "OK"
|
||||
@@ -613,11 +624,11 @@ type reposCopyPackageParams struct {
|
||||
// @Summary Copy Package
|
||||
// @Description Copies a package from a source to destination repository
|
||||
// @Tags Repos
|
||||
// @Produce json
|
||||
// @Param name path string true "Destination repo"
|
||||
// @Param src path string true "Source repo"
|
||||
// @Param file path string true "File/packages to copy"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 200 {object} task.ProcessReturnValue "msg"
|
||||
// @Failure 400 {object} Error "Bad Request"
|
||||
// @Failure 404 {object} Error "Not Found"
|
||||
@@ -901,10 +912,10 @@ func apiReposIncludePackageFromDir(c *gin.Context) {
|
||||
out.Printf("Failed files: %s\n", strings.Join(failedFiles, ", "))
|
||||
}
|
||||
|
||||
ret := reposIncludePackageFromDirResponse{
|
||||
ret := reposIncludePackageFromDirResponse{
|
||||
Report: reporter,
|
||||
FailedFiles: failedFiles,
|
||||
}
|
||||
}
|
||||
return &task.ProcessReturnValue{Code: http.StatusOK, Value: ret}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
"github.com/gin-gonic/gin"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type ReposSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&ReposSuite{})
|
||||
|
||||
func (s *ReposSuite) TestGetReposIncludesNumPackages(c *C) {
|
||||
collection := s.context.NewCollectionFactory().LocalRepoCollection()
|
||||
repo := deb.NewLocalRepo("count-repo-list", "")
|
||||
repo.UpdateRefList(makePackageRefList(c))
|
||||
c.Assert(collection.Add(repo), IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/repos", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 200)
|
||||
|
||||
var repos []map[string]interface{}
|
||||
err = json.Unmarshal(response.Body.Bytes(), &repos)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
found := false
|
||||
for _, repo := range repos {
|
||||
if repo["Name"] == "count-repo-list" {
|
||||
found = true
|
||||
value, ok := repo["NumPackages"]
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(value, Equals, float64(2))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(found, Equals, true)
|
||||
}
|
||||
|
||||
func (s *ReposSuite) TestGetReposReturns500OnCorruptRefList(c *C) {
|
||||
body, err := json.Marshal(gin.H{"Name": "broken-repo-list"})
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("POST", "/api/repos", bytes.NewReader(body))
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 201)
|
||||
|
||||
collection := s.context.NewCollectionFactory().LocalRepoCollection()
|
||||
repo, err := collection.ByName("broken-repo-list")
|
||||
c.Assert(err, IsNil)
|
||||
putRawDBValue(c, &s.APISuite, repo.RefKey(), []byte("not-msgpack"))
|
||||
|
||||
response, err = s.HTTPRequest("GET", "/api/repos", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 500)
|
||||
c.Assert(response.Body.String(), Matches, ".*msgpack.*|.*decode.*")
|
||||
}
|
||||
+20
-11
@@ -11,13 +11,19 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/aptly-dev/aptly/docs"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
// _ "github.com/aptly-dev/aptly/docs" // import docs
|
||||
// swaggerFiles "github.com/swaggo/files"
|
||||
// ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
var context *ctx.AptlyContext
|
||||
|
||||
// @Summary Get Metrics
|
||||
// @Description **Get Prometheus Metrics**
|
||||
// @Tags Status
|
||||
// @Produce text/plain
|
||||
// @Success 200 {string} string Metrics
|
||||
// @Router /api/metrics [get]
|
||||
func apiMetricsGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
countPackagesByRepos()
|
||||
@@ -63,14 +69,14 @@ func Router(c *ctx.AptlyContext) http.Handler {
|
||||
|
||||
router.Use(gin.Recovery(), gin.ErrorLogger())
|
||||
|
||||
if c.Config().EnableSwaggerEndpoint {
|
||||
router.GET("docs.html", func(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", docs.DocsHTML)
|
||||
})
|
||||
router.Use(redirectSwagger)
|
||||
url := ginSwagger.URL("/docs/doc.json")
|
||||
router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
|
||||
}
|
||||
// if c.Config().EnableSwaggerEndpoint {
|
||||
// router.GET("docs.html", func(c *gin.Context) {
|
||||
// c.Data(http.StatusOK, "text/html; charset=utf-8", docs.DocsHTML)
|
||||
// })
|
||||
// router.Use(redirectSwagger)
|
||||
// url := ginSwagger.URL("/docs/doc.json")
|
||||
// router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))
|
||||
// }
|
||||
|
||||
if c.Config().EnableMetricsEndpoint {
|
||||
MetricsCollectorRegistrar.Register(router)
|
||||
@@ -158,12 +164,15 @@ func Router(c *ctx.AptlyContext) http.Handler {
|
||||
api.GET("/mirrors/:name", apiMirrorsShow)
|
||||
api.GET("/mirrors/:name/packages", apiMirrorsPackages)
|
||||
api.POST("/mirrors", apiMirrorsCreate)
|
||||
api.POST("/mirrors/:name", apiMirrorsEdit)
|
||||
api.PUT("/mirrors/:name", apiMirrorsUpdate)
|
||||
api.DELETE("/mirrors/:name", apiMirrorsDrop)
|
||||
}
|
||||
|
||||
{
|
||||
api.GET("/gpg/keys", apiGPGListKeys)
|
||||
api.POST("/gpg/key", apiGPGAddKey)
|
||||
api.DELETE("/gpg/key", apiGPGDeleteKey)
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
+14
-6
@@ -20,7 +20,7 @@ import (
|
||||
// @Description Each snapshot is returned as in “show” API.
|
||||
// @Tags Snapshots
|
||||
// @Produce json
|
||||
// @Success 200 {array} deb.Snapshot
|
||||
// @Success 200 {array} snapshotResponse
|
||||
// @Router /api/snapshots [get]
|
||||
func apiSnapshotsList(c *gin.Context) {
|
||||
SortMethodString := c.Request.URL.Query().Get("sort")
|
||||
@@ -32,11 +32,20 @@ func apiSnapshotsList(c *gin.Context) {
|
||||
SortMethodString = "name"
|
||||
}
|
||||
|
||||
result := []*deb.Snapshot{}
|
||||
_ = collection.ForEachSorted(SortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
result = append(result, snapshot)
|
||||
result := []snapshotResponse{}
|
||||
err := collection.ForEachSorted(SortMethodString, func(snapshot *deb.Snapshot) error {
|
||||
err := collection.LoadComplete(snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result = append(result, newSnapshotResponse(snapshot))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
AbortWithJSONError(c, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
}
|
||||
@@ -217,10 +226,9 @@ type snapshotsCreateFromRepositoryParams struct {
|
||||
// @Summary Snapshot Repository
|
||||
// @Description **Create a snapshot of a repository by name**
|
||||
// @Tags Snapshots
|
||||
// @Param name path string true "Repository name"
|
||||
// @Consume json
|
||||
// @Param request body snapshotsCreateFromRepositoryParams true "Parameters"
|
||||
// @Param name path string true "Name of the snapshot"
|
||||
// @Param name path string true "Repository name"
|
||||
// @Param _async query bool false "Run in background and return task object"
|
||||
// @Produce json
|
||||
// @Success 201 {object} deb.Snapshot "Created snapshot object"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/aptly-dev/aptly/deb"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type SnapshotsSuite struct {
|
||||
APISuite
|
||||
}
|
||||
|
||||
var _ = Suite(&SnapshotsSuite{})
|
||||
|
||||
func (s *SnapshotsSuite) TestGetSnapshotsIncludesNumPackages(c *C) {
|
||||
collection := s.context.NewCollectionFactory().SnapshotCollection()
|
||||
snapshot := deb.NewSnapshotFromRefList("count-snapshot-list", nil, makePackageRefList(c), "")
|
||||
c.Assert(collection.Add(snapshot), IsNil)
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/snapshots", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 200)
|
||||
|
||||
var snapshots []map[string]interface{}
|
||||
err = json.Unmarshal(response.Body.Bytes(), &snapshots)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
found := false
|
||||
for _, snapshot := range snapshots {
|
||||
if snapshot["Name"] == "count-snapshot-list" {
|
||||
found = true
|
||||
value, ok := snapshot["NumPackages"]
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(value, Equals, float64(2))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(found, Equals, true)
|
||||
}
|
||||
|
||||
func (s *SnapshotsSuite) TestGetSnapshotsReturns500OnCorruptRefList(c *C) {
|
||||
collection := s.context.NewCollectionFactory().SnapshotCollection()
|
||||
snapshot := deb.NewSnapshotFromRefList("broken-snapshot-list", nil, makePackageRefList(c), "")
|
||||
c.Assert(collection.Add(snapshot), IsNil)
|
||||
putRawDBValue(c, &s.APISuite, snapshot.RefKey(), []byte("not-msgpack"))
|
||||
|
||||
response, err := s.HTTPRequest("GET", "/api/snapshots", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(response.Code, Equals, 500)
|
||||
c.Assert(response.Body.String(), Matches, ".*msgpack.*|.*decode.*")
|
||||
}
|
||||
+41
-48
@@ -5,35 +5,28 @@ package azure
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
)
|
||||
|
||||
func isBlobNotFound(err error) bool {
|
||||
var respErr *azcore.ResponseError
|
||||
if errors.As(err, &respErr) {
|
||||
return respErr.StatusCode == 404 // BlobNotFound
|
||||
}
|
||||
return false
|
||||
storageError, ok := err.(azblob.StorageError)
|
||||
return ok && storageError.ServiceCode() == azblob.ServiceCodeBlobNotFound
|
||||
}
|
||||
|
||||
type azContext struct {
|
||||
client *azblob.Client
|
||||
container string
|
||||
container azblob.ContainerURL
|
||||
prefix string
|
||||
}
|
||||
|
||||
func newAzContext(accountName, accountKey, container, prefix, endpoint string) (*azContext, error) {
|
||||
cred, err := azblob.NewSharedKeyCredential(accountName, accountKey)
|
||||
credential, err := azblob.NewSharedKeyCredential(accountName, accountKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -42,14 +35,15 @@ func newAzContext(accountName, accountKey, container, prefix, endpoint string) (
|
||||
endpoint = fmt.Sprintf("https://%s.blob.core.windows.net", accountName)
|
||||
}
|
||||
|
||||
serviceClient, err := azblob.NewClientWithSharedKeyCredential(endpoint, cred, nil)
|
||||
url, err := url.Parse(fmt.Sprintf("%s/%s", endpoint, container))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containerURL := azblob.NewContainerURL(*url, azblob.NewPipeline(credential, azblob.PipelineOptions{}))
|
||||
|
||||
result := &azContext{
|
||||
client: serviceClient,
|
||||
container: container,
|
||||
container: containerURL,
|
||||
prefix: prefix,
|
||||
}
|
||||
|
||||
@@ -60,6 +54,10 @@ func (az *azContext) blobPath(path string) string {
|
||||
return filepath.Join(az.prefix, path)
|
||||
}
|
||||
|
||||
func (az *azContext) blobURL(path string) azblob.BlobURL {
|
||||
return az.container.NewBlobURL(az.blobPath(path))
|
||||
}
|
||||
|
||||
func (az *azContext) internalFilelist(prefix string, progress aptly.Progress) (paths []string, md5s []string, err error) {
|
||||
const delimiter = "/"
|
||||
paths = make([]string, 0, 1024)
|
||||
@@ -69,33 +67,27 @@ func (az *azContext) internalFilelist(prefix string, progress aptly.Progress) (p
|
||||
prefix += delimiter
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
maxResults := int32(1)
|
||||
pager := az.client.NewListBlobsFlatPager(az.container, &azblob.ListBlobsFlatOptions{
|
||||
Prefix: &prefix,
|
||||
MaxResults: &maxResults,
|
||||
Include: azblob.ListBlobsInclude{Metadata: true},
|
||||
})
|
||||
|
||||
// Iterate over each page
|
||||
for pager.More() {
|
||||
page, err := pager.NextPage(ctx)
|
||||
for marker := (azblob.Marker{}); marker.NotDone(); {
|
||||
listBlob, err := az.container.ListBlobsFlatSegment(
|
||||
context.Background(), marker, azblob.ListBlobsSegmentOptions{
|
||||
Prefix: prefix,
|
||||
MaxResults: 1,
|
||||
Details: azblob.BlobListingDetails{Metadata: true}})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error listing under prefix %s in %s: %s", prefix, az, err)
|
||||
}
|
||||
|
||||
for _, blob := range page.Segment.BlobItems {
|
||||
if prefix == "" {
|
||||
paths = append(paths, *blob.Name)
|
||||
} else {
|
||||
name := *blob.Name
|
||||
paths = append(paths, name[len(prefix):])
|
||||
}
|
||||
b := *blob
|
||||
md5 := b.Properties.ContentMD5
|
||||
md5s = append(md5s, fmt.Sprintf("%x", md5))
|
||||
marker = listBlob.NextMarker
|
||||
|
||||
for _, blob := range listBlob.Segment.BlobItems {
|
||||
if prefix == "" {
|
||||
paths = append(paths, blob.Name)
|
||||
} else {
|
||||
paths = append(paths, blob.Name[len(prefix):])
|
||||
}
|
||||
md5s = append(md5s, fmt.Sprintf("%x", blob.Properties.ContentMD5))
|
||||
}
|
||||
|
||||
if progress != nil {
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
progress.AddBar(1)
|
||||
@@ -105,27 +97,28 @@ func (az *azContext) internalFilelist(prefix string, progress aptly.Progress) (p
|
||||
return paths, md5s, nil
|
||||
}
|
||||
|
||||
func (az *azContext) putFile(blobName string, source io.Reader, sourceMD5 string) error {
|
||||
uploadOptions := &azblob.UploadFileOptions{
|
||||
BlockSize: 4 * 1024 * 1024,
|
||||
Concurrency: 8,
|
||||
func (az *azContext) putFile(blob azblob.BlobURL, source io.Reader, sourceMD5 string) error {
|
||||
uploadOptions := azblob.UploadStreamToBlockBlobOptions{
|
||||
BufferSize: 4 * 1024 * 1024,
|
||||
MaxBuffers: 8,
|
||||
}
|
||||
|
||||
path := az.blobPath(blobName)
|
||||
if len(sourceMD5) > 0 {
|
||||
decodedMD5, err := hex.DecodeString(sourceMD5)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uploadOptions.HTTPHeaders = &blob.HTTPHeaders{
|
||||
BlobContentMD5: decodedMD5,
|
||||
uploadOptions.BlobHTTPHeaders = azblob.BlobHTTPHeaders{
|
||||
ContentMD5: decodedMD5,
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if file, ok := source.(*os.File); ok {
|
||||
_, err = az.client.UploadFile(context.TODO(), az.container, path, file, uploadOptions)
|
||||
}
|
||||
_, err := azblob.UploadStreamToBlockBlob(
|
||||
context.Background(),
|
||||
source,
|
||||
blob.ToBlockBlobURL(),
|
||||
uploadOptions,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
+27
-24
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/pkg/errors"
|
||||
@@ -29,7 +30,7 @@ func NewPackagePool(accountName, accountKey, container, prefix, endpoint string)
|
||||
return &PackagePool{az: azctx}, nil
|
||||
}
|
||||
|
||||
// String returns the storage as string
|
||||
// String
|
||||
func (pool *PackagePool) String() string {
|
||||
return pool.az.String()
|
||||
}
|
||||
@@ -40,7 +41,10 @@ func (pool *PackagePool) buildPoolPath(filename string, checksums *utils.Checksu
|
||||
return filepath.Join(hash[0:2], hash[2:4], hash[4:32]+"_"+filename)
|
||||
}
|
||||
|
||||
func (pool *PackagePool) ensureChecksums(poolPath string, checksumStorage aptly.ChecksumStorage) (*utils.ChecksumInfo, error) {
|
||||
func (pool *PackagePool) ensureChecksums(
|
||||
poolPath string,
|
||||
checksumStorage aptly.ChecksumStorage,
|
||||
) (*utils.ChecksumInfo, error) {
|
||||
targetChecksums, err := checksumStorage.Get(poolPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -48,7 +52,8 @@ func (pool *PackagePool) ensureChecksums(poolPath string, checksumStorage aptly.
|
||||
|
||||
if targetChecksums == nil {
|
||||
// we don't have checksums stored yet for this file
|
||||
download, err := pool.az.client.DownloadStream(context.Background(), pool.az.container, poolPath, nil)
|
||||
blob := pool.az.blobURL(poolPath)
|
||||
download, err := blob.Download(context.Background(), 0, 0, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
if isBlobNotFound(err) {
|
||||
return nil, nil
|
||||
@@ -58,7 +63,7 @@ func (pool *PackagePool) ensureChecksums(poolPath string, checksumStorage aptly.
|
||||
}
|
||||
|
||||
targetChecksums = &utils.ChecksumInfo{}
|
||||
*targetChecksums, err = utils.ChecksumsForReader(download.Body)
|
||||
*targetChecksums, err = utils.ChecksumsForReader(download.Body(azblob.RetryReaderOptions{}))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error checksumming blob at %s", poolPath)
|
||||
}
|
||||
@@ -87,49 +92,46 @@ func (pool *PackagePool) LegacyPath(_ string, _ *utils.ChecksumInfo) (string, er
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Size(path string) (int64, error) {
|
||||
serviceClient := pool.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(pool.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
|
||||
props, err := blobClient.GetProperties(context.TODO(), nil)
|
||||
blob := pool.az.blobURL(path)
|
||||
props, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error examining %s from %s", path, pool)
|
||||
}
|
||||
|
||||
return *props.ContentLength, nil
|
||||
return props.ContentLength(), nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Open(path string) (aptly.ReadSeekerCloser, error) {
|
||||
blob := pool.az.blobURL(path)
|
||||
|
||||
temp, err := os.CreateTemp("", "blob-download")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating tempfile for %s", path)
|
||||
return nil, errors.Wrap(err, "error creating temporary file for blob download")
|
||||
}
|
||||
defer func () { _ = os.Remove(temp.Name()) }()
|
||||
|
||||
_, err = pool.az.client.DownloadFile(context.TODO(), pool.az.container, path, temp, nil)
|
||||
defer os.Remove(temp.Name())
|
||||
|
||||
err = azblob.DownloadBlobToFile(context.Background(), blob, 0, 0, temp, azblob.DownloadFromBlobOptions{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error downloading blob %s", path)
|
||||
return nil, errors.Wrapf(err, "error downloading blob at %s", path)
|
||||
}
|
||||
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Remove(path string) (int64, error) {
|
||||
serviceClient := pool.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(pool.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
|
||||
props, err := blobClient.GetProperties(context.TODO(), nil)
|
||||
blob := pool.az.blobURL(path)
|
||||
props, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error examining %s from %s", path, pool)
|
||||
return 0, errors.Wrapf(err, "error getting props of %s from %s", path, pool)
|
||||
}
|
||||
|
||||
_, err = pool.az.client.DeleteBlob(context.Background(), pool.az.container, path, nil)
|
||||
_, err = blob.Delete(context.Background(), azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{})
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "error deleting %s from %s", path, pool)
|
||||
}
|
||||
|
||||
return *props.ContentLength, nil
|
||||
return props.ContentLength(), nil
|
||||
}
|
||||
|
||||
func (pool *PackagePool) Import(srcPath, basename string, checksums *utils.ChecksumInfo, _ bool, checksumStorage aptly.ChecksumStorage) (string, error) {
|
||||
@@ -143,6 +145,7 @@ func (pool *PackagePool) Import(srcPath, basename string, checksums *utils.Check
|
||||
}
|
||||
|
||||
path := pool.buildPoolPath(basename, checksums)
|
||||
blob := pool.az.blobURL(path)
|
||||
targetChecksums, err := pool.ensureChecksums(path, checksumStorage)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -156,9 +159,9 @@ func (pool *PackagePool) Import(srcPath, basename string, checksums *utils.Check
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() { _ = source.Close() }()
|
||||
defer source.Close()
|
||||
|
||||
err = pool.az.putFile(path, source, checksums.MD5)
|
||||
err = pool.az.putFile(blob, source, checksums.MD5)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
@@ -50,10 +50,8 @@ func (s *PackagePoolSuite) SetUpTest(c *C) {
|
||||
|
||||
s.pool, err = NewPackagePool(s.accountName, s.accountKey, container, "", s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.pool.az.client.CreateContainer(context.TODO(), s.pool.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
cnt := s.pool.az.container
|
||||
_, err = cnt.Create(context.Background(), azblob.Metadata{}, azblob.PublicAccessContainer)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedPool, err = NewPackagePool(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
@@ -69,8 +67,8 @@ func (s *PackagePoolSuite) TestFilepathList(c *C) {
|
||||
c.Check(err, IsNil)
|
||||
c.Check(list, DeepEquals, []string{})
|
||||
|
||||
_, _ = s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
_, _ = s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
|
||||
list, err = s.pool.FilepathList(nil)
|
||||
c.Check(err, IsNil)
|
||||
@@ -81,8 +79,8 @@ func (s *PackagePoolSuite) TestFilepathList(c *C) {
|
||||
}
|
||||
|
||||
func (s *PackagePoolSuite) TestRemove(c *C) {
|
||||
_, _ = s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
_, _ = s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
s.pool.Import(s.debFile, "a.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
s.pool.Import(s.debFile, "b.deb", &utils.ChecksumInfo{}, false, s.cs)
|
||||
|
||||
size, err := s.pool.Remove("c7/6b/4bd12fd92e4dfe1b55b18a67a669_a.deb")
|
||||
c.Check(err, IsNil)
|
||||
@@ -247,7 +245,7 @@ func (s *PackagePoolSuite) TestOpen(c *C) {
|
||||
|
||||
f, err := s.pool.Open(path)
|
||||
c.Assert(err, IsNil)
|
||||
contents, err := io.ReadAll(f)
|
||||
contents, err := ioutil.ReadAll(f)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(len(contents), Equals, 2738)
|
||||
c.Check(f.Close(), IsNil)
|
||||
|
||||
+62
-73
@@ -3,22 +3,21 @@ package azure
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/lease"
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PublishedStorage abstract file system with published files (actually hosted on Azure)
|
||||
type PublishedStorage struct {
|
||||
// FIXME: unused ???? prefix string
|
||||
container azblob.ContainerURL
|
||||
prefix string
|
||||
az *azContext
|
||||
pathCache map[string]map[string]string
|
||||
}
|
||||
@@ -38,7 +37,7 @@ func NewPublishedStorage(accountName, accountKey, container, prefix, endpoint st
|
||||
return &PublishedStorage{az: azctx}, nil
|
||||
}
|
||||
|
||||
// String returns the storage as string
|
||||
// String
|
||||
func (storage *PublishedStorage) String() string {
|
||||
return storage.az.String()
|
||||
}
|
||||
@@ -65,9 +64,9 @@ func (storage *PublishedStorage) PutFile(path string, sourceFilename string) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = source.Close() }()
|
||||
defer source.Close()
|
||||
|
||||
err = storage.az.putFile(path, source, sourceMD5)
|
||||
err = storage.az.putFile(storage.az.blobURL(path), source, sourceMD5)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("error uploading %s to %s", sourceFilename, storage))
|
||||
}
|
||||
@@ -77,15 +76,14 @@ func (storage *PublishedStorage) PutFile(path string, sourceFilename string) err
|
||||
|
||||
// RemoveDirs removes directory structure under public path
|
||||
func (storage *PublishedStorage) RemoveDirs(path string, _ aptly.Progress) error {
|
||||
path = storage.az.blobPath(path)
|
||||
filelist, err := storage.Filelist(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, filename := range filelist {
|
||||
blob := filepath.Join(path, filename)
|
||||
_, err := storage.az.client.DeleteBlob(context.Background(), storage.az.container, blob, nil)
|
||||
blob := storage.az.blobURL(filepath.Join(path, filename))
|
||||
_, err := blob.Delete(context.Background(), azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting path %s from %s: %s", filename, storage, err)
|
||||
}
|
||||
@@ -96,8 +94,8 @@ func (storage *PublishedStorage) RemoveDirs(path string, _ aptly.Progress) error
|
||||
|
||||
// Remove removes single file under public path
|
||||
func (storage *PublishedStorage) Remove(path string) error {
|
||||
path = storage.az.blobPath(path)
|
||||
_, err := storage.az.client.DeleteBlob(context.Background(), storage.az.container, path, nil)
|
||||
blob := storage.az.blobURL(path)
|
||||
_, err := blob.Delete(context.Background(), azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{})
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("error deleting %s from %s: %s", path, storage, err))
|
||||
}
|
||||
@@ -116,8 +114,9 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
sourcePath string, sourceChecksums utils.ChecksumInfo, force bool) error {
|
||||
|
||||
relFilePath := filepath.Join(publishedRelPath, fileName)
|
||||
prefixRelFilePath := filepath.Join(publishedPrefix, relFilePath)
|
||||
poolPath := storage.az.blobPath(prefixRelFilePath)
|
||||
// prefixRelFilePath := filepath.Join(publishedPrefix, relFilePath)
|
||||
// FIXME: check how to integrate publishedPrefix:
|
||||
poolPath := storage.az.blobPath(fileName)
|
||||
|
||||
if storage.pathCache == nil {
|
||||
storage.pathCache = make(map[string]map[string]string)
|
||||
@@ -158,9 +157,9 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = source.Close() }()
|
||||
defer source.Close()
|
||||
|
||||
err = storage.az.putFile(relFilePath, source, sourceMD5)
|
||||
err = storage.az.putFile(storage.az.blobURL(relFilePath), source, sourceMD5)
|
||||
if err == nil {
|
||||
pathCache[relFilePath] = sourceMD5
|
||||
} else {
|
||||
@@ -177,60 +176,57 @@ func (storage *PublishedStorage) Filelist(prefix string) ([]string, error) {
|
||||
}
|
||||
|
||||
// Internal copy or move implementation
|
||||
func (storage *PublishedStorage) internalCopyOrMoveBlob(src, dst string, metadata map[string]*string, move bool) error {
|
||||
func (storage *PublishedStorage) internalCopyOrMoveBlob(src, dst string, metadata azblob.Metadata, move bool) error {
|
||||
const leaseDuration = 30
|
||||
leaseID := uuid.NewString()
|
||||
|
||||
serviceClient := storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(storage.az.container)
|
||||
srcBlobClient := containerClient.NewBlobClient(src)
|
||||
blobLeaseClient, err := lease.NewBlobClient(srcBlobClient, &lease.BlobClientOptions{LeaseID: to.Ptr(leaseID)})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error acquiring lease on source blob %s", src)
|
||||
dstBlobURL := storage.az.blobURL(dst)
|
||||
srcBlobURL := storage.az.blobURL(src)
|
||||
leaseResp, err := srcBlobURL.AcquireLease(context.Background(), "", leaseDuration, azblob.ModifiedAccessConditions{})
|
||||
if err != nil || leaseResp.StatusCode() != http.StatusCreated {
|
||||
return fmt.Errorf("error acquiring lease on source blob %s", srcBlobURL)
|
||||
}
|
||||
defer srcBlobURL.BreakLease(context.Background(), azblob.LeaseBreakNaturally, azblob.ModifiedAccessConditions{})
|
||||
srcBlobLeaseID := leaseResp.LeaseID()
|
||||
|
||||
_, err = blobLeaseClient.AcquireLease(context.Background(), leaseDuration, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error acquiring lease on source blob %s", src)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = blobLeaseClient.BreakLease(context.Background(), &lease.BlobBreakOptions{BreakPeriod: to.Ptr(int32(60))})
|
||||
}()
|
||||
|
||||
dstBlobClient := containerClient.NewBlobClient(dst)
|
||||
copyResp, err := dstBlobClient.StartCopyFromURL(context.Background(), srcBlobClient.URL(), &blob.StartCopyFromURLOptions{
|
||||
Metadata: metadata,
|
||||
})
|
||||
|
||||
copyResp, err := dstBlobURL.StartCopyFromURL(
|
||||
context.Background(),
|
||||
srcBlobURL.URL(),
|
||||
metadata,
|
||||
azblob.ModifiedAccessConditions{},
|
||||
azblob.BlobAccessConditions{},
|
||||
azblob.DefaultAccessTier,
|
||||
nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying %s -> %s in %s: %s", src, dst, storage, err)
|
||||
}
|
||||
|
||||
copyStatus := *copyResp.CopyStatus
|
||||
copyStatus := copyResp.CopyStatus()
|
||||
for {
|
||||
if copyStatus == blob.CopyStatusTypeSuccess {
|
||||
if copyStatus == azblob.CopyStatusSuccess {
|
||||
if move {
|
||||
_, err := storage.az.client.DeleteBlob(context.Background(), storage.az.container, src, &blob.DeleteOptions{
|
||||
AccessConditions: &blob.AccessConditions{
|
||||
LeaseAccessConditions: &blob.LeaseAccessConditions{
|
||||
LeaseID: &leaseID,
|
||||
},
|
||||
},
|
||||
})
|
||||
_, err = srcBlobURL.Delete(
|
||||
context.Background(),
|
||||
azblob.DeleteSnapshotsOptionNone,
|
||||
azblob.BlobAccessConditions{
|
||||
LeaseAccessConditions: azblob.LeaseAccessConditions{LeaseID: srcBlobLeaseID},
|
||||
})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else if copyStatus == blob.CopyStatusTypePending {
|
||||
} else if copyStatus == azblob.CopyStatusPending {
|
||||
time.Sleep(1 * time.Second)
|
||||
getMetadata, err := dstBlobClient.GetProperties(context.TODO(), nil)
|
||||
blobPropsResp, err := dstBlobURL.GetProperties(
|
||||
context.Background(),
|
||||
azblob.BlobAccessConditions{LeaseAccessConditions: azblob.LeaseAccessConditions{LeaseID: srcBlobLeaseID}},
|
||||
azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting copy progress %s", dst)
|
||||
return fmt.Errorf("error getting destination blob properties %s", dstBlobURL)
|
||||
}
|
||||
copyStatus = *getMetadata.CopyStatus
|
||||
copyStatus = blobPropsResp.CopyStatus()
|
||||
|
||||
_, err = blobLeaseClient.RenewLease(context.Background(), nil)
|
||||
_, err = srcBlobURL.RenewLease(context.Background(), srcBlobLeaseID, azblob.ModifiedAccessConditions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error renewing source blob lease %s", src)
|
||||
return fmt.Errorf("error renewing source blob lease %s", srcBlobURL)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("error copying %s -> %s in %s: %s", dst, src, storage, copyStatus)
|
||||
@@ -245,9 +241,7 @@ func (storage *PublishedStorage) RenameFile(oldName, newName string) error {
|
||||
|
||||
// SymLink creates a copy of src file and adds link information as meta data
|
||||
func (storage *PublishedStorage) SymLink(src string, dst string) error {
|
||||
metadata := make(map[string]*string)
|
||||
metadata["SymLink"] = &src
|
||||
return storage.internalCopyOrMoveBlob(src, dst, metadata, false /* do not remove src */)
|
||||
return storage.internalCopyOrMoveBlob(src, dst, azblob.Metadata{"SymLink": src}, false /* move */)
|
||||
}
|
||||
|
||||
// HardLink using symlink functionality as hard links do not exist
|
||||
@@ -257,33 +251,28 @@ func (storage *PublishedStorage) HardLink(src string, dst string) error {
|
||||
|
||||
// FileExists returns true if path exists
|
||||
func (storage *PublishedStorage) FileExists(path string) (bool, error) {
|
||||
serviceClient := storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
_, err := blobClient.GetProperties(context.Background(), nil)
|
||||
blob := storage.az.blobURL(path)
|
||||
resp, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
if isBlobNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("error checking if blob %s exists: %v", path, err)
|
||||
return false, err
|
||||
} else if resp.StatusCode() == http.StatusOK {
|
||||
return true, nil
|
||||
}
|
||||
return true, nil
|
||||
return false, fmt.Errorf("error checking if blob %s exists %d", blob, resp.StatusCode())
|
||||
}
|
||||
|
||||
// ReadLink returns the symbolic link pointed to by path.
|
||||
// This simply reads text file created with SymLink
|
||||
func (storage *PublishedStorage) ReadLink(path string) (string, error) {
|
||||
serviceClient := storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
props, err := blobClient.GetProperties(context.Background(), nil)
|
||||
blob := storage.az.blobURL(path)
|
||||
resp, err := blob.GetProperties(context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get blob properties: %v", err)
|
||||
return "", err
|
||||
} else if resp.StatusCode() != http.StatusOK {
|
||||
return "", fmt.Errorf("error checking if blob %s exists %d", blob, resp.StatusCode())
|
||||
}
|
||||
|
||||
metadata := props.Metadata
|
||||
if originalBlob, exists := metadata["original_blob"]; exists {
|
||||
return *originalBlob, nil
|
||||
}
|
||||
return "", fmt.Errorf("error reading link %s: %v", path, err)
|
||||
return resp.NewMetadata()["SymLink"], nil
|
||||
}
|
||||
|
||||
+32
-35
@@ -4,14 +4,11 @@ import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"bytes"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
. "gopkg.in/check.v1"
|
||||
@@ -36,7 +33,7 @@ func randString(n int) string {
|
||||
}
|
||||
const alphanum = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
var bytes = make([]byte, n)
|
||||
_, _ = rand.Read(bytes)
|
||||
rand.Read(bytes)
|
||||
for i, b := range bytes {
|
||||
bytes[i] = alphanum[b%byte(len(alphanum))]
|
||||
}
|
||||
@@ -69,10 +66,8 @@ func (s *PublishedStorageSuite) SetUpTest(c *C) {
|
||||
|
||||
s.storage, err = NewPublishedStorage(s.accountName, s.accountKey, container, "", s.endpoint)
|
||||
c.Assert(err, IsNil)
|
||||
publicAccessType := azblob.PublicAccessTypeContainer
|
||||
_, err = s.storage.az.client.CreateContainer(context.Background(), s.storage.az.container, &azblob.CreateContainerOptions{
|
||||
Access: &publicAccessType,
|
||||
})
|
||||
cnt := s.storage.az.container
|
||||
_, err = cnt.Create(context.Background(), azblob.Metadata{}, azblob.PublicAccessContainer)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.prefixedStorage, err = NewPublishedStorage(s.accountName, s.accountKey, container, prefix, s.endpoint)
|
||||
@@ -80,39 +75,41 @@ func (s *PublishedStorageSuite) SetUpTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TearDownTest(c *C) {
|
||||
_, err := s.storage.az.client.DeleteContainer(context.Background(), s.storage.az.container, nil)
|
||||
cnt := s.storage.az.container
|
||||
_, err := cnt.Delete(context.Background(), azblob.ContainerAccessConditions{})
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) GetFile(c *C, path string) []byte {
|
||||
resp, err := s.storage.az.client.DownloadStream(context.Background(), s.storage.az.container, path, nil)
|
||||
blob := s.storage.az.container.NewBlobURL(path)
|
||||
resp, err := blob.Download(context.Background(), 0, azblob.CountToEnd, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
|
||||
c.Assert(err, IsNil)
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
body := resp.Body(azblob.RetryReaderOptions{MaxRetryRequests: 3})
|
||||
data, err := ioutil.ReadAll(body)
|
||||
c.Assert(err, IsNil)
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) AssertNoFile(c *C, path string) {
|
||||
serviceClient := s.storage.az.client.ServiceClient()
|
||||
containerClient := serviceClient.NewContainerClient(s.storage.az.container)
|
||||
blobClient := containerClient.NewBlobClient(path)
|
||||
_, err := blobClient.GetProperties(context.Background(), nil)
|
||||
_, err := s.storage.az.container.NewBlobURL(path).GetProperties(
|
||||
context.Background(), azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
c.Assert(err, NotNil)
|
||||
|
||||
storageError, ok := err.(*azcore.ResponseError)
|
||||
storageError, ok := err.(azblob.StorageError)
|
||||
c.Assert(ok, Equals, true)
|
||||
c.Assert(storageError.StatusCode, Equals, 404)
|
||||
c.Assert(string(storageError.ServiceCode()), Equals, string(string(azblob.StorageErrorCodeBlobNotFound)))
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) PutFile(c *C, path string, data []byte) {
|
||||
hash := md5.Sum(data)
|
||||
uploadOptions := &azblob.UploadStreamOptions{
|
||||
HTTPHeaders: &blob.HTTPHeaders{
|
||||
BlobContentMD5: hash[:],
|
||||
},
|
||||
}
|
||||
reader := bytes.NewReader(data)
|
||||
_, err := s.storage.az.client.UploadStream(context.Background(), s.storage.az.container, path, reader, uploadOptions)
|
||||
_, err := azblob.UploadBufferToBlockBlob(
|
||||
context.Background(),
|
||||
data,
|
||||
s.storage.az.container.NewBlockBlobURL(path),
|
||||
azblob.UploadToBlockBlobOptions{
|
||||
BlobHTTPHeaders: azblob.BlobHTTPHeaders{
|
||||
ContentMD5: hash[:],
|
||||
},
|
||||
})
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
@@ -121,7 +118,7 @@ func (s *PublishedStorageSuite) TestPutFile(c *C) {
|
||||
filename := "a/b.txt"
|
||||
|
||||
dir := c.MkDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
err := ioutil.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile(filename, filepath.Join(dir, "a"))
|
||||
@@ -140,7 +137,7 @@ func (s *PublishedStorageSuite) TestPutFilePlus(c *C) {
|
||||
filename := "a/b+c.txt"
|
||||
|
||||
dir := c.MkDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
err := ioutil.WriteFile(filepath.Join(dir, "a"), content, 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile(filename, filepath.Join(dir, "a"))
|
||||
@@ -258,7 +255,7 @@ func (s *PublishedStorageSuite) TestRemoveDirsPlus(c *C) {
|
||||
|
||||
func (s *PublishedStorageSuite) TestRenameFile(c *C) {
|
||||
dir := c.MkDir()
|
||||
err := os.WriteFile(filepath.Join(dir, "a"), []byte("Welcome to Azure!"), 0644)
|
||||
err := ioutil.WriteFile(filepath.Join(dir, "a"), []byte("Welcome to Azure!"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile("source.txt", filepath.Join(dir, "a"))
|
||||
@@ -280,18 +277,18 @@ func (s *PublishedStorageSuite) TestLinkFromPool(c *C) {
|
||||
cs := files.NewMockChecksumStorage()
|
||||
|
||||
tmpFile1 := filepath.Join(c.MkDir(), "mars-invaders_1.03.deb")
|
||||
err := os.WriteFile(tmpFile1, []byte("Contents"), 0644)
|
||||
err := ioutil.WriteFile(tmpFile1, []byte("Contents"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum1 := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
|
||||
tmpFile2 := filepath.Join(c.MkDir(), "mars-invaders_1.03.deb")
|
||||
err = os.WriteFile(tmpFile2, []byte("Spam"), 0644)
|
||||
err = ioutil.WriteFile(tmpFile2, []byte("Spam"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum2 := utils.ChecksumInfo{MD5: "e9dfd31cc505d51fc26975250750deab"}
|
||||
|
||||
tmpFile3 := filepath.Join(c.MkDir(), "netboot/boot.img.gz")
|
||||
_ = os.MkdirAll(filepath.Dir(tmpFile3), 0777)
|
||||
err = os.WriteFile(tmpFile3, []byte("Contents"), 0644)
|
||||
os.MkdirAll(filepath.Dir(tmpFile3), 0777)
|
||||
err = ioutil.WriteFile(tmpFile3, []byte("Contents"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
cksum3 := utils.ChecksumInfo{MD5: "c1df1da7a1ce305a3b60af9d5733ac1d"}
|
||||
|
||||
@@ -333,7 +330,7 @@ func (s *PublishedStorageSuite) TestLinkFromPool(c *C) {
|
||||
|
||||
// 2nd link from pool, providing wrong path for source file
|
||||
//
|
||||
// this test should check that file already exists in Azure and skip upload (which would fail if not skipped)
|
||||
// this test should check that file already exists in S3 and skip upload (which would fail if not skipped)
|
||||
s.prefixedStorage.pathCache = nil
|
||||
err = s.prefixedStorage.LinkFromPool("", filepath.Join("pool", "main", "m/mars-invaders"), "mars-invaders_1.03.deb", pool, "wrong-looks-like-pathcache-doesnt-work", cksum1, false)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
@@ -26,6 +26,7 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
|
||||
// collect information about references packages...
|
||||
existingPackageRefs := deb.NewPackageRefList()
|
||||
referencedAppStreamFiles := []string{}
|
||||
|
||||
// used only in verbose mode to report package use source
|
||||
packageRefSources := map[string][]string{}
|
||||
@@ -55,6 +56,10 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, poolPath := range repo.AppStreamFiles {
|
||||
referencedAppStreamFiles = append(referencedAppStreamFiles, poolPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -118,6 +123,11 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
for _, poolPath := range snapshot.AppStreamFiles {
|
||||
referencedAppStreamFiles = append(referencedAppStreamFiles, poolPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -236,6 +246,7 @@ func aptlyDBCleanup(cmd *commander.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
referencedFiles = append(referencedFiles, referencedAppStreamFiles...)
|
||||
sort.Strings(referencedFiles)
|
||||
context.Progress().ShutdownBar()
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ func aptlyMirrorCreate(cmd *commander.Command, args []string) error {
|
||||
downloadSources := LookupOption(context.Config().DownloadSourcePackages, context.Flags(), "with-sources")
|
||||
downloadUdebs := context.Flags().Lookup("with-udebs").Value.Get().(bool)
|
||||
downloadInstaller := context.Flags().Lookup("with-installer").Value.Get().(bool)
|
||||
downloadAppStream := context.Flags().Lookup("with-appstream").Value.Get().(bool)
|
||||
ignoreSignatures := context.Config().GpgDisableVerify
|
||||
if context.Flags().IsSet("ignore-signatures") {
|
||||
ignoreSignatures = context.Flags().Lookup("ignore-signatures").Value.Get().(bool)
|
||||
@@ -41,7 +42,7 @@ func aptlyMirrorCreate(cmd *commander.Command, args []string) error {
|
||||
}
|
||||
|
||||
repo, err := deb.NewRemoteRepo(mirrorName, archiveURL, distribution, components, context.ArchitecturesList(),
|
||||
downloadSources, downloadUdebs, downloadInstaller)
|
||||
downloadSources, downloadUdebs, downloadInstaller, downloadAppStream)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create mirror: %s", err)
|
||||
}
|
||||
@@ -100,6 +101,7 @@ Example:
|
||||
}
|
||||
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("with-appstream", false, "download AppStream (DEP-11) metadata")
|
||||
cmd.Flag.Bool("with-installer", false, "download additional not packaged installer files")
|
||||
cmd.Flag.Bool("with-sources", false, "download source packages in addition to binary packages")
|
||||
cmd.Flag.Bool("with-udebs", false, "download .udeb packages (Debian installer support)")
|
||||
|
||||
@@ -35,6 +35,8 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
repo.Filter = flag.Value.String() // allows file/stdin with @
|
||||
case "filter-with-deps":
|
||||
repo.FilterWithDeps = flag.Value.Get().(bool)
|
||||
case "with-appstream":
|
||||
repo.DownloadAppStream = flag.Value.Get().(bool)
|
||||
case "with-installer":
|
||||
repo.DownloadInstaller = flag.Value.Get().(bool)
|
||||
case "with-sources":
|
||||
@@ -53,6 +55,10 @@ func aptlyMirrorEdit(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to edit: flat mirrors don't support udebs")
|
||||
}
|
||||
|
||||
if repo.IsFlat() && repo.DownloadAppStream {
|
||||
return fmt.Errorf("unable to edit: flat mirrors don't support AppStream (DEP-11) metadata")
|
||||
}
|
||||
|
||||
if repo.Filter != "" {
|
||||
_, err = query.Parse(repo.Filter)
|
||||
if err != nil {
|
||||
@@ -107,6 +113,7 @@ Example:
|
||||
AddStringOrFileFlag(&cmd.Flag, "filter", "", "filter packages in mirror, use '@file' to read filter from file or '@-' for stdin")
|
||||
cmd.Flag.Bool("filter-with-deps", false, "when filtering, include dependencies of matching packages as well")
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("with-appstream", false, "download AppStream (DEP-11) metadata")
|
||||
cmd.Flag.Bool("with-installer", false, "download additional not packaged installer files")
|
||||
cmd.Flag.Bool("with-sources", false, "download source packages in addition to binary packages")
|
||||
cmd.Flag.Bool("with-udebs", false, "download .udeb packages (Debian installer support)")
|
||||
|
||||
@@ -61,6 +61,11 @@ func aptlyMirrorShowTxt(_ *commander.Command, args []string) error {
|
||||
downloadUdebs = Yes
|
||||
}
|
||||
fmt.Printf("Download .udebs: %s\n", downloadUdebs)
|
||||
downloadAppStream := No
|
||||
if repo.DownloadAppStream {
|
||||
downloadAppStream = Yes
|
||||
}
|
||||
fmt.Printf("Download AppStream: %s\n", downloadAppStream)
|
||||
if repo.Filter != "" {
|
||||
fmt.Printf("Filter: %s\n", repo.Filter)
|
||||
filterWithDeps := No
|
||||
|
||||
+12
-1
@@ -64,6 +64,15 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
|
||||
if repo.DownloadAppStream && !repo.IsFlat() {
|
||||
context.Progress().Printf("Downloading AppStream metadata...\n")
|
||||
err = repo.DownloadAppStreamFiles(context.Progress(), context.Downloader(),
|
||||
context.PackagePool(), collectionFactory.ChecksumCollection(nil), ignoreChecksums)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if repo.Filter != "" {
|
||||
context.Progress().Printf("Applying filter...\n")
|
||||
var filterQuery deb.PackageQuery
|
||||
@@ -87,10 +96,11 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
|
||||
)
|
||||
|
||||
skipExistingPackages := context.Flags().Lookup("skip-existing-packages").Value.Get().(bool)
|
||||
latestOnly := context.Flags().Lookup("latest").Value.Get().(bool)
|
||||
|
||||
context.Progress().Printf("Building download queue...\n")
|
||||
queue, downloadSize, err = repo.BuildDownloadQueue(context.PackagePool(), collectionFactory.PackageCollection(),
|
||||
collectionFactory.ChecksumCollection(nil), skipExistingPackages)
|
||||
collectionFactory.ChecksumCollection(nil), skipExistingPackages, latestOnly)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update: %s", err)
|
||||
@@ -292,6 +302,7 @@ Example:
|
||||
cmd.Flag.Bool("ignore-checksums", false, "ignore checksum mismatches while downloading package files and metadata")
|
||||
cmd.Flag.Bool("ignore-signatures", false, "disable verification of Release file signatures")
|
||||
cmd.Flag.Bool("skip-existing-packages", false, "do not check file existence for packages listed in the internal database of the mirror")
|
||||
cmd.Flag.Bool("latest", false, "download only latest version of each package (per architecture)")
|
||||
cmd.Flag.Int64("download-limit", 0, "limit download speed (kbytes/sec)")
|
||||
cmd.Flag.String("downloader", "default", "downloader to use (e.g. grab)")
|
||||
cmd.Flag.Int("max-tries", 1, "max download tries till process fails with download error")
|
||||
|
||||
+33
-1
@@ -1,6 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/aptly-dev/aptly/pgp"
|
||||
"github.com/smira/commander"
|
||||
"github.com/smira/flag"
|
||||
@@ -12,7 +14,20 @@ func getSigner(flags *flag.FlagSet) (pgp.Signer, error) {
|
||||
}
|
||||
|
||||
signer := context.GetSigner()
|
||||
signer.SetKey(flags.Lookup("gpg-key").Value.String())
|
||||
|
||||
var gpgKeys []string
|
||||
|
||||
// CLI args have priority over config
|
||||
cliKeys := flags.Lookup("gpg-key").Value.Get().([]string)
|
||||
if len(cliKeys) > 0 {
|
||||
gpgKeys = cliKeys
|
||||
} else if len(context.Config().GpgKeys) > 0 {
|
||||
gpgKeys = context.Config().GpgKeys
|
||||
}
|
||||
|
||||
for _, gpgKey := range gpgKeys {
|
||||
signer.SetKey(gpgKey)
|
||||
}
|
||||
signer.SetKeyRing(flags.Lookup("keyring").Value.String(), flags.Lookup("secret-keyring").Value.String())
|
||||
signer.SetPassphrase(flags.Lookup("passphrase").Value.String(), flags.Lookup("passphrase-file").Value.String())
|
||||
signer.SetBatch(flags.Lookup("batch").Value.Get().(bool))
|
||||
@@ -26,6 +41,23 @@ func getSigner(flags *flag.FlagSet) (pgp.Signer, error) {
|
||||
|
||||
}
|
||||
|
||||
type gpgKeyFlag struct {
|
||||
gpgKeys []string
|
||||
}
|
||||
|
||||
func (k *gpgKeyFlag) Set(value string) error {
|
||||
k.gpgKeys = append(k.gpgKeys, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *gpgKeyFlag) Get() interface{} {
|
||||
return k.gpgKeys
|
||||
}
|
||||
|
||||
func (k *gpgKeyFlag) String() string {
|
||||
return strings.Join(k.gpgKeys, ",")
|
||||
}
|
||||
|
||||
func makeCmdPublish() *commander.Command {
|
||||
return &commander.Command{
|
||||
UsageLine: "publish",
|
||||
|
||||
+3
-1
@@ -34,7 +34,7 @@ Example:
|
||||
}
|
||||
cmd.Flag.String("distribution", "", "distribution name to publish")
|
||||
cmd.Flag.String("component", "", "component name to publish (for multi-component publishing, separate components with commas)")
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
@@ -51,7 +51,9 @@ Example:
|
||||
cmd.Flag.String("codename", "", "codename to publish (defaults to distribution)")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.Bool("acquire-by-hash", false, "provide index files by hash")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+11
-1
@@ -150,10 +150,18 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
|
||||
published.AcquireByHash = context.Flags().Lookup("acquire-by-hash").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("signed-by") {
|
||||
published.SignedBy = context.Flags().Lookup("signed-by").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("multi-dist") {
|
||||
published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("version") {
|
||||
published.Version = context.Flags().Lookup("version").Value.String()
|
||||
}
|
||||
|
||||
duplicate := collectionFactory.PublishedRepoCollection().CheckDuplicate(published)
|
||||
if duplicate != nil {
|
||||
_ = collectionFactory.PublishedRepoCollection().LoadComplete(duplicate, collectionFactory)
|
||||
@@ -230,7 +238,7 @@ Example:
|
||||
}
|
||||
cmd.Flag.String("distribution", "", "distribution name to publish")
|
||||
cmd.Flag.String("component", "", "component name to publish (for multi-component publishing, separate components with commas)")
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
@@ -247,6 +255,8 @@ Example:
|
||||
cmd.Flag.String("codename", "", "codename to publish (defaults to distribution)")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.Bool("acquire-by-hash", false, "provide index files by hash")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
|
||||
return cmd
|
||||
|
||||
+11
-1
@@ -99,6 +99,14 @@ func aptlyPublishSwitch(cmd *commander.Command, args []string) error {
|
||||
published.SkipBz2 = context.Flags().Lookup("skip-bz2").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("signed-by") {
|
||||
published.SignedBy = context.Flags().Lookup("signed-by").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("version") {
|
||||
published.Version = context.Flags().Lookup("version").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("multi-dist") {
|
||||
published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
}
|
||||
@@ -151,7 +159,7 @@ This command would switch published repository (with one component) named ppa/wh
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-switch", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
@@ -162,6 +170,8 @@ This command would switch published repository (with one component) named ppa/wh
|
||||
cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes")
|
||||
cmd.Flag.String("component", "", "component names to update (for multi-component publishing, separate components with commas)")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
cmd.Flag.Bool("skip-cleanup", false, "don't remove unreferenced files in prefix/component")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
|
||||
|
||||
+21
-1
@@ -60,10 +60,26 @@ func aptlyPublishUpdate(cmd *commander.Command, args []string) error {
|
||||
published.SkipBz2 = context.Flags().Lookup("skip-bz2").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("signed-by") {
|
||||
published.SignedBy = context.Flags().Lookup("signed-by").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("origin") {
|
||||
published.Origin = context.Flags().Lookup("origin").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("label") {
|
||||
published.Label = context.Flags().Lookup("label").Value.String()
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("multi-dist") {
|
||||
published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool)
|
||||
}
|
||||
|
||||
if context.Flags().IsSet("version") {
|
||||
published.Version = context.Flags().Lookup("version").Value.String()
|
||||
}
|
||||
|
||||
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, context.Progress(), forceOverwrite, context.SkelPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to publish: %s", err)
|
||||
@@ -115,7 +131,7 @@ Example:
|
||||
`,
|
||||
Flag: *flag.NewFlagSet("aptly-publish-update", flag.ExitOnError),
|
||||
}
|
||||
cmd.Flag.String("gpg-key", "", "GPG key ID to use when signing the release")
|
||||
cmd.Flag.Var(&gpgKeyFlag{}, "gpg-key", "GPG key ID to use when signing the release (flag is repeatable, can be specified multiple times)")
|
||||
cmd.Flag.Var(&keyRingsFlag{}, "keyring", "GPG keyring to use (instead of default)")
|
||||
cmd.Flag.String("secret-keyring", "", "GPG secret keyring to use (instead of default)")
|
||||
cmd.Flag.String("passphrase", "", "GPG passphrase for the key (warning: could be insecure)")
|
||||
@@ -125,8 +141,12 @@ Example:
|
||||
cmd.Flag.Bool("skip-contents", false, "don't generate Contents indexes")
|
||||
cmd.Flag.Bool("skip-bz2", false, "don't generate bzipped indexes")
|
||||
cmd.Flag.Bool("force-overwrite", false, "overwrite files in package pool in case of mismatch")
|
||||
cmd.Flag.String("signed-by", "", "an optional field containing a comma separated list of OpenPGP key fingerprints to be used for validating the next Release file")
|
||||
cmd.Flag.Bool("skip-cleanup", false, "don't remove unreferenced files in prefix/component")
|
||||
cmd.Flag.Bool("multi-dist", false, "enable multiple packages with the same filename in different distributions")
|
||||
cmd.Flag.String("origin", "", "overwrite origin name to publish")
|
||||
cmd.Flag.String("label", "", "overwrite label to publish")
|
||||
cmd.Flag.String("version", "", "version of the release")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@ local keyring="*-keyring=[gpg keyring to use when verifying Release file (could
|
||||
$keyring \
|
||||
"-with-sources=[download source packages in addition to binary packages]:$bool" \
|
||||
"-with-udebs=[download .udeb packages (Debian installer support)]:$bool" \
|
||||
"-with-appstream=[download AppStream (DEP-11) metadata]:$bool" \
|
||||
"(-)2:new mirror name: " ":archive url:_urls" ":distribution:($dists)" "*:components:_values -s ' ' components $components"
|
||||
;;
|
||||
list)
|
||||
@@ -211,6 +212,7 @@ local keyring="*-keyring=[gpg keyring to use when verifying Release file (could
|
||||
$keyring \
|
||||
"-max-tries=[max download tries till process fails with download error]:number: " \
|
||||
"-skip-existing-packages=[do not check file existence for packages listed in the internal database of the mirror]:$bool" \
|
||||
"-latest=[download only latest version of each package (per architecture)]:$bool" \
|
||||
"(-)2:mirror name:$mirrors"
|
||||
;;
|
||||
rename)
|
||||
@@ -223,6 +225,7 @@ local keyring="*-keyring=[gpg keyring to use when verifying Release file (could
|
||||
"-filter-with-deps=[when filtering, include dependencies of matching packages as well]:$bool" \
|
||||
"-with-sources=[download source packages in addition to binary packages]:$bool" \
|
||||
"-with-udebs=[download .udeb packages (Debian installer support)]:$bool" \
|
||||
"-with-appstream=[download AppStream (DEP-11) metadata]:$bool" \
|
||||
"(-)2:mirror name:$mirrors"
|
||||
;;
|
||||
search)
|
||||
|
||||
+3
-3
@@ -203,7 +203,7 @@ _aptly()
|
||||
"create")
|
||||
if [[ $numargs -eq 0 ]]; then
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=($(compgen -W "-filter= -filter-with-deps -force-components -ignore-signatures -keyring= -with-installer -with-sources -with-udebs" -- ${cur}))
|
||||
COMPREPLY=($(compgen -W "-filter= -filter-with-deps -force-components -ignore-signatures -keyring= -with-appstream -with-installer -with-sources -with-udebs" -- ${cur}))
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
@@ -211,7 +211,7 @@ _aptly()
|
||||
"edit")
|
||||
if [[ $numargs -eq 0 ]]; then
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=($(compgen -W "-archive-url= -filter= -filter-with-deps -ignore-signatures -keyring= -with-installer -with-sources -with-udebs" -- ${cur}))
|
||||
COMPREPLY=($(compgen -W "-archive-url= -filter= -filter-with-deps -ignore-signatures -keyring= -with-appstream -with-installer -with-sources -with-udebs" -- ${cur}))
|
||||
else
|
||||
COMPREPLY=($(compgen -W "$(__aptly_mirror_list)" -- ${cur}))
|
||||
fi
|
||||
@@ -263,7 +263,7 @@ _aptly()
|
||||
"update")
|
||||
if [[ $numargs -eq 0 ]]; then
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=($(compgen -W "-force -download-limit= -downloader= -ignore-checksums -ignore-signatures -keyring= -skip-existing-packages" -- ${cur}))
|
||||
COMPREPLY=($(compgen -W "-force -download-limit= -downloader= -ignore-checksums -ignore-signatures -keyring= -skip-existing-packages -latest" -- ${cur}))
|
||||
else
|
||||
COMPREPLY=($(compgen -W "$(__aptly_mirror_list)" -- ${cur}))
|
||||
fi
|
||||
|
||||
@@ -11,7 +11,7 @@ func Test(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
type ProgressSuite struct {}
|
||||
type ProgressSuite struct{}
|
||||
|
||||
var _ = Suite(&ProgressSuite{})
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ func (context *AptlyContext) config() *utils.ConfigStructure {
|
||||
configLocations := []string{homeLocation, "/usr/local/etc/aptly.conf", "/etc/aptly.conf"}
|
||||
|
||||
for _, configLocation := range configLocations {
|
||||
// FIXME: check if exists, check if readable
|
||||
err = utils.LoadConfig(configLocation, &utils.Config)
|
||||
if os.IsPermission(err) || os.IsNotExist(err) {
|
||||
continue
|
||||
|
||||
@@ -14,7 +14,7 @@ func Test(t *testing.T) {
|
||||
}
|
||||
|
||||
type EtcDDBSuite struct {
|
||||
db database.Storage
|
||||
db database.Storage
|
||||
}
|
||||
|
||||
var _ = Suite(&EtcDDBSuite{})
|
||||
@@ -133,7 +133,7 @@ func (s *EtcDDBSuite) TestTransactionCommit(c *C) {
|
||||
v, err := s.db.Get(key)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(v, DeepEquals, value)
|
||||
err = transaction.Delete(key)
|
||||
err = transaction.Delete(key)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = transaction.Get(key2)
|
||||
@@ -156,4 +156,3 @@ func (s *EtcDDBSuite) TestTransactionCommit(c *C) {
|
||||
_, err = transaction.Get(key)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,11 @@ var (
|
||||
"Version",
|
||||
"Codename",
|
||||
"Date",
|
||||
"Valid-Until",
|
||||
"NotAutomatic",
|
||||
"ButAutomaticUpgrades",
|
||||
"Acquire-By-Hash",
|
||||
"Signed-By",
|
||||
"Architectures",
|
||||
"Architecture",
|
||||
"Components",
|
||||
|
||||
+34
@@ -172,6 +172,39 @@ func (l *PackageList) ForEach(handler func(*Package) error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// FilterLatest creates a copy of the package list containing only the
|
||||
// latest version for each package name/architecture pair.
|
||||
func (l *PackageList) FilterLatest() (*PackageList, error) {
|
||||
if l == nil {
|
||||
return nil, fmt.Errorf("package list is nil")
|
||||
}
|
||||
|
||||
filtered := make(map[string]*Package, l.Len())
|
||||
|
||||
err := l.ForEach(func(p *Package) error {
|
||||
key := p.Architecture + "|" + p.Name
|
||||
|
||||
if existing, found := filtered[key]; !found || CompareVersions(p.Version, existing.Version) > 0 {
|
||||
filtered[key] = p
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := NewPackageListWithDuplicates(l.duplicatesAllowed, len(filtered))
|
||||
|
||||
for _, pkg := range filtered {
|
||||
if err = result.Add(pkg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ForEachIndexed calls handler for each package in list in indexed order
|
||||
func (l *PackageList) ForEachIndexed(handler func(*Package) error) error {
|
||||
if !l.indexed {
|
||||
@@ -598,6 +631,7 @@ func (l *PackageList) Filter(options FilterOptions) (*PackageList, error) {
|
||||
//
|
||||
// when follow-all-variants is enabled, we need to try to expand anyway,
|
||||
// as even if dependency is satisfied now, there might be other ways to satisfy dependency
|
||||
// FIXME: do not search twice
|
||||
if result.Search(dep, false, true) != nil {
|
||||
if options.DependencyOptions&DepVerboseResolve == DepVerboseResolve && options.Progress != nil {
|
||||
options.Progress.ColoredPrintf("@{y}Already satisfied dependency@|: %s with %s", &dep, result.Search(dep, true, true))
|
||||
|
||||
@@ -503,3 +503,52 @@ func (s *PackageListSuite) TestArchitectures(c *C) {
|
||||
sort.Strings(archs)
|
||||
c.Check(archs, DeepEquals, []string{"amd64", "arm", "i386", "s390"})
|
||||
}
|
||||
|
||||
func (s *PackageListSuite) TestFilterLatest(c *C) {
|
||||
list := NewPackageList()
|
||||
|
||||
older := packageStanza.Copy()
|
||||
older["Version"] = "1.0"
|
||||
olderPkg := NewPackageFromControlFile(older)
|
||||
_ = list.Add(olderPkg)
|
||||
|
||||
newer := packageStanza.Copy()
|
||||
newer["Version"] = "2.0"
|
||||
newerPkg := NewPackageFromControlFile(newer)
|
||||
_ = list.Add(newerPkg)
|
||||
|
||||
shared := packageStanza.Copy()
|
||||
shared["Architecture"] = ArchitectureAll
|
||||
shared["Version"] = "3.0"
|
||||
shared["Package"] = "shared"
|
||||
sharedPkg := NewPackageFromControlFile(shared)
|
||||
_ = list.Add(sharedPkg)
|
||||
|
||||
filtered, err := list.FilterLatest()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(filtered.Len(), Equals, 2)
|
||||
c.Check(filtered.Has(newerPkg), Equals, true)
|
||||
c.Check(filtered.Has(sharedPkg), Equals, true)
|
||||
}
|
||||
|
||||
func (s *PackageListSuite) TestFilterLatestPreservesDuplicatesFlag(c *C) {
|
||||
list := NewPackageListWithDuplicates(true, 2)
|
||||
|
||||
_ = list.Add(NewPackageFromControlFile(packageStanza.Copy()))
|
||||
|
||||
another := packageStanza.Copy()
|
||||
another["Version"] = "7.41-1"
|
||||
_ = list.Add(NewPackageFromControlFile(another))
|
||||
|
||||
filtered, err := list.FilterLatest()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(filtered.duplicatesAllowed, Equals, true)
|
||||
}
|
||||
|
||||
func (s *PackageListSuite) TestFilterLatestNil(c *C) {
|
||||
var list *PackageList
|
||||
|
||||
filtered, err := list.FilterLatest()
|
||||
c.Assert(err, ErrorMatches, "package list is nil")
|
||||
c.Assert(filtered, IsNil)
|
||||
}
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ func (s *PackageSuite) TestNewUdebFromPara(c *C) {
|
||||
}
|
||||
|
||||
func (s *PackageSuite) TestNewInstallerFromPara(c *C) {
|
||||
repo, _ := NewRemoteRepo("yandex", "http://example.com/debian", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://example.com/debian", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
downloader := http.NewFakeDownloader()
|
||||
downloader.ExpectResponse("http://example.com/debian/dists/squeeze/main/installer-i386/current/images/MANIFEST.udebs", "MANIFEST.udebs")
|
||||
downloader.ExpectResponse("http://example.com/debian/dists/squeeze/main/installer-i386/current/images/udeb.list", "udeb.list")
|
||||
|
||||
+68
-1
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -54,6 +55,7 @@ type PublishedRepo struct {
|
||||
Label string
|
||||
Suite string
|
||||
Codename string
|
||||
Version string
|
||||
// Architectures is a list of all architectures published
|
||||
Architectures []string
|
||||
// SourceKind is "local"/"repo"
|
||||
@@ -81,6 +83,11 @@ type PublishedRepo struct {
|
||||
// Provide index files per hash also
|
||||
AcquireByHash bool
|
||||
|
||||
// An optional field containing a comma separated list
|
||||
// of OpenPGP key fingerprints to be used
|
||||
// for validating the next Release file
|
||||
SignedBy string
|
||||
|
||||
// Support multiple distributions
|
||||
MultiDist bool
|
||||
|
||||
@@ -520,6 +527,7 @@ func (p *PublishedRepo) MarshalJSON() ([]byte, error) {
|
||||
"Origin": p.Origin,
|
||||
"Suite": p.Suite,
|
||||
"Codename": p.Codename,
|
||||
"Version": p.Version,
|
||||
"NotAutomatic": p.NotAutomatic,
|
||||
"ButAutomaticUpgrades": p.ButAutomaticUpgrades,
|
||||
"Prefix": p.Prefix,
|
||||
@@ -529,6 +537,7 @@ func (p *PublishedRepo) MarshalJSON() ([]byte, error) {
|
||||
"Storage": p.Storage,
|
||||
"SkipContents": p.SkipContents,
|
||||
"AcquireByHash": p.AcquireByHash,
|
||||
"SignedBy": p.SignedBy,
|
||||
"MultiDist": p.MultiDist,
|
||||
})
|
||||
}
|
||||
@@ -1046,6 +1055,38 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
|
||||
}
|
||||
}
|
||||
|
||||
// Pass-through AppStream (DEP-11) files from snapshots
|
||||
for component, item := range p.sourceItems {
|
||||
if item.snapshot == nil || len(item.snapshot.AppStreamFiles) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
prefix := component + "/"
|
||||
for relPath, poolPath := range item.snapshot.AppStreamFiles {
|
||||
if !strings.HasPrefix(relPath, prefix) {
|
||||
continue
|
||||
}
|
||||
withinComponent := strings.TrimPrefix(relPath, prefix)
|
||||
|
||||
poolFile, err := packagePool.Open(poolPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open AppStream file from pool: %v", err)
|
||||
}
|
||||
|
||||
bufWriter, err := indexes.SkelIndex(component, withinComponent).BufWriter()
|
||||
if err != nil {
|
||||
_ = poolFile.Close()
|
||||
return fmt.Errorf("unable to generate AppStream index: %v", err)
|
||||
}
|
||||
|
||||
_, err = bufio.NewReader(poolFile).WriteTo(bufWriter)
|
||||
_ = poolFile.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write AppStream file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
udebs := []bool{false}
|
||||
if hadUdebs {
|
||||
udebs = append(udebs, true)
|
||||
@@ -1070,6 +1111,12 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
|
||||
if p.AcquireByHash {
|
||||
release["Acquire-By-Hash"] = "yes"
|
||||
}
|
||||
if p.SignedBy != "" {
|
||||
release["Signed-By"] = p.SignedBy
|
||||
}
|
||||
if p.Version != "" {
|
||||
release["Version"] = p.Version
|
||||
}
|
||||
|
||||
var bufWriter *bufio.Writer
|
||||
bufWriter, err = indexes.ReleaseIndex(component, arch, udeb).BufWriter()
|
||||
@@ -1126,11 +1173,31 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
|
||||
release["Label"] = p.GetLabel()
|
||||
release["Suite"] = p.GetSuite()
|
||||
release["Codename"] = p.GetCodename()
|
||||
release["Date"] = time.Now().UTC().Format("Mon, 2 Jan 2006 15:04:05 MST")
|
||||
datetimeformat := "Mon, 2 Jan 2006 15:04:05 MST"
|
||||
|
||||
publishDate := time.Now().UTC()
|
||||
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
|
||||
if sec, err := strconv.ParseInt(epoch, 10, 64); err == nil {
|
||||
publishDate = time.Unix(sec, 0).UTC()
|
||||
}
|
||||
}
|
||||
release["Date"] = publishDate.Format(datetimeformat)
|
||||
release["Architectures"] = strings.Join(utils.StrSlicesSubstract(p.Architectures, []string{ArchitectureSource}), " ")
|
||||
if p.AcquireByHash {
|
||||
release["Acquire-By-Hash"] = "yes"
|
||||
}
|
||||
if p.SignedBy != "" {
|
||||
// "If the field is present, a client should only accept future updates
|
||||
// to the repository that are signed with keys listed in the field.
|
||||
// The field should be ignored if the Valid-Until field
|
||||
// is not present or if it is expired."
|
||||
release["Signed-By"] = p.SignedBy
|
||||
// Let's use a century as a "forever" value.
|
||||
release["Valid-Until"] = publishDate.AddDate(100, 0, 0).Format(datetimeformat)
|
||||
}
|
||||
if p.Version != "" {
|
||||
release["Version"] = p.Version
|
||||
}
|
||||
release["Description"] = " Generated by aptly\n"
|
||||
release["MD5Sum"] = ""
|
||||
release["SHA1"] = ""
|
||||
|
||||
+118
-1
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/aptly-dev/aptly/database/goleveldb"
|
||||
"github.com/aptly-dev/aptly/files"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
"github.com/ugorji/go/codec"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
@@ -115,7 +116,7 @@ func (s *PublishedRepoSuite) SetUpTest(c *C) {
|
||||
|
||||
s.reflist = NewPackageRefListFromPackageList(s.list)
|
||||
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
repo.packageRefs = s.reflist
|
||||
_ = s.factory.RemoteRepoCollection().Add(repo)
|
||||
|
||||
@@ -425,6 +426,81 @@ func (s *PublishedRepoSuite) TestPublish(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedRepoSuite) TestPublishAppStream(c *C) {
|
||||
// Components + icons
|
||||
content1 := []byte("DEP-11 test content for Components-amd64.yml.gz")
|
||||
tmpFile1 := filepath.Join(c.MkDir(), "Components-amd64.yml.gz")
|
||||
c.Assert(os.WriteFile(tmpFile1, content1, 0644), IsNil)
|
||||
|
||||
checksums1 := utils.ChecksumInfo{Size: int64(len(content1))}
|
||||
poolPath1, err := s.packagePool.Import(tmpFile1, "Components-amd64.yml.gz", &checksums1, false, s.cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
content2 := []byte("DEP-11 icons tar data")
|
||||
tmpFile2 := filepath.Join(c.MkDir(), "icons-48x48.tar.gz")
|
||||
c.Assert(os.WriteFile(tmpFile2, content2, 0644), IsNil)
|
||||
|
||||
checksums2 := utils.ChecksumInfo{Size: int64(len(content2))}
|
||||
poolPath2, err := s.packagePool.Import(tmpFile2, "icons-48x48.tar.gz", &checksums2, false, s.cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Include contrib file that should be skipped
|
||||
contribContent := []byte("DEP-11 contrib content")
|
||||
tmpFile3 := filepath.Join(c.MkDir(), "Components-contrib.yml.gz")
|
||||
c.Assert(os.WriteFile(tmpFile3, contribContent, 0644), IsNil)
|
||||
|
||||
checksums3 := utils.ChecksumInfo{Size: int64(len(contribContent))}
|
||||
poolPath3, err := s.packagePool.Import(tmpFile3, "Components-contrib.yml.gz", &checksums3, false, s.cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.snapshot.AppStreamFiles = map[string]string{
|
||||
"main/dep11/Components-amd64.yml.gz": poolPath1,
|
||||
"main/dep11/icons-48x48.tar.gz": poolPath2,
|
||||
"contrib/dep11/Components-amd64.yml.gz": poolPath3,
|
||||
}
|
||||
|
||||
err = s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, "")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Both main files should exist
|
||||
appstreamPath1 := filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/main/dep11/Components-amd64.yml.gz")
|
||||
c.Check(appstreamPath1, PathExists)
|
||||
actual1, err := os.ReadFile(appstreamPath1)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(actual1, DeepEquals, content1)
|
||||
|
||||
appstreamPath2 := filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/main/dep11/icons-48x48.tar.gz")
|
||||
c.Check(appstreamPath2, PathExists)
|
||||
actual2, err := os.ReadFile(appstreamPath2)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(actual2, DeepEquals, content2)
|
||||
|
||||
// Contrib file should not appear
|
||||
contribPath := filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/contrib/dep11/Components-amd64.yml.gz")
|
||||
_, statErr := os.Stat(contribPath)
|
||||
c.Check(os.IsNotExist(statErr), Equals, true)
|
||||
|
||||
// Release file should reference AppStream files
|
||||
rf, err := os.Open(filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/Release"))
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = rf.Close() }()
|
||||
|
||||
cfr := NewControlFileReader(rf, true, false)
|
||||
st, err := cfr.ReadStanza()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
c.Check(st["MD5Sum"], Matches, "(?s).*main/dep11/Components-amd64\\.yml\\.gz.*")
|
||||
c.Check(st["SHA256"], Matches, "(?s).*main/dep11/Components-amd64\\.yml\\.gz.*")
|
||||
|
||||
// Pool open error
|
||||
s.snapshot.AppStreamFiles = map[string]string{
|
||||
"main/dep11/Components-amd64.yml.gz": "nonexistent/pool/path",
|
||||
}
|
||||
|
||||
err = s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, "")
|
||||
c.Assert(err, ErrorMatches, "unable to open AppStream file from pool.*")
|
||||
}
|
||||
|
||||
func (s *PublishedRepoSuite) TestPublishNoSigner(c *C) {
|
||||
err := s.repo.Publish(s.packagePool, s.provider, s.factory, nil, nil, false, "")
|
||||
c.Assert(err, IsNil)
|
||||
@@ -433,6 +509,47 @@ func (s *PublishedRepoSuite) TestPublishNoSigner(c *C) {
|
||||
c.Check(filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/main/binary-i386/Release"), PathExists)
|
||||
}
|
||||
|
||||
func (s *PublishedRepoSuite) TestPublishSourceDateEpoch(c *C) {
|
||||
// Test with SOURCE_DATE_EPOCH set
|
||||
_ = os.Setenv("SOURCE_DATE_EPOCH", "1234567890")
|
||||
defer func() { _ = os.Unsetenv("SOURCE_DATE_EPOCH") }()
|
||||
|
||||
err := s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, "")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
rf, err := os.Open(filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/Release"))
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = rf.Close() }()
|
||||
|
||||
cfr := NewControlFileReader(rf, true, false)
|
||||
st, err := cfr.ReadStanza()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Expected date for Unix timestamp 1234567890: Fri, 13 Feb 2009 23:31:30 UTC
|
||||
c.Check(st["Date"], Equals, "Fri, 13 Feb 2009 23:31:30 UTC")
|
||||
}
|
||||
|
||||
func (s *PublishedRepoSuite) TestPublishSourceDateEpochInvalid(c *C) {
|
||||
// Test with invalid SOURCE_DATE_EPOCH (should fallback to current time)
|
||||
_ = os.Setenv("SOURCE_DATE_EPOCH", "invalid")
|
||||
defer func() { _ = os.Unsetenv("SOURCE_DATE_EPOCH") }()
|
||||
|
||||
err := s.repo2.Publish(s.packagePool, s.provider, s.factory, nil, nil, false, "")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
rf, err := os.Open(filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/maverick/Release"))
|
||||
c.Assert(err, IsNil)
|
||||
defer func() { _ = rf.Close() }()
|
||||
|
||||
cfr := NewControlFileReader(rf, true, false)
|
||||
st, err := cfr.ReadStanza()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Should have a valid Date field (not empty, not the fixed date from SOURCE_DATE_EPOCH)
|
||||
c.Check(st["Date"], Not(Equals), "")
|
||||
c.Check(st["Date"], Not(Equals), "Fri, 13 Feb 2009 23:31:30 UTC")
|
||||
}
|
||||
|
||||
func (s *PublishedRepoSuite) TestPublishLocalRepo(c *C) {
|
||||
err := s.repo2.Publish(s.packagePool, s.provider, s.factory, nil, nil, false, "")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ func (s *PackageRefListSuite) TestNewPackageListFromRefList(c *C) {
|
||||
list, err := NewPackageListFromRefList(reflist, coll, nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(list.Len(), Equals, 4)
|
||||
c.Check(list.Add(s.p4), ErrorMatches, "package already exists and is different: .*")
|
||||
c.Check(list.Add(s.p4), ErrorMatches, "package already exists and is different: .*")
|
||||
|
||||
list, err = NewPackageListFromRefList(nil, coll, nil)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
+102
-3
@@ -70,6 +70,10 @@ type RemoteRepo struct {
|
||||
DownloadUdebs bool
|
||||
// Should we download installer files?
|
||||
DownloadInstaller bool
|
||||
// Should we download AppStream (DEP-11) metadata?
|
||||
DownloadAppStream bool
|
||||
// AppStream files: relative path (e.g. "main/dep11/Components-amd64.yml.gz") → pool path
|
||||
AppStreamFiles map[string]string `codec:"AppStreamFiles" json:"-"`
|
||||
// Packages for json output
|
||||
Packages []string `codec:"-" json:",omitempty"`
|
||||
// "Snapshot" of current list of packages
|
||||
@@ -82,7 +86,7 @@ type RemoteRepo struct {
|
||||
|
||||
// NewRemoteRepo creates new instance of Debian remote repository with specified params
|
||||
func NewRemoteRepo(name string, archiveRoot string, distribution string, components []string,
|
||||
architectures []string, downloadSources bool, downloadUdebs bool, downloadInstaller bool) (*RemoteRepo, error) {
|
||||
architectures []string, downloadSources bool, downloadUdebs bool, downloadInstaller bool, downloadAppStream bool) (*RemoteRepo, error) {
|
||||
result := &RemoteRepo{
|
||||
UUID: uuid.NewString(),
|
||||
Name: name,
|
||||
@@ -93,6 +97,7 @@ func NewRemoteRepo(name string, archiveRoot string, distribution string, compone
|
||||
DownloadSources: downloadSources,
|
||||
DownloadUdebs: downloadUdebs,
|
||||
DownloadInstaller: downloadInstaller,
|
||||
DownloadAppStream: downloadAppStream,
|
||||
}
|
||||
|
||||
err := result.prepare()
|
||||
@@ -111,6 +116,9 @@ func NewRemoteRepo(name string, archiveRoot string, distribution string, compone
|
||||
if result.DownloadUdebs {
|
||||
return nil, fmt.Errorf("debian-installer udebs aren't supported for flat repos")
|
||||
}
|
||||
if result.DownloadAppStream {
|
||||
return nil, fmt.Errorf("AppStream (DEP-11) metadata isn't supported for flat repos")
|
||||
}
|
||||
result.Components = nil
|
||||
}
|
||||
|
||||
@@ -147,6 +155,9 @@ func (repo *RemoteRepo) String() string {
|
||||
if repo.DownloadInstaller {
|
||||
srcFlag += " [installer]"
|
||||
}
|
||||
if repo.DownloadAppStream {
|
||||
srcFlag += " [appstream]"
|
||||
}
|
||||
distribution := repo.Distribution
|
||||
if distribution == "" {
|
||||
distribution = "./"
|
||||
@@ -264,6 +275,82 @@ func (repo *RemoteRepo) InstallerPath(component string, architecture string) str
|
||||
return fmt.Sprintf("%s/installer-%s/current/images/SHA256SUMS", component, architecture)
|
||||
}
|
||||
|
||||
// AppStreamPaths returns dep11 file paths from ReleaseFiles for a given component
|
||||
func (repo *RemoteRepo) AppStreamPaths(component string) []string {
|
||||
prefix := component + "/dep11/"
|
||||
var paths []string
|
||||
for path := range repo.ReleaseFiles {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
// DownloadAppStreamFiles downloads AppStream (DEP-11) metadata files and imports them into the pool
|
||||
func (repo *RemoteRepo) DownloadAppStreamFiles(progress aptly.Progress, d aptly.Downloader,
|
||||
packagePool aptly.PackagePool, checksumStorage aptly.ChecksumStorage, ignoreChecksums bool) error {
|
||||
|
||||
repo.AppStreamFiles = make(map[string]string)
|
||||
|
||||
for _, component := range repo.Components {
|
||||
paths := repo.AppStreamPaths(component)
|
||||
if len(paths) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, relativePath := range paths {
|
||||
info, ok := repo.ReleaseFiles[relativePath]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
url := repo.IndexesRootURL().ResolveReference(&url.URL{Path: relativePath}).String()
|
||||
|
||||
if progress != nil {
|
||||
progress.Printf("Downloading AppStream file %s...\n", relativePath)
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "aptly-appstream-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create temp dir for AppStream file %s: %s", relativePath, err)
|
||||
}
|
||||
|
||||
tempPath := path.Join(tempDir, path.Base(relativePath))
|
||||
|
||||
var expected *utils.ChecksumInfo
|
||||
if !ignoreChecksums {
|
||||
expected = &info
|
||||
}
|
||||
|
||||
err = d.DownloadWithChecksum(gocontext.TODO(), url, tempPath, expected, ignoreChecksums)
|
||||
if err != nil {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
// Skip files that are not found (some repos list dep11 files but don't serve them)
|
||||
if herr, ok := err.(*http.Error); ok && (herr.Code == 404 || herr.Code == 403) {
|
||||
if progress != nil {
|
||||
progress.ColoredPrintf("@y[!]@| @!skipping AppStream file %s: not found@|", relativePath)
|
||||
}
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("unable to download AppStream file %s: %s", relativePath, err)
|
||||
}
|
||||
|
||||
basename := path.Base(relativePath)
|
||||
poolPath, err := packagePool.Import(tempPath, basename, &info, true, checksumStorage)
|
||||
_ = os.RemoveAll(tempDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to import AppStream file %s: %s", relativePath, err)
|
||||
}
|
||||
|
||||
repo.AppStreamFiles[relativePath] = poolPath
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PackageURL returns URL of package file relative to repository root
|
||||
// architecture
|
||||
func (repo *RemoteRepo) PackageURL(filename string) *url.URL {
|
||||
@@ -574,7 +661,7 @@ func (repo *RemoteRepo) DownloadPackageIndexes(progress aptly.Progress, d aptly.
|
||||
if progress != nil {
|
||||
progress.ColoredPrintf("@y[!]@| @!skipping package %s: duplicate in packages index@|", p)
|
||||
}
|
||||
} else if err != nil {
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -612,7 +699,19 @@ func (repo *RemoteRepo) ApplyFilter(dependencyOptions int, filterQuery PackageQu
|
||||
}
|
||||
|
||||
// BuildDownloadQueue builds queue, discards current PackageList
|
||||
func (repo *RemoteRepo) BuildDownloadQueue(packagePool aptly.PackagePool, packageCollection *PackageCollection, checksumStorage aptly.ChecksumStorage, skipExistingPackages bool) (queue []PackageDownloadTask, downloadSize int64, err error) {
|
||||
func (repo *RemoteRepo) BuildDownloadQueue(packagePool aptly.PackagePool, packageCollection *PackageCollection, checksumStorage aptly.ChecksumStorage, skipExistingPackages, latestOnly bool) (queue []PackageDownloadTask, downloadSize int64, err error) {
|
||||
if repo.packageList == nil {
|
||||
err = fmt.Errorf("package list is empty, please (re)download package indexes")
|
||||
return
|
||||
}
|
||||
|
||||
if latestOnly {
|
||||
repo.packageList, err = repo.packageList.FilterLatest()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
queue = make([]PackageDownloadTask, 0, repo.packageList.Len())
|
||||
seen := make(map[string]int, repo.packageList.Len())
|
||||
|
||||
|
||||
+164
-26
@@ -2,6 +2,7 @@ package deb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
@@ -90,8 +91,8 @@ type RemoteRepoSuite struct {
|
||||
var _ = Suite(&RemoteRepoSuite{})
|
||||
|
||||
func (s *RemoteRepoSuite) SetUpTest(c *C) {
|
||||
s.repo, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
s.flat, _ = NewRemoteRepo("exp42", "http://repos.express42.com/virool/precise/", "./", []string{}, []string{}, false, false, false)
|
||||
s.repo, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
s.flat, _ = NewRemoteRepo("exp42", "http://repos.express42.com/virool/precise/", "./", []string{}, []string{}, false, false, false, false)
|
||||
s.downloader = http.NewFakeDownloader().ExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/Release", exampleReleaseFile)
|
||||
s.progress = console.NewProgress(false)
|
||||
s.db, _ = goleveldb.NewOpenDB(c.MkDir())
|
||||
@@ -108,7 +109,7 @@ func (s *RemoteRepoSuite) TearDownTest(c *C) {
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestInvalidURL(c *C) {
|
||||
_, err := NewRemoteRepo("s", "http://lolo%2", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
_, err := NewRemoteRepo("s", "http://lolo%2", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(err, ErrorMatches, ".*(hexadecimal escape in host|percent-encoded characters in host|invalid URL escape).*")
|
||||
}
|
||||
|
||||
@@ -117,12 +118,15 @@ func (s *RemoteRepoSuite) TestFlatCreation(c *C) {
|
||||
c.Check(s.flat.Distribution, Equals, "./")
|
||||
c.Check(s.flat.Components, IsNil)
|
||||
|
||||
flat2, _ := NewRemoteRepo("flat2", "http://pkg.jenkins-ci.org/debian-stable", "binary/", []string{}, []string{}, false, false, false)
|
||||
flat2, _ := NewRemoteRepo("flat2", "http://pkg.jenkins-ci.org/debian-stable", "binary/", []string{}, []string{}, false, false, false, false)
|
||||
c.Check(flat2.IsFlat(), Equals, true)
|
||||
c.Check(flat2.Distribution, Equals, "./binary/")
|
||||
|
||||
_, err := NewRemoteRepo("fl", "http://some.repo/", "./", []string{"main"}, []string{}, false, false, false)
|
||||
_, err := NewRemoteRepo("fl", "http://some.repo/", "./", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Check(err, ErrorMatches, "components aren't supported for flat repos")
|
||||
|
||||
_, err = NewRemoteRepo("fl", "http://some.repo/", "./", []string{}, []string{}, false, false, false, true)
|
||||
c.Check(err, ErrorMatches, "AppStream \\(DEP-11\\) metadata isn't supported for flat repos")
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestString(c *C) {
|
||||
@@ -135,6 +139,43 @@ func (s *RemoteRepoSuite) TestString(c *C) {
|
||||
s.flat.DownloadSources = true
|
||||
c.Check(s.repo.String(), Equals, "[yandex]: http://mirror.yandex.ru/debian/ squeeze [src] [udeb] [installer]")
|
||||
c.Check(s.flat.String(), Equals, "[exp42]: http://repos.express42.com/virool/precise/ ./ [src]")
|
||||
|
||||
s.repo.DownloadAppStream = true
|
||||
c.Check(s.repo.String(), Equals, "[yandex]: http://mirror.yandex.ru/debian/ squeeze [src] [udeb] [installer] [appstream]")
|
||||
|
||||
// AppStream is not supported for flat repos, so no flat test here
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestAppStreamPaths(c *C) {
|
||||
s.repo.ReleaseFiles = nil
|
||||
c.Check(s.repo.AppStreamPaths("main"), DeepEquals, []string(nil))
|
||||
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/binary-amd64/Packages": {Size: 100},
|
||||
"main/dep11/Components-amd64.yml.gz": {Size: 200},
|
||||
"main/dep11/Components-i386.yml.gz": {Size: 300},
|
||||
"main/dep11/icons-48x48.tar.gz": {Size: 400},
|
||||
"contrib/dep11/Components-amd64.yml.gz": {Size: 500},
|
||||
"main/source/Sources": {Size: 600},
|
||||
}
|
||||
|
||||
paths := s.repo.AppStreamPaths("main")
|
||||
c.Check(paths, DeepEquals, []string{
|
||||
"main/dep11/Components-amd64.yml.gz",
|
||||
"main/dep11/Components-i386.yml.gz",
|
||||
"main/dep11/icons-48x48.tar.gz",
|
||||
})
|
||||
|
||||
paths = s.repo.AppStreamPaths("contrib")
|
||||
c.Check(paths, DeepEquals, []string{
|
||||
"contrib/dep11/Components-amd64.yml.gz",
|
||||
})
|
||||
|
||||
paths = s.repo.AppStreamPaths("non-free")
|
||||
c.Check(paths, DeepEquals, []string(nil))
|
||||
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{}
|
||||
c.Check(s.repo.AppStreamPaths("main"), DeepEquals, []string(nil))
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestNumPackages(c *C) {
|
||||
@@ -236,13 +277,13 @@ func (s *RemoteRepoSuite) TestFetchNullVerifier2(c *C) {
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestFetchWrongArchitecture(c *C) {
|
||||
s.repo, _ = NewRemoteRepo("s", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{"xyz"}, false, false, false)
|
||||
s.repo, _ = NewRemoteRepo("s", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{"xyz"}, false, false, false, false)
|
||||
err := s.repo.Fetch(s.downloader, nil, true)
|
||||
c.Assert(err, ErrorMatches, "architecture xyz not available in repo.*")
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestFetchWrongComponent(c *C) {
|
||||
s.repo, _ = NewRemoteRepo("s", "http://mirror.yandex.ru/debian/", "squeeze", []string{"xyz"}, []string{"i386"}, false, false, false)
|
||||
s.repo, _ = NewRemoteRepo("s", "http://mirror.yandex.ru/debian/", "squeeze", []string{"xyz"}, []string{"i386"}, false, false, false, false)
|
||||
err := s.repo.Fetch(s.downloader, nil, true)
|
||||
c.Assert(err, ErrorMatches, "component xyz not available in repo.*")
|
||||
}
|
||||
@@ -281,7 +322,7 @@ func (s *RemoteRepoSuite) TestDownload(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(3))
|
||||
c.Check(queue, HasLen, 1)
|
||||
@@ -308,7 +349,7 @@ func (s *RemoteRepoSuite) TestDownload(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(0))
|
||||
c.Check(queue, HasLen, 0)
|
||||
@@ -329,7 +370,7 @@ func (s *RemoteRepoSuite) TestDownload(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(3))
|
||||
c.Check(queue, HasLen, 1)
|
||||
@@ -356,7 +397,7 @@ func (s *RemoteRepoSuite) TestDownloadWithInstaller(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(3)+int64(len(exampleInstallerManifestFile)))
|
||||
c.Check(queue, HasLen, 2)
|
||||
@@ -382,6 +423,35 @@ func (s *RemoteRepoSuite) TestDownloadWithInstaller(c *C) {
|
||||
c.Check(pkg.Name, Equals, "installer")
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestBuildDownloadQueueLatestOnly(c *C) {
|
||||
s.repo.Architectures = []string{"i386"}
|
||||
|
||||
err := s.repo.Fetch(s.downloader, nil, true)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.downloader.ExpectError("http://mirror.yandex.ru/debian/dists/squeeze/main/binary-i386/Packages.bz2", &http.Error{Code: 404})
|
||||
s.downloader.ExpectError("http://mirror.yandex.ru/debian/dists/squeeze/main/binary-i386/Packages.gz", &http.Error{Code: 404})
|
||||
s.downloader.ExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/main/binary-i386/Packages", examplePackagesFile)
|
||||
|
||||
err = s.repo.DownloadPackageIndexes(s.progress, s.downloader, nil, s.collectionFactory, true, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
stanza := packageStanza.Copy()
|
||||
stanza["Package"] = "amanda-client"
|
||||
stanza["Version"] = "1:3.4.0-1"
|
||||
stanza["Filename"] = "pool/main/a/amanda/amanda-client_3.4.0-1_i386.deb"
|
||||
|
||||
newest := NewPackageFromControlFile(stanza)
|
||||
_ = s.repo.packageList.Add(newest)
|
||||
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, true)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(queue, HasLen, 1)
|
||||
c.Check(queue[0].File.DownloadURL(), Equals, "pool/main/a/amanda/amanda-client_3.4.0-1_i386.deb")
|
||||
c.Check(size, Equals, int64(187518))
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestDownloadWithSources(c *C) {
|
||||
s.repo.Architectures = []string{"i386"}
|
||||
s.repo.DownloadSources = true
|
||||
@@ -400,7 +470,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSources(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(15))
|
||||
c.Check(queue, HasLen, 4)
|
||||
@@ -444,7 +514,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSources(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(0))
|
||||
c.Check(queue, HasLen, 0)
|
||||
@@ -469,7 +539,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSources(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(s.downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(15))
|
||||
c.Check(queue, HasLen, 4)
|
||||
@@ -493,7 +563,7 @@ func (s *RemoteRepoSuite) TestDownloadFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err := s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err := s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(3))
|
||||
c.Check(queue, HasLen, 1)
|
||||
@@ -521,7 +591,7 @@ func (s *RemoteRepoSuite) TestDownloadFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(0))
|
||||
c.Check(queue, HasLen, 0)
|
||||
@@ -543,7 +613,7 @@ func (s *RemoteRepoSuite) TestDownloadFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(3))
|
||||
c.Check(queue, HasLen, 1)
|
||||
@@ -574,7 +644,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSourcesFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err := s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err := s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(15))
|
||||
c.Check(queue, HasLen, 4)
|
||||
@@ -620,7 +690,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSourcesFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(0))
|
||||
c.Check(queue, HasLen, 0)
|
||||
@@ -646,7 +716,7 @@ func (s *RemoteRepoSuite) TestDownloadWithSourcesFlat(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(downloader.Empty(), Equals, true)
|
||||
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
|
||||
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(size, Equals, int64(15))
|
||||
c.Check(queue, HasLen, 4)
|
||||
@@ -655,6 +725,74 @@ func (s *RemoteRepoSuite) TestDownloadWithSourcesFlat(c *C) {
|
||||
c.Assert(s.flat.packageRefs, NotNil)
|
||||
}
|
||||
|
||||
func (s *RemoteRepoSuite) TestDownloadAppStreamFiles(c *C) {
|
||||
// No dep11 entries
|
||||
s.repo.Components = []string{"main"}
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/binary-amd64/Packages": {Size: 100},
|
||||
"main/source/Sources": {Size: 200},
|
||||
}
|
||||
|
||||
err := s.repo.DownloadAppStreamFiles(s.progress, s.downloader, s.packagePool, s.cs, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(s.repo.AppStreamFiles, HasLen, 0)
|
||||
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/dep11/Components-amd64.yml.gz": {Size: 16},
|
||||
"main/dep11/icons-48x48.tar.gz": {Size: 16},
|
||||
"main/binary-amd64/Packages": {Size: 100},
|
||||
}
|
||||
|
||||
downloader := http.NewFakeDownloader()
|
||||
downloader.AnyExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/Components-amd64.yml.gz", "dep11-components")
|
||||
downloader.AnyExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/icons-48x48.tar.gz", "dep11-icons-data")
|
||||
|
||||
err = s.repo.DownloadAppStreamFiles(s.progress, downloader, s.packagePool, s.cs, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(s.repo.AppStreamFiles, HasLen, 2)
|
||||
c.Check(s.repo.AppStreamFiles["main/dep11/Components-amd64.yml.gz"], Not(Equals), "")
|
||||
c.Check(s.repo.AppStreamFiles["main/dep11/icons-48x48.tar.gz"], Not(Equals), "")
|
||||
|
||||
// 404 skipped
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/dep11/Components-amd64.yml.gz": {Size: 16},
|
||||
"main/dep11/icons-48x48.tar.gz": {Size: 15},
|
||||
}
|
||||
|
||||
downloader = http.NewFakeDownloader()
|
||||
downloader.AnyExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/Components-amd64.yml.gz", "dep11-components")
|
||||
downloader.ExpectError("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/icons-48x48.tar.gz", &http.Error{Code: 404, URL: "http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/icons-48x48.tar.gz"})
|
||||
|
||||
err = s.repo.DownloadAppStreamFiles(s.progress, downloader, s.packagePool, s.cs, false)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(s.repo.AppStreamFiles, HasLen, 1)
|
||||
c.Check(s.repo.AppStreamFiles["main/dep11/Components-amd64.yml.gz"], Not(Equals), "")
|
||||
|
||||
// Generic download error propagated
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/dep11/Components-amd64.yml.gz": {Size: 18},
|
||||
}
|
||||
|
||||
downloader = http.NewFakeDownloader()
|
||||
downloader.ExpectError("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/Components-amd64.yml.gz", fmt.Errorf("connection refused"))
|
||||
|
||||
err = s.repo.DownloadAppStreamFiles(s.progress, downloader, s.packagePool, s.cs, false)
|
||||
c.Assert(err, ErrorMatches, "unable to download AppStream file.*connection refused")
|
||||
|
||||
// Bypass checksum validation
|
||||
s.repo.ReleaseFiles = map[string]utils.ChecksumInfo{
|
||||
"main/dep11/Components-amd64.yml.gz": {Size: 999, MD5: "bad", SHA256: "bad"},
|
||||
}
|
||||
|
||||
downloader = http.NewFakeDownloader()
|
||||
downloader.AnyExpectResponse("http://mirror.yandex.ru/debian/dists/squeeze/main/dep11/Components-amd64.yml.gz", "dep11-components")
|
||||
|
||||
err = s.repo.DownloadAppStreamFiles(s.progress, downloader, s.packagePool, s.cs, true)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(s.repo.AppStreamFiles, HasLen, 1)
|
||||
c.Check(s.repo.AppStreamFiles["main/dep11/Components-amd64.yml.gz"], Not(Equals), "")
|
||||
}
|
||||
|
||||
type RemoteRepoCollectionSuite struct {
|
||||
PackageListMixinSuite
|
||||
db database.Storage
|
||||
@@ -677,7 +815,7 @@ func (s *RemoteRepoCollectionSuite) TestAddByName(c *C) {
|
||||
_, err := s.collection.ByName("yandex")
|
||||
c.Assert(err, ErrorMatches, "*.not found")
|
||||
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(s.collection.Add(repo), IsNil)
|
||||
c.Assert(s.collection.Add(repo), ErrorMatches, ".*already exists")
|
||||
|
||||
@@ -695,7 +833,7 @@ func (s *RemoteRepoCollectionSuite) TestByUUID(c *C) {
|
||||
_, err := s.collection.ByUUID("some-uuid")
|
||||
c.Assert(err, ErrorMatches, "*.not found")
|
||||
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(s.collection.Add(repo), IsNil)
|
||||
|
||||
r, err := s.collection.ByUUID(repo.UUID)
|
||||
@@ -709,7 +847,7 @@ func (s *RemoteRepoCollectionSuite) TestByUUID(c *C) {
|
||||
}
|
||||
|
||||
func (s *RemoteRepoCollectionSuite) TestUpdateLoadComplete(c *C) {
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
c.Assert(s.collection.Update(repo), IsNil)
|
||||
|
||||
collection := NewRemoteRepoCollection(s.db)
|
||||
@@ -730,7 +868,7 @@ func (s *RemoteRepoCollectionSuite) TestUpdateLoadComplete(c *C) {
|
||||
}
|
||||
|
||||
func (s *RemoteRepoCollectionSuite) TestForEachAndLen(c *C) {
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
_ = s.collection.Add(repo)
|
||||
|
||||
count := 0
|
||||
@@ -752,10 +890,10 @@ func (s *RemoteRepoCollectionSuite) TestForEachAndLen(c *C) {
|
||||
}
|
||||
|
||||
func (s *RemoteRepoCollectionSuite) TestDrop(c *C) {
|
||||
repo1, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
repo1, _ := NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
_ = s.collection.Add(repo1)
|
||||
|
||||
repo2, _ := NewRemoteRepo("tyndex", "http://mirror.yandex.ru/debian/", "wheezy", []string{"main"}, []string{}, false, false, false)
|
||||
repo2, _ := NewRemoteRepo("tyndex", "http://mirror.yandex.ru/debian/", "wheezy", []string{"main"}, []string{}, false, false, false, false)
|
||||
_ = s.collection.Add(repo2)
|
||||
|
||||
r1, _ := s.collection.ByUUID(repo1.UUID)
|
||||
|
||||
+25
-7
@@ -40,6 +40,9 @@ type Snapshot struct {
|
||||
NotAutomatic string
|
||||
ButAutomaticUpgrades string
|
||||
|
||||
// AppStream files: relative path → pool path (pass-through from mirror)
|
||||
AppStreamFiles map[string]string `json:",omitempty"`
|
||||
|
||||
packageRefs *PackageRefList
|
||||
}
|
||||
|
||||
@@ -59,6 +62,7 @@ func NewSnapshotFromRepository(name string, repo *RemoteRepo) (*Snapshot, error)
|
||||
Origin: repo.Meta["Origin"],
|
||||
NotAutomatic: repo.Meta["NotAutomatic"],
|
||||
ButAutomaticUpgrades: repo.Meta["ButAutomaticUpgrades"],
|
||||
AppStreamFiles: repo.AppStreamFiles,
|
||||
packageRefs: repo.packageRefs,
|
||||
}, nil
|
||||
}
|
||||
@@ -94,14 +98,28 @@ func NewSnapshotFromRefList(name string, sources []*Snapshot, list *PackageRefLi
|
||||
sourceUUIDs[i] = sources[i].UUID
|
||||
}
|
||||
|
||||
// Merge AppStreamFiles from all source snapshots
|
||||
var mergedAppStream map[string]string
|
||||
for _, source := range sources {
|
||||
if len(source.AppStreamFiles) > 0 {
|
||||
if mergedAppStream == nil {
|
||||
mergedAppStream = make(map[string]string)
|
||||
}
|
||||
for k, v := range source.AppStreamFiles {
|
||||
mergedAppStream[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Snapshot{
|
||||
UUID: uuid.NewString(),
|
||||
Name: name,
|
||||
CreatedAt: time.Now(),
|
||||
SourceKind: "snapshot",
|
||||
SourceIDs: sourceUUIDs,
|
||||
Description: description,
|
||||
packageRefs: list,
|
||||
UUID: uuid.NewString(),
|
||||
Name: name,
|
||||
CreatedAt: time.Now(),
|
||||
SourceKind: "snapshot",
|
||||
SourceIDs: sourceUUIDs,
|
||||
Description: description,
|
||||
AppStreamFiles: mergedAppStream,
|
||||
packageRefs: list,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,7 @@ func BenchmarkSnapshotCollectionForEach(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
collection = NewSnapshotCollection(db)
|
||||
|
||||
|
||||
_ = collection.ForEach(func(s *Snapshot) error {
|
||||
_ = collection.ForEach(func(s *Snapshot) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
+60
-4
@@ -19,7 +19,7 @@ var _ = Suite(&SnapshotSuite{})
|
||||
|
||||
func (s *SnapshotSuite) SetUpTest(c *C) {
|
||||
s.SetUpPackages()
|
||||
s.repo, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
s.repo, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
s.repo.packageRefs = s.reflist
|
||||
}
|
||||
|
||||
@@ -101,6 +101,62 @@ func (s *SnapshotSuite) TestEncodeDecode(c *C) {
|
||||
c.Assert(snapshot2.packageRefs, IsNil)
|
||||
}
|
||||
|
||||
func (s *SnapshotSuite) TestSnapshotFromRepositoryAppStream(c *C) {
|
||||
s.repo.AppStreamFiles = map[string]string{
|
||||
"main/dep11/Components-amd64.yml.gz": "ab/cd/Components-amd64.yml.gz",
|
||||
"main/dep11/icons-48x48.tar.gz": "ef/gh/icons-48x48.tar.gz",
|
||||
}
|
||||
snapshot, err := NewSnapshotFromRepository("snap-as", s.repo)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(snapshot.AppStreamFiles, DeepEquals, s.repo.AppStreamFiles)
|
||||
|
||||
s.repo.AppStreamFiles = nil
|
||||
snapshot2, err := NewSnapshotFromRepository("snap-no-as", s.repo)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(snapshot2.AppStreamFiles, IsNil)
|
||||
}
|
||||
|
||||
func (s *SnapshotSuite) TestSnapshotFromRefListAppStreamMerge(c *C) {
|
||||
snap1, _ := NewSnapshotFromRepository("snap1", s.repo)
|
||||
snap1.AppStreamFiles = map[string]string{
|
||||
"main/dep11/Components-amd64.yml.gz": "aa/bb/Components-amd64.yml.gz",
|
||||
"main/dep11/icons-48x48.tar.gz": "cc/dd/icons-48x48.tar.gz",
|
||||
}
|
||||
|
||||
snap2, _ := NewSnapshotFromRepository("snap2", s.repo)
|
||||
snap2.AppStreamFiles = map[string]string{
|
||||
"contrib/dep11/Components-amd64.yml.gz": "ee/ff/Components-amd64.yml.gz",
|
||||
"main/dep11/Components-amd64.yml.gz": "xx/yy/Components-amd64.yml.gz",
|
||||
}
|
||||
|
||||
merged := NewSnapshotFromRefList("merged", []*Snapshot{snap1, snap2}, s.reflist, "Merged")
|
||||
|
||||
c.Check(len(merged.AppStreamFiles), Equals, 3)
|
||||
c.Check(merged.AppStreamFiles["main/dep11/icons-48x48.tar.gz"], Equals, "cc/dd/icons-48x48.tar.gz")
|
||||
c.Check(merged.AppStreamFiles["contrib/dep11/Components-amd64.yml.gz"], Equals, "ee/ff/Components-amd64.yml.gz")
|
||||
c.Check(merged.AppStreamFiles["main/dep11/Components-amd64.yml.gz"], Equals, "xx/yy/Components-amd64.yml.gz")
|
||||
|
||||
snap3, _ := NewSnapshotFromRepository("snap3", s.repo)
|
||||
snap3.AppStreamFiles = nil
|
||||
snap4, _ := NewSnapshotFromRepository("snap4", s.repo)
|
||||
snap4.AppStreamFiles = nil
|
||||
merged2 := NewSnapshotFromRefList("merged2", []*Snapshot{snap3, snap4}, s.reflist, "Merged2")
|
||||
c.Check(merged2.AppStreamFiles, IsNil)
|
||||
}
|
||||
|
||||
func (s *SnapshotSuite) TestEncodeDecodeAppStream(c *C) {
|
||||
snapshot, _ := NewSnapshotFromRepository("snap-as-enc", s.repo)
|
||||
snapshot.AppStreamFiles = map[string]string{
|
||||
"main/dep11/Components-amd64.yml.gz": "ab/cd/Components-amd64.yml.gz",
|
||||
"main/dep11/icons-48x48.tar.gz": "ef/gh/icons-48x48.tar.gz",
|
||||
}
|
||||
|
||||
decoded := &Snapshot{}
|
||||
c.Assert(decoded.Decode(snapshot.Encode()), IsNil)
|
||||
c.Check(decoded.Name, Equals, snapshot.Name)
|
||||
c.Check(decoded.AppStreamFiles, DeepEquals, snapshot.AppStreamFiles)
|
||||
}
|
||||
|
||||
type SnapshotCollectionSuite struct {
|
||||
PackageListMixinSuite
|
||||
db database.Storage
|
||||
@@ -118,11 +174,11 @@ func (s *SnapshotCollectionSuite) SetUpTest(c *C) {
|
||||
s.collection = NewSnapshotCollection(s.db)
|
||||
s.SetUpPackages()
|
||||
|
||||
s.repo1, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false)
|
||||
s.repo1, _ = NewRemoteRepo("yandex", "http://mirror.yandex.ru/debian/", "squeeze", []string{"main"}, []string{}, false, false, false, false)
|
||||
s.repo1.packageRefs = s.reflist
|
||||
s.snapshot1, _ = NewSnapshotFromRepository("snap1", s.repo1)
|
||||
|
||||
s.repo2, _ = NewRemoteRepo("android", "http://mirror.yandex.ru/debian/", "lenny", []string{"main"}, []string{}, false, false, false)
|
||||
s.repo2, _ = NewRemoteRepo("android", "http://mirror.yandex.ru/debian/", "lenny", []string{"main"}, []string{}, false, false, false, false)
|
||||
s.repo2.packageRefs = s.reflist
|
||||
s.snapshot2, _ = NewSnapshotFromRepository("snap2", s.repo2)
|
||||
|
||||
@@ -223,7 +279,7 @@ func (s *SnapshotCollectionSuite) TestFindByRemoteRepoSource(c *C) {
|
||||
c.Check(s.collection.ByRemoteRepoSource(s.repo1), DeepEquals, []*Snapshot{s.snapshot1})
|
||||
c.Check(s.collection.ByRemoteRepoSource(s.repo2), DeepEquals, []*Snapshot{s.snapshot2})
|
||||
|
||||
repo3, _ := NewRemoteRepo("other", "http://mirror.yandex.ru/debian/", "lenny", []string{"main"}, []string{}, false, false, false)
|
||||
repo3, _ := NewRemoteRepo("other", "http://mirror.yandex.ru/debian/", "lenny", []string{"main"}, []string{}, false, false, false, false)
|
||||
|
||||
c.Check(s.collection.ByRemoteRepoSource(repo3), DeepEquals, []*Snapshot(nil))
|
||||
}
|
||||
|
||||
+7
-7
@@ -30,16 +30,16 @@ func CompareVersions(ver1, ver2 string) int {
|
||||
|
||||
// parseVersions breaks down full version to components (possibly empty)
|
||||
func parseVersion(ver string) (epoch, upstream, debian string) {
|
||||
i := strings.LastIndex(ver, "-")
|
||||
if i != -1 {
|
||||
debian, ver = ver[i+1:], ver[:i]
|
||||
}
|
||||
|
||||
i = strings.Index(ver, ":")
|
||||
i := strings.Index(ver, ":")
|
||||
if i != -1 {
|
||||
epoch, ver = ver[:i], ver[i+1:]
|
||||
}
|
||||
|
||||
i = strings.Index(ver, "-")
|
||||
if i != -1 {
|
||||
debian, ver = ver[i+1:], ver[:i]
|
||||
}
|
||||
|
||||
upstream = ver
|
||||
|
||||
return
|
||||
@@ -50,7 +50,7 @@ func compareLexicographic(s1, s2 string) int {
|
||||
i := 0
|
||||
l1, l2 := len(s1), len(s2)
|
||||
|
||||
for !(i == l1 && i == l2) { // break if s1 equal to s2
|
||||
for !(i == l1 && i == l2) { // break if s1 equal to s2
|
||||
|
||||
if i == l2 {
|
||||
// s1 is longer than s2
|
||||
|
||||
+3
-2
@@ -20,10 +20,10 @@ func (s *VersionSuite) TestParseVersion(c *C) {
|
||||
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3.4", "1"})
|
||||
|
||||
e, u, d = parseVersion("1.3-pre4-1")
|
||||
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3-pre4", "1"})
|
||||
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3", "pre4-1"})
|
||||
|
||||
e, u, d = parseVersion("4:1.3-pre4-1")
|
||||
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3-pre4", "1"})
|
||||
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3", "pre4-1"})
|
||||
}
|
||||
|
||||
func (s *VersionSuite) TestCompareLexicographic(c *C) {
|
||||
@@ -100,6 +100,7 @@ func (s *VersionSuite) TestCompareVersions(c *C) {
|
||||
c.Check(CompareVersions("1.0-133-avc", "1.0"), Equals, 1)
|
||||
|
||||
c.Check(CompareVersions("5.2.0.3", "5.2.0.283"), Equals, -1)
|
||||
c.Check(CompareVersions("4.3.5a", "4.3.5-rc3-1"), Equals, 1)
|
||||
}
|
||||
|
||||
func (s *VersionSuite) TestParseDependency(c *C) {
|
||||
|
||||
Vendored
+2
-1
@@ -80,8 +80,9 @@ serve_in_api_mode: false
|
||||
# Enable metrics for Prometheus client
|
||||
enable_metrics_endpoint: false
|
||||
|
||||
# Not implemented in this version.
|
||||
# Enable API documentation on /docs
|
||||
enable_swagger_endpoint: false
|
||||
#enable_swagger_endpoint: false
|
||||
|
||||
# OBSOLETE: use via url param ?_async=true
|
||||
async_api: false
|
||||
|
||||
Vendored
+6
-26
@@ -2,21 +2,12 @@
|
||||
|
||||
include /usr/share/dpkg/pkg-info.mk
|
||||
|
||||
export GOPATH=$(shell pwd)/.go
|
||||
export DEB_BUILD_OPTIONS=crossbuildcanrunhostbinaries
|
||||
|
||||
export GOARCH := $(shell if [ $(DEB_TARGET_ARCH) = "i386" ]; then echo "386"; elif [ $(DEB_TARGET_ARCH) = "armhf" ]; then echo "arm"; else echo $(DEB_TARGET_ARCH); fi)
|
||||
export CGO_ENABLED=1
|
||||
|
||||
ifneq ($(DEB_HOST_GNU_TYPE), $(DEB_BUILD_GNU_TYPE))
|
||||
export CC=$(DEB_HOST_GNU_TYPE)-gcc
|
||||
endif
|
||||
|
||||
%:
|
||||
dh $@ --buildsystem=golang --with=golang,bash-completion
|
||||
|
||||
override_dh_auto_clean:
|
||||
rm -rf build/
|
||||
rm -f docs/docs.go
|
||||
rm -rf obj-$(DEB_TARGET_GNU_TYPE)/
|
||||
dh_auto_clean
|
||||
|
||||
@@ -24,25 +15,14 @@ override_dh_auto_test:
|
||||
# run during autopkgtests
|
||||
|
||||
override_dh_auto_install:
|
||||
rm -f obj-$(DEB_TARGET_GNU_TYPE)/bin/files # where does that file come from ?
|
||||
dh_auto_install -- --no-source
|
||||
|
||||
override_dh_strip:
|
||||
dh_strip --dbg-package=aptly-dbg
|
||||
|
||||
override_dh_golang: # fails on non native debian build
|
||||
|
||||
# override_dh_makeshlibs: # fails with cross compiling on non native debian build
|
||||
|
||||
override_dh_dwz: # somehow dwz works only with certain newer debhelper versions
|
||||
dhver=`dpkg-query -f '$${Version}' -W debhelper`; (dpkg --compare-versions "$$dhver" lt 13 || test "$$dhver" = "13.3.4" || test "$$dhver" = "13.6ubuntu1") || dh_dwz
|
||||
|
||||
override_dh_shlibdeps:
|
||||
ifneq ($(DEB_HOST_GNU_TYPE), $(DEB_BUILD_GNU_TYPE))
|
||||
LD_LIBRARY_PATH=/usr/$(DEB_HOST_GNU_TYPE)/lib:$$LD_LIBRARY_PATH dh_shlibdeps
|
||||
else
|
||||
dh_shlibdeps
|
||||
endif
|
||||
|
||||
override_dh_auto_build:
|
||||
echo $(DEB_VERSION) > VERSION
|
||||
go build -buildmode=pie -o usr/bin/aptly
|
||||
echo $(DEB_VERSION) > obj-$(DEB_TARGET_GNU_TYPE)/src/github.com/aptly-dev/aptly/VERSION
|
||||
mkdir -p obj-$(DEB_TARGET_GNU_TYPE)/src/github.com/aptly-dev/aptly/debian
|
||||
cp debian/aptly.conf obj-$(DEB_TARGET_GNU_TYPE)/src/github.com/aptly-dev/aptly/debian/
|
||||
dh_auto_build
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
# GPG Keys Management
|
||||
|
||||
GPG keys are used by aptly to verify the authenticity of remote repository Release files when creating mirrors. This document describes the API endpoints for managing GPG keys in the aptly keyring.
|
||||
|
||||
## Overview
|
||||
|
||||
Aptly uses GNU Privacy Guard (GPG) to verify signed repository metadata. You must add the repository's GPG public key to aptly's keyring before creating mirrors that verify signatures.
|
||||
|
||||
Keys are stored in the aptly keyring (default: `trustedkeys.gpg`). You can have multiple keyrings and specify which one to use via the `Keyring` parameter.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### List GPG Keys
|
||||
|
||||
**GET /api/gpg/keys**
|
||||
|
||||
Lists all public GPG keys currently installed in the aptly keyring.
|
||||
|
||||
**Parameters:**
|
||||
- `keyring` (query, optional): Keyring file to list keys from. Default: `trustedkeys.gpg`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"Keys": [
|
||||
{
|
||||
"KeyID": "8B48AD6246925553",
|
||||
"Fingerprint": "D8E8F5A516E7A2C4F3E4B5A6C7D8E9F0",
|
||||
"Validity": "f",
|
||||
"UserIDs": ["John Doe <john@example.com>"],
|
||||
"CreatedAt": "1611864000"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- `200 OK`: Keys successfully retrieved
|
||||
- `400 Bad Request`: GPG execution failed or invalid parameters
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl http://localhost:8080/api/gpg/keys
|
||||
curl "http://localhost:8080/api/gpg/keys?keyring=custom.gpg"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Add GPG Key
|
||||
|
||||
**POST /api/gpg/key**
|
||||
|
||||
Adds a GPG public key to the aptly keyring. Keys can be added in two ways:
|
||||
1. Provide the ASCII-armored key directly
|
||||
2. Provide a key server and key ID(s) to download from
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"Keyring": "trustedkeys.gpg",
|
||||
"GpgKeyArmor": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
|
||||
"Keyserver": "hkp://keyserver.ubuntu.com:80",
|
||||
"GpgKeyID": "8B48AD6246925553"
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `Keyring` (optional): Keyring file to add keys to. Default: `trustedkeys.gpg`
|
||||
- `GpgKeyArmor` (optional): ASCII-armored GPG public key
|
||||
- `Keyserver` (optional): Keyserver URL (e.g., `hkp://keyserver.ubuntu.com:80`)
|
||||
- `GpgKeyID` (optional): Space-separated key IDs to download from keyserver
|
||||
|
||||
**Status Codes:**
|
||||
- `200 OK`: Key successfully added
|
||||
- `400 Bad Request`: Invalid parameters or GPG execution failed
|
||||
|
||||
**Example - From ASCII Key:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"GpgKeyArmor": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v2\n...\n-----END PGP PUBLIC KEY BLOCK-----"
|
||||
}'
|
||||
```
|
||||
|
||||
**Example - From Keyserver:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Keyserver": "hkp://keyserver.ubuntu.com:80",
|
||||
"GpgKeyID": "8B48AD6246925553 A1B2C3D4E5F67890"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Delete GPG Key
|
||||
|
||||
**DELETE /api/gpg/key**
|
||||
|
||||
Removes a GPG key from the aptly keyring.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"Keyring": "trustedkeys.gpg",
|
||||
"GpgKeyID": "8B48AD6246925553"
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `Keyring` (optional): Keyring file to delete from. Default: `trustedkeys.gpg`
|
||||
- `GpgKeyID` (required): Key ID or fingerprint to delete
|
||||
|
||||
**Status Codes:**
|
||||
- `200 OK`: Key successfully deleted
|
||||
- `400 Bad Request`: Invalid parameters or GPG execution failed
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"GpgKeyID": "8B48AD6246925553"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Verify Downloaded Repository Metadata
|
||||
|
||||
Before creating a mirror from a signed repository, add the repository's GPG key:
|
||||
|
||||
```bash
|
||||
# Add the key from a keyserver
|
||||
curl -X POST http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Keyserver": "hkp://keyserver.ubuntu.com:80",
|
||||
"GpgKeyID": "EB9B46B91F2D3B7E"
|
||||
}'
|
||||
|
||||
# Now create a mirror with signature verification
|
||||
# (signature verification configured in mirror settings)
|
||||
```
|
||||
|
||||
### 2. Manage Multiple Keyrings
|
||||
|
||||
Aptly supports using different keyrings for different purposes. For example, one for Debian repositories and another for custom internal repositories:
|
||||
|
||||
```bash
|
||||
# Add key to Debian keyring
|
||||
curl -X POST http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"Keyring": "debian-keys.gpg",
|
||||
"Keyserver": "hkp://keyserver.ubuntu.com:80",
|
||||
"GpgKeyID": "EB9B46B91F2D3B7E"
|
||||
}'
|
||||
|
||||
# List keys in Debian keyring
|
||||
curl "http://localhost:8080/api/gpg/keys?keyring=debian-keys.gpg"
|
||||
```
|
||||
|
||||
### 3. Remove Compromised Keys
|
||||
|
||||
If a GPG key is compromised, remove it from the keyring immediately:
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8080/api/gpg/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"GpgKeyID": "COMPROMISED_KEY_ID"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Validity Values
|
||||
|
||||
Keys retrieved from `GET /api/gpg/keys` have a `Validity` field with the following possible values:
|
||||
|
||||
- `u` — Unknown validity
|
||||
- `f` — Full trust
|
||||
- `m` — Marginal trust
|
||||
- `n` — Never trust
|
||||
- `-` — Trust not set
|
||||
|
||||
The trust level is typically managed in your GPG configuration and does not affect aptly's ability to verify signatures.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"failed to list keys"**
|
||||
- Check that the keyring file exists and is readable
|
||||
- Verify GPG is installed and configured
|
||||
|
||||
**"unable to delete key: no public key"**
|
||||
- The key might not exist in the keyring
|
||||
- Verify the key ID is correct by listing keys first
|
||||
|
||||
**"invalid request body"**
|
||||
- Ensure the JSON is properly formatted
|
||||
- For POST requests, provide either `GpgKeyArmor` or (`Keyserver` + `GpgKeyID`)
|
||||
- For DELETE requests, `GpgKeyID` is required
|
||||
+20
-1
@@ -11,7 +11,26 @@ Repositories can be published to local directories, Amazon S3 buckets, Azure or
|
||||
|
||||
GPG key is required to sign any published repository. The key pari should be generated before publishing.
|
||||
|
||||
Publiс part of the key should be exported from your keyring using `gpg --export --armor` and imported on the system which uses a published repository.
|
||||
Public part of the key should be exported from your keyring using `gpg --export --armor` and imported on the system which uses a published repository.
|
||||
|
||||
* Multiple signing keys can be defined in aptly.conf using the gpgKeys array:
|
||||
```
|
||||
"gpgKeys": [
|
||||
"KEY_ID_x",
|
||||
"KEY_ID_y"
|
||||
]
|
||||
```
|
||||
|
||||
* It is also possible to pass multiple keys via the CLI using the repeatable `--gpg-key` flag:
|
||||
```
|
||||
aptly publish repo my-repo --gpg-key=KEY_ID_a --gpg-key=KEY_ID_b
|
||||
```
|
||||
* When using the REST API, the `gpgKey` parameter supports a comma-separated list of key IDs:
|
||||
```
|
||||
"gpgKey": "KEY_ID_a,KEY_ID_b"
|
||||
```
|
||||
* If `--gpg-key` is specified on the command line, or `gpgKey` is provided via the REST API, it takes precedence over any gpgKeys configuration in aptly.conf.
|
||||
* With multi-key support, aptly will sign all Release files (both clearsigned and detached signatures) with each provided key, ensuring a smooth key rotation process while maintaining compatibility for existing clients.
|
||||
|
||||
#### Parameters
|
||||
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
"github.com/aptly-dev/aptly/utils"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type LinkFromPoolConcurrencySuite struct {
|
||||
root string
|
||||
poolDir string
|
||||
storage *PublishedStorage
|
||||
pool *PackagePool
|
||||
cs aptly.ChecksumStorage
|
||||
testFile string
|
||||
testContent []byte
|
||||
testChecksums utils.ChecksumInfo
|
||||
srcPoolPath string
|
||||
}
|
||||
|
||||
var _ = Suite(&LinkFromPoolConcurrencySuite{})
|
||||
|
||||
func (s *LinkFromPoolConcurrencySuite) SetUpTest(c *C) {
|
||||
s.root = c.MkDir()
|
||||
s.poolDir = filepath.Join(s.root, "pool")
|
||||
publishDir := filepath.Join(s.root, "public")
|
||||
|
||||
// Create package pool and published storage
|
||||
s.pool = NewPackagePool(s.poolDir, true)
|
||||
s.storage = NewPublishedStorage(publishDir, "copy", "checksum")
|
||||
s.cs = NewMockChecksumStorage()
|
||||
|
||||
// Create test file content
|
||||
s.testContent = []byte("test package content for concurrency testing")
|
||||
s.testFile = filepath.Join(s.root, "test-package.deb")
|
||||
|
||||
err := os.WriteFile(s.testFile, s.testContent, 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Calculate checksums
|
||||
md5sum, err := utils.MD5ChecksumForFile(s.testFile)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
s.testChecksums = utils.ChecksumInfo{
|
||||
Size: int64(len(s.testContent)),
|
||||
MD5: md5sum,
|
||||
}
|
||||
|
||||
// Import the test file into the pool
|
||||
s.srcPoolPath, err = s.pool.Import(s.testFile, "test-package.deb", &s.testChecksums, false, s.cs)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *LinkFromPoolConcurrencySuite) TestLinkFromPoolConcurrency(c *C) {
|
||||
// Test concurrent LinkFromPool operations to ensure no race conditions
|
||||
concurrency := 5000
|
||||
iterations := 10
|
||||
|
||||
for iter := 0; iter < iterations; iter++ {
|
||||
c.Logf("Iteration %d: Testing concurrent LinkFromPool with %d goroutines", iter+1, concurrency)
|
||||
|
||||
destPath := fmt.Sprintf("main/t/test%d", iter)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, concurrency)
|
||||
successes := make(chan struct{}, concurrency)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Launch concurrent LinkFromPool operations
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
// Use force=true to test the most vulnerable code path (remove-then-create)
|
||||
err := s.storage.LinkFromPool(
|
||||
"", // publishedPrefix
|
||||
destPath, // publishedRelPath
|
||||
"test-package.deb", // fileName
|
||||
s.pool, // sourcePool
|
||||
s.srcPoolPath, // sourcePath
|
||||
s.testChecksums, // sourceChecksums
|
||||
true, // force - this triggers vulnerable remove-then-create pattern
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
errors <- fmt.Errorf("goroutine %d failed: %v", id, err)
|
||||
} else {
|
||||
successes <- struct{}{}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for completion
|
||||
wg.Wait()
|
||||
duration := time.Since(start)
|
||||
|
||||
close(errors)
|
||||
close(successes)
|
||||
|
||||
// Count results
|
||||
errorCount := 0
|
||||
successCount := 0
|
||||
var firstError error
|
||||
|
||||
for err := range errors {
|
||||
errorCount++
|
||||
if firstError == nil {
|
||||
firstError = err
|
||||
}
|
||||
c.Logf("Race condition error: %v", err)
|
||||
}
|
||||
|
||||
for range successes {
|
||||
successCount++
|
||||
}
|
||||
|
||||
c.Logf("Results: %d successes, %d errors, took %v", successCount, errorCount, duration)
|
||||
|
||||
// Assert no race conditions occurred
|
||||
if errorCount > 0 {
|
||||
c.Fatalf("Race condition detected in iteration %d! "+
|
||||
"Errors: %d out of %d operations (%.1f%% failure rate). "+
|
||||
"First error: %v. "+
|
||||
"This indicates the fix is not working properly.",
|
||||
iter+1, errorCount, concurrency,
|
||||
float64(errorCount)/float64(concurrency)*100, firstError)
|
||||
}
|
||||
|
||||
// Verify the final file exists and has correct content
|
||||
finalFile := filepath.Join(s.storage.rootPath, destPath, "test-package.deb")
|
||||
_, err := os.Stat(finalFile)
|
||||
c.Assert(err, IsNil, Commentf("Final file should exist after concurrent operations"))
|
||||
|
||||
content, err := os.ReadFile(finalFile)
|
||||
c.Assert(err, IsNil, Commentf("Should be able to read final file"))
|
||||
c.Assert(content, DeepEquals, s.testContent, Commentf("File content should be intact after concurrent operations"))
|
||||
|
||||
c.Logf("✓ Iteration %d: No race conditions detected", iter+1)
|
||||
}
|
||||
|
||||
c.Logf("SUCCESS: Handled %d total concurrent operations across %d iterations with no race conditions",
|
||||
concurrency*iterations, iterations)
|
||||
}
|
||||
|
||||
func (s *LinkFromPoolConcurrencySuite) TestLinkFromPoolConcurrencyDifferentFiles(c *C) {
|
||||
// Test concurrent operations on different files to ensure no blocking
|
||||
concurrency := 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, concurrency)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Launch concurrent operations on different destination files
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
destPath := fmt.Sprintf("main/t/test-file-%d", id)
|
||||
|
||||
err := s.storage.LinkFromPool(
|
||||
"", // publishedPrefix
|
||||
destPath, // publishedRelPath
|
||||
"test-package.deb", // fileName
|
||||
s.pool, // sourcePool
|
||||
s.srcPoolPath, // sourcePath
|
||||
s.testChecksums, // sourceChecksums
|
||||
false, // force
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
errors <- fmt.Errorf("goroutine %d failed: %v", id, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for completion
|
||||
wg.Wait()
|
||||
duration := time.Since(start)
|
||||
|
||||
close(errors)
|
||||
|
||||
// Count errors
|
||||
errorCount := 0
|
||||
for err := range errors {
|
||||
errorCount++
|
||||
c.Logf("Error: %v", err)
|
||||
}
|
||||
|
||||
c.Assert(errorCount, Equals, 0, Commentf("No errors should occur when linking to different files"))
|
||||
c.Logf("SUCCESS: %d concurrent operations on different files completed in %v", concurrency, duration)
|
||||
|
||||
// Verify all files were created correctly
|
||||
for i := 0; i < concurrency; i++ {
|
||||
finalFile := filepath.Join(s.storage.rootPath, fmt.Sprintf("main/t/test-file-%d", i), "test-package.deb")
|
||||
_, err := os.Stat(finalFile)
|
||||
c.Assert(err, IsNil, Commentf("File %d should exist", i))
|
||||
|
||||
content, err := os.ReadFile(finalFile)
|
||||
c.Assert(err, IsNil, Commentf("Should be able to read file %d", i))
|
||||
c.Assert(content, DeepEquals, s.testContent, Commentf("File %d content should be correct", i))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LinkFromPoolConcurrencySuite) TestLinkFromPoolWithoutForceNoConcurrencyIssues(c *C) {
|
||||
// Test that when force=false, concurrent operations fail gracefully without corruption
|
||||
concurrency := 20
|
||||
destPath := "main/t/single-dest"
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, concurrency)
|
||||
successes := make(chan struct{}, concurrency)
|
||||
|
||||
// First, create the file so subsequent operations will conflict
|
||||
err := s.storage.LinkFromPool("", destPath, "test-package.deb", s.pool, s.srcPoolPath, s.testChecksums, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Launch concurrent operations that should mostly fail
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
err := s.storage.LinkFromPool(
|
||||
"", // publishedPrefix
|
||||
destPath, // publishedRelPath
|
||||
"test-package.deb", // fileName
|
||||
s.pool, // sourcePool
|
||||
s.srcPoolPath, // sourcePath
|
||||
s.testChecksums, // sourceChecksums
|
||||
false, // force=false - should fail if file exists and is same
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
errors <- err
|
||||
} else {
|
||||
successes <- struct{}{}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for completion
|
||||
wg.Wait()
|
||||
duration := time.Since(start)
|
||||
|
||||
close(errors)
|
||||
close(successes)
|
||||
|
||||
errorCount := 0
|
||||
successCount := 0
|
||||
|
||||
for range errors {
|
||||
errorCount++
|
||||
}
|
||||
|
||||
for range successes {
|
||||
successCount++
|
||||
}
|
||||
|
||||
c.Logf("Results with force=false: %d successes, %d errors, took %v", successCount, errorCount, duration)
|
||||
|
||||
// With force=false and identical files, operations should succeed (file already exists with same content)
|
||||
// No race conditions should cause crashes or corruption
|
||||
c.Assert(errorCount, Equals, 0, Commentf("With identical files and force=false, operations should succeed"))
|
||||
|
||||
// Verify the file still exists and has correct content
|
||||
finalFile := filepath.Join(s.storage.rootPath, destPath, "test-package.deb")
|
||||
content, err := os.ReadFile(finalFile)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(content, DeepEquals, s.testContent, Commentf("File should not be corrupted by concurrent access"))
|
||||
}
|
||||
@@ -241,7 +241,7 @@ func (pool *PackagePool) Import(srcPath, basename string, checksums *utils.Check
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = source.Close()
|
||||
_ = source.Close()
|
||||
}()
|
||||
|
||||
sourceInfo, err := source.Stat()
|
||||
|
||||
+55
-8
@@ -15,6 +15,10 @@ import (
|
||||
"github.com/saracen/walker"
|
||||
)
|
||||
|
||||
// syncFile is a seam to allow tests to force fsync failures (e.g. ENOSPC).
|
||||
// In production it calls (*os.File).Sync().
|
||||
var syncFile = func(f *os.File) error { return f.Sync() }
|
||||
|
||||
// PublishedStorage abstract file system with public dirs (published repos)
|
||||
type PublishedStorage struct {
|
||||
rootPath string
|
||||
@@ -22,6 +26,26 @@ type PublishedStorage struct {
|
||||
verifyMethod uint
|
||||
}
|
||||
|
||||
// Global mutex map to prevent concurrent access to the same destinationPath in LinkFromPool
|
||||
var (
|
||||
fileLockMutex sync.Mutex
|
||||
fileLocks = make(map[string]*sync.Mutex)
|
||||
)
|
||||
|
||||
// getFileLock returns a mutex for a specific file path to prevent concurrent modifications
|
||||
func getFileLock(filePath string) *sync.Mutex {
|
||||
fileLockMutex.Lock()
|
||||
defer fileLockMutex.Unlock()
|
||||
|
||||
if mutex, exists := fileLocks[filePath]; exists {
|
||||
return mutex
|
||||
}
|
||||
|
||||
mutex := &sync.Mutex{}
|
||||
fileLocks[filePath] = mutex
|
||||
return mutex
|
||||
}
|
||||
|
||||
// Check interfaces
|
||||
var (
|
||||
_ aptly.PublishedStorage = (*PublishedStorage)(nil)
|
||||
@@ -99,7 +123,17 @@ func (storage *PublishedStorage) PutFile(path string, sourceFilename string) err
|
||||
}()
|
||||
|
||||
_, err = io.Copy(f, source)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sync to ensure all data is written to disk and catch ENOSPC errors
|
||||
err = syncFile(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error syncing file %s: %s", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes single file under public path
|
||||
@@ -136,6 +170,12 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
|
||||
baseName := filepath.Base(fileName)
|
||||
poolPath := filepath.Join(storage.rootPath, publishedPrefix, publishedRelPath, filepath.Dir(fileName))
|
||||
destinationPath := filepath.Join(poolPath, baseName)
|
||||
|
||||
// Acquire file-specific lock to prevent concurrent access to the same file
|
||||
fileLock := getFileLock(destinationPath)
|
||||
fileLock.Lock()
|
||||
defer fileLock.Unlock()
|
||||
|
||||
var localSourcePool aptly.LocalPackagePool
|
||||
if storage.linkMethod != LinkMethodCopy {
|
||||
@@ -154,7 +194,7 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
|
||||
var dstStat os.FileInfo
|
||||
|
||||
dstStat, err = os.Stat(filepath.Join(poolPath, baseName))
|
||||
dstStat, err = os.Stat(destinationPath)
|
||||
if err == nil {
|
||||
// already exists, check source file
|
||||
|
||||
@@ -173,7 +213,7 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
} else {
|
||||
// if source and destination have the same checksums, no need to copy
|
||||
var dstMD5 string
|
||||
dstMD5, err = utils.MD5ChecksumForFile(filepath.Join(poolPath, baseName))
|
||||
dstMD5, err = utils.MD5ChecksumForFile(destinationPath)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -204,11 +244,11 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
|
||||
// source and destination have different inodes, if !forced, this is fatal error
|
||||
if !force {
|
||||
return fmt.Errorf("error linking file to %s: file already exists and is different", filepath.Join(poolPath, baseName))
|
||||
return fmt.Errorf("error linking file to %s: file already exists and is different", destinationPath)
|
||||
}
|
||||
|
||||
// forced, so remove destination
|
||||
err = os.Remove(filepath.Join(poolPath, baseName))
|
||||
err = os.Remove(destinationPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -223,7 +263,7 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
}
|
||||
|
||||
var dst *os.File
|
||||
dst, err = os.Create(filepath.Join(poolPath, baseName))
|
||||
dst, err = os.Create(destinationPath)
|
||||
if err != nil {
|
||||
_ = r.Close()
|
||||
return err
|
||||
@@ -242,11 +282,18 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
return err
|
||||
}
|
||||
|
||||
// Sync to ensure all data is written to disk and catch ENOSPC errors
|
||||
err = syncFile(dst)
|
||||
if err != nil {
|
||||
_ = dst.Close()
|
||||
return fmt.Errorf("error syncing file %s: %s", destinationPath, err)
|
||||
}
|
||||
|
||||
err = dst.Close()
|
||||
} else if storage.linkMethod == LinkMethodSymLink {
|
||||
err = localSourcePool.Symlink(sourcePath, filepath.Join(poolPath, baseName))
|
||||
err = localSourcePool.Symlink(sourcePath, destinationPath)
|
||||
} else {
|
||||
err = localSourcePool.Link(sourcePath, filepath.Join(poolPath, baseName))
|
||||
err = localSourcePool.Link(sourcePath, destinationPath)
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/aptly-dev/aptly/aptly"
|
||||
@@ -11,6 +17,77 @@ import (
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type fakeProgress struct{ bytes.Buffer }
|
||||
|
||||
func (p *fakeProgress) Start() {}
|
||||
func (p *fakeProgress) Shutdown() {}
|
||||
func (p *fakeProgress) Flush() {}
|
||||
func (p *fakeProgress) InitBar(count int64, isBytes bool, barType aptly.BarType) {
|
||||
}
|
||||
func (p *fakeProgress) ShutdownBar() {}
|
||||
func (p *fakeProgress) AddBar(count int) {}
|
||||
func (p *fakeProgress) SetBar(count int) {}
|
||||
func (p *fakeProgress) Printf(msg string, a ...interface{}) {
|
||||
}
|
||||
func (p *fakeProgress) ColoredPrintf(msg string, a ...interface{}) {
|
||||
}
|
||||
func (p *fakeProgress) PrintfStdErr(msg string, a ...interface{}) {
|
||||
}
|
||||
|
||||
type fakeRSC struct {
|
||||
*bytes.Reader
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func (r *fakeRSC) Close() error { return r.closeErr }
|
||||
|
||||
type fakePool struct {
|
||||
sizeErr error
|
||||
openFn func(string) (aptly.ReadSeekerCloser, error)
|
||||
}
|
||||
|
||||
type fakeLocalPool struct {
|
||||
fakePool
|
||||
statErr error
|
||||
}
|
||||
|
||||
func (p *fakeLocalPool) Stat(path string) (os.FileInfo, error) { return nil, p.statErr }
|
||||
func (p *fakeLocalPool) GenerateTempPath(filename string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (p *fakeLocalPool) Link(path, dstPath string) error { return nil }
|
||||
func (p *fakeLocalPool) Symlink(path, dstPath string) error { return nil }
|
||||
func (p *fakeLocalPool) FullPath(path string) string { return path }
|
||||
|
||||
func (p *fakePool) Verify(poolPath, basename string, checksums *utils.ChecksumInfo, checksumStorage aptly.ChecksumStorage) (string, bool, error) {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func (p *fakePool) Import(srcPath, basename string, checksums *utils.ChecksumInfo, move bool, storage aptly.ChecksumStorage) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *fakePool) LegacyPath(filename string, checksums *utils.ChecksumInfo) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *fakePool) Size(path string) (int64, error) {
|
||||
if p.sizeErr != nil {
|
||||
return 0, p.sizeErr
|
||||
}
|
||||
return int64(len(path)), nil
|
||||
}
|
||||
|
||||
func (p *fakePool) Open(path string) (aptly.ReadSeekerCloser, error) {
|
||||
if p.openFn != nil {
|
||||
return p.openFn(path)
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
func (p *fakePool) FilepathList(progress aptly.Progress) ([]string, error) { return nil, nil }
|
||||
func (p *fakePool) Remove(path string) (int64, error) { return 0, nil }
|
||||
|
||||
type PublishedStorageSuite struct {
|
||||
root string
|
||||
storage *PublishedStorage
|
||||
@@ -69,6 +146,14 @@ func (s *PublishedStorageSuite) TestPutFile(c *C) {
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestPutFileReturnsErrorIfSourceMissing(c *C) {
|
||||
err := s.storage.MkDir("ppa/dists/squeeze/")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile("ppa/dists/squeeze/Release", filepath.Join(s.root, "no-such-file"))
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestFilelist(c *C) {
|
||||
err := s.storage.MkDir("ppa/pool/main/a/ab/")
|
||||
c.Assert(err, IsNil)
|
||||
@@ -134,6 +219,11 @@ func (s *PublishedStorageSuite) TestSymLink(c *C) {
|
||||
c.Assert(linkTarget, Equals, "ppa/dists/squeeze/Release")
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestReadLinkReturnsErrorOnMissingPath(c *C) {
|
||||
_, err := s.storage.ReadLink("does/not/exist")
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestHardLink(c *C) {
|
||||
err := s.storage.MkDir("ppa/dists/squeeze/")
|
||||
c.Assert(err, IsNil)
|
||||
@@ -163,6 +253,18 @@ func (s *PublishedStorageSuite) TestRemoveDirs(c *C) {
|
||||
c.Assert(os.IsNotExist(err), Equals, true)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemoveDirsWithProgress(c *C) {
|
||||
err := s.storage.MkDir("ppa/dists/squeeze/")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s.storage.PutFile("ppa/dists/squeeze/Release", "/dev/null")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
p := &fakeProgress{}
|
||||
err = s.storage.RemoveDirs("ppa/dists/", p)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *PublishedStorageSuite) TestRemove(c *C) {
|
||||
err := s.storage.MkDir("ppa/dists/squeeze/")
|
||||
c.Assert(err, IsNil)
|
||||
@@ -337,3 +439,287 @@ func (s *PublishedStorageSuite) TestRootRemove(c *C) {
|
||||
dirStorage := NewPublishedStorage(pwd, "", "")
|
||||
c.Assert(func() { _ = dirStorage.RemoveDirs("", nil) }, PanicMatches, "trying to remove the root directory")
|
||||
}
|
||||
|
||||
// DiskFullSuite uses a loopback mount; requires Linux + root.
|
||||
|
||||
type DiskFullSuite struct {
|
||||
root string
|
||||
}
|
||||
|
||||
var _ = Suite(&DiskFullSuite{})
|
||||
|
||||
func (s *DiskFullSuite) SetUpTest(c *C) {
|
||||
if runtime.GOOS != "linux" {
|
||||
c.Skip("disk full tests only run on Linux")
|
||||
}
|
||||
|
||||
s.root = c.MkDir()
|
||||
}
|
||||
|
||||
func (s *DiskFullSuite) TestPutFileOutOfSpace(c *C) {
|
||||
mountPoint := "/smallfs"
|
||||
if os.Geteuid() == 0 {
|
||||
mountPoint = filepath.Join(s.root, "smallfs")
|
||||
err := os.MkdirAll(mountPoint, 0777)
|
||||
c.Assert(err, IsNil)
|
||||
fsImage := filepath.Join(s.root, "small.img")
|
||||
cmd := exec.Command("dd", "if=/dev/zero", "of="+fsImage, "bs=1M", "count=1")
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
cmd = exec.Command("mkfs.ext4", "-F", fsImage)
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
cmd = exec.Command("mount", "-o", "loop", fsImage, mountPoint)
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
defer func() {
|
||||
_ = exec.Command("umount", mountPoint).Run()
|
||||
}()
|
||||
}
|
||||
|
||||
storage := NewPublishedStorage(mountPoint, "", "")
|
||||
largeFile := filepath.Join(s.root, "largefile")
|
||||
cmd := exec.Command("dd", "if=/dev/zero", "of="+largeFile, "bs=1M", "count=2")
|
||||
err := cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = storage.PutFile("testfile", largeFile)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(strings.Contains(err.Error(), "no space left on device") ||
|
||||
strings.Contains(err.Error(), "sync"), Equals, true,
|
||||
Commentf("Expected disk full error, got: %v", err))
|
||||
}
|
||||
|
||||
func (s *DiskFullSuite) TestLinkFromPoolCopyOutOfSpace(c *C) {
|
||||
mountPoint := "/smallfs"
|
||||
if os.Geteuid() == 0 {
|
||||
mountPoint = filepath.Join(s.root, "smallfs")
|
||||
err := os.MkdirAll(mountPoint, 0777)
|
||||
c.Assert(err, IsNil)
|
||||
fsImage := filepath.Join(s.root, "small.img")
|
||||
|
||||
cmd := exec.Command("dd", "if=/dev/zero", "of="+fsImage, "bs=1M", "count=1")
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
cmd = exec.Command("mkfs.ext4", "-F", fsImage)
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
cmd = exec.Command("mount", "-o", "loop", fsImage, mountPoint)
|
||||
err = cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
defer func() {
|
||||
_ = exec.Command("umount", mountPoint).Run()
|
||||
}()
|
||||
}
|
||||
|
||||
storage := NewPublishedStorage(mountPoint, "copy", "")
|
||||
|
||||
poolPath := filepath.Join(s.root, "pool")
|
||||
pool := NewPackagePool(poolPath, false)
|
||||
cs := NewMockChecksumStorage()
|
||||
|
||||
largeFile := filepath.Join(s.root, "package.deb")
|
||||
cmd := exec.Command("dd", "if=/dev/zero", "of="+largeFile, "bs=1M", "count=2")
|
||||
err := cmd.Run()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
sourceChecksum, err := utils.ChecksumsForFile(largeFile)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
srcPoolPath, err := pool.Import(largeFile, "package.deb",
|
||||
&utils.ChecksumInfo{MD5: "d41d8cd98f00b204e9800998ecf8427e"}, false, cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = storage.LinkFromPool("", "pool/main/p/package", "package.deb",
|
||||
pool, srcPoolPath, sourceChecksum, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(strings.Contains(err.Error(), "no space left on device") ||
|
||||
strings.Contains(err.Error(), "sync"), Equals, true,
|
||||
Commentf("Expected disk full error, got: %v", err))
|
||||
}
|
||||
|
||||
type DiskFullNoRootSuite struct {
|
||||
root string
|
||||
}
|
||||
|
||||
var _ = Suite(&DiskFullNoRootSuite{})
|
||||
|
||||
func (s *DiskFullNoRootSuite) SetUpTest(c *C) {
|
||||
s.root = c.MkDir()
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestSyncIsCalled(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "", "")
|
||||
sourceFile := filepath.Join(s.root, "source.txt")
|
||||
err := os.WriteFile(sourceFile, []byte("test content"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
err = storage.PutFile("dest.txt", sourceFile)
|
||||
c.Assert(err, IsNil)
|
||||
content, err := os.ReadFile(filepath.Join(s.root, "dest.txt"))
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(string(content), Equals, "test content")
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopySyncIsCalled(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "")
|
||||
poolPath := filepath.Join(s.root, "pool")
|
||||
pool := NewPackagePool(poolPath, false)
|
||||
cs := NewMockChecksumStorage()
|
||||
|
||||
pkgFile := filepath.Join(s.root, "package.deb")
|
||||
err := os.WriteFile(pkgFile, []byte("package content"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
sourceChecksum, err := utils.ChecksumsForFile(pkgFile)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
srcPoolPath, err := pool.Import(pkgFile, "package.deb",
|
||||
&utils.ChecksumInfo{MD5: "d41d8cd98f00b204e9800998ecf8427e"}, false, cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = storage.LinkFromPool("", "pool/main/p/package", "package.deb",
|
||||
pool, srcPoolPath, sourceChecksum, false)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
destPath := filepath.Join(s.root, "pool/main/p/package/package.deb")
|
||||
content, err := os.ReadFile(destPath)
|
||||
c.Assert(err, IsNil)
|
||||
c.Check(string(content), Equals, "package content")
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestPutFileSyncErrorIsReturned(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "", "")
|
||||
|
||||
sourceFile := filepath.Join(s.root, "source-syncfail.txt")
|
||||
err := os.WriteFile(sourceFile, []byte("test content"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
oldSyncFile := syncFile
|
||||
syncFile = func(_ *os.File) error { return syscall.ENOSPC }
|
||||
defer func() { syncFile = oldSyncFile }()
|
||||
|
||||
err = storage.PutFile("dest-syncfail.txt", sourceFile)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(strings.Contains(err.Error(), "error syncing file"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopySyncErrorIsReturned(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "")
|
||||
poolPath := filepath.Join(s.root, "pool")
|
||||
pool := NewPackagePool(poolPath, false)
|
||||
cs := NewMockChecksumStorage()
|
||||
|
||||
pkgFile := filepath.Join(s.root, "package-syncfail.deb")
|
||||
err := os.WriteFile(pkgFile, []byte("package content"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
sourceChecksum, err := utils.ChecksumsForFile(pkgFile)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
srcPoolPath, err := pool.Import(pkgFile, "package-syncfail.deb",
|
||||
&utils.ChecksumInfo{MD5: "d41d8cd98f00b204e9800998ecf8427e"}, false, cs)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
oldSyncFile := syncFile
|
||||
syncFile = func(_ *os.File) error { return syscall.ENOSPC }
|
||||
defer func() { syncFile = oldSyncFile }()
|
||||
|
||||
err = storage.LinkFromPool("", "pool/main/p/package", "package-syncfail.deb",
|
||||
pool, srcPoolPath, sourceChecksum, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(strings.Contains(err.Error(), "error syncing file"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestGetFileLockReusesMutex(c *C) {
|
||||
a := getFileLock(filepath.Join(s.root, "a"))
|
||||
b := getFileLock(filepath.Join(s.root, "a"))
|
||||
c.Check(a == b, Equals, true)
|
||||
|
||||
c1 := getFileLock(filepath.Join(s.root, "c1"))
|
||||
c2 := getFileLock(filepath.Join(s.root, "c2"))
|
||||
c.Check(c1 == c2, Equals, false)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestPutFileFailsIfDestinationDirMissing(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "", "")
|
||||
|
||||
sourceFile := filepath.Join(s.root, "src.txt")
|
||||
err := os.WriteFile(sourceFile, []byte("x"), 0644)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = storage.PutFile("missingdir/dest.txt", sourceFile)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolRejectsNonLocalPoolForHardlink(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "", "")
|
||||
pool := &fakePool{}
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(strings.Contains(err.Error(), "cannot link"), Equals, true)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopyReturnsErrorIfOpenFails(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "")
|
||||
pool := &fakePool{openFn: func(string) (aptly.ReadSeekerCloser, error) { return nil, io.ErrUnexpectedEOF }}
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopyReturnsErrorIfReaderCloseFails(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "")
|
||||
|
||||
pool := &fakePool{openFn: func(string) (aptly.ReadSeekerCloser, error) {
|
||||
return &fakeRSC{Reader: bytes.NewReader([]byte("data")), closeErr: io.ErrClosedPipe}, nil
|
||||
}}
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(err, Equals, io.ErrClosedPipe)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopyReturnsErrorIfSizeFailsWhenDestExists(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "size")
|
||||
pool := &fakePool{sizeErr: io.ErrUnexpectedEOF, openFn: func(string) (aptly.ReadSeekerCloser, error) {
|
||||
return &fakeRSC{Reader: bytes.NewReader([]byte("data")), closeErr: nil}, nil
|
||||
}}
|
||||
|
||||
destDir := filepath.Join(s.root, "pool/main/p/pkg")
|
||||
c.Assert(os.MkdirAll(destDir, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(destDir, "x.deb"), []byte("old"), 0644), IsNil)
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
c.Check(err, Equals, io.ErrUnexpectedEOF)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolCopyChecksumReturnsErrorIfDstMD5Fails(c *C) {
|
||||
storage := NewPublishedStorage(s.root, "copy", "")
|
||||
pool := &fakePool{openFn: func(string) (aptly.ReadSeekerCloser, error) {
|
||||
return &fakeRSC{Reader: bytes.NewReader([]byte("data")), closeErr: nil}, nil
|
||||
}}
|
||||
|
||||
// Make destinationPath a directory so MD5ChecksumForFile fails.
|
||||
destDir := filepath.Join(s.root, "pool/main/p/pkg")
|
||||
c.Assert(os.MkdirAll(destDir, 0777), IsNil)
|
||||
c.Assert(os.MkdirAll(filepath.Join(destDir, "x.deb"), 0777), IsNil)
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *DiskFullNoRootSuite) TestLinkFromPoolHardlinkReturnsErrorIfStatFailsWhenDestExists(c *C) {
|
||||
storage := NewPublishedStorage(c.MkDir(), "hardlink", "")
|
||||
pool := &fakeLocalPool{statErr: errors.New("stat failed")}
|
||||
|
||||
destDir := filepath.Join(storage.rootPath, "pool", "main", "p", "pkg")
|
||||
c.Assert(os.MkdirAll(destDir, 0777), IsNil)
|
||||
c.Assert(os.WriteFile(filepath.Join(destDir, "x.deb"), []byte("x"), 0644), IsNil)
|
||||
|
||||
err := storage.LinkFromPool("", "pool/main/p/pkg", "x.deb", pool, "x", utils.ChecksumInfo{MD5: "x"}, false)
|
||||
c.Assert(err, NotNil)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/aptly-dev/aptly
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/AlekSi/pointer v1.1.0
|
||||
@@ -32,29 +32,27 @@ require (
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
|
||||
github.com/ugorji/go/codec v1.2.11
|
||||
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/term v0.30.0
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/term v0.38.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect
|
||||
@@ -62,16 +60,12 @@ require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/cloudflare/circl v1.4.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
@@ -80,13 +74,12 @@ require (
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -103,30 +96,25 @@ require (
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.26.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/grpc v1.64.1 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1
|
||||
github.com/ProtonMail/go-crypto v1.0.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.5
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0
|
||||
github.com/ProtonMail/go-crypto v1.4.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.5
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.67.1
|
||||
github.com/aws/smithy-go v1.22.1
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3
|
||||
github.com/aws/smithy-go v1.24.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.3
|
||||
go.etcd.io/etcd/client/v3 v3.5.15
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -1,68 +1,66 @@
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI=
|
||||
github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1 h1:cf+OIKbkmMHBaC3u78AXomweqM0oxQSgBXRZf3WH4yM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1/go.mod h1:ap1dmS6vQKJxSMNiGJcq4QuUQkOynyD93gLw6MDF7ek=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
|
||||
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
|
||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55 h1:jbGlDKdzAZ92NzK65hUP98ri0/r50vVVvmZsFP/nIqo=
|
||||
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55/go.mod h1:GCzqZQHydohgVLSIqRKZeTt8IGb1Y4NaFfim3H40uUI=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ=
|
||||
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||
github.com/awalterschulze/gographviz v2.0.1+incompatible h1:XIECBRq9VPEQqkQL5pw2OtjCAdrtIgFKoJU8eT98AS8=
|
||||
github.com/awalterschulze/gographviz v2.0.1+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 h1:JX70yGKLj25+lMC5Yyh8wBtvB01GDilyRuJvXJ4piD0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24/go.mod h1:+Ln60j9SUTD0LEwnhEB0Xhg61DHqplBrbZpLgyjoEHg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 h1:gvZOjQKPxFXy1ft3QnEyXmT+IqneM9QAUWlM3r0mfqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5/go.mod h1:DLWnfvIcm9IET/mmjdxeXbBKmTCm0ZB8p1za9BVteM8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 h1:P1doBzv5VEg1ONxnJss1Kh5ZG/ewoIE4MQtKKc6Crgg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5/go.mod h1:NOP+euMW7W3Ukt28tAxPuoWao4rhhqJD3QEBk7oCg7w=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.67.1 h1:LXLnDfjT/P6SPIaCE86xCOjJROPn4FNB2EdN68vMK5c=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.67.1/go.mod h1:ralv4XawHjEMaHOWnTFushl0WRqim/gQWesAMF6hTow=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg=
|
||||
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
|
||||
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
@@ -78,9 +76,8 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
|
||||
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
@@ -91,28 +88,22 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -127,8 +118,6 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
@@ -146,10 +135,11 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
||||
@@ -163,8 +153,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
|
||||
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
@@ -178,7 +166,6 @@ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZX
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -190,13 +177,11 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
|
||||
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -218,7 +203,6 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/ncw/swift v1.0.53 h1:luHjjTNtekIEvHg5KdAFIBaH7bWfNkefwFnpDffSIks=
|
||||
github.com/ncw/swift v1.0.53/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
@@ -235,8 +219,6 @@ github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -274,7 +256,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
@@ -284,12 +265,6 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
||||
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
@@ -300,13 +275,24 @@ github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlV
|
||||
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
|
||||
go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU=
|
||||
go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
|
||||
go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
@@ -319,56 +305,46 @@ golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -377,30 +353,21 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -408,21 +375,19 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -431,11 +396,9 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
|
||||
@@ -449,7 +412,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
+2
-1
@@ -44,6 +44,7 @@ func NewDownloader(downLimit int64, maxTries int, progress aptly.Progress) aptly
|
||||
transport.DisableCompression = true
|
||||
initTransport(&transport)
|
||||
transport.RegisterProtocol("ftp", &protocol.FTPRoundTripper{})
|
||||
transport.RegisterProtocol("ar+https", NewGCPRoundTripper(&transport))
|
||||
|
||||
downloader := &downloaderImpl{
|
||||
progress: progress,
|
||||
@@ -240,7 +241,7 @@ func (downloader *downloaderImpl) download(req *http.Request, url, destination s
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !go1.7
|
||||
// +build !go1.7
|
||||
|
||||
package http
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
// gcpRoundTripper wraps http.RoundTripper to add Google Cloud authentication.
|
||||
// It delays GCP authentication initialization until the first actual request is made.
|
||||
// This avoids unnecessary credential loading when ar+https protocol is not actually used.
|
||||
//
|
||||
// It uses Application Default Credentials (ADC) which checks:
|
||||
// 1. GOOGLE_APPLICATION_CREDENTIALS environment variable
|
||||
// 2. gcloud auth application-default credentials
|
||||
// 3. GCE/GKE metadata server
|
||||
// See https://cloud.google.com/docs/authentication/application-default-credentials for usage details.
|
||||
type gcpRoundTripper struct {
|
||||
base http.RoundTripper
|
||||
initOnce sync.Once
|
||||
tokenSrc oauth2.TokenSource
|
||||
initErr error
|
||||
}
|
||||
|
||||
func (t *gcpRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Lazy initialization: only initialize GCP credentials on first request
|
||||
t.initOnce.Do(func() {
|
||||
creds, err := google.FindDefaultCredentials(context.Background(),
|
||||
"https://www.googleapis.com/auth/cloud-platform")
|
||||
if err != nil {
|
||||
t.initErr = fmt.Errorf("failed to find default credentials: %w", err)
|
||||
return
|
||||
}
|
||||
t.tokenSrc = creds.TokenSource
|
||||
})
|
||||
|
||||
reqCopy := req.Clone(req.Context())
|
||||
reqCopy.URL.Scheme = strings.TrimPrefix(reqCopy.URL.Scheme, "ar+")
|
||||
|
||||
// Fall back to base transport if GCP auth initialization failed
|
||||
if t.initErr != nil {
|
||||
return t.base.RoundTrip(reqCopy)
|
||||
}
|
||||
|
||||
token, err := t.tokenSrc.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get OAuth2 token: %w", err)
|
||||
}
|
||||
token.SetAuthHeader(reqCopy)
|
||||
|
||||
return t.base.RoundTrip(reqCopy)
|
||||
}
|
||||
|
||||
// NewGCPRoundTripper creates a new RoundTripper that handles GCP authentication for ar+https protocol.
|
||||
func NewGCPRoundTripper(base http.RoundTripper) http.RoundTripper {
|
||||
return &gcpRoundTripper{
|
||||
base: base,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func TestGCPAuthTransport_RoundTrip(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
t.Error("Expected Authorization header, got none")
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
transport := NewGCPRoundTripper(http.DefaultTransport)
|
||||
|
||||
if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") == "" {
|
||||
t.Skip("Skipping test: GOOGLE_APPLICATION_CREDENTIALS not set")
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: transport}
|
||||
resp, err := client.Get(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGCPAuthTransport_RoundTrip_with_dummy_tokenSource(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth != "Bearer dummy-token" {
|
||||
t.Errorf("Expected Authorization header 'Bearer dummy-token', got '%s'", auth)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Use a dummy token source for testing
|
||||
transport := &gcpRoundTripper{
|
||||
base: http.DefaultTransport,
|
||||
tokenSrc: oauth2.StaticTokenSource(&oauth2.Token{
|
||||
AccessToken: "dummy-token",
|
||||
}),
|
||||
}
|
||||
transport.initOnce.Do(func() {}) // Mark as initialized for testing
|
||||
|
||||
client := &http.Client{Transport: transport}
|
||||
resp, err := client.Get(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
defer func(){ _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGCPAuthTransport_RoundTrip_with_InvalidCredentials(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Create a temporary invalid credentials file
|
||||
tmpFile, err := os.CreateTemp("", "invalid_credentials.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %s", err)
|
||||
}
|
||||
defer func() { _ = os.Remove(tmpFile.Name()) }()
|
||||
if _, err := tmpFile.WriteString(`{"invalid": "data"}`); err != nil {
|
||||
t.Fatalf("Failed to write to temp file: %s", err)
|
||||
}
|
||||
_ = tmpFile.Close()
|
||||
|
||||
defaultEnv := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
|
||||
_ = os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", tmpFile.Name())
|
||||
defer func() { _ = os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", defaultEnv) }()
|
||||
|
||||
transport := &gcpRoundTripper{
|
||||
base: http.DefaultTransport,
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: transport}
|
||||
resp, err := client.Get(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %s", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Errorf("Expected status 403, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if transport.initErr == nil {
|
||||
t.Error("Expected init error due to invalid credentials, got none")
|
||||
}
|
||||
}
|
||||
+11
-11
@@ -49,9 +49,9 @@ func (d *GrabDownloader) Download(ctx context.Context, url string, destination s
|
||||
|
||||
func (d *GrabDownloader) DownloadWithChecksum(ctx context.Context, url string, destination string, expected *utils.ChecksumInfo, ignoreMismatch bool) error {
|
||||
maxTries := d.maxTries
|
||||
// FIXME: const delayMax = time.Duration(5 * time.Minute)
|
||||
// FIXME: const delayMax = time.Duration(5 * time.Minute)
|
||||
delay := time.Duration(1 * time.Second)
|
||||
// FIXME: const delayMultiplier = 2
|
||||
// FIXME: const delayMultiplier = 2
|
||||
err := fmt.Errorf("no tries available")
|
||||
for maxTries > 0 {
|
||||
err = d.download(ctx, url, destination, expected, ignoreMismatch)
|
||||
@@ -133,17 +133,17 @@ func (d *GrabDownloader) download(_ context.Context, url string, destination str
|
||||
|
||||
resp := d.client.Do(req)
|
||||
|
||||
<-resp.Done
|
||||
<-resp.Done
|
||||
// download is complete
|
||||
|
||||
// Loop:
|
||||
// for {
|
||||
// select {
|
||||
// case <-resp.Done:
|
||||
// // download is complete
|
||||
// break Loop
|
||||
// }
|
||||
// }
|
||||
// Loop:
|
||||
// for {
|
||||
// select {
|
||||
// case <-resp.Done:
|
||||
// // download is complete
|
||||
// break Loop
|
||||
// }
|
||||
// }
|
||||
err = resp.Err()
|
||||
if err != nil && err == grab.ErrBadChecksum && ignoreMismatch {
|
||||
fmt.Printf("Ignoring checksum mismatch for %s\n", url)
|
||||
|
||||
+45
-1
@@ -111,8 +111,9 @@ The legacy json configuration is still supported (and also supports comments):
|
||||
// Enable metrics for Prometheus client
|
||||
"enableMetricsEndpoint": false,
|
||||
|
||||
// Not implemented in this version\.
|
||||
// Enable API documentation on /docs
|
||||
"enableSwaggerEndpoint": false,
|
||||
//"enableSwaggerEndpoint": false,
|
||||
|
||||
// OBSOLETE: use via url param ?_async=true
|
||||
"AsyncAPI": false,
|
||||
@@ -615,6 +616,10 @@ gpg keyring to use when verifying Release file (could be specified multiple time
|
||||
max download tries till process fails with download error
|
||||
.
|
||||
.TP
|
||||
\-\fBwith\-appstream\fR
|
||||
download AppStream (DEP\-11) metadata
|
||||
.
|
||||
.TP
|
||||
\-\fBwith\-installer\fR
|
||||
download additional not packaged installer files
|
||||
.
|
||||
@@ -738,6 +743,10 @@ max download tries till process fails with download error
|
||||
\-\fBskip\-existing\-packages\fR
|
||||
do not check file existence for packages listed in the internal database of the mirror
|
||||
.
|
||||
.TP
|
||||
\-\fBlatest\fR
|
||||
download only latest version of each package (per architecture)
|
||||
.
|
||||
.SH "RENAMES MIRROR"
|
||||
\fBaptly\fR \fBmirror\fR \fBrename\fR \fIold\-name\fR \fInew\-name\fR
|
||||
.
|
||||
@@ -786,6 +795,10 @@ disable verification of Release file signatures
|
||||
gpg keyring to use when verifying Release file (could be specified multiple times)
|
||||
.
|
||||
.TP
|
||||
\-\fBwith\-appstream\fR
|
||||
download AppStream (DEP\-11) metadata
|
||||
.
|
||||
.TP
|
||||
\-\fBwith\-installer\fR
|
||||
download additional not packaged installer files
|
||||
.
|
||||
@@ -1561,6 +1574,14 @@ $ aptly publish repo testing
|
||||
Options:
|
||||
.
|
||||
.TP
|
||||
\-\fBsigned\-by\fR
|
||||
set value for Signed-By field
|
||||
.
|
||||
.TP
|
||||
\-\fBversion\fR
|
||||
version of the release
|
||||
.
|
||||
.TP
|
||||
\-\fBacquire\-by\-hash\fR
|
||||
provide index files by hash
|
||||
.
|
||||
@@ -1702,6 +1723,14 @@ $ aptly publish snapshot wheezy\-main
|
||||
Options:
|
||||
.
|
||||
.TP
|
||||
\-\fBsigned\-by\fR
|
||||
set value for Signed-By field
|
||||
.
|
||||
.TP
|
||||
\-\fBversion\fR
|
||||
version of the release
|
||||
.
|
||||
.TP
|
||||
\-\fBacquire\-by\-hash\fR
|
||||
provide index files by hash
|
||||
.
|
||||
@@ -2061,6 +2090,10 @@ This command would switch published repository (with one component) named ppa/wh
|
||||
Options:
|
||||
.
|
||||
.TP
|
||||
\-\fBsigned\-by\fR
|
||||
set value for Signed-By field
|
||||
.
|
||||
.TP
|
||||
\-\fBbatch\fR
|
||||
run GPG with detached tty
|
||||
.
|
||||
@@ -2167,6 +2200,14 @@ $ aptly publish update wheezy ppa
|
||||
Options:
|
||||
.
|
||||
.TP
|
||||
\-\fBsigned\-by\fR
|
||||
set value for Signed-By field
|
||||
.
|
||||
.TP
|
||||
\-\fBversion\fR
|
||||
version of the release
|
||||
.
|
||||
.TP
|
||||
\-\fBbatch\fR
|
||||
run GPG with detached tty
|
||||
.
|
||||
@@ -2452,6 +2493,9 @@ show yaml config
|
||||
.SH "ENVIRONMENT"
|
||||
If environment variable \fBHTTP_PROXY\fR is set \fBaptly\fR would use its value to proxy all HTTP requests\.
|
||||
.
|
||||
.P
|
||||
If environment variable \fBSOURCE_DATE_EPOCH\fR is set to a Unix timestamp, \fBaptly\fR would use that timestamp for the \fBDate\fR and \fBValid\-Until\fR fields in the \fBRelease\fR file when publishing\. This enables reproducible builds as specified by \fIhttps://reproducible\-builds\.org/specs/source\-date\-epoch/\fR\.
|
||||
.
|
||||
.SH "RETURN VALUES"
|
||||
\fBaptly\fR exists with:
|
||||
.
|
||||
|
||||
@@ -100,8 +100,9 @@ The legacy json configuration is still supported (and also supports comments):
|
||||
// Enable metrics for Prometheus client
|
||||
"enableMetricsEndpoint": false,
|
||||
|
||||
// Not implemented in this version.
|
||||
// Enable API documentation on /docs
|
||||
"enableSwaggerEndpoint": false,
|
||||
//"enableSwaggerEndpoint": false,
|
||||
|
||||
// OBSOLETE: use via url param ?_async=true
|
||||
"AsyncAPI": false,
|
||||
@@ -533,6 +534,11 @@ For example, default aptly display format could be presented with the following
|
||||
If environment variable `HTTP_PROXY` is set `aptly` would use its value
|
||||
to proxy all HTTP requests.
|
||||
|
||||
If environment variable `SOURCE_DATE_EPOCH` is set to a Unix timestamp,
|
||||
`aptly` would use that timestamp for the `Date` and `Valid-Until` fields
|
||||
in the `Release` file when publishing. This enables reproducible builds
|
||||
as specified by https://reproducible-builds.org/specs/source-date-epoch/.
|
||||
|
||||
## RETURN VALUES
|
||||
|
||||
`aptly` exists with:
|
||||
|
||||
+18
-4
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ var (
|
||||
type GpgSigner struct {
|
||||
gpg string
|
||||
version GPGVersion
|
||||
keyRef string
|
||||
keyRefs []string
|
||||
keyring, secretKeyring string
|
||||
passphrase, passphraseFile string
|
||||
batch bool
|
||||
@@ -35,7 +36,14 @@ func (g *GpgSigner) SetBatch(batch bool) {
|
||||
|
||||
// SetKey sets key ID to use when signing files
|
||||
func (g *GpgSigner) SetKey(keyRef string) {
|
||||
g.keyRef = keyRef
|
||||
keyRef = strings.TrimSpace(keyRef)
|
||||
if keyRef != "" {
|
||||
if g.keyRefs == nil {
|
||||
g.keyRefs = []string{keyRef}
|
||||
} else {
|
||||
g.keyRefs = append(g.keyRefs, keyRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetKeyRing allows to set custom keyring and secretkeyring
|
||||
@@ -57,8 +65,8 @@ func (g *GpgSigner) gpgArgs() []string {
|
||||
args = append(args, "--secret-keyring", g.secretKeyring)
|
||||
}
|
||||
|
||||
if g.keyRef != "" {
|
||||
args = append(args, "-u", g.keyRef)
|
||||
for _, k := range g.keyRefs {
|
||||
args = append(args, "-u", k)
|
||||
}
|
||||
|
||||
if g.passphrase != "" || g.passphraseFile != "" {
|
||||
@@ -82,6 +90,12 @@ func (g *GpgSigner) gpgArgs() []string {
|
||||
}
|
||||
}
|
||||
|
||||
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
|
||||
if _, err := strconv.ParseInt(epoch, 10, 64); err == nil {
|
||||
args = append(args, "--faked-system-time", epoch)
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -74,6 +75,14 @@ func (g *GoSigner) Init() error {
|
||||
},
|
||||
}
|
||||
|
||||
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
|
||||
if sec, err := strconv.ParseInt(epoch, 10, 64); err == nil {
|
||||
t := time.Unix(sec, 0).UTC()
|
||||
g.signerConfig.Time = func() time.Time { return t }
|
||||
g.signerConfig.NonDeterministicSignaturesViaNotation = packet.BoolPointer(false)
|
||||
}
|
||||
}
|
||||
|
||||
if g.passphraseFile != "" {
|
||||
passF, err := os.Open(g.passphraseFile)
|
||||
if err != nil {
|
||||
|
||||
+1
-1
@@ -346,7 +346,7 @@ func (storage *PublishedStorage) LinkFromPool(publishedPrefix, publishedRelPath,
|
||||
storage.pathCache = make(map[string]string, len(paths))
|
||||
|
||||
for i := range paths {
|
||||
storage.pathCache[filepath.Join("pool", paths[i])] = md5s[i]
|
||||
storage.pathCache[filepath.Join(publishedPrefix, "pool", paths[i])] = md5s[i]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-6
@@ -112,9 +112,11 @@ func NewServer(config *Config) (*Server, error) {
|
||||
buckets: make(map[string]*bucket),
|
||||
config: config,
|
||||
}
|
||||
go func() { _ = http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
srv.serveHTTP(w, req)
|
||||
})) }()
|
||||
go func() {
|
||||
_ = http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
srv.serveHTTP(w, req)
|
||||
}))
|
||||
}()
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
@@ -527,14 +529,13 @@ func (bucketResource) post(a *action) interface{} {
|
||||
// and dashes (-). You can use uppercase letters for buckets only in the
|
||||
// US Standard region.
|
||||
//
|
||||
// Must start with a number or letter
|
||||
// # Must start with a number or letter
|
||||
//
|
||||
// Must be between 3 and 255 characters long
|
||||
// # Must be between 3 and 255 characters long
|
||||
//
|
||||
// There's one extra rule (Must not be formatted as an IP address (e.g., 192.168.5.4)
|
||||
// but the real S3 server does not seem to check that rule, so we will not
|
||||
// check it either.
|
||||
//
|
||||
func validBucketName(name string) bool {
|
||||
if len(name) < 3 || len(name) > 255 {
|
||||
return false
|
||||
|
||||
@@ -34,6 +34,7 @@ class APITest(BaseTest):
|
||||
"linkMethod": "symlink"
|
||||
}
|
||||
},
|
||||
"enableMetricsEndpoint": True,
|
||||
"enableSwaggerEndpoint": True
|
||||
}
|
||||
|
||||
|
||||
@@ -15,4 +15,4 @@ else
|
||||
fi
|
||||
|
||||
cd /work/src
|
||||
sudo -u aptly PATH=$PATH:/work/src/build GOPATH=/work/src/.go $cmd
|
||||
sudo -u aptly PATH=$PATH:/work/src/build GOPATH=/work/src/.go GOCACHE=/work/src/.go/cache $cmd
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQGiBFL7pY8RBAC5uHg/9AuGJ7EF7RYty89IDLeqvlPe710eDQpJ+itsOaA/5rr3
|
||||
IV1LMlqHpM2rkZkAPpARwjrga2ByJ1ww77Zq2uPqJIO2LZYWTLXic9Zity2OVu3Z
|
||||
XwtdsqagIMfT5dAgNmhe5lL7qgGUwYcFFa52s7U4qO0z2FfwHW1IQrnMpwCg5RQh
|
||||
Uqs5iUKdDtoeQjX5mWgQhjEEAI1zfXUvvcOrRsDlGNKYZigZiWC6J46jeR8Nnf9C
|
||||
WwhXS2fzQaJyDq9DorkvPZgWUAaLLCdfGETqLzDKajynhS1+OnfFQNzvkvEPRBSb
|
||||
C5k+GOF2E1E9rGXb31+1XZTcdIprp4/F3RNLLWNUwfgPLWJx9NzHTYqgBStecHkC
|
||||
ySZRA/9PNFAbeJZ27HNuzoGnAa0piZDLeAAHsM1V6cosMh7U1IZqjZcrMC9YXNxH
|
||||
2D90PvoBvpufCMRzL/fOVPT1JzQGYoKIX17Nmzvdq/a4YyLWRODjvWXd94bae2Xd
|
||||
Vy03DYhfp8VOVJW6HuAX9JN6MKXSNxaibgOPjU822Hxd1iCIQ7QtQXB0bHkgVGVz
|
||||
dGVyIChkb24ndCB1c2UgaXQpIDx0ZXN0QGFwdGx5LmluZm8+iGIEExECACIFAlL7
|
||||
pY8CGyMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJECHbuJwW2z5t2sQAoNn+
|
||||
0cADZa66HZNY2qJi44Oq4hjaAJsHzj9JKAHEpdix5N7b6QvaZQZYhrkBDQRS+6WP
|
||||
EAQA9BX+kbIM6VJYoyY9vUHXfAF4E2y2M7vl9knZ+jMPfMbI7dE3gRJQb3mngST5
|
||||
7eZWawo1DNE6h3LbHsB4mpro9XLUXUMBgXRsOq4D5E0ygvDZ/tJhy0AwFiTOXKEs
|
||||
/erzmbF7j/TWh4LVHXFI9DrnN0+EeF/mQC/wzX7WGCKe70cAAwUEAMr7959zUYNp
|
||||
E3v4IquIJpD22bT/FiyQjFG8yGy36c+7mOP3VWi0lz5yFqqeR9NDFuLDSwOEi0nB
|
||||
zXNmimLy+hIwMaHjbQLjLODmy/T9wKCgeAmK1ygT6YBGJJflThZ05M80T5hBtRA9
|
||||
z2eoTn0wbi6MLmD/rbEt+lUPfSA4V0t2iEkEGBECAAkFAlL7pY8CGwwACgkQIdu4
|
||||
nBbbPm05hgCgvYatZXRbEdZ91jJCQi1KI7lJ5Y8AnjvrHU0g84mE45QZFegZzzQo
|
||||
9relmDMEZ3YCRhYJKwYBBAHaRw8BAQdAYDU0VSBcurX+uqAeR/w/XOLSZcghvOqz
|
||||
Y8yWdcj3HUy0L0FwdGx5IFNlY29uZGFyeSBTaWduaW5nIEtleSA8YXB0bHlAZXhh
|
||||
bXBsZS5jb20+iJYEExYKAD4WIQSu4W3wGDVPZ/5fXHK79OGUNOkeTgUCZ3YCRgIb
|
||||
AwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRC79OGUNOkeTid/AP9A
|
||||
kIMn2qI5TqZgzrnPt7SN16VvpMppPb2H0m0P6knQKQD8DHcLcrqAl2cjcEuntv75
|
||||
gOnEvmPDAO6S1rc8UgcWdQQ=
|
||||
=XPoo
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
lFgEZ3YCRhYJKwYBBAHaRw8BAQdAYDU0VSBcurX+uqAeR/w/XOLSZcghvOqzY8yW
|
||||
dcj3HUwAAP9lsZgE1YQfaS9xfVOSi3f91lbq13+U9FPdwxfiET0+bBFrtC9BcHRs
|
||||
eSBTZWNvbmRhcnkgU2lnbmluZyBLZXkgPGFwdGx5QGV4YW1wbGUuY29tPoiWBBMW
|
||||
CgA+FiEEruFt8Bg1T2f+X1xyu/ThlDTpHk4FAmd2AkYCGwMFCQPCZwAFCwkIBwIG
|
||||
FQoJCAsCBBYCAwECHgECF4AACgkQu/ThlDTpHk4nfwD/QJCDJ9qiOU6mYM65z7e0
|
||||
jdelb6TKaT29h9JtD+pJ0CkA/Ax3C3K6gJdnI3BLp7b++YDpxL5jwwDukta3PFIH
|
||||
FnUE
|
||||
=IXTY
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
@@ -20,7 +20,7 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("Error opening DB %q: %s", dbPath, err)
|
||||
}
|
||||
defer db.Close()
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
keys := db.KeysByPrefix([]byte(prefix))
|
||||
if len(keys) == 0 {
|
||||
|
||||
+8
-3
@@ -272,6 +272,9 @@ class BaseTest(object):
|
||||
self.run_cmd([
|
||||
self.gpgFinder.gpg2, "--import",
|
||||
os.path.join(os.path.dirname(inspect.getsourcefile(BaseTest)), "files") + "/aptly.sec"], expected_code=None)
|
||||
self.run_cmd([
|
||||
self.gpgFinder.gpg2, "--import",
|
||||
os.path.join(os.path.dirname(inspect.getsourcefile(BaseTest)), "files") + "/aptly3.sec"], expected_code=None)
|
||||
|
||||
if self.fixtureGpg:
|
||||
self.run_cmd([self.gpgFinder.gpg, "--no-default-keyring", "--trust-model", "always", "--batch", "--keyring", "aptlytest.gpg", "--import"] +
|
||||
@@ -310,7 +313,9 @@ class BaseTest(object):
|
||||
|
||||
if command[0] == "aptly":
|
||||
aptly_testing_bin = Path(__file__).parent / ".." / "aptly.test"
|
||||
command = [str(aptly_testing_bin), f"-test.coverprofile={Path(self.coverage_dir) / self.__class__.__name__}-{uuid4()}.out", *command[1:]]
|
||||
command = [str(aptly_testing_bin), *command[1:]]
|
||||
if self.coverage_dir is not None:
|
||||
command.insert(1, f"-test.coverprofile={Path(self.coverage_dir) / self.__class__.__name__}-{uuid4()}.out")
|
||||
|
||||
if self.faketime:
|
||||
command = ["faketime", os.environ.get("TEST_FAKETIME", "2025-01-02 03:04:05")] + command
|
||||
@@ -337,7 +342,7 @@ class BaseTest(object):
|
||||
if is_aptly_command:
|
||||
# remove the last two rows as go tests always print PASS/FAIL and coverage in those
|
||||
# two lines. This would otherwise fail the tests as they would not match gold
|
||||
matches = re.findall(r"((.|\n)*)EXIT: (\d)\n.*\ncoverage: .*", raw_output)
|
||||
matches = re.findall(r"((.|\n)*)EXIT: (\d)\n.*(?:\ncoverage: .*|$)", raw_output)
|
||||
if not matches:
|
||||
raise Exception("no matches found in command output '%s'" % raw_output)
|
||||
|
||||
@@ -517,7 +522,7 @@ class BaseTest(object):
|
||||
if gold != output:
|
||||
diff = "".join(difflib.unified_diff(
|
||||
[l + "\n" for l in gold.split("\n")], [l + "\n" for l in output.split("\n")]))
|
||||
raise Exception("content doesn't match:\n" + diff + "\n\nOutput:\n" + orig + "\n")
|
||||
raise Exception(f"content doesn't match:\n{diff}\n\nOutput:\n{orig}\n")
|
||||
|
||||
check = check_output
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
azure-storage-blob
|
||||
boto
|
||||
requests==2.28.2
|
||||
requests==2.33.0
|
||||
requests-unixsocket
|
||||
python-swiftclient
|
||||
flake8
|
||||
|
||||
+6
-3
@@ -36,7 +36,7 @@ def natural_key(string_):
|
||||
return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)]
|
||||
|
||||
|
||||
def run(include_long_tests=False, capture_results=False, tests=None, filters=None, coverage_dir=None):
|
||||
def run(include_long_tests=False, capture_results=False, tests=None, filters=None, coverage_dir=None, coverage_skip=False):
|
||||
"""
|
||||
Run system test.
|
||||
"""
|
||||
@@ -47,7 +47,7 @@ def run(include_long_tests=False, capture_results=False, tests=None, filters=Non
|
||||
fails = []
|
||||
numTests = numFailed = numSkipped = 0
|
||||
lastBase = None
|
||||
if not coverage_dir:
|
||||
if not coverage_dir and not coverage_skip:
|
||||
coverage_dir = mkdtemp(suffix="aptly-coverage")
|
||||
|
||||
failed = False
|
||||
@@ -213,6 +213,7 @@ if __name__ == "__main__":
|
||||
include_long_tests = False
|
||||
capture_results = False
|
||||
coverage_dir = None
|
||||
coverage_skip = False
|
||||
tests = None
|
||||
args = sys.argv[1:]
|
||||
|
||||
@@ -224,6 +225,8 @@ if __name__ == "__main__":
|
||||
elif args[0] == "--coverage-dir":
|
||||
coverage_dir = args[1]
|
||||
args = args[1:]
|
||||
elif args[0] == "--coverage-skip":
|
||||
coverage_skip = True
|
||||
|
||||
args = args[1:]
|
||||
|
||||
@@ -236,4 +239,4 @@ if __name__ == "__main__":
|
||||
else:
|
||||
filters.append(arg)
|
||||
|
||||
run(include_long_tests, capture_results, tests, filters, coverage_dir)
|
||||
run(include_long_tests, capture_results, tests, filters, coverage_dir, coverage_skip)
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"gpgProvider": "gpg",
|
||||
"gpgDisableSign": false,
|
||||
"gpgDisableVerify": false,
|
||||
"gpgKeys": [],
|
||||
"skipContentsPublishing": false,
|
||||
"skipBz2Publishing": false,
|
||||
"FileSystemPublishEndpoints": {},
|
||||
|
||||
@@ -27,6 +27,7 @@ download_sourcepackages: false
|
||||
gpg_provider: gpg
|
||||
gpg_disable_sign: false
|
||||
gpg_disable_verify: false
|
||||
gpg_keys: []
|
||||
skip_contents_publishing: false
|
||||
skip_bz2_publishing: false
|
||||
filesystem_publish_endpoints: {}
|
||||
|
||||
@@ -29,6 +29,7 @@ Options:
|
||||
-ignore-signatures: disable verification of Release file signatures
|
||||
-keyring=: gpg keyring to use when verifying Release file (could be specified multiple times)
|
||||
-max-tries=1: max download tries till process fails with download error
|
||||
-with-appstream: download AppStream (DEP-11) metadata
|
||||
-with-installer: download additional not packaged installer files
|
||||
-with-sources: download source packages in addition to binary packages
|
||||
-with-udebs: download .udeb packages (Debian installer support)
|
||||
|
||||
@@ -20,6 +20,7 @@ Options:
|
||||
-ignore-signatures: disable verification of Release file signatures
|
||||
-keyring=: gpg keyring to use when verifying Release file (could be specified multiple times)
|
||||
-max-tries=1: max download tries till process fails with download error
|
||||
-with-appstream: download AppStream (DEP-11) metadata
|
||||
-with-installer: download additional not packaged installer files
|
||||
-with-sources: download source packages in addition to binary packages
|
||||
-with-udebs: download .udeb packages (Debian installer support)
|
||||
|
||||
@@ -21,6 +21,7 @@ Options:
|
||||
-ignore-signatures: disable verification of Release file signatures
|
||||
-keyring=: gpg keyring to use when verifying Release file (could be specified multiple times)
|
||||
-max-tries=1: max download tries till process fails with download error
|
||||
-with-appstream: download AppStream (DEP-11) metadata
|
||||
-with-installer: download additional not packaged installer files
|
||||
-with-sources: download source packages in addition to binary packages
|
||||
-with-udebs: download .udeb packages (Debian installer support)
|
||||
|
||||
@@ -5,6 +5,7 @@ Components: main, contrib, non-free
|
||||
Architectures: amd64, arm64, armel, armhf, i386, mips, mips64el, mipsel, ppc64el, s390x
|
||||
Download Sources: no
|
||||
Download .udebs: no
|
||||
Download AppStream: no
|
||||
Last update: never
|
||||
|
||||
Information from release file:
|
||||
|
||||
@@ -5,6 +5,7 @@ Components: main, contrib, non-free
|
||||
Architectures: amd64, arm64, armel, armhf, i386, mips, mips64el, mipsel, ppc64el, s390x
|
||||
Download Sources: no
|
||||
Download .udebs: no
|
||||
Download AppStream: no
|
||||
Last update: never
|
||||
|
||||
Information from release file:
|
||||
|
||||
@@ -5,6 +5,7 @@ Components:
|
||||
Architectures: all, amd64, i386
|
||||
Download Sources: no
|
||||
Download .udebs: no
|
||||
Download AppStream: no
|
||||
Last update: never
|
||||
|
||||
Information from release file:
|
||||
|
||||
@@ -5,6 +5,7 @@ Components: main, contrib, non-free
|
||||
Architectures: i386
|
||||
Download Sources: yes
|
||||
Download .udebs: no
|
||||
Download AppStream: no
|
||||
Last update: never
|
||||
|
||||
Information from release file:
|
||||
|
||||
@@ -5,6 +5,7 @@ Components: main
|
||||
Architectures: amd64, armel, i386, powerpc
|
||||
Download Sources: no
|
||||
Download .udebs: no
|
||||
Download AppStream: no
|
||||
Last update: never
|
||||
|
||||
Information from release file:
|
||||
|
||||
@@ -5,6 +5,7 @@ Components: main
|
||||
Architectures: i386
|
||||
Download Sources: yes
|
||||
Download .udebs: no
|
||||
Download AppStream: no
|
||||
Last update: never
|
||||
|
||||
Information from release file:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user