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 }