mirror of
https://github.com/goharbor/harbor
synced 2025-04-13 16:53:43 +00:00

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>
203 lines
5.8 KiB
Go
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
|
|
})
|
|
}
|