mirror of https://github.com/harness/drone.git
feat:[AH-927]: generic artifact router and redirect url changes (#3322)
* feat:[AH-927]: fix checks * feat:[AH-926]: fix checks * feat:[AH-927]: fix checks * feat:[AH-927]: fix checks * feat:[AH-927]: generic artifact router and redirect url changespull/3616/head
parent
148b29fe7e
commit
b501dbe44a
|
@ -0,0 +1,242 @@
|
|||
// Copyright 2023 Harness, 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 generic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
usercontroller "github.com/harness/gitness/app/api/controller/user"
|
||||
"github.com/harness/gitness/app/auth/authn"
|
||||
"github.com/harness/gitness/app/auth/authz"
|
||||
corestore "github.com/harness/gitness/app/store"
|
||||
urlprovider "github.com/harness/gitness/app/url"
|
||||
"github.com/harness/gitness/registry/app/api/controller/metadata"
|
||||
"github.com/harness/gitness/registry/app/api/handler/utils"
|
||||
artifact2 "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
|
||||
"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/pkg/generic"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
packageNameRegex = `^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$`
|
||||
versionRegex = `^[a-z0-9][a-z0-9.-]*[a-z0-9]$`
|
||||
filenameRegex = `^[a-zA-Z0-9][a-zA-Z0-9._~@,/-]*[a-zA-Z0-9]$`
|
||||
// Add other route types here.
|
||||
)
|
||||
|
||||
func NewGenericArtifactHandler(
|
||||
spaceStore corestore.SpaceStore, controller *generic.Controller, tokenStore corestore.TokenStore,
|
||||
userCtrl *usercontroller.Controller, authenticator authn.Authenticator, urlProvider urlprovider.Provider,
|
||||
authorizer authz.Authorizer,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
Controller: controller,
|
||||
SpaceStore: spaceStore,
|
||||
TokenStore: tokenStore,
|
||||
UserCtrl: userCtrl,
|
||||
Authenticator: authenticator,
|
||||
URLProvider: urlProvider,
|
||||
Authorizer: authorizer,
|
||||
}
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
Controller *generic.Controller
|
||||
SpaceStore corestore.SpaceStore
|
||||
TokenStore corestore.TokenStore
|
||||
UserCtrl *usercontroller.Controller
|
||||
Authenticator authn.Authenticator
|
||||
URLProvider urlprovider.Provider
|
||||
Authorizer authz.Authorizer
|
||||
}
|
||||
|
||||
func (h *Handler) GetArtifactInfo(r *http.Request) (pkg.GenericArtifactInfo, errcode.Error) {
|
||||
ctx := r.Context()
|
||||
path := r.URL.Path
|
||||
rootIdentifier, registryIdentifier, artifact, tag, fileName, description, err := ExtractPathVars(r)
|
||||
|
||||
if err != nil {
|
||||
return pkg.GenericArtifactInfo{}, errcode.ErrCodeInvalidRequest.WithDetail(err)
|
||||
}
|
||||
|
||||
if err := metadata.ValidateIdentifier(registryIdentifier); err != nil {
|
||||
return pkg.GenericArtifactInfo{}, errcode.ErrCodeInvalidRequest.WithDetail(err)
|
||||
}
|
||||
|
||||
if err := validatePackageVersionAndFileName(artifact, tag, fileName); err != nil {
|
||||
return pkg.GenericArtifactInfo{}, errcode.ErrCodeInvalidRequest.WithDetail(err)
|
||||
}
|
||||
|
||||
rootSpace, err := h.SpaceStore.FindByRefCaseInsensitive(ctx, rootIdentifier)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Msgf("Root space not found: %s", rootIdentifier)
|
||||
return pkg.GenericArtifactInfo{}, errcode.ErrCodeRootNotFound.WithDetail(err)
|
||||
}
|
||||
|
||||
registry, err := h.Controller.DBStore.RegistryDao.GetByRootParentIDAndName(ctx, rootSpace.ID, registryIdentifier)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Msgf(
|
||||
"registry %s not found for root: %s. Reason: %s", registryIdentifier, rootSpace.Identifier, err,
|
||||
)
|
||||
return pkg.GenericArtifactInfo{}, errcode.ErrCodeRegNotFound.WithDetail(err)
|
||||
}
|
||||
|
||||
if registry.PackageType != artifact2.PackageTypeGENERIC {
|
||||
log.Ctx(ctx).Error().Msgf(
|
||||
"registry %s is not a generic artifact registry for root: %s", registryIdentifier, rootSpace.Identifier,
|
||||
)
|
||||
return pkg.GenericArtifactInfo{}, errcode.ErrCodeInvalidRequest.WithDetail(fmt.Errorf("registry %s is"+
|
||||
" not a generic artifact registry", registryIdentifier))
|
||||
}
|
||||
|
||||
_, err = h.SpaceStore.Find(r.Context(), registry.ParentID)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Msgf("Parent space not found: %d", registry.ParentID)
|
||||
return pkg.GenericArtifactInfo{}, errcode.ErrCodeParentNotFound.WithDetail(err)
|
||||
}
|
||||
|
||||
info := &pkg.GenericArtifactInfo{
|
||||
ArtifactInfo: &pkg.ArtifactInfo{
|
||||
BaseInfo: &pkg.BaseInfo{
|
||||
RootIdentifier: rootIdentifier,
|
||||
RootParentID: rootSpace.ID,
|
||||
ParentID: registry.ParentID,
|
||||
},
|
||||
RegIdentifier: registryIdentifier,
|
||||
Image: artifact,
|
||||
},
|
||||
RegistryID: registry.ID,
|
||||
Version: tag,
|
||||
FileName: fileName,
|
||||
Description: description,
|
||||
}
|
||||
|
||||
log.Ctx(ctx).Info().Msgf("Dispatch: URI: %s", path)
|
||||
if commons.IsEmpty(rootSpace.Identifier) {
|
||||
log.Ctx(ctx).Error().Msgf("ParentRef not found in context")
|
||||
return pkg.GenericArtifactInfo{}, errcode.ErrCodeParentNotFound.WithDetail(err)
|
||||
}
|
||||
|
||||
if commons.IsEmpty(registryIdentifier) {
|
||||
log.Ctx(ctx).Warn().Msgf("registry not found in context")
|
||||
return pkg.GenericArtifactInfo{}, errcode.ErrCodeRegNotFound.WithDetail(err)
|
||||
}
|
||||
|
||||
if !commons.IsEmpty(info.Image) && !commons.IsEmpty(info.Version) && !commons.IsEmpty(info.FileName) {
|
||||
flag, err2 := utils.MatchArtifactFilter(registry.AllowedPattern, registry.BlockedPattern,
|
||||
info.Image+":"+info.Version+":"+info.FileName)
|
||||
if !flag || err2 != nil {
|
||||
return pkg.GenericArtifactInfo{}, errcode.ErrCodeInvalidRequest.WithDetail(err2)
|
||||
}
|
||||
}
|
||||
|
||||
return *info, errcode.Error{}
|
||||
}
|
||||
|
||||
// ExtractPathVars extracts registry,image, reference, digest and tag from the path
|
||||
// Path format: /generic/:rootSpace/:registry/:image/:tag (for ex:
|
||||
// /generic/myRootSpace/reg1/alpine/v1).
|
||||
func ExtractPathVars(r *http.Request) (rootIdentifier, registry, artifact,
|
||||
tag, fileName string, description string, err error) {
|
||||
path := r.URL.Path
|
||||
|
||||
// Ensure the path starts with "/generic/"
|
||||
if !strings.HasPrefix(path, "/generic/") {
|
||||
return "", "", "", "", "", "", fmt.Errorf("invalid path: must start with /generic/")
|
||||
}
|
||||
|
||||
trimmedPath := strings.TrimPrefix(path, "/generic/")
|
||||
firstSlashIndex := strings.Index(trimmedPath, "/")
|
||||
if firstSlashIndex == -1 {
|
||||
return "", "", "", "", "", "", fmt.Errorf("invalid path format: missing rootIdentifier or registry")
|
||||
}
|
||||
rootIdentifier = trimmedPath[:firstSlashIndex]
|
||||
|
||||
remainingPath := trimmedPath[firstSlashIndex+1:]
|
||||
secondSlashIndex := strings.Index(remainingPath, "/")
|
||||
if secondSlashIndex == -1 {
|
||||
return "", "", "", "", "", "", fmt.Errorf("invalid path format: missing registry")
|
||||
}
|
||||
registry = remainingPath[:secondSlashIndex]
|
||||
|
||||
// Extract the artifact and tag from the remaining path
|
||||
artifactPath := remainingPath[secondSlashIndex+1:]
|
||||
|
||||
// Check if the artifactPath contains a ":" for tag and filename
|
||||
if strings.Contains(artifactPath, ":") {
|
||||
segments := strings.SplitN(artifactPath, ":", 3)
|
||||
if len(segments) < 3 {
|
||||
return "", "", "", "", "", "", fmt.Errorf("invalid artifact format: %s", artifactPath)
|
||||
}
|
||||
artifact = segments[0]
|
||||
tag = segments[1]
|
||||
fileName = segments[2]
|
||||
} else {
|
||||
segments := strings.SplitN(artifactPath, "/", 2)
|
||||
if len(segments) < 2 {
|
||||
return "", "", "", "", "", "", fmt.Errorf("invalid artifact format: %s", artifactPath)
|
||||
}
|
||||
artifact = segments[0]
|
||||
tag = segments[1]
|
||||
|
||||
fileName = r.FormValue("filename")
|
||||
if fileName == "" {
|
||||
return "", "", "", "", "", "", fmt.Errorf("filename not provided in path or form parameter")
|
||||
}
|
||||
}
|
||||
description = r.FormValue("description")
|
||||
|
||||
return rootIdentifier, registry, artifact, tag, fileName, description, nil
|
||||
}
|
||||
|
||||
func handleErrors(ctx context.Context, err errcode.Error, w http.ResponseWriter) {
|
||||
if !commons.IsEmptyError(err) {
|
||||
w.WriteHeader(err.Code.Descriptor().HTTPStatusCode)
|
||||
_ = errcode.ServeJSON(w, err)
|
||||
log.Ctx(ctx).Error().Msgf("Error occurred while performing generic artifact action: %s", err.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func validatePackageVersionAndFileName(packageName, version, filename string) error {
|
||||
// Compile the regular expressions
|
||||
packageNameRe := regexp.MustCompile(packageNameRegex)
|
||||
versionRe := regexp.MustCompile(versionRegex)
|
||||
filenameRe := regexp.MustCompile(filenameRegex)
|
||||
|
||||
// Validate package name
|
||||
if !packageNameRe.MatchString(packageName) {
|
||||
return fmt.Errorf("invalid package name: %s", packageName)
|
||||
}
|
||||
|
||||
// Validate version
|
||||
if !versionRe.MatchString(version) {
|
||||
return fmt.Errorf("invalid version: %s", version)
|
||||
}
|
||||
|
||||
// Validate filename
|
||||
if !filenameRe.MatchString(filename) {
|
||||
return fmt.Errorf("invalid filename: %s", filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2023 Harness, 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 generic
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/registry/app/pkg"
|
||||
"github.com/harness/gitness/registry/app/pkg/commons"
|
||||
"github.com/harness/gitness/registry/app/storage"
|
||||
)
|
||||
|
||||
func (h *Handler) PullArtifact(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
info, err := h.GetArtifactInfo(r)
|
||||
if !commons.IsEmptyError(err) {
|
||||
handleErrors(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
headers, fileReader, redirectURL, err := h.Controller.PullArtifact(ctx, info)
|
||||
if commons.IsEmptyError(err) {
|
||||
if redirectURL != "" {
|
||||
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
h.serveContent(w, r, fileReader, info)
|
||||
headers.WriteToResponse(w)
|
||||
return
|
||||
}
|
||||
handleErrors(r.Context(), err, w)
|
||||
}
|
||||
|
||||
func (h *Handler) serveContent(
|
||||
w http.ResponseWriter, r *http.Request, fileReader *storage.FileReader, info pkg.GenericArtifactInfo,
|
||||
) {
|
||||
if fileReader != nil {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+info.FileName)
|
||||
http.ServeContent(w, r, info.FileName, time.Time{}, fileReader)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2023 Harness, 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 generic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/registry/app/dist_temp/errcode"
|
||||
"github.com/harness/gitness/registry/app/pkg/commons"
|
||||
)
|
||||
|
||||
func (h *Handler) PushArtifact(w http.ResponseWriter, r *http.Request) {
|
||||
info, err := h.GetArtifactInfo(r)
|
||||
if !commons.IsEmptyError(err) {
|
||||
handleErrors(r.Context(), err, w)
|
||||
return
|
||||
}
|
||||
|
||||
file, _, err1 := r.FormFile("file")
|
||||
if err1 != nil {
|
||||
handleErrors(r.Context(),
|
||||
errcode.ErrCodeInvalidRequest.WithMessage(fmt.Sprintf("failed to parse file: %s, "+
|
||||
"please provide correct file path ", err.Message)), w)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
defer file.Close()
|
||||
headers, err := h.Controller.UploadArtifact(ctx, info, file)
|
||||
if commons.IsEmptyError(err) {
|
||||
headers.WriteToResponse(w)
|
||||
}
|
||||
handleErrors(r.Context(), err, w)
|
||||
}
|
|
@ -16,13 +16,19 @@ package middleware
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/registry/app/api/handler/generic"
|
||||
"github.com/harness/gitness/registry/app/api/handler/oci"
|
||||
"github.com/harness/gitness/registry/app/api/router/utils"
|
||||
"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/pkg/docker"
|
||||
generic2 "github.com/harness/gitness/registry/app/pkg/generic"
|
||||
"github.com/harness/gitness/registry/app/store/database"
|
||||
"github.com/harness/gitness/registry/types"
|
||||
"github.com/harness/gitness/store"
|
||||
|
||||
|
@ -98,6 +104,99 @@ func TrackBandwidthStat(h *oci.Handler) func(http.Handler) http.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
func TrackBandwidthStatForGenericArtifacts(h *generic.Handler) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
methodType := r.Method
|
||||
|
||||
sw := &StatusWriter{ResponseWriter: w}
|
||||
|
||||
var bandwidthType types.BandwidthType
|
||||
//nolint:gocritic
|
||||
if http.MethodGet == methodType {
|
||||
next.ServeHTTP(sw, r)
|
||||
bandwidthType = types.BandwidthTypeDOWNLOAD
|
||||
} else if http.MethodPut == methodType {
|
||||
bandwidthType = types.BandwidthTypeUPLOAD
|
||||
next.ServeHTTP(sw, r)
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if types.BandwidthTypeUPLOAD == bandwidthType && sw.StatusCode != http.StatusCreated {
|
||||
return
|
||||
} else if types.BandwidthTypeDOWNLOAD == bandwidthType && sw.StatusCode != http.StatusOK &&
|
||||
sw.StatusCode != http.StatusTemporaryRedirect {
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
|
||||
info, err := h.GetArtifactInfo(r)
|
||||
if !commons.IsEmptyError(err) {
|
||||
log.Ctx(ctx).Error().Stack().Str("middleware",
|
||||
"TrackBandwidthStat").Err(err).Msgf("error while putting bandwidth stat for artifact, %v",
|
||||
err)
|
||||
return
|
||||
}
|
||||
|
||||
err = dbBandwidthStatForGenericArtifact(ctx, h.Controller, info, bandwidthType)
|
||||
if !commons.IsEmptyError(err) {
|
||||
log.Ctx(ctx).Error().Stack().Str("middleware",
|
||||
"TrackBandwidthStat").Err(err).Msgf("error while putting bandwidth stat for artifact [%s:%s], %v",
|
||||
info.RegIdentifier, info.Image, err)
|
||||
return
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func dbBandwidthStatForGenericArtifact(
|
||||
ctx context.Context,
|
||||
c *generic2.Controller,
|
||||
info pkg.GenericArtifactInfo,
|
||||
bandwidthType types.BandwidthType,
|
||||
) errcode.Error {
|
||||
registry, err := c.DBStore.RegistryDao.GetByParentIDAndName(ctx, info.ParentID, info.RegIdentifier)
|
||||
if err != nil {
|
||||
return errcode.ErrCodeInvalidRequest.WithDetail(err)
|
||||
}
|
||||
|
||||
image, err := c.DBStore.ImageDao.GetByName(ctx, registry.ID, info.Image)
|
||||
if err != nil {
|
||||
return errcode.ErrCodeInvalidRequest.WithDetail(err)
|
||||
}
|
||||
|
||||
art, err := c.DBStore.ArtifactDao.GetByName(ctx, image.ID, info.Version)
|
||||
if err != nil {
|
||||
return errcode.ErrCodeInvalidRequest.WithDetail(err)
|
||||
}
|
||||
|
||||
var metadata database.GenericMetadata
|
||||
err = json.Unmarshal(art.Metadata, &metadata)
|
||||
|
||||
if err != nil {
|
||||
return errcode.ErrCodeNameUnknown.WithDetail(err)
|
||||
}
|
||||
|
||||
var size int64
|
||||
for _, files := range metadata.Files {
|
||||
size += files.Size
|
||||
}
|
||||
bandwidthStat := &types.BandwidthStat{
|
||||
ImageID: image.ID,
|
||||
Type: bandwidthType,
|
||||
Bytes: size,
|
||||
}
|
||||
|
||||
if err := c.DBStore.BandwidthStatDao.Create(ctx, bandwidthStat); err != nil {
|
||||
return errcode.ErrCodeNameUnknown.WithDetail(err)
|
||||
}
|
||||
return errcode.Error{}
|
||||
}
|
||||
|
||||
func dbBandwidthStat(
|
||||
ctx context.Context,
|
||||
c *docker.Controller,
|
||||
|
|
|
@ -19,10 +19,14 @@ import (
|
|||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/registry/app/api/handler/generic"
|
||||
"github.com/harness/gitness/registry/app/api/handler/oci"
|
||||
"github.com/harness/gitness/registry/app/api/router/utils"
|
||||
"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/pkg/docker"
|
||||
generic2 "github.com/harness/gitness/registry/app/pkg/generic"
|
||||
"github.com/harness/gitness/registry/types"
|
||||
"github.com/harness/gitness/store"
|
||||
|
||||
|
@ -110,3 +114,72 @@ func dbDownloadStat(
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TrackDownloadStatForGenericArtifact(h *generic.Handler) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
methodType := r.Method
|
||||
ctx := r.Context()
|
||||
sw := &StatusWriter{ResponseWriter: w}
|
||||
|
||||
if http.MethodGet == methodType {
|
||||
next.ServeHTTP(sw, r)
|
||||
} else {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if sw.StatusCode != http.StatusOK && sw.StatusCode != http.StatusTemporaryRedirect {
|
||||
return
|
||||
}
|
||||
|
||||
info, err := h.GetArtifactInfo(r)
|
||||
if !commons.IsEmptyError(err) {
|
||||
log.Ctx(ctx).Error().Stack().Str("middleware",
|
||||
"TrackDownloadStat").Err(err).Msgf("error while putting download stat of artifact, %v",
|
||||
err)
|
||||
return
|
||||
}
|
||||
|
||||
err = dbDownloadStatForGenericArtifact(ctx, h.Controller, info)
|
||||
if !commons.IsEmptyError(err) {
|
||||
log.Ctx(ctx).Error().Stack().Str("middleware",
|
||||
"TrackDownloadStat").Err(err).Msgf("error while putting download stat of artifact, %v",
|
||||
err)
|
||||
return
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func dbDownloadStatForGenericArtifact(
|
||||
ctx context.Context,
|
||||
c *generic2.Controller,
|
||||
info pkg.GenericArtifactInfo,
|
||||
) errcode.Error {
|
||||
registry, err := c.DBStore.RegistryDao.GetByParentIDAndName(ctx, info.ParentID, info.RegIdentifier)
|
||||
if err != nil {
|
||||
return errcode.ErrCodeInvalidRequest.WithDetail(err)
|
||||
}
|
||||
|
||||
image, err := c.DBStore.ImageDao.GetByName(ctx, registry.ID, info.Image)
|
||||
if err != nil {
|
||||
return errcode.ErrCodeInvalidRequest.WithDetail(err)
|
||||
}
|
||||
|
||||
artifact, err := c.DBStore.ArtifactDao.GetByName(ctx, image.ID, info.Version)
|
||||
if err != nil {
|
||||
return errcode.ErrCodeInvalidRequest.WithDetail(err)
|
||||
}
|
||||
|
||||
downloadStat := &types.DownloadStat{
|
||||
ArtifactID: artifact.ID,
|
||||
}
|
||||
|
||||
if err := c.DBStore.DownloadStatDao.Create(ctx, downloadStat); err != nil {
|
||||
return errcode.ErrCodeNameUnknown.WithDetail(err)
|
||||
}
|
||||
return errcode.Error{}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2023 Harness, 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 generic
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
middlewareauthn "github.com/harness/gitness/app/api/middleware/authn"
|
||||
"github.com/harness/gitness/registry/app/api/handler/generic"
|
||||
"github.com/harness/gitness/registry/app/api/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
http.Handler
|
||||
}
|
||||
|
||||
func NewGenericArtifactHandler(handler *generic.Handler) Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
var routeHandlers = map[string]http.HandlerFunc{
|
||||
http.MethodPut: handler.PushArtifact,
|
||||
http.MethodGet: handler.PullArtifact,
|
||||
}
|
||||
r.Route("/generic", func(r chi.Router) {
|
||||
r.Use(middlewareauthn.Attempt(handler.Authenticator))
|
||||
r.Use(middleware.TrackDownloadStatForGenericArtifact(handler))
|
||||
r.Use(middleware.TrackBandwidthStatForGenericArtifacts(handler))
|
||||
|
||||
r.Handle("/*", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
methodType := req.Method
|
||||
|
||||
if h, ok := routeHandlers[methodType]; ok {
|
||||
h(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, err := w.Write([]byte("Invalid route"))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to write response")
|
||||
return
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
|
@ -21,8 +21,10 @@ import (
|
|||
corestore "github.com/harness/gitness/app/store"
|
||||
urlprovider "github.com/harness/gitness/app/url"
|
||||
"github.com/harness/gitness/audit"
|
||||
"github.com/harness/gitness/registry/app/api/handler/generic"
|
||||
"github.com/harness/gitness/registry/app/api/handler/maven"
|
||||
hoci "github.com/harness/gitness/registry/app/api/handler/oci"
|
||||
generic2 "github.com/harness/gitness/registry/app/api/router/generic"
|
||||
"github.com/harness/gitness/registry/app/api/router/harness"
|
||||
mavenRouter "github.com/harness/gitness/registry/app/api/router/maven"
|
||||
"github.com/harness/gitness/registry/app/api/router/oci"
|
||||
|
@ -91,4 +93,9 @@ func MavenHandlerProvider(handler *maven.Handler) mavenRouter.Handler {
|
|||
return mavenRouter.NewMavenHandler(handler)
|
||||
}
|
||||
|
||||
var WireSet = wire.NewSet(APIHandlerProvider, OCIHandlerProvider, AppRouterProvider, MavenHandlerProvider)
|
||||
func GenericHandlerProvider(handler *generic.Handler) generic2.Handler {
|
||||
return generic2.NewGenericArtifactHandler(handler)
|
||||
}
|
||||
|
||||
var WireSet = wire.NewSet(APIHandlerProvider, OCIHandlerProvider, AppRouterProvider,
|
||||
MavenHandlerProvider, GenericHandlerProvider)
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/harness/gitness/app/auth/authz"
|
||||
corestore "github.com/harness/gitness/app/store"
|
||||
urlprovider "github.com/harness/gitness/app/url"
|
||||
"github.com/harness/gitness/registry/app/api/handler/generic"
|
||||
mavenhandler "github.com/harness/gitness/registry/app/api/handler/maven"
|
||||
ocihandler "github.com/harness/gitness/registry/app/api/handler/oci"
|
||||
"github.com/harness/gitness/registry/app/api/router"
|
||||
|
@ -30,6 +31,7 @@ import (
|
|||
"github.com/harness/gitness/registry/app/pkg"
|
||||
"github.com/harness/gitness/registry/app/pkg/docker"
|
||||
"github.com/harness/gitness/registry/app/pkg/filemanager"
|
||||
generic2 "github.com/harness/gitness/registry/app/pkg/generic"
|
||||
"github.com/harness/gitness/registry/app/pkg/maven"
|
||||
"github.com/harness/gitness/registry/app/store/database"
|
||||
"github.com/harness/gitness/registry/config"
|
||||
|
@ -100,10 +102,27 @@ func NewMavenHandlerProvider(
|
|||
)
|
||||
}
|
||||
|
||||
func NewGenericHandlerProvider(
|
||||
spaceStore corestore.SpaceStore, controller *generic2.Controller, tokenStore corestore.TokenStore,
|
||||
userCtrl *usercontroller.Controller, authenticator authn.Authenticator, urlProvider urlprovider.Provider,
|
||||
authorizer authz.Authorizer,
|
||||
) *generic.Handler {
|
||||
return generic.NewGenericArtifactHandler(
|
||||
spaceStore,
|
||||
controller,
|
||||
tokenStore,
|
||||
userCtrl,
|
||||
authenticator,
|
||||
urlProvider,
|
||||
authorizer,
|
||||
)
|
||||
}
|
||||
|
||||
var WireSet = wire.NewSet(
|
||||
BlobStorageProvider,
|
||||
NewHandlerProvider,
|
||||
NewMavenHandlerProvider,
|
||||
NewGenericHandlerProvider,
|
||||
database.WireSet,
|
||||
pkg.WireSet,
|
||||
docker.WireSet,
|
||||
|
@ -111,6 +130,7 @@ var WireSet = wire.NewSet(
|
|||
maven.WireSet,
|
||||
router.WireSet,
|
||||
gc.WireSet,
|
||||
generic2.WireSet,
|
||||
)
|
||||
|
||||
func Wire(_ *types.Config) (RegistryApp, error) {
|
||||
|
|
|
@ -137,6 +137,16 @@ var (
|
|||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
)
|
||||
|
||||
// ErrCodeInvalidRequest provides an error when the request is invalid.
|
||||
ErrCodeInvalidRequest = register(
|
||||
"errcode", ErrorDescriptor{
|
||||
Value: "INVALID REQUEST",
|
||||
Message: "invalid request",
|
||||
Description: "Returned when the request is invalid",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
const errGroup = "registry.api.v2"
|
||||
|
|
|
@ -17,6 +17,8 @@ package commons
|
|||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/harness/gitness/registry/app/dist_temp/errcode"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -62,6 +64,10 @@ func IsEmpty(slice interface{}) bool {
|
|||
return val.Len() == 0
|
||||
}
|
||||
|
||||
func IsEmptyError(err errcode.Error) bool {
|
||||
return err.Code == 0
|
||||
}
|
||||
|
||||
func (r *ResponseHeaders) WriteToResponse(w http.ResponseWriter) {
|
||||
if w == nil || r == nil {
|
||||
return
|
||||
|
|
|
@ -198,29 +198,34 @@ func (f *FileManager) DownloadFile(
|
|||
filePath string,
|
||||
regInfo types.Registry,
|
||||
rootIdentifier string,
|
||||
) (fileReader *storage.FileReader, size int64, err error) {
|
||||
) (fileReader *storage.FileReader, size int64, redirectURL string, err error) {
|
||||
node, err := f.nodesDao.GetByPathAndRegistryID(ctx, regInfo.ID, filePath)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get the node for path: %s, "+
|
||||
return nil, 0, "", fmt.Errorf("failed to get the node for path: %s, "+
|
||||
"with registry: %s, with error %s", filePath, regInfo.Name, err)
|
||||
}
|
||||
blob, err := f.genericBlobDao.FindByID(ctx, node.BlobID)
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get the blob for path: %s, "+
|
||||
return nil, 0, "", fmt.Errorf("failed to get the blob for path: %s, "+
|
||||
"with blob id: %s, with error %s", filePath, blob.ID, err)
|
||||
}
|
||||
|
||||
completeFilaPath := path.Join(rootPathString + rootIdentifier + rootPathString + files + rootPathString + blob.Sha256)
|
||||
//
|
||||
blobContext := f.App.GetBlobsContext(ctx, regInfo.Name, rootIdentifier)
|
||||
reader, err := blobContext.genericBlobStore.Get(ctx, completeFilaPath, blob.Size)
|
||||
reader, redirectURL, err := blobContext.genericBlobStore.Get(ctx, completeFilaPath, blob.Size)
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get the file for path: %s, "+
|
||||
return nil, 0, "", fmt.Errorf("failed to get the file for path: %s, "+
|
||||
" with error %w", completeFilaPath, err)
|
||||
}
|
||||
return reader, blob.Size, nil
|
||||
|
||||
if redirectURL != "" {
|
||||
return reader, blob.Size, redirectURL, nil
|
||||
}
|
||||
|
||||
return reader, blob.Size, "", nil
|
||||
}
|
||||
|
||||
func (f *FileManager) DeleteFile(
|
||||
|
|
|
@ -16,12 +16,11 @@ package generic
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/app/auth/authz"
|
||||
|
@ -90,7 +89,7 @@ func NewDBStore(
|
|||
const regNameFormat = "registry : [%s]"
|
||||
|
||||
func (c Controller) UploadArtifact(ctx context.Context, info pkg.GenericArtifactInfo,
|
||||
file multipart.File) (*commons.ResponseHeaders, []error) {
|
||||
file multipart.File) (*commons.ResponseHeaders, errcode.Error) {
|
||||
responseHeaders := &commons.ResponseHeaders{
|
||||
Headers: make(map[string]string),
|
||||
Code: 0,
|
||||
|
@ -100,14 +99,14 @@ func (c Controller) UploadArtifact(ctx context.Context, info pkg.GenericArtifact
|
|||
enum.PermissionArtifactsUpload,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, []error{errcode.ErrCodeDenied, err}
|
||||
return nil, errcode.ErrCodeDenied.WithDetail(err)
|
||||
}
|
||||
|
||||
path := info.Image + "/" + info.Version + "/" + info.FileName
|
||||
fileInfo, err := c.fileManager.UploadFile(ctx, path, info.RegIdentifier, info.RegistryID,
|
||||
info.RootParentID, info.RootIdentifier, file, nil, info.FileName)
|
||||
if err != nil {
|
||||
return responseHeaders, []error{errcode.ErrCodeUnknown.WithDetail(err)}
|
||||
return responseHeaders, errcode.ErrCodeUnknown.WithDetail(err)
|
||||
}
|
||||
err = c.tx.WithTx(
|
||||
ctx, func(ctx context.Context) error {
|
||||
|
@ -124,7 +123,7 @@ func (c Controller) UploadArtifact(ctx context.Context, info pkg.GenericArtifact
|
|||
|
||||
dbArtifact, err := c.DBStore.ArtifactDao.GetByName(ctx, image.ID, info.Version)
|
||||
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
if err != nil && !strings.Contains(err.Error(), "resource not found") {
|
||||
return fmt.Errorf("failed to fetch artifact : [%s] with "+
|
||||
regNameFormat, info.Image, info.RegIdentifier)
|
||||
}
|
||||
|
@ -158,10 +157,10 @@ func (c Controller) UploadArtifact(ctx context.Context, info pkg.GenericArtifact
|
|||
})
|
||||
|
||||
if err != nil {
|
||||
return responseHeaders, []error{errcode.ErrCodeUnknown.WithDetail(err)}
|
||||
return responseHeaders, errcode.ErrCodeUnknown.WithDetail(err)
|
||||
}
|
||||
responseHeaders.Code = http.StatusCreated
|
||||
return responseHeaders, nil
|
||||
return responseHeaders, errcode.Error{}
|
||||
}
|
||||
|
||||
func (c Controller) updateMetadata(dbArtifact *types.Artifact, metadata *database.GenericMetadata,
|
||||
|
@ -194,9 +193,8 @@ func (c Controller) updateMetadata(dbArtifact *types.Artifact, metadata *databas
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c Controller) PullArtifact(ctx context.Context,
|
||||
info pkg.GenericArtifactInfo) (*commons.ResponseHeaders,
|
||||
*storage.FileReader, []error) {
|
||||
func (c Controller) PullArtifact(ctx context.Context, info pkg.GenericArtifactInfo) (*commons.ResponseHeaders,
|
||||
*storage.FileReader, string, errcode.Error) {
|
||||
responseHeaders := &commons.ResponseHeaders{
|
||||
Headers: make(map[string]string),
|
||||
Code: 0,
|
||||
|
@ -206,17 +204,17 @@ func (c Controller) PullArtifact(ctx context.Context,
|
|||
enum.PermissionArtifactsDownload,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, []error{errcode.ErrCodeDenied}
|
||||
return nil, nil, "", errcode.ErrCodeDenied.WithDetail(err)
|
||||
}
|
||||
|
||||
path := "/" + info.Image + "/" + info.Version + "/" + info.FileName
|
||||
fileReader, _, err := c.fileManager.DownloadFile(ctx, path, types.Registry{
|
||||
fileReader, _, redirectURL, err := c.fileManager.DownloadFile(ctx, path, types.Registry{
|
||||
ID: info.RegistryID,
|
||||
Name: info.RootIdentifier,
|
||||
}, info.RootIdentifier)
|
||||
if err != nil {
|
||||
return responseHeaders, nil, []error{errcode.ErrCodeUnknown.WithDetail(err)}
|
||||
return responseHeaders, nil, "", errcode.ErrCodeUnknown.WithDetail(err)
|
||||
}
|
||||
responseHeaders.Code = http.StatusOK
|
||||
return responseHeaders, fileReader, nil
|
||||
return responseHeaders, fileReader, redirectURL, errcode.Error{}
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ func (r *LocalRegistry) FetchArtifact(ctx context.Context, info pkg.MavenArtifac
|
|||
}
|
||||
var fileReader *storage.FileReader
|
||||
if serveFile {
|
||||
fileReader, _, err = r.fileManager.DownloadFile(ctx, filePath, types.Registry{
|
||||
fileReader, _, _, err = r.fileManager.DownloadFile(ctx, filePath, types.Registry{
|
||||
ID: info.RegistryID,
|
||||
Name: info.RootIdentifier,
|
||||
}, info.RootIdentifier)
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/registry/app/dist_temp/dcontext"
|
||||
"github.com/harness/gitness/registry/app/driver"
|
||||
|
@ -35,20 +36,33 @@ type genericBlobStore struct {
|
|||
repoKey string
|
||||
driver driver.StorageDriver
|
||||
rootParentRef string
|
||||
redirect bool
|
||||
}
|
||||
|
||||
func (bs *genericBlobStore) Info() string {
|
||||
return bs.rootParentRef + " " + bs.repoKey
|
||||
}
|
||||
|
||||
func (bs *genericBlobStore) Get(ctx context.Context, filePath string, size int64) (*FileReader, error) {
|
||||
func (bs *genericBlobStore) Get(ctx context.Context, filePath string, size int64) (*FileReader, string, error) {
|
||||
dcontext.GetLogger(ctx, log.Ctx(ctx).Debug()).Msg("(*genericBlobStore).Get")
|
||||
|
||||
if bs.redirect {
|
||||
redirectURL, err := bs.driver.RedirectURL(ctx, http.MethodGet, filePath)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if redirectURL != "" {
|
||||
// Redirect to storage URL.
|
||||
// http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
|
||||
return nil, redirectURL, nil
|
||||
}
|
||||
// Fallback to serving the content directly.
|
||||
}
|
||||
br, err := NewFileReader(ctx, bs.driver, filePath, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
return br, nil
|
||||
return br, "", nil
|
||||
}
|
||||
|
||||
var _ GenericBlobStore = &genericBlobStore{}
|
||||
|
|
|
@ -177,5 +177,5 @@ type GenericBlobStore interface {
|
|||
Move(ctx context.Context, srcPath string, dstPath string) error
|
||||
Delete(ctx context.Context, filePath string) error
|
||||
|
||||
Get(ctx context.Context, filePath string, size int64) (*FileReader, error)
|
||||
Get(ctx context.Context, filePath string, size int64) (*FileReader, string, error)
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ func (storage *Service) GenericBlobsStore(repoKey string, rootParentRef string)
|
|||
return &genericBlobStore{
|
||||
repoKey: repoKey,
|
||||
driver: storage.driver,
|
||||
redirect: storage.redirect,
|
||||
rootParentRef: rootParentRef,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue