diff --git a/api/publish.go b/api/publish.go new file mode 100644 index 00000000..e6318eaf --- /dev/null +++ b/api/publish.go @@ -0,0 +1,177 @@ +package api + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/smira/aptly/deb" + "github.com/smira/aptly/utils" + "strings" +) + +type SigningOptions struct { + Skip bool + Batch bool + GpgKey string + Keyring string + SecretKeyring string + Passphrase string + PassphraseFile string +} + +func getSigner(options *SigningOptions) (utils.Signer, error) { + if options.Skip { + return nil, nil + } + + signer := &utils.GpgSigner{} + signer.SetKey(options.GpgKey) + signer.SetKeyRing(options.Keyring, options.SecretKeyring) + signer.SetPassphrase(options.Passphrase, options.PassphraseFile) + signer.SetBatch(options.Batch) + + err := signer.Init() + if err != nil { + return nil, err + } + + return signer, nil +} + +// Replace '_' with '/' and double '__' with single '_' +func parseEscapedPath(path string) string { + return strings.Replace(strings.Replace(path, "__", "_", -1), "_", "/", -1) +} + +// GET /publish +func apiPublishList(c *gin.Context) { + c.JSON(400, gin.H{}) +} + +// POST /publish/:prefix/repos | /publish/:prefix/snapshots +func apiPublishRepoOrSnapshot(c *gin.Context) { + param := parseEscapedPath(c.Params.ByName("prefix")) + storage, prefix := deb.ParsePrefix(param) + + var b struct { + Sources []struct { + Component string + Name string `binding:"required"` + } `binding:"required"` + Distribution string + Label string + Origin string + ForceOverwrite bool + Signing SigningOptions + } + + if !c.Bind(&b) { + return + } + + signer, err := getSigner(&b.Signing) + if err != nil { + c.Fail(500, fmt.Errorf("unable to initialize GPG signer: %s", err)) + return + } + + if len(b.Sources) == 0 { + c.Fail(400, fmt.Errorf("unable to publish: soures are empty")) + return + } + + var components []string + var sources []interface{} + + if strings.HasSuffix(c.Request.URL.Path, "/snapshots") { + var snapshot *deb.Snapshot + + snapshotCollection := context.CollectionFactory().SnapshotCollection() + snapshotCollection.RLock() + defer snapshotCollection.RUnlock() + + for _, source := range b.Sources { + components = append(components, source.Component) + + snapshot, err = snapshotCollection.ByName(source.Name) + if err != nil { + c.Fail(404, fmt.Errorf("unable to publish: %s", err)) + return + } + + err = snapshotCollection.LoadComplete(snapshot) + if err != nil { + c.Fail(500, fmt.Errorf("unable to publish: %s", err)) + return + } + + sources = append(sources, snapshot) + } + } else if strings.HasSuffix(c.Request.URL.Path, "/repos") { + var localRepo *deb.LocalRepo + + localCollection := context.CollectionFactory().LocalRepoCollection() + localCollection.RLock() + defer localCollection.RUnlock() + + for _, source := range b.Sources { + components = append(components, source.Component) + + localRepo, err = localCollection.ByName(source.Name) + if err != nil { + c.Fail(404, fmt.Errorf("unable to publish: %s", err)) + return + } + + err = localCollection.LoadComplete(localRepo) + if err != nil { + c.Fail(500, fmt.Errorf("unable to publish: %s", err)) + } + + sources = append(sources, localRepo) + } + } else { + panic("unknown command") + } + + collection := context.CollectionFactory().PublishedRepoCollection() + collection.Lock() + defer collection.Unlock() + + published, err := deb.NewPublishedRepo(storage, prefix, b.Distribution, context.ArchitecturesList(), components, sources, context.CollectionFactory()) + if err != nil { + c.Fail(500, fmt.Errorf("unable to publish: %s", err)) + return + } + published.Origin = b.Origin + published.Label = b.Label + + duplicate := collection.CheckDuplicate(published) + if duplicate != nil { + context.CollectionFactory().PublishedRepoCollection().LoadComplete(duplicate, context.CollectionFactory()) + c.Fail(400, fmt.Errorf("prefix/distribution already used by another published repo: %s", duplicate)) + return + } + + err = published.Publish(context.PackagePool(), context, context.CollectionFactory(), signer, context.Progress(), b.ForceOverwrite) + if err != nil { + c.Fail(500, fmt.Errorf("unable to publish: %s", err)) + return + } + + err = collection.Add(published) + if err != nil { + c.Fail(500, fmt.Errorf("unable to save to DB: %s", err)) + } + + c.JSON(200, published) +} + +// PUT /publish/:prefix/:distribution +func apiPublishUpdateSwitch(c *gin.Context) { + c.JSON(400, gin.H{}) +} + +// DELETE /publish/:prefix/:distribution +func apiPublishDrop(c *gin.Context) { + c.JSON(400, gin.H{}) +} diff --git a/api/router.go b/api/router.go index 3c6badb0..3e5b5efd 100644 --- a/api/router.go +++ b/api/router.go @@ -39,5 +39,13 @@ func Router(c *ctx.AptlyContext) http.Handler { root.DELETE("/files/:dir/:name", apiFilesDeleteFile) } + { + root.GET("/publish", apiPublishList) + root.POST("/publish/:prefix/repos", apiPublishRepoOrSnapshot) + root.POST("/publish/:prefix/snapshots", apiPublishRepoOrSnapshot) + root.PUT("/publish/:prefix/:distribution", apiPublishUpdateSwitch) + root.DELETE("/publish/:prefix/:distribution", apiPublishDrop) + } + return router } diff --git a/system/t12_api/__init__.py b/system/t12_api/__init__.py index 51c8c87f..9642eebe 100644 --- a/system/t12_api/__init__.py +++ b/system/t12_api/__init__.py @@ -4,3 +4,4 @@ Testing aptly REST API from .repos import * from .files import * +from .publish import * diff --git a/system/t12_api/publish.py b/system/t12_api/publish.py new file mode 100644 index 00000000..95379656 --- /dev/null +++ b/system/t12_api/publish.py @@ -0,0 +1,56 @@ +import os +import inspect + +from api_lib import APITest + +DefaultSigningOptions = { + "Keyring": os.path.join(os.path.dirname(inspect.getsourcefile(APITest)), "files") + "/aptly.pub", + "SecretKeyring": os.path.join(os.path.dirname(inspect.getsourcefile(APITest)), "files") + "/aptly.sec", +} + + +class PublishAPITestRepo(APITest): + """ + POST /publish/:prefix/repos + """ + fixtureGpg = True + + def check(self): + repo_name = self.random_name() + self.check_equal(self.post("/api/repos", json={"Name": repo_name, "DefaultDistribution": "wheezy"}).status_code, 201) + + d = self.random_name() + self.check_equal(self.upload("/api/files/" + d, + "libboost-program-options-dev_1.49.0.1_i386.deb", "pyspi_0.6.1-1.3.dsc", + "pyspi_0.6.1-1.3.diff.gz", "pyspi_0.6.1.orig.tar.gz", + "pyspi-0.6.1-1.3.stripped.dsc").status_code, 200) + + self.check_equal(self.post("/api/repos/" + repo_name + "/file/" + d).status_code, 200) + + prefix = self.random_name() + resp = self.post("/api/publish/" + prefix + "/repos", + json={ + "Sources": [{"Name": repo_name}], + "Signing": DefaultSigningOptions, + }) + self.check_equal(resp.status_code, 200) + self.check_equal(resp.json(), { + 'Architectures': ['i386', 'source'], + 'Distribution': 'wheezy', + 'Label': '', + 'Origin': '', + 'Prefix': prefix, + 'SourceKind': 'local', + 'Sources': [{'Component': 'main', 'Name': repo_name}], + 'Storage': ''}) + + +class PublishSnapshotAPITestRepo(APITest): + """ + POST /publish/:prefix/snapshot + + XXX: test me when snapshot API becomes available + """ + + def check(self): + pass