From b501dbe44aa212258ddef80d4ac24eecd341c38c Mon Sep 17 00:00:00 2001 From: Sourabh Awashti Date: Mon, 27 Jan 2025 07:26:26 +0000 Subject: [PATCH] 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 --- registry/app/api/handler/generic/base.go | 242 ++++++++++++++++++ .../app/api/handler/generic/pull_artifact.go | 54 ++++ .../app/api/handler/generic/push_artifact.go | 46 ++++ .../app/api/middleware/bandwidth_stats.go | 99 +++++++ registry/app/api/middleware/download_stats.go | 73 ++++++ registry/app/api/router/generic/route.go | 62 +++++ registry/app/api/router/wire.go | 9 +- registry/app/api/wire.go | 20 ++ registry/app/dist_temp/errcode/register.go | 10 + registry/app/pkg/commons/request.go | 6 + registry/app/pkg/filemanager/file_manager.go | 17 +- registry/app/pkg/generic/controller.go | 28 +- registry/app/pkg/maven/local.go | 2 +- registry/app/storage/blobStore.go | 20 +- registry/app/storage/blobs.go | 2 +- registry/app/storage/storageservice.go | 1 + 16 files changed, 664 insertions(+), 27 deletions(-) create mode 100644 registry/app/api/handler/generic/base.go create mode 100644 registry/app/api/handler/generic/pull_artifact.go create mode 100644 registry/app/api/handler/generic/push_artifact.go create mode 100644 registry/app/api/router/generic/route.go diff --git a/registry/app/api/handler/generic/base.go b/registry/app/api/handler/generic/base.go new file mode 100644 index 000000000..95ffb405e --- /dev/null +++ b/registry/app/api/handler/generic/base.go @@ -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 +} diff --git a/registry/app/api/handler/generic/pull_artifact.go b/registry/app/api/handler/generic/pull_artifact.go new file mode 100644 index 000000000..e6fba47af --- /dev/null +++ b/registry/app/api/handler/generic/pull_artifact.go @@ -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) + } +} diff --git a/registry/app/api/handler/generic/push_artifact.go b/registry/app/api/handler/generic/push_artifact.go new file mode 100644 index 000000000..71a333923 --- /dev/null +++ b/registry/app/api/handler/generic/push_artifact.go @@ -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) +} diff --git a/registry/app/api/middleware/bandwidth_stats.go b/registry/app/api/middleware/bandwidth_stats.go index 2b1d32cd4..250055654 100644 --- a/registry/app/api/middleware/bandwidth_stats.go +++ b/registry/app/api/middleware/bandwidth_stats.go @@ -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, diff --git a/registry/app/api/middleware/download_stats.go b/registry/app/api/middleware/download_stats.go index e6243df3c..229f44c3e 100644 --- a/registry/app/api/middleware/download_stats.go +++ b/registry/app/api/middleware/download_stats.go @@ -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{} +} diff --git a/registry/app/api/router/generic/route.go b/registry/app/api/router/generic/route.go new file mode 100644 index 000000000..21dcebcd5 --- /dev/null +++ b/registry/app/api/router/generic/route.go @@ -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 +} diff --git a/registry/app/api/router/wire.go b/registry/app/api/router/wire.go index c97fc0b91..dd6562e2b 100644 --- a/registry/app/api/router/wire.go +++ b/registry/app/api/router/wire.go @@ -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) diff --git a/registry/app/api/wire.go b/registry/app/api/wire.go index dfb8afd9d..e4d0c4b4b 100644 --- a/registry/app/api/wire.go +++ b/registry/app/api/wire.go @@ -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) { diff --git a/registry/app/dist_temp/errcode/register.go b/registry/app/dist_temp/errcode/register.go index 15e188d93..d00e6af89 100644 --- a/registry/app/dist_temp/errcode/register.go +++ b/registry/app/dist_temp/errcode/register.go @@ -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" diff --git a/registry/app/pkg/commons/request.go b/registry/app/pkg/commons/request.go index 7759934df..13bfeb578 100644 --- a/registry/app/pkg/commons/request.go +++ b/registry/app/pkg/commons/request.go @@ -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 diff --git a/registry/app/pkg/filemanager/file_manager.go b/registry/app/pkg/filemanager/file_manager.go index c8c1e6868..a0bbe4dc8 100644 --- a/registry/app/pkg/filemanager/file_manager.go +++ b/registry/app/pkg/filemanager/file_manager.go @@ -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( diff --git a/registry/app/pkg/generic/controller.go b/registry/app/pkg/generic/controller.go index aa0981a7d..9ef5c85da 100644 --- a/registry/app/pkg/generic/controller.go +++ b/registry/app/pkg/generic/controller.go @@ -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{} } diff --git a/registry/app/pkg/maven/local.go b/registry/app/pkg/maven/local.go index 6e6d0cd5c..e7c318b1b 100644 --- a/registry/app/pkg/maven/local.go +++ b/registry/app/pkg/maven/local.go @@ -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) diff --git a/registry/app/storage/blobStore.go b/registry/app/storage/blobStore.go index 42ab30b16..5a4a24009 100644 --- a/registry/app/storage/blobStore.go +++ b/registry/app/storage/blobStore.go @@ -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{} diff --git a/registry/app/storage/blobs.go b/registry/app/storage/blobs.go index 88485c94d..b17197531 100644 --- a/registry/app/storage/blobs.go +++ b/registry/app/storage/blobs.go @@ -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) } diff --git a/registry/app/storage/storageservice.go b/registry/app/storage/storageservice.go index 366e9ef27..a0233225c 100644 --- a/registry/app/storage/storageservice.go +++ b/registry/app/storage/storageservice.go @@ -80,6 +80,7 @@ func (storage *Service) GenericBlobsStore(repoKey string, rootParentRef string) return &genericBlobStore{ repoKey: repoKey, driver: storage.driver, + redirect: storage.redirect, rootParentRef: rootParentRef, } }