diff --git a/api/router.go b/api/router.go index b6897e5f1..73adcf5a6 100644 --- a/api/router.go +++ b/api/router.go @@ -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) } diff --git a/api/snapshot.go b/api/snapshot.go index 9d9740690..3307861af 100644 --- a/api/snapshot.go +++ b/api/snapshot.go @@ -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 @@ -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) @@ -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) @@ -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" diff --git a/system/t12_api/snapshots.py b/system/t12_api/snapshots.py index 51915dd1b..137f0202b 100644 --- a/system/t12_api/snapshots.py +++ b/system/t12_api/snapshots.py @@ -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): @@ -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], }, ) @@ -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( @@ -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" @@ -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"], @@ -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"}, @@ -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): @@ -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': [ @@ -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': [ @@ -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': [ @@ -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': [ @@ -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': [ @@ -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': [