add get manifest information middleware in new v2 hanlder

To add a new middleware to resolve the manifest information from request path, then pass
it into the request context for later use.

Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
wang yan 2020-01-17 15:58:12 +08:00
parent c4dd6c077e
commit bffbc4009b
3 changed files with 209 additions and 0 deletions

View File

@ -0,0 +1,74 @@
package manifestinfo
import (
"fmt"
"github.com/goharbor/harbor/src/common/utils"
ierror "github.com/goharbor/harbor/src/internal/error"
project2 "github.com/goharbor/harbor/src/pkg/project"
"github.com/goharbor/harbor/src/server/middleware"
reg_err "github.com/goharbor/harbor/src/server/registry/error"
"github.com/opencontainers/go-digest"
"net/http"
"regexp"
"strings"
)
var (
manifestURLRe = regexp.MustCompile(`^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)manifests/([\w][\w.:-]{0,127})`)
)
// Middleware gets the manifest information from request and inject it into the context
func Middleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
mf, err := parseManifestInfoFromPath(req)
if err != nil {
reg_err.Handle(rw, req, err)
return
}
*req = *(req.WithContext(middleware.NewManifestInfoContext(req.Context(), mf)))
next.ServeHTTP(rw, req)
})
}
}
// parseManifestInfoFromPath parse manifest from request path
func parseManifestInfoFromPath(req *http.Request) (*middleware.ManifestInfo, error) {
match, repository, reference := MatchManifestURL(req)
if !match {
return nil, fmt.Errorf("not match url %s for manifest", req.URL.Path)
}
projectName, _ := utils.ParseRepository(repository)
project, err := project2.Mgr.Get(projectName)
if err != nil {
return nil, fmt.Errorf("failed to get project %s, error: %v", projectName, err)
}
if project == nil {
return nil, ierror.NotFoundError(nil).WithMessage("project %s not found", projectName)
}
info := &middleware.ManifestInfo{
ProjectID: project.ProjectID,
Repository: repository,
}
dgt, err := digest.Parse(reference)
if err != nil {
info.Tag = reference
} else {
info.Digest = dgt.String()
}
return info, nil
}
// MatchManifestURL ...
func MatchManifestURL(req *http.Request) (bool, string, string) {
s := manifestURLRe.FindStringSubmatch(req.URL.Path)
if len(s) == 3 {
s[1] = strings.TrimSuffix(s[1], "/")
return true, s[1], s[2]
}
return false, "", ""
}

View File

@ -0,0 +1,104 @@
package manifestinfo
import (
"fmt"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/server/middleware"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"net/http"
"net/http/httptest"
"reflect"
)
type mfinfoTestSuite struct {
suite.Suite
require *require.Assertions
assert *assert.Assertions
}
func (t *mfinfoTestSuite) SetupSuite() {
t.require = require.New(t.T())
t.assert = assert.New(t.T())
test.InitDatabaseFromEnv()
}
func (t *mfinfoTestSuite) TestParseManifestInfoFromPath() {
mustRequest := func(method, url string) *http.Request {
req, _ := http.NewRequest(method, url, nil)
return req
}
type args struct {
req *http.Request
}
tests := []struct {
name string
args args
want *middleware.ManifestInfo
wantErr bool
}{
{
"ok for digest",
args{mustRequest(http.MethodDelete, "/v2/library/photon/manifests/sha256:3e17b60ab9d92d953fb8ebefa25624c0d23fb95f78dde5572285d10158044059")},
&middleware.ManifestInfo{
ProjectID: 1,
Repository: "library/photon",
Digest: "sha256:3e17b60ab9d92d953fb8ebefa25624c0d23fb95f78dde5572285d10158044059",
},
false,
},
{
"ok for tag",
args{mustRequest(http.MethodDelete, "/v2/library/photon/manifests/latest")},
&middleware.ManifestInfo{
ProjectID: 1,
Repository: "library/photon",
Tag: "latest",
},
false,
},
{
"project not found",
args{mustRequest(http.MethodDelete, "/v2/notfound/photon/manifests/latest")},
nil,
true,
},
{
"url not match",
args{mustRequest(http.MethodDelete, "/v2/library/photon/manifest/latest")},
nil,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func() {
got, err := parseManifestInfoFromPath(tt.args.req)
if (err != nil) != tt.wantErr {
t.Errorf(err, fmt.Sprintf("ParseManifestInfoFromPath() error = %v, wantErr %v", err, tt.wantErr))
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf(err, fmt.Sprintf("ParseManifestInfoFromPath() = %v, want %v", got, tt.want))
}
})
}
}
func (t *mfinfoTestSuite) TestResolveManifest() {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
req := httptest.NewRequest(http.MethodDelete, "/v2/library/hello-world/manifests/latest", nil)
rec := httptest.NewRecorder()
Middleware()(next).ServeHTTP(rec, req)
t.assert.Equal(rec.Code, http.StatusOK)
mf, ok := middleware.ManifestInfoFromContext(req.Context())
t.assert.True(ok)
t.assert.Equal(mf.Tag, "latest")
t.assert.Equal(mf.ProjectID, int64(1))
}

View File

@ -0,0 +1,31 @@
package middleware
import (
"context"
)
type contextKey string
const (
// manifestInfoKey the context key for manifest info
manifestInfoKey = contextKey("ManifestInfo")
)
// ManifestInfo ...
type ManifestInfo struct {
ProjectID int64
Repository string
Tag string
Digest string
}
// NewManifestInfoContext returns context with manifest info
func NewManifestInfoContext(ctx context.Context, info *ManifestInfo) context.Context {
return context.WithValue(ctx, manifestInfoKey, info)
}
// ManifestInfoFromContext returns manifest info from context
func ManifestInfoFromContext(ctx context.Context) (*ManifestInfo, bool) {
info, ok := ctx.Value(manifestInfoKey).(*ManifestInfo)
return info, ok
}