Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ List of contributors, in chronological order:
* Roman Lebedev (https://github.com/LebedevRI)
* Brian Witt (https://github.com/bwitt)
* Ales Bregar (https://github.com/abregar)
* Tom Nguyen (https://github.com/lecafard)
152 changes: 152 additions & 0 deletions api/mirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"`
Expand Down
1 change: 1 addition & 0 deletions api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
149 changes: 149 additions & 0 deletions system/t12_api/mirrors.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,152 @@ def check(self):
'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'])
Loading