diff --git a/Makefile b/Makefile index 26f773a8..af873d9a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ GOVERSION=$(shell go version | awk '{print $$3;}') PACKAGES=context database deb files http query s3 utils -ALL_PACKAGES=aptly context cmd console database deb files http query s3 utils +ALL_PACKAGES=api aptly context cmd console database deb files http query s3 utils BINPATH=$(abspath ./_vendor/bin) GOM_ENVIRONMENT=-test PYTHON?=python diff --git a/api/api.go b/api/api.go new file mode 100644 index 00000000..0dc2d89f --- /dev/null +++ b/api/api.go @@ -0,0 +1,2 @@ +// Package api provides implementation of aptly REST API +package api diff --git a/api/repos.go b/api/repos.go new file mode 100644 index 00000000..231864e5 --- /dev/null +++ b/api/repos.go @@ -0,0 +1,184 @@ +package api + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/smira/aptly/deb" +) + +// GET /api/repos +func apiReposList(c *gin.Context) { + result := []*deb.LocalRepo{} + + collection := context.CollectionFactory().LocalRepoCollection() + collection.RLock() + defer collection.RUnlock() + + context.CollectionFactory().LocalRepoCollection().ForEach(func(r *deb.LocalRepo) error { + result = append(result, r) + return nil + }) + + c.JSON(200, result) +} + +// POST /api/repos +func apiReposCreate(c *gin.Context) { + var b struct { + Name string `binding:"required"` + Comment string + DefaultDistribution string + DefaultComponent string + } + + if !c.Bind(&b) { + return + } + + repo := deb.NewLocalRepo(b.Name, b.Comment) + repo.DefaultComponent = b.DefaultComponent + repo.DefaultDistribution = b.DefaultDistribution + + collection := context.CollectionFactory().LocalRepoCollection() + collection.Lock() + defer collection.Unlock() + + err := context.CollectionFactory().LocalRepoCollection().Add(repo) + if err != nil { + c.Fail(400, err) + return + } + + c.JSON(201, repo) +} + +// PUT /api/repos/:name +func apiReposEdit(c *gin.Context) { + var b struct { + Comment string + DefaultDistribution string + DefaultComponent string + } + + if !c.Bind(&b) { + return + } + + collection := context.CollectionFactory().LocalRepoCollection() + collection.Lock() + defer collection.Unlock() + + repo, err := collection.ByName(c.Params.ByName("name")) + if err != nil { + c.Fail(404, err) + return + } + + if b.Comment != "" { + repo.Comment = b.Comment + } + if b.DefaultDistribution != "" { + repo.DefaultDistribution = b.DefaultDistribution + } + if b.DefaultComponent != "" { + repo.DefaultComponent = b.DefaultComponent + } + + err = collection.Update(repo) + if err != nil { + c.Fail(500, err) + return + } + + c.JSON(200, repo) +} + +// GET /api/repos/:name +func apiReposShow(c *gin.Context) { + collection := context.CollectionFactory().LocalRepoCollection() + collection.RLock() + defer collection.RUnlock() + + repo, err := collection.ByName(c.Params.ByName("name")) + if err != nil { + c.Fail(404, err) + return + } + + c.JSON(200, repo) +} + +// DELETE /api/repos/:name +func apiReposDrop(c *gin.Context) { + var b struct { + Force bool + } + + if !c.Bind(&b) { + return + } + + collection := context.CollectionFactory().LocalRepoCollection() + collection.Lock() + defer collection.Unlock() + + snapshotCollection := context.CollectionFactory().SnapshotCollection() + snapshotCollection.RLock() + defer snapshotCollection.RUnlock() + + publishedCollection := context.CollectionFactory().PublishedRepoCollection() + publishedCollection.RLock() + defer publishedCollection.RUnlock() + + repo, err := collection.ByName(c.Params.ByName("name")) + if err != nil { + c.Fail(404, err) + return + } + + published := publishedCollection.ByLocalRepo(repo) + if len(published) > 0 { + c.Fail(409, fmt.Errorf("unable to drop, local repo is published")) + return + } + + snapshots := snapshotCollection.ByLocalRepoSource(repo) + if len(snapshots) > 0 { + c.Fail(409, fmt.Errorf("unable to drop, local repo has snapshots, use Force to override")) + return + } + + err = collection.Drop(repo) + if err != nil { + c.Fail(500, err) + return + } + + c.JSON(200, gin.H{}) +} + +// GET /api/repos/:name/packages +func apiReposPackagesShow(c *gin.Context) { + collection := context.CollectionFactory().LocalRepoCollection() + collection.RLock() + defer collection.RUnlock() + + repo, err := collection.ByName(c.Params.ByName("name")) + if err != nil { + c.Fail(404, err) + return + } + + err = collection.LoadComplete(repo) + if err != nil { + c.Fail(500, err) + return + } + + c.JSON(200, repo.RefList().Strings()) +} + +// POST /repos/:name/packages +func apiReposPackagesAdd(c *gin.Context) { + +} diff --git a/api/router.go b/api/router.go new file mode 100644 index 00000000..4a2bd47c --- /dev/null +++ b/api/router.go @@ -0,0 +1,31 @@ +package api + +import ( + "github.com/gin-gonic/gin" + ctx "github.com/smira/aptly/context" + "net/http" +) + +var context *ctx.AptlyContext + +// Router returns prebuilt with routes http.Handler +func Router(c *ctx.AptlyContext) http.Handler { + context = c + + router := gin.Default() + router.Use(gin.ErrorLogger()) + + root := router.Group("/api") + { + root.GET("/repos", apiReposList) + root.POST("/repos", apiReposCreate) + root.GET("/repos/:name", apiReposShow) + root.PUT("/repos/:name", apiReposEdit) + root.DELETE("/repos/:name", apiReposDrop) + + root.GET("/repos/:name/packages", apiReposPackagesShow) + root.POST("/repos/:name/packages", apiReposPackagesAdd) + } + + return router +} diff --git a/cmd/api.go b/cmd/api.go new file mode 100644 index 00000000..3057b840 --- /dev/null +++ b/cmd/api.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/smira/commander" +) + +func makeCmdAPI() *commander.Command { + return &commander.Command{ + UsageLine: "api", + Short: "start API server/issue requests", + Subcommands: []*commander.Command{ + makeCmdAPIServe(), + }, + } +} diff --git a/cmd/api_serve.go b/cmd/api_serve.go new file mode 100644 index 00000000..6d62322e --- /dev/null +++ b/cmd/api_serve.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "fmt" + "github.com/smira/aptly/api" + "github.com/smira/commander" + "github.com/smira/flag" + "net/http" +) + +func aptlyAPIServe(cmd *commander.Command, args []string) error { + var ( + err error + ) + + if len(args) != 0 { + cmd.Usage() + return commander.ErrCommandError + } + + listen := context.Flags().Lookup("listen").Value.String() + + fmt.Printf("\nStarting web server at: %s (press Ctrl+C to quit)...\n", listen) + + err = http.ListenAndServe(listen, api.Router(context)) + if err != nil { + return fmt.Errorf("unable to serve: %s", err) + } + + return err +} + +func makeCmdAPIServe() *commander.Command { + cmd := &commander.Command{ + Run: aptlyAPIServe, + UsageLine: "serve", + Short: "start API HTTP service", + Long: ` +Stat HTTP server with aptly REST API. + +Example: + + $ aptly api serve -listen=:8080 +`, + Flag: *flag.NewFlagSet("aptly-serve", flag.ExitOnError), + } + + cmd.Flag.String("listen", ":8080", "host:port for HTTP listening") + + return cmd + +} diff --git a/cmd/cmd.go b/cmd/cmd.go index c20d451c..86df0f33 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -76,7 +76,7 @@ package environment to new version.`, makeCmdPublish(), makeCmdVersion(), makeCmdPackage(), - //makeCmdAPI(), + makeCmdAPI(), }, } diff --git a/system/t03_help/MainTest_gold b/system/t03_help/MainTest_gold index b468f99b..a127f259 100644 --- a/system/t03_help/MainTest_gold +++ b/system/t03_help/MainTest_gold @@ -2,6 +2,7 @@ aptly - Debian repository management tool Commands: + api start API server/issue requests db manage aptly's internal database and package pool graph render graph of relationships mirror manage mirrors of remote repositories