Skip to content

Commit

Permalink
snapshot merge: use proper REST api
Browse files Browse the repository at this point in the history
- this breaks the existing api, which is only available in CI builds
- improve swagger doc
  • Loading branch information
neolynx committed Oct 3, 2024
1 parent 06b2b92 commit 478c674
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 36 deletions.
2 changes: 1 addition & 1 deletion api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func Router(c *ctx.AptlyContext) http.Handler {
api.GET("/snapshots/:name/packages", apiSnapshotsSearchPackages)
api.DELETE("/snapshots/:name", apiSnapshotsDrop)
api.GET("/snapshots/:name/diff/:withSnapshot", apiSnapshotsDiff)
api.POST("/snapshots/merge", apiSnapshotsMerge)
api.POST("/snapshots/:name/merge", apiSnapshotsMerge)
api.POST("/snapshots/:name/pull", apiSnapshotsPull)
}

Expand Down
63 changes: 46 additions & 17 deletions api/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,17 +406,37 @@ func apiSnapshotsSearchPackages(c *gin.Context) {
showPackages(c, snapshot.RefList(), collectionFactory)
}

// POST /api/snapshots/merge
type snapshotsMergeBody struct {
// list of snapshot names to be merged together
Sources []string `binding:"required"`
}

// @Summary Snapshot Merge
// @Description **Merge several source snapshots into a new snapshot**
// @Description
// @Description Merge happens from left to right. By default, packages with the same name-architecture pair are replaced during merge (package from latest snapshot on the list wins).
// @Description
// @Description If only one snapshot is specified, merge copies source into destination.
// @Tags Snapshots
// @Param latest query int false "merge only the latest version of each package"
// @Param no-remove query int false "all versions of packages are preserved during merge"
// @Accept json
// @Param name path string true "Name of the snapshot to be created"
// @Param request body snapshotsMergeBody true "json parameters"
// @Produce json
// @Success 200
// @Failure 400 {object} Error "Bad Request"
// @Failure 404 {object} Error "Not Found"
// @Failure 500 {object} Error "Internal Error"
// @Router /api/snapshots/{name}/merge [post]
func apiSnapshotsMerge(c *gin.Context) {
var (
err error
snapshot *deb.Snapshot
body snapshotsMergeBody
)

var body struct {
Destination string `binding:"required"`
Sources []string `binding:"required"`
}
name := c.Params.ByName("name")

if c.Bind(&body) != nil {
return
Expand Down Expand Up @@ -456,7 +476,7 @@ func apiSnapshotsMerge(c *gin.Context) {
resources[i] = string(sources[i].ResourceKey())
}

maybeRunTaskInBackground(c, "Merge snapshot "+body.Destination, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
maybeRunTaskInBackground(c, "Merge snapshot "+name, resources, func(_ aptly.Progress, _ *task.Detail) (*task.ProcessReturnValue, error) {
result := sources[0].RefList()
for i := 1; i < len(sources); i++ {
result = result.Merge(sources[i].RefList(), overrideMatching, false)
Expand All @@ -471,7 +491,7 @@ func apiSnapshotsMerge(c *gin.Context) {
sourceDescription[i] = fmt.Sprintf("'%s'", s.Name)
}

snapshot = deb.NewSnapshotFromRefList(body.Destination, sources, result,
snapshot = deb.NewSnapshotFromRefList(name, sources, result,
fmt.Sprintf("Merged from sources: %s", strings.Join(sourceDescription, ", ")))

err = collectionFactory.SnapshotCollection().Add(snapshot)
Expand All @@ -484,26 +504,35 @@ func apiSnapshotsMerge(c *gin.Context) {
}

type snapshotsPullBody struct {
// Source name where packages and dependencies will be searched
// Source name to be searched for packages and dependencies
Source string `binding:"required" json:"Source" example:"source-snapshot"`
// Name of the snapshot that will be created
// Name of the snapshot to be created
Destination string `binding:"required" json:"Destination" example:"idestination-snapshot"`
// List of package queries, in the simplest form, name of package to be pulled from
// List of package queries (i.e. name of package to be pulled from `Source`)
Queries []string `binding:"required" json:"Queries" example:"xserver-xorg"`
// List of architectures (optional)
Architectures []string ` json:"Architectures" example:"amd64, armhf"`
}

// @Summary Snapshot Pull
// @Description Pulls new packages (along with its dependencies) to name snapshot from source snapshot. Also pull command can upgrade package versions if name snapshot already contains packages being pulled. New snapshot destination is created as result of this process.
// @Description **Pulls new packages and dependencies from a source snapshot into a new snapshot**
// @Description
// @Description May also upgrade package versions if name snapshot already contains packages being pulled. New snapshot `Destination` is created as result of this process.
// @Description If architectures are limited (with config architectures or parameter `Architectures`, only mentioned architectures are processed, otherwise aptly will process all architectures in the snapshot.
// @Description If following dependencies by source is enabled (using dependencyFollowSource config), pulling binary packages would also pull corresponding source packages as well.
// @Description By default aptly would remove packages matching name and architecture while importing: e.g. when importing software_1.3_amd64, package software_1.2.9_amd64 would be removed.
// @Description
// @Description With flag `no-remove` both package versions would stay in the snapshot.
// @Description
// @Description Aptly pulls first package matching each of package queries, but with flag -all-matches all matching packages would be pulled.
// @Tags Snapshots
// @Param all-matches query int false "all-matches: 1 to enable"
// @Param dry-run query int false "dry-run: 1 to enable"
// @Param no-deps query int false "no-deps: 1 to enable"
// @Param no-remove query int false "no-remove: 1 to enable"
// @Param all-matches query int false "pull all the packages that satisfy the dependency version requirements (default is to pull first matching package): 1 to enable"
// @Param dry-run query int false "don’t create destination snapshot, just show what would be pulled: 1 to enable"
// @Param no-deps query int false "don’t process dependencies, just pull listed packages: 1 to enable"
// @Param no-remove query int false "don’t remove other package versions when pulling package: 1 to enable"
// @Accept json
// @Param name path string true "Snapshot where packages and dependencies will be pulled to"
// @Param request body snapshotsPullBody true "See api.snapshotsPullBody"
// @Param name path string true "Name of the snapshot to be created"
// @Param request body snapshotsPullBody true "json parameters"
// @Produce json
// @Success 200
// @Failure 400 {object} Error "Bad Request"
Expand Down
34 changes: 16 additions & 18 deletions system/t12_api/snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ def check(self):

class SnapshotsAPITestMerge(APITest):
"""
POST /api/snapshots, POST /api/snapshots/merge, GET /api/snapshots/:name, DELETE /api/snapshots/:name
POST /api/snapshots, POST /api/snapshots/:name/merge, GET /api/snapshots/:name, DELETE /api/snapshots/:name
"""

def check(self):
Expand All @@ -325,9 +325,8 @@ def check(self):
# create merge snapshot
merged_name = self.random_name()
task = self.post_task(
"/api/snapshots/merge",
f"/api/snapshots/{merged_name}/merge",
json={
"Destination": merged_name,
"Sources": [source["Name"] for source in sources],
},
)
Expand All @@ -352,7 +351,7 @@ def check(self):
# create merge snapshot without sources
merged_name = self.random_name()
resp = self.post(
"/api/snapshots/merge", json={"Destination": merged_name, "Sources": []}
f"/api/snapshots/{merged_name}/merge", json={"Sources": []}
)
self.check_equal(resp.status_code, 400)
self.check_equal(
Expand All @@ -364,8 +363,8 @@ def check(self):
merged_name = self.random_name()
non_existing_source = self.random_name()
resp = self.post(
"/api/snapshots/merge",
json={"Destination": merged_name, "Sources": [non_existing_source]},
f"/api/snapshots/{merged_name}/merge",
json={"Sources": [non_existing_source]},
)
self.check_equal(
resp.json()["error"], f"snapshot with name {non_existing_source} not found"
Expand All @@ -377,8 +376,8 @@ def check(self):
# create merge snapshot with used name
merged_name = sources[0]["Name"]
resp = self.post(
"/api/snapshots/merge",
json={"Destination": merged_name, "Sources": [source["Name"] for source in sources]},
f"/api/snapshots/{merged_name}/merge",
json={"Sources": [source["Name"] for source in sources]},
)
self.check_equal(
resp.json()["error"],
Expand All @@ -389,9 +388,8 @@ def check(self):
# create merge snapshot with "latest" and "no-remove" flags (should fail)
merged_name = self.random_name()
resp = self.post(
"/api/snapshots/merge",
f"/api/snapshots/{merged_name}/merge",
json={
"Destination": merged_name,
"Sources": [source["Name"] for source in sources],
},
params={"latest": "1", "no-remove": "1"},
Expand All @@ -404,7 +402,7 @@ def check(self):

class SnapshotsAPITestPull(APITest):
"""
POST /api/snapshots/pull, POST /api/snapshots, GET /api/snapshots/:name/packages?name=:package_name
POST /api/snapshots/:name/pull, POST /api/snapshots, GET /api/snapshots/:name/packages?name=:package_name
"""

def check(self):
Expand Down Expand Up @@ -448,7 +446,7 @@ def check(self):
self.check_equal(resp.status_code, 200)

# dry run, all-matches
resp = self.post("/api/snapshots/{snapshot_empty_repo}/pull?dry-run=1&all-matches=1", json={
resp = self.post(f"/api/snapshots/{snapshot_empty_repo}/pull?dry-run=1&all-matches=1", json={
'Source': snapshot_repo_with_libboost,
'Destination': snapshot_pull_libboost,
'Queries': [
Expand All @@ -462,14 +460,14 @@ def check(self):
self.check_equal(resp.status_code, 200)

# missing argument
resp = self.post("/api/snapshots/{snapshot_empty_repo}/pull", json={
resp = self.post("f/api/snapshots/{snapshot_empty_repo}/pull", json={
'Source': snapshot_repo_with_libboost,
'Destination': snapshot_pull_libboost,
})
self.check_equal(resp.status_code, 400)

# dry run, emtpy architectures
resp = self.post("/api/snapshots/{snapshot_empty_repo}/pull?dry-run=1", json={
resp = self.post("f/api/snapshots/{snapshot_empty_repo}/pull?dry-run=1", json={
'Source': snapshot_repo_with_libboost,
'Destination': snapshot_pull_libboost,
'Queries': [
Expand All @@ -490,7 +488,7 @@ def check(self):
self.check_equal(resp.status_code, 404)

# dry run, non-existing source
resp = self.post("/api/snapshots/{snapshot_empty_repo}/pull?dry-run=1", json={
resp = self.post("f/api/snapshots/{snapshot_empty_repo}/pull?dry-run=1", json={
'Source': "asd123",
'Destination': snapshot_pull_libboost,
'Queries': [
Expand All @@ -500,7 +498,7 @@ def check(self):
self.check_equal(resp.status_code, 404)

# snapshot pull
resp = self.post("/api/snapshots/{snapshot_empty_repo}/pull", json={
resp = self.post("f/api/snapshots/{snapshot_empty_repo}/pull", json={
'Source': snapshot_repo_with_libboost,
'Destination': snapshot_pull_libboost,
'Queries': [
Expand All @@ -525,7 +523,7 @@ def check(self):
# pull from non-existing source
non_existing_source = self.random_name()
destination = self.random_name()
resp = self.post("/api/snapshots/{snapshot_empty_repo}/pull", json={
resp = self.post(f"/api/snapshots/{snapshot_empty_repo}/pull", json={
'Source': non_existing_source,
'Destination': destination,
'Queries': [
Expand All @@ -541,7 +539,7 @@ def check(self):
# pull to non-existing snapshot
non_existing_snapshot = self.random_name()
destination = self.random_name()
resp = self.post("/api/snapshots/{snapshot_empty_repo}/pull", json={
resp = self.post(f"/api/snapshots/{snapshot_empty_repo}/pull", json={
'Source': non_existing_snapshot,
'Destination': destination,
'Queries': [
Expand Down

0 comments on commit 478c674

Please sign in to comment.