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/httpw" "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/rs/zerolog/log" "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.Bootc, hub.Container, hub.CoreDNS, hub.Falco, hub.Gatekeeper, hub.Headlamp, hub.Helm, hub.HelmPlugin, hub.InspektorGadget, hub.Kagent, hub.KCL, hub.KedaScaler, hub.Keptn, hub.KnativeClientPlugin, hub.Krew, hub.KubeArmor, hub.Kubewarden, hub.Kyverno, hub.Meshery, hub.OLM, hub.OPA, hub.OpenCost, hub.Radius, hub.TBAction, hub.TektonPipeline, hub.TektonTask, hub.TektonStepAction, } ) // 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.Bootc, hub.CoreDNS, hub.Falco, hub.Gatekeeper, hub.Headlamp, hub.HelmPlugin, hub.InspektorGadget, hub.KCL, hub.KedaScaler, hub.Keptn, hub.KnativeClientPlugin, hub.Krew, hub.KubeArmor, hub.Kubewarden, hub.Kyverno, hub.Meshery, hub.OLM, hub.OPA, hub.OpenCost, hub.Radius, hub.TBAction, hub.TektonPipeline, hub.TektonTask, hub.TektonStepAction: 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, hub.Kagent: 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.Bootc, hub.CoreDNS, hub.Falco, hub.Gatekeeper, hub.Headlamp, hub.HelmPlugin, hub.InspektorGadget, hub.KCL, hub.KedaScaler, hub.Keptn, hub.KnativeClientPlugin, hub.Krew, hub.KubeArmor, hub.Kubewarden, hub.Kyverno, hub.Meshery, hub.OLM, hub.OPA, hub.OpenCost, hub.Radius, hub.TBAction, hub.TektonPipeline, hub.TektonTask, hub.TektonStepAction: 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, err := httpw.NewRequest("GET", mdFile, nil) if err != nil { return nil, err } 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 || r.Kind == hub.Kagent: 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, 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.Kind == hub.TektonStepAction) && 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, hub.Kagent: if SchemeIsHTTP(u) { if _, _, err := m.il.LoadIndex(r); err != nil { log.Error().Err(err).Str("url", r.URL).Msg("error loading index") return errors.New("the url provided does not point to a valid Helm repository") } } case hub.ArgoTemplate, hub.Backstage, hub.Bootc, hub.CoreDNS, hub.Falco, hub.Gatekeeper, hub.Headlamp, hub.HelmPlugin, hub.InspektorGadget, hub.KCL, hub.KedaScaler, hub.Keptn, hub.KnativeClientPlugin, hub.Krew, hub.KubeArmor, hub.Kubewarden, hub.Kyverno, hub.Meshery, hub.OLM, hub.OPA, hub.OpenCost, hub.Radius, hub.TBAction, hub.TektonPipeline, hub.TektonTask, hub.TektonStepAction: 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 }