Compare commits

..
Author SHA1 Message Date
dependabot[bot] a6c57c83b3 build(deps): bump go.opentelemetry.io/otel/sdk from 1.45.0 to 1.47.0
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.45.0 to 1.47.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.45.0...v1.47.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.47.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-14 03:23:25 +00:00
André Roth e35afce6c6 Merge pull request #1638 from hjmitsd/architecture-variants
Add support for architecture variants
2026-11-06 16:37:31 +01:00
Hansjoerg Maurer b6cb4de05d Add system test for architecture variant mirrors 2026-11-05 23:10:30 +01:00
Hansjoerg Maurer 972cf0b981 Adapt command tests to gocheck 2026-11-05 21:16:51 +01:00
Hansjoerg Maurer 8fb07c7e9c Fix lint issues in architecture variant tests 2026-11-05 21:16:51 +01:00
Hansjoerg Maurer 673b4f02d8 Use neutral fixtures in architecture variant tests 2026-11-05 21:16:51 +01:00
Hansjoerg Maurer 0bcfabcead Fix filtered mirror dependencies for architecture variants 2026-11-05 21:16:51 +01:00
Hansjoerg Maurer 2f228199c5 Fix latest package filtering by architecture 2026-11-05 21:16:51 +01:00
Hansjoerg Maurer 217fcc3d2e Fix force replace for architecture variants 2026-11-05 21:00:37 +01:00
Hansjoerg Maurer d5812148a8 Fix snapshot pull for architecture variants 2026-11-05 21:00:37 +01:00
Hansjoerg Maurer dab494ecf3 Fix API snapshot pull destination loading 2026-11-05 21:00:37 +01:00
Hansjoerg Maurer 2452dadac7 Fix architecture variant package queries 2026-11-05 21:00:37 +01:00
Hansjoerg Maurer b5096eaea9 Display architecture variants in package output 2026-11-05 21:00:37 +01:00
Hansjoerg Maurer 6efb8e17fd Publish architecture variants in separate indexes 2026-11-05 21:00:37 +01:00
Hansjoerg Maurer 915fd24d7b Test mirror support for architecture variants 2026-11-05 21:00:37 +01:00
Hansjoerg Maurer bcb34b7140 Test architecture variant package persistence 2026-11-05 21:00:37 +01:00
Hansjoerg Maurer 38290613f3 Handle architecture variants in package reference lists 2026-11-05 21:00:37 +01:00
Hansjoerg Maurer 248296ddbf Add package identity support for architecture variants 2026-11-05 21:00:37 +01:00
André Roth f72347a0a8 Merge pull request #1650 from Rascalov/mirror-update-max-versions
Add `-max-versions` to get the N most recent versions of a package.
2026-11-05 13:29:10 +01:00
André Roth 151f67a294 fix merge conflicts 2026-11-05 12:51:13 +01:00
Rascalov 300837fe06 tests: cover mirror update -max-versions in system and unit tests 2026-11-05 11:22:28 +01:00
Rascalov 0609ac257a mirror update: clarify -max-versions output 2026-11-05 11:22:28 +01:00
Rascalov 78cd0c71ca docs: document mirror update -max-versions 2026-11-05 11:22:28 +01:00
Rascalov d7e58623a9 deb: add tests for keeping the N most recent versions 2026-11-05 11:22:28 +01:00
Rascalov 5bc1047ba8 mirror update: add -max-versions to keep the N most recent versions 2026-11-05 11:21:36 +01:00
André Roth 296e901cb6 Merge pull request #1633 from barnumbirr/concurrent-index-renames
deb: rename published indexes concurrently
2026-11-04 13:38:01 +01:00
Martin Simon b80f151b6d deb: make the index rename concurrency configurable
publishRenameConcurrency (publish_rename_concurrency in YAML) sets how
many index files RenameFiles renames at once. It defaults to 4, the
value that was hard-coded, and 1 renames them one at a time. Values
below 1 are treated as 1, since a semaphore with no capacity would
never admit a rename.

The setting is global rather than per publishing endpoint because the
renames run against every storage backend, not only S3.
2026-11-04 12:24:18 +01:00
Martin Simon c531cac535 deb: rename published indexes concurrently
RenameFiles walks renameMap one entry at a time. Every rename is a round trip,
and on an S3 backend RenameFile is a CopyObject followed by a DeleteObject, so
a publish that rewrites a dozen indexes spends most of its wall clock waiting
on them in sequence.

Bounded at four in flight, matching the default used for concurrent package
uploads. The first error is returned rather than the last, and the remaining
renames still run, so a failure does not leave part of the set staged behind a
half-finished switch.

Measured against MinIO behind a proxy adding 50ms each way, publishing a
repository whose Contents is dominated by one package carrying 19,500 files:
publish update drops from 2703ms to 1624ms median, n=5 either side, a 40% cut.
The published object set and its sizes are identical before and after.

RenameFiles had no test coverage. It has four cases now: every entry renamed,
the concurrency limit respected, a failure reported without abandoning the
rest, and an empty rename map.
2026-11-04 12:24:18 +01:00
André Roth 6f95fdca32 Merge pull request #1660 from barnumbirr/complete-default-config
conf: document the config keys missing from aptly.conf and the man page
2026-11-04 12:12:19 +01:00
André Roth 5eb3a8dc93 Merge pull request #1661 from barnumbirr/fix-misspelled-config-keys
conf: fix two misspelled keys in the default aptly.conf
2026-11-04 12:10:55 +01:00
Martin Simon 5b9a1a73fa conf: document gpg_keys, and the three keys the man page lacks
gpg_keys was missing from debian/aptly.conf, and the man page had no
entry for ppaBaseURL, enablePprofEndpoint or gpgKeys. The wording
follows the existing template comments where there is one.
2026-11-03 14:01:36 +01:00
Martin Simon 8c655504d2 conf: fix two misspelled keys in the default aptly.conf
debian/aptly.conf spelled dep_follow_all_variants as
dep_follow_allvariants and dep_verboseresolve as dep_verbose_resolve.
The YAML loader ignores unknown keys, so setting either to true in a
config written from this template had no effect.
2026-11-03 13:57:08 +01:00
Martin Simon 925996b8da conf: document acquire_by_hash_algorithms in the default aptly.conf
The key added in #1626 was missing from debian/aptly.conf, which aptly
writes out as ~/.aptly.conf on first run, so new installs never saw it.
2026-11-03 13:53:52 +01:00
André Roth 519602b3e8 Merge pull request #1626 from barnumbirr/configurable-by-hash-algorithms
Make the set of Acquire-By-Hash algorithms configurable
2026-11-03 13:44:00 +01:00
Martin Simon 0fcc80db7d Add the new config key to the YAML config-show gold file
Same reason as the JSON one: ConfigShowYAMLTest snapshots `aptly config
show` in YAML form. Checked all three gold files under t02_config this
time rather than the one CI named. CreateConfigTest_gold is a commented
template rather than struct output, so it is unaffected, and it passes.
2026-11-03 13:13:18 +01:00
Martin Simon 74ff0194db Add the new config key to the config-show gold file
ConfigShowTest snapshots the output of `aptly config show`, which now
carries acquireByHashAlgorithms with its default. The other three system
files mentioning "architectures" are inputs rather than expected output,
which is why only this one test failed.
2026-11-03 13:13:18 +01:00
Martin Simon e58bab9587 Make the set of Acquire-By-Hash algorithms configurable
Acquire-By-Hash entries are written for MD5Sum, SHA1, SHA256 and SHA512,
hardcoded in two places in deb/index_files.go. Every algorithm is a full
set of index copies on each publish, and on S3-like storage each of those
is a network round trip rather than a link.

Measured on an R2-backed archive of 25 packages across three suites, one
`aptly publish update` per suite:

  Acquire-By-Hash off   38 S3 requests
  Acquire-By-Hash on   222 S3 requests   (88 HeadObject, 83 CopyObject,
                                          35 DeleteObject, 13 PutObject)

Modern apt verifies with SHA256; MD5Sum and SHA1 are deprecated and
Debian has been phasing them out of Release files for years. Publishers
who do not need them are paying three quarters of that for nothing, with
no way to say so.

acquireByHashAlgorithms in aptly.conf now selects the set. An empty or
absent value keeps all four, so existing configurations are unaffected.
The order entries are written in comes from the code rather than the
config, so the directories created in one loop and the entries written in
another cannot disagree. An unrecognised name is an error rather than
something to skip: skipping a typo would produce "no by-hash entries for
that algorithm", which looks exactly like having configured it away.

Set per publish invocation from the config, the way SkipBz2 already is,
rather than persisted on the publish point.

