Add edit mirror API endpoint

This commit is contained in:
tom
2026-02-14 15:10:30 +11:00
committed by André Roth
parent 144265122a
commit b3f5d96490
4 changed files with 303 additions and 0 deletions

View File

@@ -82,3 +82,4 @@ List of contributors, in chronological order:
* Ales Bregar (https://github.com/abregar)
* Tim Foerster (https://github.com/tonobo)
* Zhang Xiao (https://github.com/xzhang1)
* Tom Nguyen (https://github.com/lecafard)

View File

@@ -31,6 +31,36 @@ func getVerifier(keyRings []string) (pgp.Verifier, error) {
return verifier, nil
}
// stringSlicesEqual compares two string slices for equality (order matters)
func stringSlicesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// uniqueStrings returns a new slice with only unique strings from the input, sorted
func uniqueStrings(input []string) []string {
if len(input) == 0 {
return input
}
seen := make(map[string]struct{}, len(input))
result := make([]string, 0, len(input))
for _, s := range input {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
result = append(result, s)
}
}
sort.Strings(result)
return result
}
// @Summary List Mirrors
// @Description **Show list of currently available mirrors**
// @Description Each mirror is returned as in “show” API.
@@ -330,6 +360,128 @@ func apiMirrorsPackages(c *gin.Context) {
}
}
type mirrorEditParams struct {
// Package query that is applied to mirror packages
Filter *string ` json:"Filter" example:"xserver-xorg"`
// Set "true" to include dependencies of matching packages when filtering
FilterWithDeps *bool ` json:"FilterWithDeps"`
// Set "true" to mirror installer files
DownloadInstaller *bool `json:"DownloadInstaller"`
// Set "true" to mirror source packages
DownloadSources *bool ` json:"DownloadSources"`
// Set "true" to mirror udeb files
DownloadUdebs *bool ` json:"DownloadUdebs"`
// URL of the archive to mirror
ArchiveURL *string ` json:"ArchiveURL" example:"http://deb.debian.org/debian"`
// Comma separated list of architectures
Architectures *[]string `json:"Architectures" example:"amd64"`
// Gpg keyring(s) for verifying Release file if a mirror update is required.
Keyrings []string ` json:"Keyrings" example:"trustedkeys.gpg"`
// Set "true" to skip the verification of Release file signatures
IgnoreSignatures *bool ` json:"IgnoreSignatures"`
}
// @Summary Edit Mirror
// @Description **Edit mirror config**
// @Tags Mirrors
// @Param name path string true "mirror name to edit"
// @Consume json
// @Param request body mirrorEditParams true "Parameters"
// @Produce json
// @Success 200 {object} deb.RemoteRepo "Mirror was edited successfully"
// @Failure 400 {object} Error "Bad Request"
// @Failure 404 {object} Error "Mirror not found"
// @Failure 409 {object} Error "Aptly db locked"
// @Failure 500 {object} Error "Internal Error"
// @Router /api/mirrors/{name} [post]
func apiMirrorsEdit(c *gin.Context) {
var (
err error
b mirrorEditParams
repo *deb.RemoteRepo
)
collectionFactory := context.NewCollectionFactory()
collection := collectionFactory.RemoteRepoCollection()
name := c.Params.ByName("name")
repo, err = collection.ByName(name)
if err != nil {
AbortWithJSONError(c, 404, fmt.Errorf("unable to edit: %s", err))
return
}
err = repo.CheckLock()
if err != nil {
AbortWithJSONError(c, 409, fmt.Errorf("unable to edit: %s", err))
return
}
if c.Bind(&b) != nil {
return
}
fetchMirror := false
ignoreSignatures := context.Config().GpgDisableVerify
if b.Filter != nil {
repo.Filter = *b.Filter
}
if b.FilterWithDeps != nil {
repo.FilterWithDeps = *b.FilterWithDeps
}
if b.DownloadInstaller != nil {
repo.DownloadInstaller = *b.DownloadInstaller
}
if b.DownloadSources != nil {
repo.DownloadSources = *b.DownloadSources
}
if b.DownloadUdebs != nil {
repo.DownloadUdebs = *b.DownloadUdebs
}
if b.ArchiveURL != nil && *b.ArchiveURL != repo.ArchiveRoot {
repo.SetArchiveRoot(*b.ArchiveURL)
fetchMirror = true
}
if b.Architectures != nil {
uniqueArchitectures := uniqueStrings(*b.Architectures)
if !stringSlicesEqual(uniqueArchitectures, uniqueStrings(repo.Architectures)) {
repo.Architectures = uniqueArchitectures
fetchMirror = true
}
}
if b.IgnoreSignatures != nil {
ignoreSignatures = *b.IgnoreSignatures
}
if repo.IsFlat() && repo.DownloadUdebs {
AbortWithJSONError(c, 400, fmt.Errorf("unable to edit: flat mirrors don't support udebs"))
return
}
if fetchMirror {
verifier, err := getVerifier(b.Keyrings)
if err != nil {
AbortWithJSONError(c, 500, fmt.Errorf("unable to initialize GPG verifier: %s", err))
return
}
err = repo.Fetch(context.Downloader(), verifier, ignoreSignatures)
if err != nil {
AbortWithJSONError(c, 500, fmt.Errorf("unable to edit: %s", err))
return
}
}
err = collection.Update(repo)
if err != nil {
AbortWithJSONError(c, 500, fmt.Errorf("unable to edit: %s", err))
return
}
c.JSON(200, repo)
}
type mirrorUpdateParams struct {
// Change mirror name to `Name`
Name string ` json:"Name" example:"mirror1"`

View File

@@ -158,6 +158,7 @@ func Router(c *ctx.AptlyContext) http.Handler {
api.GET("/mirrors/:name", apiMirrorsShow)
api.GET("/mirrors/:name/packages", apiMirrorsPackages)
api.POST("/mirrors", apiMirrorsCreate)
api.POST("/mirrors/:name", apiMirrorsEdit)
api.PUT("/mirrors/:name", apiMirrorsUpdate)
api.DELETE("/mirrors/:name", apiMirrorsDrop)
}

View File

@@ -151,3 +151,152 @@ class MirrorsAPITestSkipArchitectureCheck(APITest):
'IgnoreSignatures': True}
resp = self.put_task("/api/mirrors/" + mirror_name, json=mirror_desc)
self.check_task(resp)
class MirrorsAPITestEdit(APITest):
"""
POST /api/mirrors/{name} - Edit mirror configuration
"""
def check(self):
# Create a mirror first
mirror_name = self.random_name()
mirror_desc = {'Name': mirror_name,
'ArchiveURL': 'http://repo.aptly.info/system-tests/packagecloud.io/varnishcache/varnish30/debian/',
'IgnoreSignatures': True,
'Distribution': 'wheezy',
'Components': ['main'],
'Architectures': ['amd64']}
resp = self.post("/api/mirrors", json=mirror_desc)
self.check_equal(resp.status_code, 201)
# Test editing basic properties (Filter, FilterWithDeps, Download options)
edit_params = {
'Filter': 'varnish',
'FilterWithDeps': True,
'DownloadSources': True,
'DownloadInstaller': False,
'DownloadUdebs': False
}
resp = self.post("/api/mirrors/" + mirror_name, json=edit_params)
self.check_equal(resp.status_code, 200)
self.check_subset({
'Name': mirror_name,
'Filter': 'varnish',
'FilterWithDeps': True,
'DownloadSources': True
}, resp.json())
# Verify the changes persisted
resp = self.get("/api/mirrors/" + mirror_name)
self.check_equal(resp.status_code, 200)
self.check_subset({
'Filter': 'varnish',
'FilterWithDeps': True,
'DownloadSources': True
}, resp.json())
# Test editing with empty filter to clear it
edit_params = {'Filter': ''}
resp = self.post("/api/mirrors/" + mirror_name, json=edit_params)
self.check_equal(resp.status_code, 200)
self.check_equal(resp.json()['Filter'], '')
class MirrorsAPITestEditNotFound(APITest):
"""
POST /api/mirrors/{name} - Edit non-existent mirror
"""
def check(self):
resp = self.post("/api/mirrors/non-existent-mirror", json={'Filter': 'test'})
self.check_equal(resp.status_code, 404)
self.check_in('unable to edit', resp.json()['error'])
class MirrorsAPITestEditArchitectures(APITest):
"""
POST /api/mirrors/{name} - Edit mirror architectures (triggers fetch)
"""
def check(self):
# Create a mirror
mirror_name = self.random_name()
mirror_desc = {'Name': mirror_name,
'ArchiveURL': 'http://repo.aptly.info/system-tests/security.debian.org/debian-security/',
'IgnoreSignatures': True,
'Distribution': 'buster/updates',
'Components': ['main'],
'Architectures': ['amd64']}
resp = self.post("/api/mirrors", json=mirror_desc)
self.check_equal(resp.status_code, 201)
# Edit architectures (should trigger a fetch)
edit_params = {
'Architectures': ['amd64', 'i386'],
'IgnoreSignatures': True
}
resp = self.post("/api/mirrors/" + mirror_name, json=edit_params)
self.check_equal(resp.status_code, 200)
# Verify architectures were updated
resp = self.get("/api/mirrors/" + mirror_name)
self.check_equal(resp.status_code, 200)
architectures = resp.json()['Architectures']
self.check_equal(sorted(architectures), ['amd64', 'i386'])
class MirrorsAPITestEditArchiveURL(APITest):
"""
POST /api/mirrors/{name} - Edit mirror archive URL (triggers fetch)
"""
def check(self):
# Create a mirror
mirror_name = self.random_name()
mirror_desc = {'Name': mirror_name,
'ArchiveURL': 'http://repo.aptly.info/system-tests/ftp.ru.debian.org/debian',
'IgnoreSignatures': True,
'Distribution': 'bookworm',
'Components': ['main'],
'Architectures': ['amd64']}
resp = self.post("/api/mirrors", json=mirror_desc)
self.check_equal(resp.status_code, 201)
# Edit archive URL (should trigger a fetch)
edit_params = {
'ArchiveURL': 'http://repo.aptly.info/system-tests/ftp.ch.debian.org/debian',
'IgnoreSignatures': True
}
resp = self.post("/api/mirrors/" + mirror_name, json=edit_params)
self.check_equal(resp.status_code, 200)
# Verify URL was updated
resp = self.get("/api/mirrors/" + mirror_name)
self.check_equal(resp.status_code, 200)
self.check_equal(resp.json()['ArchiveRoot'], 'http://repo.aptly.info/system-tests/ftp.ch.debian.org/debian/')
class MirrorsAPITestEditFlatMirrorUdebs(APITest):
"""
POST /api/mirrors/{name} - Edit flat mirror with udebs (should fail)
"""
def check(self):
# Create a flat mirror
mirror_name = self.random_name()
mirror_desc = {'Name': mirror_name,
'ArchiveURL': 'http://repo.aptly.info/system-tests/cloud.r-project.org/bin/linux/debian/bullseye-cran40/',
'IgnoreSignatures': True,
'Architectures': ['amd64']}
resp = self.post("/api/mirrors", json=mirror_desc)
self.check_equal(resp.status_code, 201)
# Try to enable udebs on a flat mirror (should fail)
edit_params = {'DownloadUdebs': True}
resp = self.post("/api/mirrors/" + mirror_name, json=edit_params)
self.check_equal(resp.status_code, 400)
self.check_in("flat mirrors don't support udebs", resp.json()['error'])