Merge pull request #10482 from wy65701436/api-list-repo

add code for catalog and list tag API
This commit is contained in:
Wang Yan 2020-01-19 15:26:13 +08:00 committed by GitHub
commit 63ef743ba7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 328 additions and 14 deletions

View File

@ -14,19 +14,117 @@
package catalog
import "net/http"
import (
"encoding/json"
"fmt"
ierror "github.com/goharbor/harbor/src/internal/error"
"github.com/goharbor/harbor/src/pkg/repository"
reg_error "github.com/goharbor/harbor/src/server/registry/error"
"github.com/goharbor/harbor/src/server/registry/util"
"net/http"
"sort"
"strconv"
)
// NewHandler returns the handler to handler catalog request
func NewHandler() http.Handler {
return &handler{}
func NewHandler(repoMgr repository.Manager) http.Handler {
return &handler{
repoMgr: repoMgr,
}
}
type handler struct{}
type handler struct {
repoMgr repository.Manager
}
// ServeHTTP ...
func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO implement
switch req.Method {
case http.MethodGet:
h.get(w, req)
}
}
type catalogResponse struct {
func (h *handler) get(w http.ResponseWriter, req *http.Request) {
var maxEntries int
var err error
reqQ := req.URL.Query()
lastEntry := reqQ.Get("last")
withN := reqQ.Get("n") != ""
if withN {
maxEntries, err = strconv.Atoi(reqQ.Get("n"))
if err != nil || maxEntries < 0 {
err := ierror.New(err).WithCode(ierror.BadRequestCode).WithMessage("the N must be a positive int type")
reg_error.Handle(w, req, err)
return
}
}
var repoNames []string
// get all repositories
// ToDo filter out the untagged repos
total, repoRecords, err := h.repoMgr.List(req.Context(), nil)
if err != nil {
reg_error.Handle(w, req, err)
return
}
if total <= 0 {
h.sendResponse(w, req, repoNames)
return
}
for _, r := range repoRecords {
repoNames = append(repoNames, r.Name)
}
sort.Strings(repoNames)
if !withN {
h.sendResponse(w, req, repoNames)
return
}
// handle the pagination
resRepos := repoNames
// with "last", get items form lastEntryIndex+1 to lastEntryIndex+maxEntries
// without "last", get items from 0 to maxEntries'
if lastEntry != "" {
lastEntryIndex := util.IndexString(repoNames, lastEntry)
if lastEntryIndex == -1 {
err := ierror.New(nil).WithCode(ierror.BadRequestCode).WithMessage(fmt.Sprintf("the last: %s should be a valid repository name.", lastEntry))
reg_error.Handle(w, req, err)
return
}
resRepos = repoNames[lastEntryIndex+1 : lastEntryIndex+maxEntries]
} else {
resRepos = repoNames[0:maxEntries]
}
// compare the last item to define whether return the link header.
// if equals, means that there is no more items in DB. Do not need to give the link header.
if repoNames[len(repoNames)-1] != resRepos[len(resRepos)-1] {
urlStr, err := util.SetLinkHeader(req.URL.String(), maxEntries, resRepos[len(resRepos)-1])
if err != nil {
reg_error.Handle(w, req, err)
return
}
w.Header().Set("Link", urlStr)
}
h.sendResponse(w, req, resRepos)
return
}
// sendResponse ...
func (h *handler) sendResponse(w http.ResponseWriter, req *http.Request, repositoryNames []string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
enc := json.NewEncoder(w)
if err := enc.Encode(catalogAPIResponse{
Repositories: repositoryNames,
}); err != nil {
reg_error.Handle(w, req, err)
return
}
}
type catalogAPIResponse struct {
Repositories []string `json:"repositories"`
}

View File

@ -0,0 +1 @@
package catalog

View File

@ -16,6 +16,8 @@ package registry
import (
"github.com/goharbor/harbor/src/pkg/project"
pkg_repo "github.com/goharbor/harbor/src/pkg/repository"
pkg_tag "github.com/goharbor/harbor/src/pkg/tag"
"github.com/goharbor/harbor/src/server/middleware"
"github.com/goharbor/harbor/src/server/registry/blob"
"net/http"
@ -39,10 +41,10 @@ func New(url *url.URL) http.Handler {
rootRouter.StrictSlash(true)
// handle catalog
rootRouter.Path("/v2/_catalog").Methods(http.MethodGet).Handler(catalog.NewHandler())
rootRouter.Path("/v2/_catalog").Methods(http.MethodGet).Handler(catalog.NewHandler(pkg_repo.Mgr))
// handle list tag
rootRouter.Path("/v2/{name:.*}/tags/list").Methods(http.MethodGet).Handler(tag.NewHandler())
rootRouter.Path("/v2/{name:.*}/tags/list").Methods(http.MethodGet).Handler(tag.NewHandler(pkg_repo.Mgr, pkg_tag.Mgr))
// handle manifest
// TODO maybe we should split it into several sub routers based on the method

View File

@ -15,22 +15,167 @@
package tag
import (
"encoding/json"
"fmt"
ierror "github.com/goharbor/harbor/src/internal/error"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/repository"
"github.com/goharbor/harbor/src/pkg/tag"
reg_error "github.com/goharbor/harbor/src/server/registry/error"
"github.com/goharbor/harbor/src/server/registry/util"
"github.com/gorilla/mux"
"net/http"
"sort"
"strconv"
)
// NewHandler returns the handler to handle listing tag request
func NewHandler() http.Handler {
return &handler{}
func NewHandler(repoMgr repository.Manager, tagMgr tag.Manager) http.Handler {
return &handler{
repoMgr: repoMgr,
tagMgr: tagMgr,
}
}
type handler struct{}
type handler struct {
repoMgr repository.Manager
tagMgr tag.Manager
repositoryName string
}
// ServeHTTP ...
func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO implement
// support pagination, this is required for OCI registry spec
switch req.Method {
case http.MethodGet:
h.get(w, req)
}
}
type listTagResponse struct {
// get return the list of tags
// Content-Type: application/json
// Link: <<url>?n=<n from the request>&last=<last tag value from previous response>>; rel="next"
//
// {
// "name": "<name>",
// "tags": [
// "<tag>",
// ...
// ]
// }
func (h *handler) get(w http.ResponseWriter, req *http.Request) {
var maxEntries int
var err error
reqQ := req.URL.Query()
lastEntry := reqQ.Get("last")
withN := reqQ.Get("n") != ""
if withN {
maxEntries, err = strconv.Atoi(reqQ.Get("n"))
if err != nil || maxEntries < 0 {
err := ierror.New(err).WithCode(ierror.BadRequestCode).WithMessage("the N must be a positive int type")
reg_error.Handle(w, req, err)
return
}
}
var repoID int64
var tagNames []string
vars := mux.Vars(req)
h.repositoryName = vars["name"]
repoID, err = h.getRepoID(req)
if err != nil {
reg_error.Handle(w, req, err)
return
}
// get tags ...
total, tags, err := h.tagMgr.List(req.Context(), &q.Query{
Keywords: map[string]interface{}{
"RepositoryID": repoID,
},
})
if err != nil {
reg_error.Handle(w, req, err)
return
}
if total == 0 {
h.sendResponse(w, req, tagNames)
return
}
for _, tag := range tags {
tagNames = append(tagNames, tag.Name)
}
sort.Strings(tagNames)
if !withN {
h.sendResponse(w, req, tagNames)
return
}
// handle the pagination
resTags := tagNames
// with "last", get items form lastEntryIndex+1 to lastEntryIndex+maxEntries
// without "last", get items from 0 to maxEntries'
if lastEntry != "" {
lastEntryIndex := util.IndexString(tagNames, lastEntry)
if lastEntryIndex == -1 {
err := ierror.New(nil).WithCode(ierror.BadRequestCode).WithMessage(fmt.Sprintf("the last: %s should be a valid tag name.", lastEntry))
reg_error.Handle(w, req, err)
return
}
resTags = tagNames[lastEntryIndex+1 : lastEntryIndex+maxEntries]
} else {
resTags = tagNames[0:maxEntries]
}
// compare the last item to define whether return the link header.
// if equals, means that there is no more items in DB. Do not need to give the link header.
if tagNames[len(tagNames)-1] != resTags[len(resTags)-1] {
urlStr, err := util.SetLinkHeader(req.URL.String(), maxEntries, resTags[len(resTags)-1])
if err != nil {
reg_error.Handle(w, req, err)
return
}
w.Header().Set("Link", urlStr)
}
h.sendResponse(w, req, resTags)
return
}
// getRepoID ...
func (h *handler) getRepoID(req *http.Request) (int64, error) {
total, repoRecord, err := h.repoMgr.List(req.Context(), &q.Query{
Keywords: map[string]interface{}{
"name": h.repositoryName,
},
})
if err != nil {
return 0, err
}
if total <= 0 {
err := ierror.New(nil).WithCode(ierror.NotFoundCode).WithMessage("repositoryNotFound")
return 0, err
}
return repoRecord[0].RepositoryID, nil
}
// sendResponse ...
func (h *handler) sendResponse(w http.ResponseWriter, req *http.Request, tagNames []string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
enc := json.NewEncoder(w)
if err := enc.Encode(tagsAPIResponse{
Name: h.repositoryName,
Tags: tagNames,
}); err != nil {
reg_error.Handle(w, req, err)
return
}
}
type tagsAPIResponse struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}

View File

@ -0,0 +1 @@
package tag

View File

@ -0,0 +1,38 @@
package util
import (
"fmt"
"net/url"
"sort"
"strconv"
)
// SetLinkHeader ...
func SetLinkHeader(origURL string, n int, last string) (string, error) {
passedURL, err := url.Parse(origURL)
if err != nil {
return "", err
}
passedURL.Fragment = ""
v := url.Values{}
v.Add("n", strconv.Itoa(n))
v.Add("last", last)
passedURL.RawQuery = v.Encode()
urlStr := fmt.Sprintf("<%s>; rel=\"next\"", passedURL.String())
return urlStr, nil
}
// IndexString returns the index of X in a sorts string array
// If the array is not sorted, sort it.
func IndexString(strs []string, x string) int {
if !sort.StringsAreSorted(strs) {
sort.Strings(strs)
}
i := sort.Search(len(strs), func(i int) bool { return x <= strs[i] })
if i < len(strs) && strs[i] == x {
return i
}
return -1
}

View File

@ -0,0 +1,29 @@
package util
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestCreateLinkEntry(t *testing.T) {
u1, err := SetLinkHeader("/v2/hello-wrold/tags/list", 10, "v10")
assert.Nil(t, err)
assert.Equal(t, u1, "</v2/hello-wrold/tags/list?last=v10&n=10>; rel=\"next\"")
u2, err := SetLinkHeader("/v2/hello-wrold/tags/list", 5, "v5")
assert.Nil(t, err)
assert.Equal(t, u2, "</v2/hello-wrold/tags/list?last=v5&n=5>; rel=\"next\"")
}
func TestIndexString(t *testing.T) {
a := []string{"B", "A", "C", "E"}
assert.True(t, IndexString(a, "E") == 3)
assert.True(t, IndexString(a, "B") == 1)
assert.True(t, IndexString(a, "A") == 0)
assert.True(t, IndexString(a, "C") == 2)
assert.True(t, IndexString(a, "Z") == -1)
assert.True(t, IndexString(a, "") == -1)
}