mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-05-05 22:08:27 +00:00
Merge pull request #1535 from lecafard/push-qxtqtunqqqnu
Add edit mirror API endpoint
This commit is contained in:
1
AUTHORS
1
AUTHORS
@@ -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)
|
||||
|
||||
152
api/mirror.go
152
api/mirror.go
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user