mirror of https://github.com/artifacthub/hub.git
921 lines
26 KiB
Go
921 lines
26 KiB
Go
package repo
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/artifacthub/hub/internal/hub"
|
|
"github.com/artifacthub/hub/internal/oci"
|
|
"github.com/artifacthub/hub/internal/util"
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/config"
|
|
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
|
"github.com/go-git/go-git/v5/storage/memory"
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
|
"github.com/satori/uuid"
|
|
"github.com/spf13/viper"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
const (
|
|
// Database queries
|
|
addRepoDBQ = `select add_repository($1::uuid, $2::text, $3::jsonb)`
|
|
checkRepoNameAvailDBQ = `select repository_id from repository where name = $1`
|
|
checkRepoURLAvailDBQ = `select repository_id from repository where trim(trailing '/' from url) = $1`
|
|
deleteRepoDBQ = `select delete_repository($1::uuid, $2::text)`
|
|
getRepoByIDDBQ = `select get_repository_by_id($1::uuid, $2::boolean)`
|
|
getRepoByNameDBQ = `select get_repository_by_name($1::text, $2::boolean)`
|
|
getRepoPkgsDigestDBQ = `select get_repository_packages_digest($1::uuid)`
|
|
getUserEmailDBQ = `select email from "user" where user_id = $1`
|
|
searchRepositoriesDBQ = `select * from search_repositories($1::jsonb)`
|
|
setLastScanningResultsDBQ = `select set_last_scanning_results($1::uuid, $2::text, $3::boolean)`
|
|
setLastTrackingResultsDBQ = `select set_last_tracking_results($1::uuid, $2::text, $3::boolean)`
|
|
setVerifiedPublisherDBQ = `select set_verified_publisher($1::uuid, $2::boolean)`
|
|
transferRepoDBQ = `select transfer_repository($1::text, $2::uuid, $3::text, $4::boolean)`
|
|
updateRepoDBQ = `select update_repository($1::uuid, $2::jsonb)`
|
|
updateRepoDigestDBQ = `update repository set digest = $2 where repository_id = $1`
|
|
)
|
|
|
|
const (
|
|
// MetadataLayerMediaType represents the media type used for the layer that
|
|
// contains the repository metadata in an OCI image.
|
|
MetadataLayerMediaType = "application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml"
|
|
|
|
artifacthubTag = "artifacthub.io"
|
|
maxContainerImageTags = 10
|
|
)
|
|
|
|
var (
|
|
// repositoryNameRE is a regexp used to validate a repository name.
|
|
repositoryNameRE = regexp.MustCompile(`^[a-z][a-z0-9-]*$`)
|
|
|
|
// ErrInvalidMetadata indicates that the repository metadata is not valid.
|
|
ErrInvalidMetadata = errors.New("invalid metadata")
|
|
|
|
// ErrMetadataNotFound indicates that the repository metadata was not found.
|
|
ErrMetadataNotFound = errors.New("metadata not found")
|
|
|
|
// ErrSchemeNotSupported error indicates that the scheme used in the
|
|
// repository url is not supported.
|
|
ErrSchemeNotSupported = errors.New("scheme not supported")
|
|
|
|
// GitRepoURLRE is a regexp used to validate and parse an http based git
|
|
// repository URL.
|
|
GitRepoURLRE = regexp.MustCompile(`^(https:\/\/([A-Za-z0-9_.-]+)\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\/?(.*)$`)
|
|
|
|
// validRepositoryKinds contains the repository kinds supported.
|
|
validRepositoryKinds = []hub.RepositoryKind{
|
|
hub.ArgoTemplate,
|
|
hub.Backstage,
|
|
hub.Container,
|
|
hub.CoreDNS,
|
|
hub.Falco,
|
|
hub.Gatekeeper,
|
|
hub.Headlamp,
|
|
hub.Helm,
|
|
hub.HelmPlugin,
|
|
hub.KCL,
|
|
hub.KedaScaler,
|
|
hub.Keptn,
|
|
hub.KnativeClientPlugin,
|
|
hub.Krew,
|
|
hub.KubeArmor,
|
|
hub.Kubewarden,
|
|
hub.Kyverno,
|
|
hub.OLM,
|
|
hub.OPA,
|
|
hub.TBAction,
|
|
hub.TektonPipeline,
|
|
hub.TektonTask,
|
|
}
|
|
)
|
|
|
|
// Manager provides an API to manage repositories.
|
|
type Manager struct {
|
|
cfg *viper.Viper
|
|
db hub.DB
|
|
hc hub.HTTPClient
|
|
rc hub.RepositoryCloner
|
|
il hub.HelmIndexLoader
|
|
tg hub.OCITagsGetter
|
|
op hub.OCIPuller
|
|
az hub.Authorizer
|
|
}
|
|
|
|
// NewManager creates a new Manager instance.
|
|
func NewManager(
|
|
cfg *viper.Viper,
|
|
db hub.DB,
|
|
az hub.Authorizer,
|
|
hc hub.HTTPClient,
|
|
opts ...func(m *Manager),
|
|
) *Manager {
|
|
// Setup manager
|
|
m := &Manager{
|
|
cfg: cfg,
|
|
db: db,
|
|
il: &HelmIndexLoader{},
|
|
tg: oci.NewTagsGetter(cfg),
|
|
op: oci.NewPuller(cfg),
|
|
az: az,
|
|
hc: hc,
|
|
}
|
|
for _, o := range opts {
|
|
o(m)
|
|
}
|
|
|
|
// Setup repository cloner
|
|
if m.rc == nil {
|
|
m.rc = &Cloner{}
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// WithHelmIndexLoader allows providing a specific HelmIndexLoader
|
|
// implementation for a Manager instance.
|
|
func WithHelmIndexLoader(l hub.HelmIndexLoader) func(m *Manager) {
|
|
return func(m *Manager) {
|
|
m.il = l
|
|
}
|
|
}
|
|
|
|
// WithOCITagsGetter allows providing a specific OCITagsGetter implementation
|
|
// for a Manager instance.
|
|
func WithOCITagsGetter(tg hub.OCITagsGetter) func(m *Manager) {
|
|
return func(m *Manager) {
|
|
m.tg = tg
|
|
}
|
|
}
|
|
|
|
// WithOCIPuller allows providing a specific OCIPuller implementation for a
|
|
// Manager instance.
|
|
func WithOCIPuller(p hub.OCIPuller) func(m *Manager) {
|
|
return func(m *Manager) {
|
|
m.op = p
|
|
}
|
|
}
|
|
|
|
// Add adds the provided repository to the database.
|
|
func (m *Manager) Add(ctx context.Context, orgName string, r *hub.Repository) error {
|
|
userID := ctx.Value(hub.UserIDKey).(string)
|
|
|
|
// Validate input
|
|
if !isValidKind(r.Kind) {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid kind")
|
|
}
|
|
if r.Name == "" {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "name not provided")
|
|
}
|
|
if !repositoryNameRE.MatchString(r.Name) {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid name")
|
|
}
|
|
if err := m.validateURL(r); err != nil {
|
|
return fmt.Errorf("%w: %w", hub.ErrInvalidInput, err)
|
|
}
|
|
if err := m.validateCredentials(r); err != nil {
|
|
return fmt.Errorf("%w: %w", hub.ErrInvalidInput, err)
|
|
}
|
|
if err := validateData(r); err != nil {
|
|
return fmt.Errorf("%w: %w", hub.ErrInvalidInput, err)
|
|
}
|
|
|
|
// Authorize action if the repository will be added to an organization
|
|
if orgName != "" {
|
|
if err := m.az.Authorize(ctx, &hub.AuthorizeInput{
|
|
OrganizationName: orgName,
|
|
UserID: userID,
|
|
Action: hub.AddOrganizationRepository,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Add repository to the database
|
|
rJSON, _ := json.Marshal(r)
|
|
_, err := m.db.Exec(ctx, addRepoDBQ, userID, orgName, rJSON)
|
|
if err != nil && err.Error() == util.ErrDBInsufficientPrivilege.Error() {
|
|
return hub.ErrInsufficientPrivilege
|
|
}
|
|
return err
|
|
}
|
|
|
|
// CheckAvailability checks the availability of a given value for the provided
|
|
// resource kind.
|
|
func (m *Manager) CheckAvailability(ctx context.Context, resourceKind, value string) (bool, error) {
|
|
var available bool
|
|
|
|
// Validate input
|
|
validResourceKinds := []string{
|
|
"repositoryName",
|
|
"repositoryURL",
|
|
}
|
|
isResourceKindValid := func(resourceKind string) bool {
|
|
for _, k := range validResourceKinds {
|
|
if resourceKind == k {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
if !isResourceKindValid(resourceKind) {
|
|
return available, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid resource kind")
|
|
}
|
|
if value == "" {
|
|
return available, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid value")
|
|
}
|
|
|
|
// Check availability in database
|
|
var query string
|
|
switch resourceKind {
|
|
case "repositoryName":
|
|
query = checkRepoNameAvailDBQ
|
|
case "repositoryURL":
|
|
query = checkRepoURLAvailDBQ
|
|
}
|
|
query = fmt.Sprintf("select not exists (%s)", query)
|
|
value = strings.TrimSuffix(value, "/")
|
|
err := m.db.QueryRow(ctx, query, value).Scan(&available)
|
|
return available, err
|
|
}
|
|
|
|
// ClaimOwnership allows a user to claim the ownership of a given repository.
|
|
// The repository will be transferred to the destination entity requested if
|
|
// the user is listed as one of the owners in the repository metadata file.
|
|
func (m *Manager) ClaimOwnership(ctx context.Context, repoName, orgName string) error {
|
|
userID := ctx.Value(hub.UserIDKey).(string)
|
|
|
|
// Validate input
|
|
if repoName == "" {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "repository name not provided")
|
|
}
|
|
|
|
// Get repository information
|
|
r, err := m.GetByName(ctx, repoName, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Some extra validation
|
|
u, _ := url.Parse(r.URL)
|
|
if r.Kind == hub.OLM && SchemeIsOCI(u) {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "ownership claim not available for this repo kind")
|
|
}
|
|
|
|
// Get repository metadata
|
|
var basePath string
|
|
switch r.Kind {
|
|
case
|
|
hub.ArgoTemplate,
|
|
hub.Backstage,
|
|
hub.CoreDNS,
|
|
hub.Falco,
|
|
hub.Gatekeeper,
|
|
hub.Headlamp,
|
|
hub.HelmPlugin,
|
|
hub.KCL,
|
|
hub.KedaScaler,
|
|
hub.Keptn,
|
|
hub.KnativeClientPlugin,
|
|
hub.Krew,
|
|
hub.KubeArmor,
|
|
hub.Kubewarden,
|
|
hub.Kyverno,
|
|
hub.OLM,
|
|
hub.OPA,
|
|
hub.TBAction,
|
|
hub.TektonPipeline,
|
|
hub.TektonTask:
|
|
tmpDir, packagesPath, err := m.rc.CloneRepository(ctx, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
basePath = filepath.Join(tmpDir, packagesPath)
|
|
}
|
|
md, err := m.GetMetadata(r, basePath)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: error getting repository metadata: %w", hub.ErrInsufficientPrivilege, err)
|
|
}
|
|
|
|
// Get requesting user email
|
|
var userEmail string
|
|
if err := m.db.QueryRow(ctx, getUserEmailDBQ, userID).Scan(&userEmail); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Transfer repository if the requesting user is listed as one of the
|
|
// repository owners in the metadata file
|
|
for _, owner := range md.Owners {
|
|
if owner.Email == userEmail {
|
|
return m.Transfer(ctx, repoName, orgName, true)
|
|
}
|
|
}
|
|
return hub.ErrInsufficientPrivilege
|
|
}
|
|
|
|
// Delete deletes the provided repository from the database.
|
|
func (m *Manager) Delete(ctx context.Context, name string) error {
|
|
userID := ctx.Value(hub.UserIDKey).(string)
|
|
|
|
// Validate input
|
|
if name == "" {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "name not provided")
|
|
}
|
|
|
|
// Authorize action if the repository is owned by an organization
|
|
r, err := m.GetByName(ctx, name, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if r.OrganizationName != "" {
|
|
if err := m.az.Authorize(ctx, &hub.AuthorizeInput{
|
|
OrganizationName: r.OrganizationName,
|
|
UserID: userID,
|
|
Action: hub.DeleteOrganizationRepository,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Delete repository from database
|
|
_, err = m.db.Exec(ctx, deleteRepoDBQ, userID, name)
|
|
if err != nil && err.Error() == util.ErrDBInsufficientPrivilege.Error() {
|
|
return hub.ErrInsufficientPrivilege
|
|
}
|
|
return err
|
|
}
|
|
|
|
// GetByID returns the repository identified by the id provided.
|
|
func (m *Manager) GetByID(
|
|
ctx context.Context,
|
|
repositoryID string,
|
|
includeCredentials bool,
|
|
) (*hub.Repository, error) {
|
|
// Validate input
|
|
if repositoryID == "" {
|
|
return nil, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "repository id not provided")
|
|
}
|
|
if _, err := uuid.FromString(repositoryID); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid repository id")
|
|
}
|
|
|
|
// Get repository from database
|
|
var r *hub.Repository
|
|
err := util.DBQueryUnmarshal(ctx, m.db, &r, getRepoByIDDBQ, repositoryID, includeCredentials)
|
|
return r, err
|
|
}
|
|
|
|
// GetByName returns the repository identified by the name provided.
|
|
func (m *Manager) GetByName(
|
|
ctx context.Context,
|
|
name string,
|
|
includeCredentials bool,
|
|
) (*hub.Repository, error) {
|
|
// Validate input
|
|
if name == "" {
|
|
return nil, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "name not provided")
|
|
}
|
|
|
|
// Get repository from database
|
|
var r *hub.Repository
|
|
err := util.DBQueryUnmarshal(ctx, m.db, &r, getRepoByNameDBQ, name, includeCredentials)
|
|
return r, err
|
|
}
|
|
|
|
// GetMetadata reads and parses the metadata file of the repository provided.
|
|
// When needed, the repository must be previously cloned and the path pointing
|
|
// to the location of the packages must be provided (basePath).
|
|
func (m *Manager) GetMetadata(r *hub.Repository, basePath string) (*hub.RepositoryMetadata, error) {
|
|
var data []byte
|
|
var err error
|
|
|
|
// Get metadata
|
|
mdFile := m.locateMetadataFile(r, basePath)
|
|
if strings.HasPrefix(mdFile, hub.RepositoryOCIPrefix) {
|
|
// OCI image
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
ref := fmt.Sprintf("%s:%s", strings.TrimPrefix(mdFile, hub.RepositoryOCIPrefix), artifacthubTag)
|
|
_, data, err = m.op.PullLayer(ctx, ref, MetadataLayerMediaType, r.AuthUser, r.AuthPass)
|
|
if errors.Is(err, oci.ErrArtifactNotFound) || errors.Is(err, oci.ErrLayerNotFound) {
|
|
err = ErrMetadataNotFound
|
|
}
|
|
} else {
|
|
// Remote HTTP url / local file path
|
|
for _, extension := range []string{".yml", ".yaml"} {
|
|
data, err = m.readMetadataFile(mdFile+extension, r.AuthUser, r.AuthPass)
|
|
if err == nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse and validate metadata
|
|
var md *hub.RepositoryMetadata
|
|
if err = yaml.Unmarshal(data, &md); err != nil || md == nil {
|
|
return nil, fmt.Errorf("error unmarshaling repository metadata file: %w", err)
|
|
}
|
|
if md.RepositoryID != "" {
|
|
if _, err := uuid.FromString(md.RepositoryID); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", ErrInvalidMetadata, "invalid repository id")
|
|
}
|
|
}
|
|
|
|
return md, nil
|
|
}
|
|
|
|
// locateMetadataFile returns the location of the metadata file for the
|
|
// repository provided.
|
|
func (m *Manager) locateMetadataFile(r *hub.Repository, basePath string) string {
|
|
var mdFile string
|
|
u, _ := url.Parse(r.URL)
|
|
switch r.Kind {
|
|
case hub.Container:
|
|
mdFile = r.URL
|
|
case hub.Helm:
|
|
switch u.Scheme {
|
|
case "http", "https":
|
|
u.Path = path.Join(u.Path, hub.RepositoryMetadataFile)
|
|
mdFile = u.String()
|
|
case "oci":
|
|
mdFile = r.URL
|
|
}
|
|
case
|
|
hub.ArgoTemplate,
|
|
hub.Backstage,
|
|
hub.CoreDNS,
|
|
hub.Falco,
|
|
hub.Gatekeeper,
|
|
hub.Headlamp,
|
|
hub.HelmPlugin,
|
|
hub.KCL,
|
|
hub.KedaScaler,
|
|
hub.Keptn,
|
|
hub.KnativeClientPlugin,
|
|
hub.Krew,
|
|
hub.KubeArmor,
|
|
hub.Kubewarden,
|
|
hub.Kyverno,
|
|
hub.OLM,
|
|
hub.OPA,
|
|
hub.TBAction,
|
|
hub.TektonPipeline,
|
|
hub.TektonTask:
|
|
mdFile = filepath.Join(basePath, hub.RepositoryMetadataFile)
|
|
}
|
|
return mdFile
|
|
}
|
|
|
|
// readMetadataFile reads the repository metadata from the provided file.
|
|
func (m *Manager) readMetadataFile(mdFile, username, password string) ([]byte, error) {
|
|
var data []byte
|
|
u, err := url.Parse(mdFile)
|
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
|
if _, err := os.Stat(mdFile); os.IsNotExist(err) {
|
|
return nil, ErrMetadataNotFound
|
|
}
|
|
data, err = os.ReadFile(mdFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading repository metadata file: %w", err)
|
|
}
|
|
} else {
|
|
req, _ := http.NewRequest("GET", mdFile, nil)
|
|
if username != "" || password != "" {
|
|
req.SetBasicAuth(username, password)
|
|
}
|
|
resp, err := m.hc.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error downloading repository metadata file: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
switch resp.StatusCode {
|
|
case http.StatusOK:
|
|
case http.StatusNotFound:
|
|
return nil, ErrMetadataNotFound
|
|
default:
|
|
return nil, fmt.Errorf("unexpected status code received: %d", resp.StatusCode)
|
|
}
|
|
data, err = io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading repository metadata file: %w", err)
|
|
}
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// GetPackagesDigest returns the digests for all packages in the repository
|
|
// identified by the id provided.
|
|
func (m *Manager) GetPackagesDigest(
|
|
ctx context.Context,
|
|
repositoryID string,
|
|
) (map[string]string, error) {
|
|
// Validate input
|
|
if _, err := uuid.FromString(repositoryID); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid repository id")
|
|
}
|
|
|
|
// Get repository packages digest from database
|
|
pd := make(map[string]string)
|
|
err := util.DBQueryUnmarshal(ctx, m.db, &pd, getRepoPkgsDigestDBQ, repositoryID)
|
|
return pd, err
|
|
}
|
|
|
|
// GetRemoteDigest gets the repository's digest available in the remote.
|
|
func (m *Manager) GetRemoteDigest(ctx context.Context, r *hub.Repository) (string, error) {
|
|
var digest string
|
|
u, _ := url.Parse(r.URL)
|
|
|
|
switch {
|
|
case r.Kind == hub.Helm:
|
|
switch {
|
|
case SchemeIsHTTP(u):
|
|
// Digest is obtained by hashing the repository index.yaml file
|
|
var err error
|
|
_, digest, err = m.il.LoadIndex(r)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
case SchemeIsOCI(u):
|
|
// Digest is obtained by hashing the list of versions available
|
|
versions, err := m.tg.Tags(ctx, r, true)
|
|
if err != nil {
|
|
return digest, err
|
|
}
|
|
digest = fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(versions, ","))))
|
|
}
|
|
|
|
case r.Kind == hub.OLM && SchemeIsOCI(u):
|
|
// Digest is obtained from the index image digest
|
|
refName := strings.TrimPrefix(r.URL, hub.RepositoryOCIPrefix)
|
|
ref, err := name.ParseReference(refName)
|
|
if err != nil {
|
|
return digest, err
|
|
}
|
|
desc, err := remote.Head(ref)
|
|
if err != nil {
|
|
return digest, err
|
|
}
|
|
digest = desc.Digest.String()
|
|
|
|
case GitRepoURLRE.MatchString(r.URL):
|
|
// Do not track repo's digest for Tekton repos using git based versioning
|
|
if (r.Kind == hub.TektonTask || r.Kind == hub.TektonPipeline) && r.Data != nil {
|
|
var data *hub.TektonData
|
|
if err := json.Unmarshal(r.Data, &data); err != nil {
|
|
return "", fmt.Errorf("invalid tekton repository data: %w", err)
|
|
}
|
|
if data.Versioning == hub.TektonGitBasedVersioning {
|
|
return "", nil
|
|
}
|
|
}
|
|
|
|
// Digest is obtained from the last commit in the repository
|
|
matches := GitRepoURLRE.FindStringSubmatch(r.URL)
|
|
repoBaseURL := matches[1]
|
|
remote := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{
|
|
URLs: []string{repoBaseURL},
|
|
})
|
|
listOptions := &git.ListOptions{}
|
|
if r.AuthPass != "" {
|
|
listOptions.Auth = &githttp.BasicAuth{
|
|
Username: "artifact-hub",
|
|
Password: r.AuthPass,
|
|
}
|
|
}
|
|
refs, err := remote.List(listOptions)
|
|
if err != nil {
|
|
return digest, err
|
|
}
|
|
branch := GetBranch(r)
|
|
for _, ref := range refs {
|
|
if ref.Name().IsBranch() && ref.Name().Short() == branch {
|
|
digest = ref.Hash().String()
|
|
}
|
|
}
|
|
}
|
|
|
|
return digest, nil
|
|
}
|
|
|
|
// Search searches for repositories in the database that the criteria defined
|
|
// in the input provided.
|
|
func (m *Manager) Search(
|
|
ctx context.Context,
|
|
input *hub.SearchRepositoryInput,
|
|
) (*hub.SearchRepositoryResult, error) {
|
|
// Validate input
|
|
if err := validateSearchInput(input); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Search repositories in database
|
|
inputJSON, _ := json.Marshal(input)
|
|
result, err := util.DBQueryJSONWithPagination(ctx, m.db, searchRepositoriesDBQ, inputJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var repositories []*hub.Repository
|
|
if err := json.Unmarshal(result.Data, &repositories); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &hub.SearchRepositoryResult{
|
|
Repositories: repositories,
|
|
TotalCount: result.TotalCount,
|
|
}, nil
|
|
}
|
|
|
|
// SearchJSON returns a json object with the search results produced by the
|
|
// input provided. The json object is built by the database.
|
|
func (m *Manager) SearchJSON(
|
|
ctx context.Context,
|
|
input *hub.SearchRepositoryInput,
|
|
) (*hub.JSONQueryResult, error) {
|
|
// Validate input
|
|
if err := validateSearchInput(input); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Search repositories in database
|
|
inputJSON, _ := json.Marshal(input)
|
|
return util.DBQueryJSONWithPagination(ctx, m.db, searchRepositoriesDBQ, inputJSON)
|
|
}
|
|
|
|
// SetLastScanningResults updates the timestamp and errors of the last scanning
|
|
// of the provided repository in the database.
|
|
func (m *Manager) SetLastScanningResults(ctx context.Context, repositoryID, errs string) error {
|
|
// Validate input
|
|
if _, err := uuid.FromString(repositoryID); err != nil {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid repository id")
|
|
}
|
|
|
|
// Update last scanning results in database
|
|
scanningErrorsEventsEnabled := m.cfg.GetBool("events.scanningErrors")
|
|
_, err := m.db.Exec(ctx, setLastScanningResultsDBQ, repositoryID, errs, scanningErrorsEventsEnabled)
|
|
return err
|
|
}
|
|
|
|
// SetLastTrackingResults updates the timestamp and errors of the last tracking
|
|
// of the provided repository in the database.
|
|
func (m *Manager) SetLastTrackingResults(ctx context.Context, repositoryID, errs string) error {
|
|
// Validate input
|
|
if _, err := uuid.FromString(repositoryID); err != nil {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid repository id")
|
|
}
|
|
|
|
// Update last tracking results in database
|
|
trackingErrorsEventsEnabled := m.cfg.GetBool("events.trackingErrors")
|
|
_, err := m.db.Exec(ctx, setLastTrackingResultsDBQ, repositoryID, errs, trackingErrorsEventsEnabled)
|
|
return err
|
|
}
|
|
|
|
// SetVerifiedPublisher updates the verified publisher flag of the provided
|
|
// repository in the database.
|
|
func (m *Manager) SetVerifiedPublisher(ctx context.Context, repositoryID string, verified bool) error {
|
|
// Validate input
|
|
if _, err := uuid.FromString(repositoryID); err != nil {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid repository id")
|
|
}
|
|
|
|
// Update verified publisher status in database
|
|
_, err := m.db.Exec(ctx, setVerifiedPublisherDBQ, repositoryID, verified)
|
|
return err
|
|
}
|
|
|
|
// Transfer transfers the provided repository to a different owner. A user
|
|
// owned repo can be transferred to an organization the requesting user belongs
|
|
// to. An org owned repo can be transfer to the requesting user, provided the
|
|
// user belongs to the owning org, or to a different organization the user
|
|
// belongs to.
|
|
func (m *Manager) Transfer(ctx context.Context, repoName, orgName string, ownershipClaim bool) error {
|
|
var orgNameP *string
|
|
if orgName != "" {
|
|
orgNameP = &orgName
|
|
}
|
|
var userIDP *string
|
|
userID := ctx.Value(hub.UserIDKey).(string)
|
|
if userID != "" {
|
|
userIDP = &userID
|
|
}
|
|
|
|
// Validate input
|
|
if repoName == "" {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "repository name not provided")
|
|
}
|
|
|
|
// Authorize action if this is not an ownership claim operation and the
|
|
// repository is owned by an organization
|
|
if !ownershipClaim {
|
|
r, err := m.GetByName(ctx, repoName, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if r.OrganizationName != "" {
|
|
if err := m.az.Authorize(ctx, &hub.AuthorizeInput{
|
|
OrganizationName: r.OrganizationName,
|
|
UserID: userID,
|
|
Action: hub.TransferOrganizationRepository,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update repository owner in database
|
|
_, err := m.db.Exec(ctx, transferRepoDBQ, repoName, userIDP, orgNameP, ownershipClaim)
|
|
if err != nil && err.Error() == util.ErrDBInsufficientPrivilege.Error() {
|
|
return hub.ErrInsufficientPrivilege
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Update updates the provided repository in the database.
|
|
func (m *Manager) Update(ctx context.Context, r *hub.Repository) error {
|
|
userID := ctx.Value(hub.UserIDKey).(string)
|
|
|
|
// Validate input
|
|
if r.Name == "" {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "name not provided")
|
|
}
|
|
if err := m.validateURL(r); err != nil {
|
|
return fmt.Errorf("%w: %w", hub.ErrInvalidInput, err)
|
|
}
|
|
if err := m.validateCredentials(r); err != nil {
|
|
return fmt.Errorf("%w: %w", hub.ErrInvalidInput, err)
|
|
}
|
|
if err := validateData(r); err != nil {
|
|
return fmt.Errorf("%w: %w", hub.ErrInvalidInput, err)
|
|
}
|
|
|
|
// Authorize action if the repository is owned by an organization
|
|
rBefore, err := m.GetByName(ctx, r.Name, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rBefore.OrganizationName != "" {
|
|
if err := m.az.Authorize(ctx, &hub.AuthorizeInput{
|
|
OrganizationName: rBefore.OrganizationName,
|
|
UserID: userID,
|
|
Action: hub.UpdateOrganizationRepository,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update repository in database
|
|
rJSON, _ := json.Marshal(r)
|
|
_, err = m.db.Exec(ctx, updateRepoDBQ, userID, rJSON)
|
|
if err != nil && err.Error() == util.ErrDBInsufficientPrivilege.Error() {
|
|
return hub.ErrInsufficientPrivilege
|
|
}
|
|
return err
|
|
}
|
|
|
|
// UpdateDigest updates the digest of the provided repository in the database.
|
|
func (m *Manager) UpdateDigest(ctx context.Context, repositoryID, digest string) error {
|
|
_, err := m.db.Exec(ctx, updateRepoDigestDBQ, repositoryID, digest)
|
|
return err
|
|
}
|
|
|
|
// validateURL validates the url of the repository provided.
|
|
func (m *Manager) validateURL(r *hub.Repository) error {
|
|
if r.URL == "" {
|
|
return errors.New("url not provided")
|
|
}
|
|
u, err := url.Parse(r.URL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isSchemeSupported(u) {
|
|
return ErrSchemeNotSupported
|
|
}
|
|
if u.User != nil {
|
|
return errors.New("urls with credentials not allowed")
|
|
}
|
|
switch r.Kind {
|
|
case hub.Container:
|
|
if !SchemeIsOCI(u) {
|
|
return errors.New("invalid url format")
|
|
}
|
|
case hub.Helm:
|
|
if SchemeIsHTTP(u) {
|
|
if _, _, err := m.il.LoadIndex(r); err != nil {
|
|
return errors.New("the url provided does not point to a valid Helm repository")
|
|
}
|
|
}
|
|
case
|
|
hub.ArgoTemplate,
|
|
hub.Backstage,
|
|
hub.CoreDNS,
|
|
hub.Falco,
|
|
hub.Gatekeeper,
|
|
hub.Headlamp,
|
|
hub.HelmPlugin,
|
|
hub.KCL,
|
|
hub.KedaScaler,
|
|
hub.Keptn,
|
|
hub.KnativeClientPlugin,
|
|
hub.Krew,
|
|
hub.KubeArmor,
|
|
hub.Kubewarden,
|
|
hub.Kyverno,
|
|
hub.OLM,
|
|
hub.OPA,
|
|
hub.TBAction,
|
|
hub.TektonPipeline,
|
|
hub.TektonTask:
|
|
if SchemeIsHTTP(u) && !GitRepoURLRE.MatchString(r.URL) {
|
|
return errors.New("invalid url format")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateCredentials validates the credentials of the repository provided.
|
|
func (m *Manager) validateCredentials(r *hub.Repository) error {
|
|
allowPrivateRepos := m.cfg.GetBool("server.allowPrivateRepositories")
|
|
if !allowPrivateRepos && (r.AuthUser != "" || r.AuthPass != "") {
|
|
return errors.New("private repositories not allowed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateData checks the kind specific data provided.
|
|
func validateData(r *hub.Repository) error {
|
|
switch r.Kind {
|
|
case hub.Container:
|
|
if r.Data != nil {
|
|
var data *hub.ContainerImageData
|
|
if err := json.Unmarshal(r.Data, &data); err != nil {
|
|
return fmt.Errorf("invalid container image data: %w", err)
|
|
}
|
|
if len(data.Tags) > maxContainerImageTags {
|
|
return fmt.Errorf("too many tags (max allowed: %d)", maxContainerImageTags)
|
|
}
|
|
}
|
|
return nil
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// validateSearchInput validates the search input provided, returning an error
|
|
// in case it's invalid.
|
|
func validateSearchInput(input *hub.SearchRepositoryInput) error {
|
|
for _, alias := range input.Users {
|
|
if alias == "" {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid user alias")
|
|
}
|
|
}
|
|
for _, name := range input.Orgs {
|
|
if name == "" {
|
|
return fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid organization name")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SchemeIsHTTP is a helper that checks if the scheme of the url provided is
|
|
// http or https.
|
|
func SchemeIsHTTP(u *url.URL) bool {
|
|
return u.Scheme == "http" || u.Scheme == "https"
|
|
}
|
|
|
|
// SchemeIsOCI is a helper that checks if the scheme of the url provided is oci.
|
|
func SchemeIsOCI(u *url.URL) bool {
|
|
return u.Scheme == "oci"
|
|
}
|
|
|
|
// isSchemeSupported is a helper that checks if the scheme of the url provided
|
|
// is supported.
|
|
func isSchemeSupported(u *url.URL) bool {
|
|
return SchemeIsHTTP(u) || SchemeIsOCI(u)
|
|
}
|
|
|
|
// isValidKind checks if the provided repository kind is valid.
|
|
func isValidKind(kind hub.RepositoryKind) bool {
|
|
for _, validKind := range validRepositoryKinds {
|
|
if kind == validKind {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|