Pre-existing test failures on this tree are unchanged by this commit:
deb goes 173->177 passed with the same 11 failures, utils keeps its one
(TestCompress), s3 stays green.
2026-11-03 13:13:18 +01:00
André Roth db3e062edc Merge pull request #1637 from luanmdang/api-metrics-listener
api: add metrics-only listener
2026-11-03 12:52:46 +01:00
Luan Dang 0d23d32840 test: handle HEAD metrics response cleanup 2026-11-03 11:42:38 +01:00
Luan DangandCopilot Autofix powered by AI 198c801f00 Refactor TestHeadMetrics for improved testing
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-11-03 11:42:38 +01:00
Luan Dang 328120f80a api: add metrics-only listener 2026-11-03 11:42:38 +01:00
André Roth 431f75c109 Merge pull request #1646 from twaik/fix-version-split-last-hyphen
deb: split Debian revision at the last hyphen
2026-11-03 10:38:54 +01:00
André Roth 70f423adaa Merge pull request #1603 from jmunson/jmunson/enable-pprof
Expose net/http/pprof handlers
2026-11-02 20:01:01 +01:00
James Munson 2d72d1f0d5 Expose net/http/pprof handlers under /api/debug/pprof/* when the new
enablePprofEndpoint (JSON) / enable_pprof_endpoint (YAML) config option
is true. The endpoint is disabled by default.
2026-11-02 15:14:35 +01:00
André Roth 81d119f421 deb: rebase changes 2026-11-02 14:59:52 +01:00
Twaik Yont 242a0d4010 deb: split Debian revision at the last hyphen
Per Debian Policy, the version is split at the last hyphen:
https://www.debian.org/doc/debian-policy/ch-controlfields.html#version

Add tests for the ordering of '+', which is a regular non-letter sorted by its ASCII value (see Comparing version numbers on the same page).
2026-11-02 14:48:41 +01:00
André Roth eaf9453897 Merge pull request #1601 from jaymzh/debian-version
Fix version parsing, validate debian package version
2026-11-02 10:11:20 +01:00
Phil Dibowitz 21487d4b5d Fix version parsing, validate debian package version
This patch adds validation to the version pasing logic (parseVersion) to
not allow packages to be registered which apt wouldn't accept anyway.

It then propogates validation errors through CompareVersions, the way
in which parseVersion gets called, so that appropriate decisions can be
made.

New packages with invalid versions are not accepted.

For any pre-exisiting packages with invalid versions, as much care is
taken as possible to log warnings, and not use those packages.

This PR is loosely bnased on one by by Aaron Foster
<afoster@cloudflare.com> though it has largely been written at this
point.

Signed off with internal and external emails for clarity, but commit
is under personal email where we typically do open source contributions
from.

Signed-off-by: Phil Dibowitz <pdibowitz@cloudflare.com>
Signed-off-by: Phil Dibowitz <pdibowitz@ipom.com>
2026-11-01 14:25:17 -08:00
André Roth 0b55b09eb2 Merge pull request #1659 from aptly-dev/fix/tmp-suffix
publish s3: remove tmp suffix for testing
2026-11-01 17:03:11 +01:00
André Roth 10e748a097 publish s3: remove tmp suffix for testing 2026-11-01 16:14:10 +01:00
André Roth 28609a918e Merge pull request #1629 from daniel-pinder/pgp-accept-multisigned-releases 2026-11-01 14:45:00 +01:00
André Roth 29db8333c5 recreate test keys with faketime 2026-11-01 13:26:36 +01:00
Daniel Pinder 59ff63de62 pgp: accept multi-signed releases when the keyring holds only some signers
Debian co-signs each suite's InRelease with more than one archive key, so
gpgv reports a good signature from the key you trust and NO_PUBKEY for the
others, exiting 2. Both verifiers treat that as failure, which means
mirroring Debian requires importing every key Debian happens to sign with.
apt does not require this: it is fatal only when there is no good signature
or a BADSIG.

Adopt that rule for unknown signers. When gpgv exits non-zero the status
stream is parsed per NEWSIG block and the result is a success only when the
process exited, at least one block is a good signature from a held key, at
least one block is an unknown issuer (ERRSIG reason 9 with NO_PUBKEY), and
nothing else appears. BADSIG, EXPSIG, EXPKEYSIG and REVKEYSIG from a held
key still fail, as does any unexplained failure. The exit-zero path is
unchanged.

KEYEXPIRED and KEYREVOKED are deliberately not disqualifiers: doc/DETAILS
notes KEYEXPIRED is emitted for an expired subkey even when that subkey was
not used, so treating it as a verdict rejects valid signatures.

checkDetachedSignature gets the equivalent change for the internal verifier.
2026-11-01 13:26:36 +01:00
André Roth a65c2cd850 Merge pull request #1602 from andrewshadura/deb-tmp-uniq
deb publish: Create unique temporary files each time
2026-11-01 12:45:23 +01:00
André Roth 5775d2e31c Merge pull request #1656 from aptly-dev/fix/mirror-rename
mirror: fix rename
2026-11-01 10:36:13 +01:00
André Roth f668b1016f Merge pull request #1647 from aptly-dev/fix/swagger-validation
Documentation cleanup: fix validation error and config file headings
2026-11-01 10:35:50 +01:00
André Roth bddfed27e5 swagger: fix validation error 2026-11-01 02:30:33 +01:00
André Roth cd22f09fc1 doc: fix comment order in aptly.conf 2026-11-01 02:30:33 +01:00
André Roth cd365b2899 mirror: fix rename 2026-11-01 02:30:11 +01:00
André Roth add8d82b44 Merge pull request #1658 from aptly-dev/dependabot/go_modules/github.com/klauspost/compress-1.18.7
build(deps): bump github.com/klauspost/compress from 1.18.2 to 1.18.7
2026-11-01 01:27:03 +01:00
André Roth ffe77f7b1e Merge pull request #1657 from aptly-dev/dependabot/go_modules/github.com/go-jose/go-jose/v4-4.1.6
build(deps): bump github.com/go-jose/go-jose/v4 from 4.1.4 to 4.1.6
2026-11-01 01:24:13 +01:00
André Roth 65a8237574 Merge pull request #1642 from filbranden/fix-download-url-opaque
Show the full URL in download error messages
2026-11-01 00:50:52 +01:00
dependabot[bot] fba40f12bd build(deps): bump github.com/klauspost/compress from 1.18.2 to 1.18.7
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.2 to 1.18.7.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.18.2...v1.18.7)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 23:50:33 +00:00
dependabot[bot] 9c37e279dd build(deps): bump github.com/go-jose/go-jose/v4 from 4.1.4 to 4.1.6
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.1.4 to 4.1.6.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.1.4...v4.1.6)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-version: 4.1.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 23:50:18 +00:00
André Roth 3c46f7976b Merge pull request #1624 from chipflake/api-codename
api/publish: allow setting custom Codename on the Release file
2026-11-01 00:36:28 +01:00
Filipe Brandenburger 4e671cc366 Show the full URL in download error messages
Downloads set req.URL.Opaque to keep "+" percent-encoded in the path.
URL.String() prints "scheme:opaque" and drops the host, so every error
from the http client showed a URL such as "https:/repos/debian/...",
lacking a host name. The request itself was correct, but the message
sent me to look for a malformed URL.

Set req.URL.RawPath instead. The escaping still reaches the wire through
EscapedPath(), and String() keeps the scheme and the host.

The proxy special case is no longer necessary. Requests through a proxy
now get the same "+" escaping as direct requests.
2026-11-01 00:14:42 +01:00
André Roth bf06c688ed Merge pull request #1611 from tbe/api-pull-fix
api: fix snasphot-pull
2026-10-31 23:35:07 +01:00
André Roth 2f102086b7 Merge pull request #1640 from aptly-dev/dependabot/go_modules/go.opentelemetry.io/otel/sdk-1.45.0
build(deps): bump go.opentelemetry.io/otel/sdk from 1.44.0 to 1.45.0
2026-10-31 22:54:51 +01:00
dependabot[bot] 3812982982 build(deps): bump go.opentelemetry.io/otel/sdk from 1.44.0 to 1.45.0
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.44.0 to 1.45.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.44.0...v1.45.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 21:21:17 +00:00
André Roth 17d0899e72 Merge pull request #1628 from aptly-dev/dependabot/go_modules/google.golang.org/grpc-1.83.1
build(deps): bump google.golang.org/grpc from 1.79.3 to 1.83.2
2026-10-31 21:41:38 +01:00
André Roth 103e4c2810 Merge pull request #1621 from KereMath/fix/skel-appstream-publish
publish: fix duplicated skel/AppStream files and missing by-hash entries
2026-10-31 21:17:31 +01:00
dependabot[bot] 76bd237954 build(deps): bump google.golang.org/grpc from 1.79.3 to 1.83.2
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.79.3 to 1.83.2.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.79.3...v1.83.2)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 20:17:16 +00:00
André Roth a10f3bcc0e Merge pull request #1618 from aptly-dev/dependabot/go_modules/github.com/go-git/go-git/v5-5.19.2
build(deps): bump github.com/go-git/go-git/v5 from 5.19.1 to 5.19.2
2026-10-31 21:14:00 +01:00
André Roth c54e3f31a2 Merge pull request #1625 from barnumbirr/fix-readlink-metadata-case
s3: match the SymLink metadata key case-insensitively in ReadLink
2026-10-31 20:45:29 +01:00
dependabot[bot] 24adc1790c build(deps): bump github.com/go-git/go-git/v5 from 5.19.1 to 5.19.2
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.19.1 to 5.19.2.
- [Release notes](https://github.com/go-git/go-git/releases)
- [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md)
- [Commits](https://github.com/go-git/go-git/compare/v5.19.1...v5.19.2)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
  dependency-version: 5.19.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 19:45:23 +00:00
André Roth 0eee5c02d8 Merge pull request #1605 from aptly-dev/dependabot/go_modules/golang.org/x/crypto-0.52.0
build(deps): bump golang.org/x/crypto from 0.51.0 to 0.52.0
2026-10-31 20:43:00 +01:00
dependabot[bot] 585db213ec build(deps): bump golang.org/x/crypto from 0.51.0 to 0.52.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.51.0 to 0.52.0.
- [Commits](https://github.com/golang/crypto/compare/v0.51.0...v0.52.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.52.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 19:09:19 +00:00
André Roth 57529f53ef Merge pull request #1600 from aptly-dev/dependabot/go_modules/golang.org/x/net-0.55.0
build(deps): bump golang.org/x/net from 0.53.0 to 0.55.0
2026-10-31 20:07:37 +01:00
André Roth bc0fe9c0b7 Merge pull request #1648 from AdamMagued/fix/issue-1619-snapshots-async-lock
fix(api): handle concurrent snapshot deletion during list endpoint
2026-10-31 20:06:28 +01:00
dependabot[bot] d6066de308 build(deps): bump golang.org/x/net from 0.53.0 to 0.55.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.53.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.53.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 17:02:45 +00:00
adam magued f525d653a5 test(api): add regression test for concurrent snapshot deletion 2026-10-14 15:55:55 +03:00
adam magued 74c13f73f2 fix(api): handle concurrent snapshot deletion during list endpoint 2026-10-14 15:55:53 +03:00
André Roth 516fcc6bfd Merge pull request #1636 from mmayabi/add-aptlypilot-integration
Add AptlyPilot to integrations
2026-10-12 16:12:56 +02:00
André Roth 1c5db5f5b3 Merge pull request #1639 from voegelas/update-workflows
Update GitHub workflows
2026-10-12 16:09:33 +02:00
Andreas Vögele f51224958b Remove Debian 11 from ci-debian-build
Debian 11 reached its end-of-life on 31 August 2026.
2026-09-15 13:40:57 +02:00
Andreas Vögele fef55f81e5 Update the versions of the GitHub actions 2026-09-15 12:29:24 +02:00
mmayabi 16691aad83 add Integrations aptlyPilot 2026-09-10 19:41:13 +03:30
mmayabi e4c3399ae5 add Integrations AptlyPilot 2026-09-10 19:40:20 +03:30
Martin Simon ed180e8e16 Cover ReadLink's non-link and error paths
codecov/patch flagged the change at 75% of diff hit. The uncovered lines
were the two the existing test could not reach: the fallthrough when no
metadata key matches, and the HeadObject error return.

Both are worth pinning regardless of the coverage gate.
packageIndexByHash calls ReadLink on a path it has only just confirmed
exists and treats an error as "leave it alone", so "this is not a link"
and "I could not find out" have to stay distinguishable: the first is an
empty string with a nil error, the second is an error.

ReadLink goes to 100%.
2026-08-31 13:21:22 +02:00
Martin Simon 563e4eab2e s3: match the SymLink metadata key case-insensitively in ReadLink
S3 lowercases user metadata keys. SymLink writes "SymLink" and ReadLink
looks up output.Metadata["SymLink"], but the value comes back under
"symlink", so the lookup misses and ReadLink returns an empty string with
a nil error. To the caller that reads as "this object is not a link"
rather than as a failure.

deb.packageIndexByHash uses ReadLink to find the physical file its .old
pointer refers to, and removes it before rotating:

    linkTarget, err = publishedStorage.ReadLink(oldIndexPath)
    if err == nil {
        _ = publishedStorage.Remove(linkTarget)
    }

With "" returned the Remove is a no-op and its error is discarded, so the
superseded by-hash index file is never deleted. The pointers rotate
correctly and the physical files accumulate: one per index file per hash
algorithm per publish, indefinitely. Filesystem backends are unaffected
because they have real symlinks.

Measured on a live R2-backed archive publishing three suites a few times
a day: dists/ held 711 objects, of which 672 were by-hash and only 39
were current indexes, growing by about 48 objects per publish since
Acquire-By-Hash was enabled.

The existing TestSymLink cannot catch this, because it ends in
c.Skip("copy not available in s3test") and never runs. The regression
test added here writes the metadata with PutObject instead, so it needs
no CopyObject support, and fails before this change with
obtained string = "".
2026-08-31 11:17:35 +02:00
Eleni Michalaki 3d7f17e849 api/publish: allow setting custom Codename on the Release file 2026-08-26 11:35:43 +02:00
KereMath 54a519b062 tests: cover skel files with multiple components at unit level
Regression test for the duplicated skel payload: publishing two
components used to append each skeleton file once per component. Fails
against the previous code with doubled file contents, mirroring the
PublishRepo40Test system test at unit level.
2026-08-20 11:07:06 +03:00
KereMath c54d44e070 tests: cover skel and AppStream publish error paths
Publish moved the skel and AppStream emission out of the per-component
loop, and codecov flags the relocated error branches as uncovered. Cover
them with deterministic failures:

- a regular file in the middle of the skel path makes the walk fail
  with ENOTDIR, which is not swallowed as a not-exist error
- nested skel/AppStream names each within NAME_MAX flatten into a
  temporary index file name that exceeds it, failing index creation
- an unreadable skel file (mode 0) fails the open (non-root only)
- a ~1MB filesystem as TMPDIR runs out of space mid-write, reusing the
  /smallfs convention from DiskFullSuite

Also add a unit-level happy path test publishing nested skel files and
checking they land in the published tree and the Release checksums.
2026-08-20 11:04:01 +03:00
KereMath bef02fe62b tests: cover skel files with multiple components
Publishing two components appended each skeleton payload twice; the
existing skel test only publishes a single component so it did not
catch it.
2026-08-20 03:26:30 +03:00
KereMath c4ad756b47 publish: add by-hash entries for skel and AppStream files
SkelIndex was the only index constructor not propagating acquireByHash,
so with -acquire-by-hash the Release file advertises Acquire-By-Hash: yes
while no by-hash/ entries exist for these files and apt gets a 404.
2026-08-20 03:09:25 +03:00
KereMath 23c5a31922 publish: write skel and AppStream files once per publish
The skeleton and AppStream loops were nested inside the per-component
loop while iterating over all components themselves, so every file was
written once per component. Publishing main + contrib appended each
payload twice. Release checksums are computed after the fact, so the
duplicated content is signed and apt accepts it silently.
2026-08-20 03:09:09 +03:00
Thomas Berger f95153a7c9 api: fix snasphot-pull
in the API, snapshotPull never finished reading the target repository
within the async block. So the target was treated as an empty
snapshot, and a pull would only carry over the content from the sources
2026-07-25 01:48:55 +02:00
Andrej Shadura ef65e2539a deb publish: Create unique temporary files each time
If two publish jobs run concurrently (which they arguably shouldn’t),
they’d step on each other toes and remove Packages.tmp file the other
job’s just created.

The API has locking for concurrent publish/drop operations on the same
prefix, but command-line operations bypass that lock. Also, not having
a predictable temporary filename is generally good from a security
standpoint, given that cryptographic signing is involved.

Work around the issue by creating a per-job temporary file.

Signed-off-by: Andrej Shadura <andrew.shadura@collabora.co.uk>
2026-07-07 16:05:12 +02:00
André Roth f59b0d2b06 go: 1.26 2026-07-01 10:40:38 +02:00
André Roth aa42fc1025 Merge pull request #1598 from C0deSentin3l/master
Dont lock the go version to the .0 release
2026-06-29 16:58:46 +02:00
C0deSentin3l 080da87b4e Dont lock the go version to the .0 release
the .0 release of go 1.25 is not the latest version of that release so missing out on security fixes etc
2026-06-29 12:06:48 +01:00
André Roth 87e6a36d9b docs: update release task list 2026-06-25 16:33:40 +02:00
André Roth 908f7348d9 Merge pull request #1596 from aptly-dev/release/1.6.3
Release 1.6.3
2026-06-24 23:35:03 +02:00
André Roth 65b34cfec5 debian: update changelog 2026-06-24 20:37:39 +02:00
105 changed files with 5106 additions and 443 deletions

No files matched your search

+18 -21
View File
@@ -24,7 +24,7 @@ jobs:
timeout-minutes: 30
steps:
- name: "Checkout Repository"
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
# fetch the whole repo for `git describe` to work
fetch-depth: 0
@@ -36,7 +36,7 @@ jobs:
make docker-unit-test
mkdir -p out/coverage
mv unit.out out/coverage/
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: unit-tests-coverage
path: out/
@@ -60,7 +60,7 @@ jobs:
sudo apt-get install -y --no-install-recommends graphviz gnupg2 gpgv2 git gcc make devscripts python3 python3-requests-unixsocket python3-termcolor python3-swiftclient python3-boto python3-azure-storage python3-etcd3 python3-plyvel flake8 faketime dput-ng
- name: "Checkout Repository"
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
# fetch the whole repo for `git describe` to work
fetch-depth: 0
@@ -77,7 +77,7 @@ jobs:
id: goversion
- name: "Setup Go"
uses: actions/setup-go@v3
uses: actions/setup-go@v7
with:
go-version: ${{ steps.goversion.outputs.GOVER }}
@@ -108,7 +108,7 @@ jobs:
mkdir -p out/coverage
COVERAGE_DIR=$PWD/out/coverage make system-test
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: system-tests-coverage
path: out/
@@ -123,15 +123,15 @@ jobs:
- test
steps:
- name: "Checkout Repository"
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: "Download Unit Test Coverage"
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: unit-tests-coverage
- name: "Download System Test Coverage"
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: system-tests-coverage
@@ -158,7 +158,7 @@ jobs:
strategy:
fail-fast: false
matrix:
name: ["Debian 13/trixie", "Debian 12/bookworm", "Debian 11/bullseye", "Ubuntu 26.04", "Ubuntu 24.04", "Ubuntu 22.04", "Ubuntu 20.04"]
name: ["Debian 13/trixie", "Debian 12/bookworm", "Ubuntu 26.04", "Ubuntu 24.04", "Ubuntu 22.04", "Ubuntu 20.04"]
arch: ["amd64", "i386" , "arm64" , "armhf"]
include:
- name: "Debian 13/trixie"
@@ -167,9 +167,6 @@ jobs:
- name: "Debian 12/bookworm"
suite: bookworm
image: debian:bookworm-slim
- name: "Debian 11/bullseye"
suite: bullseye
image: debian:bullseye-slim
- name: "Ubuntu 26.04"
suite: resolute
image: ubuntu:26.04
@@ -199,7 +196,7 @@ jobs:
git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: "Checkout Repository"
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
# fetch the whole repo for `git describe` to work
fetch-depth: 0
@@ -212,7 +209,7 @@ jobs:
id: goversion
- name: "Setup Go"
uses: actions/setup-go@v3
uses: actions/setup-go@v7
with:
go-version: ${{ steps.goversion.outputs.GOVER }}
@@ -268,7 +265,7 @@ jobs:
- name: "Upload CI Artifacts"
if: github.ref != 'refs/heads/master' && !startsWith(github.event.ref, 'refs/tags')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.suite }}_${{ matrix.arch }}
path: build/
@@ -290,7 +287,7 @@ jobs:
goarch: arm
steps:
- name: "Checkout Repository"
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
# fetch the whole repo for `git describe` to work
fetch-depth: 0
@@ -301,7 +298,7 @@ jobs:
id: goversion
- name: "Setup Go"
uses: actions/setup-go@v3
uses: actions/setup-go@v7
with:
go-version: ${{ steps.goversion.outputs.GOVER }}
@@ -328,7 +325,7 @@ jobs:
make binaries GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }}
- name: "Upload Artifacts"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: startsWith(github.event.ref, 'refs/tags')
with:
name: aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}
@@ -336,7 +333,7 @@ jobs:
compression-level: 0 # no compression
- name: "Upload CI Artifacts"
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: "!startsWith(github.event.ref, 'refs/tags')"
with:
name: aptly_${{ steps.releaseversion.outputs.VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}
@@ -352,7 +349,7 @@ jobs:
if: startsWith(github.event.ref, 'refs/tags')
steps:
- name: "Checkout Repository"
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: "Get aptly version"
env:
@@ -364,7 +361,7 @@ jobs:
id: releaseversion
- name: "Download Artifacts"
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: out/
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: "Read go version from go.mod"
run: |
@@ -23,7 +23,7 @@ jobs:
id: goversion
- name: "Setup Go"
uses: actions/setup-go@v3
uses: actions/setup-go@v7
with:
go-version: ${{ steps.goversion.outputs.GOVER }}
-21
View File
@@ -1,21 +0,0 @@
name: test aptly upload-lint
on:
push:
pull_request:
permissions:
contents: read
jobs:
aptly-upload:
name: upload
runs-on: ubuntu-latest
steps:
- name: Publish to aptly server
uses: neolynx/aptly-github-action/publish@v0.0.15
with:
directory: buildout
aptly_server: https://aptly.info
aptly_user: ${{ secrets.APTLY_USER }}
aptly_password: ${{ secrets.APTLY_PASSWORD }}
+5
View File
@@ -84,3 +84,8 @@ List of contributors, in chronological order:
* Zhang Xiao (https://github.com/xzhang1)
* Tom Nguyen (https://github.com/lecafard)
* Philip Cramer (https://github.com/PhilipCramer)
* Kerem Gur (https://github.com/KereMath)
* Phil Dibowitz (https://github.com/jaymzh)
* James Munson (https://github.com/jmunson)
* Luan Dang (https://github.com/luanmdang)
* Martin Simon (https://github.com/barnumbirr)
+4
View File
@@ -126,6 +126,10 @@ GUI for aptly API:
- `Python aptly GUI (via pyqt5) <https://github.com/chnyda/python-aptly-gui>`_ by Cedric Hnyda
Repository management:
- `AptlyPilot <https://github.com/mmayabi/AptlyPilot>`_ by Maryam Mayabi - Web-based management platform for Aptly repositories with Repository as Code workflows and lifecycle automation.
Scala sbt:
- `sbt aptly plugin <https://github.com/amalakar/sbt-aptly>`_ by Arup Malakar
+1
View File
@@ -16,3 +16,4 @@ git push origin v$version master
- update version in content/download.md
- push commit to master
- create release announcement on https://github.com/aptly-dev/aptly/discussions
- update aptly infrastructure
+5 -27
View File
@@ -285,33 +285,11 @@ func showPackages(c *gin.Context, reflist *deb.PackageRefList, collectionFactory
// filter packages by version
if c.Request.URL.Query().Get("maximumVersion") == "1" {
list.PrepareIndex()
_ = list.ForEach(func(p *deb.Package) error {
versionQ, err := query.Parse(fmt.Sprintf("Name (%s), $Version (<= %s)", p.Name, p.Version))
if err != nil {
fmt.Println("filter packages by version, query string parse err: ", err)
_ = c.AbortWithError(500, fmt.Errorf("unable to parse %s maximum version query string: %s", p.Name, err))
} else {
tmpList, err := list.Filter(deb.FilterOptions{
Queries: []deb.PackageQuery{versionQ},
})
if err == nil {
if tmpList.Len() > 0 {
_ = tmpList.ForEach(func(tp *deb.Package) error {
list.Remove(tp)
return nil
})
_ = list.Add(p)
}
} else {
fmt.Println("filter packages by version, filter err: ", err)
_ = c.AbortWithError(500, fmt.Errorf("unable to get %s maximum version: %s", p.Name, err))
}
}
return nil
})
list, err = list.FilterLatest(1)
if err != nil {
AbortWithJSONError(c, 500, fmt.Errorf("unable to filter latest packages: %s", err))
return
}
}
if c.Request.URL.Query().Get("format") == "details" {
+18
View File
@@ -42,6 +42,7 @@ func createTestConfig() *os.File {
jsonString, err := json.Marshal(gin.H{
"architectures": []string{},
"enableMetricsEndpoint": true,
"enablePprofEndpoint": false,
"S3PublishEndpoints": map[string]map[string]string{
"test-s3": {
"region": "us-east-1",
@@ -158,6 +159,23 @@ func (s *APISuite) TestGetMetrics(c *C) {
c.Check(b, Matches, ".*aptly_build_info.*version=\"testVersion\".*")
}
func (s *APISuite) TestHeadMetrics(c *C) {
server := httptest.NewServer(s.router)
defer server.Close()
req, err := http.NewRequest(http.MethodHead, server.URL+"/api/metrics", nil)
c.Assert(err, IsNil)
resp, err := server.Client().Do(req)
c.Assert(err, IsNil)
defer func() { _ = resp.Body.Close() }()
c.Check(resp.StatusCode, Equals, 200)
body, readErr := io.ReadAll(resp.Body)
c.Assert(readErr, IsNil)
c.Check(len(body), Equals, 0)
}
func (s *APISuite) TestRepoCreate(c *C) {
body, err := json.Marshal(gin.H{
"Name": "dummy",
+1 -1
View File
@@ -141,7 +141,7 @@ func apiGPGAddKey(c *gin.Context) {
// @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)
// @Param keyring query string false "Keyring file to list keys from (default: trustedkeys.gpg)"
// @Produce json
// @Success 200 {object} gpgKeyListResponse "OK"
// @Failure 400 {object} Error "Bad Request"
+20 -2
View File
@@ -520,8 +520,10 @@ 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
// Set "true" to download only the latest version per package/architecture (same as `MaxVersions`: 1)
LatestOnly bool ` json:"LatestOnly"`
// Download only the N most recent versions per package/architecture, 0 means all versions
MaxVersions int ` json:"MaxVersions" example:"5"`
}
// @Summary Update Mirror
@@ -573,6 +575,14 @@ func apiMirrorsUpdate(c *gin.Context) {
}
}
if b.MaxVersions < 0 {
AbortWithJSONError(c, 400, fmt.Errorf("MaxVersions should be a positive number"))
return
}
if b.LatestOnly && b.MaxVersions == 0 {
b.MaxVersions = 1
}
verifier, err := getVerifier(b.Keyrings)
if err != nil {
AbortWithJSONError(c, 400, fmt.Errorf("unable to initialize GPG verifier: %s", err))
@@ -597,6 +607,7 @@ func apiMirrorsUpdate(c *gin.Context) {
if err == nil {
return &task.ProcessReturnValue{Code: http.StatusConflict, Value: nil}, fmt.Errorf("unable to rename: mirror %s already exists", b.Name)
}
remote.Name = b.Name
}
downloader := context.NewDownloader(out)
@@ -639,8 +650,15 @@ func apiMirrorsUpdate(c *gin.Context) {
}
}
if b.MaxVersions > 0 {
_, _, err := remote.FilterLatest(b.MaxVersions)
if err != nil {
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
}
}
queue, downloadSize, err := remote.BuildDownloadQueue(context.PackagePool(), taskCollectionFactory.PackageCollection(),
taskCollectionFactory.ChecksumCollection(nil), b.SkipExistingPackages, b.LatestOnly)
taskCollectionFactory.ChecksumCollection(nil), b.SkipExistingPackages)
if err != nil {
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
}
+112
View File
@@ -0,0 +1,112 @@
package api
import (
"encoding/json"
"net/http/httptest"
"reflect"
"sort"
"testing"
"github.com/aptly-dev/aptly/database/goleveldb"
"github.com/aptly-dev/aptly/deb"
"github.com/gin-gonic/gin"
)
// Exercise the common API listing handler with persisted packages, checking
// both reference and detailed JSON responses independently of response order.
func TestPackagesMaximumVersionArchitectureVariant(t *testing.T) {
pkg := func(arch, variant, version string) *deb.Package {
stanza := deb.Stanza{"Package": "example", "Version": version, "Architecture": arch}
if variant != "" {
stanza["Architecture-Variant"] = variant
}
return deb.NewPackageFromControlFile(stanza)
}
n1, n3 := pkg("amd64", "", "1"), pkg("amd64", "", "3")
v1, v2 := pkg("amd64", "amd64v3", "1"), pkg("amd64", "amd64v3", "2")
a1, a4 := pkg("arm64", "", "1"), pkg("arm64", "", "4")
all := []*deb.Package{n1, n3, v1, v2, a1, a4}
cases := []struct {
name, option string
input, want []*deb.Package
}{
{"same_version", "1", []*deb.Package{n1, v1}, []*deb.Package{n1, v1}},
{"independent_latest", "1", []*deb.Package{n1, n3, v1, v2}, []*deb.Package{n3, v2}},
{"ordinary_multi_architecture", "1", []*deb.Package{n1, n3, a1, a4}, []*deb.Package{n3, a4}},
{"ordinary_single_architecture", "1", []*deb.Package{n1, n3}, []*deb.Package{n3}},
{"disabled_absent", "", all, all},
{"disabled_zero", "0", all, all},
{"empty", "1", nil, nil},
}
for _, tc := range cases {
for _, format := range []string{"refs", "details"} {
t.Run(tc.name+"/"+format, func(t *testing.T) {
db, err := goleveldb.NewOpenDB(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Error(err)
}
})
factory := deb.NewCollectionFactory(db)
list := deb.NewPackageList()
for _, p := range tc.input {
// Update offloads metadata; use a fresh package for each database.
p := pkg(p.Architecture, p.ArchitectureVariant, p.Version)
if err := factory.PackageCollection().Update(p); err != nil {
t.Fatal(err)
}
if err := list.Add(p); err != nil {
t.Fatal(err)
}
}
refs := deb.NewPackageRefListFromPackageList(list)
url := "/api/packages?format=" + format
if tc.option != "" {
url += "&maximumVersion=" + tc.option
}
response := httptest.NewRecorder()
c, _ := gin.CreateTestContext(response)
c.Request = httptest.NewRequest("GET", url, nil)
showPackages(c, refs, deb.NewCollectionFactory(db))
if response.Code != 200 {
t.Fatalf("status %d: %s", response.Code, response.Body.String())
}
got := []string{}
if format == "details" {
var details []map[string]string
if err := json.Unmarshal(response.Body.Bytes(), &details); err != nil {
t.Fatal(err)
}
for _, detail := range details {
got = append(got, detail["Key"])
// Metadata must remain separate from the identity encoded in Key.
for _, p := range tc.want {
if detail["Key"] == string(p.Key("")) {
if detail["Architecture"] != p.Architecture || detail["Architecture-Variant"] != p.ArchitectureVariant || detail["ShortKey"] != string(p.ShortKey("")) {
t.Errorf("incorrect package metadata: %v", detail)
}
}
}
}
} else if err := json.Unmarshal(response.Body.Bytes(), &got); err != nil {
t.Fatal(err)
}
want := []string{}
for _, p := range tc.want {
want = append(want, string(p.Key("")))
}
sort.Strings(got)
sort.Strings(want)
if !reflect.DeepEqual(got, want) {
t.Errorf("package references:\n got %v\nwant %v", got, want)
}
if !reflect.DeepEqual(refs.Strings(), deb.NewPackageRefListFromPackageList(list).Strings()) {
t.Error("listing mutated source references")
}
})
}
}
}
+121
View File
@@ -0,0 +1,121 @@
package api
import (
"encoding/json"
"fmt"
"net/http/httptest"
"os"
"os/exec"
"strings"
"testing"
"github.com/aptly-dev/aptly/aptly"
ctx "github.com/aptly-dev/aptly/context"
"github.com/gin-gonic/gin"
)
// pprofTestServer starts a real httptest.Server with the aptly router configured
// with enablePprofEndpoint: true. The caller is responsible for calling
// server.Close() and aptlyCtx.Shutdown().
func pprofTestServer(t *testing.T) (*httptest.Server, *ctx.AptlyContext) {
t.Helper()
aptly.Version = "testVersion"
file, err := os.CreateTemp("", "aptly-pprof-integration")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Remove(file.Name()) })
jsonString, err := json.Marshal(gin.H{
"architectures": []string{},
"enablePprofEndpoint": true,
})
if err != nil {
t.Fatal(err)
}
if _, err = file.Write(jsonString); err != nil {
t.Fatal(err)
}
_ = file.Close()
aptlyCtx, router, err := newPprofContext(file.Name())
if err != nil {
t.Fatal(err)
}
return httptest.NewServer(router), aptlyCtx
}
// runPprof runs "go tool pprof -top <url>" and returns the combined output.
func runPprof(t *testing.T, url string) string {
t.Helper()
cmd := exec.Command("go", "tool", "pprof", "-top", url)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("go tool pprof -top %s failed: %v\noutput:\n%s", url, err, out)
}
return string(out)
}
// TestPprofToolGoroutine verifies that go tool pprof can fetch and parse the
// goroutine profile from the /api/debug/pprof/goroutine endpoint.
func TestPprofToolGoroutine(t *testing.T) {
server, aptlyCtx := pprofTestServer(t)
defer server.Close()
defer aptlyCtx.Shutdown()
url := fmt.Sprintf("%s/api/debug/pprof/goroutine", server.URL)
out := runPprof(t, url)
t.Logf("go tool pprof goroutine output:\n%s", out)
if !strings.Contains(out, "Type: goroutine") {
t.Errorf("expected 'Type: goroutine' in output, got:\n%s", out)
}
if !strings.Contains(out, "flat") {
t.Errorf("expected 'flat' column header in output, got:\n%s", out)
}
}
// TestPprofToolHeap verifies that go tool pprof can fetch and parse the heap
// profile from the /api/debug/pprof/heap endpoint.
func TestPprofToolHeap(t *testing.T) {
server, aptlyCtx := pprofTestServer(t)
defer server.Close()
defer aptlyCtx.Shutdown()
url := fmt.Sprintf("%s/api/debug/pprof/heap", server.URL)
out := runPprof(t, url)
t.Logf("go tool pprof heap output:\n%s", out)
// Heap profiles are typed as "inuse_space" by default.
if !strings.Contains(out, "Type: inuse_space") {
t.Errorf("expected 'Type: inuse_space' in output, got:\n%s", out)
}
if !strings.Contains(out, "flat") {
t.Errorf("expected 'flat' column header in output, got:\n%s", out)
}
}
// TestPprofToolCmdline verifies that the cmdline endpoint is reachable.
// cmdline returns plain text (the process argv), not a pprof binary profile,
// so we confirm the HTTP fetch succeeds rather than parsing it as a profile.
func TestPprofToolCmdline(t *testing.T) {
server, aptlyCtx := pprofTestServer(t)
defer server.Close()
defer aptlyCtx.Shutdown()
url := fmt.Sprintf("%s/api/debug/pprof/cmdline", server.URL)
resp, err := server.Client().Get(url)
if err != nil {
t.Fatalf("GET %s: %v", url, err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != 200 {
t.Errorf("expected 200, got %d", resp.StatusCode)
}
t.Logf("cmdline endpoint status: %d", resp.StatusCode)
}
+183
View File
@@ -0,0 +1,183 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"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"
)
// newPprofContext builds an aptly context and router from the given config file
// path. It returns an error if context initialisation fails. The caller is
// responsible for calling aptlyCtx.Shutdown() when done.
func newPprofContext(configPath string) (*ctx.AptlyContext, http.Handler, error) {
flags := flag.NewFlagSet("fakeFlags", flag.ContinueOnError)
flags.Bool("no-lock", false, "dummy")
flags.Int("db-open-attempts", 3, "dummy")
flags.String("config", configPath, "dummy")
flags.String("architectures", "", "dummy")
aptlyCtx, err := ctx.NewContext(flags)
if err != nil {
return nil, nil, err
}
return aptlyCtx, Router(aptlyCtx), nil
}
// PprofEnabledSuite tests pprof endpoints when EnablePprofEndpoint is true.
// It uses a dedicated router built from a config that enables only the pprof
// endpoint, ensuring no interaction with other optional features.
type PprofEnabledSuite struct {
configFile *os.File
aptlyCtx *ctx.AptlyContext
router http.Handler
}
var _ = Suite(&PprofEnabledSuite{})
func (s *PprofEnabledSuite) SetUpSuite(c *C) {
aptly.Version = "testVersion"
file, err := os.CreateTemp("", "aptly-pprof")
c.Assert(err, IsNil)
s.configFile = file
jsonString, err := json.Marshal(gin.H{
"architectures": []string{},
"enablePprofEndpoint": true,
})
c.Assert(err, IsNil)
_, err = file.Write(jsonString)
c.Assert(err, IsNil)
_ = file.Close()
aptlyCtx, router, err := newPprofContext(file.Name())
c.Assert(err, IsNil)
s.aptlyCtx = aptlyCtx
s.router = router
}
func (s *PprofEnabledSuite) TearDownSuite(c *C) {
if s.configFile != nil {
_ = os.Remove(s.configFile.Name())
}
if s.aptlyCtx != nil {
s.aptlyCtx.Shutdown()
}
}
func (s *PprofEnabledSuite) request(method, url string) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
if err != nil {
panic(err)
}
s.router.ServeHTTP(w, req)
return w
}
// TestPprofIndexReturns200 verifies the pprof index page is served when the
// endpoint is enabled.
func (s *PprofEnabledSuite) TestPprofIndexReturns200(c *C) {
w := s.request("GET", "/api/debug/pprof/")
c.Check(w.Code, Equals, 200)
c.Check(w.Header().Get("Content-Type"), Matches, "text/html.*")
}
// TestPprofGoroutineReturns200 verifies the goroutine sub-profile is served.
func (s *PprofEnabledSuite) TestPprofGoroutineReturns200(c *C) {
w := s.request("GET", "/api/debug/pprof/goroutine?debug=1")
c.Check(w.Code, Equals, 200)
}
// TestPprofHeapReturns200 verifies the heap sub-profile is served.
func (s *PprofEnabledSuite) TestPprofHeapReturns200(c *C) {
w := s.request("GET", "/api/debug/pprof/heap?debug=1")
c.Check(w.Code, Equals, 200)
}
// TestPprofCmdlineReturns200 verifies the cmdline handler is served.
func (s *PprofEnabledSuite) TestPprofCmdlineReturns200(c *C) {
w := s.request("GET", "/api/debug/pprof/cmdline")
c.Check(w.Code, Equals, 200)
}
// TestPprofSymbolReturns200 verifies the symbol lookup handler responds (GET
// with no addresses returns 200 with a count of 0 symbols found).
func (s *PprofEnabledSuite) TestPprofSymbolReturns200(c *C) {
w := s.request("GET", "/api/debug/pprof/symbol")
c.Check(w.Code, Equals, 200)
}
// TestPprofTraceReturns200 verifies the execution trace handler is wired
// correctly. The minimum trace duration is 1 second, so this test blocks
// briefly but confirms end-to-end handler registration.
func (s *PprofEnabledSuite) TestPprofTraceReturns200(c *C) {
w := s.request("GET", "/api/debug/pprof/trace?seconds=1")
c.Check(w.Code, Equals, 200)
c.Check(w.Header().Get("Content-Type"), Equals, "application/octet-stream")
}
// TestNonPprofRoutesUnaffectedWhenEnabled verifies that existing API routes
// continue to work correctly when the pprof endpoint is enabled.
func (s *PprofEnabledSuite) TestNonPprofRoutesUnaffectedWhenEnabled(c *C) {
w := s.request("GET", "/api/version")
c.Check(w.Code, Equals, 200)
}
// PprofDisabledSuite tests that pprof endpoints are NOT exposed when the config
// option is off. It embeds APISuite, whose config does not set
// enablePprofEndpoint, confirming the default state is safe. The inherited
// APISuite tests also run here, verifying that normal API operation is
// unaffected when pprof is disabled.
type PprofDisabledSuite struct {
APISuite
}
var _ = Suite(&PprofDisabledSuite{})
// TestPprofIndexNotExposed verifies the pprof index is not reachable when
// enablePprofEndpoint is false (the default).
func (s *PprofDisabledSuite) TestPprofIndexNotExposed(c *C) {
response, err := s.HTTPRequest("GET", "/api/debug/pprof/", nil)
c.Assert(err, IsNil)
c.Check(response.Code, Equals, 404)
}
// TestPprofGoroutineNotExposed verifies the goroutine profile is not reachable
// when disabled.
func (s *PprofDisabledSuite) TestPprofGoroutineNotExposed(c *C) {
response, err := s.HTTPRequest("GET", "/api/debug/pprof/goroutine", nil)
c.Assert(err, IsNil)
c.Check(response.Code, Equals, 404)
}
// TestPprofHeapNotExposed verifies the heap profile is not reachable when
// disabled.
func (s *PprofDisabledSuite) TestPprofHeapNotExposed(c *C) {
response, err := s.HTTPRequest("GET", "/api/debug/pprof/heap", nil)
c.Assert(err, IsNil)
c.Check(response.Code, Equals, 404)
}
// TestPprofCmdlineNotExposed verifies the cmdline endpoint is not reachable
// when disabled.
func (s *PprofDisabledSuite) TestPprofCmdlineNotExposed(c *C) {
response, err := s.HTTPRequest("GET", "/api/debug/pprof/cmdline", nil)
c.Assert(err, IsNil)
c.Check(response.Code, Equals, 404)
}
// TestPprofTraceNotExposed verifies the trace endpoint is not reachable when
// disabled.
func (s *PprofDisabledSuite) TestPprofTraceNotExposed(c *C) {
response, err := s.HTTPRequest("GET", "/api/debug/pprof/trace", nil)
c.Assert(err, IsNil)
c.Check(response.Code, Equals, 404)
}
+12
View File
@@ -160,6 +160,8 @@ type publishedRepoCreateParams struct {
Sources []sourceParams `binding:"required" json:"Sources"`
// Distribution name, if missing Aptly would try to guess from sources
Distribution string ` json:"Distribution" example:"bookworm"`
// Value of Codename: field in published repository stanza, if missing equals Distribution
Codename string ` json:"Codename" example:"bookworm"`
// Value of Label: field in published repository stanza
Label string ` json:"Label" example:""`
// Value of Origin: field in published repository stanza
@@ -354,6 +356,7 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
if b.ButAutomaticUpgrades != "" {
published.ButAutomaticUpgrades = b.ButAutomaticUpgrades
}
published.Codename = b.Codename
published.Label = b.Label
published.SkipContents = context.Config().SkipContentsPublishing
@@ -384,6 +387,9 @@ func apiPublishRepoOrSnapshot(c *gin.Context) {
return &task.ProcessReturnValue{Code: http.StatusBadRequest, Value: nil}, fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate)
}
// Which hash algorithms get Acquire-By-Hash entries. Each one is a full
// set of index copies per publish, which is felt most on S3-like storage.
published.AcquireByHashAlgorithms = context.Config().AcquireByHashAlgorithms
err = published.Publish(context.PackagePool(), context, taskCollectionFactory, signer, publishOutput, b.ForceOverwrite, context.SkelPath())
if err != nil {
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to publish: %s", err)
@@ -575,6 +581,9 @@ func apiPublishUpdateSwitch(c *gin.Context) {
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
}
// Which hash algorithms get Acquire-By-Hash entries. Each one is a full
// set of index copies per publish, which is felt most on S3-like storage.
published.AcquireByHashAlgorithms = context.Config().AcquireByHashAlgorithms
err = published.Publish(context.PackagePool(), context, taskCollectionFactory, signer, out, b.ForceOverwrite, context.SkelPath())
if err != nil {
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
@@ -1242,6 +1251,9 @@ func apiPublishUpdate(c *gin.Context) {
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
}
// Which hash algorithms get Acquire-By-Hash entries. Each one is a full
// set of index copies per publish, which is felt most on S3-like storage.
published.AcquireByHashAlgorithms = context.Config().AcquireByHashAlgorithms
err = published.Publish(context.PackagePool(), context, taskCollectionFactory, signer, out, b.ForceOverwrite, context.SkelPath())
if err != nil {
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, fmt.Errorf("unable to update: %s", err)
+26 -1
View File
@@ -2,6 +2,8 @@ package api
import (
"net/http"
"net/http/pprof"
"strings"
"sync/atomic"
"github.com/aptly-dev/aptly/aptly"
@@ -82,6 +84,27 @@ func Router(c *ctx.AptlyContext) http.Handler {
MetricsCollectorRegistrar.Register(router)
}
if c.Config().EnablePprofEndpoint {
pprofMux := http.NewServeMux()
pprofMux.HandleFunc("/debug/pprof/", pprof.Index)
pprofMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
pprofMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
pprofMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
pprofMux.HandleFunc("/debug/pprof/trace", pprof.Trace)
router.Any("/api/debug/pprof/*any", func(c *gin.Context) {
origPath := c.Request.URL.Path
origRawPath := c.Request.URL.RawPath
c.Request.URL.Path = strings.TrimPrefix(origPath, "/api")
if origRawPath != "" {
c.Request.URL.RawPath = strings.TrimPrefix(origRawPath, "/api")
}
pprofMux.ServeHTTP(c.Writer, c.Request)
c.Request.URL.Path = origPath
c.Request.URL.RawPath = origRawPath
})
}
if c.Config().ServeInAPIMode {
router.GET("/repos/", reposListInAPIMode(c.Config().FileSystemPublishRoots))
router.GET("/repos/:storage/*pkgPath", reposServeInAPIMode)
@@ -122,7 +145,9 @@ func Router(c *ctx.AptlyContext) http.Handler {
{
if c.Config().EnableMetricsEndpoint {
api.GET("/metrics", apiMetricsGet())
metricsHandler := apiMetricsGet()
api.GET("/metrics", metricsHandler)
api.HEAD("/metrics", metricsHandler)
}
api.GET("/version", apiVersion)
api.GET("/storage", apiDiskFree)
+11 -1
View File
@@ -36,6 +36,9 @@ func apiSnapshotsList(c *gin.Context) {
err := collection.ForEachSorted(SortMethodString, func(snapshot *deb.Snapshot) error {
err := collection.LoadComplete(snapshot)
if err != nil {
if err == database.ErrNotFound {
return nil
}
return err
}
@@ -800,6 +803,10 @@ func apiSnapshotsPull(c *gin.Context) {
if err != nil {
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
}
err = taskCollectionFactory.SnapshotCollection().LoadComplete(freshToSnapshot)
if err != nil {
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
}
freshSourceSnapshot, err := taskCollectionFactory.SnapshotCollection().ByName(body.Source)
if err != nil {
return &task.ProcessReturnValue{Code: http.StatusInternalServerError, Value: nil}, err
@@ -873,7 +880,7 @@ func apiSnapshotsPull(c *gin.Context) {
alreadySeen := map[string]bool{}
_ = destinationPackageList.ForEachIndexed(func(pkg *deb.Package) error {
key := pkg.Architecture + "_" + pkg.Name
key := pkg.IndexArchitecture() + "_" + pkg.Name
_, seen := alreadySeen[key]
// If we haven't seen such name-architecture pair and were instructed to remove, remove it
@@ -881,6 +888,9 @@ func apiSnapshotsPull(c *gin.Context) {
// Remove all packages with the same name and architecture
packageSearchResults := toPackageList.Search(deb.Dependency{Architecture: pkg.Architecture, Pkg: pkg.Name}, true, false)
for _, p := range packageSearchResults {
if p.ArchitectureVariant != pkg.ArchitectureVariant {
continue
}
toPackageList.Remove(p)
removedPackages = append(removedPackages, p.String())
}
+138
View File
@@ -0,0 +1,138 @@
package api
import (
"bytes"
"encoding/json"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"testing"
ctx "github.com/aptly-dev/aptly/context"
"github.com/aptly-dev/aptly/deb"
"github.com/aptly-dev/aptly/utils"
"github.com/smira/flag"
)
// Exercise the real pull entry point and persisted references, without package
// files or mirrors. Architecture selection remains base-architecture selection.
func TestSnapshotPullArchitectureVariant(t *testing.T) {
normal := &deb.Package{Name: "pull-test", Version: "1", Architecture: "amd64"}
variant := &deb.Package{Name: "pull-test", Version: "1", Architecture: "amd64", ArchitectureVariant: "amd64v3"}
normal2 := &deb.Package{Name: "pull-test", Version: "2", Architecture: "amd64"}
variant2 := &deb.Package{Name: "pull-test", Version: "2", Architecture: "amd64", ArchitectureVariant: "amd64v3"}
cases := []struct {
name string
dest, source, want []*deb.Package
noRemove, allMatches bool
}{
{"normal", nil, []*deb.Package{normal}, []*deb.Package{normal}, false, false},
{"variant", nil, []*deb.Package{variant}, []*deb.Package{variant}, false, false},
{"both", nil, []*deb.Package{normal, variant}, []*deb.Package{normal, variant}, false, false},
{"variant_into_normal", []*deb.Package{normal}, []*deb.Package{variant}, []*deb.Package{normal, variant}, false, false},
{"normal_into_variant", []*deb.Package{variant}, []*deb.Package{normal}, []*deb.Package{normal, variant}, false, false},
{"both_all_matches", nil, []*deb.Package{normal, variant}, []*deb.Package{normal, variant}, false, true},
{"both_no_remove", nil, []*deb.Package{normal, variant}, []*deb.Package{normal, variant}, true, false},
{"variant_into_normal_no_remove", []*deb.Package{normal}, []*deb.Package{variant}, []*deb.Package{normal, variant}, true, false},
{"normal_into_variant_no_remove", []*deb.Package{variant}, []*deb.Package{normal}, []*deb.Package{normal, variant}, true, false},
{"variant_into_normal_all_matches", []*deb.Package{normal}, []*deb.Package{variant}, []*deb.Package{normal, variant}, false, true},
{"normal_into_variant_all_matches", []*deb.Package{variant}, []*deb.Package{normal}, []*deb.Package{normal, variant}, false, true},
{"normal_replace", []*deb.Package{normal}, []*deb.Package{normal2}, []*deb.Package{normal2}, false, false},
{"normal_no_remove", []*deb.Package{normal}, []*deb.Package{normal2}, []*deb.Package{normal, normal2}, true, false},
{"normal_latest", nil, []*deb.Package{normal, normal2}, []*deb.Package{normal2}, false, false},
{"normal_all_matches", nil, []*deb.Package{normal, normal2}, []*deb.Package{normal, normal2}, false, true},
{"variant_replace_preserves_normal", []*deb.Package{normal, variant}, []*deb.Package{variant2}, []*deb.Package{normal, variant2}, false, false},
{"latest_per_identity", nil, []*deb.Package{normal, normal2, variant, variant2}, []*deb.Package{normal2, variant2}, false, false},
{"all_versions_both_identities", nil, []*deb.Package{normal, normal2, variant, variant2}, []*deb.Package{normal, normal2, variant, variant2}, false, true},
}
for _, tc := range cases {
for _, mode := range []string{"inferred", "explicit"} {
t.Run(tc.name+"/"+mode, func(t *testing.T) {
savedConfig := utils.Config
t.Cleanup(func() { utils.Config = savedConfig })
dir := t.TempDir()
configPath := filepath.Join(dir, "aptly.conf")
configData, err := json.Marshal(map[string]interface{}{"rootDir": dir, "architectures": []string{}})
if err != nil {
t.Fatal(err)
}
if err = os.WriteFile(configPath, configData, 0600); err != nil {
t.Fatal(err)
}
flags := flag.NewFlagSet("pull-test", flag.ContinueOnError)
flags.String("config", configPath, "")
// Empty destinations need explicit architecture; populated destinations
// exercise automatic base-architecture discovery.
arch := ""
if len(tc.dest) == 0 || mode == "explicit" {
arch = "amd64"
}
flags.String("architectures", arch, "")
flags.Bool("no-lock", false, "")
flags.Int("db-open-attempts", 1, "")
flags.Bool("no-deps", true, "")
flags.Bool("no-remove", tc.noRemove, "")
flags.Bool("all-matches", tc.allMatches, "")
flags.Bool("dry-run", false, "")
testContext, err := ctx.NewContext(flags)
if err != nil {
t.Fatal(err)
}
t.Cleanup(testContext.Shutdown)
factory := testContext.NewCollectionFactory()
refs := func(packages []*deb.Package) *deb.PackageRefList {
list := deb.NewPackageList()
for _, p := range packages {
if err := list.Add(p); err != nil {
t.Fatal(err)
}
}
return deb.NewPackageRefListFromPackageList(list)
}
for _, p := range append(append([]*deb.Package{}, tc.dest...), tc.source...) {
if err := factory.PackageCollection().Update(p); err != nil {
t.Fatal(err)
}
}
for name, packages := range map[string][]*deb.Package{"base": tc.dest, "source": tc.source} {
if err := factory.SnapshotCollection().Add(deb.NewSnapshotFromRefList(name, nil, refs(packages), "")); err != nil {
t.Fatal(err)
}
}
body, err := json.Marshal(snapshotsPullParams{Source: "source", Destination: "result", Queries: []string{"pull-test"}})
if err != nil {
t.Fatal(err)
}
url := "/api/snapshots/base/pull?no-deps=1"
if tc.noRemove {
url += "&no-remove=1"
}
if tc.allMatches {
url += "&all-matches=1"
}
req := httptest.NewRequest("POST", url, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
savedContext := context
t.Cleanup(func() { context = savedContext })
Router(testContext).ServeHTTP(response, req)
if response.Code != 201 {
t.Fatalf("pull status %d: %s", response.Code, response.Body.String())
}
snapshots := testContext.NewCollectionFactory().SnapshotCollection()
result, err := snapshots.ByName("result")
if err != nil {
t.Fatal(err)
}
if err = snapshots.LoadComplete(result); err != nil {
t.Fatal(err)
}
got, want := result.RefList().Strings(), refs(tc.want).Strings()
if !reflect.DeepEqual(got, want) {
t.Errorf("persisted package identities:\n got %v\nwant %v", got, want)
}
})
}
}
}
+74
View File
@@ -1,7 +1,14 @@
package api
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
ctx "github.com/aptly-dev/aptly/context"
"github.com/aptly-dev/aptly/utils"
"github.com/smira/flag"
"github.com/aptly-dev/aptly/deb"
. "gopkg.in/check.v1"
@@ -51,3 +58,70 @@ func (s *SnapshotsSuite) TestGetSnapshotsReturns500OnCorruptRefList(c *C) {
c.Assert(response.Code, Equals, 500)
c.Assert(response.Body.String(), Matches, ".*msgpack.*|.*decode.*")
}
func (s *SnapshotsSuite) TestGetSnapshotsHandlesConcurrentDeletion(c *C) {
collection := s.context.NewCollectionFactory().SnapshotCollection()
snapshot := deb.NewSnapshotFromRefList("deleted-snapshot-list", nil, makePackageRefList(c), "")
c.Assert(collection.Add(snapshot), IsNil)
// Simulate concurrent deletion by dropping the ref key or entire snapshot record
c.Assert(collection.Drop(snapshot), IsNil)
response, err := s.HTTPRequest("GET", "/api/snapshots", nil)
c.Assert(err, IsNil)
c.Assert(response.Code, Equals, 200)
}
// Pull must load the destination references before inferring architectures or
// preserving existing packages. Both fixtures are ordinary amd64 packages.
func (*SnapshotsSuite) TestPullPreservesDestinationPackages(c *C) {
savedConfig, savedContext := utils.Config, context
defer func() { utils.Config, context = savedConfig, savedContext }()
dir := c.MkDir()
configPath := filepath.Join(dir, "aptly.conf")
configData, err := json.Marshal(map[string]interface{}{"rootDir": dir, "architectures": []string{}})
c.Assert(err, IsNil)
c.Assert(os.WriteFile(configPath, configData, 0600), IsNil)
flags := flag.NewFlagSet("pull-test", flag.ContinueOnError)
flags.String("config", configPath, "")
flags.String("architectures", "", "")
flags.Bool("no-lock", false, "")
flags.Int("db-open-attempts", 1, "")
testContext, err := ctx.NewContext(flags)
c.Assert(err, IsNil)
defer testContext.Shutdown()
s := &APISuite{context: testContext, router: Router(testContext)}
factory := s.context.NewCollectionFactory()
for _, mode := range []string{"inferred", "explicit"} {
a := &deb.Package{Name: "package-a", Version: "1", Architecture: "amd64"}
b := &deb.Package{Name: "package-b", Version: "1", Architecture: "amd64"}
for name, pkg := range map[string]*deb.Package{"base": a, "source": b} {
c.Assert(factory.PackageCollection().Update(pkg), IsNil)
list := deb.NewPackageList()
c.Assert(list.Add(pkg), IsNil)
snapshot := deb.NewSnapshotFromPackageList("pull-"+name+"-"+mode, nil, list, "")
c.Assert(factory.SnapshotCollection().Add(snapshot), IsNil)
}
params := snapshotsPullParams{
Source: "pull-source-" + mode,
Destination: "pull-result-" + mode,
Queries: []string{"package-b"},
}
if mode == "explicit" {
params.Architectures = []string{"amd64"}
}
body, err := json.Marshal(params)
c.Assert(err, IsNil)
response, err := s.HTTPRequest("POST", "/api/snapshots/pull-base-"+mode+"/pull?no-remove=1&no-deps=1", bytes.NewReader(body))
c.Assert(err, IsNil)
if !c.Check(response.Code, Equals, 201, Commentf("%s: %s", mode, response.Body.String())) {
continue
}
snapshots := s.context.NewCollectionFactory().SnapshotCollection()
result, err := snapshots.ByName(params.Destination)
c.Assert(err, IsNil)
c.Assert(snapshots.LoadComplete(result), IsNil)
c.Check(result.RefList().Strings(), DeepEquals, []string{
string(a.Key("")), string(b.Key("")),
}, Commentf("%s: preserve A and add B", mode))
}
}
+241 -11
View File
@@ -9,7 +9,9 @@ import (
"net/url"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/aptly-dev/aptly/api"
"github.com/aptly-dev/aptly/systemd/activation"
@@ -18,6 +20,214 @@ import (
"github.com/smira/flag"
)
const (
metricsReadHeaderTimeout = 5 * time.Second
httpShutdownTimeout = 30 * time.Second
)
type apiHTTPServer struct {
name string
server *http.Server
listener net.Listener
}
type apiServeResult struct {
name string
err error
}
func metricsOnlyHandler(apiHandler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.EscapedPath() != "/metrics" {
http.NotFound(w, r)
return
}
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
request := r.Clone(r.Context())
requestURL := *r.URL
requestURL.Path = "/api/metrics"
requestURL.RawPath = ""
request.URL = &requestURL
request.RequestURI = requestURL.RequestURI()
apiHandler.ServeHTTP(w, request)
})
}
func validateMetricsListener(address string, metricsEnabled bool) error {
if address != "" && !metricsEnabled {
return errors.New("-metrics-listen requires enableMetricsEndpoint to be true")
}
return nil
}
func selectActivatedAPIListener(listeners []net.Listener) (net.Listener, error) {
switch len(listeners) {
case 0:
return nil, nil
case 1:
if listeners[0] == nil {
return nil, errors.New("systemd file descriptor is not a supported network listener")
}
return listeners[0], nil
default:
return nil, fmt.Errorf("got %d listeners from systemd; only one API listener is supported", len(listeners))
}
}
func listenForAPI(address string) (net.Listener, error) {
listenURL, err := url.Parse(address)
if err == nil && listenURL.Scheme == "unix" {
file := listenURL.Path
_ = os.Remove(file)
listener, err := net.Listen("unix", file)
if err != nil {
return nil, fmt.Errorf("failed to listen on API Unix socket %q: %w", file, err)
}
return listener, nil
}
listener, err := net.Listen("tcp", address)
if err != nil {
return nil, fmt.Errorf("failed to listen on API address %q: %w", address, err)
}
return listener, nil
}
func shutdownHTTPServers(servers []apiHTTPServer, timeout time.Duration) {
shutdownContext, cancel := stdcontext.WithTimeout(stdcontext.Background(), timeout)
defer cancel()
failed := make(chan *http.Server, len(servers))
var waitGroup sync.WaitGroup
for _, httpServer := range servers {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
if err := httpServer.server.Shutdown(shutdownContext); err != nil {
failed <- httpServer.server
}
}()
}
waitGroup.Wait()
close(failed)
for server := range failed {
_ = server.Close()
}
}
func serveHTTPServers(servers []apiHTTPServer, sigchan <-chan os.Signal, restoreSignals func(), waitForTasks func(), shutdownTimeout time.Duration) error {
results := make(chan apiServeResult, len(servers))
for _, httpServer := range servers {
go func() {
results <- apiServeResult{
name: httpServer.name,
err: httpServer.server.Serve(httpServer.listener),
}
}()
}
serveResults := make([]apiServeResult, 0, len(servers))
shutdownFromSignal := false
select {
case result := <-results:
serveResults = append(serveResults, result)
case <-sigchan:
shutdownFromSignal = true
}
if restoreSignals != nil {
restoreSignals()
}
if shutdownFromSignal {
fmt.Printf("\nShutdown signal received, stopping HTTP servers...\n")
} else {
fmt.Printf("\nHTTP server stopped unexpectedly, stopping sibling server...\n")
}
shutdownHTTPServers(servers, shutdownTimeout)
for len(serveResults) < len(servers) {
serveResults = append(serveResults, <-results)
}
fmt.Printf("Waiting for background tasks...\n")
waitForTasks()
if shutdownFromSignal {
for _, result := range serveResults {
if result.err != nil && !errors.Is(result.err, http.ErrServerClosed) {
return fmt.Errorf("%s server failed during shutdown: %w", result.name, result.err)
}
}
return nil
}
result := serveResults[0]
if result.err == nil {
return fmt.Errorf("%s server stopped unexpectedly", result.name)
}
return fmt.Errorf("%s server stopped unexpectedly: %w", result.name, result.err)
}
func serveAPIWithMetrics(apiListener net.Listener, activated bool, apiAddress, metricsAddress string) error {
var err error
if apiListener == nil {
apiListener, err = listenForAPI(apiAddress)
if err != nil {
return err
}
}
defer func() { _ = apiListener.Close() }()
metricsListener, err := net.Listen("tcp", metricsAddress)
if err != nil {
return fmt.Errorf("failed to listen on metrics address %q: %w", metricsAddress, err)
}
defer func() { _ = metricsListener.Close() }()
apiHandler := api.Router(context)
servers := []apiHTTPServer{
{
name: "API",
server: &http.Server{Handler: apiHandler},
listener: apiListener,
},
{
name: "metrics",
server: &http.Server{
Handler: metricsOnlyHandler(apiHandler),
ReadHeaderTimeout: metricsReadHeaderTimeout,
},
listener: metricsListener,
},
}
if activated {
fmt.Printf("\nTaking over API web server at: %s (press Ctrl+C to quit)...\n", apiListener.Addr().String())
} else {
fmt.Printf("\nStarting API web server at: %s (press Ctrl+C to quit)...\n", apiListener.Addr().String())
}
fmt.Printf("Starting metrics web server at: %s...\n", metricsListener.Addr().String())
sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(sigchan)
restoreSignals := func() {
signal.Reset(syscall.SIGINT, syscall.SIGTERM)
}
return serveHTTPServers(servers, sigchan, restoreSignals, context.TaskList().Wait, httpShutdownTimeout)
}
func aptlyAPIServe(cmd *commander.Command, args []string) error {
var (
err error
@@ -39,24 +249,37 @@ func aptlyAPIServe(cmd *commander.Command, args []string) error {
return err
}
metricsListen := context.Flags().Lookup("metrics-listen").Value.String()
if err = validateMetricsListener(metricsListen, context.Config().EnableMetricsEndpoint); err != nil {
return err
}
// Try to recycle systemd fds for listening
listeners, err := activation.Listeners(true)
if len(listeners) > 1 {
panic("Got more than 1 listener from systemd. This is currently not supported!")
}
if err == nil && len(listeners) == 1 {
listener := listeners[0]
defer func() { _ = listener.Close() }()
fmt.Printf("\nTaking over web server at: %s (press Ctrl+C to quit)...\n", listener.Addr().String())
err = http.Serve(listener, api.Router(context))
if err != nil {
return fmt.Errorf("unable to serve: %s", err)
if err == nil {
listener, listenerErr := selectActivatedAPIListener(listeners)
if listenerErr != nil {
return listenerErr
}
if listener != nil && metricsListen != "" {
return serveAPIWithMetrics(listener, true, "", metricsListen)
}
if listener != nil {
defer func() { _ = listener.Close() }()
fmt.Printf("\nTaking over web server at: %s (press Ctrl+C to quit)...\n", listener.Addr().String())
err = http.Serve(listener, api.Router(context))
if err != nil {
return fmt.Errorf("unable to serve: %s", err)
}
return nil
}
return nil
}
// If there are none: use the listen argument.
listen := context.Flags().Lookup("listen").Value.String()
if metricsListen != "" {
return serveAPIWithMetrics(nil, false, listen, metricsListen)
}
fmt.Printf("\nStarting web server at: %s (press Ctrl+C to quit)...\n", listen)
server := http.Server{Handler: api.Router(context)}
@@ -108,15 +331,22 @@ or Unix domain socket. When using a socket, Aptly will fully manage the socket
file. This command also supports taking over from a systemd file descriptors to
enable systemd socket activation.
An optional metrics-only TCP listener can be enabled with -metrics-listen. It
serves GET and HEAD requests at /metrics and requires enableMetricsEndpoint in
the aptly configuration. This listener uses plain HTTP; provide access control
and TLS through network policy or a proxy when required.
Example:
$ aptly api serve -listen=:8080
$ aptly api serve -listen=unix:///tmp/aptly.sock
$ aptly api serve -listen=:8080 -metrics-listen=127.0.0.1:9090
`,
Flag: *flag.NewFlagSet("aptly-serve", flag.ExitOnError),
}
cmd.Flag.String("listen", ":8080", "host:port for HTTP listening or unix://path to listen on a Unix domain socket")
cmd.Flag.String("metrics-listen", "", "host:port for optional metrics-only HTTP listening (requires enableMetricsEndpoint)")
cmd.Flag.Bool("no-lock", false, "don't lock the database")
return cmd
+422
View File
@@ -0,0 +1,422 @@
package cmd
import (
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"strings"
"sync"
"syscall"
"testing"
"time"
check "gopkg.in/check.v1"
)
func Test(t *testing.T) {
check.TestingT(t)
}
func TestMetricsOnlyHandler(t *testing.T) {
tests := []struct {
name string
method string
target string
wantStatus int
wantForwarded bool
wantQuery string
wantRequestURI string
wantAllowMethods string
}{
{
name: "get",
method: http.MethodGet,
target: "/metrics?_async=true&value=a%2Bb",
wantStatus: http.StatusNoContent,
wantForwarded: true,
wantQuery: "_async=true&value=a%2Bb",
wantRequestURI: "/api/metrics?_async=true&value=a%2Bb",
},
{
name: "head",
method: http.MethodHead,
target: "/metrics",
wantStatus: http.StatusNoContent,
wantForwarded: true,
wantRequestURI: "/api/metrics",
},
{
name: "api metrics path",
method: http.MethodGet,
target: "/api/metrics",
wantStatus: http.StatusNotFound,
},
{
name: "trailing slash",
method: http.MethodGet,
target: "/metrics/",
wantStatus: http.StatusNotFound,
},
{
name: "encoded alias",
method: http.MethodGet,
target: "/met%72ics",
wantStatus: http.StatusNotFound,
},
{
name: "case variant",
method: http.MethodGet,
target: "/METRICS",
wantStatus: http.StatusNotFound,
},
{
name: "unsupported method",
method: http.MethodPost,
target: "/metrics",
wantStatus: http.StatusMethodNotAllowed,
wantAllowMethods: "GET, HEAD",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
forwarded := false
var forwardedMethod, forwardedPath, forwardedQuery, forwardedRequestURI string
apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
forwarded = true
forwardedMethod = r.Method
forwardedPath = r.URL.Path
forwardedQuery = r.URL.RawQuery
forwardedRequestURI = r.RequestURI
w.WriteHeader(http.StatusNoContent)
})
request := httptest.NewRequest(test.method, test.target, nil)
originalPath := request.URL.Path
originalRawPath := request.URL.RawPath
originalRequestURI := request.RequestURI
response := httptest.NewRecorder()
metricsOnlyHandler(apiHandler).ServeHTTP(response, request)
if response.Code != test.wantStatus {
t.Fatalf("status = %d, want %d", response.Code, test.wantStatus)
}
if forwarded != test.wantForwarded {
t.Fatalf("forwarded = %t, want %t", forwarded, test.wantForwarded)
}
if got := response.Header().Get("Allow"); got != test.wantAllowMethods {
t.Errorf("Allow = %q, want %q", got, test.wantAllowMethods)
}
if request.URL.Path != originalPath || request.URL.RawPath != originalRawPath || request.RequestURI != originalRequestURI {
t.Errorf("original request was mutated")
}
if test.wantForwarded {
if forwardedMethod != test.method {
t.Errorf("forwarded method = %q, want %q", forwardedMethod, test.method)
}
if forwardedPath != "/api/metrics" {
t.Errorf("forwarded path = %q, want /api/metrics", forwardedPath)
}
if forwardedQuery != test.wantQuery {
t.Errorf("forwarded query = %q, want %q", forwardedQuery, test.wantQuery)
}
if forwardedRequestURI != test.wantRequestURI {
t.Errorf("forwarded request URI = %q, want %q", forwardedRequestURI, test.wantRequestURI)
}
}
})
}
}
func TestValidateMetricsListener(t *testing.T) {
for _, test := range []struct {
name string
address string
metricsEnabled bool
wantError bool
}{
{name: "disabled listener", metricsEnabled: false},
{name: "existing API metrics only", metricsEnabled: true},
{name: "enabled listener", address: "127.0.0.1:9090", metricsEnabled: true},
{name: "listener requires metrics", address: "127.0.0.1:9090", metricsEnabled: false, wantError: true},
} {
t.Run(test.name, func(t *testing.T) {
err := validateMetricsListener(test.address, test.metricsEnabled)
if (err != nil) != test.wantError {
t.Fatalf("error = %v, wantError %t", err, test.wantError)
}
})
}
}
func TestSelectActivatedAPIListener(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = listener.Close() })
selected, err := selectActivatedAPIListener(nil)
if err != nil || selected != nil {
t.Fatalf("no listeners: selected = %v, error = %v", selected, err)
}
selected, err = selectActivatedAPIListener([]net.Listener{listener})
if err != nil || selected != listener {
t.Fatalf("one listener: selected = %v, error = %v", selected, err)
}
if _, err = selectActivatedAPIListener([]net.Listener{nil}); err == nil {
t.Fatal("nil listener did not return an error")
}
if _, err = selectActivatedAPIListener([]net.Listener{listener, listener}); err == nil {
t.Fatal("multiple listeners did not return an error")
}
}
func TestServeAPIWithMetricsClosesAPIListenerOnMetricsBindFailure(t *testing.T) {
apiListener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = apiListener.Close() })
apiAddress := apiListener.Addr().String()
occupiedMetricsListener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
_ = apiListener.Close()
t.Fatal(err)
}
defer func() { _ = occupiedMetricsListener.Close() }()
err = serveAPIWithMetrics(apiListener, false, "", occupiedMetricsListener.Addr().String())
if err == nil || !strings.Contains(err.Error(), "failed to listen on metrics address") {
t.Fatalf("error = %v, want metrics bind failure", err)
}
connection, dialErr := net.DialTimeout("tcp", apiAddress, 100*time.Millisecond)
if dialErr == nil {
_ = connection.Close()
t.Fatal("API listener still accepts connections after metrics bind failure")
}
}
func TestShutdownHTTPServersForcesCloseAfterTimeout(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
requestStarted := make(chan struct{})
releaseRequest := make(chan struct{})
var releaseOnce sync.Once
t.Cleanup(func() {
releaseOnce.Do(func() { close(releaseRequest) })
})
server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
close(requestStarted)
<-releaseRequest
w.WriteHeader(http.StatusOK)
})}
serveResult := make(chan error, 1)
go func() { serveResult <- server.Serve(listener) }()
clientResult := make(chan error, 1)
go func() {
response, requestErr := http.Get("http://" + listener.Addr().String())
if response != nil {
_ = response.Body.Close()
}
clientResult <- requestErr
}()
select {
case <-requestStarted:
case <-time.After(time.Second):
t.Fatal("request did not reach server")
}
started := time.Now()
shutdownHTTPServers([]apiHTTPServer{{name: "test", server: server, listener: listener}}, 50*time.Millisecond)
if elapsed := time.Since(started); elapsed < 40*time.Millisecond {
t.Fatalf("shutdown closed an active request immediately after %s", elapsed)
}
if elapsed := time.Since(started); elapsed > time.Second {
t.Fatalf("forced shutdown took %s", elapsed)
}
select {
case requestErr := <-clientResult:
if requestErr == nil {
t.Error("client request unexpectedly completed during forced shutdown")
}
case <-time.After(time.Second):
t.Fatal("client connection was not closed")
}
select {
case serveErr := <-serveResult:
if serveErr != http.ErrServerClosed {
t.Errorf("Serve error = %v, want %v", serveErr, http.ErrServerClosed)
}
case <-time.After(time.Second):
t.Fatal("Serve did not return")
}
releaseOnce.Do(func() { close(releaseRequest) })
}
func TestServeHTTPServersStopsSiblingOnFailure(t *testing.T) {
apiListener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
metricsListener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
_ = apiListener.Close()
t.Fatal(err)
}
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
servers := []apiHTTPServer{
{name: "API", server: &http.Server{Handler: handler}, listener: apiListener},
{name: "metrics", server: &http.Server{Handler: handler}, listener: metricsListener},
}
for _, server := range servers {
server := server
t.Cleanup(func() {
_ = server.server.Close()
_ = server.listener.Close()
})
}
sigchan := make(chan os.Signal)
signalsRestored := make(chan struct{})
tasksWaited := make(chan struct{})
result := make(chan error, 1)
go func() {
result <- serveHTTPServers(
servers,
sigchan,
func() { close(signalsRestored) },
func() { close(tasksWaited) },
time.Second,
)
}()
waitForHTTP(t, apiListener.Addr().String())
waitForHTTP(t, metricsListener.Addr().String())
if err = apiListener.Close(); err != nil {
t.Fatal(err)
}
select {
case err = <-result:
if err == nil || !strings.Contains(err.Error(), "API server stopped unexpectedly") {
t.Fatalf("error = %v, want unexpected API server failure", err)
}
case <-time.After(2 * time.Second):
t.Fatal("server coordinator did not return")
}
select {
case <-signalsRestored:
default:
t.Error("signal handling was not restored")
}
select {
case <-tasksWaited:
default:
t.Error("background tasks were not awaited")
}
connection, dialErr := net.DialTimeout("tcp", metricsListener.Addr().String(), 100*time.Millisecond)
if dialErr == nil {
_ = connection.Close()
t.Error("metrics sibling still accepts connections")
}
}
func TestServeHTTPServersShutsDownOnSignal(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
})}
t.Cleanup(func() {
_ = server.Close()
_ = listener.Close()
})
sigchan := make(chan os.Signal, 1)
signalsRestored := make(chan struct{})
tasksWaitedAfterShutdown := make(chan bool, 1)
result := make(chan error, 1)
go func() {
result <- serveHTTPServers(
[]apiHTTPServer{{name: "API", server: server, listener: listener}},
sigchan,
func() { close(signalsRestored) },
func() {
connection, dialErr := net.DialTimeout("tcp", listener.Addr().String(), 100*time.Millisecond)
if dialErr == nil {
_ = connection.Close()
}
tasksWaitedAfterShutdown <- dialErr != nil
},
time.Second,
)
}()
waitForHTTP(t, listener.Addr().String())
sigchan <- syscall.SIGTERM
select {
case err = <-result:
if err != nil {
t.Fatalf("signal shutdown returned error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("server coordinator did not return")
}
select {
case <-signalsRestored:
default:
t.Error("signal handling was not restored")
}
select {
case listenerClosed := <-tasksWaitedAfterShutdown:
if !listenerClosed {
t.Error("background tasks were awaited before HTTP shutdown")
}
case <-time.After(time.Second):
t.Error("background tasks were not awaited")
}
}
func waitForHTTP(t *testing.T, address string) {
t.Helper()
client := &http.Client{Timeout: 100 * time.Millisecond}
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
response, err := client.Get("http://" + address)
if err == nil {
_, _ = io.Copy(io.Discard, response.Body)
_ = response.Body.Close()
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatalf("HTTP server at %s did not become ready", address)
}
+24 -3
View File
@@ -23,6 +23,14 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
name := args[0]
maxVersions := context.Flags().Lookup("max-versions").Value.Get().(int)
if maxVersions < 0 {
return fmt.Errorf("unable to update: -max-versions should be a positive number")
}
if maxVersions == 0 && context.Flags().Lookup("latest").Value.Get().(bool) {
maxVersions = 1
}
collectionFactory := context.NewCollectionFactory()
repo, err := collectionFactory.RemoteRepoCollection().ByName(name)
if err != nil {
@@ -90,17 +98,29 @@ func aptlyMirrorUpdate(cmd *commander.Command, args []string) error {
context.Progress().Printf("Packages filtered: %d -> %d.\n", oldLen, newLen)
}
if maxVersions > 0 {
var oldLen, newLen int
oldLen, newLen, err = repo.FilterLatest(maxVersions)
if err != nil {
return fmt.Errorf("unable to update: %s", err)
}
versionWord := "versions"
if maxVersions == 1 {
versionWord = "version"
}
context.Progress().Printf("Keeping at most %d %s of each package: %d -> %d.\n", maxVersions, versionWord, oldLen, newLen)
}
var (
downloadSize int64
queue []deb.PackageDownloadTask
)
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, latestOnly)
collectionFactory.ChecksumCollection(nil), skipExistingPackages)
if err != nil {
return fmt.Errorf("unable to update: %s", err)
@@ -302,7 +322,8 @@ 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.Bool("latest", false, "download only latest version of each package (per architecture), same as -max-versions=1")
cmd.Flag.Int("max-versions", 0, "download only the N most recent versions of each package (per architecture), 0 means all versions")
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")
+3
View File
@@ -178,6 +178,9 @@ func aptlyPublishSnapshotOrRepo(cmd *commander.Command, args []string) error {
context.Progress().ColoredPrintf("@rWARNING@|: force overwrite mode enabled, aptly might corrupt other published repositories sharing the same package pool.\n")
}
// Which hash algorithms get Acquire-By-Hash entries. Each one is a full
// set of index copies per publish, which is felt most on S3-like storage.
published.AcquireByHashAlgorithms = context.Config().AcquireByHashAlgorithms
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, context.Progress(), forceOverwrite, context.SkelPath())
if err != nil {
return fmt.Errorf("unable to publish: %s", err)
+3
View File
@@ -111,6 +111,9 @@ func aptlyPublishSwitch(cmd *commander.Command, args []string) error {
published.MultiDist = context.Flags().Lookup("multi-dist").Value.Get().(bool)
}
// Which hash algorithms get Acquire-By-Hash entries. Each one is a full
// set of index copies per publish, which is felt most on S3-like storage.
published.AcquireByHashAlgorithms = context.Config().AcquireByHashAlgorithms
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, context.Progress(), forceOverwrite, context.SkelPath())
if err != nil {
return fmt.Errorf("unable to publish: %s", err)
+3
View File
@@ -80,6 +80,9 @@ func aptlyPublishUpdate(cmd *commander.Command, args []string) error {
published.Version = context.Flags().Lookup("version").Value.String()
}
// Which hash algorithms get Acquire-By-Hash entries. Each one is a full
// set of index copies per publish, which is felt most on S3-like storage.
published.AcquireByHashAlgorithms = context.Config().AcquireByHashAlgorithms
err = published.Publish(context.PackagePool(), context, collectionFactory, signer, context.Progress(), forceOverwrite, context.SkelPath())
if err != nil {
return fmt.Errorf("unable to publish: %s", err)
+151
View File
@@ -0,0 +1,151 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
ctx "github.com/aptly-dev/aptly/context"
"github.com/aptly-dev/aptly/deb"
"github.com/aptly-dev/aptly/utils"
"github.com/smira/flag"
"gopkg.in/check.v1"
)
type RepoAddVariantSuite struct{}
var _ = check.Suite(&RepoAddVariantSuite{})
// Every fixture has the same uninformative basename. Architecture-Variant is
// supplied exclusively by control metadata, and rebuilt packages change payload.
func (*RepoAddVariantSuite) TestRepoAddArchitectureVariant(c *check.C) {
builder, err := exec.LookPath("dpkg-deb")
c.Assert(err, check.IsNil, check.Commentf("these real .deb import tests require dpkg-deb"))
type fixture struct {
path string
pkg *deb.Package
data []byte
}
fixtures := map[string]fixture{}
for _, spec := range []struct{ id, variant, version, payload string }{
{"normal", "", "1", "original"}, {"variant", "amd64v3", "1", "original"},
{"normal-rebuilt", "", "1", "rebuilt"}, {"variant-rebuilt", "amd64v3", "1", "rebuilt"},
{"normal-v2", "", "2", "new version"}, {"variant-v2", "amd64v3", "2", "new version"},
} {
dir := c.MkDir()
root := filepath.Join(dir, "root")
c.Assert(os.MkdirAll(filepath.Join(root, "DEBIAN"), 0755), check.IsNil)
control := fmt.Sprintf("Package: import-test\nVersion: %s\nArchitecture: amd64\nMaintainer: Test <test@example.invalid>\nDescription: tiny import fixture\n", spec.version)
if spec.variant != "" {
control += "Architecture-Variant: " + spec.variant + "\n"
}
c.Assert(os.WriteFile(filepath.Join(root, "DEBIAN", "control"), []byte(control), 0644), check.IsNil)
c.Assert(os.WriteFile(filepath.Join(root, "payload"), []byte(spec.payload), 0644), check.IsNil)
path := filepath.Join(dir, "payload.deb")
output, err := exec.Command(builder, "--build", "--root-owner-group", root, path).CombinedOutput()
c.Assert(err, check.IsNil, check.Commentf("build fixture: %s", output))
checksums, err := utils.ChecksumsForFile(path)
c.Assert(err, check.IsNil)
pkg := &deb.Package{Name: "import-test", Version: spec.version, Architecture: "amd64", ArchitectureVariant: spec.variant, V06Plus: true}
pkg.UpdateFiles(deb.PackageFiles{{Filename: "payload.deb", Checksums: checksums}})
data, err := os.ReadFile(path)
c.Assert(err, check.IsNil)
fixtures[spec.id] = fixture{path, pkg, data}
}
for _, tc := range []struct {
name string
initial []string
incoming string
force, reject bool
want []string
}{
{"normal_empty", nil, "normal", false, false, []string{"normal"}},
{"variant_empty", nil, "variant", false, false, []string{"variant"}},
{"normal_then_variant", []string{"normal"}, "variant", false, false, []string{"normal", "variant"}},
{"variant_then_normal", []string{"variant"}, "normal", false, false, []string{"normal", "variant"}},
{"identical_normal", []string{"normal", "variant"}, "normal", false, false, []string{"normal", "variant"}},
{"identical_variant", []string{"normal", "variant"}, "variant", false, false, []string{"normal", "variant"}},
{"conflicting_normal_without_force", []string{"normal", "variant"}, "normal-rebuilt", false, true, []string{"normal", "variant"}},
{"conflicting_variant_without_force", []string{"normal", "variant"}, "variant-rebuilt", false, true, []string{"normal", "variant"}},
{"force_normal_preserves_variant", []string{"normal", "variant"}, "normal-rebuilt", true, false, []string{"normal-rebuilt", "variant"}},
{"force_variant_preserves_normal", []string{"normal", "variant"}, "variant-rebuilt", true, false, []string{"normal", "variant-rebuilt"}},
{"force_identical_normal", []string{"normal", "variant"}, "normal", true, false, []string{"normal", "variant"}},
{"force_identical_variant", []string{"normal", "variant"}, "variant", true, false, []string{"normal", "variant"}},
{"force_new_variant_family", []string{"normal"}, "variant", true, false, []string{"normal", "variant"}},
{"force_new_normal_family", []string{"variant"}, "normal", true, false, []string{"normal", "variant"}},
{"force_normal_only", []string{"normal"}, "normal-rebuilt", true, false, []string{"normal-rebuilt"}},
{"force_variant_only", []string{"variant"}, "variant-rebuilt", true, false, []string{"variant-rebuilt"}},
{"force_normal_new_version", []string{"normal", "variant"}, "normal-v2", true, false, []string{"normal", "variant", "normal-v2"}},
{"force_variant_new_version", []string{"normal", "variant"}, "variant-v2", true, false, []string{"normal", "variant", "variant-v2"}},
} {
c.Logf("case: %s", tc.name)
func() {
savedConfig, savedContext := utils.Config, context
defer func() { utils.Config, context = savedConfig, savedContext }()
dir := c.MkDir()
configPath := filepath.Join(dir, "aptly.conf")
config, err := json.Marshal(map[string]interface{}{"rootDir": dir, "gpgProvider": "internal"})
c.Assert(err, check.IsNil)
c.Assert(os.WriteFile(configPath, config, 0600), check.IsNil)
flags := flag.NewFlagSet("repo-add-test", flag.ContinueOnError)
flags.String("config", configPath, "")
flags.Bool("no-lock", false, "")
flags.Int("db-open-attempts", 1, "")
flags.Bool("force-replace", false, "")
flags.Bool("remove-files", false, "")
testContext, err := ctx.NewContext(flags)
c.Assert(err, check.IsNil)
context = testContext
defer testContext.Shutdown()
factory := testContext.NewCollectionFactory()
c.Assert(factory.LocalRepoCollection().Add(deb.NewLocalRepo("test", "")), check.IsNil)
add := func(id string) error { return aptlyRepoAdd(makeCmdRepoAdd(), []string{"test", fixtures[id].path}) }
for _, id := range tc.initial {
c.Assert(add(id), check.IsNil)
}
c.Assert(flags.Set("force-replace", fmt.Sprint(tc.force)), check.IsNil)
err = add(tc.incoming)
c.Assert(err != nil, check.Equals, tc.reject, check.Commentf("case %s: repo add error: %v", tc.name, err))
// Reload persisted references and metadata rather than checking an in-memory list.
factory = testContext.NewCollectionFactory()
repo, err := factory.LocalRepoCollection().ByName("test")
c.Assert(err, check.IsNil)
c.Assert(factory.LocalRepoCollection().LoadComplete(repo), check.IsNil)
got := map[string]string{}
for _, key := range repo.RefList().Refs {
p, err := factory.PackageCollection().ByKey(key)
c.Assert(err, check.IsNil)
c.Check(p.Architecture, check.Equals, "amd64", check.Commentf("case %s", tc.name))
c.Assert(p.Files(), check.HasLen, 1)
got[string(p.ShortKey(""))] = p.Files()[0].Checksums.SHA256
}
want := map[string]string{}
for _, id := range tc.want {
p := fixtures[id].pkg
want[string(p.ShortKey(""))] = p.Files()[0].Checksums.SHA256
}
c.Check(got, check.DeepEquals, want, check.Commentf("case %s: persisted package identities", tc.name))
// Import stores files and full DB records before resolving repo conflicts.
// Neither rejection nor replacement should corrupt/remove these pool blobs.
for _, id := range append(append([]string{}, tc.initial...), tc.incoming) {
f := fixtures[id]
p, err := factory.PackageCollection().ByKey(f.pkg.Key(""))
c.Assert(err, check.IsNil)
c.Check(p.ArchitectureVariant, check.Equals, f.pkg.ArchitectureVariant, check.Commentf("case %s", tc.name))
stream, err := testContext.PackagePool().Open(p.Files()[0].PoolPath)
c.Assert(err, check.IsNil)
data, err := io.ReadAll(stream)
closeErr := stream.Close()
c.Assert(err, check.IsNil)
c.Assert(closeErr, check.IsNil)
c.Check(data, check.DeepEquals, f.data, check.Commentf("case %s: pool content for %s", tc.name, id))
_, err = os.Stat(f.path)
c.Check(err, check.IsNil, check.Commentf("case %s: input file %s", tc.name, f.path))
}
}()
}
}
+2 -2
View File
@@ -60,10 +60,10 @@ func aptlySnapshotDiff(cmd *commander.Command, args []string) error {
verA = "-"
verB = pdiff.Right.Version
pkg = pdiff.Right.Name
arch = pdiff.Right.Architecture
arch = pdiff.Right.IndexArchitecture()
} else {
pkg = pdiff.Left.Name
arch = pdiff.Left.Architecture
arch = pdiff.Left.IndexArchitecture()
verA = pdiff.Left.Version
if pdiff.Right == nil {
verB = "-"
+4 -1
View File
@@ -117,7 +117,7 @@ func aptlySnapshotPull(cmd *commander.Command, args []string) error {
alreadySeen := map[string]bool{}
_ = result.ForEachIndexed(func(pkg *deb.Package) error {
key := pkg.Architecture + "_" + pkg.Name
key := pkg.IndexArchitecture() + "_" + pkg.Name
_, seen := alreadySeen[key]
// If we haven't seen such name-architecture pair and were instructed to remove, remove it
@@ -125,6 +125,9 @@ func aptlySnapshotPull(cmd *commander.Command, args []string) error {
// Remove all packages with the same name and architecture
pS := packageList.Search(deb.Dependency{Architecture: pkg.Architecture, Pkg: pkg.Name}, true, false)
for _, p := range pS {
if p.ArchitectureVariant != pkg.ArchitectureVariant {
continue
}
packageList.Remove(p)
context.Progress().ColoredPrintf("@r[-]@| %s removed", p)
}
+105
View File
@@ -0,0 +1,105 @@
package cmd
import (
"encoding/json"
"os"
"path/filepath"
ctx "github.com/aptly-dev/aptly/context"
"github.com/aptly-dev/aptly/deb"
"github.com/aptly-dev/aptly/utils"
"github.com/smira/flag"
"gopkg.in/check.v1"
)
type SnapshotPullVariantSuite struct{}
var _ = check.Suite(&SnapshotPullVariantSuite{})
// Exercise the real pull entry point and persisted references, without package
// files or mirrors. Architecture selection remains base-architecture selection.
func (*SnapshotPullVariantSuite) TestSnapshotPullArchitectureVariant(c *check.C) {
normal := &deb.Package{Name: "pull-test", Version: "1", Architecture: "amd64"}
variant := &deb.Package{Name: "pull-test", Version: "1", Architecture: "amd64", ArchitectureVariant: "amd64v3"}
normal2 := &deb.Package{Name: "pull-test", Version: "2", Architecture: "amd64"}
variant2 := &deb.Package{Name: "pull-test", Version: "2", Architecture: "amd64", ArchitectureVariant: "amd64v3"}
cases := []struct {
name string
dest, source, want []*deb.Package
noRemove, allMatches bool
}{
{"normal", nil, []*deb.Package{normal}, []*deb.Package{normal}, false, false},
{"variant", nil, []*deb.Package{variant}, []*deb.Package{variant}, false, false},
{"both", nil, []*deb.Package{normal, variant}, []*deb.Package{normal, variant}, false, false},
{"variant_into_normal", []*deb.Package{normal}, []*deb.Package{variant}, []*deb.Package{normal, variant}, false, false},
{"normal_into_variant", []*deb.Package{variant}, []*deb.Package{normal}, []*deb.Package{normal, variant}, false, false},
{"both_all_matches", nil, []*deb.Package{normal, variant}, []*deb.Package{normal, variant}, false, true},
{"both_no_remove", nil, []*deb.Package{normal, variant}, []*deb.Package{normal, variant}, true, false},
{"variant_into_normal_no_remove", []*deb.Package{normal}, []*deb.Package{variant}, []*deb.Package{normal, variant}, true, false},
{"normal_into_variant_no_remove", []*deb.Package{variant}, []*deb.Package{normal}, []*deb.Package{normal, variant}, true, false},
{"variant_into_normal_all_matches", []*deb.Package{normal}, []*deb.Package{variant}, []*deb.Package{normal, variant}, false, true},
{"normal_into_variant_all_matches", []*deb.Package{variant}, []*deb.Package{normal}, []*deb.Package{normal, variant}, false, true},
{"normal_replace", []*deb.Package{normal}, []*deb.Package{normal2}, []*deb.Package{normal2}, false, false},
{"normal_no_remove", []*deb.Package{normal}, []*deb.Package{normal2}, []*deb.Package{normal, normal2}, true, false},
{"normal_latest", nil, []*deb.Package{normal, normal2}, []*deb.Package{normal2}, false, false},
{"normal_all_matches", nil, []*deb.Package{normal, normal2}, []*deb.Package{normal, normal2}, false, true},
{"variant_replace_preserves_normal", []*deb.Package{normal, variant}, []*deb.Package{variant2}, []*deb.Package{normal, variant2}, false, false},
{"latest_per_identity", nil, []*deb.Package{normal, normal2, variant, variant2}, []*deb.Package{normal2, variant2}, false, false},
{"all_versions_both_identities", nil, []*deb.Package{normal, normal2, variant, variant2}, []*deb.Package{normal, normal2, variant, variant2}, false, true},
}
for _, tc := range cases {
c.Logf("case: %s", tc.name)
func() {
savedConfig := utils.Config
defer func() { utils.Config = savedConfig }()
dir := c.MkDir()
configPath := filepath.Join(dir, "aptly.conf")
configData, err := json.Marshal(map[string]interface{}{"rootDir": dir, "architectures": []string{}})
c.Assert(err, check.IsNil)
c.Assert(os.WriteFile(configPath, configData, 0600), check.IsNil)
flags := flag.NewFlagSet("pull-test", flag.ContinueOnError)
flags.String("config", configPath, "")
// Empty destinations need explicit architecture; populated destinations
// exercise automatic base-architecture discovery.
arch := ""
if len(tc.dest) == 0 {
arch = "amd64"
}
flags.String("architectures", arch, "")
flags.Bool("no-lock", false, "")
flags.Int("db-open-attempts", 1, "")
flags.Bool("no-deps", true, "")
flags.Bool("no-remove", tc.noRemove, "")
flags.Bool("all-matches", tc.allMatches, "")
flags.Bool("dry-run", false, "")
testContext, err := ctx.NewContext(flags)
c.Assert(err, check.IsNil)
defer testContext.Shutdown()
factory := testContext.NewCollectionFactory()
refs := func(packages []*deb.Package) *deb.PackageRefList {
list := deb.NewPackageList()
for _, p := range packages {
c.Assert(list.Add(p), check.IsNil)
}
return deb.NewPackageRefListFromPackageList(list)
}
for _, p := range append(append([]*deb.Package{}, tc.dest...), tc.source...) {
c.Assert(factory.PackageCollection().Update(p), check.IsNil)
}
for name, packages := range map[string][]*deb.Package{"base": tc.dest, "source": tc.source} {
c.Assert(factory.SnapshotCollection().Add(deb.NewSnapshotFromRefList(name, nil, refs(packages), "")), check.IsNil)
}
savedContext := context
context = testContext
defer func() { context = savedContext }()
c.Assert(aptlySnapshotPull(makeCmdSnapshotPull(), []string{"base", "source", "result", "pull-test"}), check.IsNil)
snapshots := testContext.NewCollectionFactory().SnapshotCollection()
result, err := snapshots.ByName("result")
c.Assert(err, check.IsNil)
c.Assert(snapshots.LoadComplete(result), check.IsNil)
got, want := result.RefList().Strings(), refs(tc.want).Strings()
c.Check(got, check.DeepEquals, want, check.Commentf("case %s: persisted package identities", tc.name))
}()
}
}
+3 -1
View File
@@ -212,7 +212,8 @@ 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" \
"-latest=[download only latest version of each package (per architecture), same as -max-versions=1]:$bool" \
"-max-versions=[download only the N most recent versions of each package (per architecture), 0 means all versions]:number: " \
"(-)2:mirror name:$mirrors"
;;
rename)
@@ -537,6 +538,7 @@ local keyring="*-keyring=[gpg keyring to use when verifying Release file (could
serve)
_arguments '1:: :' \
"-listen=[host:port for HTTP listening or unix://path to listen on a Unix domain socket]:host\:port or unix\://path: " \
"-metrics-listen=[host:port for optional metrics-only HTTP listening]:host\:port: " \
"-no-lock=[don’t lock the database]:$bool"
;;
esac
+2 -2
View File
@@ -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 -latest" -- ${cur}))
COMPREPLY=($(compgen -W "-force -download-limit= -downloader= -ignore-checksums -ignore-signatures -keyring= -skip-existing-packages -latest -max-versions=" -- ${cur}))
else
COMPREPLY=($(compgen -W "$(__aptly_mirror_list)" -- ${cur}))
fi
@@ -674,7 +674,7 @@ _aptly()
"serve")
if [[ $numargs -eq 0 ]]; then
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "-listen=" -- ${cur}))
COMPREPLY=($(compgen -W "-listen= -metrics-listen=" -- ${cur}))
fi
return 0
fi
+9
View File
@@ -124,6 +124,12 @@ func ImportPackageFiles(list *PackageList, packageFiles []string, forceReplace b
continue
}
if !isValidVersion(p.Version) {
reporter.Warning("Version number ('%s') for the '%s' package is invalid", p.Version, p.Name)
failedFiles = append(failedFiles, file)
continue
}
if p.Architecture == "" {
reporter.Warning("Empty architecture on %s", file)
failedFiles = append(failedFiles, file)
@@ -211,6 +217,9 @@ func ImportPackageFiles(list *PackageList, packageFiles []string, forceReplace b
if forceReplace {
conflictingPackages := list.Search(Dependency{Pkg: p.Name, Version: p.Version, Relation: VersionEqual, Architecture: p.Architecture}, true, false)
for _, cp := range conflictingPackages {
if cp.ArchitectureVariant != p.ArchitectureVariant {
continue
}
reporter.Removed("%s removed due to conflict with package being added", cp)
list.Remove(cp)
}
+47
View File
@@ -0,0 +1,47 @@
package deb
import (
"github.com/aptly-dev/aptly/aptly"
"github.com/aptly-dev/aptly/database"
"github.com/aptly-dev/aptly/database/goleveldb"
. "gopkg.in/check.v1"
)
type ImportSuite struct {
db database.Storage
packageCollection *PackageCollection
reporter *aptly.RecordingResultReporter
}
var _ = Suite(&ImportSuite{})
func (s *ImportSuite) SetUpTest(c *C) {
s.db, _ = goleveldb.NewOpenDB(c.MkDir())
s.packageCollection = NewPackageCollection(s.db)
s.reporter = &aptly.RecordingResultReporter{
Warnings: []string{},
AddedLines: []string{},
RemovedLines: []string{},
}
}
func (s *ImportSuite) TearDownTest(c *C) {
_ = s.db.Close()
}
func (s *ImportSuite) TestImportPackageFilesRejectsInvalidVersion(c *C) {
list := NewPackageList()
processedFiles, failedFiles, err := ImportPackageFiles(
list, []string{"testdata/import/invalid-version.dsc"}, false, &NullVerifier{},
nil, s.packageCollection, s.reporter, nil,
func(database.ReaderWriter) aptly.ChecksumStorage { return nil })
c.Assert(err, IsNil)
c.Check(processedFiles, HasLen, 0)
c.Check(failedFiles, DeepEquals, []string{"testdata/import/invalid-version.dsc"})
c.Check(s.reporter.Warnings, DeepEquals, []string{
"Version number ('1.2.3-') for the 'aptly-test-invalid-version' package is invalid",
})
}
+100 -15
View File
@@ -7,6 +7,7 @@ import (
"path"
"path/filepath"
"strings"
"sync"
"github.com/aptly-dev/aptly/aptly"
"github.com/aptly-dev/aptly/pgp"
@@ -22,6 +23,7 @@ type indexFiles struct {
suffix string
indexes map[string]*indexFile
acquireByHash bool
byHashAlgorithms []string
skipBz2 bool
}
@@ -111,7 +113,11 @@ func (file *indexFile) Finalize(signer pgp.Signer) error {
}
if file.acquireByHash {
for _, hash := range []string{"MD5Sum", "SHA1", "SHA256", "SHA512"} {
algorithms, algErr := file.parent.hashAlgorithms()
if algErr != nil {
return algErr
}
for _, hash := range algorithms {
err = file.parent.publishedStorage.MkDir(filepath.Join(filedir, "by-hash", hash))
if err != nil {
return fmt.Errorf("unable to create dir: %s", err)
@@ -133,8 +139,13 @@ func (file *indexFile) Finalize(signer pgp.Signer) error {
if file.acquireByHash {
sums := file.parent.generatedFiles[file.relativePath+ext]
for hash, sum := range map[string]string{"SHA512": sums.SHA512, "SHA256": sums.SHA256, "SHA1": sums.SHA1, "MD5Sum": sums.MD5} {
err = packageIndexByHash(file, ext, hash, sum)
all := map[string]string{"SHA512": sums.SHA512, "SHA256": sums.SHA256, "SHA1": sums.SHA1, "MD5Sum": sums.MD5}
algorithms, algErr := file.parent.hashAlgorithms()
if algErr != nil {
return algErr
}
for _, hash := range algorithms {
err = packageIndexByHash(file, ext, hash, all[hash])
if err != nil {
return fmt.Errorf("unable to build hash file: %s", err)
}
@@ -185,6 +196,51 @@ func (file *indexFile) Finalize(signer pgp.Signer) error {
return nil
}
// knownHashAlgorithms are the algorithms aptly can write Acquire-By-Hash
// entries for, in the order the entries are written.
var knownHashAlgorithms = []string{"MD5Sum", "SHA1", "SHA256", "SHA512"}
// hashAlgorithms is the set of Acquire-By-Hash algorithms to write entries for.
// Empty means all of them, which is what aptly did before the set was
// configurable, so an existing config behaves exactly as before.
//
// The order is knownHashAlgorithms' rather than the config's, so that the
// directories created in one loop and the entries written in another always
// agree whatever order the user listed.
//
// An unrecognised name is an error rather than something to skip. Skipping it
// would turn a typo into "no by-hash entries for that algorithm", which looks
// identical to having configured it away.
func (files *indexFiles) hashAlgorithms() ([]string, error) {
if len(files.byHashAlgorithms) == 0 {
return knownHashAlgorithms, nil
}
wanted := make(map[string]bool, len(files.byHashAlgorithms))
for _, want := range files.byHashAlgorithms {
known := false
for _, k := range knownHashAlgorithms {
if k == want {
known = true
break
}
}
if !known {
return nil, fmt.Errorf("unknown Acquire-By-Hash algorithm %q, expected one of %v",
want, knownHashAlgorithms)
}
wanted[want] = true
}
ordered := make([]string, 0, len(wanted))
for _, known := range knownHashAlgorithms {
if wanted[known] {
ordered = append(ordered, known)
}
}
return ordered, nil
}
func packageIndexByHash(file *indexFile, ext string, hash string, sum string) error {
src := filepath.Join(file.parent.basePath, file.relativePath)
indexfile := path.Base(src + ext)
@@ -235,7 +291,7 @@ func packageIndexByHash(file *indexFile, ext string, hash string, sum string) er
return nil
}
func newIndexFiles(publishedStorage aptly.PublishedStorage, basePath, tempDir, suffix string, acquireByHash bool, skipBz2 bool) *indexFiles {
func newIndexFiles(publishedStorage aptly.PublishedStorage, basePath, tempDir, suffix string, acquireByHash bool, byHashAlgorithms []string, skipBz2 bool) *indexFiles {
return &indexFiles{
publishedStorage: publishedStorage,
basePath: basePath,
@@ -245,6 +301,7 @@ func newIndexFiles(publishedStorage aptly.PublishedStorage, basePath, tempDir, s
suffix: suffix,
indexes: make(map[string]*indexFile),
acquireByHash: acquireByHash,
byHashAlgorithms: byHashAlgorithms,
skipBz2: skipBz2,
}
}
@@ -397,11 +454,12 @@ func (files *indexFiles) SkelIndex(component, path string) *indexFile {
relativePath := filepath.Join(component, path)
file = &indexFile{
parent: files,
discardable: false,
compressable: false,
onlyGzip: false,
relativePath: relativePath,
parent: files,
discardable: false,
compressable: false,
onlyGzip: false,
acquireByHash: files.acquireByHash,
relativePath: relativePath,
}
files.indexes[key] = file
@@ -443,14 +501,41 @@ func (files *indexFiles) FinalizeAll(progress aptly.Progress, signer pgp.Signer)
}
func (files *indexFiles) RenameFiles() error {
var err error
type renamePair struct{ oldName, newName string }
pairs := make([]renamePair, 0, len(files.renameMap))
for oldName, newName := range files.renameMap {
err = files.publishedStorage.RenameFile(oldName, newName)
if err != nil {
return fmt.Errorf("unable to rename: %s", err)
}
pairs = append(pairs, renamePair{oldName, newName})
}
return nil
var (
wg sync.WaitGroup
mu sync.Mutex
firstErr error
)
// Below 1 the semaphore would admit nothing, so treat it as serial.
sem := make(chan struct{}, max(utils.Config.PublishRenameConcurrency, 1))
for _, pair := range pairs {
wg.Add(1)
go func(pair renamePair) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
if err := files.publishedStorage.RenameFile(pair.oldName, pair.newName); err != nil {
mu.Lock()
if firstErr == nil {
firstErr = fmt.Errorf("unable to rename: %s", err)
}
mu.Unlock()
}
}(pair)
}
wg.Wait()
return firstErr
}
+151
View File
@@ -0,0 +1,151 @@
package deb
import (
"fmt"
"sync"
"time"
"github.com/aptly-dev/aptly/aptly"
"github.com/aptly-dev/aptly/utils"
. "gopkg.in/check.v1"
)
type RenameFilesSuite struct {
savedRenameConcurrency int
}
var _ = Suite(&RenameFilesSuite{})
func (s *RenameFilesSuite) SetUpTest(c *C) {
s.savedRenameConcurrency = utils.Config.PublishRenameConcurrency
}
func (s *RenameFilesSuite) TearDownTest(c *C) {
utils.Config.PublishRenameConcurrency = s.savedRenameConcurrency
}
// Only RenameFile is exercised by RenameFiles, so the rest of the interface is
// embedded and left nil: a call to anything else would panic, which is the
// loudest way to notice this fake drifting from what the code under test does.
type renameRecordingStorage struct {
aptly.PublishedStorage
mu sync.Mutex
renamed map[string]string
failOn string
inFlight int
maxSeen int
// With waitFor set, every rename blocks until that many run at once, so a
// test proves the limit is reached rather than only respected.
waitFor int
released bool
release chan struct{}
}
func (s *renameRecordingStorage) RenameFile(oldName, newName string) error {
s.mu.Lock()
s.inFlight++
if s.inFlight > s.maxSeen {
s.maxSeen = s.inFlight
}
if s.waitFor > 0 && s.inFlight == s.waitFor && !s.released {
s.released = true
close(s.release)
}
s.mu.Unlock()
defer func() {
s.mu.Lock()
s.inFlight--
s.mu.Unlock()
}()
if s.waitFor > 0 {
select {
case <-s.release:
case <-time.After(5 * time.Second):
return fmt.Errorf("never saw %d renames in flight at once", s.waitFor)
}
}
if oldName == s.failOn {
return fmt.Errorf("rename refused for %s", oldName)
}
s.mu.Lock()
s.renamed[oldName] = newName
s.mu.Unlock()
return nil
}
func newRenameFiles(storage aptly.PublishedStorage, n int) (*indexFiles, *renameRecordingStorage) {
rec, ok := storage.(*renameRecordingStorage)
if !ok {
rec = &renameRecordingStorage{renamed: make(map[string]string), release: make(chan struct{})}
}
files := &indexFiles{
publishedStorage: rec,
renameMap: make(map[string]string, n),
}
for i := 0; i < n; i++ {
files.renameMap[fmt.Sprintf("dists/x/Packages.tmp.%d", i)] = fmt.Sprintf("dists/x/Packages.%d", i)
}
return files, rec
}
func (s *RenameFilesSuite) TestRenameFilesRenamesEveryEntry(c *C) {
files, rec := newRenameFiles(nil, 20)
c.Assert(files.RenameFiles(), IsNil)
c.Check(len(rec.renamed), Equals, 20)
for i := 0; i < 20; i++ {
c.Check(rec.renamed[fmt.Sprintf("dists/x/Packages.tmp.%d", i)], Equals,
fmt.Sprintf("dists/x/Packages.%d", i))
}
}
func (s *RenameFilesSuite) TestRenameFilesRunsUpToTheConfiguredLimit(c *C) {
// Above the default of 4, so a limit that ignored the config would time out.
utils.Config.PublishRenameConcurrency = 6
files, rec := newRenameFiles(nil, 50)
rec.waitFor = 6
c.Assert(files.RenameFiles(), IsNil)
c.Check(rec.maxSeen, Equals, 6)
c.Check(len(rec.renamed), Equals, 50)
}
func (s *RenameFilesSuite) TestRenameFilesIsSerialAtALimitOfOneOrLess(c *C) {
for _, limit := range []int{1, 0, -3} {
utils.Config.PublishRenameConcurrency = limit
files, rec := newRenameFiles(nil, 10)
c.Assert(files.RenameFiles(), IsNil, Commentf("limit %d", limit))
c.Check(rec.maxSeen, Equals, 1, Commentf("limit %d", limit))
c.Check(len(rec.renamed), Equals, 10, Commentf("limit %d", limit))
}
}
func (s *RenameFilesSuite) TestRenameFilesReportsAFailure(c *C) {
files, rec := newRenameFiles(nil, 10)
rec.failOn = "dists/x/Packages.tmp.4"
err := files.RenameFiles()
c.Assert(err, NotNil)
c.Check(err, ErrorMatches, "unable to rename: rename refused for dists/x/Packages.tmp.4")
// The failure is reported, and it does not abandon the rest: every other
// entry is still renamed rather than left staged.
c.Check(len(rec.renamed), Equals, 9)
}
func (s *RenameFilesSuite) TestRenameFilesWithNothingToDo(c *C) {
files, _ := newRenameFiles(nil, 0)
c.Assert(files.RenameFiles(), IsNil)
}
+49
View File
@@ -0,0 +1,49 @@
package deb
import (
. "gopkg.in/check.v1"
)
type IndexFilesSuite struct{}
var _ = Suite(&IndexFilesSuite{})
func (s *IndexFilesSuite) TestHashAlgorithmsDefaultsToAll(c *C) {
// Unset must behave exactly as aptly did before the set was configurable,
// so an existing config keeps writing all four.
for _, files := range []*indexFiles{{}, {byHashAlgorithms: []string{}}} {
got, err := files.hashAlgorithms()
c.Check(err, IsNil)
c.Check(got, DeepEquals, []string{"MD5Sum", "SHA1", "SHA256", "SHA512"})
}
}
func (s *IndexFilesSuite) TestHashAlgorithmsHonoursTheConfiguredSet(c *C) {
got, err := (&indexFiles{byHashAlgorithms: []string{"SHA256"}}).hashAlgorithms()
c.Check(err, IsNil)
c.Check(got, DeepEquals, []string{"SHA256"})
got, err = (&indexFiles{byHashAlgorithms: []string{"SHA256", "SHA512"}}).hashAlgorithms()
c.Check(err, IsNil)
c.Check(got, DeepEquals, []string{"SHA256", "SHA512"})
}
func (s *IndexFilesSuite) TestHashAlgorithmsIsOrderedIndependentlyOfConfig(c *C) {
// The directories are created in one loop and the entries written in
// another. If the order came from the config those two could disagree,
// so it is fixed here whatever order the user listed.
got, err := (&indexFiles{byHashAlgorithms: []string{"SHA512", "MD5Sum", "SHA256"}}).hashAlgorithms()
c.Check(err, IsNil)
c.Check(got, DeepEquals, []string{"MD5Sum", "SHA256", "SHA512"})
}
func (s *IndexFilesSuite) TestHashAlgorithmsRejectsUnknownNames(c *C) {
// Loudly, not by skipping. Filtering a typo would produce "no by-hash
// entries for that algorithm", which is indistinguishable from having
// deliberately configured it away.
_, err := (&indexFiles{byHashAlgorithms: []string{"SHA256", "SHA3"}}).hashAlgorithms()
c.Check(err, ErrorMatches, `unknown Acquire-By-Hash algorithm "SHA3".*`)
_, err = (&indexFiles{byHashAlgorithms: []string{"sha256"}}).hashAlgorithms()
c.Check(err, NotNil)
}
+71 -13
View File
@@ -173,20 +173,21 @@ func (l *PackageList) ForEach(handler func(*Package) error) error {
}
// 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) {
// count latest versions for each package name/architecture pair.
func (l *PackageList) FilterLatest(count int) (*PackageList, error) {
if l == nil {
return nil, fmt.Errorf("package list is nil")
}
filtered := make(map[string]*Package, l.Len())
if count < 1 {
return nil, fmt.Errorf("number of latest versions should be at least 1, got %d", count)
}
groups := 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
}
key := p.IndexArchitecture() + "|" + p.Name
groups[key] = append(groups[key], p)
return nil
})
@@ -194,11 +195,49 @@ func (l *PackageList) FilterLatest() (*PackageList, error) {
return nil, err
}
result := NewPackageListWithDuplicates(l.duplicatesAllowed, len(filtered))
result := NewPackageListWithDuplicates(l.duplicatesAllowed, len(groups)*count)
for _, pkg := range filtered {
if err = result.Add(pkg); err != nil {
return nil, err
for _, pkgs := range groups {
// sort from latest to oldest
sort.SliceStable(pkgs, func(i, j int) bool {
cmp, cmpErr := CompareVersions(pkgs[i].Version, pkgs[j].Version)
switch cmpErr {
case LeftInvalid, BothInvalid:
return false
case RightInvalid:
return true
}
return cmp > 0
})
versions := 0
for i, p := range pkgs {
if i == 0 {
// first element always included
} else {
cmp, cmpErr := CompareVersions(p.Version, pkgs[i-1].Version)
switch cmpErr {
case LeftInvalid, BothInvalid:
// invalid version is different from previous
versions++
case RightInvalid:
// previous was invalid, current is valid — different
versions++
default:
if cmp == 0 {
// same version, skip
} else {
versions++
}
}
if versions >= count {
break
}
}
if err = result.Add(p); err != nil {
return nil, err
}
}
}
@@ -287,6 +326,19 @@ func (l *PackageList) Architectures(includeSource bool) (result []string) {
return
}
// IndexArchitectures returns repository index architectures present in the list.
// If includeSource is true, the source meta-architecture is included.
func (l *PackageList) IndexArchitectures(includeSource bool) (result []string) {
result = make([]string, 0, 10)
for _, pkg := range l.packages {
arch := pkg.IndexArchitecture()
if arch != ArchitectureAll && (arch != ArchitectureSource || includeSource) && !utils.StrSliceHasItem(result, arch) {
result = append(result, arch)
}
}
return
}
// Strings builds list of strings with package keys
func (l *PackageList) Strings() []string {
result := make([]string, l.Len())
@@ -427,7 +479,13 @@ func (l *PackageList) Swap(i, j int) {
func (l *PackageList) lessPackages(iPkg, jPkg *Package) bool {
if iPkg.Name == jPkg.Name {
cmp := CompareVersions(iPkg.Version, jPkg.Version)
cmp, err := CompareVersions(iPkg.Version, jPkg.Version)
switch err {
case LeftInvalid, BothInvalid:
return false
case RightInvalid:
return true
}
if cmp == 0 {
return iPkg.Architecture < jPkg.Architecture
}
+166 -3
View File
@@ -134,6 +134,66 @@ func (s *PackageListSuite) TestAddLen(c *C) {
c.Check(s.list.Add(s.p4), ErrorMatches, "package already exists and is different: .*")
}
func (s *PackageListSuite) TestArchitectureVariantAdd(c *C) {
normal := &Package{Name: "example", Version: "1.0", Architecture: "amd64"}
variant := &Package{Name: "example", Version: "1.0", Architecture: "amd64", ArchitectureVariant: "amd64v3"}
c.Assert(s.list.Add(normal), IsNil)
c.Assert(s.list.Add(variant), IsNil)
c.Check(s.list.Len(), Equals, 2)
var packages []*Package
c.Assert(s.list.ForEach(func(p *Package) error {
packages = append(packages, p)
return nil
}), IsNil)
c.Check(packages, HasLen, 2)
found := map[*Package]bool{}
for _, p := range packages {
found[p] = true
}
c.Check(found[normal], Equals, true)
c.Check(found[variant], Equals, true)
}
func (s *PackageListSuite) TestArchitectureVariantHas(c *C) {
normal := &Package{Name: "example", Version: "1.0", Architecture: "amd64"}
variant := &Package{Name: "example", Version: "1.0", Architecture: "amd64", ArchitectureVariant: "amd64v3"}
c.Check(s.list.Has(normal), Equals, false)
c.Check(s.list.Has(variant), Equals, false)
c.Assert(s.list.Add(normal), IsNil)
c.Check(s.list.Has(normal), Equals, true)
c.Check(s.list.Has(variant), Equals, false)
c.Assert(s.list.Add(variant), IsNil)
c.Check(s.list.Has(normal), Equals, true)
c.Check(s.list.Has(variant), Equals, true)
}
func (s *PackageListSuite) TestArchitectureVariantRemove(c *C) {
normal := &Package{Name: "example", Version: "1.0", Architecture: "amd64"}
variant := &Package{Name: "example", Version: "1.0", Architecture: "amd64", ArchitectureVariant: "amd64v3"}
c.Assert(s.list.Add(normal), IsNil)
c.Assert(s.list.Add(variant), IsNil)
s.list.Remove(variant)
c.Check(s.list.Len(), Equals, 1)
c.Check(s.list.Has(normal), Equals, true)
c.Check(s.list.Has(variant), Equals, false)
}
func (s *PackageListSuite) TestArchitectureVariantFilterLatest(c *C) {
normal := &Package{Name: "example", Version: "1.0", Architecture: "amd64"}
variant := &Package{Name: "example", Version: "1.0", Architecture: "amd64", ArchitectureVariant: "amd64v3"}
c.Assert(s.list.Add(normal), IsNil)
c.Assert(s.list.Add(variant), IsNil)
filtered, err := s.list.FilterLatest(1)
c.Assert(err, IsNil)
c.Check(filtered.Len(), Equals, 2)
c.Check(filtered.Has(normal), Equals, true)
c.Check(filtered.Has(variant), Equals, true)
}
func (s *PackageListSuite) TestRemove(c *C) {
c.Check(s.list.Add(s.p1), IsNil)
c.Check(s.list.Add(s.p3), IsNil)
@@ -524,13 +584,85 @@ func (s *PackageListSuite) TestFilterLatest(c *C) {
sharedPkg := NewPackageFromControlFile(shared)
_ = list.Add(sharedPkg)
filtered, err := list.FilterLatest()
filtered, err := list.FilterLatest(1)
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) TestFilterLatestMultiple(c *C) {
list := NewPackageList()
pkgs := make([]*Package, 0, 4)
for _, version := range []string{"2.0", "1.0", "10.0", "1:0.1"} {
stanza := packageStanza.Copy()
stanza["Version"] = version
pkg := NewPackageFromControlFile(stanza)
_ = list.Add(pkg)
pkgs = append(pkgs, pkg)
}
shared := packageStanza.Copy()
shared["Architecture"] = ArchitectureAll
shared["Version"] = "3.0"
shared["Package"] = "shared"
sharedPkg := NewPackageFromControlFile(shared)
_ = list.Add(sharedPkg)
filtered, err := list.FilterLatest(3)
c.Assert(err, IsNil)
c.Assert(filtered.Len(), Equals, 4)
c.Check(filtered.Has(pkgs[0]), Equals, true)
c.Check(filtered.Has(pkgs[1]), Equals, false)
c.Check(filtered.Has(pkgs[2]), Equals, true)
c.Check(filtered.Has(pkgs[3]), Equals, true)
c.Check(filtered.Has(sharedPkg), Equals, true)
// fewer versions available than requested: everything is kept
filtered, err = list.FilterLatest(5)
c.Assert(err, IsNil)
c.Check(filtered.Len(), Equals, 5)
}
func (s *PackageListSuite) TestFilterLatestInvalidCount(c *C) {
list := NewPackageList()
filtered, err := list.FilterLatest(0)
c.Assert(err, ErrorMatches, "number of latest versions should be at least 1, got 0")
c.Assert(filtered, IsNil)
}
func (s *PackageListSuite) TestFilterLatestInvalidVersions(c *C) {
list := NewPackageList()
valid := &Package{Name: "pkg", Version: "1.0", Architecture: "amd64"}
invalid := &Package{Name: "pkg", Version: "2.0-", Architecture: "amd64"}
_ = list.Add(valid)
_ = list.Add(invalid)
filtered, err := list.FilterLatest(1)
c.Assert(err, IsNil)
c.Check(filtered.Len(), Equals, 1)
c.Check(filtered.Has(valid), Equals, true)
invalidOnly := NewPackageList()
_ = invalidOnly.Add(&Package{Name: "pkg", Version: "1.0-", Architecture: "amd64"})
_ = invalidOnly.Add(&Package{Name: "pkg", Version: "2.0-", Architecture: "amd64"})
filtered, err = invalidOnly.FilterLatest(1)
c.Assert(err, IsNil)
c.Check(filtered.Len(), Equals, 1)
}
func (s *PackageListSuite) TestLessPackagesInvalidVersions(c *C) {
list := NewPackageList()
valid := &Package{Name: "pkg", Version: "1.0", Architecture: "amd64"}
invalid := &Package{Name: "pkg", Version: "2.0-", Architecture: "amd64"}
c.Check(list.lessPackages(valid, invalid), Equals, true)
c.Check(list.lessPackages(invalid, valid), Equals, false)
c.Check(list.lessPackages(invalid, invalid), Equals, false)
}
func (s *PackageListSuite) TestFilterLatestPreservesDuplicatesFlag(c *C) {
list := NewPackageListWithDuplicates(true, 2)
@@ -540,7 +672,7 @@ func (s *PackageListSuite) TestFilterLatestPreservesDuplicatesFlag(c *C) {
another["Version"] = "7.41-1"
_ = list.Add(NewPackageFromControlFile(another))
filtered, err := list.FilterLatest()
filtered, err := list.FilterLatest(1)
c.Assert(err, IsNil)
c.Assert(filtered.duplicatesAllowed, Equals, true)
}
@@ -548,7 +680,38 @@ func (s *PackageListSuite) TestFilterLatestPreservesDuplicatesFlag(c *C) {
func (s *PackageListSuite) TestFilterLatestNil(c *C) {
var list *PackageList
filtered, err := list.FilterLatest()
filtered, err := list.FilterLatest(1)
c.Assert(err, ErrorMatches, "package list is nil")
c.Assert(filtered, IsNil)
}
func (s *PackageListSuite) TestArchitectureVariantIndexArchitectures(c *C) {
for _, p := range []*Package{
{Name: "example", Version: "1.0", Architecture: "amd64"},
{Name: "example", Version: "1.0", Architecture: "amd64", ArchitectureVariant: "amd64v3"},
{Name: "example", Version: "2.0", Architecture: "amd64", ArchitectureVariant: "amd64v3"},
{Name: "example", Version: "1.0", Architecture: "i386"},
{Name: "data", Version: "1.0", Architecture: "all"},
{Name: "example", Version: "1.0", Architecture: "source", IsSource: true},
} {
c.Assert(s.list.Add(p), IsNil)
}
indexes := s.list.IndexArchitectures(false)
sort.Strings(indexes)
c.Check(indexes, DeepEquals, []string{"amd64", "amd64v3", "i386"})
indexes = s.list.IndexArchitectures(true)
sort.Strings(indexes)
c.Check(indexes, DeepEquals, []string{"amd64", "amd64v3", "i386", "source"})
base := s.list.Architectures(false)
sort.Strings(base)
c.Check(base, DeepEquals, []string{"amd64", "i386"})
}
func (s *PackageListSuite) TestArchitectureVariantFullNames(c *C) {
for _, variant := range []string{"", "amd64v3"} {
c.Assert(s.list.Add(&Package{Name: "test-package", Version: "1.0", Architecture: "amd64", ArchitectureVariant: variant}), IsNil)
}
names := s.list.FullNames()
sort.Strings(names)
c.Check(names, DeepEquals, []string{"test-package_1.0_amd64", "test-package_1.0_amd64v3"})
}
+43 -19
View File
@@ -16,9 +16,10 @@ import (
// Package is single instance of Debian package
type Package struct {
// Basic package properties
Name string
Version string
Architecture string
Name string
Version string
Architecture string
ArchitectureVariant string
// If this source package, this field holds "real" architecture value,
// while Architecture would be equal to "source"
SourceArchitecture string
@@ -68,16 +69,18 @@ var (
// NewPackageFromControlFile creates Package from parsed Debian control file
func NewPackageFromControlFile(input Stanza) *Package {
result := &Package{
Name: input["Package"],
Version: input["Version"],
Architecture: input["Architecture"],
Source: input["Source"],
V06Plus: true,
Name: input["Package"],
Version: input["Version"],
Architecture: input["Architecture"],
ArchitectureVariant: input["Architecture-Variant"],
Source: input["Source"],
V06Plus: true,
}
delete(input, "Package")
delete(input, "Version")
delete(input, "Architecture")
delete(input, "Architecture-Variant")
delete(input, "Source")
filesize, _ := strconv.ParseInt(input["Size"], 10, 64)
@@ -213,10 +216,18 @@ func NewInstallerPackageFromControlFile(input Stanza, repo *RemoteRepo, componen
return p, nil
}
// IndexArchitecture returns the variant when present, otherwise the base architecture.
func (p *Package) IndexArchitecture() string {
if p.ArchitectureVariant != "" {
return p.ArchitectureVariant
}
return p.Architecture
}
// Key returns unique key identifying package
func (p *Package) Key(prefix string) []byte {
if p.V06Plus {
return []byte(fmt.Sprintf("%sP%s %s %s %08x", prefix, p.Architecture, p.Name, p.Version, p.FilesHash))
return []byte(fmt.Sprintf("%sP%s %s %s %08x", prefix, p.IndexArchitecture(), p.Name, p.Version, p.FilesHash))
}
return p.ShortKey(prefix)
@@ -224,12 +235,12 @@ func (p *Package) Key(prefix string) []byte {
// ShortKey returns key for the package that should be unique in one list
func (p *Package) ShortKey(prefix string) []byte {
return []byte(fmt.Sprintf("%sP%s %s %s", prefix, p.Architecture, p.Name, p.Version))
return []byte(fmt.Sprintf("%sP%s %s %s", prefix, p.IndexArchitecture(), p.Name, p.Version))
}
// String creates readable representation
func (p *Package) String() string {
return fmt.Sprintf("%s_%s_%s", p.Name, p.Version, p.Architecture)
return fmt.Sprintf("%s_%s_%s", p.Name, p.Version, p.IndexArchitecture())
}
// ExtendedStanza returns package stanza enhanced with aptly-specific fields
@@ -292,6 +303,8 @@ func (p *Package) GetField(name string) string {
return p.SourceArchitecture
}
return p.Architecture
case "Architecture-Variant":
return p.ArchitectureVariant
case "Source":
return p.Source
case "Depends":
@@ -339,6 +352,14 @@ func (p *Package) MatchesArchitecture(arch string) bool {
return p.Architecture == arch
}
// MatchesIndexArchitecture checks membership in a repository index architecture.
func (p *Package) MatchesIndexArchitecture(arch string) bool {
if p.Architecture == ArchitectureAll && arch != ArchitectureSource {
return true
}
return p.IndexArchitecture() == arch
}
func JoinErrors(errs ...error) error {
var combinedErr error
for _, err := range errs {
@@ -405,19 +426,19 @@ func (p *Package) MatchesDependency(dep Dependency) bool {
}
func versionSatisfiesDependency(version string, dep Dependency) bool {
r := CompareVersions(version, dep.Version)
r, err := CompareVersions(version, dep.Version)
switch dep.Relation {
case VersionEqual:
return r == 0
return err == nil && r == 0
case VersionLess:
return r < 0
return err == nil && r < 0
case VersionGreater:
return r > 0
return err == nil && r > 0
case VersionLessOrEqual:
return r <= 0
return err == nil && r <= 0
case VersionGreaterOrEqual:
return r >= 0
return err == nil && r >= 0
case VersionPatternMatch:
matched, err := filepath.Match(dep.Version, version)
return err == nil && matched
@@ -437,7 +458,7 @@ func (p *Package) GetName() string {
// GetFullName returns the package full name
func (p *Package) GetFullName() string {
return strings.Join([]string{p.Name, p.Version, p.Architecture}, "_")
return strings.Join([]string{p.Name, p.Version, p.IndexArchitecture()}, "_")
}
// GetVersion returns package version
@@ -594,6 +615,9 @@ func (p *Package) Stanza() (result Stanza) {
result["Architecture"] = p.SourceArchitecture
} else {
result["Architecture"] = p.Architecture
if p.ArchitectureVariant != "" {
result["Architecture-Variant"] = p.ArchitectureVariant
}
if p.Source != "" {
result["Source"] = p.Source
}
@@ -682,7 +706,7 @@ func (p *Package) Stanza() (result Stanza) {
func (p *Package) Equals(p2 *Package) bool {
return p.Name == p2.Name && p.Version == p2.Version && p.SourceArchitecture == p2.SourceArchitecture &&
p.Architecture == p2.Architecture && p.Source == p2.Source && p.IsSource == p2.IsSource &&
p.FilesHash == p2.FilesHash
p.ArchitectureVariant == p2.ArchitectureVariant && p.FilesHash == p2.FilesHash
}
// LinkFromPool links package file from pool to dist's pool location
+1 -1
View File
@@ -336,7 +336,7 @@ func (collection *PackageCollection) SearchByKey(arch, name, version string) (re
panic(fmt.Sprintf("unable to load package: %s", err))
}
if pkg.Architecture == arch && pkg.Name == name && pkg.Version == version {
if pkg.IndexArchitecture() == arch && pkg.Name == name && pkg.Version == version {
_ = result.Add(pkg)
}
}
+100
View File
@@ -65,6 +65,106 @@ func (s *PackageCollectionSuite) TestByKey(c *C) {
c.Check(p2.Files()[0].Filename, Equals, "alien-arena-common_7.40-2_i386.deb")
}
func architectureVariantCollectionPackage(variant string) *Package {
stanza := Stanza{
"Package": "example", "Version": "1.0", "Architecture": "amd64",
"Filename": "pool/example_1.0_amd64.deb", "Size": "123",
"Depends": "test-dependency (>= 1.0)", "Priority": "optional",
}
if variant != "" {
stanza["Architecture-Variant"] = variant
stanza["Filename"] = "pool/example_1.0_" + variant + ".deb"
}
return NewPackageFromControlFile(stanza)
}
func (s *PackageCollectionSuite) TestArchitectureVariantNormalRoundTrip(c *C) {
p := architectureVariantCollectionPackage("")
key := p.Key("")
c.Assert(s.collection.Update(p), IsNil)
loaded, err := s.collection.ByKey(key)
c.Assert(err, IsNil)
c.Check(loaded.Name, Equals, "example")
c.Check(loaded.Version, Equals, "1.0")
c.Check(loaded.Architecture, Equals, "amd64")
c.Check(loaded.ArchitectureVariant, Equals, "")
c.Check(loaded.IndexArchitecture(), Equals, "amd64")
c.Check(loaded.Key(""), DeepEquals, key)
c.Check(loaded.Equals(p), Equals, true)
c.Check(loaded.Files()[0].Filename, Equals, "example_1.0_amd64.deb")
c.Check(loaded.GetDependencies(0), DeepEquals, []string{"test-dependency (>= 1.0)"})
c.Check(loaded.Extra()["Priority"], Equals, "optional")
_, hasVariant := loaded.Stanza()["Architecture-Variant"]
c.Check(hasVariant, Equals, false)
}
func (s *PackageCollectionSuite) TestArchitectureVariantRoundTrip(c *C) {
p := architectureVariantCollectionPackage("amd64v3")
key := p.Key("")
c.Assert(s.collection.Update(p), IsNil)
loaded, err := s.collection.ByKey(key)
c.Assert(err, IsNil)
c.Check(loaded.Name, Equals, "example")
c.Check(loaded.Version, Equals, "1.0")
c.Check(loaded.Architecture, Equals, "amd64")
c.Check(loaded.ArchitectureVariant, Equals, "amd64v3")
c.Check(loaded.IndexArchitecture(), Equals, "amd64v3")
c.Check(loaded.Key(""), DeepEquals, key)
c.Check(loaded.Equals(p), Equals, true)
// Exercise offloaded records using the reloaded variant-aware key.
c.Check(loaded.Files()[0].Filename, Equals, "example_1.0_amd64v3.deb")
c.Check(loaded.GetDependencies(0), DeepEquals, []string{"test-dependency (>= 1.0)"})
c.Check(loaded.Extra()["Priority"], Equals, "optional")
stanza := loaded.Stanza()
c.Check(stanza["Architecture"], Equals, "amd64")
c.Check(stanza["Architecture-Variant"], Equals, "amd64v3")
}
func (s *PackageCollectionSuite) TestArchitectureVariantCoexistence(c *C) {
normal := architectureVariantCollectionPackage("")
variant := architectureVariantCollectionPackage("amd64v3")
c.Assert(s.collection.Update(normal), IsNil)
c.Assert(s.collection.Update(variant), IsNil)
refs := s.collection.AllPackageRefs()
c.Check(refs.Len(), Equals, 2)
c.Check(refs.Has(normal), Equals, true)
c.Check(refs.Has(variant), Equals, true)
for _, p := range []*Package{normal, variant} {
loaded, err := s.collection.ByKey(p.Key(""))
c.Assert(err, IsNil)
c.Check(loaded.Equals(p), Equals, true)
c.Check(loaded.Architecture, Equals, "amd64")
c.Check(loaded.ArchitectureVariant, Equals, p.ArchitectureVariant)
}
}
func (s *PackageCollectionSuite) TestArchitectureVariantFullKeyLookup(c *C) {
normal := architectureVariantCollectionPackage("")
variant := architectureVariantCollectionPackage("amd64v3")
// Keep the file hash identical so the architecture token alone must
// distinguish the full keys and stored records.
variant.UpdateFiles(normal.Files())
normalKey, variantKey := normal.Key(""), variant.Key("")
c.Assert(normalKey, Not(DeepEquals), variantKey)
c.Assert(s.collection.Update(normal), IsNil)
_, err := s.collection.ByKey(variantKey)
c.Assert(err, Equals, database.ErrNotFound)
c.Assert(s.collection.Update(variant), IsNil)
loaded, err := s.collection.ByKey(variantKey)
c.Assert(err, IsNil)
c.Check(loaded.Key(""), DeepEquals, variantKey)
c.Check(loaded.Architecture, Equals, "amd64")
c.Check(loaded.ArchitectureVariant, Equals, "amd64v3")
c.Check(loaded.IndexArchitecture(), Equals, "amd64v3")
c.Check(loaded.Equals(variant), Equals, true)
c.Check(loaded.Equals(normal), Equals, false)
loadedNormal, err := s.collection.ByKey(normalKey)
c.Assert(err, IsNil)
c.Check(loadedNormal.Equals(normal), Equals, true)
c.Check(loadedNormal.ArchitectureVariant, Equals, "")
}
func (s *PackageCollectionSuite) TestByKeyOld0_3(c *C) {
key := []byte("Pi386 vmware-view-open-client 4.5.0-297975+dfsg-4+b1")
_ = s.db.Put(key, old0_3Package)
+96
View File
@@ -43,6 +43,66 @@ func (s *PackageSuite) TestNewFromPara(c *C) {
c.Check(p.deps.Depends, DeepEquals, []string{"libc6 (>= 2.7)", "alien-arena-data (>= 7.40)"})
}
func (s *PackageSuite) TestArchitectureVariantParsedSeparately(c *C) {
input, err := NewControlFileReader(bytes.NewBufferString("Package: example\nVersion: 1.0\nArchitecture: amd64\nArchitecture-Variant: amd64v3\nFilename: pool/example_1.0_amd64v3.deb\n"), false, false).ReadStanza()
c.Assert(err, IsNil)
p := NewPackageFromControlFile(input)
c.Check(p.Architecture, Equals, "amd64")
c.Check(p.ArchitectureVariant, Equals, "amd64v3")
_, inExtra := p.Extra()["Architecture-Variant"]
c.Check(inExtra, Equals, false)
c.Check(p.GetField("Architecture"), Equals, "amd64")
c.Check(p.GetField("Architecture-Variant"), Equals, "amd64v3")
}
func (s *PackageSuite) TestArchitectureVariantShortKey(c *C) {
input := Stanza{"Package": "example", "Version": "1.0", "Architecture": "amd64"}
normal := NewPackageFromControlFile(input.Copy())
input["Architecture-Variant"] = "amd64v3"
variant := NewPackageFromControlFile(input)
c.Check(normal.Architecture, Equals, "amd64")
c.Check(variant.Architecture, Equals, "amd64")
c.Check(string(normal.ShortKey("")), Equals, "Pamd64 example 1.0")
c.Check(string(variant.ShortKey("")), Equals, "Pamd64v3 example 1.0")
c.Check(string(variant.ShortKey("")), Not(Equals), string(normal.ShortKey("")))
c.Check(string(variant.ShortKey("xD")), Equals, "xDPamd64v3 example 1.0")
}
func (s *PackageSuite) TestArchitectureVariantKeyAndEquals(c *C) {
normal := &Package{Name: "example", Version: "1.0", Architecture: "amd64", V06Plus: true, FilesHash: 0x12345678}
variant := *normal
variant.ArchitectureVariant = "amd64v3"
c.Check(normal.IndexArchitecture(), Equals, "amd64")
c.Check(variant.IndexArchitecture(), Equals, "amd64v3")
c.Check(string(normal.Key("")), Equals, "Pamd64 example 1.0 12345678")
c.Check(string(variant.Key("")), Equals, "Pamd64v3 example 1.0 12345678")
c.Check(string(variant.Key("xD")), Equals, "xDPamd64v3 example 1.0 12345678")
c.Check(normal.Equals(&variant), Equals, false)
c.Check(variant.Equals(normal), Equals, false)
otherVariant := variant
c.Check(variant.Equals(&otherVariant), Equals, true)
otherVariant.ArchitectureVariant = "amd64v4"
c.Check(variant.Equals(&otherVariant), Equals, false)
variant.V06Plus = false
c.Check(string(variant.Key("")), Equals, "Pamd64v3 example 1.0")
c.Check(string(variant.Key("xD")), Equals, "xDPamd64v3 example 1.0")
}
func (s *PackageSuite) TestArchitectureVariantStanza(c *C) {
s.stanza["Architecture"] = "amd64"
s.stanza["Architecture-Variant"] = "amd64v3"
p := NewPackageFromControlFile(s.stanza)
stanza := p.Stanza()
c.Check(p.Architecture, Equals, "amd64")
c.Check(stanza["Architecture"], Equals, "amd64")
c.Check(stanza["Architecture-Variant"], Equals, "amd64v3")
}
func (s *PackageSuite) TestNewUdebFromPara(c *C) {
stanza, _ := NewControlFileReader(bytes.NewBufferString(udebPackageMeta), false, false).ReadStanza()
p := NewUdebPackageFromControlFile(stanza)
@@ -520,3 +580,39 @@ SHA256: bbb3a2cb07f741c3995b6d4bb08d772d83582b93a0236d4ea7736bc0370fc320`
const installerPackageMeta = `9d8bb14044dee520f4706ab197dfff10e9e39ecb3c1a402331712154e8284b2e ./MANIFEST.udebs
dab96042d8e25e0f6bbb8d7c5bd78543afb5eb31a4a8b122ece68ab197228028 ./udeb.list`
func (s *PackageSuite) TestArchitectureVariantMatchesIndexArchitecture(c *C) {
for _, tc := range []struct {
base, variant, target string
matches bool
}{
{"amd64", "", "amd64", true},
{"amd64", "", "amd64v3", false},
{"amd64", "amd64v3", "amd64", false},
{"amd64", "amd64v3", "amd64v3", true},
{"all", "", "amd64", true},
{"all", "", "amd64v3", true},
{"all", "", "source", false},
{"source", "", "source", true},
{"source", "", "amd64v3", false},
} {
p := &Package{Architecture: tc.base, ArchitectureVariant: tc.variant, IsSource: tc.base == "source"}
c.Check(p.MatchesIndexArchitecture(tc.target), Equals, tc.matches, Commentf("base=%s variant=%s target=%s", tc.base, tc.variant, tc.target))
}
p := &Package{Architecture: "amd64", ArchitectureVariant: "amd64v3"}
c.Check(p.MatchesArchitecture("amd64"), Equals, true)
c.Check(p.MatchesArchitecture("amd64v3"), Equals, false)
}
func (s *PackageSuite) TestArchitectureVariantDisplayNames(c *C) {
for _, variant := range []string{"", "amd64v3"} {
p := &Package{Name: "test-package", Version: "1.0", Architecture: "amd64", ArchitectureVariant: variant}
expected := "test-package_1.0_amd64"
if variant != "" {
expected = "test-package_1.0_amd64v3"
}
c.Check(p.String(), Equals, expected)
c.Check(p.GetFullName(), Equals, expected)
c.Check(p.GetArchitecture(), Equals, "amd64")
}
}
+71 -63
View File
@@ -76,6 +76,10 @@ type PublishedRepo struct {
// Skip bz2 compression for index files
SkipBz2 bool
// AcquireByHashAlgorithms limits which hash algorithms get Acquire-By-Hash
// entries. Set per invocation from the config rather than persisted, the
// same way SkipBz2 is. Empty means all four.
AcquireByHashAlgorithms []string
// True if repo is being re-published
rePublishing bool
@@ -879,7 +883,7 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
if !p.rePublishing {
if len(p.Architectures) == 0 {
for _, list := range lists {
p.Architectures = append(p.Architectures, list.Architectures(true)...)
p.Architectures = append(p.Architectures, list.IndexArchitectures(true)...)
}
}
@@ -893,7 +897,7 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
var suffix string
if p.rePublishing {
suffix = ".tmp"
suffix = ".tmp." + uuid.NewString()
}
if progress != nil {
@@ -907,7 +911,7 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
}
defer func() { _ = os.RemoveAll(tempDir) }()
indexes := newIndexFiles(publishedStorage, basePath, tempDir, suffix, p.AcquireByHash, p.SkipBz2)
indexes := newIndexFiles(publishedStorage, basePath, tempDir, suffix, p.AcquireByHash, p.AcquireByHashAlgorithms, p.SkipBz2)
legacyContentIndexes := map[string]*ContentsIndex{}
var count int64
@@ -937,7 +941,7 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
}
for _, arch := range p.Architectures {
if pkg.MatchesArchitecture(arch) {
if pkg.MatchesIndexArchitecture(arch) {
hadUdebs = hadUdebs || pkg.IsUdeb
var relPath string
@@ -975,7 +979,7 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
batch := tempDB.CreateBatch()
for _, arch := range p.Architectures {
if pkg.MatchesArchitecture(arch) {
if pkg.MatchesIndexArchitecture(arch) {
var bufWriter *bufio.Writer
if !p.SkipContents && !pkg.IsInstaller {
@@ -1043,62 +1047,6 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
}
}
for component := range p.sourceItems {
skelFiles, err := p.GetSkelFiles(skelDir, component)
if err != nil {
return fmt.Errorf("unable to get skeleton files: %v", err)
}
for relPath, absPath := range skelFiles {
bufWriter, err := indexes.SkelIndex(component, relPath).BufWriter()
if err != nil {
return fmt.Errorf("unable to generate skeleton index: %v", err)
}
file, err := os.Open(absPath)
if err != nil {
return fmt.Errorf("unable to read skeleton file: %v", err)
}
_, err = bufio.NewReader(file).WriteTo(bufWriter)
if err != nil {
return fmt.Errorf("unable to write skeleton file: %v", err)
}
}
}
// 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)
@@ -1144,6 +1092,66 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
}
}
// Skeleton and AppStream (DEP-11) files are keyed by component and already
// iterate over every component, so they must be emitted once for the whole
// publish -- not once per component, which would append each payload again.
for component := range p.sourceItems {
skelFiles, err := p.GetSkelFiles(skelDir, component)
if err != nil {
return fmt.Errorf("unable to get skeleton files: %v", err)
}
for relPath, absPath := range skelFiles {
bufWriter, err := indexes.SkelIndex(component, relPath).BufWriter()
if err != nil {
return fmt.Errorf("unable to generate skeleton index: %v", err)
}
file, err := os.Open(absPath)
if err != nil {
return fmt.Errorf("unable to read skeleton file: %v", err)
}
_, err = bufio.NewReader(file).WriteTo(bufWriter)
_ = file.Close()
if err != nil {
return fmt.Errorf("unable to write skeleton file: %v", err)
}
}
}
// 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)
}
}
}
for _, arch := range p.Architectures {
for _, udeb := range []bool{true, false} {
index := legacyContentIndexes[fmt.Sprintf("%s-%v", arch, udeb)]
@@ -1208,8 +1216,8 @@ func (p *PublishedRepo) Publish(packagePool aptly.PackagePool, publishedStorageP
release["Valid-Until"] = publishDate.AddDate(100, 0, 0).Format(datetimeformat)
}
if p.Version != "" {
release["Version"] = p.Version
}
release["Version"] = p.Version
}
release["Description"] = " Generated by aptly\n"
release["MD5Sum"] = ""
release["SHA1"] = ""
+314
View File
@@ -6,8 +6,11 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
"github.com/aptly-dev/aptly/aptly"
"github.com/aptly-dev/aptly/database"
@@ -379,6 +382,83 @@ func (s *PublishedRepoSuite) TestUpdate(c *C) {
c.Assert(result.RemovedComponents(), DeepEquals, []string{})
}
func (s *PublishedRepoSuite) checkArchitectureVariantPublication(c *C, architectures []string) {
list := NewPackageList()
for _, arch := range []string{"amd64", "amd64v3"} {
filename := "example_1.0_" + arch + ".deb"
stanza := Stanza{"Package": "example", "Version": "1.0", "Architecture": "amd64", "Filename": filename}
if arch == "amd64v3" {
stanza["Architecture-Variant"] = "amd64v3"
}
p := NewPackageFromControlFile(stanza)
inputPath := filepath.Join(c.MkDir(), filename)
c.Assert(os.WriteFile(inputPath, nil, 0644), IsNil)
var err error
p.Files()[0].PoolPath, err = s.packagePool.Import(inputPath, filename, &p.Files()[0].Checksums, false, s.cs)
c.Assert(err, IsNil)
p.UpdateFiles(p.Files())
c.Assert(s.packageCollection.Update(p), IsNil)
c.Assert(list.Add(p), IsNil)
}
local := NewLocalRepo("architecture-variant", "")
local.packageRefs = NewPackageRefListFromPackageList(list)
c.Assert(s.factory.LocalRepoCollection().Add(local), IsNil)
repo, err := NewPublishedRepo("", "variant-test", "test", architectures, []string{"main"}, []interface{}{local}, s.factory, false)
c.Assert(err, IsNil)
repo.SkipContents = true
c.Assert(repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, ""), IsNil)
root := filepath.Join(s.publishedStorage.PublicPath(), "variant-test")
releaseData, err := os.ReadFile(filepath.Join(root, "dists/test/Release"))
c.Assert(err, IsNil)
release, err := NewControlFileReader(bytes.NewReader(releaseData), true, false).ReadStanza()
c.Assert(err, IsNil)
c.Check(release["Architectures"], Equals, "amd64 amd64v3")
for _, arch := range []string{"amd64", "amd64v3"} {
filename := "example_1.0_" + arch + ".deb"
c.Check(filepath.Join(root, "pool/main/e/example", filename), PathExists)
indexPath := filepath.Join(root, "dists/test/main", "binary-"+arch, "Packages")
data, err := os.ReadFile(indexPath)
if !c.Check(err, IsNil, Commentf("index %s must exist", arch)) {
continue
}
reader := NewControlFileReader(bytes.NewReader(data), false, false)
var entries []Stanza
for {
entry, err := reader.ReadStanza()
c.Assert(err, IsNil)
if entry == nil {
break
}
entries = append(entries, entry)
}
c.Check(entries, HasLen, 1, Commentf("index %s must contain exactly one package", arch))
var filenames []string
for _, entry := range entries {
filenames = append(filenames, entry["Filename"])
c.Check(entry["Package"], Equals, "example")
c.Check(entry["Version"], Equals, "1.0")
c.Check(entry["Architecture"], Equals, "amd64")
if entry["Filename"] == "pool/main/e/example/example_1.0_amd64v3.deb" {
c.Check(entry["Architecture-Variant"], Equals, "amd64v3")
} else {
_, hasVariant := entry["Architecture-Variant"]
c.Check(hasVariant, Equals, false)
}
}
c.Check(filenames, DeepEquals, []string{"pool/main/e/example/" + filename}, Commentf("index %s package membership", arch))
}
}
func (s *PublishedRepoSuite) TestArchitectureVariantPublishAutoArchitectures(c *C) {
s.checkArchitectureVariantPublication(c, nil)
}
func (s *PublishedRepoSuite) TestArchitectureVariantPublishExplicitArchitectures(c *C) {
s.checkArchitectureVariantPublication(c, []string{"amd64", "amd64v3"})
}
func (s *PublishedRepoSuite) TestPublish(c *C) {
err := s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, "")
c.Assert(err, IsNil)
@@ -501,6 +581,240 @@ func (s *PublishedRepoSuite) TestPublishAppStream(c *C) {
c.Assert(err, ErrorMatches, "unable to open AppStream file from pool.*")
}
func (s *PublishedRepoSuite) importAppStreamFile(c *C, name string, content []byte) string {
tmpFile := filepath.Join(c.MkDir(), name)
c.Assert(os.WriteFile(tmpFile, content, 0644), IsNil)
checksums := utils.ChecksumInfo{Size: int64(len(content))}
poolPath, err := s.packagePool.Import(tmpFile, name, &checksums, false, s.cs)
c.Assert(err, IsNil)
return poolPath
}
func (s *PublishedRepoSuite) TestPublishAppStreamMultipleComponents(c *C) {
mainContent := []byte("DEP-11 content for main\n")
contribContent := []byte("DEP-11 content for contrib\n")
s.snapshot.AppStreamFiles = map[string]string{
"main/dep11/Components-amd64.yml.gz": s.importAppStreamFile(c, "Components-main.yml.gz", mainContent),
}
s.snapshot2.AppStreamFiles = map[string]string{
"contrib/dep11/Components-amd64.yml.gz": s.importAppStreamFile(c, "Components-contrib.yml.gz", contribContent),
}
err := s.repo3.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, "")
c.Assert(err, IsNil)
base := filepath.Join(s.publishedStorage.PublicPath(), "linux/dists/natty")
published, err := os.ReadFile(filepath.Join(base, "main/dep11/Components-amd64.yml.gz"))
c.Assert(err, IsNil)
c.Check(published, DeepEquals, mainContent)
published, err = os.ReadFile(filepath.Join(base, "contrib/dep11/Components-amd64.yml.gz"))
c.Assert(err, IsNil)
c.Check(published, DeepEquals, contribContent)
}
func (s *PublishedRepoSuite) TestPublishAppStreamAcquireByHash(c *C) {
s.snapshot.AppStreamFiles = map[string]string{
"main/dep11/Components-amd64.yml.gz": s.importAppStreamFile(c, "Components-amd64.yml.gz", []byte("DEP-11 content\n")),
}
s.repo.AcquireByHash = true
err := s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, "")
c.Assert(err, IsNil)
base := filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze")
c.Check(filepath.Join(base, "main/dep11/by-hash/SHA256"), PathExists)
}
// setupSmallTempFS points TMPDIR at a ~1MB filesystem so that Publish's
// temporary index files run out of space mid-write. Follows the DiskFullSuite
// convention: /smallfs is pre-mounted in CI (see docker-unit-test), a loopback
// mount is created when running as root, otherwise the test is skipped.
func (s *PublishedRepoSuite) setupSmallTempFS(c *C) func() {
if runtime.GOOS != "linux" {
c.Skip("disk full tests only run on Linux")
}
mountPoint := "/smallfs"
mounted := false
if os.Geteuid() == 0 {
mountPoint = filepath.Join(c.MkDir(), "smallfs")
c.Assert(os.MkdirAll(mountPoint, 0777), IsNil)
fsImage := filepath.Join(c.MkDir(), "small.img")
c.Assert(exec.Command("dd", "if=/dev/zero", "of="+fsImage, "bs=1M", "count=1").Run(), IsNil)
c.Assert(exec.Command("mkfs.ext4", "-F", fsImage).Run(), IsNil)
c.Assert(exec.Command("mount", "-o", "loop", fsImage, mountPoint).Run(), IsNil)
mounted = true
} else if _, err := os.Stat(mountPoint); err != nil {
c.Skip("/smallfs is not mounted")
}
// remove leftovers from other tests sharing the mount
entries, err := os.ReadDir(mountPoint)
c.Assert(err, IsNil)
for _, entry := range entries {
if entry.Name() == "lost+found" {
continue
}
c.Assert(os.RemoveAll(filepath.Join(mountPoint, entry.Name())), IsNil)
}
oldTMPDIR, hadTMPDIR := os.LookupEnv("TMPDIR")
c.Assert(os.Setenv("TMPDIR", mountPoint), IsNil)
return func() {
if hadTMPDIR {
_ = os.Setenv("TMPDIR", oldTMPDIR)
} else {
_ = os.Unsetenv("TMPDIR")
}
if mounted {
_ = exec.Command("umount", mountPoint).Run()
}
}
}
func (s *PublishedRepoSuite) TestPublishSkelFiles(c *C) {
skelDir := c.MkDir()
dir := filepath.Join(skelDir, "ppa", "dists", "squeeze", "main")
c.Assert(os.MkdirAll(filepath.Join(dir, "extra"), 0755), IsNil)
c.Assert(os.WriteFile(filepath.Join(dir, "top.txt"), []byte("top-level skel file\n"), 0644), IsNil)
c.Assert(os.WriteFile(filepath.Join(dir, "extra", "metadata.json"), []byte("{\"nested\": true}\n"), 0644), IsNil)
err := s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, skelDir)
c.Assert(err, IsNil)
base := filepath.Join(s.publishedStorage.PublicPath(), "ppa/dists/squeeze/main")
published, err := os.ReadFile(filepath.Join(base, "top.txt"))
c.Assert(err, IsNil)
c.Check(string(published), Equals, "top-level skel file\n")
published, err = os.ReadFile(filepath.Join(base, "extra", "metadata.json"))
c.Assert(err, IsNil)
c.Check(string(published), Equals, "{\"nested\": true}\n")
// Release file should reference skel 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["SHA256"], Matches, "(?s).*main/top\\.txt.*")
c.Check(st["SHA256"], Matches, "(?s).*main/extra/metadata\\.json.*")
}
func (s *PublishedRepoSuite) TestPublishSkelFilesMultipleComponents(c *C) {
// regression test: skel files used to be emitted once per component,
// appending each payload to the index again (issue fixed in this change)
skelDir := c.MkDir()
mainDir := filepath.Join(skelDir, "linux", "dists", "natty", "main")
contribDir := filepath.Join(skelDir, "linux", "dists", "natty", "contrib")
c.Assert(os.MkdirAll(mainDir, 0755), IsNil)
c.Assert(os.MkdirAll(contribDir, 0755), IsNil)
c.Assert(os.WriteFile(filepath.Join(mainDir, "main.txt"), []byte("skel for main\n"), 0644), IsNil)
c.Assert(os.WriteFile(filepath.Join(contribDir, "contrib.txt"), []byte("skel for contrib\n"), 0644), IsNil)
err := s.repo3.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, skelDir)
c.Assert(err, IsNil)
base := filepath.Join(s.publishedStorage.PublicPath(), "linux/dists/natty")
// each payload must be published exactly once, not duplicated per component
published, err := os.ReadFile(filepath.Join(base, "main/main.txt"))
c.Assert(err, IsNil)
c.Check(string(published), Equals, "skel for main\n")
published, err = os.ReadFile(filepath.Join(base, "contrib/contrib.txt"))
c.Assert(err, IsNil)
c.Check(string(published), Equals, "skel for contrib\n")
}
func (s *PublishedRepoSuite) TestPublishSkelFilesWalkError(c *C) {
if runtime.GOOS == "windows" {
c.Skip("a file in the middle of a path is reported as not-exist on Windows")
}
// "dists" is a regular file, so walking skelDir/ppa/dists/squeeze/main
// fails with ENOTDIR, which is not swallowed as a not-exist error
skelDir := c.MkDir()
c.Assert(os.MkdirAll(filepath.Join(skelDir, "ppa"), 0755), IsNil)
c.Assert(os.WriteFile(filepath.Join(skelDir, "ppa", "dists"), []byte("not a directory"), 0644), IsNil)
err := s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, skelDir)
c.Assert(err, ErrorMatches, "unable to get skeleton files: .*")
}
func (s *PublishedRepoSuite) TestPublishSkelFilesIndexError(c *C) {
// each name stays within NAME_MAX, but BufWriter flattens the relative
// path into a single temporary file name that exceeds it
skelDir := c.MkDir()
dir := filepath.Join(skelDir, "ppa", "dists", "squeeze", "main", strings.Repeat("a", 200))
c.Assert(os.MkdirAll(dir, 0755), IsNil)
c.Assert(os.WriteFile(filepath.Join(dir, strings.Repeat("b", 200)), []byte("skel content"), 0644), IsNil)
err := s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, skelDir)
c.Assert(err, ErrorMatches, "unable to generate skeleton index: .*")
}
func (s *PublishedRepoSuite) TestPublishSkelFilesOpenError(c *C) {
if runtime.GOOS == "windows" || os.Geteuid() == 0 {
c.Skip("requires POSIX permissions and a non-root user")
}
skelDir := c.MkDir()
dir := filepath.Join(skelDir, "ppa", "dists", "squeeze", "main")
c.Assert(os.MkdirAll(dir, 0755), IsNil)
skelFile := filepath.Join(dir, "InRelease")
c.Assert(os.WriteFile(skelFile, []byte("skel content"), 0644), IsNil)
c.Assert(os.Chmod(skelFile, 0), IsNil)
err := s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, skelDir)
c.Assert(err, ErrorMatches, "unable to read skeleton file: .*")
}
func (s *PublishedRepoSuite) TestPublishSkelFilesWriteError(c *C) {
cleanup := s.setupSmallTempFS(c)
defer cleanup()
skelDir := c.MkDir()
dir := filepath.Join(skelDir, "ppa", "dists", "squeeze", "main")
c.Assert(os.MkdirAll(dir, 0755), IsNil)
c.Assert(os.WriteFile(filepath.Join(dir, "Contents-huge"), bytes.Repeat([]byte{'x'}, 2*1024*1024), 0644), IsNil)
err := s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, skelDir)
c.Assert(err, ErrorMatches, "unable to write skeleton file: .*")
}
func (s *PublishedRepoSuite) TestPublishAppStreamIndexError(c *C) {
// flattened temporary file name for the index exceeds NAME_MAX
s.snapshot.AppStreamFiles = map[string]string{
"main/" + strings.Repeat("a", 200) + "/" + strings.Repeat("b", 200): s.importAppStreamFile(c, "Components-amd64.yml.gz", []byte("DEP-11 content\n")),
}
err := s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, "")
c.Assert(err, ErrorMatches, "unable to generate AppStream index: .*")
}
func (s *PublishedRepoSuite) TestPublishAppStreamWriteError(c *C) {
cleanup := s.setupSmallTempFS(c)
defer cleanup()
s.snapshot.AppStreamFiles = map[string]string{
"main/dep11/Components-amd64.yml.gz": s.importAppStreamFile(c, "Components-amd64.yml.gz", bytes.Repeat([]byte{'x'}, 2*1024*1024)),
}
err := s.repo.Publish(s.packagePool, s.provider, s.factory, &NullSigner{}, nil, false, "")
c.Assert(err, ErrorMatches, "unable to write AppStream file: .*")
}
func (s *PublishedRepoSuite) TestPublishNoSigner(c *C) {
err := s.repo.Publish(s.packagePool, s.provider, s.factory, nil, nil, false, "")
c.Assert(err, IsNil)
+15 -6
View File
@@ -168,15 +168,20 @@ func (q *FieldQuery) Matches(pkg PackageLike) bool {
case VersionDontCare:
return field != ""
case VersionEqual:
return CompareVersions(field, q.Value) == 0
result, err := CompareVersions(field, q.Value)
return err == nil && result == 0
case VersionGreater:
return CompareVersions(field, q.Value) > 0
result, err := CompareVersions(field, q.Value)
return err == nil && result > 0
case VersionGreaterOrEqual:
return CompareVersions(field, q.Value) >= 0
result, err := CompareVersions(field, q.Value)
return err == nil && result >= 0
case VersionLess:
return CompareVersions(field, q.Value) < 0
result, err := CompareVersions(field, q.Value)
return err == nil && result < 0
case VersionLessOrEqual:
return CompareVersions(field, q.Value) <= 0
result, err := CompareVersions(field, q.Value)
return err == nil && result <= 0
case VersionPatternMatch:
matched, err := filepath.Match(q.Value, field)
return err == nil && matched
@@ -261,7 +266,11 @@ func (q *DependencyQuery) String() string {
// Matches on specific properties
func (q *PkgQuery) Matches(pkg PackageLike) bool {
return pkg.GetName() == q.Pkg && pkg.GetVersion() == q.Version && pkg.GetArchitecture() == q.Arch
arch := pkg.GetArchitecture()
if indexed, ok := pkg.(interface{ IndexArchitecture() string }); ok {
arch = indexed.IndexArchitecture()
}
return pkg.GetName() == q.Pkg && pkg.GetVersion() == q.Version && arch == q.Arch
}
// Fast is always true for package query
+40
View File
@@ -21,3 +21,43 @@ func (s *QuerySuite) TestVersionCompare(c *C) {
c.Check(q.Matches(&p100), Equals, false)
c.Check(q.Matches(&p1), Equals, true)
}
func (s *QuerySuite) TestVersionCompareGreater(c *C) {
q := FieldQuery{"Version", VersionGreater, "5.0.0.2", nil}
p100 := Package{}
p100.Version = "5.0.0.100"
p1 := Package{}
p1.Version = "5.0.0.1"
c.Check(q.Matches(&p100), Equals, true)
c.Check(q.Matches(&p1), Equals, false)
// invalid version on either side of the comparison must not match
pInvalid := Package{}
pInvalid.Version = "1.2.3-"
c.Check(q.Matches(&pInvalid), Equals, false)
}
func (s *QuerySuite) TestVersionCompareGreaterOrEqual(c *C) {
q := FieldQuery{"Version", VersionGreaterOrEqual, "5.0.0.2", nil}
p100 := Package{}
p100.Version = "5.0.0.100"
pEqual := Package{}
pEqual.Version = "5.0.0.2"
p1 := Package{}
p1.Version = "5.0.0.1"
c.Check(q.Matches(&p100), Equals, true)
c.Check(q.Matches(&pEqual), Equals, true)
c.Check(q.Matches(&p1), Equals, false)
// invalid version on either side of the comparison must not match
pInvalid := Package{}
pInvalid.Version = "1.2.3-"
c.Check(q.Matches(&pInvalid), Equals, false)
}
+24 -20
View File
@@ -240,7 +240,7 @@ func (l *PackageRefList) Diff(r *PackageRefList, packageCollection *PackageColle
if rel < 0 {
// compaction: +(,A) -(B,) --> !(A,B)
if len(result) > 0 && result[len(result)-1].Left == nil && result[len(result)-1].Right.Name == pl.Name &&
result[len(result)-1].Right.Architecture == pl.Architecture {
result[len(result)-1].Right.IndexArchitecture() == pl.IndexArchitecture() {
result[len(result)-1] = PackageDiff{Left: pl, Right: result[len(result)-1].Right}
} else {
result = append(result, PackageDiff{Left: pl, Right: nil})
@@ -250,7 +250,7 @@ func (l *PackageRefList) Diff(r *PackageRefList, packageCollection *PackageColle
} else {
// compaction: -(A,) +(,B) --> !(A,B)
if len(result) > 0 && result[len(result)-1].Right == nil && result[len(result)-1].Left.Name == pr.Name &&
result[len(result)-1].Left.Architecture == pr.Architecture {
result[len(result)-1].Left.IndexArchitecture() == pr.IndexArchitecture() {
result[len(result)-1] = PackageDiff{Left: result[len(result)-1].Left, Right: pr}
} else {
result = append(result, PackageDiff{Left: nil, Right: pr})
@@ -365,32 +365,36 @@ func (l *PackageRefList) Merge(r *PackageRefList, overrideMatching, ignoreConfli
func (l *PackageRefList) FilterLatestRefs() {
var (
lastArch, lastName, lastVer []byte
arch, name, ver []byte
parts [][]byte
)
for i := 0; i < len(l.Refs); i++ {
parts = bytes.Split(l.Refs[i][1:], []byte(" "))
arch, name, ver = parts[0], parts[1], parts[2]
filtered := l.Refs[:0]
for _, ref := range l.Refs {
parts := bytes.Split(ref[1:], []byte(" "))
arch, name, ver := parts[0], parts[1], parts[2]
if bytes.Equal(arch, lastArch) && bytes.Equal(name, lastName) {
// Two packages are identical, check version and only one wins
vres := CompareVersions(string(ver), string(lastVer))
// Remove the older refs from the result
if vres > 0 {
// ver[i] > ver[i-1], remove element i-1
l.Refs = append(l.Refs[:i-1], l.Refs[i:]...)
} else {
// ver[i] < ver[i-1], remove element i
l.Refs = append(l.Refs[:i], l.Refs[i+1:]...)
arch, name, ver = lastArch, lastName, lastVer
// The previous ref is valid, so this also validates the current ref.
vres, err := CompareVersions(string(ver), string(lastVer))
if err != nil {
continue
}
// Compensate for the reduced set
i--
// Replace the older ref in the result.
if vres > 0 {
filtered[len(filtered)-1] = ref
lastArch, lastName, lastVer = arch, name, ver
}
continue
}
// Comparing a version with itself validates singleton refs.
if _, err := CompareVersions(string(ver), string(ver)); err != nil {
continue
}
filtered = append(filtered, ref)
lastArch, lastName, lastVer = arch, name, ver
}
l.Refs = filtered
}
+115
View File
@@ -398,3 +398,118 @@ func (s *PackageRefListSuite) TestFilterLatestRefs(c *C) {
c.Check(toStrSlice(result), DeepEquals,
[]string{"Pi386 dpkg 1.6", "Pi386 lib 1.2"})
}
func (s *PackageRefListSuite) TestFilterLatestRefsRemovesInvalidVersions(c *C) {
result := &PackageRefList{Refs: [][]byte{
[]byte("Pi386 app 1.0-"),
[]byte("Pi386 dpkg 1.0"),
[]byte("Pi386 dpkg 2.0-"),
[]byte("Pi386 lib 1.0-"),
[]byte("Pi386 lib 2.0-"),
}}
result.FilterLatestRefs()
c.Check(toStrSlice(result), DeepEquals, []string{"Pi386 dpkg 1.0"})
}
func (s *PackageRefListSuite) TestArchitectureVariantCoexistence(c *C) {
normal := &Package{Name: "example", Version: "1.0", Architecture: "amd64", V06Plus: true}
variant := &Package{Name: "example", Version: "1.0", Architecture: "amd64", ArchitectureVariant: "amd64v3", V06Plus: true}
c.Assert(s.list.Add(normal), IsNil)
c.Assert(s.list.Add(variant), IsNil)
refs := NewPackageRefListFromPackageList(s.list)
c.Check(toStrSlice(refs), DeepEquals, []string{
"Pamd64 example 1.0 00000000",
"Pamd64v3 example 1.0 00000000",
})
c.Check(refs.Has(normal), Equals, true)
c.Check(refs.Has(variant), Equals, true)
}
func (s *PackageRefListSuite) TestArchitectureVariantMerge(c *C) {
normal := &Package{Name: "example", Version: "1.0", Architecture: "amd64", V06Plus: true}
variant := &Package{Name: "example", Version: "1.0", Architecture: "amd64", ArchitectureVariant: "amd64v3", V06Plus: true}
left, right := NewPackageList(), NewPackageList()
c.Assert(left.Add(normal), IsNil)
c.Assert(right.Add(variant), IsNil)
leftRefs := NewPackageRefListFromPackageList(left)
rightRefs := NewPackageRefListFromPackageList(right)
for _, overrideMatching := range []bool{false, true} {
for _, ignoreConflicting := range []bool{false, true} {
for _, reverse := range []bool{false, true} {
l, r := leftRefs, rightRefs
if reverse {
l, r = r, l
}
merged := l.Merge(r, overrideMatching, ignoreConflicting)
c.Check(toStrSlice(merged), DeepEquals, []string{
"Pamd64 example 1.0 00000000",
"Pamd64v3 example 1.0 00000000",
}, Commentf("overrideMatching=%v ignoreConflicting=%v reverse=%v", overrideMatching, ignoreConflicting, reverse))
}
}
}
}
func (s *PackageRefListSuite) TestArchitectureVariantFilterLatestRefs(c *C) {
for _, variant := range []string{"", "amd64v3"} {
for _, version := range []string{"1.0", "2.0"} {
c.Assert(s.list.Add(&Package{
Name: "example", Version: version, Architecture: "amd64", ArchitectureVariant: variant, V06Plus: true,
}), IsNil)
}
}
refs := NewPackageRefListFromPackageList(s.list)
c.Assert(refs.Len(), Equals, 4)
refs.FilterLatestRefs()
c.Check(toStrSlice(refs), DeepEquals, []string{
"Pamd64 example 2.0 00000000",
"Pamd64v3 example 2.0 00000000",
})
}
func (s *PackageRefListSuite) TestArchitectureVariantDiffCompaction(c *C) {
db, err := goleveldb.NewOpenDB(c.MkDir())
c.Assert(err, IsNil)
defer func() { c.Check(db.Close(), IsNil) }()
coll := NewPackageCollection(db)
normal := &Package{Name: "example", Version: "1.0", Architecture: "amd64", V06Plus: true}
// Equal and differing versions must both remain separate additions/removals.
for _, version := range []string{"1.0", "2.0"} {
variant := &Package{Name: "example", Version: version, Architecture: "amd64", ArchitectureVariant: "amd64v3", V06Plus: true}
c.Assert(coll.Update(normal), IsNil)
c.Assert(coll.Update(variant), IsNil)
left, right := NewPackageList(), NewPackageList()
c.Assert(left.Add(normal), IsNil)
c.Assert(right.Add(variant), IsNil)
leftRefs := NewPackageRefListFromPackageList(left)
rightRefs := NewPackageRefListFromPackageList(right)
for _, reverse := range []bool{false, true} {
l, r := leftRefs, rightRefs
if reverse {
l, r = r, l
}
diff, err := l.Diff(r, coll)
c.Assert(err, IsNil)
context := Commentf("variant version=%s reverse=%v", version, reverse)
c.Check(diff, HasLen, 2, context)
var removed, added []string
for _, entry := range diff {
c.Check(entry.Left != nil && entry.Right != nil, Equals, false, context)
if entry.Left != nil {
removed = append(removed, string(entry.Left.Key("")))
}
if entry.Right != nil {
added = append(added, string(entry.Right.Key("")))
}
}
c.Check(removed, DeepEquals, toStrSlice(l), context)
c.Check(added, DeepEquals, toStrSlice(r), context)
}
}
}
+21 -8
View File
@@ -682,6 +682,11 @@ func (repo *RemoteRepo) ApplyFilter(dependencyOptions int, filterQuery PackageQu
emptyList := NewPackageList()
emptyList.PrepareIndex()
architectures := repo.packageList.Architectures(false)
if len(architectures) == 0 {
architectures = repo.Architectures
}
oldLen = repo.packageList.Len()
repo.packageList, err = repo.packageList.Filter(FilterOptions{
Queries: []PackageQuery{filterQuery},
@@ -689,7 +694,7 @@ func (repo *RemoteRepo) ApplyFilter(dependencyOptions int, filterQuery PackageQu
Source: emptyList,
WithSources: repo.DownloadSources,
DependencyOptions: dependencyOptions,
Architectures: repo.Architectures,
Architectures: architectures,
Progress: progress,
})
if repo.packageList != nil {
@@ -698,18 +703,26 @@ func (repo *RemoteRepo) ApplyFilter(dependencyOptions int, filterQuery PackageQu
return
}
// BuildDownloadQueue builds queue, discards current PackageList
func (repo *RemoteRepo) BuildDownloadQueue(packagePool aptly.PackagePool, packageCollection *PackageCollection, checksumStorage aptly.ChecksumStorage, skipExistingPackages, latestOnly bool) (queue []PackageDownloadTask, downloadSize int64, err error) {
// FilterLatest keeps only count latest versions of each package (per architecture)
func (repo *RemoteRepo) FilterLatest(count int) (oldLen, newLen int, 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
}
oldLen = repo.packageList.Len()
repo.packageList, err = repo.packageList.FilterLatest(count)
if repo.packageList != nil {
newLen = repo.packageList.Len()
}
return
}
// 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) {
if repo.packageList == nil {
err = fmt.Errorf("package list is empty, please (re)download package indexes")
return
}
queue = make([]PackageDownloadTask, 0, repo.packageList.Len())
+107
View File
@@ -0,0 +1,107 @@
package deb
import (
"reflect"
"sort"
"strings"
"testing"
)
// ApplyFilter operates on the package list already parsed from downloaded
// indexes. All architecture information here comes from control metadata.
func TestRemoteRepoApplyFilterArchitectureVariant(t *testing.T) {
binary := func(name, variant, arch, depends, source string) *Package {
control := "Package: " + name + "\nVersion: 1\nArchitecture: " + arch + "\n"
if variant != "" {
control += "Architecture-Variant: " + variant + "\n"
}
if depends != "" {
control += "Depends: " + depends + "\n"
}
if source != "" {
control += "Source: " + source + "\n"
}
stanza, err := NewControlFileReader(strings.NewReader(control), false, false).ReadStanza()
if err != nil {
t.Fatal(err)
}
return NewPackageFromControlFile(stanza)
}
source, err := NewSourcePackageFromControlFile(Stanza{
"Package": "source-app", "Version": "1", "Architecture": "any all", "Build-Depends": "build-tool",
})
if err != nil {
t.Fatal(err)
}
appNormal := binary("app", "", "amd64", "helper", "")
appVariant := binary("app", "amd64v3", "amd64", "helper", "")
helperNormal := binary("helper", "", "amd64", "", "")
helperVariant := binary("helper", "amd64v3", "amd64", "", "")
helperAll := binary("helper", "", "all", "", "")
appAll := binary("app", "", "all", "helper", "")
sourceVariant := binary("app", "amd64v3", "amd64", "", "source-app")
sourceNormal := binary("app", "", "amd64", "", "source-app")
buildTool := binary("build-tool", "", "amd64", "", "")
for _, tc := range []struct {
name string
indexes []string
input, want []*Package
queryName string
withDeps, withSources bool
options int
}{
{"variant_index_normal_dependency", []string{"amd64v3"}, []*Package{appVariant, helperNormal}, []*Package{appVariant, helperNormal}, "app", true, false, 0},
{"variant_index_variant_dependency", []string{"amd64v3"}, []*Package{appVariant, helperVariant}, []*Package{appVariant, helperVariant}, "app", true, false, 0},
{"variant_index_all_dependency", []string{"amd64v3"}, []*Package{appVariant, helperAll}, []*Package{appVariant, helperAll}, "app", true, false, 0},
{"variant_index_all_selected", []string{"amd64v3"}, []*Package{appAll, helperVariant}, []*Package{appAll, helperVariant}, "app", true, false, 0},
{"mixed_indexes_shared_dependency", []string{"amd64", "amd64v3"}, []*Package{appNormal, appVariant, helperNormal}, []*Package{appNormal, appVariant, helperNormal}, "app", true, false, 0},
{"mixed_indexes_shared_all_dependency", []string{"amd64", "amd64v3"}, []*Package{appNormal, appVariant, helperAll}, []*Package{appNormal, appVariant, helperAll}, "app", true, false, 0},
{"normal_index_binary_dependency", []string{"amd64"}, []*Package{appNormal, helperNormal}, []*Package{appNormal, helperNormal}, "app", true, false, 0},
{"normal_index_all_dependency", []string{"amd64"}, []*Package{appNormal, helperAll}, []*Package{appNormal, helperAll}, "app", true, false, 0},
{"dependencies_disabled", []string{"amd64v3"}, []*Package{appVariant, helperVariant}, []*Package{appVariant}, "app", false, false, 0},
{"variant_follow_source", []string{"amd64v3"}, []*Package{sourceVariant, source}, []*Package{sourceVariant, source}, "app", true, false, DepFollowSource},
{"normal_follow_source", []string{"amd64"}, []*Package{sourceNormal, source}, []*Package{sourceNormal, source}, "app", true, false, DepFollowSource},
{"download_sources_explicit_name", []string{"amd64v3"}, []*Package{sourceVariant, source}, []*Package{sourceVariant, source}, "app", true, true, 0},
// These protect existing behavior against blindly passing Architectures(false).
{"only_all_normal_index", []string{"amd64"}, []*Package{appAll, helperAll}, []*Package{appAll, helperAll}, "app", true, false, 0},
{"only_all_variant_index", []string{"amd64v3"}, []*Package{appAll, helperAll}, []*Package{appAll, helperAll}, "app", true, false, 0},
{"source_only", []string{"amd64"}, []*Package{source}, []*Package{source}, "source-app", true, true, DepFollowBuild},
// Mirrors do not currently walk source-package build dependencies. Retaining
// that behavior avoids treating the source pseudo-architecture as a CPU arch.
{"source_build_dependencies_unchanged", []string{"amd64"}, []*Package{source, buildTool}, []*Package{source}, "source-app", true, true, DepFollowBuild},
{"empty_selection", []string{"amd64v3"}, []*Package{appVariant, helperVariant}, nil, "absent", true, false, 0},
} {
t.Run(tc.name, func(t *testing.T) {
list := NewPackageList()
for _, p := range tc.input {
if err := list.Add(p); err != nil {
t.Fatal(err)
}
}
bases := list.Architectures(false)
sort.Strings(bases)
t.Logf("index architectures=%v; metadata base architectures=%v", tc.indexes, bases)
repo := &RemoteRepo{Architectures: append([]string{}, tc.indexes...), FilterWithDeps: tc.withDeps, DownloadSources: tc.withSources, packageList: list}
oldLen, newLen, err := repo.ApplyFilter(tc.options, &FieldQuery{Field: "Name", Relation: VersionEqual, Value: tc.queryName}, nil)
if err != nil {
t.Fatal(err)
}
if oldLen != len(tc.input) {
t.Errorf("oldLen=%d, want %d", oldLen, len(tc.input))
}
got := repo.packageList.FullNames()
want := make([]string, 0, len(tc.want))
for _, p := range tc.want {
want = append(want, p.GetFullName())
}
sort.Strings(got)
sort.Strings(want)
if newLen != len(tc.want) || !reflect.DeepEqual(got, want) {
t.Errorf("filtered packages (newLen=%d):\n got %v\nwant %v", newLen, got, want)
}
if !reflect.DeepEqual(repo.Architectures, tc.indexes) {
t.Errorf("index configuration changed: %v", repo.Architectures)
}
})
}
}
+123 -15
View File
@@ -1,6 +1,7 @@
package deb
import (
"crypto/sha256"
"errors"
"fmt"
"io"
@@ -308,6 +309,97 @@ func (s *RemoteRepoSuite) TestRefKey(c *C) {
c.Assert(s.repo.RefKey()[1:], DeepEquals, s.repo.Key()[1:])
}
type architectureVariantRemoteProgress struct {
aptly.Progress
messages []string
}
func (p *architectureVariantRemoteProgress) ColoredPrintf(format string, args ...interface{}) {
p.messages = append(p.messages, fmt.Sprintf(format, args...))
}
func (s *RemoteRepoSuite) loadArchitectureVariantIndexes(c *C) {
var err error
s.repo, err = NewRemoteRepo("variant-test", "http://mirror.example.invalid/repo", "test", []string{"main"}, nil, false, false, false, false)
c.Assert(err, IsNil)
const normalIndex = `Package: example
Version: 1.0
Architecture: amd64
Filename: pool/main/e/example/example_1.0_amd64.deb
Size: 3
MD5sum: 900150983cd24fb0d6963f7d28e17f72
`
const variantIndex = `Package: example
Version: 1.0
Architecture: amd64
Architecture-Variant: amd64v3
Filename: pool/main/e/example/example_1.0_amd64v3.deb
Size: 4
MD5sum: e2fc714c4727ee9395f324cd2e7f331f
`
const root = "http://mirror.example.invalid/repo/dists/test/"
release := fmt.Sprintf("Architectures: amd64 amd64v3\nComponents: main\nSHA256:\n %x %d main/binary-amd64/Packages\n %x %d main/binary-amd64v3/Packages\n",
sha256.Sum256([]byte(normalIndex)), len(normalIndex), sha256.Sum256([]byte(variantIndex)), len(variantIndex))
s.downloader = http.NewFakeDownloader().ExpectResponse(root+"Release", release)
s.downloader.ExpectResponse(root+"main/binary-amd64/Packages", normalIndex)
s.downloader.ExpectResponse(root+"main/binary-amd64v3/Packages", variantIndex)
c.Assert(s.repo.Fetch(s.downloader, nil, true), IsNil)
c.Check(s.repo.Architectures, DeepEquals, []string{"amd64", "amd64v3"})
progress := &architectureVariantRemoteProgress{Progress: s.progress}
c.Assert(s.repo.DownloadPackageIndexes(progress, s.downloader, nil, s.collectionFactory, true, false), IsNil)
c.Check(s.downloader.Empty(), Equals, true)
// Duplicate rejection is reported through ColoredPrintf, not as an error.
c.Check(progress.messages, HasLen, 0)
c.Assert(s.repo.packageList.Len(), Equals, 2)
}
func (s *RemoteRepoSuite) TestArchitectureVariantPackageIndexes(c *C) {
s.loadArchitectureVariantIndexes(c)
packages := map[string]*Package{}
c.Assert(s.repo.packageList.ForEach(func(p *Package) error {
packages[p.IndexArchitecture()] = p
return nil
}), IsNil)
c.Assert(packages, HasLen, 2)
for _, arch := range []string{"amd64", "amd64v3"} {
p := packages[arch]
c.Assert(p, NotNil)
c.Check(p.Name, Equals, "example")
c.Check(p.Version, Equals, "1.0")
c.Check(p.Architecture, Equals, "amd64")
variant := ""
if arch == "amd64v3" {
variant = "amd64v3"
}
c.Check(p.ArchitectureVariant, Equals, variant)
c.Check(p.IndexArchitecture(), Equals, arch)
c.Assert(p.Files(), HasLen, 1)
c.Check(p.Files()[0].Filename, Equals, "example_1.0_"+arch+".deb")
c.Check(p.Files()[0].DownloadURL(), Equals, "pool/main/e/example/example_1.0_"+arch+".deb")
}
}
func (s *RemoteRepoSuite) TestArchitectureVariantDownloadQueue(c *C) {
s.loadArchitectureVariantIndexes(c)
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
c.Assert(err, IsNil)
c.Check(queue, HasLen, 2)
c.Check(size, Equals, int64(7))
var urls []string
for _, task := range queue {
urls = append(urls, task.File.DownloadURL())
c.Check(task.Additional, HasLen, 0)
}
sort.Strings(urls)
c.Check(urls, DeepEquals, []string{
"pool/main/e/example/example_1.0_amd64.deb",
"pool/main/e/example/example_1.0_amd64v3.deb",
})
}
func (s *RemoteRepoSuite) TestDownload(c *C) {
s.repo.Architectures = []string{"i386"}
@@ -322,7 +414,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, false)
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(3))
c.Check(queue, HasLen, 1)
@@ -349,7 +441,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, false)
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(0))
c.Check(queue, HasLen, 0)
@@ -370,7 +462,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, false)
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(3))
c.Check(queue, HasLen, 1)
@@ -397,7 +489,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, false)
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(3)+int64(len(exampleInstallerManifestFile)))
c.Check(queue, HasLen, 2)
@@ -423,7 +515,7 @@ func (s *RemoteRepoSuite) TestDownloadWithInstaller(c *C) {
c.Check(pkg.Name, Equals, "installer")
}
func (s *RemoteRepoSuite) TestBuildDownloadQueueLatestOnly(c *C) {
func (s *RemoteRepoSuite) TestFilterLatest(c *C) {
s.repo.Architectures = []string{"i386"}
err := s.repo.Fetch(s.downloader, nil, true)
@@ -445,13 +537,29 @@ func (s *RemoteRepoSuite) TestBuildDownloadQueueLatestOnly(c *C) {
newest := NewPackageFromControlFile(stanza)
_ = s.repo.packageList.Add(newest)
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false, true)
oldLen, newLen, err := s.repo.FilterLatest(1)
c.Assert(err, IsNil)
c.Check(oldLen, Equals, 2)
c.Check(newLen, Equals, 1)
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
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) TestFilterLatestErrors(c *C) {
_, _, err := s.repo.FilterLatest(1)
c.Assert(err, ErrorMatches, "package list is empty, please \\(re\\)download package indexes")
s.repo.packageList = NewPackageList()
oldLen, newLen, err := s.repo.FilterLatest(0)
c.Assert(err, ErrorMatches, "number of latest versions should be at least 1, got 0")
c.Check(oldLen, Equals, 0)
c.Check(newLen, Equals, 0)
}
func (s *RemoteRepoSuite) TestDownloadWithSources(c *C) {
s.repo.Architectures = []string{"i386"}
s.repo.DownloadSources = true
@@ -470,7 +578,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, false)
queue, size, err := s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(15))
c.Check(queue, HasLen, 4)
@@ -514,7 +622,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, false)
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(0))
c.Check(queue, HasLen, 0)
@@ -539,7 +647,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, false)
queue, size, err = s.repo.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(15))
c.Check(queue, HasLen, 4)
@@ -563,7 +671,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, false)
queue, size, err := s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(3))
c.Check(queue, HasLen, 1)
@@ -591,7 +699,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, false)
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(0))
c.Check(queue, HasLen, 0)
@@ -613,7 +721,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, false)
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(3))
c.Check(queue, HasLen, 1)
@@ -644,7 +752,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, false)
queue, size, err := s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(15))
c.Check(queue, HasLen, 4)
@@ -690,7 +798,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, false)
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, true)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(0))
c.Check(queue, HasLen, 0)
@@ -716,7 +824,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, false)
queue, size, err = s.flat.BuildDownloadQueue(s.packagePool, s.collectionFactory.PackageCollection(), s.cs, false)
c.Assert(err, IsNil)
c.Check(size, Equals, int64(15))
c.Check(queue, HasLen, 4)
+7
View File
@@ -0,0 +1,7 @@
Format: 1.0
Source: aptly-test-invalid-version
Binary: aptly-test-invalid-version
Architecture: any
Version: 1.2.3-
Maintainer: Aptly Test <test@example.com>
Standards-Version: 3.9.3
+92 -8
View File
@@ -6,38 +6,86 @@ import (
"strconv"
"strings"
"unicode"
"github.com/rs/zerolog/log"
)
var (
upstreamVersionRegex = regexp.MustCompile(`^[0-9][A-Za-z0-9.+~\-]*$`)
debianRevisionRegex = regexp.MustCompile(`^[A-Za-z0-9.+~]*$`)
)
// VersionCompareError identifies an invalid operand passed to CompareVersions.
type VersionCompareError string
// Error implements the error interface.
func (e VersionCompareError) Error() string {
return string(e)
}
// CompareVersions errors.
const (
LeftInvalid VersionCompareError = "left version is invalid"
RightInvalid VersionCompareError = "right version is invalid"
BothInvalid VersionCompareError = "both versions are invalid"
)
// Using documentation from: http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version
// CompareVersions compares two package versions
func CompareVersions(ver1, ver2 string) int {
e1, u1, d1 := parseVersion(ver1)
e2, u2, d2 := parseVersion(ver2)
func CompareVersions(ver1, ver2 string) (int, error) {
e1, u1, d1, err1 := parseVersion(ver1)
e2, u2, d2, err2 := parseVersion(ver2)
if err1 != nil {
log.Warn().Err(err1).Str("version", ver1).Msg("Invalid package version")
}
if err2 != nil && (err1 == nil || ver1 != ver2) {
log.Warn().Err(err2).Str("version", ver2).Msg("Invalid package version")
}
if err1 != nil && err2 != nil {
return 0, BothInvalid
}
if err1 != nil {
return 0, LeftInvalid
}
if err2 != nil {
return 0, RightInvalid
}
r := compareVersionPart(e1, e2)
if r != 0 {
return r
return r, nil
}
r = compareVersionPart(u1, u2)
if r != 0 {
return r
return r, nil
}
return compareVersionPart(d1, d2)
return compareVersionPart(d1, d2), nil
}
// parseVersions breaks down full version to components (possibly empty)
func parseVersion(ver string) (epoch, upstream, debian string) {
func parseVersion(ver string) (epoch, upstream, debian string, err error) {
i := strings.Index(ver, ":")
if i != -1 {
epoch, ver = ver[:i], ver[i+1:]
}
i = strings.Index(ver, "-")
// Debian policy specifies that the upstream_version and debian_revision
// are separated by the LAST hyphen in the string, since upstream_version
// itself may legitimately contain hyphens.
i = strings.LastIndex(ver, "-")
if i != -1 {
debian, ver = ver[i+1:], ver[:i]
if debian == "" {
// if a hyphen is detected in the upstream version
// string without a debian revision following it, the
// version is invalid
return "", "", "", fmt.Errorf("could not parse version: version string ('%s-') includes hyphen without Debian revision", ver)
}
}
upstream = ver
@@ -45,6 +93,42 @@ func parseVersion(ver string) (epoch, upstream, debian string) {
return
}
// isValidVersion checks whether package version is compliant with control field spec
// source: https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-version
func isValidVersion(ver string) bool {
epoch, upstream, deb, err := parseVersion(ver)
if err != nil {
return false
}
// validate epoch component
if epoch != "" {
// uint64 for unexpectedly high epoch values
_, err := strconv.ParseUint(epoch, 10, 64)
if err != nil {
return false
}
}
if upstream == "" || !isValidUpstreamVersion(upstream) {
return false
}
if deb != "" && !isValidDebianRevision(deb) {
return false
}
return true
}
func isValidUpstreamVersion(upstream string) bool {
return upstreamVersionRegex.MatchString(upstream)
}
func isValidDebianRevision(debian string) bool {
return debianRevisionRegex.MatchString(debian)
}
// compareLexicographic compares in "Debian lexicographic" way, see below compareVersionPart for details
func compareLexicographic(s1, s2 string) int {
i := 0
+108 -18
View File
@@ -10,20 +10,82 @@ type VersionSuite struct {
var _ = Suite(&VersionSuite{})
func (s *VersionSuite) TestParseVersion(c *C) {
e, u, d := parseVersion("1.3.4")
e, u, d, err := parseVersion("1.3.4")
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3.4", ""})
c.Check(err, Equals, nil)
e, u, d = parseVersion("4:1.3:4")
e, u, d, err = parseVersion("4:1.3:4")
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3:4", ""})
c.Check(err, Equals, nil)
e, u, d = parseVersion("1.3.4-1")
e, u, d, err = parseVersion("1.3.4-1")
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3.4", "1"})
c.Check(err, Equals, nil)
e, u, d = parseVersion("1.3-pre4-1")
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3", "pre4-1"})
// upstream_version and debian_revision are separated by the LAST
// hyphen, since upstream_version may itself contain hyphens.
e, u, d, err = parseVersion("1.3-pre4-1")
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3-pre4", "1"})
c.Check(err, Equals, nil)
e, u, d = parseVersion("4:1.3-pre4-1")
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3", "pre4-1"})
e, u, d, err = parseVersion("4:1.3-pre4-1")
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3-pre4", "1"})
c.Check(err, Equals, nil)
e, u, d, err = parseVersion("1:2026.07.07-0325-54-a773b756")
c.Check([]string{e, u, d}, DeepEquals, []string{"1", "2026.07.07-0325-54", "a773b756"})
c.Check(err, Equals, nil)
e, u, d, err = parseVersion("1:1.2024-")
c.Check([]string{e, u, d}, DeepEquals, []string{"", "", ""})
c.Check(err.Error(), Equals, "could not parse version: version string ('1.2024-') includes hyphen without Debian revision")
}
func (s *VersionSuite) TestIsValidVersion(c *C) {
// valid cases
valid := isValidVersion("1.2.3-abc")
c.Check(valid, Equals, true)
valid = isValidVersion("1.0.1337~rc2-3")
c.Check(valid, Equals, true)
valid = isValidVersion("1.2.3+fdsfgs")
c.Check(valid, Equals, true)
valid = isValidVersion("1:2.3~4-5six")
c.Check(valid, Equals, true)
// upstream_version containing multiple hyphens (e.g. date/build-number/
// git-sha style versioning) is valid as long as it's split on the LAST
// hyphen for the debian_revision.
valid = isValidVersion("1:2026.07.07-0325-54-a773b756")
c.Check(valid, Equals, true)
// invalid cases
valid = isValidVersion("1:1.2.3-")
c.Check(valid, Equals, false)
valid = isValidVersion("42:")
c.Check(valid, Equals, false)
valid = isValidVersion("1:a.1.2.3~-4")
c.Check(valid, Equals, false)
// non-numeric epoch
valid = isValidVersion("abc:1.2.3")
c.Check(valid, Equals, false)
// valid upstream_version, but debian_revision contains a disallowed character
valid = isValidVersion("1.2.3-abc!")
c.Check(valid, Equals, false)
e, u, d, err := parseVersion("1.3-pre4-1")
c.Check(err, IsNil)
c.Check([]string{e, u, d}, DeepEquals, []string{"", "1.3-pre4", "1"})
e, u, d, err = parseVersion("4:1.3-pre4-1")
c.Check(err, IsNil)
c.Check([]string{e, u, d}, DeepEquals, []string{"4", "1.3-pre4", "1"})
}
func (s *VersionSuite) TestCompareLexicographic(c *C) {
@@ -86,21 +148,49 @@ func (s *VersionSuite) TestCompareVersionPart(c *C) {
}
func (s *VersionSuite) TestCompareVersions(c *C) {
c.Check(CompareVersions("3:1.0~beta1~svn1245-1", "3:1.0~beta1~svn1245-1"), Equals, 0)
check := func(left, right string, expectedCmp int, expectedErr error) {
cmp, err := CompareVersions(left, right)
c.Check(cmp, Equals, expectedCmp)
c.Check(err, Equals, expectedErr)
}
c.Check(CompareVersions("1:1.0~beta1~svn1245-1", "3:1.0~beta1~svn1245-1"), Equals, -1)
c.Check(CompareVersions("1:1.0~beta1~svn1245-1", "1.0~beta1~svn1245-1"), Equals, 1)
c.Check(CompareVersions("1.0~beta1~svn1245-1", "1.0~beta1~svn1245-2"), Equals, -1)
c.Check(CompareVersions("3:1.0~beta1~svn1245-1", "3:1.0~beta1-1"), Equals, -1)
check("3:1.0~beta1~svn1245-1", "3:1.0~beta1~svn1245-1", 0, nil)
c.Check(CompareVersions("1.0~beta1~svn1245", "1.0~beta1"), Equals, -1)
c.Check(CompareVersions("1.0~beta1", "1.0"), Equals, -1)
check("1:1.0~beta1~svn1245-1", "3:1.0~beta1~svn1245-1", -1, nil)
check("1:1.0~beta1~svn1245-1", "1.0~beta1~svn1245-1", 1, nil)
check("1.0~beta1~svn1245-1", "1.0~beta1~svn1245-2", -1, nil)
check("3:1.0~beta1~svn1245-1", "3:1.0~beta1-1", -1, nil)
c.Check(CompareVersions("1.0-133-avc", "1.1"), Equals, -1)
c.Check(CompareVersions("1.0-133-avc", "1.0"), Equals, 1)
check("1.0~beta1~svn1245", "1.0~beta1", -1, nil)
check("1.0~beta1", "1.0", -1, nil)
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)
check("1.0-133-avc", "1.1", -1, nil)
check("1.0-133-avc", "1.0", 1, nil)
check("5.2.0.3", "5.2.0.283", -1, nil)
// upstream_version/debian_revision split on the LAST hyphen: "4.3.5-rc3-1"
// is upstream="4.3.5-rc3", debian="1", so it's actually greater than "4.3.5a"
// (confirmed against `dpkg --compare-versions`).
check("4.3.5a", "4.3.5-rc3-1", -1, nil)
// Version validation happens independently, so these exercise parse failures.
check("1:abc~1.2.3-", "1:1.2.3~abc-good", 0, LeftInvalid)
check("1:1.2.3~abc-good", "1:abc~1.2.3-", 0, RightInvalid)
check("1:abc~1.2.3-", "2:def~4.5.6-", 0, BothInvalid)
// The following tests exercise the last-hyphen split and additional
// comparison edge cases (added by PR #1646, rebased onto master).
check("1.6.0-rc1+really1.6.0-0", "1.6.0-rc1-0", 1, nil)
check("1.2-3-4", "1.2-3-5", -1, nil)
check("1.0-1-1", "1.0-1", 1, nil)
check("1.0+1", "1.0", 1, nil)
check("1.0+a-1", "1.0-a-1", -1, nil)
check("1.0+1-1", "1.0-1", 1, nil)
check("1.0-1+b1", "1.0-1", 1, nil)
check("1.0-1+b1", "1.0-1~bpo1", 1, nil)
check("1.0-1+b2", "1.0-1+b10", -1, nil)
check("1.0.1+dfsg-1", "1.0.1-1", 1, nil)
check("1.0-1+b1", "1.0-2", -1, nil)
}
func (s *VersionSuite) TestParseDependency(c *C) {
+35 -14
View File
@@ -32,6 +32,17 @@ database_open_attempts: -1
architectures:
# - amd64
# Hash algorithms to write Acquire-By-Hash entries for, when a publish
# has Acquire-By-Hash enabled. Each algorithm costs a full set of index
# copies on every publish, which is felt most on S3-like storage where
# every write is a round trip. An empty list defaults to all four.
# Modern apt uses SHA256; MD5Sum and SHA1 are deprecated.
acquire_by_hash_algorithms:
- MD5Sum
- SHA1
- SHA256
- SHA512
# OBSOLETE
# in aptly up to version 1.0.0, package files were stored in internal package pool
# with MD5-dervied path, since 1.1.0 package pool layout was changed;
@@ -51,13 +62,13 @@ dep_follow_suggests: false
dep_follow_recommends: false
# When dependency looks like `package-a | package-b`, follow both variants always
dep_follow_allvariants: false
dep_follow_all_variants: false
# Follow dependency from binary package to source package
dep_follow_source: false
# Log additional details while resolving dependencies (useful for debugging)
dep_verbose_resolve: false
dep_verboseresolve: false
# PPA
@@ -86,6 +97,9 @@ enable_metrics_endpoint: false
# Enable API documentation on /docs
enable_swagger_endpoint: false
# Enable pprof profiling endpoints on /api/debug/pprof/*
enable_pprof_endpoint: false
# OBSOLETE: use via url param ?_async=true
async_api: false
@@ -143,6 +157,10 @@ gpg_disable_sign: false
# Disable signature verification of remote repositories
gpg_disable_verify: false
# GPG key IDs to sign with when a publish does not name one itself
# (the -gpg-key flag, or GpgKey in the API's signing options)
gpg_keys: []
# Publishing
#############
@@ -153,6 +171,10 @@ skip_contents_publishing: false
# Do not create bz2 files
skip_bz2_publishing: false
# Number of index files renamed in parallel when a publish finishes
# (set to 1 to rename them one at a time)
publish_rename_concurrency: 4
# Storage
##########
@@ -185,18 +207,6 @@ filesystem_publish_endpoints:
# # * size (compare file size)
# verify_method: md5
# S3 Endpoint Support
#
# cloud storage). First, publishing
# endpoints should be described in aptly configuration file. Each endpoint has name
# and associated settings.
#
# In order to publish to S3, specify endpoint as `s3:endpoint-name:` before
# publishing prefix on the command line, e.g.:
#
# `aptly publish snapshot wheezy-main s3:test:`
#
# JFrog Artifactory Endpoint Support
#
# aptly can be configured to publish repositories directly to JFrog Artifactory. First,
@@ -225,6 +235,17 @@ jfrog_publish_endpoints:
# api_key: api_key
# access_token: access_token
# S3 Endpoint Support
#
# cloud storage). First, publishing
# endpoints should be described in aptly configuration file. Each endpoint has name
# and associated settings.
#
# In order to publish to S3, specify endpoint as `s3:endpoint-name:` before
# publishing prefix on the command line, e.g.:
#
# `aptly publish snapshot wheezy-main s3:test:`
#
s3_publish_endpoints:
# # Endpoint Name
# test:
+52
View File
@@ -1,3 +1,55 @@
aptly (1.6.3) stable; urgency=medium
* NEW FEATURES:
* Google Cloud Storage (GCS) publish backend (https://github.com/aptly-dev/aptly/pull/1550)
* dput-compatible file upload API (https://github.com/aptly-dev/aptly/pull/1436)
* JFrog Artifactory publish backend (https://github.com/aptly-dev/aptly/pull/1553)
* AppStream (DEP-11) mirror support (https://github.com/aptly-dev/aptly/pull/1543)
* Multiple GPG keys support (https://github.com/aptly-dev/aptly/pull/1479)
* GPG key list & delete API (https://github.com/aptly-dev/aptly/pull/1558)
* Edit mirror API endpoint (https://github.com/aptly-dev/aptly/pull/1535)
* NumPackages in list responses (https://github.com/aptly-dev/aptly/pull/1559)
* Mirror latest packages (https://github.com/aptly-dev/aptly/pull/1513)
* Reproducible builds / `SOURCE_DATE_EPOCH` support (https://github.com/aptly-dev/aptly/pull/1537), https://github.com/aptly-dev/aptly/pull/1542)
* `Release` file `Version` field support (https://github.com/aptly-dev/aptly/pull/1533)
* `InRelease` file `Signed-By` field support (https://github.com/aptly-dev/aptly/pull/1518), https://github.com/aptly-dev/aptly/pull/1519)
* GCP / Google Artifact Registry authentication (https://github.com/aptly-dev/aptly/pull/1505)
* Update publish label & origin (https://github.com/aptly-dev/aptly/pull/1484)
* Ubuntu 26.04 / resolute builds (https://github.com/aptly-dev/aptly/pull/1571)
* BUG FIXES:
* Race condition & concurrency fixes for the REST API (https://github.com/aptly-dev/aptly/pull/1574)
* Fix empty line in `Package-List` for source packages (https://github.com/aptly-dev/aptly/pull/1588)
* Publish: check storage exists before publishing (https://github.com/aptly-dev/aptly/pull/1587)
* S3 publish race condition (https://github.com/aptly-dev/aptly/pull/1594)
* Repo edit name optionally (https://github.com/aptly-dev/aptly/pull/1593)
* Fix crash in `aptly db recover` (https://github.com/aptly-dev/aptly/pull/1565)
* Fix deadlocks in task list (https://github.com/aptly-dev/aptly/pull/1529)
* Fix S3 re-upload issue (https://github.com/aptly-dev/aptly/pull/1480)
* Fix `aptly repo edit` API (https://github.com/aptly-dev/aptly/pull/1493)
* Fix out-of-disk-space error handling (https://github.com/aptly-dev/aptly/pull/1504)
* Fix `aptly mirror update` removing unrelated params (https://github.com/aptly-dev/aptly/pull/1466)
* Fix concurrent pool linking race condition (https://github.com/aptly-dev/aptly/pull/1481)
* Fix `dpkg`-compliant version comparison (https://github.com/aptly-dev/aptly/pull/1509)
* Fix Swagger property casing and spec errors (https://github.com/aptly-dev/aptly/pull/1510), https://github.com/aptly-dev/aptly/pull/1498)
* Remove useless nil check (https://github.com/aptly-dev/aptly/pull/1482)
* Format Go code with gofmt (https://github.com/aptly-dev/aptly/pull/1483)
* DEPENDENCIES CHANGES:
* Go toolchain → 1.25.0
* `go.opentelemetry.io/otel` → v1.41.0 (https://github.com/aptly-dev/aptly/pull/1586)
* `go.opentelemetry.io/otel/sdk` → v1.43.0 (https://github.com/aptly-dev/aptly/pull/1584)
* `github.com/go-jose/go-jose/v4` → v4.1.4 (https://github.com/aptly-dev/aptly/pull/1585)
* `github.com/go-git/go-git/v5` → v5.19.1 (https://github.com/aptly-dev/aptly/pull/1590)
* `github.com/ulikunitz/xz` → v0.5.15 (fixes 32-bit build failures)
* `golang.org/x/crypto` → v0.45.0 (https://github.com/aptly-dev/aptly/pull/1506)
* `google.golang.org/grpc` → v1.79.3 (https://github.com/aptly-dev/aptly/pull/1546)
* `github.com/aws/aws-sdk-go-v2/service/s3` → v1.97.3 (https://github.com/aptly-dev/aptly/pull/1554)
* `github.com/cloudflare/circl` → v1.6.3 (https://github.com/aptly-dev/aptly/pull/1461), https://github.com/aptly-dev/aptly/pull/1541)
* `requests` (Python, system tests) → 2.33.0 (https://github.com/aptly-dev/aptly/pull/1460), https://github.com/aptly-dev/aptly/pull/1547)
* `github.com/ProtonMail/go-crypto` → v1.4.0
* `golang.org/x/net` → v0.48.0
-- André Roth <neolynx@gmail.com> Wed, 24 Jun 2026 18:47:05 +0200
aptly (1.6.2) stable; urgency=medium
* doc: add swagger doc for /api/gpg/key (https://github.com/aptly-dev/aptly/pull/1456)
+30 -29
View File
@@ -1,6 +1,6 @@
module github.com/aptly-dev/aptly
go 1.25.0
go 1.26.0
require (
github.com/AlekSi/pointer v1.1.0
@@ -13,7 +13,7 @@ require (
github.com/h2non/filetype v1.1.3
github.com/jlaffaye/ftp v0.2.0 // indirect
github.com/kjk/lzma v0.0.0-20120628231508-2a7c55cad4a2
github.com/klauspost/compress v1.18.2
github.com/klauspost/compress v1.18.7
github.com/klauspost/pgzip v1.2.6
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
@@ -32,18 +32,18 @@ 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.50.0 // indirect
golang.org/x/sys v0.43.0
golang.org/x/term v0.42.0
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/sys v0.48.0
golang.org/x/term v0.45.0
golang.org/x/time v0.14.0
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
)
require (
cel.dev/expr v0.25.1 // indirect
cel.dev/expr v0.25.2 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
@@ -52,7 +52,7 @@ require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/CycloneDX/cyclonedx-go v0.9.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
@@ -78,14 +78,14 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/forPelevin/gomoji v1.3.0 // indirect
@@ -93,9 +93,9 @@ require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-git/go-git/v5 v5.19.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-git/go-git/v5 v5.19.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.6 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
@@ -148,7 +148,7 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
@@ -158,26 +158,27 @@ require (
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/otel v1.47.0 // indirect
go.opentelemetry.io/otel/log v1.47.0 // indirect
go.opentelemetry.io/otel/metric v1.47.0 // indirect
go.opentelemetry.io/otel/sdk v1.47.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.47.0 // indirect
go.opentelemetry.io/otel/trace v1.47.0 // indirect
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/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.44.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.83.2 // indirect
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@@ -200,7 +201,7 @@ require (
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.35.0
golang.org/x/oauth2 v0.36.0
google.golang.org/api v0.266.0
gopkg.in/yaml.v3 v3.0.1
)
+68 -64
View File
@@ -1,10 +1,10 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
@@ -44,8 +44,8 @@ github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk
github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg=
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/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
@@ -131,8 +131,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
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/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
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=
@@ -158,13 +158,13 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -193,15 +193,15 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.6 h1:sNRkftxFJxwsILiQukqp5z83QBSmntVwmgRY7AqBDO4=
github.com/go-jose/go-jose/v4 v4.1.6/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
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/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@@ -315,8 +315,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/kjk/lzma v0.0.0-20120628231508-2a7c55cad4a2 h1:TVZQgMi+I83S3rCuE65HnmDO6+wFPRi3n2LOzr+tr68=
github.com/kjk/lzma v0.0.0-20120628231508-2a7c55cad4a2/go.mod h1:phT/jsRPBAEqjAibu1BurrabCBNTYiVI+zbmyCZJY6Q=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -446,8 +446,8 @@ github.com/smira/go-ftp-protocol v0.0.0-20140829150050-066b75c2b70d h1:rvtR4+9N2
github.com/smira/go-ftp-protocol v0.0.0-20140829150050-066b75c2b70d/go.mod h1:Jm7yHrROA5tC42gyJ5EwiR8EWp0PUy0qOc4sE7Y8Uzo=
github.com/smira/go-xz v0.1.0 h1:1zVLT1sITUKcWNysfHMLZWJ2Yh7yJfhREsgmUdK4zb0=
github.com/smira/go-xz v0.1.0/go.mod h1:OmdEWnIIkuLzRLHGF4YtjDzF9VFUevEcP6YxDPRqVrs=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4=
github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -463,8 +463,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
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=
@@ -515,32 +515,36 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
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/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM=
go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k=
go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
@@ -549,8 +553,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
@@ -560,8 +564,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
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.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -581,11 +585,11 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -593,8 +597,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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=
@@ -623,20 +627,20 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -649,15 +653,15 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
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=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -667,17 +671,17 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
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/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
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=
+4 -5
View File
@@ -163,11 +163,10 @@ func (downloader *downloaderImpl) newRequest(ctx context.Context, method, url st
req.Close = true
req = req.WithContext(ctx)
proxyURL, _ := downloader.client.Transport.(*http.Transport).Proxy(req)
if proxyURL == nil && (req.URL.Scheme == "http" || req.URL.Scheme == "https") {
req.URL.Opaque = strings.Replace(req.URL.RequestURI(), "+", "%2b", -1)
req.URL.RawQuery = ""
}
// Some servers (Amazon S3) read a literal "+" in a path as a space, so keep
// it percent-encoded. RawPath does this without hiding the host the way
// URL.Opaque does, so error messages still show the full URL.
req.URL.RawPath = strings.ReplaceAll(req.URL.EscapedPath(), "+", "%2b")
return req, nil
}
+10
View File
@@ -109,6 +109,16 @@ func (s *DownloaderSuite) TestDownloadWithChecksum(c *C) {
c.Check(checksums.SHA512, Equals, "bac18bf4e564856369acc2ed57300fecba3a2c1af5ae8304021e4252488678feb18118466382ee4e1210fe1f065080210e453a80cfb37ccb8752af3269df160e")
}
func (s *DownloaderSuite) TestNewRequestEscapesPlus(c *C) {
d := s.d.(*downloaderImpl)
req, err := d.newRequest(s.ctx, "GET", s.url+"/test/pkg_1.0+dfsg-1_amd64.deb?a=b")
c.Assert(err, IsNil)
c.Check(req.URL.RequestURI(), Equals, "/test/pkg_1.0%2bdfsg-1_amd64.deb?a=b")
// the host must stay visible, it ends up in error messages
c.Check(req.URL.String(), Equals, s.url+"/test/pkg_1.0%2bdfsg-1_amd64.deb?a=b")
}
func (s *DownloaderSuite) TestDownload404(c *C) {
c.Assert(s.d.Download(s.ctx, s.url+"/doesntexist", s.tempfile.Name()),
ErrorMatches, "HTTP code 404.*")
+34 -2
View File
@@ -71,6 +71,13 @@ The legacy json configuration is still supported (and also supports comments):
// empty array defaults to all available architectures
"architectures": [],
// Hash algorithms to write Acquire\-By\-Hash entries for, when a publish
// has Acquire\-By\-Hash enabled. Each algorithm costs a full set of index
// copies on every publish, which is felt most on S3\-like storage where
// every write is a round trip. An empty array defaults to all four.
// Modern apt uses SHA256; MD5Sum and SHA1 are deprecated.
"acquireByHashAlgorithms": ["MD5Sum", "SHA1", "SHA256", "SHA512"],
// Follow contents of `Suggests:` field when processing dependencies for the package
"dependencyFollowSuggests": false,
@@ -93,6 +100,9 @@ The legacy json configuration is still supported (and also supports comments):
// Codename for short PPA url expansion
"ppaCodename": "",
// PPA Base URL
"ppaBaseURL": "http://ppa.launchpad.net",
// OBSOLETE
// in aptly up to version 1\.0\.0, package files were stored in internal package pool
// with MD5\-dervied path, since 1\.1\.0 package pool layout was changed;
@@ -114,6 +124,9 @@ The legacy json configuration is still supported (and also supports comments):
// Enable API documentation on /docs
"enableSwaggerEndpoint": false,
// Enable pprof profiling endpoints on /api/debug/pprof/*
"enablePprofEndpoint": false,
// OBSOLETE: use via url param ?_async=true
"AsyncAPI": false,
@@ -174,6 +187,10 @@ The legacy json configuration is still supported (and also supports comments):
// Disable signature verification of remote repositories
"gpgDisableVerify": false,
// GPG key IDs to sign with when a publish does not name one itself
// (the \-gpg\-key flag, or GpgKey in the API's signing options)
"gpgKeys": [],
// Publishing
//////////////
@@ -184,6 +201,10 @@ The legacy json configuration is still supported (and also supports comments):
// Do not create bz2 files
"skipBz2Publishing": false,
// Number of index files renamed in parallel when a publish finishes
// (set to 1 to rename them one at a time)
"publishRenameConcurrency": 4,
// Storage
///////////
@@ -804,7 +825,11 @@ do not check file existence for packages listed in the internal database of the
.
.TP
\-\fBlatest\fR
download only latest version of each package (per architecture)
download only latest version of each package (per architecture), same as \-max\-versions=1
.
.TP
\-\fBmax\-versions\fR=0
download only the N most recent versions of each package (per architecture), 0 means all versions
.
.SH "RENAMES MIRROR"
\fBaptly\fR \fBmirror\fR \fBrename\fR \fIold\-name\fR \fInew\-name\fR
@@ -2437,10 +2462,13 @@ host:port for HTTP listening
Start HTTP server with aptly REST API\. The server can listen to either a port or Unix domain socket\. When using a socket, Aptly will fully manage the socket file\. This command also supports taking over from a systemd file descriptors to enable systemd socket activation\.
.
.P
An optional metrics\-only TCP listener can be enabled with \-metrics\-listen\. It serves GET and HEAD requests at /metrics and requires enableMetricsEndpoint in the aptly configuration\. This listener uses plain HTTP; provide access control and TLS through network policy or a proxy when required\.
.
.P
Example:
.
.P
$ aptly api serve \-listen=:8080 $ aptly api serve \-listen=unix:///tmp/aptly\.sock
$ aptly api serve \-listen=:8080 $ aptly api serve \-listen=unix:///tmp/aptly\.sock $ aptly api serve \-listen=:8080 \-metrics\-listen=127\.0\.0\.1:9090
.
.P
Options:
@@ -2450,6 +2478,10 @@ Options:
host:port for HTTP listening or unix://path to listen on a Unix domain socket
.
.TP
\-\fBmetrics\-listen\fR=
host:port for optional metrics\-only HTTP listening (requires enableMetricsEndpoint)
.
.TP
\-\fBno\-lock\fR
don\(cqt lock the database
.
+21
View File
@@ -60,6 +60,13 @@ The legacy json configuration is still supported (and also supports comments):
// empty array defaults to all available architectures
"architectures": [],
// Hash algorithms to write Acquire-By-Hash entries for, when a publish
// has Acquire-By-Hash enabled. Each algorithm costs a full set of index
// copies on every publish, which is felt most on S3-like storage where
// every write is a round trip. An empty array defaults to all four.
// Modern apt uses SHA256; MD5Sum and SHA1 are deprecated.
"acquireByHashAlgorithms": ["MD5Sum", "SHA1", "SHA256", "SHA512"],
// Follow contents of `Suggests:` field when processing dependencies for the package
"dependencyFollowSuggests": false,
@@ -82,6 +89,9 @@ The legacy json configuration is still supported (and also supports comments):
// Codename for short PPA url expansion
"ppaCodename": "",
// PPA Base URL
"ppaBaseURL": "http://ppa.launchpad.net",
// OBSOLETE
// in aptly up to version 1.0.0, package files were stored in internal package pool
// with MD5-dervied path, since 1.1.0 package pool layout was changed;
@@ -103,6 +113,9 @@ The legacy json configuration is still supported (and also supports comments):
// Enable API documentation on /docs
"enableSwaggerEndpoint": false,
// Enable pprof profiling endpoints on /api/debug/pprof/*
"enablePprofEndpoint": false,
// OBSOLETE: use via url param ?_async=true
"AsyncAPI": false,
@@ -163,6 +176,10 @@ The legacy json configuration is still supported (and also supports comments):
// Disable signature verification of remote repositories
"gpgDisableVerify": false,
// GPG key IDs to sign with when a publish does not name one itself
// (the -gpg-key flag, or GpgKey in the API's signing options)
"gpgKeys": [],
// Publishing
//////////////
@@ -173,6 +190,10 @@ The legacy json configuration is still supported (and also supports comments):
// Do not create bz2 files
"skipBz2Publishing": false,
// Number of index files renamed in parallel when a publish finishes
// (set to 1 to rename them one at a time)
"publishRenameConcurrency": 4,
// Storage
///////////
+98 -4
View File
@@ -209,6 +209,40 @@ func (g *GpgVerifier) argsKeyrings() (args []string) {
return
}
type gpgvSignatureDisposition int
const (
gpgvSignatureOther gpgvSignatureDisposition = iota
gpgvSignatureGood
gpgvSignatureUnknown
gpgvSignatureDisqualified
)
type gpgvSignatureStatus struct {
goodSig bool
validSig bool
missingKeyErrSig bool
noPubKey bool
disqualified bool
otherFailure bool
}
func (status gpgvSignatureStatus) disposition() gpgvSignatureDisposition {
if status.disqualified {
return gpgvSignatureDisqualified
}
if status.otherFailure {
return gpgvSignatureOther
}
if status.goodSig && status.validSig {
return gpgvSignatureGood
}
if status.missingKeyErrSig && status.noPubKey && !status.goodSig && !status.validSig {
return gpgvSignatureUnknown
}
return gpgvSignatureOther
}
func (g *GpgVerifier) runGpgv(args []string, context string, showKeyTip bool) (*KeyInfo, error) {
args = append([]string{"--status-fd", "3"}, args...)
cmd := exec.Command(g.gpgv, args...)
@@ -255,22 +289,82 @@ func (g *GpgVerifier) runGpgv(args []string, context string, showKeyTip bool) (*
statusr := bufio.NewScanner(tempf)
result := &KeyInfo{}
statuses := []gpgvSignatureStatus{}
var status *gpgvSignatureStatus
flushStatus := func() {
if status != nil {
statuses = append(statuses, *status)
}
}
for statusr.Scan() {
line := strings.TrimSpace(statusr.Text())
fields := strings.Fields(line)
if len(fields) >= 2 && fields[0] == "[GNUPG:]" && fields[1] == "NEWSIG" {
flushStatus()
status = &gpgvSignatureStatus{}
continue
}
if status == nil {
status = &gpgvSignatureStatus{otherFailure: true}
}
if len(fields) < 2 || fields[0] != "[GNUPG:]" {
status.otherFailure = true
continue
}
if strings.HasPrefix(line, "[GNUPG:] GOODSIG ") {
result.GoodKeys = append(result.GoodKeys, Key(strings.Fields(line)[2]))
} else if strings.HasPrefix(line, "[GNUPG:] NO_PUBKEY ") {
result.MissingKeys = append(result.MissingKeys, Key(strings.Fields(line)[2]))
switch fields[1] {
case "GOODSIG":
if len(fields) < 3 {
status.otherFailure = true
continue
}
status.goodSig = true
result.GoodKeys = append(result.GoodKeys, Key(fields[2]))
case "VALIDSIG":
status.validSig = true
case "NO_PUBKEY":
if len(fields) < 3 {
status.otherFailure = true
continue
}
status.noPubKey = true
result.MissingKeys = append(result.MissingKeys, Key(fields[2]))
case "ERRSIG":
// Field 7 is the gpg-error reason; 9 is GPG_ERR_NO_PUBKEY.
if len(fields) > 7 && fields[7] == "9" {
status.missingKeyErrSig = true
} else {
status.otherFailure = true
}
case "BADSIG", "EXPSIG", "EXPKEYSIG", "REVKEYSIG":
status.disqualified = true
}
}
flushStatus()
if err = statusr.Err(); err != nil {
return nil, err
}
if cmderr != nil {
goodSignatures := 0
unknownSignatures := 0
failureExplained := cmd.ProcessState != nil && cmd.ProcessState.Exited()
for _, signatureStatus := range statuses {
switch signatureStatus.disposition() {
case gpgvSignatureGood:
goodSignatures++
case gpgvSignatureUnknown:
unknownSignatures++
default:
failureExplained = false
}
}
if failureExplained && goodSignatures > 0 && unknownSignatures > 0 {
return result, nil
}
if showKeyTip && len(g.keyRings) == 0 && len(result.MissingKeys) > 0 {
fmt.Printf("\nLooks like some keys are missing in your trusted keyring, you may consider importing them from keyserver:\n\n")
+189
View File
@@ -93,8 +93,14 @@ func (s *Gnupg1VerifierSuite) SetUpTest(c *C) {
s.verifier = NewGpgVerifier(finder)
s.verifier.AddKeyring("./trusted.gpg")
s.multisigOneVerifier = NewGpgVerifier(finder)
s.multisigOneVerifier.AddKeyring("./multisig-one.gpg")
s.multisigBothVerifier = NewGpgVerifier(finder)
s.multisigBothVerifier.AddKeyring("./multisig-both.gpg")
c.Assert(s.verifier.InitKeyring(false), IsNil)
c.Assert(s.multisigOneVerifier.InitKeyring(false), IsNil)
c.Assert(s.multisigBothVerifier.InitKeyring(false), IsNil)
}
type Gnupg1SignerSuite struct {
@@ -142,8 +148,191 @@ func (s *Gnupg2VerifierSuite) SetUpTest(c *C) {
s.verifier = NewGpgVerifier(finder)
s.verifier.AddKeyring("./trusted.gpg")
s.multisigOneVerifier = NewGpgVerifier(finder)
s.multisigOneVerifier.AddKeyring("./multisig-one.gpg")
s.multisigBothVerifier = NewGpgVerifier(finder)
s.multisigBothVerifier.AddKeyring("./multisig-both.gpg")
c.Assert(s.verifier.InitKeyring(false), IsNil)
c.Assert(s.multisigOneVerifier.InitKeyring(false), IsNil)
c.Assert(s.multisigBothVerifier.InitKeyring(false), IsNil)
}
// The status fixtures were generated with GnuPG 2.4 at fixed times so the
// expired key and subkey remain expired without making the other keys age out:
//
// export GNUPGHOME=$(mktemp -d); chmod 700 "$GNUPGHOME"
// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-generate-key 'Aptly status good fixture <good@fixture.invalid>' rsa2048 sign 0
// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-generate-key 'Aptly status expired fixture <expired@fixture.invalid>' rsa2048 sign seconds=5
// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-generate-key 'Aptly status unknown fixture <unknown-status@fixture.invalid>' rsa2048 sign 0
// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-generate-key 'Aptly status revoked fixture <revoked@fixture.invalid>' rsa2048 sign 0
// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-generate-key 'Aptly unused expired subkey fixture <subkey@fixture.invalid>' rsa2048 cert 0
// good=A35DB05543372EE999F6EF74EBC0B25127B9BF5A expired=DA1728275CB25D14DC8B6DC97D0DD4A295844522 unknown=A068D4E79AD454D37C0888F5B316843CAB67504E
// revoked=CD11F407D4CEB092E66A83EC402BF908BCB202A9 primary=E1745C372D24AC29BD57FAB094ABAB0251F5E2FC active=724D68416C95AA282D8280DF22EE1E88523FD531
// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-add-key "$primary" rsa2048 sign 0
// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-add-key "$primary" rsa2048 sign seconds=5
// gpg --batch --yes --faked-system-time 20250101T000002 --local-user "$good!" --local-user "$expired!" --local-user "$unknown!" --detach-sign --output status-expired-held.signature multisig.text
// gpg --batch --yes --export "$good" "$expired" > status-expired-held.gpg
// gpg --batch --yes --faked-system-time 20250101T000002 --local-user "$good!" --local-user "$revoked!" --local-user "$unknown!" --detach-sign --output status-revoked-held.signature multisig.text
// sed 's/^://' "$GNUPGHOME/openpgp-revocs.d/$revoked.rev" | gpg --batch --yes --import
// gpg --batch --yes --export "$good" "$revoked" > status-revoked-held.gpg
// gpg --batch --yes --faked-system-time 20260902T143800 --local-user "$active!" --local-user "$unknown!" --detach-sign --output status-unused-expired-subkey.signature multisig.text
// gpg --batch --yes --export "$primary" > status-unused-expired-subkey.gpg
func verifyGpgStatusFixture(c *C, finder GPGFinder, keyring, signature string) error {
verifier := NewGpgVerifier(finder)
verifier.AddKeyring(keyring)
c.Assert(verifier.InitKeyring(false), IsNil)
cleartext, err := os.Open("multisig.text")
c.Assert(err, IsNil)
defer func() { _ = cleartext.Close() }()
signatureFile, err := os.Open(signature)
c.Assert(err, IsNil)
defer func() { _ = signatureFile.Close() }()
return verifier.VerifyDetachedSignature(signatureFile, cleartext, false)
}
func (s *Gnupg2VerifierSuite) TestGpgVerifierRejectsExpiredHeldSignatureAlongsideGood(c *C) {
err := verifyGpgStatusFixture(c, GPG2Finder(), "./status-expired-held.gpg", "status-expired-held.signature")
c.Assert(err, NotNil)
}
func (s *Gnupg2VerifierSuite) TestGpgVerifierRejectsRevokedHeldSignatureAlongsideGood(c *C) {
err := verifyGpgStatusFixture(c, GPG2Finder(), "./status-revoked-held.gpg", "status-revoked-held.signature")
c.Assert(err, NotNil)
}
func (s *Gnupg2VerifierSuite) TestGpgVerifierIgnoresUnusedExpiredSubkey(c *C) {
err := verifyGpgStatusFixture(c, GPG2Finder(), "./status-unused-expired-subkey.gpg", "status-unused-expired-subkey.signature")
c.Assert(err, IsNil)
}
type fixedGPGFinder struct {
gpgv string
}
func (f fixedGPGFinder) FindGPG() (string, GPGVersion, error) {
return f.gpgv, GPG22xPlus, nil
}
func (f fixedGPGFinder) FindGPGV() (string, GPGVersion, error) {
return f.gpgv, GPG22xPlus, nil
}
func (s *GnupgSuite) TestGpgVerifierRejectsUnexplainedFailure(c *C) {
tests := []struct {
name string
body string
wantError bool
goodKeys []Key
missingKeys []Key
}{
{
name: "unexplained exit",
body: `#!/bin/sh
printf '%s\n' \
'[GNUPG:] NEWSIG' \
'[GNUPG:] GOODSIG 0123456789ABCDEF held' \
'[GNUPG:] VALIDSIG 00000000000000000123456789ABCDEF 0 0 0 0 0 0 0 00 00000000000000000123456789ABCDEF' >&3
exit 70
`,
wantError: true,
goodKeys: []Key{"0123456789ABCDEF"},
},
{
name: "unknown co-signer",
body: `#!/bin/sh
printf '%s\n' \
'[GNUPG:] NEWSIG' \
'[GNUPG:] GOODSIG 0123456789ABCDEF held' \
'[GNUPG:] VALIDSIG 00000000000000000123456789ABCDEF 0 0 0 0 0 0 0 00 00000000000000000123456789ABCDEF' \
'[GNUPG:] NEWSIG' \
'[GNUPG:] ERRSIG FEDCBA9876543210 1 10 00 0 9 0000000000000000FEDCBA9876543210' \
'[GNUPG:] NO_PUBKEY FEDCBA9876543210' >&3
exit 2
`,
goodKeys: []Key{"0123456789ABCDEF"},
missingKeys: []Key{"FEDCBA9876543210"},
},
{
name: "non-missing-key ERRSIG",
body: `#!/bin/sh
printf '%s\n' \
'[GNUPG:] NEWSIG' \
'[GNUPG:] GOODSIG 0123456789ABCDEF held' \
'[GNUPG:] VALIDSIG 00000000000000000123456789ABCDEF 0 0 0 0 0 0 0 00 00000000000000000123456789ABCDEF' \
'[GNUPG:] NEWSIG' \
'[GNUPG:] ERRSIG FEDCBA9876543210 1 10 00 0 1 0000000000000000FEDCBA9876543210' >&3
exit 2
`,
wantError: true,
goodKeys: []Key{"0123456789ABCDEF"},
},
{
name: "empty signature block",
body: `#!/bin/sh
printf '%s\n' \
'[GNUPG:] NEWSIG' \
'[GNUPG:] GOODSIG 0123456789ABCDEF held' \
'[GNUPG:] VALIDSIG 00000000000000000123456789ABCDEF 0 0 0 0 0 0 0 00 00000000000000000123456789ABCDEF' \
'[GNUPG:] NEWSIG' >&3
exit 2
`,
wantError: true,
goodKeys: []Key{"0123456789ABCDEF"},
},
{
name: "GOODSIG without VALIDSIG",
body: `#!/bin/sh
printf '%s\n' \
'[GNUPG:] NEWSIG' \
'[GNUPG:] GOODSIG 0123456789ABCDEF held' >&3
exit 2
`,
wantError: true,
goodKeys: []Key{"0123456789ABCDEF"},
},
{
name: "signal death",
body: `#!/bin/sh
printf '%s\n' \
'[GNUPG:] NEWSIG' \
'[GNUPG:] GOODSIG 0123456789ABCDEF held' \
'[GNUPG:] VALIDSIG 00000000000000000123456789ABCDEF 0 0 0 0 0 0 0 00 00000000000000000123456789ABCDEF' >&3
kill -TERM "$$"
`,
wantError: true,
goodKeys: []Key{"0123456789ABCDEF"},
},
{
name: "exit zero preserves upstream behavior",
body: `#!/bin/sh
printf '%s\n' \
'[GNUPG:] NEWSIG' \
'[GNUPG:] BADSIG 0123456789ABCDEF held' >&3
exit 0
`,
},
}
for _, test := range tests {
directory := c.MkDir()
gpgv := filepath.Join(directory, "gpgv")
c.Assert(os.WriteFile(gpgv, []byte(test.body), 0o700), IsNil)
verifier := NewGpgVerifier(fixedGPGFinder{gpgv: gpgv})
keyInfo, err := verifier.runGpgv(nil, "detached signature", false)
if test.wantError {
c.Check(err, NotNil, Commentf(test.name))
} else {
c.Check(err, IsNil, Commentf(test.name))
}
c.Check(keyInfo.GoodKeys, DeepEquals, test.goodKeys, Commentf(test.name))
c.Check(keyInfo.MissingKeys, DeepEquals, test.missingKeys, Commentf(test.name))
}
}
type Gnupg2SignerSuite struct {
+6
View File
@@ -13,8 +13,14 @@ var _ = Suite(&GoVerifierSuite{})
func (s *GoVerifierSuite) SetUpTest(c *C) {
s.verifier = &GoVerifier{}
s.verifier.AddKeyring("./trusted.gpg")
s.multisigOneVerifier = &GoVerifier{}
s.multisigOneVerifier.AddKeyring("./multisig-one.gpg")
s.multisigBothVerifier = &GoVerifier{}
s.multisigBothVerifier.AddKeyring("./multisig-both.gpg")
c.Assert(s.verifier.InitKeyring(false), IsNil)
c.Assert(s.multisigOneVerifier.InitKeyring(false), IsNil)
c.Assert(s.multisigBothVerifier.InitKeyring(false), IsNil)
}
type GoSignerSuite struct {
+1
View File
@@ -0,0 +1 @@
tampered aptly multi-signature verification fixture
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
aptly multi-signature verification fixture
-----BEGIN PGP SIGNATURE-----
iQEzBAEBCgAdFiEEqYoHYeLwl1cZL9VhhT6Gnff0SYYFAmrmaZIACgkQhT6Gnff0
SYYKYAgAujy2QKb0Gjg9YSX16Ff5XCkd/XJSVwSHxVfX5TXN2daby+a3fns78Z9u
gbNF8qB0n6P8Q8xpPvrvnx125yJidutBR77Y1fjBm5Ks6N/x1kWg9lIXwS5eYglA
I7Ctm3vEdD96wyYnHiBOo9sufasrfX1CnCBd1q1MkDrNpaiYmvSMeK+f6Ozs7OK2
IfYNB5dNsI/LI65bKbgGFiOXoqMlXYFDn9GA734jrk3CvOImks4Q9GEi/qhaPKHe
sU9lZ9KU9Okj9Yy96c7Dk29iGaxhhKTiZ+2UGvyczVRenf2+v5t8/M11CgagApPP
nq8MRgPBKk6BdMgMd0SyZZhPj0VEpokBMwQBAQoAHRYhBOyPYRMwLkNvKOuL0hPZ
BG62qCWlBQJq5mmSAAoJEBPZBG62qCWlgv8H/1CEchjYZDgPWFu7cZE+abBPByTX
VcYU62CQPBpkxJ00pviuKgYRzQX0FCxQxpQOQlHv3ToVzL3a62m1vrTRa3NhT0U8
DYdRxcW8o/11hnY3VeiPavOvORhwn70FeBYtYw7BipHHxwOElh7Ey5dkzHS+Z2OL
0kCNvltjKEFZM+q4w2yPbSpam7wCS5gYLl0d+99ornPtzDwBOreXFoC1MER3y1hB
H3XbLqggE6gfumfiGR9Hljvh5CT/ebk7RwMWCcrp3/MdXwfpH+6JGg1AvF/TwmEi
XAl8LljwTybylH+8wKAHYX4ed7GvJBOigJth+3o7sh8n7oqL0YJH0JoSjbM=
=ThV5
-----END PGP SIGNATURE-----
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
aptly multi-signature verification fixture
+3 -1
View File
@@ -63,7 +63,9 @@ func checkDetachedSignature(keyring openpgp.KeyRing, signed, signature io.Reader
for {
p, err = packets.Next()
if err == io.EOF {
if len(signers) == 0 || missingKeys > 0 {
// Revocation and expiry of held keys deliberately remain unchanged
// from upstream in this provider; only unknown co-signers are ignored.
if len(signers) == missingKeys {
err = errors.ErrUnknownIssuer
} else {
err = nil
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+99 -14
View File
@@ -10,9 +10,33 @@ import (
// Common set of tests shared by internal & external GnuPG implementations
type VerifierSuite struct {
verifier Verifier
verifier Verifier
multisigOneVerifier Verifier
multisigBothVerifier Verifier
}
// The partial-keyring fixtures were generated with GnuPG 2.4 at a fixed time
// so they remain valid under the CI faketime window (2025-01-02):
//
// gpg --batch --pinentry-mode loopback --passphrase '' \
// --faked-system-time 20250101T000000 \
// --quick-generate-key 'Aptly multisign known fixture <known@fixture.invalid>' rsa2048 sign 0
// gpg --batch --pinentry-mode loopback --passphrase '' \
// --faked-system-time 20250101T000000 \
// --quick-generate-key 'Aptly multisign unknown fixture <unknown@fixture.invalid>' rsa2048 sign 0
// gpg --export <FPR_known> <FPR_unknown> > multisig-both.gpg
// gpg --export <FPR_known> > multisig-one.gpg
// gpg --local-user <FPR_known> --local-user <FPR_unknown> \
// --detach-sign --output multisig.signature multisig.text
// gpg --local-user <FPR_known> --local-user <FPR_unknown> \
// --clearsign --output multisig.clearsigned multisig.text
// gpg --local-user <FPR_known> --detach-sign --output multisig-known-good.signature multisig.text
// gpg --local-user <FPR_unknown> --detach-sign --output multisig-known-bad.signature multisig-bad.text
//
// Replace <FPR_known> and <FPR_unknown> with the actual fingerprints from step 2.
//
// multisig-bad.text was made by changing multisig.text after signing it.
func (s *VerifierSuite) TestVerifyDetached(c *C) {
for _, test := range []struct {
textName, signatureName string
@@ -37,21 +61,82 @@ func (s *VerifierSuite) TestVerifyDetached(c *C) {
}
func (s *VerifierSuite) TestVerifyClearsigned(c *C) {
for _, test := range []struct {
clearSignedName string
}{
{"1.clearsigned"},
} {
clearsigned, err := os.Open(test.clearSignedName)
c.Assert(err, IsNil)
clearsigned, err := os.Open("multisig.clearsigned")
c.Assert(err, IsNil)
defer func() { _ = clearsigned.Close() }()
keyInfo, err := s.verifier.VerifyClearsigned(clearsigned, false)
c.Assert(err, IsNil)
c.Check(keyInfo.GoodKeys, DeepEquals, []Key{"04EE7237B7D453EC", "648ACFD622F3D138", "DCC9EFBF77E11517"})
c.Check(keyInfo.MissingKeys, DeepEquals, []Key(nil))
keyInfo, err := s.multisigBothVerifier.VerifyClearsigned(clearsigned, false)
c.Assert(err, IsNil)
c.Check(keyInfo.GoodKeys, DeepEquals, []Key{"853E869DF7F44986", "13D9046EB6A825A5"})
c.Check(keyInfo.MissingKeys, DeepEquals, []Key(nil))
}
_ = clearsigned.Close()
}
func (s *VerifierSuite) TestVerifyClearsignedPartialKeyring(c *C) {
clearsigned, err := os.Open("multisig.clearsigned")
c.Assert(err, IsNil)
defer func() { _ = clearsigned.Close() }()
keyInfo, err := s.multisigOneVerifier.VerifyClearsigned(clearsigned, false)
c.Assert(err, IsNil)
c.Check(keyInfo.GoodKeys, DeepEquals, []Key{"853E869DF7F44986"})
c.Check(keyInfo.MissingKeys, DeepEquals, []Key{"13D9046EB6A825A5"})
}
func (s *VerifierSuite) TestVerifyDetachedPartialKeyring(c *C) {
cleartext, err := os.Open("multisig.text")
c.Assert(err, IsNil)
defer func() { _ = cleartext.Close() }()
signature, err := os.Open("multisig.signature")
c.Assert(err, IsNil)
defer func() { _ = signature.Close() }()
err = s.multisigOneVerifier.VerifyDetachedSignature(signature, cleartext, false)
c.Assert(err, IsNil)
}
func (s *VerifierSuite) TestVerifyPartialKeyringRejectsBadSignature(c *C) {
cleartext, err := os.Open("multisig-bad.text")
c.Assert(err, IsNil)
defer func() { _ = cleartext.Close() }()
signature, err := os.Open("multisig.signature")
c.Assert(err, IsNil)
defer func() { _ = signature.Close() }()
err = s.multisigOneVerifier.VerifyDetachedSignature(signature, cleartext, false)
c.Assert(err, NotNil)
}
func (s *VerifierSuite) TestVerifyRejectsBadSignatureAlongsideGood(c *C) {
cleartext, err := os.Open("multisig.text")
c.Assert(err, IsNil)
defer func() { _ = cleartext.Close() }()
goodSignature, err := os.Open("multisig-known-good.signature")
c.Assert(err, IsNil)
defer func() { _ = goodSignature.Close() }()
badSignature, err := os.Open("multisig-known-bad.signature")
c.Assert(err, IsNil)
defer func() { _ = badSignature.Close() }()
signature := io.MultiReader(goodSignature, badSignature)
err = s.multisigBothVerifier.VerifyDetachedSignature(signature, cleartext, false)
c.Assert(err, NotNil)
}
func (s *VerifierSuite) TestVerifyNoKnownSignerStillFails(c *C) {
cleartext, err := os.Open("multisig.text")
c.Assert(err, IsNil)
defer func() { _ = cleartext.Close() }()
signature, err := os.Open("multisig.signature")
c.Assert(err, IsNil)
defer func() { _ = signature.Close() }()
err = s.verifier.VerifyDetachedSignature(signature, cleartext, false)
c.Assert(err, NotNil)
}
func (s *VerifierSuite) TestExtractClearsigned(c *C) {
+113
View File
@@ -0,0 +1,113 @@
package query
import (
"reflect"
"sort"
"testing"
"github.com/aptly-dev/aptly/database/goleveldb"
"github.com/aptly-dev/aptly/deb"
)
func TestArchitectureVariantQueries(t *testing.T) {
db, err := goleveldb.NewOpenDB(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer func() {
if err := db.Close(); err != nil {
t.Error(err)
}
}()
collection := deb.NewPackageCollection(db)
list := deb.NewPackageList()
const normal = "test-package_1.0_amd64"
const variant = "test-package_1.0_amd64v3"
for _, value := range []string{"", "amd64v3"} {
p := deb.NewPackageFromControlFile(deb.Stanza{
"Package": "test-package", "Version": "1.0",
"Architecture": "amd64", "Architecture-Variant": value,
})
if err := collection.Update(p); err != nil {
t.Fatal(err)
}
if err := list.Add(p); err != nil {
t.Fatal(err)
}
}
list.PrepareIndex()
for _, catalog := range []struct {
name string
value deb.PackageCatalog
}{{"list", list}, {"collection", collection}} {
t.Run(catalog.name, func(t *testing.T) {
for _, tc := range []struct {
name, expression string
want []string
}{
{"coexistence", "Name (= test-package)", []string{normal, variant}},
{"exact_normal", normal, []string{normal}},
{"exact_variant", variant, []string{variant}},
{"base_architecture", "Architecture (= amd64)", []string{normal, variant}},
{"variant_is_not_base_architecture", "Architecture (= amd64v3)", []string{}},
{"variant_field", "Architecture-Variant (= amd64v3)", []string{variant}},
{"special_base_architecture", "$Architecture (= amd64)", []string{normal, variant}},
{"special_variant_architecture", "$Architecture (= amd64v3)", []string{}},
{"normal_and_field", normal + ", Architecture (= amd64)", []string{normal}},
{"variant_and_field", variant + ", Architecture (= amd64)", []string{variant}},
// A field OR operand forces scanning, including PkgQuery.Matches.
{"scan_normal", normal + " | Name (= absent)", []string{normal}},
{"scan_variant", variant + " | Name (= absent)", []string{variant}},
{"not_normal", "!(" + normal + ")", []string{variant}},
{"not_variant", "!(" + variant + ")", []string{normal}},
} {
t.Run(tc.name, func(t *testing.T) {
q, err := Parse(tc.expression)
if err != nil {
t.Fatal(err)
}
got := q.Query(catalog.value).FullNames()
sort.Strings(got)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("query %q: got %v; want %v", tc.expression, got, tc.want)
}
})
}
})
}
}
func TestArchitectureVariantQueryParsing(t *testing.T) {
for _, arch := range []string{"amd64", "amd64v3"} {
q, err := Parse("test-package_1.0_" + arch)
if err != nil {
t.Fatal(err)
}
want := &deb.PkgQuery{Pkg: "test-package", Version: "1.0", Arch: arch}
if !reflect.DeepEqual(q, want) {
t.Errorf("got %#v; want %#v", q, want)
}
}
}
func TestArchitectureVariantArchitectureAllQuerySemantics(t *testing.T) {
p := &deb.Package{Name: "data", Architecture: "all"}
for _, tc := range []struct {
expression string
want bool
}{
{"Architecture (= amd64)", false},
{"Architecture (= amd64v3)", false},
{"$Architecture (= amd64)", true},
{"$Architecture (= amd64v3)", true},
{"$Architecture (= source)", false},
} {
q, err := Parse(tc.expression)
if err != nil {
t.Fatal(err)
}
if got := q.Matches(p); got != tc.want {
t.Errorf("query %q: got %v; want %v", tc.expression, got, tc.want)
}
}
}
+10 -1
View File
@@ -576,5 +576,14 @@ func (storage *PublishedStorage) ReadLink(path string) (string, error) {
return "", err
}
return output.Metadata["SymLink"], nil
// S3 lowercases user metadata keys, so the value SymLink writes as
// "SymLink" is returned as "symlink" and a case-sensitive lookup finds
// nothing. Matched case-insensitively so either spelling resolves.
for key, value := range output.Metadata {
if strings.EqualFold(key, "SymLink") {
return value, nil
}
}
return "", nil
}
+46
View File
@@ -409,6 +409,52 @@ func (s *PublishedStorageSuite) TestSymLink(c *C) {
c.Skip("copy not available in s3test")
}
func (s *PublishedStorageSuite) TestReadLinkMetadataKeyIsCaseInsensitive(c *C) {
// S3 lowercases user metadata keys, so a value written as "SymLink" comes
// back as "symlink". A case-sensitive map lookup therefore returns "" with
// a nil error, which reads as "this is not a link" rather than as failure.
//
// deb.packageIndexByHash relies on ReadLink to find the physical file its
// .old pointer refers to, and removes it before rotating. With "" returned
// the Remove is a no-op whose error is discarded, so every superseded
// by-hash index file is left behind. On a live S3-backed archive that is
// one leaked object per index per hash algorithm per publish, forever;
// filesystem backends are unaffected because they have real symlinks.
//
// Written with PutObject rather than SymLink because the test server has
// no CopyObject, which is why TestSymLink above is skipped and why this
// path was never covered.
_, err := s.storage.s3.PutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String("test"),
Key: aws.String("a/b.link"),
Body: bytes.NewReader([]byte("test")),
Metadata: map[string]string{"symlink": "a/b"},
})
c.Assert(err, IsNil)
link, err := s.storage.ReadLink("a/b.link")
c.Check(err, IsNil)
c.Check(link, Equals, "a/b")
}
func (s *PublishedStorageSuite) TestReadLinkOnPlainObject(c *C) {
// An object that is not a link has no SymLink metadata, and ReadLink
// returns an empty string with a nil error rather than failing.
// packageIndexByHash relies on that: it calls ReadLink on a path it has
// only just confirmed exists, and treats an error as "leave it alone".
s.PutFile(c, "a/plain", []byte("test"))
link, err := s.storage.ReadLink("a/plain")
c.Check(err, IsNil)
c.Check(link, Equals, "")
// A HeadObject that fails is an error, not an empty link. The two are
// distinguishable so a caller can tell "this is not a link" from "I could
// not find out".
_, err = s.noSuchBucketStorage.ReadLink("a/plain")
c.Check(err, NotNil)
}
func (s *PublishedStorageSuite) TestFileExists(c *C) {
s.PutFile(c, "a/b", []byte("test"))
+3 -3
View File
@@ -2,7 +2,7 @@ FROM debian:trixie-slim
RUN echo 'deb http://deb.debian.org/debian trixie-backports main' > /etc/apt/sources.list.d/backports.list && \
apt-get update -y && apt-get install -y --no-install-recommends curl gnupg bzip2 xz-utils ca-certificates vim procps \
golang-1.25 golang-1.25-go golang-1.25-src \
golang-1.26 golang-1.26-go golang-1.26-src \
make git python3 python3-requests-unixsocket python3-termcolor python3-swiftclient python3-boto3 python3-azure-storage \
g++ python3-etcd3 python3-plyvel graphviz devscripts sudo dh-golang binutils-i686-linux-gnu binutils-aarch64-linux-gnu \
binutils-arm-linux-gnueabihf bash-completion zip ruby-dev lintian npm \
@@ -10,8 +10,8 @@ RUN echo 'deb http://deb.debian.org/debian trixie-backports main' > /etc/apt/sou
gcc-i686-linux-gnu gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu \
faketime dput-ng && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
ln -sf /usr/lib/go-1.25/bin/go /usr/local/bin/go && \
ln -sf /usr/lib/go-1.25/bin/gofmt /usr/local/bin/gofmt
ln -sf /usr/lib/go-1.26/bin/go /usr/local/bin/go && \
ln -sf /usr/lib/go-1.26/bin/gofmt /usr/local/bin/gofmt
RUN useradd -m --shell /bin/bash --home-dir /var/lib/aptly aptly
RUN sed -i 's/#force_color_prompt=yes/force_color_prompt=yes/' /var/lib/aptly/.bashrc
+5
View File
@@ -25,6 +25,11 @@ from pathlib import Path
from uuid import uuid4
def remove_tmp_suffix(_, output):
"""Normalize .tmp.<uuid> suffixes to .tmp for deterministic golden output."""
return re.sub(r'(\.tmp)\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', r'\1', output)
def ungzip_if_required(output):
if isinstance(output, bytes) and output.startswith(b"\x1f\x8b"):
return zlib.decompress(output, 16 + zlib.MAX_WBITS).decode('utf-8')
+8
View File
@@ -4,6 +4,12 @@
"logFormat": "default",
"databaseOpenAttempts": 10,
"architectures": [],
"acquireByHashAlgorithms": [
"MD5Sum",
"SHA1",
"SHA256",
"SHA512"
],
"skipLegacyPool": false,
"dependencyFollowSuggests": false,
"dependencyFollowRecommends": false,
@@ -16,6 +22,7 @@
"serveInAPIMode": true,
"enableMetricsEndpoint": true,
"enableSwaggerEndpoint": false,
"enablePprofEndpoint": false,
"AsyncAPI": false,
"databaseBackend": {
"type": "",
@@ -33,6 +40,7 @@
"gpgKeys": [],
"skipContentsPublishing": false,
"skipBz2Publishing": false,
"publishRenameConcurrency": 4,
"FileSystemPublishEndpoints": {},
"JFrogPublishEndpoints": null,
"S3PublishEndpoints": {},
@@ -3,6 +3,11 @@ log_level: debug
log_format: default
database_open_attempts: 10
architectures: []
acquire_by_hash_algorithms:
- MD5Sum
- SHA1
- SHA256
- SHA512
skip_legacy_pool: false
dep_follow_suggests: false
dep_follow_recommends: false
@@ -15,6 +20,7 @@ ppa_baseurl: http://ppa.launchpad.net
serve_in_api_mode: true
enable_metrics_endpoint: true
enable_swagger_endpoint: false
enable_pprof_endpoint: false
async_api: false
database_backend:
type: ""
@@ -31,6 +37,7 @@ gpg_disable_verify: false
gpg_keys: []
skip_contents_publishing: false
skip_bz2_publishing: false
publish_rename_concurrency: 4
filesystem_publish_endpoints: {}
jfrog_publish_endpoints: {}
s3_publish_endpoints: {}
+35 -16
View File
@@ -32,6 +32,17 @@ database_open_attempts: -1
architectures:
# - amd64
# Hash algorithms to write Acquire-By-Hash entries for, when a publish
# has Acquire-By-Hash enabled. Each algorithm costs a full set of index
# copies on every publish, which is felt most on S3-like storage where
# every write is a round trip. An empty list defaults to all four.
# Modern apt uses SHA256; MD5Sum and SHA1 are deprecated.
acquire_by_hash_algorithms:
- MD5Sum
- SHA1
- SHA256
- SHA512
# OBSOLETE
# in aptly up to version 1.0.0, package files were stored in internal package pool
# with MD5-dervied path, since 1.1.0 package pool layout was changed;
@@ -51,13 +62,13 @@ dep_follow_suggests: false
dep_follow_recommends: false
# When dependency looks like `package-a | package-b`, follow both variants always
dep_follow_allvariants: false
dep_follow_all_variants: false
# Follow dependency from binary package to source package
dep_follow_source: false
# Log additional details while resolving dependencies (useful for debugging)
dep_verbose_resolve: false
dep_verboseresolve: false
# PPA
@@ -86,6 +97,9 @@ enable_metrics_endpoint: false
# Enable API documentation on /docs
enable_swagger_endpoint: false
# Enable pprof profiling endpoints on /api/debug/pprof/*
enable_pprof_endpoint: false
# OBSOLETE: use via url param ?_async=true
async_api: false
@@ -143,6 +157,10 @@ gpg_disable_sign: false
# Disable signature verification of remote repositories
gpg_disable_verify: false
# GPG key IDs to sign with when a publish does not name one itself
# (the -gpg-key flag, or GpgKey in the API's signing options)
gpg_keys: []
# Publishing
#############
@@ -153,6 +171,10 @@ skip_contents_publishing: false
# Do not create bz2 files
skip_bz2_publishing: false
# Number of index files renamed in parallel when a publish finishes
# (set to 1 to rename them one at a time)
publish_rename_concurrency: 4
# Storage
##########
@@ -185,18 +207,6 @@ filesystem_publish_endpoints:
# # * size (compare file size)
# verify_method: md5
# S3 Endpoint Support
#
# cloud storage). First, publishing
# endpoints should be described in aptly configuration file. Each endpoint has name
# and associated settings.
#
# In order to publish to S3, specify endpoint as `s3:endpoint-name:` before
# publishing prefix on the command line, e.g.:
#
# `aptly publish snapshot wheezy-main s3:test:`
#
# JFrog Artifactory Endpoint Support
#
# aptly can be configured to publish repositories directly to JFrog Artifactory. First,
@@ -225,6 +235,17 @@ jfrog_publish_endpoints:
# api_key: api_key
# access_token: access_token
# S3 Endpoint Support
#
# cloud storage). First, publishing
# endpoints should be described in aptly configuration file. Each endpoint has name
# and associated settings.
#
# In order to publish to S3, specify endpoint as `s3:endpoint-name:` before
# publishing prefix on the command line, e.g.:
#
# `aptly publish snapshot wheezy-main s3:test:`
#
s3_publish_endpoints:
# # Endpoint Name
# test:
@@ -425,5 +446,3 @@ packagepool_storage:
# # defaults to "https://<accountName>.blob.core.windows.net"
# endpoint: ""
+32
View File
@@ -0,0 +1,32 @@
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/dists/wheezy/Release
Downloading & parsing package files...
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/dists/wheezy/main/binary-i386/Packages.bz2
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/dists/wheezy/main/binary-amd64/Packages.bz2
Keeping at most 2 versions of each package: 52 -> 23.
Building download queue...
Download queue: 23 items (8.41 MiB)
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.6-1%29/varnish_3.0.6-1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish-agent%20%282.2.1%29/varnish-agent_2.2.1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish-agent%20%283.0.0%29/varnish-agent_3.0.0~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.7-1%29/varnish-dbg_3.0.7-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.6-1%29/libvarnishapi1_3.0.6-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.7-1%29/libvarnishapi-dev_3.0.7-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.7-1%29/libvarnishapi1_3.0.7-1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.6-1%29/libvarnishapi1_3.0.6-1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.7-1%29/varnish_3.0.7-1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish-agent%20%282.2.0%29/varnish-agent_2.2.0~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.7-1%29/libvarnishapi-dev_3.0.7-1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.7-1%29/varnish_3.0.7-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.6-1%29/varnish_3.0.6-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.6-1%29/libvarnishapi-dev_3.0.6-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish-agent%20%283.0.1%29/varnish-agent_3.0.1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.6-1%29/varnish-dbg_3.0.6-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.6-1%29/libvarnishapi-dev_3.0.6-1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.7-1%29/varnish-dbg_3.0.7-1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.7-1%29/varnish-doc_3.0.7-1~wheezy_all.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.6-1%29/varnish-doc_3.0.6-1~wheezy_all.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.7-1%29/libvarnishapi1_3.0.7-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish-agent%20%281.16.0%29/varnish-agent_1.16.0~wheezy_all.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.6-1%29/varnish-dbg_3.0.6-1~wheezy_i386.deb
Mirror `varnish` has been updated successfully.
@@ -0,0 +1,45 @@
Name: varnish
Archive Root URL: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/
Distribution: wheezy
Components: main
Architectures: i386, amd64
Download Sources: no
Download .udebs: no
Download AppStream: no
Number of packages: 23
Information from release file:
Acquire-By-Hash: yes
Architectures: amd64 armel armhf i386 ia64 mips mipsel powerpc ppc64el s390 s390x sparc alpha avr32 hppa m32 m68k sh arm64
Codename: wheezy
Components: main
Date: Tue, 22 Jan 2019 00:28:39 +0000
Description:
Label: varnish30
Origin: packagecloud.io/varnishcache/varnish30
Suite: wheezy
Version: 1
Packages:
libvarnishapi-dev_3.0.7-1~wheezy_amd64
libvarnishapi-dev_3.0.7-1~wheezy_i386
libvarnishapi-dev_3.0.6-1~wheezy_amd64
libvarnishapi-dev_3.0.6-1~wheezy_i386
libvarnishapi1_3.0.7-1~wheezy_amd64
libvarnishapi1_3.0.7-1~wheezy_i386
libvarnishapi1_3.0.6-1~wheezy_amd64
libvarnishapi1_3.0.6-1~wheezy_i386
varnish_3.0.7-1~wheezy_amd64
varnish_3.0.7-1~wheezy_i386
varnish_3.0.6-1~wheezy_amd64
varnish_3.0.6-1~wheezy_i386
varnish-agent_3.0.1~wheezy_amd64
varnish-agent_3.0.0~wheezy_amd64
varnish-agent_2.2.1~wheezy_i386
varnish-agent_2.2.0~wheezy_i386
varnish-agent_1.16.0~wheezy_all
varnish-dbg_3.0.7-1~wheezy_amd64
varnish-dbg_3.0.7-1~wheezy_i386
varnish-dbg_3.0.6-1~wheezy_amd64
varnish-dbg_3.0.6-1~wheezy_i386
varnish-doc_3.0.7-1~wheezy_all
varnish-doc_3.0.6-1~wheezy_all
+21
View File
@@ -0,0 +1,21 @@
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/dists/wheezy/Release
Downloading & parsing package files...
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/dists/wheezy/main/binary-i386/Packages.bz2
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/dists/wheezy/main/binary-amd64/Packages.bz2
Keeping at most 1 version of each package: 52 -> 12.
Building download queue...
Download queue: 12 items (4.19 MiB)
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.7-1%29/varnish-dbg_3.0.7-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.7-1%29/libvarnishapi1_3.0.7-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish-agent%20%281.16.0%29/varnish-agent_1.16.0~wheezy_all.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish-agent%20%283.0.1%29/varnish-agent_3.0.1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.7-1%29/varnish_3.0.7-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.7-1%29/varnish-dbg_3.0.7-1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.7-1%29/varnish_3.0.7-1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.7-1%29/libvarnishapi-dev_3.0.7-1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.7-1%29/libvarnishapi1_3.0.7-1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/libv/varnish%20%283.0.7-1%29/libvarnishapi-dev_3.0.7-1~wheezy_amd64.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish-agent%20%282.2.1%29/varnish-agent_2.2.1~wheezy_i386.deb
Downloading: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/pool/wheezy/main/v/varnish%20%283.0.7-1%29/varnish-doc_3.0.7-1~wheezy_all.deb
Mirror `varnish` has been updated successfully.
@@ -0,0 +1,34 @@
Name: varnish
Archive Root URL: http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/
Distribution: wheezy
Components: main
Architectures: i386, amd64
Download Sources: no
Download .udebs: no
Download AppStream: no
Number of packages: 12
Information from release file:
Acquire-By-Hash: yes
Architectures: amd64 armel armhf i386 ia64 mips mipsel powerpc ppc64el s390 s390x sparc alpha avr32 hppa m32 m68k sh arm64
Codename: wheezy
Components: main
Date: Tue, 22 Jan 2019 00:28:39 +0000
Description:
Label: varnish30
Origin: packagecloud.io/varnishcache/varnish30
Suite: wheezy
Version: 1
Packages:
libvarnishapi-dev_3.0.7-1~wheezy_amd64
libvarnishapi-dev_3.0.7-1~wheezy_i386
libvarnishapi1_3.0.7-1~wheezy_amd64
libvarnishapi1_3.0.7-1~wheezy_i386
varnish_3.0.7-1~wheezy_amd64
varnish_3.0.7-1~wheezy_i386
varnish-agent_3.0.1~wheezy_amd64
varnish-agent_2.2.1~wheezy_i386
varnish-agent_1.16.0~wheezy_all
varnish-dbg_3.0.7-1~wheezy_amd64
varnish-dbg_3.0.7-1~wheezy_i386
varnish-doc_3.0.7-1~wheezy_all
@@ -0,0 +1 @@
ERROR: unable to update: -max-versions should be a positive number
+12
View File
@@ -0,0 +1,12 @@
Downloading: http://repo.aptly.info/system-tests/archive.ubuntu.com/ubuntu/dists/resolute/Release
Downloading & parsing package files...
Downloading: http://repo.aptly.info/system-tests/archive.ubuntu.com/ubuntu/dists/resolute/main/binary-amd64/Packages.gz
Downloading: http://repo.aptly.info/system-tests/archive.ubuntu.com/ubuntu/dists/resolute/main/binary-amd64v3/Packages.gz
Applying filter...
Packages filtered: 9974 -> 2.
Building download queue...
Download queue: 2 items (0.54 MiB)
Downloading: http://repo.aptly.info/system-tests/archive.ubuntu.com/ubuntu/pool/main/r/rust-threecpio/3cpio_0.14.0-1ubuntu1_amd64v3.deb
Downloading: http://repo.aptly.info/system-tests/archive.ubuntu.com/ubuntu/pool/main/r/rust-threecpio/3cpio_0.14.0-1ubuntu1_amd64.deb
Mirror `arch-variant` has been updated successfully.
@@ -0,0 +1,2 @@
Pamd64 3cpio 0.14.0-1ubuntu1
Pamd64v3 3cpio 0.14.0-1ubuntu1
+64
View File
@@ -502,3 +502,67 @@ class UpdateMirror28Test(BaseTest):
self.check_output()
self.check_cmd_output("aptly mirror show appstream-test", "mirror_show",
match_prepare=lambda s: re.sub(r"Last update: [0-9:+A-Za-z -]+\n", "", s))
class UpdateMirror29Test(BaseTest):
"""
update mirrors: keep only the 2 most recent versions of each package
"""
sortOutput = True
fixtureCmds = [
"aptly -architectures=i386,amd64 mirror create --ignore-signatures varnish http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/ wheezy main",
]
runCmd = "aptly mirror update --ignore-signatures -max-versions=2 varnish"
outputMatchPrepare = filterOutRedirects
def check(self):
self.check_output()
self.check_cmd_output("aptly mirror show -with-packages varnish", "mirror_show",
match_prepare=lambda s: re.sub(r"Last update: [0-9:+A-Za-z -]+\n", "", s))
class UpdateMirror30Test(BaseTest):
"""
update mirrors: -latest keeps only the most recent version of each package
"""
sortOutput = True
fixtureCmds = [
"aptly -architectures=i386,amd64 mirror create --ignore-signatures varnish http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/ wheezy main",
]
runCmd = "aptly mirror update --ignore-signatures -latest varnish"
outputMatchPrepare = filterOutRedirects
def check(self):
self.check_output()
self.check_cmd_output("aptly mirror show -with-packages varnish", "mirror_show",
match_prepare=lambda s: re.sub(r"Last update: [0-9:+A-Za-z -]+\n", "", s))
class UpdateMirror31Test(BaseTest):
"""
update mirrors: negative -max-versions
"""
runCmd = "aptly mirror update -max-versions=-1 varnish"
expectedCode = 1
class UpdateMirror32Test(BaseTest):
"""
update mirrors: architecture variants
"""
sortOutput = True
longTest = False
fixtureCmds = [
"aptly mirror create --ignore-signatures "
"-architectures=amd64,amd64v3 "
"-filter='Name (3cpio)' "
"arch-variant http://repo.aptly.info/system-tests/archive.ubuntu.com/ubuntu/ resolute main",
]
runCmd = "aptly mirror update --ignore-signatures arch-variant"
def check(self):
self.check_output()
self.check_cmd_output(
"aptly mirror search -format='{{.ShortKey}}' arch-variant 'Name (3cpio)'",
"packages",
)
+57
View File
@@ -1,4 +1,6 @@
import re
import tempfile
from pathlib import Path
from lib import BaseTest
@@ -80,3 +82,58 @@ class DiffSnapshot6Test(BaseTest):
"aptly snapshot create snap2 from mirror wheezy-main",
]
runCmd = "aptly snapshot diff snap1 snap2"
class DiffSnapshotArchitectureVariantTest(BaseTest):
"""
snapshot diff: display base and variant architectures independently
"""
runCmd = "aptly snapshot diff variant-empty variant-normal"
def prepare_fixture(self):
super().prepare_fixture()
self.run_cmd("aptly repo create variant-display")
self.run_cmd("aptly snapshot create variant-empty empty")
with tempfile.TemporaryDirectory(prefix="aptly-diff-variant-") as tmp:
for arch in ("amd64", "amd64v3"):
package_dir = Path(tmp) / arch
control_dir = package_dir / "DEBIAN"
control_dir.mkdir(parents=True)
control = (
"Package: test-package\n"
"Version: 1.0\n"
"Architecture: amd64\n"
"Maintainer: Aptly Test <test@example.com>\n"
"Description: Snapshot diff display fixture\n"
)
if arch == "amd64v3":
control += "Architecture-Variant: amd64v3\n"
(control_dir / "control").write_text(control)
package_file = str(Path(tmp) / (
"test-package_1.0_" + arch + ".deb"))
self.run_cmd(["dpkg-deb", "--build", str(package_dir), package_file])
self.run_cmd(["aptly", "repo", "add", "variant-display", package_file])
snapshot = "variant-normal" if arch == "amd64" else "variant-both"
self.run_cmd([
"aptly", "snapshot", "create", snapshot,
"from", "repo", "variant-display",
])
def check(self):
def check_row(output, expected):
lines = output.strip().splitlines()
self.check_equal(len(lines), 2)
self.check_equal(" ".join(lines[0].split()),
"Arch | Package | Version in A | Version in B")
self.check_equal(" ".join(lines[1].split()), expected)
check_row(self.output, "+ amd64 | test-package | - | 1.0")
for left, right, expected in (
("variant-normal", "variant-both",
"+ amd64v3 | test-package | - | 1.0"),
("variant-both", "variant-normal",
"- amd64v3 | test-package | 1.0 | -"),
("variant-normal", "variant-empty",
"- amd64 | test-package | 1.0 | -"),
):
check_row(self.run_cmd(["aptly", "snapshot", "diff", left, right]), expected)
+14
View File
@@ -0,0 +1,14 @@
Loading packages...
Generating metadata files and linking package files...
Finalizing metadata files...
Signing file 'Release' with gpg, please enter your passphrase when prompted:
Clearsigning file 'Release' with gpg, please enter your passphrase when prompted:
Local repos repo1, repo2 have been successfully published.
Please setup your webserver to serve directory '${HOME}/.aptly/public' with autoindexing.
Now you can add following line to apt sources:
deb http://your-server/ maverick contrib main
deb-src http://your-server/ maverick contrib main
Don't forget to add your GPG key to apt with apt-key.
You can also use `aptly serve` to publish your repositories over HTTP quickly.
+31
View File
@@ -1098,3 +1098,34 @@ class PublishRepo39Test(BaseTest):
self.check_equal(first_release, self.read_file('public/dists/maverick/Release'))
self.check_equal(first_release_gpg, self.read_file('public/dists/maverick/Release.gpg'))
self.check_equal(first_inrelease, self.read_file('public/dists/maverick/InRelease'))
class PublishRepo40Test(BaseTest):
"""
publish repo: skeleton files with multiple components
"""
fixtureCmds = [
"aptly repo create repo1",
"aptly repo create repo2",
"aptly repo add repo1 ${files}/libboost-program-options-dev_1.49.0.1_i386.deb",
"aptly repo add repo2 ${files}/pyspi-0.6.1-1.3.stripped.dsc",
]
runCmd = "aptly publish repo -keyring=${files}/aptly.pub -secret-keyring=${files}/aptly.sec -component=main,contrib -distribution=maverick -skip-contents repo1 repo2"
gold_processor = BaseTest.expand_environ
def prepare_fixture(self):
super(PublishRepo40Test, self).prepare_fixture()
self.write_file(os.path.join('skel', 'dists', 'maverick', 'main', 'dep11',
'Components-amd64.yml'), 'main dep11 payload')
self.write_file(os.path.join('skel', 'dists', 'maverick', 'contrib', 'dep11',
'Components-amd64.yml'), 'contrib dep11 payload')
def check(self):
super(PublishRepo40Test, self).check()
# each skeleton file is emitted once per publish, not once per component
self.check_equal(self.read_file('public/dists/maverick/main/dep11/Components-amd64.yml'),
'main dep11 payload')
self.check_equal(self.read_file('public/dists/maverick/contrib/dep11/Components-amd64.yml'),
'contrib dep11 payload')
+4
View File
@@ -1,3 +1,4 @@
from lib import remove_tmp_suffix
from s3_lib import S3Test
@@ -55,6 +56,7 @@ class S3Publish2Test(S3Test):
"aptly repo remove local-repo pyspi"
]
sortOutput = True
outputMatchPrepare = remove_tmp_suffix
runCmd = "aptly publish update -keyring=${files}/aptly.pub -secret-keyring=${files}/aptly.sec maverick s3:test1:"
def check(self):
@@ -96,6 +98,7 @@ class S3Publish3Test(S3Test):
"aptly publish snapshot -keyring=${files}/aptly.pub -secret-keyring=${files}/aptly.sec -distribution=maverick snap1 s3:test1:",
]
sortOutput = True
outputMatchPrepare = remove_tmp_suffix
runCmd = "aptly publish switch -keyring=${files}/aptly.pub -secret-keyring=${files}/aptly.sec maverick s3:test1: snap3"
def check(self):
@@ -178,6 +181,7 @@ class S3Publish6Test(S3Test):
"aptly repo remove local-repo pyspi"
]
sortOutput = True
outputMatchPrepare = remove_tmp_suffix
runCmd = "aptly publish update -keyring=${files}/aptly.pub -secret-keyring=${files}/aptly.sec maverick s3:test1:"
def check(self):
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more