hub/internal/tracker/source/helmplugin/helmplugin.go

172 lines
4.6 KiB
Go

package helmplugin
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/Masterminds/semver/v3"
"github.com/artifacthub/hub/internal/hub"
"github.com/artifacthub/hub/internal/license"
"github.com/artifacthub/hub/internal/pkg"
"github.com/artifacthub/hub/internal/util"
"github.com/hashicorp/go-multierror"
"helm.sh/helm/v3/pkg/plugin"
"sigs.k8s.io/yaml"
)
var (
// licenseRE is a regular expression used to locate a license file in the
// repository.
licenseRE = regexp.MustCompile(`(?i)license.*`)
)
// TrackerSource is a hub.TrackerSource implementation for Helm plugins
// repositories.
type TrackerSource struct {
i *hub.TrackerSourceInput
}
// NewTrackerSource creates a new TrackerSource instance.
func NewTrackerSource(i *hub.TrackerSourceInput) *TrackerSource {
return &TrackerSource{i}
}
// GetPackagesAvailable implements the TrackerSource interface.
func (s *TrackerSource) GetPackagesAvailable() (map[string]*hub.Package, error) {
packagesAvailable := make(map[string]*hub.Package)
// Walk the path provided looking for available packages
err := filepath.Walk(s.i.BasePath, func(pkgPath string, info os.FileInfo, err error) error {
// Return ASAP if context is cancelled
select {
case <-s.i.Svc.Ctx.Done():
return s.i.Svc.Ctx.Err()
default:
}
// If an error is raised while visiting a path or the path is not a
// directory, we skip it
if err != nil || !info.IsDir() {
return nil
}
// Get plugin metadata
pluginMetadataPath := filepath.Join(pkgPath, plugin.PluginFileName)
md, err := GetMetadata(pluginMetadataPath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
s.warn(fmt.Errorf("error getting plugin metadata (path: %s): %w", pluginMetadataPath, err))
}
return nil
}
// Prepare and store package version
p, err := PreparePackage(s.i.Repository, md, pkgPath)
if err != nil {
s.warn(fmt.Errorf("error preparing package %s version %s: %w", md.Name, md.Version, err))
return nil
}
packagesAvailable[pkg.BuildKey(p)] = p
return nil
})
if err != nil {
return nil, err
}
return packagesAvailable, nil
}
// warn is a helper that sends the error provided to the errors collector and
// logs it as a warning.
func (s *TrackerSource) warn(err error) {
s.i.Svc.Logger.Warn().Err(err).Send()
s.i.Svc.Ec.Append(s.i.Repository.RepositoryID, err.Error())
}
// GetManifest reads and parses the plugin metadata file.
func GetMetadata(pkgPath string) (*plugin.Metadata, error) {
data, err := os.ReadFile(pkgPath)
if err != nil {
return nil, fmt.Errorf("error reading plugin metadata file: %w", err)
}
var md *plugin.Metadata
if err = yaml.Unmarshal(data, &md); err != nil || md == nil {
return nil, fmt.Errorf("error unmarshaling plugin metadata file: %w", err)
}
if err := validateMetadata(md); err != nil {
return nil, fmt.Errorf("error validating plugin metadata: %w", err)
}
return md, nil
}
// validateMetadata checks if the plugin metadata provided is valid.
func validateMetadata(md *plugin.Metadata) error {
var errs *multierror.Error
if md.Name == "" {
errs = multierror.Append(errs, errors.New("name not provided"))
}
if md.Version == "" {
errs = multierror.Append(errs, errors.New("version not provided"))
} else if _, err := semver.NewVersion(md.Version); err != nil {
errs = multierror.Append(errs, fmt.Errorf("invalid version (semver expected): %w", err))
}
if md.Description == "" {
errs = multierror.Append(errs, errors.New("description not provided"))
}
return errs.ErrorOrNil()
}
// PreparePackage prepares a package version using the plugin metadata and the
// files in the path provided.
func PreparePackage(r *hub.Repository, md *plugin.Metadata, pkgPath string) (*hub.Package, error) {
// Parse version (previously validated)
sv, _ := semver.NewVersion(md.Version)
version := sv.String()
// Prepare package from metadata
p := &hub.Package{
Name: md.Name,
Version: version,
Description: md.Description,
Keywords: []string{
"helm",
"helm-plugin",
},
Links: []*hub.Link{
{
Name: "Source",
URL: r.URL,
},
},
Repository: r,
}
// Include readme file if available
readme, err := util.ReadRegularFile(filepath.Join(pkgPath, "README.md"))
if err == nil {
p.Readme = string(readme)
}
// Process and include license if available
files, err := os.ReadDir(pkgPath)
if err == nil {
for _, file := range files {
if licenseRE.Match([]byte(file.Name())) {
licenseFile, err := util.ReadRegularFile(filepath.Join(pkgPath, file.Name()))
if err == nil {
p.License = license.Detect(licenseFile)
break
}
}
}
}
return p, nil
}