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 changes
pull/3616/head
Sourabh Awashti 2025-01-27 07:26:26 +00:00 committed by Harness
parent 148b29fe7e
commit b501dbe44a
16 changed files with 664 additions and 27 deletions

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -16,13 +16,19 @@ package middleware
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"net/http" "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/handler/oci"
"github.com/harness/gitness/registry/app/api/router/utils" "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"
"github.com/harness/gitness/registry/app/pkg/commons"
"github.com/harness/gitness/registry/app/pkg/docker" "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/registry/types"
"github.com/harness/gitness/store" "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( func dbBandwidthStat(
ctx context.Context, ctx context.Context,
c *docker.Controller, c *docker.Controller,

View File

@ -19,10 +19,14 @@ import (
"errors" "errors"
"net/http" "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/handler/oci"
"github.com/harness/gitness/registry/app/api/router/utils" "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"
"github.com/harness/gitness/registry/app/pkg/commons"
"github.com/harness/gitness/registry/app/pkg/docker" "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/registry/types"
"github.com/harness/gitness/store" "github.com/harness/gitness/store"
@ -110,3 +114,72 @@ func dbDownloadStat(
} }
return nil 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{}
}

View File

@ -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
}

View File

@ -21,8 +21,10 @@ import (
corestore "github.com/harness/gitness/app/store" corestore "github.com/harness/gitness/app/store"
urlprovider "github.com/harness/gitness/app/url" urlprovider "github.com/harness/gitness/app/url"
"github.com/harness/gitness/audit" "github.com/harness/gitness/audit"
"github.com/harness/gitness/registry/app/api/handler/generic"
"github.com/harness/gitness/registry/app/api/handler/maven" "github.com/harness/gitness/registry/app/api/handler/maven"
hoci "github.com/harness/gitness/registry/app/api/handler/oci" 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" "github.com/harness/gitness/registry/app/api/router/harness"
mavenRouter "github.com/harness/gitness/registry/app/api/router/maven" mavenRouter "github.com/harness/gitness/registry/app/api/router/maven"
"github.com/harness/gitness/registry/app/api/router/oci" "github.com/harness/gitness/registry/app/api/router/oci"
@ -91,4 +93,9 @@ func MavenHandlerProvider(handler *maven.Handler) mavenRouter.Handler {
return mavenRouter.NewMavenHandler(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)

View File

@ -20,6 +20,7 @@ import (
"github.com/harness/gitness/app/auth/authz" "github.com/harness/gitness/app/auth/authz"
corestore "github.com/harness/gitness/app/store" corestore "github.com/harness/gitness/app/store"
urlprovider "github.com/harness/gitness/app/url" 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" mavenhandler "github.com/harness/gitness/registry/app/api/handler/maven"
ocihandler "github.com/harness/gitness/registry/app/api/handler/oci" ocihandler "github.com/harness/gitness/registry/app/api/handler/oci"
"github.com/harness/gitness/registry/app/api/router" "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"
"github.com/harness/gitness/registry/app/pkg/docker" "github.com/harness/gitness/registry/app/pkg/docker"
"github.com/harness/gitness/registry/app/pkg/filemanager" "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/pkg/maven"
"github.com/harness/gitness/registry/app/store/database" "github.com/harness/gitness/registry/app/store/database"
"github.com/harness/gitness/registry/config" "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( var WireSet = wire.NewSet(
BlobStorageProvider, BlobStorageProvider,
NewHandlerProvider, NewHandlerProvider,
NewMavenHandlerProvider, NewMavenHandlerProvider,
NewGenericHandlerProvider,
database.WireSet, database.WireSet,
pkg.WireSet, pkg.WireSet,
docker.WireSet, docker.WireSet,
@ -111,6 +130,7 @@ var WireSet = wire.NewSet(
maven.WireSet, maven.WireSet,
router.WireSet, router.WireSet,
gc.WireSet, gc.WireSet,
generic2.WireSet,
) )
func Wire(_ *types.Config) (RegistryApp, error) { func Wire(_ *types.Config) (RegistryApp, error) {

View File

@ -137,6 +137,16 @@ var (
HTTPStatusCode: http.StatusBadRequest, 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" const errGroup = "registry.api.v2"

View File

@ -17,6 +17,8 @@ package commons
import ( import (
"net/http" "net/http"
"reflect" "reflect"
"github.com/harness/gitness/registry/app/dist_temp/errcode"
) )
const ( const (
@ -62,6 +64,10 @@ func IsEmpty(slice interface{}) bool {
return val.Len() == 0 return val.Len() == 0
} }
func IsEmptyError(err errcode.Error) bool {
return err.Code == 0
}
func (r *ResponseHeaders) WriteToResponse(w http.ResponseWriter) { func (r *ResponseHeaders) WriteToResponse(w http.ResponseWriter) {
if w == nil || r == nil { if w == nil || r == nil {
return return

View File

@ -198,29 +198,34 @@ func (f *FileManager) DownloadFile(
filePath string, filePath string,
regInfo types.Registry, regInfo types.Registry,
rootIdentifier string, 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) node, err := f.nodesDao.GetByPathAndRegistryID(ctx, regInfo.ID, filePath)
if err != nil { 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) "with registry: %s, with error %s", filePath, regInfo.Name, err)
} }
blob, err := f.genericBlobDao.FindByID(ctx, node.BlobID) blob, err := f.genericBlobDao.FindByID(ctx, node.BlobID)
if err != nil { 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) "with blob id: %s, with error %s", filePath, blob.ID, err)
} }
completeFilaPath := path.Join(rootPathString + rootIdentifier + rootPathString + files + rootPathString + blob.Sha256) completeFilaPath := path.Join(rootPathString + rootIdentifier + rootPathString + files + rootPathString + blob.Sha256)
// //
blobContext := f.App.GetBlobsContext(ctx, regInfo.Name, rootIdentifier) 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 { 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) " 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( func (f *FileManager) DeleteFile(

View File

@ -16,12 +16,11 @@ package generic
import ( import (
"context" "context"
"database/sql"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/harness/gitness/app/auth/authz" "github.com/harness/gitness/app/auth/authz"
@ -90,7 +89,7 @@ func NewDBStore(
const regNameFormat = "registry : [%s]" const regNameFormat = "registry : [%s]"
func (c Controller) UploadArtifact(ctx context.Context, info pkg.GenericArtifactInfo, 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{ responseHeaders := &commons.ResponseHeaders{
Headers: make(map[string]string), Headers: make(map[string]string),
Code: 0, Code: 0,
@ -100,14 +99,14 @@ func (c Controller) UploadArtifact(ctx context.Context, info pkg.GenericArtifact
enum.PermissionArtifactsUpload, enum.PermissionArtifactsUpload,
) )
if err != nil { if err != nil {
return nil, []error{errcode.ErrCodeDenied, err} return nil, errcode.ErrCodeDenied.WithDetail(err)
} }
path := info.Image + "/" + info.Version + "/" + info.FileName path := info.Image + "/" + info.Version + "/" + info.FileName
fileInfo, err := c.fileManager.UploadFile(ctx, path, info.RegIdentifier, info.RegistryID, fileInfo, err := c.fileManager.UploadFile(ctx, path, info.RegIdentifier, info.RegistryID,
info.RootParentID, info.RootIdentifier, file, nil, info.FileName) info.RootParentID, info.RootIdentifier, file, nil, info.FileName)
if err != nil { if err != nil {
return responseHeaders, []error{errcode.ErrCodeUnknown.WithDetail(err)} return responseHeaders, errcode.ErrCodeUnknown.WithDetail(err)
} }
err = c.tx.WithTx( err = c.tx.WithTx(
ctx, func(ctx context.Context) error { 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) 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 "+ return fmt.Errorf("failed to fetch artifact : [%s] with "+
regNameFormat, info.Image, info.RegIdentifier) regNameFormat, info.Image, info.RegIdentifier)
} }
@ -158,10 +157,10 @@ func (c Controller) UploadArtifact(ctx context.Context, info pkg.GenericArtifact
}) })
if err != nil { if err != nil {
return responseHeaders, []error{errcode.ErrCodeUnknown.WithDetail(err)} return responseHeaders, errcode.ErrCodeUnknown.WithDetail(err)
} }
responseHeaders.Code = http.StatusCreated responseHeaders.Code = http.StatusCreated
return responseHeaders, nil return responseHeaders, errcode.Error{}
} }
func (c Controller) updateMetadata(dbArtifact *types.Artifact, metadata *database.GenericMetadata, 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 return nil
} }
func (c Controller) PullArtifact(ctx context.Context, func (c Controller) PullArtifact(ctx context.Context, info pkg.GenericArtifactInfo) (*commons.ResponseHeaders,
info pkg.GenericArtifactInfo) (*commons.ResponseHeaders, *storage.FileReader, string, errcode.Error) {
*storage.FileReader, []error) {
responseHeaders := &commons.ResponseHeaders{ responseHeaders := &commons.ResponseHeaders{
Headers: make(map[string]string), Headers: make(map[string]string),
Code: 0, Code: 0,
@ -206,17 +204,17 @@ func (c Controller) PullArtifact(ctx context.Context,
enum.PermissionArtifactsDownload, enum.PermissionArtifactsDownload,
) )
if err != nil { if err != nil {
return nil, nil, []error{errcode.ErrCodeDenied} return nil, nil, "", errcode.ErrCodeDenied.WithDetail(err)
} }
path := "/" + info.Image + "/" + info.Version + "/" + info.FileName 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, ID: info.RegistryID,
Name: info.RootIdentifier, Name: info.RootIdentifier,
}, info.RootIdentifier) }, info.RootIdentifier)
if err != nil { if err != nil {
return responseHeaders, nil, []error{errcode.ErrCodeUnknown.WithDetail(err)} return responseHeaders, nil, "", errcode.ErrCodeUnknown.WithDetail(err)
} }
responseHeaders.Code = http.StatusOK responseHeaders.Code = http.StatusOK
return responseHeaders, fileReader, nil return responseHeaders, fileReader, redirectURL, errcode.Error{}
} }

