From 986989af3ce0e03ca13e05a5b11bf750df250f6a Mon Sep 17 00:00:00 2001 From: wang yan Date: Tue, 14 Jan 2020 16:45:08 +0800 Subject: [PATCH] add code for catalog and list tag API Signed-off-by: wang yan --- src/server/registry/catalog/catalog.go | 110 +++++++++++++- src/server/registry/catalog/catalog_test.go | 1 + src/server/registry/handler.go | 6 +- src/server/registry/tag/tag.go | 157 +++++++++++++++++++- src/server/registry/tag/tag_test.go | 1 + src/server/registry/util/util.go | 38 +++++ src/server/registry/util/util_test.go | 29 ++++ 7 files changed, 328 insertions(+), 14 deletions(-) create mode 100644 src/server/registry/catalog/catalog_test.go create mode 100644 src/server/registry/tag/tag_test.go create mode 100644 src/server/registry/util/util.go create mode 100644 src/server/registry/util/util_test.go diff --git a/src/server/registry/catalog/catalog.go b/src/server/registry/catalog/catalog.go index 44882df00..4e4560de2 100644 --- a/src/server/registry/catalog/catalog.go +++ b/src/server/registry/catalog/catalog.go @@ -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"` } diff --git a/src/server/registry/catalog/catalog_test.go b/src/server/registry/catalog/catalog_test.go new file mode 100644 index 000000000..e571e24c6 --- /dev/null +++ b/src/server/registry/catalog/catalog_test.go @@ -0,0 +1 @@ +package catalog diff --git a/src/server/registry/handler.go b/src/server/registry/handler.go index 8229f9fe5..4d43fb009 100644 --- a/src/server/registry/handler.go +++ b/src/server/registry/handler.go @@ -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 diff --git a/src/server/registry/tag/tag.go b/src/server/registry/tag/tag.go index d7ae2ab8c..057614130 100644 --- a/src/server/registry/tag/tag.go +++ b/src/server/registry/tag/tag.go @@ -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: <?n=&last=>; rel="next" +// +// { +// "name": "", +// "tags": [ +// "", +// ... +// ] +// } +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"` } diff --git a/src/server/registry/tag/tag_test.go b/src/server/registry/tag/tag_test.go new file mode 100644 index 000000000..686c0dfff --- /dev/null +++ b/src/server/registry/tag/tag_test.go @@ -0,0 +1 @@ +package tag diff --git a/src/server/registry/util/util.go b/src/server/registry/util/util.go new file mode 100644 index 000000000..9972149b1 --- /dev/null +++ b/src/server/registry/util/util.go @@ -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 +} diff --git a/src/server/registry/util/util_test.go b/src/server/registry/util/util_test.go new file mode 100644 index 000000000..df0e73ed5 --- /dev/null +++ b/src/server/registry/util/util_test.go @@ -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, "; rel=\"next\"") + + u2, err := SetLinkHeader("/v2/hello-wrold/tags/list", 5, "v5") + assert.Nil(t, err) + assert.Equal(t, u2, "; 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) +}