mirror of
https://github.com/harness/drone.git
synced 2025-05-05 15:32:56 +00:00
feat:[AH-1093]: NPM search package support (#3711)
* feat:[AH-1093]: NPM search package support * Merge branch 'main' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness * Merge branch 'main' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness * Merge branch 'main' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness * Merge branch 'release/registry-api_1.18.0' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness * feat:[AH-1083]: url fix (#3669) * feat:[AH-1083]: url fix
This commit is contained in:
parent
3e07a681a1
commit
ca888e532b
@ -80,6 +80,12 @@ type Controller interface {
|
||||
ctx context.Context,
|
||||
info npm.ArtifactInfo,
|
||||
) *DeleteEntityResponse
|
||||
|
||||
SearchPackage(
|
||||
ctx context.Context,
|
||||
info npm.ArtifactInfo,
|
||||
limit int, offset int,
|
||||
) *SearchArtifactResponse
|
||||
}
|
||||
|
||||
// NewController creates a new PyPI controller.
|
||||
|
@ -81,3 +81,12 @@ type DeleteEntityResponse struct {
|
||||
func (r *DeleteEntityResponse) GetError() error {
|
||||
return r.Error
|
||||
}
|
||||
|
||||
type SearchArtifactResponse struct {
|
||||
BaseResponse
|
||||
Artifacts *npm2.PackageSearch
|
||||
}
|
||||
|
||||
func (r *SearchArtifactResponse) GetError() error {
|
||||
return r.Error
|
||||
}
|
||||
|
66
registry/app/api/controller/pkg/npm/search_package.go
Normal file
66
registry/app/api/controller/pkg/npm/search_package.go
Normal file
@ -0,0 +1,66 @@
|
||||
// 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 npm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/registry/app/pkg"
|
||||
"github.com/harness/gitness/registry/app/pkg/base"
|
||||
"github.com/harness/gitness/registry/app/pkg/commons"
|
||||
npm2 "github.com/harness/gitness/registry/app/pkg/npm"
|
||||
"github.com/harness/gitness/registry/app/pkg/response"
|
||||
"github.com/harness/gitness/registry/app/pkg/types/npm"
|
||||
"github.com/harness/gitness/registry/types"
|
||||
)
|
||||
|
||||
func (c *controller) SearchPackage(
|
||||
ctx context.Context,
|
||||
info npm.ArtifactInfo,
|
||||
limit int, offset int,
|
||||
) *SearchArtifactResponse {
|
||||
f := func(registry types.Registry, a pkg.Artifact) response.Response {
|
||||
info.RegIdentifier = registry.Name
|
||||
info.RegistryID = registry.ID
|
||||
info.ParentID = registry.ParentID
|
||||
info.Registry = registry
|
||||
npmRegistry, ok := a.(npm2.Registry)
|
||||
if !ok {
|
||||
return &GetMetadataResponse{
|
||||
BaseResponse: BaseResponse{Error: fmt.Errorf("invalid registry type: expected npm.Registry")},
|
||||
}
|
||||
}
|
||||
artifacts, err := npmRegistry.SearchPackage(ctx, info, limit, offset)
|
||||
return &SearchArtifactResponse{
|
||||
BaseResponse: BaseResponse{Error: err},
|
||||
Artifacts: artifacts,
|
||||
}
|
||||
}
|
||||
|
||||
result, err := base.NoProxyWrapper(ctx, c.registryDao, f, info)
|
||||
if !commons.IsEmpty(err) {
|
||||
return &SearchArtifactResponse{
|
||||
BaseResponse: BaseResponse{Error: err},
|
||||
}
|
||||
}
|
||||
metadataResponse, ok := result.(*SearchArtifactResponse)
|
||||
if !ok {
|
||||
return &SearchArtifactResponse{
|
||||
BaseResponse: BaseResponse{Error: fmt.Errorf("invalid response type: expected GetMetadataResponse, got %T", result)},
|
||||
}
|
||||
}
|
||||
return metadataResponse
|
||||
}
|
@ -56,6 +56,7 @@ type Handler interface {
|
||||
DeletePackage(w http.ResponseWriter, r *http.Request)
|
||||
DeleteVersion(w http.ResponseWriter, r *http.Request)
|
||||
DeletePreview(w http.ResponseWriter, r *http.Request)
|
||||
SearchPackage(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
|
66
registry/app/api/handler/npm/search.go
Normal file
66
registry/app/api/handler/npm/search.go
Normal file
@ -0,0 +1,66 @@
|
||||
// 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 npm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
errors2 "github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/registry/app/pkg/commons"
|
||||
npm2 "github.com/harness/gitness/registry/app/pkg/types/npm"
|
||||
"github.com/harness/gitness/registry/request"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (h *handler) SearchPackage(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
info, ok := request.ArtifactInfoFrom(ctx).(npm2.ArtifactInfo)
|
||||
if !ok {
|
||||
log.Ctx(ctx).Error().Msg("Failed to get npm artifact info from context")
|
||||
h.HandleErrors(r.Context(), []error{fmt.Errorf("failed to fetch npm artifact info from context")}, w)
|
||||
return
|
||||
}
|
||||
|
||||
info.Image = r.FormValue("text")
|
||||
limit, err := strconv.Atoi(r.FormValue("size"))
|
||||
if err != nil {
|
||||
limit = 20
|
||||
}
|
||||
offset, err := strconv.Atoi(r.FormValue("from"))
|
||||
if err != nil {
|
||||
offset = 0
|
||||
}
|
||||
response := h.controller.SearchPackage(ctx, info, limit, offset)
|
||||
|
||||
if !commons.IsEmpty(response.GetError()) {
|
||||
if errors2.IsNotFound(response.GetError()) {
|
||||
http.Error(w, response.GetError().Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, response.GetError().Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(response.Artifacts)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
@ -197,6 +197,12 @@ func NewRouter(
|
||||
r.Route("/{id}/-rev/{revision}", func(r chi.Router) {
|
||||
registerRevisionRoutes(r, npmHandler, packageHandler)
|
||||
})
|
||||
|
||||
r.Route("/-/v1/search", func(r chi.Router) {
|
||||
r.With(middleware.StoreArtifactInfo(npmHandler)).
|
||||
With(middleware.RequestPackageAccess(packageHandler, enum.PermissionArtifactsDownload)).
|
||||
Get("/", npmHandler.SearchPackage)
|
||||
})
|
||||
})
|
||||
r.Route("/rpm", func(r chi.Router) {
|
||||
r.Use(middlewareauthn.Attempt(packageHandler.GetAuthenticator()))
|
||||
|
@ -110,12 +110,12 @@ type PackageSearchPackage struct {
|
||||
Scope string `json:"scope"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Date time.Time `json:"date"`
|
||||
Description string `json:"description"`
|
||||
Author User `json:"author"`
|
||||
Publisher User `json:"publisher"`
|
||||
Maintainers []User `json:"maintainers"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Date interface{} `json:"date"`
|
||||
Description interface{} `json:"description"`
|
||||
Author interface{} `json:"author"`
|
||||
Publisher interface{} `json:"publisher"`
|
||||
Maintainers interface{} `json:"maintainers"`
|
||||
Keywords interface{} `json:"keywords,omitempty"`
|
||||
Links *PackageSearchPackageLinks `json:"links"`
|
||||
}
|
||||
|
||||
@ -132,7 +132,7 @@ type User struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// PythonMetadata represents the metadata for a Python package.
|
||||
// NpmMetadata represents the metadata for a Python package.
|
||||
//
|
||||
//nolint:revive
|
||||
type NpmMetadata struct {
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/app/api/usererror"
|
||||
urlprovider "github.com/harness/gitness/app/url"
|
||||
@ -165,6 +166,98 @@ func (c *localRegistry) GetPackageMetadata(ctx context.Context, info npm.Artifac
|
||||
return packageMetadata, nil
|
||||
}
|
||||
|
||||
func (c *localRegistry) SearchPackage(ctx context.Context, info npm.ArtifactInfo,
|
||||
limit int, offset int) (*npm2.PackageSearch, error) {
|
||||
metadataList, err := c.artifactDao.SearchLatestByName(ctx, info.RegistryID, info.Image, limit, offset)
|
||||
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Failed to search package for search term: [%s]", info.Image)
|
||||
return &npm2.PackageSearch{}, err
|
||||
}
|
||||
count, err := c.artifactDao.CountLatestByName(ctx, info.RegistryID, info.Image)
|
||||
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Failed to search package for search term: [%s]", info.Image)
|
||||
return &npm2.PackageSearch{}, err
|
||||
}
|
||||
psList := make([]*npm2.PackageSearchObject, 0)
|
||||
registryURL := c.urlProvider.PackageURL(ctx,
|
||||
info.BaseArtifactInfo().RootIdentifier+"/"+info.BaseArtifactInfo().RegIdentifier, "npm")
|
||||
|
||||
for _, metadata := range *metadataList {
|
||||
pso, err := mapToPackageSearch(metadata, registryURL)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Failed to map search package results: [%s]", info.Image)
|
||||
return &npm2.PackageSearch{}, err
|
||||
}
|
||||
psList = append(psList, pso)
|
||||
}
|
||||
return &npm2.PackageSearch{
|
||||
Objects: psList,
|
||||
Total: count,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapToPackageSearch(metadata types.Artifact, registryURL string) (*npm2.PackageSearchObject, error) {
|
||||
var art *npm2.NpmMetadata
|
||||
if err := json.Unmarshal(metadata.Metadata, &art); err != nil {
|
||||
return &npm2.PackageSearchObject{}, err
|
||||
}
|
||||
|
||||
for _, version := range art.Versions {
|
||||
var author npm2.User
|
||||
if version.Author != nil {
|
||||
data, err := json.Marshal(version.Author)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Failed to marshal search package results: [%s]", art.Name)
|
||||
return &npm2.PackageSearchObject{}, err
|
||||
}
|
||||
err = json.Unmarshal(data, &author)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Failed to unmarshal search package results: [%s]", art.Name)
|
||||
return &npm2.PackageSearchObject{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return &npm2.PackageSearchObject{
|
||||
Package: &npm2.PackageSearchPackage{
|
||||
Name: version.Name,
|
||||
Version: version.Version,
|
||||
Description: version.Description,
|
||||
Date: metadata.CreatedAt,
|
||||
|
||||
Scope: getScope(art.Name),
|
||||
Author: npm2.User{Username: author.Name},
|
||||
Publisher: npm2.User{Username: author.Name},
|
||||
Maintainers: getValueOrDefault(version.Maintainers, []npm2.User{}), // npm cli needs this field
|
||||
Keywords: getValueOrDefault(version.Keywords, []string{}),
|
||||
Links: &npm2.PackageSearchPackageLinks{
|
||||
Registry: registryURL,
|
||||
Homepage: registryURL,
|
||||
Repository: registryURL,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return &npm2.PackageSearchObject{}, fmt.Errorf("no version found in the metadata for image:[%s]", art.Name)
|
||||
}
|
||||
|
||||
func getValueOrDefault(value interface{}, defaultValue interface{}) interface{} {
|
||||
if value != nil {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getScope(name string) string {
|
||||
if strings.HasPrefix(name, "@") {
|
||||
if i := strings.Index(name, "/"); i != -1 {
|
||||
return name[1:i] // Strip @ and return only the scope
|
||||
}
|
||||
}
|
||||
return "unscoped"
|
||||
}
|
||||
|
||||
func CreatePackageMetadataVersion(registryURL string,
|
||||
metadata *npm2.PackageMetadataVersion) *npm2.PackageMetadataVersion {
|
||||
return &npm2.PackageMetadataVersion{
|
||||
|
@ -53,6 +53,10 @@ type proxy struct {
|
||||
localRegistryHelper LocalRegistryHelper
|
||||
}
|
||||
|
||||
func (r *proxy) SearchPackage(_ context.Context, _ npm2.ArtifactInfo, _ int, _ int) (*npm.PackageSearch, error) {
|
||||
return nil, commons.ErrNotSupported
|
||||
}
|
||||
|
||||
func (r *proxy) UploadPackageFileReader(_ context.Context,
|
||||
_ npm2.ArtifactInfo) (*commons.ResponseHeaders, string, error) {
|
||||
return nil, " ", commons.ErrNotSupported
|
||||
|
@ -53,4 +53,6 @@ type Registry interface {
|
||||
|
||||
DeletePackage(ctx context.Context, info npm.ArtifactInfo) error
|
||||
DeleteVersion(ctx context.Context, info npm.ArtifactInfo) error
|
||||
|
||||
SearchPackage(ctx context.Context, info npm.ArtifactInfo, limit int, offset int) (*npm3.PackageSearch, error)
|
||||
}
|
||||
|
@ -499,6 +499,14 @@ type ArtifactRepository interface {
|
||||
GetAllArtifactsByRepo(
|
||||
ctx context.Context, registryID int64, batchSize int, artifactID int64,
|
||||
) (*[]types.ArtifactMetadata, error)
|
||||
|
||||
SearchLatestByName(
|
||||
ctx context.Context, regID int64, name string, limit int, offset int,
|
||||
) (*[]types.Artifact, error)
|
||||
|
||||
CountLatestByName(
|
||||
ctx context.Context, regID int64, name string,
|
||||
) (int64, error)
|
||||
}
|
||||
|
||||
type DownloadStatRepository interface {
|
||||
|
@ -292,6 +292,80 @@ func (a ArtifactDao) mapToArtifact(_ context.Context, dst *artifactDB) (*types.A
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a ArtifactDao) SearchLatestByName(
|
||||
ctx context.Context, regID int64, name string, limit int, offset int,
|
||||
) (*[]types.Artifact, error) {
|
||||
subQuery := `
|
||||
SELECT artifact_image_id, MAX(artifact_created_at) AS max_created_at
|
||||
FROM artifacts
|
||||
GROUP BY artifact_image_id`
|
||||
|
||||
q := databaseg.Builder.
|
||||
Select("a.artifact_metadata,"+
|
||||
"a.artifact_created_at").
|
||||
From("artifacts a").
|
||||
Join("images i ON a.artifact_image_id = i.image_id").
|
||||
Join(fmt.Sprintf(`(%s) latest
|
||||
ON a.artifact_image_id = latest.artifact_image_id
|
||||
AND a.artifact_created_at = latest.max_created_at
|
||||
`, subQuery)).
|
||||
Where("i.image_name LIKE ? AND i.image_registry_id = ?", "%"+name+"%", regID).
|
||||
Limit(util.SafeIntToUInt64(limit)).
|
||||
Offset(util.SafeIntToUInt64(offset))
|
||||
|
||||
sql, args, err := q.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to build SQL for latest artifact metadata with pagination")
|
||||
}
|
||||
db := dbtx.GetAccessor(ctx, a.db)
|
||||
|
||||
var metadataList []*artifactDB
|
||||
if err := db.SelectContext(ctx, &metadataList, sql, args...); err != nil {
|
||||
return nil, databaseg.ProcessSQLErrorf(ctx, err, "Failed to get artifact metadata")
|
||||
}
|
||||
|
||||
artifactList, err := a.mapArtifactToArtifactMetadataList(ctx, metadataList)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to map artifact metadata")
|
||||
}
|
||||
|
||||
return artifactList, nil
|
||||
}
|
||||
|
||||
func (a ArtifactDao) CountLatestByName(
|
||||
ctx context.Context, regID int64, name string,
|
||||
) (int64, error) {
|
||||
subQuery := `
|
||||
SELECT artifact_image_id, MAX(artifact_created_at) AS max_created_at
|
||||
FROM artifacts
|
||||
GROUP BY artifact_image_id`
|
||||
|
||||
// Main count query
|
||||
q := databaseg.Builder.
|
||||
Select("COUNT(*)").
|
||||
From("artifacts a").
|
||||
Join("images i ON a.artifact_image_id = i.image_id").
|
||||
Join(fmt.Sprintf(`(%s) latest
|
||||
ON a.artifact_image_id = latest.artifact_image_id
|
||||
AND a.artifact_created_at = latest.max_created_at
|
||||
`, subQuery)).
|
||||
Where("i.image_name LIKE ? AND i.image_registry_id = ?", "%"+name+"%", regID)
|
||||
|
||||
sql, args, err := q.ToSql()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "Failed to build count SQL")
|
||||
}
|
||||
|
||||
db := dbtx.GetAccessor(ctx, a.db)
|
||||
|
||||
var count int64
|
||||
if err := db.GetContext(ctx, &count, sql, args...); err != nil {
|
||||
return 0, databaseg.ProcessSQLErrorf(ctx, err, "Failed to count artifact metadata")
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (a ArtifactDao) GetAllArtifactsByParentID(
|
||||
ctx context.Context,
|
||||
parentID int64,
|
||||
@ -610,6 +684,20 @@ func (a ArtifactDao) GetLatestArtifactMetadata(
|
||||
return a.mapToArtifactMetadata(dst)
|
||||
}
|
||||
|
||||
func (a ArtifactDao) mapArtifactToArtifactMetadataList(ctx context.Context,
|
||||
dst []*artifactDB,
|
||||
) (*[]types.Artifact, error) {
|
||||
artifacts := make([]types.Artifact, 0, len(dst))
|
||||
for _, d := range dst {
|
||||
artifact, err := a.mapToArtifact(ctx, d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
artifacts = append(artifacts, *artifact)
|
||||
}
|
||||
return &artifacts, nil
|
||||
}
|
||||
|
||||
func (a ArtifactDao) mapToArtifactMetadataList(
|
||||
dst []*artifactMetadataDB,
|
||||
) (*[]types.ArtifactMetadata, error) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user