feat: [AH-1158]: Fixed around auth, download command and metadata api update (#3610)

* [AH-1158]: Fixed around auth, download command and metadata api update
main
Arvind Choudhary 2025-04-02 16:34:00 +00:00 committed by Harness
parent edcad7b744
commit cc86280a6c
7 changed files with 83 additions and 22 deletions

View File

@ -369,6 +369,8 @@ func GetPullCommand(
return GetHelmPullCommand(image, tag, registryURL)
case string(a.PackageTypeGENERIC):
return GetGenericArtifactFileDownloadCommand(registryURL, image, tag, "<FILENAME>")
case string(a.PackageTypePYTHON):
return GetPythonDownloadCommand(image, tag)
default:
return ""
}
@ -385,6 +387,22 @@ func GetHelmPullCommand(image string, tag string, registryURL string) string {
return "helm pull oci://" + GetRepoURLWithoutProtocol(registryURL) + "/" + image + " --version " + tag
}
func GetPythonDownloadCommand(artifact, version string) string {
downloadCommand := "pip install <ARTIFACT>==<VERSION> "
// Replace the placeholders with the actual values
replacements := map[string]string{
"<ARTIFACT>": artifact,
"<VERSION>": version,
}
for placeholder, value := range replacements {
downloadCommand = strings.ReplaceAll(downloadCommand, placeholder, value)
}
return downloadCommand
}
func GetGenericArtifactFileDownloadCommand(regURL, artifact, version, filename string) string {
downloadCommand := "curl --location '<HOSTNAME>/<ARTIFACT>:<VERSION>:<FILENAME>' --header 'x-api-key: <API_KEY>'" +
" -J -O"

View File

@ -68,7 +68,7 @@ func (r *remoteRegistryHelper) init(
spaceFinder refcache.SpaceFinder,
service secret.Service,
) error {
key := string(artifact.UpstreamConfigSourcePyPi)
key := string(artifact.PackageTypePYTHON)
if r.registry.Source == string(artifact.UpstreamConfigSourcePyPi) {
r.registry.RepoURL = pypi.PyPiURL
}

View File

@ -17,9 +17,11 @@ package pypi
import (
"errors"
"fmt"
"net/url"
"regexp"
"strings"
"github.com/rs/zerolog/log"
"golang.org/x/net/html"
)
@ -48,14 +50,33 @@ type SimpleMetadata struct {
}
type Package struct {
Name string
ATags map[string]string
SimpleURL string
Name string
ATags map[string]string
}
// URL returns the "href" attribute from the package's map.
func (p Package) URL() string {
return p.ATags["href"]
href := p.ATags["href"]
parsedURL, err := url.Parse(href)
if err != nil {
return href
}
if parsedURL.IsAbs() {
return href
}
// If href is relative, resolve it against SimpleURL
baseURL, err := url.Parse(p.SimpleURL)
if err != nil {
log.Err(err).Msgf("failed to parse url %s", p.SimpleURL)
return href
}
return baseURL.ResolveReference(parsedURL).String()
}
func (p Package) Valid() bool {
return p.URL() != "" && p.Name != ""
}

View File

@ -20,6 +20,7 @@ import (
"context"
"github.com/harness/gitness/app/services/refcache"
"github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/registry/app/common/lib"
"github.com/harness/gitness/registry/app/common/lib/errors"
adp "github.com/harness/gitness/registry/app/remote/adapter"
@ -60,7 +61,8 @@ func NewAdapter(
return nil
}
adapter.Client = registry.NewClient(url, accessKey, secretKey, false)
adapter.Client = registry.NewClient(url, accessKey, secretKey, false,
reg.PackageType == artifact.PackageTypeDOCKER || reg.PackageType == artifact.PackageTypeHELM)
return adapter
}

View File

@ -76,7 +76,7 @@ func (f *factory) Create(
}
func init() {
adapterType := string(artifact.UpstreamConfigSourcePyPi)
adapterType := string(artifact.PackageTypePYTHON)
if err := adp.RegisterFactory(adapterType, new(factory)); err != nil {
log.Error().Stack().Err(err).Msgf("Failed to register adapter factory for %s", adapterType)
return
@ -85,12 +85,16 @@ func init() {
}
func (a *adapter) GetMetadata(_ context.Context, pkg string) (*pypi.SimpleMetadata, error) {
_, readCloser, err := a.GetFile("simple/" + pkg)
filePath := "simple/" + pkg
if a.registry.RepoURL != PyPiURL {
filePath += "/index.html"
}
_, readCloser, err := a.GetFile(filePath)
if err != nil {
return nil, err
}
defer readCloser.Close()
response, err := ParsePyPISimple(readCloser)
response, err := ParsePyPISimple(readCloser, a.GetURL(filePath))
if err != nil {
return nil, err
}
@ -170,7 +174,7 @@ func ParseMetadata(ctx context.Context, body io.ReadCloser) (python.Metadata, er
}
// ParsePyPISimple parses the given HTML and returns a SimpleMetadata DTO.
func ParsePyPISimple(r io.ReadCloser) (pypi.SimpleMetadata, error) {
func ParsePyPISimple(r io.ReadCloser, url string) (pypi.SimpleMetadata, error) {
doc, err := html.Parse(r)
if err != nil {
return pypi.SimpleMetadata{}, err
@ -214,8 +218,9 @@ func ParsePyPISimple(r io.ReadCloser) (pypi.SimpleMetadata, error) {
linkText = n.FirstChild.Data
}
packages = append(packages, pypi.Package{
ATags: aMap,
Name: linkText,
SimpleURL: url,
ATags: aMap,
Name: linkText,
})
}
}

View File

@ -34,10 +34,11 @@ import (
)
// NewAuthorizer creates an authorizer that can handle different auth schemes.
func NewAuthorizer(username, password string, insecure bool) lib.Authorizer {
func NewAuthorizer(username, password string, insecure, isOCI bool) lib.Authorizer {
return &authorizer{
username: username,
password: password,
isOCI: isOCI,
client: &http.Client{
Transport: commonhttp.GetHTTPTransport(commonhttp.WithInsecure(insecure)),
},
@ -54,6 +55,7 @@ type authorizer struct {
client *http.Client
url *url.URL // registry URL
authorizer modifier.Modifier // the underlying authorizer
isOCI bool
}
func (a *authorizer) Modify(req *http.Request) error {
@ -82,6 +84,12 @@ func (a *authorizer) initialize(u *url.URL) error {
if a.authorizer != nil {
return nil
}
if !a.isOCI {
a.authorizer = basic.NewAuthorizer(a.username, a.password)
return nil
}
url, err := url.Parse(u.Scheme + "://" + u.Host + "/v2/")
if err != nil {
return fmt.Errorf("failed to parse URL for scheme %s and host %s: %w", u.Scheme, u.Host, err)
@ -128,15 +136,17 @@ func (a *authorizer) initialize(u *url.URL) error {
// If not, the request shouldn't be handled by the authorizer, e.g., requests sent to backend storage (S3, etc.).
func (a *authorizer) isTarget(req *http.Request) bool {
// Check if the path contains the versioned API endpoint (e.g., "/v2/")
const versionedPath = "/v2/"
if !strings.Contains(req.URL.Path, versionedPath) {
return false
}
if a.isOCI {
const versionedPath = "/v2/"
if !strings.Contains(req.URL.Path, versionedPath) {
return false
}
// Ensure that the request's host, scheme, and versioned path match the authorizer's URL.
if req.URL.Host != a.url.Host || req.URL.Scheme != a.url.Scheme ||
!strings.HasPrefix(req.URL.Path, a.url.Path) {
return false
// Ensure that the request's host, scheme, and versioned path match the authorizer's URL.
if req.URL.Host != a.url.Host || req.URL.Scheme != a.url.Scheme ||
!strings.HasPrefix(req.URL.Path, a.url.Path) {
return false
}
}
return true

View File

@ -152,13 +152,14 @@ type Client interface {
// GetFileFromURL Download the file from URL instead of provided endpoint. Authorizer still remains the same.
GetFileFromURL(url string) (*commons.ResponseHeaders, io.ReadCloser, error)
GetURL(filePath string) string
}
// NewClient creates a registry client with the default authorizer which determines the auth scheme
// of the registry automatically and calls the corresponding underlying authorizers(basic/bearer) to
// do the auth work. If a customized authorizer is needed, use "NewClientWithAuthorizer" instead.
func NewClient(url, username, password string, insecure bool, interceptors ...interceptor.Interceptor) Client {
authorizer := auth.NewAuthorizer(username, password, insecure)
func NewClient(url, username, password string, insecure, isOCI bool, interceptors ...interceptor.Interceptor) Client {
authorizer := auth.NewAuthorizer(username, password, insecure, isOCI)
return NewClientWithAuthorizer(url, authorizer, insecure, interceptors...)
}
@ -875,3 +876,7 @@ func (c *client) HeadFile(filePath string) (*commons.ResponseHeaders, bool, erro
func buildFileURL(endpoint, filePath string) string {
return fmt.Sprintf("%s/%s", endpoint, filePath)
}
func (c *client) GetURL(filePath string) string {
return buildFileURL(c.url, filePath)
}