-
Notifications
You must be signed in to change notification settings - Fork 212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generate a mixin feed based on a directory #279
Changes from 2 commits
9f76ac3
961d2dc
de542d0
4720c36
eeaaae1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package feed | ||
|
||
type MixinFeed struct { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
package feed | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"regexp" | ||
"sort" | ||
"time" | ||
|
||
"github.com/cbroglie/mustache" | ||
"github.com/deislabs/porter/pkg/context" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
type GenerateOptions struct { | ||
SearchDirectory string | ||
AtomFile string | ||
TemplateFile string | ||
} | ||
|
||
func (o *GenerateOptions) Validate(c *context.Context) error { | ||
err := o.ValidateSearchDirectory(c) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return o.ValidateTemplateFile(c) | ||
} | ||
|
||
func (o *GenerateOptions) ValidateSearchDirectory(cxt *context.Context) error { | ||
if o.SearchDirectory == "" { | ||
wd, err := os.Getwd() | ||
if err != nil { | ||
return errors.Wrap(err, "could not get current working directory") | ||
} | ||
|
||
o.SearchDirectory = wd | ||
} | ||
|
||
if _, err := cxt.FileSystem.Stat(o.SearchDirectory); err != nil { | ||
return errors.Wrapf(err, "invalid --directory %s", o.SearchDirectory) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (o *GenerateOptions) ValidateTemplateFile(cxt *context.Context) error { | ||
if _, err := cxt.FileSystem.Stat(o.TemplateFile); err != nil { | ||
return errors.Wrapf(err, "invalid --template %s", o.TemplateFile) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type mixinFileset struct { | ||
Mixin string | ||
Version string | ||
Files []mixinFile | ||
} | ||
|
||
func (f *mixinFileset) Updated() string { | ||
return toAtomTimestamp(f.GetLastUpdated()) | ||
} | ||
|
||
func (f *mixinFileset) GetLastUpdated() time.Time { | ||
var max time.Time | ||
for _, f := range f.Files { | ||
if f.Updated.After(max) { | ||
max = f.Updated | ||
} | ||
} | ||
return max | ||
} | ||
|
||
type mixinEntries []*mixinFileset | ||
|
||
func (e mixinEntries) Len() int { | ||
return len(e) | ||
} | ||
|
||
func (e mixinEntries) Swap(i, j int) { | ||
e[i], e[j] = e[j], e[i] | ||
} | ||
|
||
func (e mixinEntries) Less(i, j int) bool { | ||
return e[i].GetLastUpdated().Before(e[j].GetLastUpdated()) | ||
} | ||
|
||
type mixinFile struct { | ||
File string | ||
Updated time.Time | ||
} | ||
|
||
type mixinFeed map[string]map[string]*mixinFileset | ||
|
||
func Generate(opts GenerateOptions, cxt *context.Context) error { | ||
feedTmpl, err := cxt.FileSystem.ReadFile(opts.TemplateFile) | ||
if err != nil { | ||
return errors.Wrapf(err, "error reading template file at %s", opts.TemplateFile) | ||
} | ||
|
||
mixinRegex := regexp.MustCompile(`(.*/)?(.+)/([a-z]+)-([a-z0-9]+)-([a-z0-9]+)(\.exe)?`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to restrict some of these regex fields to only the currently supported values? (For instance, OS can be only one of I can also understand wanting to keep it unrestricted with a vision towards supporting other variants... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I originally didn't want to be in the business of keeping up to date with our build matrix but honestly, it is not going to change often. So we just need to be aware that this is here. I'll update it and leave a note in the makefile's build matrix that this should be updated too when it's changed. |
||
|
||
feed := mixinFeed{} | ||
found := 0 | ||
err = cxt.FileSystem.Walk(opts.SearchDirectory, func(path string, info os.FileInfo, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
|
||
matches := mixinRegex.FindStringSubmatch(path) | ||
if len(matches) > 0 { | ||
version := matches[2] | ||
mixin := matches[3] | ||
filename := info.Name() | ||
|
||
versions, ok := feed[mixin] | ||
if !ok { | ||
versions = map[string]*mixinFileset{} | ||
feed[mixin] = versions | ||
} | ||
|
||
fileset, ok := versions[version] | ||
if !ok { | ||
fileset = &mixinFileset{ | ||
Mixin: mixin, | ||
Version: version, | ||
} | ||
versions[version] = fileset | ||
found++ | ||
} | ||
fileset.Files = append(fileset.Files, mixinFile{File: filename, Updated: info.ModTime()}) | ||
} | ||
|
||
return nil | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
tmplData := map[string]interface{}{} | ||
mixins := make([]string, 0, len(feed)) | ||
entries := make(mixinEntries, 0, found) | ||
for m, versions := range feed { | ||
mixins = append(mixins, m) | ||
for _, fileset := range versions { | ||
entries = append(entries, fileset) | ||
} | ||
} | ||
sort.Sort(sort.Reverse(entries)) | ||
|
||
tmplData["Mixins"] = mixins | ||
tmplData["Entries"] = entries | ||
tmplData["Updated"] = entries[0].Updated() | ||
|
||
atomXml, err := mustache.Render(string(feedTmpl), tmplData) | ||
err = cxt.FileSystem.WriteFile(opts.AtomFile, []byte(atomXml), 0644) | ||
if err != nil { | ||
return errors.Wrapf(err, "could not write feed to %s", opts.AtomFile) | ||
} | ||
|
||
fmt.Fprintf(cxt.Out, "wrote feed to %s\n", opts.AtomFile) | ||
return nil | ||
} | ||
|
||
func toAtomTimestamp(t time.Time) string { | ||
return t.UTC().Format(time.RFC3339) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package feed | ||
|
||
import ( | ||
"io/ioutil" | ||
"sort" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/deislabs/porter/pkg/context" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestGenerate(t *testing.T) { | ||
tc := context.NewTestContext(t) | ||
tc.AddTestFile("testdata/atom-template.xml", "template.xml") | ||
|
||
tc.FileSystem.Create("bin/v1.2.3/helm-darwin-amd64") | ||
tc.FileSystem.Create("bin/v1.2.3/helm-linux-amd64") | ||
tc.FileSystem.Create("bin/v1.2.3/helm-windows-amd64.exe") | ||
|
||
// Force the up3 timestamps to stay the same for each test run | ||
up3, _ := time.Parse("2006-Jan-02", "2013-Feb-03") | ||
tc.FileSystem.Chtimes("bin/v1.2.3/helm-darwin-amd64", up3, up3) | ||
tc.FileSystem.Chtimes("bin/v1.2.3/helm-linux-amd64", up3, up3) | ||
tc.FileSystem.Chtimes("bin/v1.2.3/helm-windows-amd64.exe", up3, up3) | ||
|
||
tc.FileSystem.Create("bin/v1.2.4/helm-darwin-amd64") | ||
tc.FileSystem.Create("bin/v1.2.4/helm-linux-amd64") | ||
tc.FileSystem.Create("bin/v1.2.4/helm-windows-amd64.exe") | ||
|
||
up4, _ := time.Parse("2006-Jan-02", "2013-Feb-04") | ||
tc.FileSystem.Chtimes("bin/v1.2.4/helm-darwin-amd64", up4, up4) | ||
tc.FileSystem.Chtimes("bin/v1.2.4/helm-linux-amd64", up4, up4) | ||
tc.FileSystem.Chtimes("bin/v1.2.4/helm-windows-amd64.exe", up4, up4) | ||
|
||
tc.FileSystem.Create("bin/v1.2.3/exec-darwin-amd64") | ||
tc.FileSystem.Create("bin/v1.2.3/exec-linux-amd64") | ||
tc.FileSystem.Create("bin/v1.2.3/exec-windows-amd64.exe") | ||
|
||
up2, _ := time.Parse("2006-Jan-02", "2013-Feb-02") | ||
tc.FileSystem.Chtimes("bin/v1.2.3/exec-darwin-amd64", up2, up2) | ||
tc.FileSystem.Chtimes("bin/v1.2.3/exec-linux-amd64", up2, up2) | ||
tc.FileSystem.Chtimes("bin/v1.2.3/exec-windows-amd64.exe", up2, up2) | ||
|
||
opts := GenerateOptions{ | ||
AtomFile: "atom.xml", | ||
SearchDirectory: "bin", | ||
TemplateFile: "template.xml", | ||
} | ||
err := Generate(opts, tc.Context) | ||
require.NoError(t, err) | ||
|
||
b, err := tc.FileSystem.ReadFile("atom.xml") | ||
require.NoError(t, err) | ||
gotXml := string(b) | ||
|
||
b, err = ioutil.ReadFile("testdata/atom.xml") | ||
require.NoError(t, err) | ||
wantXml := string(b) | ||
|
||
assert.Equal(t, wantXml, gotXml) | ||
} | ||
|
||
func TestMixinEntries_Sort(t *testing.T) { | ||
up2, _ := time.Parse("2006-Jan-02", "2013-Feb-02") | ||
up3, _ := time.Parse("2006-Jan-02", "2013-Feb-03") | ||
up4, _ := time.Parse("2006-Jan-02", "2013-Feb-04") | ||
|
||
entries := mixinEntries{ | ||
{ | ||
Files: []mixinFile{ | ||
{Updated: up3}, | ||
}, | ||
}, | ||
{ | ||
Files: []mixinFile{ | ||
{Updated: up2}, | ||
}, | ||
}, | ||
{ | ||
Files: []mixinFile{ | ||
{Updated: up4}, | ||
}, | ||
}, | ||
} | ||
|
||
sort.Sort(sort.Reverse(entries)) | ||
|
||
assert.Equal(t, up4, entries[0].Files[0].Updated) | ||
assert.Equal(t, up3, entries[1].Files[0].Updated) | ||
assert.Equal(t, up2, entries[2].Files[0].Updated) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: should we print
invalid --dir
orinvalid -d
to represent the flag strings supported? Or perhaps justinvalid directory
for a more general error msg?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, yes thanks it should be
--dir