get.porter.sh/porter@v1.3.0/pkg/pkgmgmt/feed/generate.go (about)

     1  package feed
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"regexp"
     8  	"sort"
     9  	"time"
    10  
    11  	"get.porter.sh/porter/pkg"
    12  	"get.porter.sh/porter/pkg/portercontext"
    13  	"get.porter.sh/porter/pkg/tracing"
    14  	"github.com/Masterminds/semver/v3"
    15  	"github.com/cbroglie/mustache"
    16  )
    17  
    18  type GenerateOptions struct {
    19  	SearchDirectory string
    20  	AtomFile        string
    21  	TemplateFile    string
    22  }
    23  
    24  func (o *GenerateOptions) Validate(c *portercontext.Context) error {
    25  	err := o.ValidateSearchDirectory(c)
    26  	if err != nil {
    27  		return err
    28  	}
    29  
    30  	return o.ValidateTemplateFile(c)
    31  }
    32  
    33  func (o *GenerateOptions) ValidateSearchDirectory(cxt *portercontext.Context) error {
    34  	if o.SearchDirectory == "" {
    35  		o.SearchDirectory = cxt.Getwd()
    36  	}
    37  
    38  	if _, err := cxt.FileSystem.Stat(o.SearchDirectory); err != nil {
    39  		return fmt.Errorf("invalid --dir %s: %w", o.SearchDirectory, err)
    40  	}
    41  
    42  	return nil
    43  }
    44  
    45  func (o *GenerateOptions) ValidateTemplateFile(cxt *portercontext.Context) error {
    46  	if _, err := cxt.FileSystem.Stat(o.TemplateFile); err != nil {
    47  		return fmt.Errorf("invalid --template %s: %w", o.TemplateFile, err)
    48  	}
    49  
    50  	return nil
    51  }
    52  
    53  func (feed *MixinFeed) Generate(ctx context.Context, opts GenerateOptions) error {
    54  	ctx, span := tracing.StartSpan(ctx)
    55  	defer span.EndSpan()
    56  
    57  	existingFeed, err := feed.FileSystem.Exists(opts.AtomFile)
    58  	if err != nil {
    59  		return err
    60  	}
    61  	if existingFeed {
    62  		err := feed.Load(ctx, opts.AtomFile)
    63  		if err != nil {
    64  			return err
    65  		}
    66  	}
    67  
    68  	mixinRegex := regexp.MustCompile(`(.*[/\\])?(.+)[/\\]([a-z0-9-]+)-(linux|windows|darwin)-(amd64|arm64)(\.exe)?`)
    69  
    70  	err = feed.FileSystem.Walk(opts.SearchDirectory, func(path string, info os.FileInfo, err error) error {
    71  		if err != nil {
    72  			return err
    73  		}
    74  
    75  		matches := mixinRegex.FindStringSubmatch(path)
    76  		if len(matches) > 0 {
    77  			version := matches[2]
    78  
    79  			if !shouldPublishVersion(version) {
    80  				return nil
    81  			}
    82  
    83  			mixin := matches[3]
    84  			filename := info.Name()
    85  			updated := info.ModTime()
    86  
    87  			_, ok := feed.Index[mixin]
    88  			if !ok {
    89  				feed.Index[mixin] = map[string]*MixinFileset{}
    90  			}
    91  
    92  			_, ok = feed.Index[mixin][version]
    93  			if !ok {
    94  				fileset := MixinFileset{
    95  					Mixin:   mixin,
    96  					Version: version,
    97  				}
    98  
    99  				feed.Index[mixin][version] = &fileset
   100  			}
   101  
   102  			for i := range feed.Index[mixin][version].Files {
   103  				mixinFile := feed.Index[mixin][version].Files[i]
   104  				if mixinFile.File == filename {
   105  					if mixinFile.Updated.Before(updated) {
   106  						mixinFile.Updated = updated
   107  					}
   108  
   109  					return nil
   110  				}
   111  			}
   112  
   113  			feed.Index[mixin][version].Files = append(feed.Index[mixin][version].Files, &MixinFile{File: filename, Updated: updated})
   114  		}
   115  
   116  		return nil
   117  	})
   118  
   119  	if err != nil {
   120  		return span.Error(fmt.Errorf("failed to traverse the %s directory: %w", opts.SearchDirectory, err))
   121  	}
   122  
   123  	if len(feed.Index) == 0 {
   124  		return span.Error(fmt.Errorf("no mixin binaries found in %s matching the regex %q", opts.SearchDirectory, mixinRegex))
   125  	}
   126  
   127  	return nil
   128  }
   129  
   130  var versionRegex = regexp.MustCompile(`\d+-g[a-z0-9]+`)
   131  
   132  // As a safety measure, skip versions that shouldn't be put in the feed, we only want canary and tagged releases.
   133  func shouldPublishVersion(version string) bool {
   134  	// Publish canary permalinks, for now ignore canary-v1
   135  	if version == "canary" {
   136  		return true
   137  	}
   138  
   139  	v, err := semver.NewVersion(version)
   140  	if err != nil {
   141  		// If it's not a version, don't publish
   142  		return false
   143  	}
   144  
   145  	// Check if this is an untagged version, i.e. the output of git describe, v1.2.3-2-ga1b3c5
   146  	untagged := versionRegex.MatchString(v.Prerelease())
   147  	return !untagged
   148  }
   149  
   150  func (feed *MixinFeed) Save(opts GenerateOptions) error {
   151  	feedTmpl, err := feed.FileSystem.ReadFile(opts.TemplateFile)
   152  	if err != nil {
   153  		return fmt.Errorf("error reading template file at %s: %w", opts.TemplateFile, err)
   154  	}
   155  
   156  	tmplData := map[string]interface{}{}
   157  	mixins := make([]string, 0, len(feed.Index))
   158  	entries := make(MixinEntries, 0, len(feed.Index))
   159  	for m, versions := range feed.Index {
   160  		mixins = append(mixins, m)
   161  		for _, fileset := range versions {
   162  			entries = append(entries, fileset)
   163  		}
   164  	}
   165  	sort.Sort(sort.Reverse(entries))
   166  
   167  	sort.Strings(mixins)
   168  
   169  	tmplData["Mixins"] = mixins
   170  	tmplData["Entries"] = entries
   171  	tmplData["Updated"] = entries[0].Updated()
   172  
   173  	atomXml, err := mustache.Render(string(feedTmpl), tmplData)
   174  	if err != nil {
   175  		return fmt.Errorf("error rendering template:%w", err)
   176  	}
   177  	err = feed.FileSystem.WriteFile(opts.AtomFile, []byte(atomXml), pkg.FileModeWritable)
   178  	if err != nil {
   179  		return fmt.Errorf("could not write feed to %s: %w", opts.AtomFile, err)
   180  	}
   181  
   182  	fmt.Fprintf(feed.Out, "wrote feed to %s\n", opts.AtomFile)
   183  	return nil
   184  }
   185  
   186  func toAtomTimestamp(t time.Time) string {
   187  	return t.UTC().Format(time.RFC3339)
   188  }