diff --git a/main.go b/main.go index d00a8d8..8a2816c 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "os" "path" + "github.com/apex/log" "github.com/rancher/wrangler/pkg/signals" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" @@ -44,11 +45,11 @@ func main() { app.Commands = common.GetCommands() app.CommandNotFound = func(context *cli.Context, command string) { - logrus.Fatalf("command %s not found.", command) + log.Fatalf("command %s not found.", command) } ctx := signals.SetupSignalContext() if err := app.RunContext(ctx, os.Args); err != nil { - logrus.Fatal(err) + log.Error(err.Error()) } } diff --git a/pkg/asset/asset.go b/pkg/asset/asset.go index e8f49a2..df61198 100644 --- a/pkg/asset/asset.go +++ b/pkg/asset/asset.go @@ -17,7 +17,6 @@ import ( "github.com/gabriel-vasile/mimetype" "github.com/h2non/filetype" "github.com/h2non/filetype/matchers" - "github.com/h2non/filetype/types" "github.com/krolaw/zipstream" "github.com/sirupsen/logrus" "github.com/xi2/xz" @@ -38,11 +37,13 @@ var ( ignoreFileExtensions = []string{ ".txt", ".sbom", + ".json", } executableMimetypes = []string{ "application/x-mach-binary", "application/x-executable", + "application/x-elf", "application/vnd.microsoft.portable-executable", } ) @@ -73,7 +74,6 @@ func New(name, displayName, osName, osArch, version string) *Asset { Arch: osArch, Version: version, Files: make([]*File, 0), - score: 0, } a.Classify() @@ -101,8 +101,6 @@ type Asset struct { Hash string TempDir string Files []*File - - score int } func (a *Asset) ID() string { @@ -117,10 +115,6 @@ func (a *Asset) GetDisplayName() string { return a.DisplayName } -func (a *Asset) GetScore() int { - return a.score -} - func (a *Asset) GetType() Type { return a.Type } @@ -144,12 +138,6 @@ func (a *Asset) GetFilePath() string { return a.DownloadPath } -type ScoreOptions struct { - OS []string - Arch []string - Extensions []string -} - // Classify determines the type of asset based on the file extension func (a *Asset) Classify() { //nolint:gocyclo if ext := strings.TrimPrefix(filepath.Ext(a.Name), "."); ext != "" { @@ -194,55 +182,6 @@ func (a *Asset) Classify() { //nolint:gocyclo logrus.Tracef("classified: %s (%d)", a.Name, a.Type) } -// Score returns the score of the asset based on the options provided -func (a *Asset) Score(opts *ScoreOptions) int { - var scoringValues = make(map[string]int) - - // Note: if it has the word "update" in it, we want to deprioritize it as it's likely an update binary from - // a rust or go binary distribution - scoringValues["update"] = -20 - - for _, os1 := range opts.OS { - scoringValues[strings.ToLower(os1)] = 10 - } - for _, arch := range opts.Arch { - scoringValues[strings.ToLower(arch)] = 5 - } - for _, ext := range opts.Extensions { - scoringValues[strings.ToLower(ext)] = 15 - } - - if !a.IsSupportedExtension() { - a.score = -1 - return a.score - } - - for keyMatch, keyScore := range scoringValues { - if strings.Contains(strings.ToLower(a.Name), keyMatch) { - a.score += keyScore - } - } - - return a.score -} - -func (a *Asset) IsSupportedExtension() bool { - if ext := strings.TrimPrefix(filepath.Ext(a.Name), "."); ext != "" { - switch filetype.GetType(ext) { - case matchers.TypeGz, types.Unknown, matchers.TypeZip, matchers.TypeXz, matchers.TypeTar, matchers.TypeBz2, matchers.TypeExe: - break - case msiType, matchers.TypeDeb, matchers.TypeRpm, ascType: - logrus.Debugf("filename %s doesn't have a supported extension", a.Name) - return false - default: - logrus.Debugf("filename %s doesn't have a supported extension", a.Name) - return false - } - } - - return true -} - func (a *Asset) copyFile(srcFile, dstFile string) error { // Open the source file for reading src, err := os.Open(srcFile) @@ -280,6 +219,8 @@ func (a *Asset) Install(id, binDir string) error { return err } + logrus.Debug("found mimetype: ", m.String()) + if slices.Contains(ignoreFileExtensions, m.Extension()) { logrus.Tracef("ignoring file: %s", file.Name) continue @@ -330,7 +271,9 @@ func (a *Asset) Install(id, binDir string) error { // TODO: allow override if runtime.GOOS == a.OS && runtime.GOARCH == a.Arch { logrus.Debugf("creating symlink: %s to %s", defaultBinFilename, destBinFilename) + logrus.Debugf("creating symlink: %s to %s", versionedBinFilename, destBinFilename) _ = os.Remove(defaultBinFilename) + _ = os.Remove(versionedBinFilename) _ = os.Symlink(destBinFilename, defaultBinFilename) _ = os.Symlink(destBinFilename, versionedBinFilename) } @@ -414,6 +357,7 @@ func (a *Asset) doExtract(in io.Reader) error { } func (a *Asset) processDirect(in io.Reader) (io.Reader, error) { + logrus.Tracef("processing direct file") outFile, err := os.Create(filepath.Join(a.TempDir, filepath.Base(a.DownloadPath))) if err != nil { return nil, err @@ -484,6 +428,7 @@ func (a *Asset) processZip(in io.Reader) (io.Reader, error) { } func (a *Asset) processTar(in io.Reader) (io.Reader, error) { + logrus.Trace("processing tar file") tr := tar.NewReader(in) a.Files = make([]*File, 0) diff --git a/pkg/asset/asset_test.go b/pkg/asset/asset_test.go index 742bff5..875716a 100644 --- a/pkg/asset/asset_test.go +++ b/pkg/asset/asset_test.go @@ -9,88 +9,36 @@ import ( "fmt" "io" "os" + "path/filepath" + "runtime" + "slices" "strings" "testing" "github.com/dsnet/compress/bzip2" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/ulikunitz/xz" ) -func TestAsset(t *testing.T) { - cases := []struct { - name string - displayName string - expectType Type - expectScore int - }{ - {"test", "Test", Unknown, 0}, - {"test.tar.gz", "Test", Archive, 0}, - {"test.tar.gz.asc", "Test", Signature, 0}, - {"dist.tar.gz.sig", "dist.tar.gz.sig", Signature, 0}, - } - - for _, c := range cases { - asset := New(c.name, c.displayName, "linux", "amd64", "1.0.0") - - if asset.GetName() != c.name { - t.Errorf("expected name to be %s, got %s", c.name, asset.GetName()) - } - if asset.GetDisplayName() != c.displayName { - t.Errorf("expected display name to be %s, got %s", c.displayName, asset.GetDisplayName()) - } - if asset.Type != c.expectType { - t.Errorf("expected type to be %d, got %d", c.expectType, asset.Type) - } - if asset.score != c.expectScore { - t.Errorf("expected score to be %d, got %d", c.expectScore, asset.score) - } - } +func init() { + logrus.SetLevel(logrus.TraceLevel) } -func TestAssetScoring(t *testing.T) { +func TestAsset(t *testing.T) { cases := []struct { name string displayName string - scoringOpts *ScoreOptions expectType Type - expectScore int }{ - { - "test.tar.gz", - "Test", - &ScoreOptions{OS: []string{"linux"}, Arch: []string{"amd64"}, Extensions: []string{".tar.gz"}}, - Archive, - 15, - }, - { - "test_amd64.tar.gz", - "Test", - &ScoreOptions{OS: []string{"linux"}, Arch: []string{"amd64"}, Extensions: []string{".tar.gz"}}, - Archive, - 20, - }, - { - "test_linux_amd64.tar.gz", - "Test", - &ScoreOptions{OS: []string{"linux"}, Arch: []string{"amd64"}, Extensions: []string{"unknown", ".tar.gz"}}, - Archive, - 30, - }, - { - "test_linux_x86_64.tar.gz", - "Test", - &ScoreOptions{ - OS: []string{"Linux"}, Arch: []string{"amd64", "x86_64"}, Extensions: []string{"unknown", ".tar.gz"}, - }, - Archive, - 30, - }, + {"test", "Test", Unknown}, + {"test.tar.gz", "Test", Archive}, + {"test.tar.gz.asc", "Test", Signature}, + {"dist.tar.gz.sig", "dist.tar.gz.sig", Signature}, } for _, c := range cases { asset := New(c.name, c.displayName, "linux", "amd64", "1.0.0") - asset.Score(c.scoringOpts) if asset.GetName() != c.name { t.Errorf("expected name to be %s, got %s", c.name, asset.GetName()) @@ -101,21 +49,16 @@ func TestAssetScoring(t *testing.T) { if asset.Type != c.expectType { t.Errorf("expected type to be %d, got %d", c.expectType, asset.Type) } - if asset.score != c.expectScore { - t.Errorf("expected score to be %d, got %d", c.expectScore, asset.score) - } } } -func TestDefaultAsset(t *testing.T) { +func TestAssetDefaults(t *testing.T) { asset := New("dist-linux-amd64.tar.gz", "dist-linux-amd64.tar.gz", "linux", "amd64", "1.0.0") - asset.Score(&ScoreOptions{OS: []string{"linux"}, Arch: []string{"amd64"}, Extensions: []string{".tar.gz"}}) err := asset.Download(context.TODO()) assert.Error(t, err) assert.Equal(t, Archive, asset.GetType()) assert.Equal(t, "not-implemented", asset.ID()) - assert.Equal(t, 30, asset.GetScore()) assert.Equal(t, "dist-linux-amd64.tar.gz", asset.GetDisplayName()) assert.Equal(t, "dist-linux-amd64.tar.gz", asset.GetName()) assert.Equal(t, "", asset.GetFilePath()) @@ -201,48 +144,126 @@ func TestAssetTypes(t *testing.T) { } } +type internalFile struct { + name string + mode int64 + content []byte +} + func TestAssetExtract(t *testing.T) { cases := []struct { - name string - fileType Type - internalFile string - downloadFile string + name string + fileType Type + downloadFile string + expectedFiles []string + expectError bool }{ { - name: "dist-linux-amd64.tar.gz", - fileType: Archive, - internalFile: "test-file", - downloadFile: createTarGz(t, "test-file", "This is a test file content"), + name: "dist-linux-amd64.tar.gz", + fileType: Archive, + downloadFile: createTarGz(t, []internalFile{ + { + name: "test-file", + mode: 0755, + content: []byte{0x7F, 0x45, 0x4C, 0x46}, + }, + }), + expectedFiles: []string{ + "test-file", + }, }, { name: "dist-linux-amd64.zip", fileType: Archive, - internalFile: "test-file", - downloadFile: createZip(t, "test-file", "This is a test file content"), + downloadFile: createZip(t, "test-file", []byte{0x7F, 0x45, 0x4C, 0x46}), + expectedFiles: []string{ + "test-file", + "docs/readme.md", + }, }, { - name: "dist-linux-amd64.tar.bz2", - fileType: Archive, - internalFile: "test-file", - downloadFile: createTarBz2(t, "test-file", "This is a test file content"), + name: "dist-linux-amd64.tar.bz2", + fileType: Archive, + downloadFile: createTarBz2(t, []internalFile{ + { + name: "test-file", + mode: 0755, + content: []byte{0x7F, 0x45, 0x4C, 0x46}, + }, + }), + expectedFiles: []string{ + "test-file", + }, }, { - name: "dist-linux-amd64.tar.xz", - fileType: Archive, - internalFile: "test-file", - downloadFile: createTarXz(t, "test-file", "This is a test file content"), + name: "dist-linux-amd64.tar.xz", + fileType: Archive, + downloadFile: createTarXz(t, []internalFile{ + { + name: "test-file", + mode: 0644, + content: []byte("This is a test file content"), + }, + }), + expectedFiles: []string{ + "test-file", + }, }, { name: "dist-linux-amd64", fileType: Binary, - internalFile: "test-*", - downloadFile: createFile(t, "This is a test file content"), + downloadFile: createFile(t, []byte("This is a test file content")), + expectedFiles: []string{ + "dist-linux-amd64", + }, }, { name: "windows-executable", fileType: Binary, - internalFile: "test-*", - downloadFile: createFile(t, "This is a test file content"), + downloadFile: createFile(t, []byte("This is a test file content")), + expectedFiles: []string{ + "windows-executable", + }, + }, + { + name: "dist-linux-multi-amd64.tar.gz", + fileType: Archive, + downloadFile: createTarGz(t, []internalFile{ + { + name: "bin1", + mode: 0755, + content: []byte{0x7F, 0x45, 0x4C, 0x46}, + }, + { + name: "bin2", + mode: 0755, + content: []byte{0x7F, 0x45, 0x4C, 0x46}, + }, + { + name: "docs/readme.md", + mode: 0600, + content: []byte("this is a readme"), + }, + }), + expectedFiles: []string{ + "bin1", + "bin2", + "docs/readme.md", + }, + }, + { + name: "empty.zip", + fileType: Archive, + downloadFile: createEmptyZip(t), + expectError: true, + }, + { + name: "empty.tar.gz", + fileType: Archive, + downloadFile: createTarGz(t, []internalFile{}), + expectedFiles: []string{ + "test-*.tar.gz", + }, }, } @@ -260,66 +281,314 @@ func TestAssetExtract(t *testing.T) { }(c.downloadFile) err := asset.Extract() + if c.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Len(t, asset.Files, len(c.expectedFiles)) + + for i, f := range asset.Files { + if slices.Contains(c.expectedFiles, f.Name) { + assert.Equal(t, c.expectedFiles[i], f.Name) + } + } + }) + } +} + +func TestAssetInstall(t *testing.T) { + cases := []struct { + name string + os string + arch string + fileType Type + downloadFile string + expectedFiles []string + expectError bool + }{ + { + name: "dist-linux-amd64.tar.gz", + os: "linux", + arch: "amd64", + fileType: Archive, + downloadFile: createTarGz(t, []internalFile{ + { + name: "test-binary", + mode: 0755, + content: []byte{0x7F, 0x45, 0x4C, 0x46}, + }, + }), + expectedFiles: []string{ + "test-binary", + }, + }, + { + name: "dist-darwin-amd64.tar.gz", + os: "darwin", + arch: "amd64", + downloadFile: createTarGz(t, []internalFile{ + { + name: "test-binary", + mode: 0755, + content: []byte{0xFE, 0xED, 0xFA, 0xCE}, + }, + }), + expectedFiles: []string{ + "test-binary", + }, + }, + { + name: "dist-linux-amd64.zip", + os: "linux", + arch: "amd64", + fileType: Archive, + downloadFile: createZip(t, "test-binary", []byte{0x7F, 0x45, 0x4C, 0x46}), + expectedFiles: []string{ + "test-binary", + }, + }, + { + name: "dist-linux-amd64.tar.bz2", + os: "linux", + arch: "amd64", + fileType: Archive, + downloadFile: createTarBz2(t, []internalFile{ + { + name: "test-binary", + mode: 0755, + content: []byte{0x7F, 0x45, 0x4C, 0x46}, + }, + }), + expectedFiles: []string{ + "test-binary", + }, + }, + { + name: "dist-linux-amd64.tar.xz", + os: "linux", + arch: "amd64", + fileType: Archive, + downloadFile: createTarXz(t, []internalFile{ + { + name: "test-binary", + mode: 0755, + content: []byte{0x7F, 0x45, 0x4C, 0x46}, + }, + }), + expectedFiles: []string{ + "test-binary", + }, + }, + { + name: "dist-darwin-amd64", + os: "darwin", + arch: "amd64", + fileType: Binary, + downloadFile: createFile(t, []byte{0xFE, 0xED, 0xFA, 0xCE}), + expectedFiles: []string{ + "dist", + }, + }, + { + name: "test.exe", + os: "windows", + arch: "amd64", + fileType: Binary, + downloadFile: createFile(t, []byte{0x4D, 0x5A}), + expectedFiles: []string{ + "test.exe", + }, + }, + { + name: "dist-linux-multi-amd64.tar.gz", + os: "linux", + arch: "amd64", + fileType: Archive, + downloadFile: createTarGz(t, []internalFile{ + { + name: "bin1", + mode: 0755, + content: []byte{0x7F, 0x45, 0x4C, 0x46}, + }, + { + name: "bin2", + mode: 0755, + content: []byte{0x7F, 0x45, 0x4C, 0x46}, + }, + { + name: "docs/readme.md", + mode: 0600, + content: []byte("this is a readme"), + }, + }), + expectedFiles: []string{ + "bin1", + "bin2", + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + // Create a temporary directory for the binary installation + binDir, err := os.MkdirTemp("", "bin") assert.NoError(t, err) + defer os.RemoveAll(binDir) - // Verify the asset - if strings.HasSuffix(c.internalFile, "-*") { - assert.True(t, strings.HasPrefix(asset.Files[0].Name, "test-")) - } else { - assert.Equal(t, c.internalFile, asset.Files[0].Name) + asset := New(c.name, c.name, c.os, c.arch, "1.0.0") + asset.DownloadPath = c.downloadFile + + err = asset.Extract() + assert.NoError(t, err) + + err = asset.Install("test-id", binDir) + assert.NoError(t, err) + + for _, fileName := range c.expectedFiles { + destBinaryName := fmt.Sprintf("test-id-%s", filepath.Base(fileName)) + destBinPath := filepath.Join(binDir, destBinaryName) + + baseLinkName := filepath.Join(binDir, filepath.Base(fileName)) + versionedLinkName := filepath.Join(binDir, fmt.Sprintf("%s@%s", filepath.Base(fileName), "1.0.0")) + + _, err = os.Stat(destBinPath) + assert.NoError(t, err) + + if c.os == runtime.GOOS && c.arch == runtime.GOARCH { + _, err = os.Stat(baseLinkName) + assert.NoError(t, err) + + linkPath, err := os.Readlink(baseLinkName) + assert.NoError(t, err) + assert.Equal(t, destBinaryName, filepath.Base(linkPath)) + + _, err = os.Stat(versionedLinkName) + assert.NoError(t, err) + + linkPath, err = os.Readlink(versionedLinkName) + assert.NoError(t, err) + assert.Equal(t, destBinaryName, filepath.Base(linkPath)) + } } - assert.Equal(t, 1, len(asset.Files)) + _ = filepath.Walk(binDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + fmt.Println(">", path) + + return nil + }) }) } } -func createFile(t *testing.T, content string) string { +// -- helper functions below -- + +// createEmptyZip creates an empty zip file +func createEmptyZip(t *testing.T) string { t.Helper() // Create a temporary file - tmpFile, err := os.CreateTemp("", "test-*") + tmpFile, err := os.CreateTemp("", "test-empty-*.zip") assert.NoError(t, err) defer tmpFile.Close() - _, err = tmpFile.WriteString(content) - assert.NoError(t, err) + // Create a zip writer + zw := zip.NewWriter(tmpFile) + defer zw.Close() return tmpFile.Name() } -func createTarGz(t *testing.T, fileName, content string) string { +// createFile creates a temporary file with the given content +func createFile(t *testing.T, content []byte) string { t.Helper() // Create a temporary file - tmpFile, err := os.CreateTemp("", "test-*.tar.gz") + tmpFile, err := os.CreateTemp("", "test-*") assert.NoError(t, err) defer tmpFile.Close() - // Create a gzip writer - gw := gzip.NewWriter(tmpFile) - defer gw.Close() + _, err = tmpFile.Write(content) + assert.NoError(t, err) + + return tmpFile.Name() +} + +// createTar creates a tar archive with the given files +func createTar(t *testing.T, out io.Writer, files []internalFile) error { + t.Helper() // Create a tar writer - tw := tar.NewWriter(gw) + tw := tar.NewWriter(out) defer tw.Close() - // Add a file to the tar archive - hdr := &tar.Header{ - Name: fileName, - Mode: 0600, - Size: int64(len(content)), + for _, f := range files { + parts := strings.Split(f.name, "/") + if len(parts) > 1 { + for i := 0; i < len(parts)-1; i++ { + // Add a directory to the tar archive + dirHdr := &tar.Header{ + Name: parts[0] + "/", + Mode: 0755, + Size: 0, + } + err := tw.WriteHeader(dirHdr) + if err != nil { + return err + } + } + } + + // Add a file to the tar archive + hdr := &tar.Header{ + Name: f.name, + Mode: f.mode, + Size: int64(len(f.content)), + } + err := tw.WriteHeader(hdr) + if err != nil { + return err + } + + _, err = tw.Write(f.content) + if err != nil { + return err + } } - err = tw.WriteHeader(hdr) + + return nil +} + +// createTarGz creates a tar.gz archive with the given files +func createTarGz(t *testing.T, files []internalFile) string { + t.Helper() + + // Create a temporary file + tmpFile, err := os.CreateTemp("", "test-*.tar.gz") assert.NoError(t, err) + defer tmpFile.Close() - _, err = tw.Write([]byte(content)) + // Create a gzip writer + gw := gzip.NewWriter(tmpFile) + defer gw.Close() + + err = createTar(t, gw, files) assert.NoError(t, err) return tmpFile.Name() } -func createTarBz2(t *testing.T, fileName, content string) string { +// createTarBz2 creates a tar.bz2 archive with the given files +func createTarBz2(t *testing.T, files []internalFile) string { t.Helper() // Create a temporary file @@ -332,26 +601,14 @@ func createTarBz2(t *testing.T, fileName, content string) string { assert.NoError(t, err) defer bw.Close() - // Create a tar writer - tw := tar.NewWriter(bw) - defer tw.Close() - - // Add a file to the tar archive - hdr := &tar.Header{ - Name: fileName, - Mode: 0600, - Size: int64(len(content)), - } - err = tw.WriteHeader(hdr) - assert.NoError(t, err) - - _, err = tw.Write([]byte(content)) + err = createTar(t, bw, files) assert.NoError(t, err) return tmpFile.Name() } -func createTarXz(t *testing.T, fileName, content string) string { +// createTarXz creates a tar.xz archive with the given files +func createTarXz(t *testing.T, files []internalFile) string { t.Helper() // Create a temporary file @@ -364,26 +621,14 @@ func createTarXz(t *testing.T, fileName, content string) string { assert.NoError(t, err) defer xw.Close() - // Create a tar writer - tw := tar.NewWriter(xw) - defer tw.Close() - - // Add a file to the tar archive - hdr := &tar.Header{ - Name: fileName, - Mode: 0600, - Size: int64(len(content)), - } - err = tw.WriteHeader(hdr) - assert.NoError(t, err) - - _, err = tw.Write([]byte(content)) + err = createTar(t, xw, files) assert.NoError(t, err) return tmpFile.Name() } -func createZip(t *testing.T, fileName, content string) string { +// createZip creates a zip archive with the given content +func createZip(t *testing.T, fileName string, content []byte) string { t.Helper() // Create a temporary file @@ -399,7 +644,19 @@ func createZip(t *testing.T, fileName, content string) string { w, err := zw.Create(fileName) assert.NoError(t, err) - _, err = io.Copy(w, bytes.NewReader([]byte(content))) + _, err = io.Copy(w, bytes.NewReader(content)) + assert.NoError(t, err) + + // Add docs/ directory to the zip archive + _, err = zw.Create("docs/") + assert.NoError(t, err) + + // Add README.md file to the docs/ directory + readmeContent := "This is a README file." + w, err = zw.Create("docs/README.md") + assert.NoError(t, err) + + _, err = io.Copy(w, bytes.NewReader([]byte(readmeContent))) assert.NoError(t, err) return tmpFile.Name() diff --git a/pkg/asset/interface.go b/pkg/asset/interface.go index ae0b8f0..cc07be5 100644 --- a/pkg/asset/interface.go +++ b/pkg/asset/interface.go @@ -5,13 +5,11 @@ import "context" type IAsset interface { GetName() string GetDisplayName() string - GetScore() int GetType() Type GetAsset() *Asset GetFiles() []*File GetTempPath() string GetFilePath() string - Score(options *ScoreOptions) int Download(context.Context) error Extract() error Install(string, string) error diff --git a/pkg/clients/gitlab/client.go b/pkg/clients/gitlab/client.go index 4cb0ce7..5e274f8 100644 --- a/pkg/clients/gitlab/client.go +++ b/pkg/clients/gitlab/client.go @@ -29,11 +29,11 @@ func (c *Client) SetToken(token string) { c.token = token } -func (c *Client) ListReleases(slug string) ([]*Release, error) { +func (c *Client) ListReleases(ctx context.Context, slug string) ([]*Release, error) { releaseURL := fmt.Sprintf("%s/projects/%s/releases", baseURL, url.QueryEscape(slug)) logrus.Tracef("GET %s", releaseURL) - req, err := http.NewRequestWithContext(context.TODO(), "GET", releaseURL, http.NoBody) + req, err := http.NewRequestWithContext(ctx, "GET", releaseURL, http.NoBody) if err != nil { return nil, err } @@ -58,11 +58,11 @@ func (c *Client) ListReleases(slug string) ([]*Release, error) { return releases, nil } -func (c *Client) GetLatestRelease(slug string) (*Release, error) { +func (c *Client) GetLatestRelease(ctx context.Context, slug string) (*Release, error) { releaseURL := fmt.Sprintf("%s/projects/%s/releases?per_page=1", baseURL, url.QueryEscape(slug)) logrus.Tracef("GET %s", releaseURL) - req, err := http.NewRequestWithContext(context.TODO(), "GET", releaseURL, http.NoBody) + req, err := http.NewRequestWithContext(ctx, "GET", releaseURL, http.NoBody) if err != nil { return nil, err } @@ -87,11 +87,11 @@ func (c *Client) GetLatestRelease(slug string) (*Release, error) { return releases[0], nil } -func (c *Client) GetRelease(slug, version string) (*Release, error) { +func (c *Client) GetRelease(ctx context.Context, slug, version string) (*Release, error) { releaseURL := fmt.Sprintf("%s/projects/%s/releases/%s", baseURL, url.QueryEscape(slug), url.QueryEscape(version)) logrus.Tracef("GET %s", releaseURL) - req, err := http.NewRequestWithContext(context.TODO(), "GET", releaseURL, http.NoBody) + req, err := http.NewRequestWithContext(ctx, "GET", releaseURL, http.NoBody) if err != nil { return nil, err } diff --git a/pkg/clients/gitlab/gitlab_test.go b/pkg/clients/gitlab/gitlab_test.go new file mode 100644 index 0000000..93c8bb3 --- /dev/null +++ b/pkg/clients/gitlab/gitlab_test.go @@ -0,0 +1,179 @@ +package gitlab_test + +import ( + "context" + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ekristen/distillery/pkg/clients/gitlab" +) + +func loadTestData(t *testing.T, filename string) string { + data, err := os.ReadFile("testdata/" + filename) + assert.NoError(t, err) + return string(data) +} + +func newMockClient(responseBody string, statusCode int) *http.Client { + return &http.Client{ + Transport: roundTripperFunc(func(req *http.Request) *http.Response { + return &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(strings.NewReader(responseBody)), + Header: make(http.Header), + } + }), + } +} + +type roundTripperFunc func(req *http.Request) *http.Response + +func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +func TestListReleases(t *testing.T) { + mockResponse := loadTestData(t, "list-releases.json") + client := gitlab.NewClient(newMockClient(mockResponse, http.StatusOK)) + client.SetToken("test-token") + + releases, err := client.ListReleases(context.Background(), "owner/repo") + assert.NoError(t, err) + assert.NotNil(t, releases) + assert.Equal(t, 2, len(releases)) +} + +func TestGetLatestRelease(t *testing.T) { + mockResponse := loadTestData(t, "latest-release.json") + client := gitlab.NewClient(newMockClient(mockResponse, http.StatusOK)) + client.SetToken("test-token") + + release, err := client.GetLatestRelease(context.Background(), "owner/repo") + assert.NoError(t, err) + assert.NotNil(t, release) + assert.Equal(t, "v1.0.0", release.TagName) +} + +func TestGetRelease(t *testing.T) { + mockResponse := loadTestData(t, "get-release.json") + client := gitlab.NewClient(newMockClient(mockResponse, http.StatusOK)) + client.SetToken("test-token") + + release, err := client.GetRelease(context.Background(), "owner/repo", "v1.0.0") + assert.NoError(t, err) + assert.NotNil(t, release) + assert.Equal(t, "v1.0.0", release.TagName) +} + +func TestGitlabClientErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + testFunc func() error + shouldFail bool + }{ + { + name: "ListReleases_InvalidURL", + testFunc: func() error { + client := gitlab.NewClient(newMockClient("", http.StatusOK)) + client.SetToken("test-token") + _, err := client.ListReleases(context.Background(), "invalid-url-%%") + return err + }, + shouldFail: true, + }, + { + name: "GetLatestRelease_InvalidURL", + testFunc: func() error { + client := gitlab.NewClient(newMockClient("", http.StatusOK)) + client.SetToken("test-token") + _, err := client.GetLatestRelease(context.Background(), "invalid-url-%%") + return err + }, + shouldFail: true, + }, + { + name: "GetRelease_InvalidURL", + testFunc: func() error { + client := gitlab.NewClient(newMockClient("", http.StatusOK)) + client.SetToken("test-token") + _, err := client.GetRelease(context.Background(), "invalid-url-%%", "v1.0.0") + return err + }, + shouldFail: true, + }, + { + name: "ListReleases_HTTPError", + testFunc: func() error { + client := gitlab.NewClient(newMockClient("", http.StatusInternalServerError)) + _, err := client.ListReleases(context.Background(), "owner/repo") + return err + }, + shouldFail: true, + }, + { + name: "ListReleases_InvalidJSON", + testFunc: func() error { + client := gitlab.NewClient(newMockClient("invalid json", http.StatusOK)) + _, err := client.ListReleases(context.Background(), "owner/repo") + return err + }, + shouldFail: true, + }, + { + name: "GetLatestRelease_HTTPError", + testFunc: func() error { + client := gitlab.NewClient(newMockClient("", http.StatusInternalServerError)) + _, err := client.GetLatestRelease(context.Background(), "owner/repo") + return err + }, + shouldFail: true, + }, + { + name: "GetLatestRelease_InvalidJSON", + testFunc: func() error { + client := gitlab.NewClient(newMockClient("invalid json", http.StatusOK)) + _, err := client.GetLatestRelease(context.Background(), "owner/repo") + return err + }, + shouldFail: true, + }, + { + name: "GetRelease_HTTPError", + testFunc: func() error { + client := gitlab.NewClient(newMockClient("", http.StatusInternalServerError)) + _, err := client.GetRelease(context.Background(), "owner/repo", "v1.0.0") + return err + }, + shouldFail: true, + }, + { + name: "GetRelease_InvalidJSON", + testFunc: func() error { + client := gitlab.NewClient(newMockClient("invalid json", http.StatusOK)) + _, err := client.GetRelease(context.Background(), "owner/repo", "v1.0.0") + return err + }, + shouldFail: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := tt.testFunc() + if tt.shouldFail { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/clients/gitlab/testdata/get-release.json b/pkg/clients/gitlab/testdata/get-release.json new file mode 100644 index 0000000..8aa6bff --- /dev/null +++ b/pkg/clients/gitlab/testdata/get-release.json @@ -0,0 +1,4 @@ +{ + "tag_name": "v1.0.0", + "name": "Release 1.0.0" +} \ No newline at end of file diff --git a/pkg/clients/gitlab/testdata/latest-release.json b/pkg/clients/gitlab/testdata/latest-release.json new file mode 100644 index 0000000..9a0b474 --- /dev/null +++ b/pkg/clients/gitlab/testdata/latest-release.json @@ -0,0 +1,6 @@ +[ + { + "tag_name": "v1.0.0", + "name": "Release 1.0.0" + } +] \ No newline at end of file diff --git a/pkg/clients/gitlab/testdata/list-releases.json b/pkg/clients/gitlab/testdata/list-releases.json new file mode 100644 index 0000000..c6e8264 --- /dev/null +++ b/pkg/clients/gitlab/testdata/list-releases.json @@ -0,0 +1,10 @@ +[ + { + "tag_name": "v1.0.0", + "name": "Release 1.0.0" + }, + { + "tag_name": "v1.1.0", + "name": "Release 1.1.0" + } +] \ No newline at end of file diff --git a/pkg/clients/hashicorp/client.go b/pkg/clients/hashicorp/client.go index 46b762c..7a86245 100644 --- a/pkg/clients/hashicorp/client.go +++ b/pkg/clients/hashicorp/client.go @@ -26,9 +26,9 @@ func NewClient(client *http.Client) *Client { } // ListProducts returns a list of products available from the HashiCorp Releases API -func (c *Client) ListProducts() (Products, error) { +func (c *Client) ListProducts(ctx context.Context) (Products, error) { req, err := http.NewRequestWithContext( - context.TODO(), http.MethodGet, "https://api.releases.hashicorp.com/v1/products", http.NoBody) + ctx, http.MethodGet, "https://api.releases.hashicorp.com/v1/products", http.NoBody) if err != nil { return nil, err } @@ -56,7 +56,7 @@ type ListReleasesOptions struct { } // ListReleases returns a list of releases for a product from the HashiCorp Releases API -func (c *Client) ListReleases(product string, opts *ListReleasesOptions) ([]*Release, error) { +func (c *Client) ListReleases(ctx context.Context, product string, opts *ListReleasesOptions) ([]*Release, error) { if opts == nil { opts = &ListReleasesOptions{ LicenseClass: "oss", @@ -68,7 +68,7 @@ func (c *Client) ListReleases(product string, opts *ListReleasesOptions) ([]*Rel licenseClass = fmt.Sprintf("license_class=%s", opts.LicenseClass) } - req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://api.releases.hashicorp.com/v1/releases/%s?%s", product, licenseClass), http.NoBody) if err != nil { return nil, err @@ -103,10 +103,10 @@ func (c *Client) ListReleases(product string, opts *ListReleasesOptions) ([]*Rel } // GetVersion returns a specific release for a product from the HashiCorp Releases API -func (c *Client) GetVersion(product, version string) (*Release, error) { +func (c *Client) GetVersion(ctx context.Context, product, version string) (*Release, error) { licenseClass := "license_class=oss" - req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://api.releases.hashicorp.com/v1/releases/%s/%s?%s", product, version, licenseClass), http.NoBody) if err != nil { diff --git a/pkg/clients/hashicorp/hashicorp_test.go b/pkg/clients/hashicorp/hashicorp_test.go new file mode 100644 index 0000000..acc572d --- /dev/null +++ b/pkg/clients/hashicorp/hashicorp_test.go @@ -0,0 +1,170 @@ +package hashicorp_test + +import ( + "context" + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ekristen/distillery/pkg/clients/hashicorp" +) + +func loadTestData(t *testing.T, filename string) string { + data, err := os.ReadFile("testdata/" + filename) + assert.NoError(t, err) + return string(data) +} + +func newMockClient(responseBody string, statusCode int) *http.Client { + return &http.Client{ + Transport: roundTripperFunc(func(req *http.Request) *http.Response { + return &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(strings.NewReader(responseBody)), + Header: make(http.Header), + } + }), + } +} + +type roundTripperFunc func(req *http.Request) *http.Response + +func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +func TestHashicorpClient(t *testing.T) { + client := hashicorp.NewClient(newMockClient("", http.StatusOK)) + + tests := []struct { + name string + testFunc func() error + shouldFail bool + }{ + { + name: "ListProducts_Success", + testFunc: func() error { + mockResponse := loadTestData(t, "list-products.json") + client := hashicorp.NewClient(newMockClient(mockResponse, http.StatusOK)) + _, err := client.ListProducts(context.Background()) + return err + }, + shouldFail: false, + }, + { + name: "ListReleases_Success", + testFunc: func() error { + mockResponse := loadTestData(t, "list-releases.json") + client := hashicorp.NewClient(newMockClient(mockResponse, http.StatusOK)) + _, err := client.ListReleases(context.Background(), "owner/repo", nil) + return err + }, + shouldFail: false, + }, + { + name: "GetVersion_Success", + testFunc: func() error { + mockResponse := loadTestData(t, "get-release.json") + client := hashicorp.NewClient(newMockClient(mockResponse, http.StatusOK)) + _, err := client.GetVersion(context.Background(), "owner/repo", "v1.0.0") + return err + }, + shouldFail: false, + }, + { + name: "ListProducts_InvalidURL", + testFunc: func() error { + _, err := client.ListProducts(context.Background()) + return err + }, + shouldFail: true, + }, + { + name: "ListReleases_InvalidURL", + testFunc: func() error { + _, err := client.ListReleases(context.Background(), "invalid-url-%%", nil) + return err + }, + shouldFail: true, + }, + { + name: "GetVersion_InvalidURL", + testFunc: func() error { + _, err := client.GetVersion(context.Background(), "invalid-url-%%", "v1.0.0") + return err + }, + shouldFail: true, + }, + { + name: "ListProducts_HTTPError", + testFunc: func() error { + client := hashicorp.NewClient(newMockClient("", http.StatusInternalServerError)) + _, err := client.ListProducts(context.Background()) + return err + }, + shouldFail: true, + }, + { + name: "ListReleases_HTTPError", + testFunc: func() error { + client := hashicorp.NewClient(newMockClient("", http.StatusInternalServerError)) + _, err := client.ListReleases(context.Background(), "owner/repo", nil) + return err + }, + shouldFail: true, + }, + { + name: "GetVersion_HTTPError", + testFunc: func() error { + client := hashicorp.NewClient(newMockClient("", http.StatusInternalServerError)) + _, err := client.GetVersion(context.Background(), "owner/repo", "v1.0.0") + return err + }, + shouldFail: true, + }, + { + name: "ListProducts_InvalidJSON", + testFunc: func() error { + client := hashicorp.NewClient(newMockClient("invalid json", http.StatusOK)) + _, err := client.ListProducts(context.Background()) + return err + }, + shouldFail: true, + }, + { + name: "ListReleases_InvalidJSON", + testFunc: func() error { + client := hashicorp.NewClient(newMockClient("invalid json", http.StatusOK)) + _, err := client.ListReleases(context.Background(), "owner/repo", nil) + return err + }, + shouldFail: true, + }, + { + name: "GetVersion_InvalidJSON", + testFunc: func() error { + client := hashicorp.NewClient(newMockClient("invalid json", http.StatusOK)) + _, err := client.GetVersion(context.Background(), "owner/repo", "v1.0.0") + return err + }, + shouldFail: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := tt.testFunc() + if tt.shouldFail { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/clients/hashicorp/testdata/get-release.json b/pkg/clients/hashicorp/testdata/get-release.json new file mode 100644 index 0000000..1f2e835 --- /dev/null +++ b/pkg/clients/hashicorp/testdata/get-release.json @@ -0,0 +1,5 @@ +{ + "version": "v1.0.0", + "name": "Release 1.0.0", + "is_prerelease": false +} \ No newline at end of file diff --git a/pkg/clients/hashicorp/testdata/list-products.json b/pkg/clients/hashicorp/testdata/list-products.json new file mode 100644 index 0000000..22b7da2 --- /dev/null +++ b/pkg/clients/hashicorp/testdata/list-products.json @@ -0,0 +1,4 @@ +[ + "product1", + "product2" +] \ No newline at end of file diff --git a/pkg/clients/hashicorp/testdata/list-releases.json b/pkg/clients/hashicorp/testdata/list-releases.json new file mode 100644 index 0000000..c4602ec --- /dev/null +++ b/pkg/clients/hashicorp/testdata/list-releases.json @@ -0,0 +1,12 @@ +[ + { + "version": "v1.0.0", + "name": "Release 1.0.0", + "is_prerelease": false + }, + { + "version": "v1.1.0-beta", + "name": "Release 1.1.0 Beta", + "is_prerelease": true + } +] \ No newline at end of file diff --git a/pkg/clients/homebrew/client.go b/pkg/clients/homebrew/client.go index 922b8c8..c8a82b3 100644 --- a/pkg/clients/homebrew/client.go +++ b/pkg/clients/homebrew/client.go @@ -25,12 +25,12 @@ type Client struct { client *http.Client } -func (h *Client) GetFormula(formula string) (*Formula, error) { +func (h *Client) GetFormula(ctx context.Context, formula string) (*Formula, error) { url := fmt.Sprintf("https://formulae.brew.sh/api/formula/%s.json", formula) logrus.Debugf("fetching formula: %s", url) - req, err := http.NewRequestWithContext(context.TODO(), "GET", url, http.NoBody) + req, err := http.NewRequestWithContext(ctx, "GET", url, http.NoBody) if err != nil { return nil, err } diff --git a/pkg/clients/homebrew/homebrew_test.go b/pkg/clients/homebrew/homebrew_test.go new file mode 100644 index 0000000..7368c2d --- /dev/null +++ b/pkg/clients/homebrew/homebrew_test.go @@ -0,0 +1,88 @@ +package homebrew_test + +import ( + "context" + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ekristen/distillery/pkg/clients/homebrew" +) + +func loadTestData(t *testing.T, filename string) string { + data, err := os.ReadFile("testdata/" + filename) + assert.NoError(t, err) + return string(data) +} + +func newMockClient(responseBody string, statusCode int) *http.Client { + return &http.Client{ + Transport: roundTripperFunc(func(req *http.Request) *http.Response { + return &http.Response{ + StatusCode: statusCode, + Body: io.NopCloser(strings.NewReader(responseBody)), + Header: make(http.Header), + } + }), + } +} + +type roundTripperFunc func(req *http.Request) *http.Response + +func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +func TestHomebrewClient(t *testing.T) { + tests := []struct { + name string + testFunc func() error + shouldFail bool + }{ + { + name: "GetFormula_Success", + testFunc: func() error { + mockResponse := loadTestData(t, "get-formula.json") + client := homebrew.NewClient(newMockClient(mockResponse, http.StatusOK)) + _, err := client.GetFormula(context.Background(), "formula-name") + return err + }, + shouldFail: false, + }, + { + name: "GetFormula_InvalidJSON", + testFunc: func() error { + client := homebrew.NewClient(newMockClient("invalid json", http.StatusOK)) + _, err := client.GetFormula(context.Background(), "formula-name") + return err + }, + shouldFail: true, + }, + { + name: "GetFormula_HTTPError", + testFunc: func() error { + client := homebrew.NewClient(newMockClient("", http.StatusInternalServerError)) + _, err := client.GetFormula(context.Background(), "formula-name") + return err + }, + shouldFail: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := tt.testFunc() + if tt.shouldFail { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/clients/homebrew/testdata/get-formula.json b/pkg/clients/homebrew/testdata/get-formula.json new file mode 100644 index 0000000..201962b --- /dev/null +++ b/pkg/clients/homebrew/testdata/get-formula.json @@ -0,0 +1,22 @@ +{ + "name": "formula-name", + "desc": "Description of the formula", + "homepage": "https://example.com", + "versions": { + "stable": "1.0.0", + "devel": null, + "head": "HEAD" + }, + "bottle": { + "stable": { + "rebuild": 1, + "cellar": ":any", + "files": { + "arm64_big_sur": { + "url": "https://example.com/bottle.tar.gz", + "sha256": "abc123" + } + } + } + } +} \ No newline at end of file diff --git a/pkg/commands/install/install.go b/pkg/commands/install/install.go index a6bf167..b336a89 100644 --- a/pkg/commands/install/install.go +++ b/pkg/commands/install/install.go @@ -52,6 +52,7 @@ func Execute(c *cli.Context) error { "gitlab-token": c.String("gitlab-token"), "no-checksum-verify": c.Bool("no-checksum-verify"), "include-pre-releases": c.Bool("include-pre-releases"), + "no-score-check": c.Bool("no-score-check"), }, }) if err != nil { @@ -69,7 +70,7 @@ func Execute(c *cli.Context) error { log.Infof("including pre-releases") } - if err := src.Run(c.Context, c.String("version"), c.String("github-token")); err != nil { + if err := src.Run(c.Context); err != nil { return err } @@ -163,6 +164,10 @@ func Flags() []cli.Flag { Name: "no-checksum-verify", Usage: "Disable checksum verification", }, + &cli.BoolFlag{ + Name: "no-score-check", + Usage: "Disable scoring check", + }, } } diff --git a/pkg/osconfig/os.go b/pkg/osconfig/os.go index f68a39f..39c29e6 100644 --- a/pkg/osconfig/os.go +++ b/pkg/osconfig/os.go @@ -39,7 +39,7 @@ func New(os, arch string) *OS { newOS.Aliases = []string{"win"} newOS.Extensions = []string{".exe"} case Linux: - newOS.Aliases = []string{"linux"} + newOS.Aliases = []string{} newOS.Extensions = []string{".AppImage"} case Darwin: newOS.Aliases = []string{"macos", "sonoma"} diff --git a/pkg/osconfig/osconfig_test.go b/pkg/osconfig/osconfig_test.go new file mode 100644 index 0000000..98f06e3 --- /dev/null +++ b/pkg/osconfig/osconfig_test.go @@ -0,0 +1,104 @@ +package osconfig_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ekristen/distillery/pkg/osconfig" +) + +func TestOS_GetOS(t *testing.T) { + tests := []struct { + name string + os *osconfig.OS + expected []string + }{ + { + name: "Windows", + os: osconfig.New(osconfig.Windows, osconfig.AMD64), + expected: []string{"windows", "win"}, + }, + { + name: "Linux", + os: osconfig.New(osconfig.Linux, osconfig.ARM64), + expected: []string{"linux"}, + }, + { + name: "Darwin", + os: osconfig.New(osconfig.Darwin, osconfig.AMD64), + expected: []string{"darwin", "macos", "sonoma"}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + fmt.Println(tt.os.GetOS()) + assert.ElementsMatch(t, tt.expected, tt.os.GetOS()) + }) + } +} + +func TestOS_GetArchitectures(t *testing.T) { + tests := []struct { + name string + os *osconfig.OS + expected []string + }{ + { + name: "Windows AMD64", + os: osconfig.New(osconfig.Windows, osconfig.AMD64), + expected: []string{"amd64", "x86_64", "64bit", "64"}, + }, + { + name: "Linux ARM64", + os: osconfig.New(osconfig.Linux, osconfig.ARM64), + expected: []string{"arm64", "aarch64"}, + }, + { + name: "Darwin Universal", + os: osconfig.New(osconfig.Darwin, osconfig.AMD64), + expected: []string{"amd64", "x86_64", "64bit", "64", "universal"}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert.ElementsMatch(t, tt.expected, tt.os.GetArchitectures()) + }) + } +} + +func TestOS_GetExtensions(t *testing.T) { + tests := []struct { + name string + os *osconfig.OS + expected []string + }{ + { + name: "Windows", + os: osconfig.New(osconfig.Windows, osconfig.AMD64), + expected: []string{".exe"}, + }, + { + name: "Linux", + os: osconfig.New(osconfig.Linux, osconfig.ARM64), + expected: []string{".AppImage"}, + }, + { + name: "Darwin", + os: osconfig.New(osconfig.Darwin, osconfig.AMD64), + expected: []string{}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert.ElementsMatch(t, tt.expected, tt.os.GetExtensions()) + }) + } +} diff --git a/pkg/score/score.go b/pkg/score/score.go index 4a89a6b..2223f36 100644 --- a/pkg/score/score.go +++ b/pkg/score/score.go @@ -24,23 +24,23 @@ func Score(names []string, opts *Options) []Sorted { // Note: if it has the word "update" in it, we want to deprioritize it as it's likely an update binary from // a rust or go binary distribution - scoringValues["update"] = -20 + scoringValues["update"] = -100 for _, os1 := range opts.OS { - scoringValues[strings.ToLower(os1)] = 10 + scoringValues[strings.ToLower(os1)] = 40 } for _, arch := range opts.Arch { - scoringValues[strings.ToLower(arch)] = 5 + scoringValues[strings.ToLower(arch)] = 30 } for _, ext := range opts.Extensions { - scoringValues[strings.ToLower(ext)] = 15 + scoringValues[strings.ToLower(ext)] = 20 } for _, name1 := range opts.Names { - scoringValues[strings.ToLower(name1)] = 20 + scoringValues[strings.ToLower(name1)] = 10 } for keyMatch, keyScore := range scoringValues { - if keyScore == 15 { // handle extensions special + if keyScore == 20 { // handle extensions special if ext := strings.TrimPrefix(filepath.Ext(strings.ToLower(name)), "."); ext != "" { for _, fileExt := range opts.Extensions { if filetype.GetType(ext) == filetype.GetType(fileExt) { diff --git a/pkg/score/score_test.go b/pkg/score/score_test.go index 7b40fc8..02bb807 100644 --- a/pkg/score/score_test.go +++ b/pkg/score/score_test.go @@ -33,7 +33,7 @@ func TestScore(t *testing.T) { expected: []Sorted{ { Key: "dist-linux-amd64.deb", - Value: 15, + Value: 70, }, }, }, @@ -58,7 +58,27 @@ func TestScore(t *testing.T) { expected: []Sorted{ { Key: "dist-linux-amd64", - Value: 15, + Value: 70, + }, + }, + }, + { + name: "unknown binary", + names: []string{ + "something-linux", + }, + opts: &Options{ + OS: []string{"macos"}, + Arch: []string{"amd64"}, + Extensions: []string{ + types.Unknown.Extension, + }, + Names: []string{"something"}, + }, + expected: []Sorted{ + { + Key: "something-linux", + Value: 10, }, }, }, @@ -76,7 +96,7 @@ func TestScore(t *testing.T) { expected: []Sorted{ { Key: "dist-linux-amd64.sig", - Value: 50, + Value: 100, }, }, }, @@ -93,7 +113,7 @@ func TestScore(t *testing.T) { expected: []Sorted{ { Key: "dist-linux-amd64.pem", - Value: 45, + Value: 110, }, }, }, @@ -105,14 +125,17 @@ func TestScore(t *testing.T) { "SHASUMS", }, opts: &Options{ - OS: []string{"linux"}, - Arch: []string{"amd64"}, + OS: []string{}, + Arch: []string{}, Extensions: []string{"txt"}, + Names: []string{ + "checksums", + }, }, expected: []Sorted{ { Key: "checksums.txt", - Value: 15, + Value: 30, }, { Key: "SHA256SUMS", diff --git a/pkg/source/github.go b/pkg/source/github.go index 979a71b..8da9f43 100644 --- a/pkg/source/github.go +++ b/pkg/source/github.go @@ -10,12 +10,12 @@ import ( "github.com/google/go-github/v62/github" "github.com/gregjones/httpcache" "github.com/gregjones/httpcache/diskcache" - "github.com/sirupsen/logrus" "github.com/ekristen/distillery/pkg/asset" - "github.com/ekristen/distillery/pkg/score" ) +const GitHubSource = "github" + type GitHub struct { Source @@ -26,12 +26,10 @@ type GitHub struct { Repo string // Repository name Release *github.RepositoryRelease - - Assets []*GitHubAsset } func (s *GitHub) GetSource() string { - return "github" + return GitHubSource } func (s *GitHub) GetOwner() string { return s.Owner @@ -51,7 +49,8 @@ func (s *GitHub) GetID() string { return strings.Join([]string{s.GetSource(), s.GetOwner(), s.GetRepo(), s.GetOS(), s.GetArch()}, "-") } -func (s *GitHub) Run(ctx context.Context, _, _ string) error { +// sourceRun - run the source specific logic +func (s *GitHub) sourceRun(ctx context.Context) error { cacheFile := filepath.Join(s.Options.MetadataDir, fmt.Sprintf("cache-%s", s.GetID())) s.client = github.NewClient(httpcache.NewTransport(diskcache.New(cacheFile)).Client()) @@ -69,29 +68,20 @@ func (s *GitHub) Run(ctx context.Context, _, _ string) error { return err } - ra, err := s.FindReleaseAsset() - if err != nil { - return err - } - _ = ra - - if err := s.Download(ctx); err != nil { - return err - } - - defer func(s *GitHub) { - _ = s.Cleanup() - }(s) + return nil +} - if err := s.Verify(); err != nil { +// Run - run the source +func (s *GitHub) Run(ctx context.Context) error { + if err := s.sourceRun(ctx); err != nil { return err } - if err := s.Extract(); err != nil { + if err := s.Discover(s.Assets, []string{s.Repo}); err != nil { return err } - if err := s.Install(); err != nil { + if err := s.commonRun(ctx); err != nil { return err } @@ -138,7 +128,7 @@ func (s *GitHub) FindRelease(ctx context.Context) error { return fmt.Errorf("release not found") } - log.Infof("installing version: %s", release.GetTagName()) + log.Infof("installing version: %s", strings.TrimPrefix(release.GetTagName(), "v")) s.Release = release @@ -178,105 +168,3 @@ func (s *GitHub) GetReleaseAssets(ctx context.Context) error { return nil } - -// FindReleaseAsset - find the asset that matches the current OS and Arch, if multiple matches are found it -// will attempt to find the best match based on the suffix for the appropriate OS. If no match is found an error -// is returned. -func (s *GitHub) FindReleaseAsset() (*GitHubAsset, error) { //nolint:gocyclo - // 1. Setup Assets - // 2. Determine Asset Type (checksum, archive, other, unknown) - // 3. Score Assets - // 4. Select best Asset Type (archive/binary) - // 5. If Archive, we need to extract and determine which files we are keeping (binaries) - // 6. Extract files, and copy/symlink them into place - for _, a := range s.Assets { - a.Score(&asset.ScoreOptions{ - OS: s.OSConfig.GetOS(), - Arch: s.OSConfig.GetArchitectures(), - Extensions: s.OSConfig.GetExtensions(), - }) - - logrus.Debugf("name: %s, score: %d", a.GetName(), a.GetScore()) - } - - var best *GitHubAsset - for _, a := range s.Assets { - logrus.Tracef("finding best: %s (%d)", a.GetName(), a.GetScore()) - if best == nil || - a.GetScore() > best.GetScore() && - (a.GetType() == asset.Archive || a.GetType() == asset.Unknown || a.GetType() == asset.Binary) { - best = a - } - } - - s.Binary = best - - fileScoring := map[asset.Type][]string{} - fileScored := map[asset.Type][]score.Sorted{} - for _, a := range s.Assets { - if _, ok := fileScoring[a.GetType()]; !ok { - fileScoring[a.GetType()] = []string{} - } - fileScoring[a.GetType()] = append(fileScoring[a.GetType()], a.GetName()) - } - for k, v := range fileScoring { - var ext []string - if k == asset.Key { - ext = []string{"key", "pub", "pem"} - } else if k == asset.Signature { - ext = []string{"sig", "asc"} - } else if k == asset.Checksum { - ext = []string{"sha256", "md5", "sha1", "txt"} - } - - if _, ok := fileScored[k]; !ok { - fileScored[k] = []score.Sorted{} - } - - fileScored[k] = score.Score(v, &score.Options{ - OS: s.OSConfig.GetOS(), - Arch: s.OSConfig.GetArchitectures(), - Extensions: ext, - Names: []string{strings.ReplaceAll(s.Binary.GetName(), filepath.Ext(s.Binary.GetName()), "")}, - }) - - if len(fileScored[k]) > 0 { - logrus.Debugf("file scoring sorted ! type: %d, scored: %v", k, fileScored[k][0]) - } - } - - for _, a := range s.Assets { - for k, v := range fileScored { - vv := v[0] - - if a.GetType() == asset.Checksum && a.GetType() == k && a.GetName() == vv.Key { //nolint:gocritic - s.Checksum = a - } - if a.GetType() == asset.Signature && a.GetType() == k && a.GetName() == vv.Key { //nolint:gocritic - s.Signature = a - } - if a.GetType() == asset.Key && a.GetType() == k && a.GetName() == vv.Key { //nolint:gocritic - s.Key = a - } - } - } - - if s.Binary != nil { - logrus.Tracef("best binary: %s", s.Binary.GetName()) - } - if s.Checksum != nil { - logrus.Tracef("best checksum: %s", s.Checksum.GetName()) - } - if s.Signature != nil { - logrus.Tracef("best signature: %s", s.Signature.GetName()) - } - if s.Key != nil { - logrus.Tracef("best key: %s", s.Key.GetName()) - } - - if best != nil { - return best, nil - } - - return nil, fmt.Errorf("no matching asset found") -} diff --git a/pkg/source/gitlab.go b/pkg/source/gitlab.go index 910d36b..43cc29f 100644 --- a/pkg/source/gitlab.go +++ b/pkg/source/gitlab.go @@ -5,13 +5,10 @@ import ( "fmt" "path/filepath" - "github.com/gregjones/httpcache" - "github.com/gregjones/httpcache/diskcache" - "github.com/sirupsen/logrus" - "github.com/ekristen/distillery/pkg/asset" "github.com/ekristen/distillery/pkg/clients/gitlab" - "github.com/ekristen/distillery/pkg/osconfig" + "github.com/gregjones/httpcache" + "github.com/gregjones/httpcache/diskcache" ) type GitLab struct { @@ -24,8 +21,6 @@ type GitLab struct { Version string Release *gitlab.Release - - Assets []*GitLabAsset } func (s *GitLab) GetSource() string { @@ -48,7 +43,7 @@ func (s *GitLab) GetDownloadsDir() string { return filepath.Join(s.Options.DownloadsDir, s.GetSource(), s.GetOwner(), s.GetRepo(), s.Version) } -func (s *GitLab) Run(ctx context.Context, _, _ string) error { //nolint:gocyclo +func (s *GitLab) sourceRun(ctx context.Context) error { cacheFile := filepath.Join(s.Options.MetadataDir, fmt.Sprintf("cache-%s", s.GetID())) s.client = gitlab.NewClient(httpcache.NewTransport(diskcache.New(cacheFile)).Client()) @@ -58,7 +53,7 @@ func (s *GitLab) Run(ctx context.Context, _, _ string) error { //nolint:gocyclo } if s.Version == VersionLatest { - release, err := s.client.GetLatestRelease(fmt.Sprintf("%s/%s", s.Owner, s.Repo)) + release, err := s.client.GetLatestRelease(ctx, fmt.Sprintf("%s/%s", s.Owner, s.Repo)) if err != nil { return err } @@ -66,7 +61,7 @@ func (s *GitLab) Run(ctx context.Context, _, _ string) error { //nolint:gocyclo s.Version = release.TagName s.Release = release } else { - release, err := s.client.GetRelease(fmt.Sprintf("%s/%s", s.Owner, s.Repo), s.Version) + release, err := s.client.GetRelease(ctx, fmt.Sprintf("%s/%s", s.Owner, s.Repo), s.Version) if err != nil { return err } @@ -86,49 +81,19 @@ func (s *GitLab) Run(ctx context.Context, _, _ string) error { //nolint:gocyclo }) } - detectedOS := osconfig.New(s.GetOS(), s.GetArch()) - - for _, a := range s.Assets { - a.Score(&asset.ScoreOptions{ - OS: detectedOS.GetOS(), - Arch: detectedOS.GetArchitectures(), - Extensions: detectedOS.GetExtensions(), - }) - - logrus.Debugf("name: %s, score: %d", a.GetName(), a.GetScore()) - } - - var best *GitLabAsset - for _, a := range s.Assets { - logrus.Tracef("finding best: %s (%d)", a.GetName(), a.GetScore()) - if best == nil || - a.GetScore() > best.GetScore() && - (a.GetType() == asset.Archive || a.GetType() == asset.Unknown || a.GetType() == asset.Binary) { - best = a - } - } - - s.Binary = best - - if best == nil { - return fmt.Errorf("unable to find best asset") - } - - logrus.Tracef("best: %s", best.GetName()) + return nil +} - if err := best.Download(ctx); err != nil { +func (s *GitLab) Run(ctx context.Context) error { + if err := s.sourceRun(ctx); err != nil { return err } - defer func(s *GitLab) { - _ = s.Cleanup() - }(s) - - if err := s.Extract(); err != nil { + if err := s.Discover(s.Assets, []string{s.Repo}); err != nil { return err } - if err := s.Install(); err != nil { + if err := s.commonRun(ctx); err != nil { return err } diff --git a/pkg/source/gitlab_asset.go b/pkg/source/gitlab_asset.go index 6c45c74..12186fd 100644 --- a/pkg/source/gitlab_asset.go +++ b/pkg/source/gitlab_asset.go @@ -24,7 +24,7 @@ type GitLabAsset struct { } func (a *GitLabAsset) ID() string { - return fmt.Sprintf("%s-%s-%s", a.GitLab.GetOwner(), a.GitLab.GetRepo(), a.GitLab.Version) + return fmt.Sprintf("%s-%s-%s-%d", a.GitLab.GetOwner(), a.GitLab.GetRepo(), a.GitLab.Version, a.Link.ID) } func (a *GitLabAsset) Download(ctx context.Context) error { //nolint:dupl,nolintlint @@ -51,7 +51,7 @@ func (a *GitLabAsset) Download(ctx context.Context) error { //nolint:dupl,nolint return nil } - logrus.Infof("downloading asset: %s", a.Link.URL) + logrus.Debugf("downloading asset: %s", a.Link.URL) req, err := http.NewRequestWithContext(context.TODO(), "GET", a.Link.URL, http.NoBody) if err != nil { diff --git a/pkg/source/hashicorp.go b/pkg/source/hashicorp.go index 1f09823..8114476 100644 --- a/pkg/source/hashicorp.go +++ b/pkg/source/hashicorp.go @@ -7,14 +7,11 @@ import ( "path/filepath" "github.com/apex/log" - "github.com/sirupsen/logrus" - "github.com/gregjones/httpcache" "github.com/gregjones/httpcache/diskcache" "github.com/ekristen/distillery/pkg/asset" "github.com/ekristen/distillery/pkg/clients/hashicorp" - "github.com/ekristen/distillery/pkg/osconfig" ) const HashicorpSource = "hashicorp" @@ -27,8 +24,6 @@ type Hashicorp struct { Owner string Repo string Version string - - Assets []*HashicorpAsset } func (s *Hashicorp) GetSource() string { @@ -51,7 +46,7 @@ func (s *Hashicorp) GetDownloadsDir() string { return filepath.Join(s.Options.DownloadsDir, s.GetSource(), s.GetOwner(), s.GetRepo(), s.Version) } -func (s *Hashicorp) Run(ctx context.Context, _, _ string) error { //nolint:gocyclo +func (s *Hashicorp) sourceRun(ctx context.Context) error { cacheFile := filepath.Join(s.Options.MetadataDir, fmt.Sprintf("cache-%s", s.GetID())) s.client = hashicorp.NewClient(httpcache.NewTransport(diskcache.New(cacheFile)).Client()) @@ -59,7 +54,7 @@ func (s *Hashicorp) Run(ctx context.Context, _, _ string) error { //nolint:gocyc var release *hashicorp.Release if s.Version == "latest" { - releases, err := s.client.ListReleases(s.Repo, nil) + releases, err := s.client.ListReleases(ctx, s.Repo, nil) if err != nil { return err } @@ -71,7 +66,7 @@ func (s *Hashicorp) Run(ctx context.Context, _, _ string) error { //nolint:gocyc s.Version = releases[0].Version release = releases[0] } else { - version, err := s.client.GetVersion(s.Repo, s.Version) + version, err := s.client.GetVersion(ctx, s.Repo, s.Version) if err != nil { return err } @@ -85,9 +80,6 @@ func (s *Hashicorp) Run(ctx context.Context, _, _ string) error { //nolint:gocyc log.Infof("installing %s@%s", release.Name, release.Version) - detectedOS := osconfig.New(s.GetOS(), s.GetArch()) - - s.Assets = make([]*HashicorpAsset, 0) for _, build := range release.Builds { s.Assets = append(s.Assets, &HashicorpAsset{ Asset: asset.New(filepath.Base(build.URL), "", s.GetOS(), s.GetArch(), s.Version), @@ -97,45 +89,19 @@ func (s *Hashicorp) Run(ctx context.Context, _, _ string) error { //nolint:gocyc }) } - for _, a := range s.Assets { - a.Score(&asset.ScoreOptions{ - OS: detectedOS.GetOS(), - Arch: detectedOS.GetArchitectures(), - Extensions: detectedOS.GetExtensions(), - }) - - logrus.Debugf("name: %s, score: %d", a.GetName(), a.GetScore()) - } - - var best *HashicorpAsset - for _, a := range s.Assets { - logrus.Tracef("finding best: %s (%d)", a.GetName(), a.GetScore()) - if best == nil || - a.GetScore() > best.GetScore() && - (a.GetType() == asset.Archive || a.GetType() == asset.Unknown || a.GetType() == asset.Binary) { - best = a - } - } - - s.Binary = best - - if best == nil { - return fmt.Errorf("unable to find best asset") - } + return nil +} - if err := best.Download(ctx); err != nil { +func (s *Hashicorp) Run(ctx context.Context) error { + if err := s.sourceRun(ctx); err != nil { return err } - defer func(s *Hashicorp) { - _ = s.Cleanup() - }(s) - - if err := s.Extract(); err != nil { + if err := s.Discover(s.Assets, []string{s.Repo}); err != nil { return err } - if err := s.Install(); err != nil { + if err := s.commonRun(ctx); err != nil { return err } diff --git a/pkg/source/hashicorp_asset.go b/pkg/source/hashicorp_asset.go index d3eff0d..df15f0b 100644 --- a/pkg/source/hashicorp_asset.go +++ b/pkg/source/hashicorp_asset.go @@ -25,7 +25,11 @@ type HashicorpAsset struct { } func (a *HashicorpAsset) ID() string { - return fmt.Sprintf("%s-%s-%s", a.Hashicorp.GetSource(), a.Hashicorp.GetRepo(), a.Hashicorp.Version) + urlHash := sha256.Sum256([]byte(a.Build.URL)) + urlHashShort := fmt.Sprintf("%x", urlHash)[:9] + + return fmt.Sprintf("%s-%s-%s-%s", + a.Hashicorp.GetSource(), a.Hashicorp.GetRepo(), a.Hashicorp.Version, urlHashShort) } func (a *HashicorpAsset) Download(ctx context.Context) error { @@ -52,7 +56,7 @@ func (a *HashicorpAsset) Download(ctx context.Context) error { return nil } - logrus.Infof("downloading asset: %s", a.Build.URL) + logrus.Debugf("downloading asset: %s", a.Build.URL) req, err := http.NewRequestWithContext(context.TODO(), "GET", a.Build.URL, http.NoBody) if err != nil { diff --git a/pkg/source/homebrew.go b/pkg/source/homebrew.go index aa49c43..7cdd4aa 100644 --- a/pkg/source/homebrew.go +++ b/pkg/source/homebrew.go @@ -12,7 +12,6 @@ import ( "github.com/ekristen/distillery/pkg/asset" "github.com/ekristen/distillery/pkg/clients/homebrew" - "github.com/ekristen/distillery/pkg/osconfig" ) const HomebrewSource = "homebrew" @@ -24,8 +23,6 @@ type Homebrew struct { Formula string Version string - - Assets []*HomebrewAsset } func (s *Homebrew) GetSource() string { @@ -48,14 +45,14 @@ func (s *Homebrew) GetDownloadsDir() string { return filepath.Join(s.Options.DownloadsDir, s.GetSource(), s.GetOwner(), s.GetRepo(), s.Version) } -func (s *Homebrew) Run(ctx context.Context, _, _ string) error { //nolint:gocyclo +func (s *Homebrew) sourceRun(ctx context.Context) error { cacheFile := filepath.Join(s.Options.MetadataDir, fmt.Sprintf("cache-%s", s.GetID())) s.client = homebrew.NewClient(httpcache.NewTransport(diskcache.New(cacheFile)).Client()) logrus.Debug("fetching formula") - formula, err := s.client.GetFormula(s.Formula) + formula, err := s.client.GetFormula(ctx, s.Formula) if err != nil { return err } @@ -67,13 +64,10 @@ func (s *Homebrew) Run(ctx context.Context, _, _ string) error { //nolint:gocycl logrus.Debug("selecting version") } - detectedOS := osconfig.New(s.GetOS(), s.GetArch()) - if len(formula.Dependencies) > 0 { return fmt.Errorf("formula with dependencies are not currently supported") } - s.Assets = make([]*HomebrewAsset, 0) for osSlug, variant := range formula.Bottle.Stable.Files { newVariant := variant osSlug = strings.ReplaceAll(osSlug, "_", "-") @@ -96,47 +90,19 @@ func (s *Homebrew) Run(ctx context.Context, _, _ string) error { //nolint:gocycl }) } - for _, a := range s.Assets { - a.Score(&asset.ScoreOptions{ - OS: detectedOS.GetOS(), - Arch: detectedOS.GetArchitectures(), - Extensions: detectedOS.GetExtensions(), - }) - - logrus.Debugf("name: %s, score: %d", a.GetName(), a.GetScore()) - } - - var best *HomebrewAsset - for _, a := range s.Assets { - logrus.Tracef("finding best: %s (%d)", a.GetName(), a.GetScore()) - if best == nil || - a.GetScore() > best.GetScore() && - (a.GetType() == asset.Archive || a.GetType() == asset.Unknown || a.GetType() == asset.Binary) { - best = a - } - } - - s.Binary = best - - if best == nil { - return fmt.Errorf("unable to find best asset") - } - - logrus.Tracef("best found: %s", best.GetName()) + return nil +} - if err := best.Download(ctx); err != nil { +func (s *Homebrew) Run(ctx context.Context) error { + if err := s.sourceRun(ctx); err != nil { return err } - defer func(s *Homebrew) { - _ = s.Cleanup() - }(s) - - if err := s.Extract(); err != nil { + if err := s.Discover(s.Assets, []string{s.Formula}); err != nil { return err } - if err := s.Install(); err != nil { + if err := s.commonRun(ctx); err != nil { return err } diff --git a/pkg/source/homebrew_asset.go b/pkg/source/homebrew_asset.go index 2e96d75..38d5747 100644 --- a/pkg/source/homebrew_asset.go +++ b/pkg/source/homebrew_asset.go @@ -25,7 +25,8 @@ type HomebrewAsset struct { } func (a *HomebrewAsset) ID() string { - return fmt.Sprintf("%s-%s-%s", a.Homebrew.GetOwner(), a.Homebrew.GetRepo(), a.Homebrew.Version) + return fmt.Sprintf("%s-%s-%s-%s", + a.Homebrew.GetOwner(), a.Homebrew.GetRepo(), a.Homebrew.Version, a.FileVariant.Sha256[:9]) } type GHCRAuth struct { diff --git a/pkg/source/source.go b/pkg/source/source.go index f224973..3e493ac 100644 --- a/pkg/source/source.go +++ b/pkg/source/source.go @@ -6,7 +6,9 @@ import ( "encoding/base64" "errors" "fmt" + "os" + "path/filepath" "strings" "github.com/apex/log" @@ -16,6 +18,7 @@ import ( "github.com/ekristen/distillery/pkg/checksum" "github.com/ekristen/distillery/pkg/cosign" "github.com/ekristen/distillery/pkg/osconfig" + "github.com/ekristen/distillery/pkg/score" ) const ( @@ -29,7 +32,7 @@ type ISource interface { GetApp() string GetID() string GetDownloadsDir() string - Run(context.Context, string, string) error + Run(context.Context) error } type Options struct { @@ -46,11 +49,9 @@ type Options struct { } type Source struct { - Options *Options - OSConfig *osconfig.OS - - File string - + Options *Options + OSConfig *osconfig.OS + Assets []asset.IAsset Binary asset.IAsset Signature asset.IAsset Checksum asset.IAsset @@ -65,6 +66,161 @@ func (s *Source) GetArch() string { return s.Options.Arch } +func (s *Source) commonRun(ctx context.Context) error { + if err := s.Download(ctx); err != nil { + return err + } + + defer func(s *Source) { + err := s.Cleanup() + if err != nil { + log.WithError(err).Error("unable to cleanup") + } + }(s) + + if err := s.Extract(); err != nil { + return err + } + + if err := s.Install(); err != nil { + return err + } + + return nil +} + +// Discover will attempt to discover and categorize the assets provided +// TODO(ek): split up and refactor this function +func (s *Source) Discover(assets []asset.IAsset, names []string) error { //nolint:funlen,gocyclo + fileScoring := map[asset.Type][]string{} + fileScored := map[asset.Type][]score.Sorted{} + for _, a := range assets { + if _, ok := fileScoring[a.GetType()]; !ok { + fileScoring[a.GetType()] = []string{} + } + fileScoring[a.GetType()] = append(fileScoring[a.GetType()], a.GetName()) + } + // Note: first pass we want to look for just binaries and score them + for k, v := range fileScoring { + if k != asset.Binary && k != asset.Unknown && k != asset.Archive { + continue + } + + detectedOS := s.OSConfig.GetOS() + arch := s.OSConfig.GetArchitectures() + ext := s.OSConfig.GetExtensions() + + if _, ok := fileScored[k]; !ok { + fileScored[k] = []score.Sorted{} + } + + fileScored[k] = score.Score(v, &score.Options{ + OS: detectedOS, + Arch: arch, + Extensions: ext, + Names: names, + }) + + if len(fileScored[k]) > 0 { + logrus.Debugf("file scoring sorted ! type: %d, scored: %v", k, fileScored[k][0]) + } + } + + for _, a := range assets { + if a.GetType() != asset.Binary && a.GetType() != asset.Unknown && a.GetType() != asset.Archive { + continue + } + + for k, v := range fileScored { + if a.GetType() != k { + continue + } + + vv := v[0] + if a.GetName() == vv.Key { + if vv.Value < 40 && !s.Options.Settings["no-score-check"].(bool) { + log.Error("no matching asset found, score too low") + log.Errorf("closest matching: %s (%d) (threshold: 40) -- override with --no-score-check", a.GetName(), vv.Value) + return fmt.Errorf("no matching asset found, score too low") + } + + s.Binary = a + } + } + } + + // Note: second pass we want to look for everything else, using binary results to help score + for k, v := range fileScoring { + if k == asset.Binary || k == asset.Unknown || k == asset.Archive { + continue + } + + detectedOS := s.OSConfig.GetOS() + arch := s.OSConfig.GetArchitectures() + ext := s.OSConfig.GetExtensions() + + if k == asset.Key { + ext = []string{"key", "pub", "pem"} + detectedOS = []string{} + arch = []string{} + } else if k == asset.Signature { + ext = []string{"sig", "asc"} + detectedOS = []string{} + arch = []string{} + } else if k == asset.Checksum { + ext = []string{"sha256", "md5", "sha1", "txt"} + detectedOS = []string{} + arch = []string{} + } + + if _, ok := fileScored[k]; !ok { + fileScored[k] = []score.Sorted{} + } + + fileScored[k] = score.Score(v, &score.Options{ + OS: detectedOS, + Arch: arch, + Extensions: ext, + Names: []string{strings.ReplaceAll(s.Binary.GetName(), filepath.Ext(s.Binary.GetName()), "")}, + }) + + if len(fileScored[k]) > 0 { + logrus.Debugf("file scoring sorted ! type: %d, scored: %v", k, fileScored[k][0]) + } + } + + for _, a := range assets { + for k, v := range fileScored { + vv := v[0] + + if a.GetType() == asset.Checksum && a.GetType() == k && a.GetName() == vv.Key { //nolint:gocritic + s.Checksum = a + } + if a.GetType() == asset.Signature && a.GetType() == k && a.GetName() == vv.Key { //nolint:gocritic + s.Signature = a + } + if a.GetType() == asset.Key && a.GetType() == k && a.GetName() == vv.Key { //nolint:gocritic + s.Key = a + } + } + } + + if s.Binary != nil { + logrus.Tracef("best binary: %s", s.Binary.GetName()) + } + if s.Checksum != nil { + logrus.Tracef("best checksum: %s", s.Checksum.GetName()) + } + if s.Signature != nil { + logrus.Tracef("best signature: %s", s.Signature.GetName()) + } + if s.Key != nil { + logrus.Tracef("best key: %s", s.Key.GetName()) + } + + return nil +} + func (s *Source) Download(ctx context.Context) error { log.Info("downloading assets") if s.Binary != nil {