View File

@ -94,7 +94,7 @@ func (r *LocalRegistry) FetchArtifact(ctx context.Context, info pkg.MavenArtifac
} }
var fileReader *storage.FileReader var fileReader *storage.FileReader
if serveFile { if serveFile {
fileReader, _, err = r.fileManager.DownloadFile(ctx, filePath, types.Registry{ fileReader, _, _, err = r.fileManager.DownloadFile(ctx, filePath, types.Registry{
ID: info.RegistryID, ID: info.RegistryID,
Name: info.RootIdentifier, Name: info.RootIdentifier,
}, info.RootIdentifier) }, info.RootIdentifier)

View File

@ -23,6 +23,7 @@ import (
"fmt" "fmt"
"io" "io"
"mime/multipart" "mime/multipart"
"net/http"
"github.com/harness/gitness/registry/app/dist_temp/dcontext" "github.com/harness/gitness/registry/app/dist_temp/dcontext"
"github.com/harness/gitness/registry/app/driver" "github.com/harness/gitness/registry/app/driver"
@ -35,20 +36,33 @@ type genericBlobStore struct {
repoKey string repoKey string
driver driver.StorageDriver driver driver.StorageDriver
rootParentRef string rootParentRef string
redirect bool
} }
func (bs *genericBlobStore) Info() string { func (bs *genericBlobStore) Info() string {
return bs.rootParentRef + " " + bs.repoKey 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") 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) br, err := NewFileReader(ctx, bs.driver, filePath, size)
if err != nil { if err != nil {
return nil, err return nil, "", err
} }
return br, nil return br, "", nil
} }
var _ GenericBlobStore = &genericBlobStore{} var _ GenericBlobStore = &genericBlobStore{}

View File

@ -177,5 +177,5 @@ type GenericBlobStore interface {
Move(ctx context.Context, srcPath string, dstPath string) error Move(ctx context.Context, srcPath string, dstPath string) error
Delete(ctx context.Context, filePath 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)
} }

View File

@ -80,6 +80,7 @@ func (storage *Service) GenericBlobsStore(repoKey string, rootParentRef string)
return &genericBlobStore{ return &genericBlobStore{
repoKey: repoKey, repoKey: repoKey,
driver: storage.driver, driver: storage.driver,
redirect: storage.redirect,
rootParentRef: rootParentRef, rootParentRef: rootParentRef,
} }
} }