drone/registry/app/pkg/docker/controller.go

510 lines
14 KiB
Go

// Source: https://gitlab.com/gitlab-org/container-registry
// Copyright 2019 Gitlab Inc.
//
// 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 docker
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"github.com/harness/gitness/app/auth/authz"
corestore "github.com/harness/gitness/app/store"
"github.com/harness/gitness/registry/app/dist_temp/errcode"
"github.com/harness/gitness/registry/app/pkg"
"github.com/harness/gitness/registry/app/pkg/commons"
"github.com/harness/gitness/registry/app/storage"
"github.com/harness/gitness/registry/app/store"
registrytypes "github.com/harness/gitness/registry/types"
"github.com/harness/gitness/types/enum"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/rs/zerolog/log"
)
type Controller struct {
*pkg.CoreController
local *LocalRegistry
remote *RemoteRegistry
spaceStore corestore.SpaceStore
authorizer authz.Authorizer
DBStore *DBStore
}
type DBStore struct {
BlobRepo store.BlobRepository
ImageDao store.ImageRepository
ArtifactDao store.ArtifactRepository
BandwidthStatDao store.BandwidthStatRepository
DownloadStatDao store.DownloadStatRepository
}
type TagsAPIResponse struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
var _ pkg.Artifact = (*LocalRegistry)(nil)
var _ pkg.Artifact = (*RemoteRegistry)(nil)
func NewController(
local *LocalRegistry,
remote *RemoteRegistry,
coreController *pkg.CoreController,
spaceStore corestore.SpaceStore,
authorizer authz.Authorizer,
dBStore *DBStore,
) *Controller {
c := &Controller{
CoreController: coreController,
local: local,
remote: remote,
spaceStore: spaceStore,
authorizer: authorizer,
DBStore: dBStore,
}
pkg.TypeRegistry[pkg.LocalRegistry] = local
pkg.TypeRegistry[pkg.RemoteRegistry] = remote
return c
}
func NewDBStore(
blobRepo store.BlobRepository,
imageDao store.ImageRepository,
artifactDao store.ArtifactRepository,
bandwidthStatDao store.BandwidthStatRepository,
downloadStatDao store.DownloadStatRepository,
) *DBStore {
return &DBStore{
BlobRepo: blobRepo,
ImageDao: imageDao,
ArtifactDao: artifactDao,
BandwidthStatDao: bandwidthStatDao,
DownloadStatDao: downloadStatDao,
}
}
func isEmpty(slice interface{}) bool {
if slice == nil {
return true
}
return reflect.ValueOf(slice).Len() == 0
}
func (c *Controller) ProxyWrapper(
ctx context.Context,
f func(registry registrytypes.Registry, imageName string, artInfo pkg.Artifact) Response,
info pkg.RegistryInfo,
) Response {
none := pkg.RegistryInfo{}
if info == none {
log.Ctx(ctx).Error().Stack().Msg("artifactinfo is not found")
return nil
}
var response Response
requestRepoKey := info.RegIdentifier
imageName := info.Image
if repos, err := c.GetOrderedRepos(ctx, requestRepoKey, info); err == nil {
for _, registry := range repos {
log.Ctx(ctx).Info().Msgf("Using Repository: %s, Type: %s", registry.Name, registry.Type)
artifact, ok := c.GetArtifact(registry).(Registry)
if !ok {
log.Ctx(ctx).Warn().Msgf("artifact %s is not a registry", registry.Name)
continue
}
if artifact != nil {
response = f(registry, imageName, artifact)
if isEmpty(response.GetErrors()) {
return response
}
}
}
}
return response
}
func (c *Controller) HeadManifest(
ctx context.Context,
art pkg.RegistryInfo,
acceptHeaders []string,
ifNoneMatchHeader []string,
) Response {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, art.RegIdentifier, art.ParentID, enum.PermissionArtifactsDownload,
)
if err != nil {
return &GetManifestResponse{
Errors: []error{errcode.ErrCodeDenied},
}
}
f := func(registry registrytypes.Registry, _ string, a pkg.Artifact) Response {
art.SetRepoKey(registry.Name)
headers, desc, man, e := a.(Registry).ManifestExist(ctx, art, acceptHeaders, ifNoneMatchHeader)
response := &GetManifestResponse{e, headers, desc, man}
return response
}
result := c.ProxyWrapper(ctx, f, art)
return result
}
func (c *Controller) PullManifest(
ctx context.Context,
art pkg.RegistryInfo,
acceptHeaders []string,
ifNoneMatchHeader []string,
) Response {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, art.RegIdentifier, art.ParentID, enum.PermissionArtifactsDownload,
)
if err != nil {
return &GetManifestResponse{
Errors: []error{errcode.ErrCodeDenied},
}
}
f := func(registry registrytypes.Registry, _ string, a pkg.Artifact) Response {
art.SetRepoKey(registry.Name)
headers, desc, man, e := a.(Registry).PullManifest(ctx, art, acceptHeaders, ifNoneMatchHeader)
response := &GetManifestResponse{e, headers, desc, man}
return response
}
result := c.ProxyWrapper(ctx, f, art)
return result
}
func (c *Controller) PutManifest(
ctx context.Context,
artInfo pkg.RegistryInfo,
mediaType string,
body io.ReadCloser,
length int64,
) (responseHeaders *commons.ResponseHeaders, errs []error) {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, artInfo.RegIdentifier,
artInfo.ParentID, enum.PermissionArtifactsUpload, enum.PermissionArtifactsDownload,
)
if err != nil {
return nil, []error{errcode.ErrCodeDenied}
}
return c.local.PutManifest(ctx, artInfo, mediaType, body, length)
}
func (c *Controller) DeleteManifest(
ctx context.Context,
artInfo pkg.RegistryInfo,
) (errs []error, responseHeaders *commons.ResponseHeaders) {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, artInfo.RegIdentifier, artInfo.ParentID,
enum.PermissionArtifactsDelete,
)
if err != nil {
return []error{errcode.ErrCodeDenied}, nil
}
return c.local.DeleteManifest(ctx, artInfo)
}
func (c *Controller) HeadBlob(
ctx context.Context,
info pkg.RegistryInfo,
) (
responseHeaders *commons.ResponseHeaders, fr *storage.FileReader, size int64, readCloser io.ReadCloser,
redirectURL string, errs []error,
) {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, info.RegIdentifier, info.ParentID, enum.PermissionArtifactsDownload,
)
if err != nil {
return nil, nil, 0, nil, "", []error{errcode.ErrCodeDenied}
}
return c.local.HeadBlob(ctx, info)
}
func (c *Controller) GetBlob(ctx context.Context, info pkg.RegistryInfo) Response {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, info.RegIdentifier, info.ParentID, enum.PermissionArtifactsDownload,
)
if err != nil {
return &GetBlobResponse{
Errors: []error{errcode.ErrCodeDenied},
}
}
f := func(registry registrytypes.Registry, _ string, a pkg.Artifact) Response {
info.SetRepoKey(registry.Name)
headers, body, size, readCloser, redirectURL, errs := a.(Registry).GetBlob(ctx, info)
return &GetBlobResponse{errs, headers, body, size, readCloser, redirectURL}
}
return c.ProxyWrapper(ctx, f, info)
}
func (c *Controller) InitiateUploadBlob(
ctx context.Context,
info pkg.RegistryInfo,
fromImageRef string,
mountDigest string,
) (*commons.ResponseHeaders, []error) {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, info.RegIdentifier, info.ParentID, enum.PermissionArtifactsUpload,
enum.PermissionArtifactsDownload,
)
if err != nil {
return nil, []error{errcode.ErrCodeDenied}
}
return c.local.InitBlobUpload(ctx, info, fromImageRef, mountDigest)
}
func (c *Controller) GetUploadBlobStatus(
ctx context.Context,
info pkg.RegistryInfo,
token string,
) (responseHeaders *commons.ResponseHeaders, errs []error) {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, info.RegIdentifier, info.ParentID, enum.PermissionArtifactsDownload,
)
if err != nil {
return nil, []error{errcode.ErrCodeDenied}
}
blobCtx := c.local.App.GetBlobsContext(ctx, info)
return c.local.GetBlobUploadStatus(blobCtx, info, token)
}
func (c *Controller) PatchBlobUpload(
ctx context.Context,
info pkg.RegistryInfo,
ct string,
cr string,
cl string,
length int64,
token string,
body io.ReadCloser,
) (responseHeaders *commons.ResponseHeaders, errors []error) {
blobCtx := c.local.App.GetBlobsContext(ctx, info)
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, info.RegIdentifier, info.ParentID, enum.PermissionArtifactsDownload,
enum.PermissionArtifactsUpload,
)
if err != nil {
return nil, []error{errcode.ErrCodeDenied}
}
errors = make([]error, 0)
if blobCtx.UUID != "" {
errs := ResumeBlobUpload(blobCtx, token)
errors = append(errors, errs...)
}
if blobCtx.Upload != nil {
defer blobCtx.Upload.Close()
}
rs, errs := c.local.PushBlobChunk(blobCtx, info, ct, cr, cl, body, length)
if !commons.IsEmpty(errs) {
errors = append(errors, errs...)
}
return rs, errors
}
func (c *Controller) CompleteBlobUpload(
ctx context.Context,
info pkg.RegistryInfo,
body io.ReadCloser,
length int64,
stateToken string,
) (responseHeaders *commons.ResponseHeaders, errs []error) {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, info.RegIdentifier, info.ParentID, enum.PermissionArtifactsUpload,
enum.PermissionArtifactsDownload,
)
if err != nil {
return nil, []error{errcode.ErrCodeDenied}
}
return c.local.PushBlob(ctx, info, body, length, stateToken)
}
func (c *Controller) CancelBlobUpload(
ctx context.Context,
info pkg.RegistryInfo,
stateToken string,
) (responseHeaders *commons.ResponseHeaders, errors []error) {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, info.RegIdentifier, info.ParentID, enum.PermissionArtifactsDelete,
)
if err != nil {
return nil, []error{errcode.ErrCodeDenied}
}
blobCtx := c.local.App.GetBlobsContext(ctx, info)
errors = make([]error, 0)
if blobCtx.UUID != "" {
errs := ResumeBlobUpload(blobCtx, stateToken)
errors = append(errors, errs...)
}
if blobCtx.Upload == nil {
e := errcode.ErrCodeBlobUploadUnknown
errors = append(errors, e)
return responseHeaders, errors
}
defer blobCtx.Upload.Close()
responseHeaders = &commons.ResponseHeaders{
Headers: map[string]string{"Docker-Upload-UUID": blobCtx.UUID},
}
if err := blobCtx.Upload.Cancel(blobCtx); err != nil {
log.Ctx(ctx).Error().Stack().Err(err).Msgf("error encountered canceling upload: %v", err)
errors = append(errors, errcode.ErrCodeUnknown.WithDetail(err))
}
responseHeaders.Code = http.StatusNoContent
return responseHeaders, errors
}
func (c *Controller) DeleteBlob(
ctx context.Context,
info pkg.RegistryInfo,
) (responseHeaders *commons.ResponseHeaders, errs []error) {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, info.RegIdentifier, info.ParentID, enum.PermissionArtifactsDelete,
)
if err != nil {
return nil, []error{errcode.ErrCodeDenied}
}
blobCtx := c.local.App.GetBlobsContext(ctx, info)
return c.local.DeleteBlob(blobCtx, info)
}
func (c *Controller) GetTags(
ctx context.Context,
lastEntry string,
maxEntries int,
origURL string,
artInfo pkg.RegistryInfo,
) (*commons.ResponseHeaders, []string, error) {
err := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, artInfo.RegIdentifier,
artInfo.ParentID, enum.PermissionArtifactsDownload,
)
if err != nil {
return nil, nil, errcode.ErrCodeDenied
}
return c.local.ListTags(ctx, lastEntry, maxEntries, origURL, artInfo)
}
func (c *Controller) GetCatalog(_ http.ResponseWriter, _ *http.Request) {
log.Info().Msgf("Not implemented yet!")
}
func (c *Controller) GetReferrers(
ctx context.Context,
artInfo pkg.RegistryInfo,
artifactType string,
) (index *v1.Index, responseHeaders *commons.ResponseHeaders, err error) {
accessErr := GetRegistryCheckAccess(
ctx, c.RegistryDao, c.authorizer, c.spaceStore, artInfo.RegIdentifier,
artInfo.ParentID, enum.PermissionArtifactsDownload,
)
if accessErr != nil {
return nil, nil, errcode.ErrCodeDenied
}
return c.local.ListReferrers(ctx, artInfo, artifactType)
}
func ResumeBlobUpload(ctx *Context, stateToken string) []error {
var errs []error
state, err := hmacKey(ctx.App.Config.Registry.HTTP.Secret).unpackUploadState(stateToken)
if err != nil {
log.Ctx(ctx).Info().Msgf("error resolving upload: %v", err)
errs = append(errs, errcode.ErrCodeBlobUploadInvalid.WithDetail(err))
return errs
}
ctx.State = state
if state.Path != ctx.OciBlobStore.Path() {
log.Ctx(ctx).Info().Msgf("mismatched path in upload state: %q != %q", state.Path, ctx.OciBlobStore.Path())
errs = append(errs, errcode.ErrCodeBlobUploadInvalid.WithDetail(err))
return errs
}
if state.UUID != ctx.UUID {
log.Ctx(ctx).Info().Msgf("mismatched uuid in upload state: %q != %q", state.UUID, ctx.UUID)
errs = append(errs, errcode.ErrCodeBlobUploadInvalid.WithDetail(err))
return errs
}
blobs := ctx.OciBlobStore
upload, err := blobs.Resume(ctx.Context, ctx.UUID)
if err != nil {
log.Ctx(ctx).Error().Stack().Err(err).Msgf("error resolving upload: %v", err)
if errors.Is(err, storage.ErrBlobUploadUnknown) {
errs = append(errs, errcode.ErrCodeBlobUploadUnknown.WithDetail(err))
return errs
}
errs = append(errs, errcode.ErrCodeUnknown.WithDetail(err))
return errs
}
ctx.Upload = upload
if size := upload.Size(); size != ctx.State.Offset {
log.Ctx(ctx).Error().Stack().Err(err).Msgf("upload resumed at wrong offset: %d != %d", size, ctx.State.Offset)
errs = append(errs, errcode.ErrCodeRangeInvalid.WithDetail(err))
return errs
}
return errs
}
// unpackUploadState unpacks and validates the blob upload state from the
// token, using the hmacKey secret.
func (secret hmacKey) unpackUploadState(token string) (BlobUploadState, error) {
var state BlobUploadState
tokenBytes, err := base64.URLEncoding.DecodeString(token)
if err != nil {
return state, fmt.Errorf("failed to decode token: %w", err)
}
mac := hmac.New(sha256.New, []byte(secret))
if len(tokenBytes) < mac.Size() {
return state, errInvalidSecret
}
macBytes := tokenBytes[:mac.Size()]
messageBytes := tokenBytes[mac.Size():]
mac.Write(messageBytes)
if !hmac.Equal(mac.Sum(nil), macBytes) {
return state, errInvalidSecret
}
if err := json.Unmarshal(messageBytes, &state); err != nil {
return state, err
}
return state, nil
}