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
|
||||
|
||||
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"`
|
||||
}
|
||||
|
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 (
|
||||
"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
|
||||
|
@ -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"`
|
||||
}
|
||||
|
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