harbor/src/server/middleware/repoproxy/proxy.go
stonezdj ee4b16ccdb Change the condition of LocalManifest
Compare the local digest and the remote digest when pull by tag
Use HEAD request (ManifestExist) instead of GET request (GetManifest) to avoid been throttled
For manifest list, it can avoid GET request because cached manifest list maybe different with the original manifest list
Make RemoteInterface public
Fixes #13112

Signed-off-by: stonezdj <stonezdj@gmail.com>
2020-10-14 15:15:48 +08:00

203 lines
5.8 KiB
Go

// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package repoproxy
import (
"context"
"fmt"
"io"
"net/http"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/security/proxycachesecret"
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/controller/proxy"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
httpLib "github.com/goharbor/harbor/src/lib/http"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/replication/model"
"github.com/goharbor/harbor/src/replication/registry"
"github.com/goharbor/harbor/src/server/middleware"
)
var registryMgr = registry.NewDefaultManager()
// BlobGetMiddleware handle get blob request
func BlobGetMiddleware() func(http.Handler) http.Handler {
return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
if err := handleBlob(w, r, next); err != nil {
httpLib.SendError(w, err)
}
})
}
func handleBlob(w http.ResponseWriter, r *http.Request, next http.Handler) error {
ctx := r.Context()
art, p, proxyCtl, err := preCheck(ctx)
if err != nil {
return err
}
if !canProxy(p) || proxyCtl.UseLocalBlob(ctx, art) {
next.ServeHTTP(w, r)
return nil
}
size, reader, err := proxyCtl.ProxyBlob(ctx, p, art)
if err != nil {
return err
}
defer reader.Close()
// Use io.CopyN to avoid out of memory when pulling big blob
written, err := io.CopyN(w, reader, size)
if err != nil {
return err
}
if written != size {
return errors.Errorf("The size mismatch, actual:%d, expected: %d", written, size)
}
setHeaders(w, size, "", art.Digest)
return nil
}
func preCheck(ctx context.Context) (art lib.ArtifactInfo, p *models.Project, ctl proxy.Controller, err error) {
none := lib.ArtifactInfo{}
art = lib.GetArtifactInfo(ctx)
if art == none {
return none, nil, nil, errors.New("artifactinfo is not found").WithCode(errors.NotFoundCode)
}
ctl = proxy.ControllerInstance()
p, err = project.Ctl.GetByName(ctx, art.ProjectName, project.Metadata(false))
return
}
// ManifestGetMiddleware middleware handle request for get manifest
func ManifestGetMiddleware() func(http.Handler) http.Handler {
return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
if err := handleManifest(w, r, next); err != nil {
httpLib.SendError(w, err)
}
})
}
func handleManifest(w http.ResponseWriter, r *http.Request, next http.Handler) error {
ctx := r.Context()
art, p, proxyCtl, err := preCheck(ctx)
if err != nil {
return err
}
if !canProxy(p) {
next.ServeHTTP(w, r)
return nil
}
remote, err := proxy.NewRemoteHelper(p.RegistryID)
if err != nil {
return err
}
useLocal, err := proxyCtl.UseLocalManifest(ctx, art, remote)
if err != nil {
return err
}
if useLocal {
next.ServeHTTP(w, r)
return nil
}
log.Debugf("the tag is %v, digest is %v", art.Tag, art.Digest)
err = proxyManifest(ctx, w, proxyCtl, p, art, remote)
if err != nil {
if errors.IsNotFoundErr(err) {
return err
}
log.Warningf("Proxy to remote failed, fallback to local repo, error: %v", err)
next.ServeHTTP(w, r)
}
return nil
}
func proxyManifest(ctx context.Context, w http.ResponseWriter, ctl proxy.Controller, p *models.Project, art lib.ArtifactInfo, remote proxy.RemoteInterface) error {
man, err := ctl.ProxyManifest(ctx, p, art, remote)
if err != nil {
return err
}
ct, payload, err := man.Payload()
if err != nil {
return err
}
setHeaders(w, int64(len(payload)), ct, art.Digest)
if _, err = w.Write(payload); err != nil {
return err
}
return nil
}
func canProxy(p *models.Project) bool {
if p.RegistryID < 1 {
return false
}
reg, err := registryMgr.Get(p.RegistryID)
if err != nil {
log.Errorf("failed to get registry, error:%v", err)
return false
}
if reg.Status != model.Healthy {
log.Errorf("current registry is unhealthy, regID:%v, Name:%v, Status: %v", reg.ID, reg.Name, reg.Status)
}
return reg.Status == model.Healthy
}
func setHeaders(w http.ResponseWriter, size int64, mediaType string, dig string) {
h := w.Header()
h.Set("Content-Length", fmt.Sprintf("%v", size))
if len(mediaType) > 0 {
h.Set("Content-Type", mediaType)
}
h.Set("Docker-Content-Digest", dig)
h.Set("Etag", dig)
}
// isProxySession check if current security context is proxy session
func isProxySession(ctx context.Context) bool {
sc, ok := security.FromContext(ctx)
if !ok {
log.Error("Failed to get security context")
return false
}
if sc.GetUsername() == proxycachesecret.ProxyCacheService {
return true
}
return false
}
// DisableBlobAndManifestUploadMiddleware disable push artifact to a proxy project with a non-proxy session
func DisableBlobAndManifestUploadMiddleware() func(http.Handler) http.Handler {
return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
ctx := r.Context()
art := lib.GetArtifactInfo(ctx)
p, err := project.Ctl.GetByName(ctx, art.ProjectName)
if err != nil {
httpLib.SendError(w, err)
return
}
if p.IsProxy() && !isProxySession(ctx) {
httpLib.SendError(w,
errors.DeniedError(
errors.Errorf("can not push artifact to a proxy project: %v", p.Name)))
return
}
next.ServeHTTP(w, r)
return
})
}