Publish repo REST API. #116

This commit is contained in:
Andrey Smirnov
2015-01-07 16:11:34 +03:00
parent 6e32e3dcf4
commit 98ca0cdf33
4 changed files with 242 additions and 0 deletions
+177
View File
@@ -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{})
}
+8
View File
@@ -39,5 +39,13 @@ func Router(c *ctx.AptlyContext) http.Handler {
root.DELETE("/files/:dir/:name", apiFilesDeleteFile) 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 return router
} }
+1
View File
@@ -4,3 +4,4 @@ Testing aptly REST API
from .repos import * from .repos import *
from .files import * from .files import *
from .publish import *
+56
View File
@@ -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