hub/internal/tracker/source/tekton/tekton.go

258 lines
7.0 KiB
Go

package tekton
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/artifacthub/hub/internal/hub"
"github.com/artifacthub/hub/internal/pkg"
"github.com/artifacthub/hub/internal/repo"
"github.com/artifacthub/hub/internal/tracker/source"
"github.com/ghodss/yaml"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
)
const (
changesAnnotation = "artifacthub.io/changes"
licenseAnnotation = "artifacthub.io/license"
linksAnnotation = "artifacthub.io/links"
maintainersAnnotation = "artifacthub.io/maintainers"
providerAnnotation = "artifacthub.io/provider"
recommendationsAnnotation = "artifacthub.io/recommendations"
)
// TrackerSource is a hub.TrackerSource implementation for Tekton 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 package manifest
manifest, manifestRaw, err := getManifest(pkgPath)
if err != nil {
s.warn(fmt.Errorf("error getting package manifest: %w", err))
return nil
}
if manifest == nil {
// Package manifest not found, not a package path
return nil
}
// Parse and validate version
versionLabel := manifest.Labels["app.kubernetes.io/version"]
sv, err := semver.NewVersion(versionLabel)
if err != nil {
s.warn(fmt.Errorf("invalid package (%s) version (%s): %w", manifest.Name, versionLabel, err))
return nil
}
// Prepare and store package version
p, err := s.preparePackage(s.i.Repository, manifest, manifestRaw, pkgPath, sv.String())
if err != nil {
s.warn(fmt.Errorf("error preparing package: %w", err))
return nil
}
packagesAvailable[pkg.BuildKey(p)] = p
return nil
})
if err != nil {
return nil, err
}
return packagesAvailable, nil
}
// preparePackage prepares a package version using the package manifest and the
// files in the path provided.
func (s *TrackerSource) preparePackage(
r *hub.Repository,
manifest *v1beta1.Task,
manifestRaw []byte,
pkgPath,
version string,
) (*hub.Package, error) {
// Prepare content and source urls
var repoBaseURL, pkgsPath, provider string
matches := repo.GitRepoURLRE.FindStringSubmatch(r.URL)
if len(matches) >= 3 {
repoBaseURL = matches[1]
provider = matches[2]
}
if len(matches) == 4 {
pkgsPath = strings.TrimSuffix(matches[3], "/")
}
var blobPath, rawPath string
switch provider {
case "github":
blobPath = "blob"
rawPath = "raw"
case "gitlab":
blobPath = "-/blob"
rawPath = "-/raw"
}
branch := repo.GetBranch(r)
pkgVersionPath := strings.TrimPrefix(pkgPath, s.i.BasePath)
contentURL := fmt.Sprintf("%s/%s/%s/%s%s/%s.yaml",
repoBaseURL, rawPath, branch, pkgsPath, pkgVersionPath, manifest.Name)
sourceURL := fmt.Sprintf("%s/%s/%s/%s%s/%s.yaml",
repoBaseURL, blobPath, branch, pkgsPath, pkgVersionPath, manifest.Name)
// Prepare keywords
keywords := []string{
"tekton",
"task",
}
tags := strings.Split(manifest.Annotations["tekton.dev/tags"], ",")
for _, tag := range tags {
keywords = append(keywords, strings.TrimSpace(tag))
}
// Prepare package from manifest
p := &hub.Package{
Name: manifest.Name,
Version: version,
DisplayName: manifest.Annotations["tekton.dev/displayName"],
Description: manifest.Spec.Description,
Keywords: keywords,
ContentURL: contentURL,
Repository: r,
Links: []*hub.Link{
{
Name: "source",
URL: sourceURL,
},
},
Data: map[string]interface{}{
"manifestRaw": string(manifestRaw),
"pipelines.minVersion": manifest.Annotations["tekton.dev/pipelines.minVersion"],
},
}
// Include readme file if available
readme, err := ioutil.ReadFile(filepath.Join(pkgPath, "README.md"))
if err == nil {
p.Readme = string(readme)
}
// Enrich package with information from annotations
if err := enrichPackageFromAnnotations(p, manifest.Annotations); err != nil {
return nil, fmt.Errorf("error enriching package %s version %s: %w", manifest.Name, version, err)
}
return p, 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 package manifest.
func getManifest(pkgPath string) (*v1beta1.Task, []byte, error) {
// Locate manifest file
matches, err := filepath.Glob(filepath.Join(pkgPath, "*.yaml"))
if err != nil {
return nil, nil, fmt.Errorf("error locating manifest file: %w", err)
}
if len(matches) != 1 {
return nil, nil, nil
}
// Process matches, returning the first valid resource manifest found
for _, match := range matches {
// Read and parse manifest file
manifestData, err := ioutil.ReadFile(match)
if err != nil {
continue
}
manifest := &v1beta1.Task{}
if err = yaml.Unmarshal(manifestData, &manifest); err != nil {
continue
}
if manifest.Kind != "Task" && manifest.Kind != "ClusterTask" {
continue
}
return manifest, manifestData, nil
}
return nil, nil, nil
}
// enrichPackageFromAnnotations adds some extra information to the package from
// the provided annotations.
func enrichPackageFromAnnotations(p *hub.Package, annotations map[string]string) error {
// Changes
if v, ok := annotations[changesAnnotation]; ok {
changes, err := source.ParseChangesAnnotation(v)
if err != nil {
return err
}
p.Changes = changes
}
// License
p.License = annotations[licenseAnnotation]
// Links
if v, ok := annotations[linksAnnotation]; ok {
var links []*hub.Link
if err := yaml.Unmarshal([]byte(v), &links); err != nil {
return fmt.Errorf("invalid links value: %s", v)
}
p.Links = append(p.Links, links...)
}
// Maintainers
if v, ok := annotations[maintainersAnnotation]; ok {
var maintainers []*hub.Maintainer
if err := yaml.Unmarshal([]byte(v), &maintainers); err != nil {
return fmt.Errorf("invalid maintainers value: %s", v)
}
p.Maintainers = maintainers
}
// Provider
p.Provider = annotations[providerAnnotation]
// Recommendations
if v, ok := annotations[recommendationsAnnotation]; ok {
var recommendations []*hub.Recommendation
if err := yaml.Unmarshal([]byte(v), &recommendations); err != nil {
return fmt.Errorf("invalid recommendations value: %s", v)
}
p.Recommendations = recommendations
}
return nil
}