mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-18 14:47:38 +01:00
add code for catalog and list tag API
Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
parent
c4dd6c077e
commit
986989af3c
@ -14,19 +14,117 @@
|
|||||||
|
|
||||||
package catalog
|
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
|
// NewHandler returns the handler to handler catalog request
|
||||||
func NewHandler() http.Handler {
|
func NewHandler(repoMgr repository.Manager) http.Handler {
|
||||||
return &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) {
|
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"`
|
Repositories []string `json:"repositories"`
|
||||||
}
|
}
|
||||||
|
1
src/server/registry/catalog/catalog_test.go
Normal file
1
src/server/registry/catalog/catalog_test.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package catalog
|
@ -16,6 +16,8 @@ package registry
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/goharbor/harbor/src/pkg/project"
|
"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/middleware"
|
||||||
"github.com/goharbor/harbor/src/server/registry/blob"
|
"github.com/goharbor/harbor/src/server/registry/blob"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -39,10 +41,10 @@ func New(url *url.URL) http.Handler {
|
|||||||
rootRouter.StrictSlash(true)
|
rootRouter.StrictSlash(true)
|
||||||
|
|
||||||
// handle catalog
|
// 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
|
// 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
|
// handle manifest
|
||||||
// TODO maybe we should split it into several sub routers based on the method
|
// TODO maybe we should split it into several sub routers based on the method
|
||||||
|
@ -15,22 +15,167 @@
|
|||||||
package tag
|
package tag
|
||||||
|
|
||||||
import (
|
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"
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHandler returns the handler to handle listing tag request
|
// NewHandler returns the handler to handle listing tag request
|
||||||
func NewHandler() http.Handler {
|
func NewHandler(repoMgr repository.Manager, tagMgr tag.Manager) http.Handler {
|
||||||
return &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) {
|
func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
// TODO implement
|
switch req.Method {
|
||||||
// support pagination, this is required for OCI registry spec
|
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"`
|
Name string `json:"name"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
}
|
}
|
||||||
|
1
src/server/registry/tag/tag_test.go
Normal file
1
src/server/registry/tag/tag_test.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package tag
|
38
src/server/registry/util/util.go
Normal file
38
src/server/registry/util/util.go
Normal 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
|
||||||
|
}
|
29
src/server/registry/util/util_test.go
Normal file
29
src/server/registry/util/util_test.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user