diff --git a/.github/workflows/wc-integration-test.yaml b/.github/workflows/wc-integration-test.yaml index 99a403aa7..349ee0d52 100644 --- a/.github/workflows/wc-integration-test.yaml +++ b/.github/workflows/wc-integration-test.yaml @@ -136,6 +136,34 @@ jobs: run: aqua cp --exclude-tags test -t foo working-directory: tests/tag + - name: update only registrires + run: | + aqua update -r + git diff . + git checkout -- . + working-directory: tests/update + + - name: update only packages + run: | + aqua up -p + git diff . + git checkout -- . + working-directory: tests/update + + - name: update all registries and packages + run: | + aqua update + git diff . + git checkout -- . + working-directory: tests/update + + - name: update only specific command + run: | + aqua update tfcmt ci-info + git diff . + git checkout -- . + working-directory: tests/update + - run: aqua update-checksum -a - name: Test rm diff --git a/pkg/cli/runner.go b/pkg/cli/runner.go index 73d61aa83..72eeddbbc 100644 --- a/pkg/cli/runner.go +++ b/pkg/cli/runner.go @@ -67,6 +67,8 @@ func (r *Runner) setParam(c *cli.Context, commandName string, param *config.Para param.GlobalConfigFilePaths = finder.ParseGlobalConfigFilePaths(os.Getenv("AQUA_GLOBAL_CONFIG")) param.Deep = c.Bool("deep") param.Pin = c.Bool("pin") + param.OnlyPackage = c.Bool("only-package") + param.OnlyRegistry = c.Bool("only-registry") wd, err := os.Getwd() if err != nil { return fmt.Errorf("get the current directory: %w", err) @@ -172,6 +174,7 @@ func (r *Runner) Run(ctx context.Context, args ...string) error { r.newRootDirCommand(), r.newUpdateChecksumCommand(), r.newRemoveCommand(), + r.newUpdateCommand(), }, } diff --git a/pkg/cli/update.go b/pkg/cli/update.go new file mode 100644 index 000000000..9a142725f --- /dev/null +++ b/pkg/cli/update.go @@ -0,0 +1,124 @@ +package cli + +import ( + "fmt" + "net/http" + + "github.com/aquaproj/aqua/v2/pkg/config" + "github.com/aquaproj/aqua/v2/pkg/controller" + "github.com/urfave/cli/v2" +) + +func (r *Runner) newUpdateCommand() *cli.Command { + return &cli.Command{ + Name: "update", + Aliases: []string{"up"}, + Usage: "Update registries and packages", + Description: updateDescription, + Action: r.updateAction, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "i", + Usage: `Select packages with fuzzy finder`, + }, + &cli.BoolFlag{ + Name: "select-version", + Aliases: []string{"s"}, + Usage: `Select the version with fuzzy finder`, + }, + &cli.BoolFlag{ + Name: "only-registry", + Aliases: []string{"r"}, + Usage: `Update only registries`, + }, + &cli.BoolFlag{ + Name: "only-package", + Aliases: []string{"p"}, + Usage: `Update only packages`, + }, + }, + } +} + +const updateDescription = `Update registries and packages. +If no argument is passed, all registries and packages are updated to the latest. + + # Update all packages and registries to the latest versions + $ aqua update + +This command has an alias "up" + + $ aqua up + +This command gets the latest version from GitHub Releases, GitHub Tags, and crates.io and updates aqua.yaml. +This command doesn't update commit hashes. +This command doesn't install packages. +This command updates only a nearest aqua.yaml from the current directory. +If this command finds a aqua.yaml, it ignores other aqua.yaml including global configuration files ($AQUA_GLOBAL_CONFIG). + +So if you want to update other files, please change the current directory or specify the configuration file path with the option '-c'. + + $ aqua -c foo/aqua.yaml update + +If you want to update only registries, please use the --only-registry [-r] option. + + # Update only registries + $ aqua update -r + +If you want to update only packages, please use the --only-package [-p] option. + + # Update only packages + $ aqua update -p + +If you want to update only specific packages, please use the -i option. +You can select packages with the fuzzy finder. +If -i option is used, registries aren't updated. + + # Select updated packages with fuzzy finder + $ aqua update -i + +If you want to select versions, please use the --select-version [-s] option. +You can select versions with the fuzzy finder. You can not only update but also downgrade packages. + + # Select updated packages and versions with fuzzy finder + $ aqua update -i -s + +This command doesn't update packages if the field 'version' is used. + + packages: + - name: cli/cli@v2.0.0 # Update + - name: gohugoio/hugo + version: v0.118.0 # Doesn't update + +So if you don't want to update specific packages, the field 'version' is useful. + +You can specify packages with command names. aqua finds packages that have these commands and updates them. + + $ aqua update [ ...] + +e.g. + + # Update cli/cli + $ aqua update gh +` + +func (r *Runner) updateAction(c *cli.Context) error { + tracer, err := startTrace(c.String("trace")) + if err != nil { + return err + } + defer tracer.Stop() + + cpuProfiler, err := startCPUProfile(c.String("cpu-profile")) + if err != nil { + return err + } + defer cpuProfiler.Stop() + + param := &config.Param{} + if err := r.setParam(c, "update", param); err != nil { + return fmt.Errorf("parse the command line arguments: %w", err) + } + ctrl := controller.InitializeUpdateCommandController(c.Context, param, http.DefaultClient, r.Runtime) + return ctrl.Update(c.Context, r.LogE, param) //nolint:wrapcheck +} diff --git a/pkg/config-reader/reader.go b/pkg/config-reader/reader.go index 4ba460862..330795ff6 100644 --- a/pkg/config-reader/reader.go +++ b/pkg/config-reader/reader.go @@ -96,7 +96,10 @@ func (r *ConfigReaderImpl) readImports(configFilePath string, cfg *aqua.Config) if err := r.Read(filePath, subCfg); err != nil { return err } - pkgs = append(pkgs, subCfg.Packages...) + for _, pkg := range subCfg.Packages { + pkg.FilePath = filePath + pkgs = append(pkgs, pkg) + } } } cfg.Packages = pkgs diff --git a/pkg/config-reader/reader_test.go b/pkg/config-reader/reader_test.go index bb26c29bd..70a3960c6 100644 --- a/pkg/config-reader/reader_test.go +++ b/pkg/config-reader/reader_test.go @@ -91,6 +91,7 @@ packages: Name: "aquaproj/aqua-installer", Registry: "standard", Version: "v1.0.0", + FilePath: "/home/workspace/foo/aqua-installer.yaml", }, }, }, diff --git a/pkg/config-reader/update.go b/pkg/config-reader/update.go new file mode 100644 index 000000000..b6a3b16d0 --- /dev/null +++ b/pkg/config-reader/update.go @@ -0,0 +1,79 @@ +package reader + +import ( + "fmt" + "path/filepath" + "sort" + "strings" + + "github.com/aquaproj/aqua/v2/pkg/config/aqua" + "github.com/aquaproj/aqua/v2/pkg/osfile" + "github.com/spf13/afero" + "gopkg.in/yaml.v2" +) + +func (r *ConfigReaderImpl) ReadToUpdate(configFilePath string, cfg *aqua.Config) (map[string]*aqua.Config, error) { + file, err := r.fs.Open(configFilePath) + if err != nil { + return nil, err //nolint:wrapcheck + } + defer file.Close() + if err := yaml.NewDecoder(file).Decode(cfg); err != nil { + return nil, fmt.Errorf("parse a configuration file as YAML %s: %w", configFilePath, err) + } + var configFileDir string + for _, rgst := range cfg.Registries { + rgst := rgst + if rgst.Type == "local" { + if strings.HasPrefix(rgst.Path, homePrefix) { + if r.homeDir == "" { + return nil, errHomeDirEmpty + } + rgst.Path = filepath.Join(r.homeDir, rgst.Path[6:]) // 6: "$HOME/" + } + if configFileDir == "" { + configFileDir = filepath.Dir(configFilePath) + } + rgst.Path = osfile.Abs(configFileDir, rgst.Path) + } + } + cfgs, err := r.readImportsToUpdate(configFilePath, cfg) + if err != nil { + return nil, fmt.Errorf("read imports (%s): %w", configFilePath, err) + } + return cfgs, nil +} + +func (r *ConfigReaderImpl) readImportsToUpdate(configFilePath string, cfg *aqua.Config) (map[string]*aqua.Config, error) { + cfgs := map[string]*aqua.Config{} + pkgs := []*aqua.Package{} + for _, pkg := range cfg.Packages { + if pkg == nil { + continue + } + if pkg.Import == "" { + pkgs = append(pkgs, pkg) + continue + } + p := filepath.Join(filepath.Dir(configFilePath), pkg.Import) + filePaths, err := afero.Glob(r.fs, p) + if err != nil { + return nil, fmt.Errorf("read files with glob pattern (%s): %w", p, err) + } + sort.Strings(filePaths) + for _, filePath := range filePaths { + subCfg := &aqua.Config{} + subCfgs, err := r.ReadToUpdate(filePath, subCfg) + if err != nil { + return nil, err + } + subCfg.Registries = cfg.Registries + cfgs[filePath] = subCfg + for k, subCfg := range subCfgs { + cfgs[k] = subCfg + } + } + } + cfg.Packages = pkgs + return cfgs, nil +} diff --git a/pkg/config-reader/update_test.go b/pkg/config-reader/update_test.go new file mode 100644 index 000000000..de4b12a4c --- /dev/null +++ b/pkg/config-reader/update_test.go @@ -0,0 +1,147 @@ +package reader_test + +import ( + "testing" + + "github.com/aquaproj/aqua/v2/pkg/config" + reader "github.com/aquaproj/aqua/v2/pkg/config-reader" + "github.com/aquaproj/aqua/v2/pkg/config/aqua" + "github.com/aquaproj/aqua/v2/pkg/testutil" + "github.com/google/go-cmp/cmp" +) + +func Test_configReader_ReadToUpdate(t *testing.T) { //nolint:funlen + t.Parallel() + data := []struct { + name string + cfg *aqua.Config + cfgs map[string]*aqua.Config + isErr bool + files map[string]string + configFilePath string + homeDir string + }{ + { + name: "file isn't found", + isErr: true, + }, + { + name: "normal", + files: map[string]string{ + "/home/workspace/foo/aqua.yaml": `registries: +- type: standard + ref: v2.5.0 +- type: local + name: local + path: registry.yaml +packages:`, + }, + configFilePath: "/home/workspace/foo/aqua.yaml", + cfg: &aqua.Config{ + Registries: aqua.Registries{ + "standard": { + Type: "github_content", + Name: "standard", + Ref: "v2.5.0", + RepoOwner: "aquaproj", + RepoName: "aqua-registry", + Path: "registry.yaml", + }, + "local": { + Type: "local", + Name: "local", + Path: "/home/workspace/foo/registry.yaml", + }, + }, + Packages: []*aqua.Package{}, + }, + cfgs: map[string]*aqua.Config{}, + }, + { + name: "import package", + files: map[string]string{ + "/home/workspace/foo/aqua.yaml": `registries: +- type: standard + ref: v2.5.0 +packages: +- name: suzuki-shunsuke/ci-info@v1.0.0 +- import: aqua-installer.yaml +`, + "/home/workspace/foo/aqua-installer.yaml": `packages: +- name: aquaproj/aqua-installer@v1.0.0 +`, + }, + configFilePath: "/home/workspace/foo/aqua.yaml", + cfg: &aqua.Config{ + Registries: aqua.Registries{ + "standard": { + Type: "github_content", + Name: "standard", + Ref: "v2.5.0", + RepoOwner: "aquaproj", + RepoName: "aqua-registry", + Path: "registry.yaml", + }, + }, + Packages: []*aqua.Package{ + { + Name: "suzuki-shunsuke/ci-info", + Registry: "standard", + Version: "v1.0.0", + }, + }, + }, + cfgs: map[string]*aqua.Config{ + "/home/workspace/foo/aqua-installer.yaml": { + Packages: []*aqua.Package{ + { + Name: "aquaproj/aqua-installer", + Registry: "standard", + Version: "v1.0.0", + }, + }, + Registries: aqua.Registries{ + "standard": { + Type: "github_content", + Name: "standard", + Ref: "v2.5.0", + RepoOwner: "aquaproj", + RepoName: "aqua-registry", + Path: "registry.yaml", + }, + }, + }, + }, + }, + } + for _, d := range data { + d := d + t.Run(d.name, func(t *testing.T) { + t.Parallel() + fs, err := testutil.NewFs(d.files) + if err != nil { + t.Fatal(err) + } + reader := reader.New(fs, &config.Param{ + HomeDir: d.homeDir, + }) + cfg := &aqua.Config{} + cfgs, err := reader.ReadToUpdate(d.configFilePath, cfg) + if err != nil { + if d.isErr { + return + } + t.Fatal(err) + } + if d.isErr { + t.Fatal("error must be returned") + } + if diff := cmp.Diff(d.cfg, cfg); diff != "" { + t.Fatal("cfg:", diff) + } + if diff := cmp.Diff(d.cfgs, cfgs); diff != "" { + t.Fatal("cfgs:", diff) + } + }) + } +} diff --git a/pkg/config/aqua/config.go b/pkg/config/aqua/config.go index 27f0e8508..b40d46462 100644 --- a/pkg/config/aqua/config.go +++ b/pkg/config/aqua/config.go @@ -14,6 +14,7 @@ type Package struct { Tags []string `yaml:",omitempty" json:"tags,omitempty"` Description string `yaml:",omitempty" json:"description,omitempty"` Link string `yaml:",omitempty" json:"link,omitempty"` + FilePath string `json:"-" yaml:"-"` } func (p *Package) UnmarshalYAML(unmarshal func(interface{}) error) error { diff --git a/pkg/config/package.go b/pkg/config/package.go index a7da000ff..746c6c620 100644 --- a/pkg/config/package.go +++ b/pkg/config/package.go @@ -266,6 +266,8 @@ type Param struct { RequireChecksum bool DisablePolicy bool Detail bool + OnlyPackage bool + OnlyRegistry bool PolicyConfigFilePaths []string } diff --git a/pkg/controller/generate/cargo.go b/pkg/controller/generate/cargo.go deleted file mode 100644 index 97e4f9f42..000000000 --- a/pkg/controller/generate/cargo.go +++ /dev/null @@ -1,33 +0,0 @@ -package generate - -import ( - "context" - - "github.com/aquaproj/aqua/v2/pkg/config" - "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" - "github.com/sirupsen/logrus" -) - -func (c *Controller) getCargoVersion(ctx context.Context, logE *logrus.Entry, param *config.Param, pkg *fuzzyfinder.Package) string { - pkgInfo := pkg.PackageInfo - if param.SelectVersion { - versionStrings, err := c.cargoClient.ListVersions(ctx, pkgInfo.Crate) - if err != nil { - logE.WithError(err).Warn("list versions") - return "" - } - versions := fuzzyfinder.ConvertStringsToVersions(versionStrings) - - idx, err := c.fuzzyFinder.Find(versions, false) - if err != nil { - return "" - } - return versions[idx].(*fuzzyfinder.Version).Version //nolint:forcetypeassert - } - version, err := c.cargoClient.GetLatestVersion(ctx, pkgInfo.Crate) - if err != nil { - logE.WithError(err).Warn("get a latest version") - return "" - } - return version -} diff --git a/pkg/controller/generate/controller.go b/pkg/controller/generate/controller.go index cb8c5e877..846b23263 100644 --- a/pkg/controller/generate/controller.go +++ b/pkg/controller/generate/controller.go @@ -1,14 +1,17 @@ package generate import ( + "context" "io" "os" "github.com/aquaproj/aqua/v2/pkg/cargo" reader "github.com/aquaproj/aqua/v2/pkg/config-reader" + "github.com/aquaproj/aqua/v2/pkg/config/registry" "github.com/aquaproj/aqua/v2/pkg/controller/generate/output" "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" rgst "github.com/aquaproj/aqua/v2/pkg/install-registry" + "github.com/sirupsen/logrus" "github.com/spf13/afero" ) @@ -22,18 +25,19 @@ type Controller struct { fs afero.Fs outputter Outputter cargoClient cargo.Client + fuzzyGetter FuzzyGetter } -type VersionSelector interface { - Find(versions []*fuzzyfinder.Version, hasPreview bool) (int, error) +type FuzzyGetter interface { + Get(ctx context.Context, logE *logrus.Entry, pkg *registry.PackageInfo, currentVersion string, useFinder bool) string } type FuzzyFinder interface { - Find(items []fuzzyfinder.Item, hasPreview bool) (int, error) - FindMulti(items []fuzzyfinder.Item, hasPreview bool) ([]int, error) + Find(items []*fuzzyfinder.Item, hasPreview bool) (int, error) + FindMulti(items []*fuzzyfinder.Item, hasPreview bool) ([]int, error) } -func New(configFinder ConfigFinder, configReader reader.ConfigReader, registInstaller rgst.Installer, gh RepositoriesService, fs afero.Fs, fuzzyFinder FuzzyFinder, cargoClient cargo.Client) *Controller { +func New(configFinder ConfigFinder, configReader reader.ConfigReader, registInstaller rgst.Installer, gh RepositoriesService, fs afero.Fs, fuzzyFinder FuzzyFinder, cargoClient cargo.Client, fuzzyGetter FuzzyGetter) *Controller { return &Controller{ stdin: os.Stdin, configFinder: configFinder, @@ -44,5 +48,6 @@ func New(configFinder ConfigFinder, configReader reader.ConfigReader, registInst fuzzyFinder: fuzzyFinder, cargoClient: cargoClient, outputter: output.New(os.Stdout, fs), + fuzzyGetter: fuzzyGetter, } } diff --git a/pkg/controller/generate/generate.go b/pkg/controller/generate/generate.go index 1e3aba0f2..969f99fde 100644 --- a/pkg/controller/generate/generate.go +++ b/pkg/controller/generate/generate.go @@ -93,18 +93,24 @@ func (c *Controller) listPkgs(ctx context.Context, logE *logrus.Entry, param *co func (c *Controller) listPkgsWithFinder(ctx context.Context, logE *logrus.Entry, param *config.Param, registryContents map[string]*registry.Config) ([]*aqua.Package, error) { // maps the package and the registry - var pkgs []fuzzyfinder.Item + var items []*fuzzyfinder.Item + var pkgs []*fuzzyfinder.Package for registryName, registryContent := range registryContents { for _, pkg := range registryContent.PackageInfos { - pkgs = append(pkgs, &fuzzyfinder.Package{ + p := &fuzzyfinder.Package{ PackageInfo: pkg, RegistryName: registryName, + } + pkgs = append(pkgs, p) + items = append(items, &fuzzyfinder.Item{ + Item: p.Item(), + Preview: fuzzyfinder.PreviewPackage(p), }) } } // Launch the fuzzy finder - idxes, err := c.fuzzyFinder.FindMulti(pkgs, true) + idxes, err := c.fuzzyFinder.FindMulti(items, true) if err != nil { if errors.Is(err, fuzzyfinder.ErrAbort) { return nil, nil @@ -113,7 +119,7 @@ func (c *Controller) listPkgsWithFinder(ctx context.Context, logE *logrus.Entry, } arr := make([]*aqua.Package, len(idxes)) for i, idx := range idxes { - arr[i] = c.getOutputtedPkg(ctx, logE, param, pkgs[idx].(*fuzzyfinder.Package)) //nolint:forcetypeassert + arr[i] = c.getOutputtedPkg(ctx, logE, param, pkgs[idx]) } return arr, nil @@ -177,36 +183,6 @@ func (c *Controller) listPkgsWithoutFinder(ctx context.Context, logE *logrus.Ent return outputPkgs, nil } -func (c *Controller) getVersionFromGitHub(ctx context.Context, logE *logrus.Entry, param *config.Param, pkgInfo *registry.PackageInfo) string { - if pkgInfo.VersionSource == "github_tag" { - return c.getVersionFromGitHubTag(ctx, logE, param, pkgInfo) - } - if param.SelectVersion { - return c.selectVersionFromReleases(ctx, logE, pkgInfo) - } - if pkgInfo.VersionFilter != "" || pkgInfo.VersionPrefix != "" { - return c.listAndGetTagName(ctx, logE, pkgInfo) - } - return c.getVersionFromLatestRelease(ctx, logE, pkgInfo) -} - -func (c *Controller) getVersion(ctx context.Context, logE *logrus.Entry, param *config.Param, pkg *fuzzyfinder.Package) string { - if pkg.Version != "" { - return pkg.Version - } - pkgInfo := pkg.PackageInfo - if pkgInfo.Type == "cargo" { - return c.getCargoVersion(ctx, logE, param, pkg) - } - if c.github == nil { - return "" - } - if pkgInfo.HasRepo() { - return c.getVersionFromGitHub(ctx, logE, param, pkgInfo) - } - return "" -} - func (c *Controller) getOutputtedPkg(ctx context.Context, logE *logrus.Entry, param *config.Param, pkg *fuzzyfinder.Package) *aqua.Package { outputPkg := &aqua.Package{ Name: pkg.PackageInfo.GetName(), @@ -221,7 +197,7 @@ func (c *Controller) getOutputtedPkg(ctx context.Context, logE *logrus.Entry, pa outputPkg.Registry = "" } if outputPkg.Version == "" { - version := c.getVersion(ctx, logE, param, pkg) + version := c.fuzzyGetter.Get(ctx, logE, pkg.PackageInfo, "", param.SelectVersion) if version == "" { outputPkg.Version = "[SET PACKAGE VERSION]" return outputPkg diff --git a/pkg/controller/generate/generate_test.go b/pkg/controller/generate/generate_test.go index 12caa42b5..ed9a84bf4 100644 --- a/pkg/controller/generate/generate_test.go +++ b/pkg/controller/generate/generate_test.go @@ -20,6 +20,7 @@ import ( "github.com/aquaproj/aqua/v2/pkg/runtime" "github.com/aquaproj/aqua/v2/pkg/slsa" "github.com/aquaproj/aqua/v2/pkg/testutil" + "github.com/aquaproj/aqua/v2/pkg/versiongetter" "github.com/sirupsen/logrus" ) @@ -402,7 +403,7 @@ packages: configReader := reader.New(fs, d.param) fuzzyFinder := fuzzyfinder.NewMock(d.idxs, d.fuzzyFinderErr) cargoClient := &cargo.MockClient{} - ctrl := generate.New(configFinder, configReader, registryInstaller, gh, fs, fuzzyFinder, cargoClient) + ctrl := generate.New(configFinder, configReader, registryInstaller, gh, fs, fuzzyFinder, cargoClient, versiongetter.NewMockFuzzyGetter(map[string]string{})) if err := ctrl.Generate(ctx, logE, d.param, d.args...); err != nil { if d.isErr { return diff --git a/pkg/controller/generate/github_release.go b/pkg/controller/generate/github_release.go deleted file mode 100644 index cc9ec81a7..000000000 --- a/pkg/controller/generate/github_release.go +++ /dev/null @@ -1,195 +0,0 @@ -package generate - -import ( - "context" - "strings" - - "github.com/antonmedv/expr/vm" - "github.com/aquaproj/aqua/v2/pkg/config/registry" - "github.com/aquaproj/aqua/v2/pkg/expr" - "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" - "github.com/aquaproj/aqua/v2/pkg/github" - "github.com/sirupsen/logrus" - "github.com/suzuki-shunsuke/logrus-error/logerr" -) - -func (c *Controller) selectVersionFromReleases(ctx context.Context, logE *logrus.Entry, pkgInfo *registry.PackageInfo) string { - releases := c.listReleases(ctx, logE, pkgInfo) - versions := make([]fuzzyfinder.Item, len(releases)) - for i, release := range releases { - versions[i] = &fuzzyfinder.Version{ - Name: release.GetName(), - Version: release.GetTagName(), - Description: release.GetBody(), - URL: release.GetHTMLURL(), - } - } - idx, err := c.fuzzyFinder.Find(versions, true) - if err != nil { - return "" - } - return versions[idx].(*fuzzyfinder.Version).Version //nolint:forcetypeassert -} - -func (c *Controller) getVersionFromLatestRelease(ctx context.Context, logE *logrus.Entry, pkgInfo *registry.PackageInfo) string { - repoOwner := pkgInfo.RepoOwner - repoName := pkgInfo.RepoName - release, _, err := c.github.GetLatestRelease(ctx, repoOwner, repoName) - if err != nil { - logerr.WithError(logE, err).WithFields(logrus.Fields{ - "repo_owner": repoOwner, - "repo_name": repoName, - }).Warn("get the latest release") - return "" - } - return release.GetTagName() -} - -type Filter struct { - Prefix string - Filter *vm.Program - Constraint string -} - -func createFilters(pkgInfo *registry.PackageInfo) ([]*Filter, error) { - filters := make([]*Filter, 0, 1+len(pkgInfo.VersionOverrides)) - topFilter := &Filter{} - if pkgInfo.VersionFilter != "" { - f, err := expr.CompileVersionFilter(pkgInfo.VersionFilter) - if err != nil { - return nil, err //nolint:wrapcheck - } - topFilter.Filter = f - } - topFilter.Constraint = pkgInfo.VersionConstraints - if pkgInfo.VersionPrefix != "" { - topFilter.Prefix = pkgInfo.VersionPrefix - } - filters = append(filters, topFilter) - - for _, vo := range pkgInfo.VersionOverrides { - flt := &Filter{ - Prefix: topFilter.Prefix, - Filter: topFilter.Filter, - Constraint: topFilter.Constraint, - } - if vo.VersionFilter != nil { - f, err := expr.CompileVersionFilter(*vo.VersionFilter) - if err != nil { - return nil, err //nolint:wrapcheck - } - flt.Filter = f - } - flt.Constraint = vo.VersionConstraints - if vo.VersionPrefix != nil { - flt.Prefix = *vo.VersionPrefix - } - filters = append(filters, flt) - } - return filters, nil -} - -func (c *Controller) listReleases(ctx context.Context, logE *logrus.Entry, pkgInfo *registry.PackageInfo) []*github.RepositoryRelease { - repoOwner := pkgInfo.RepoOwner - repoName := pkgInfo.RepoName - opt := &github.ListOptions{ - PerPage: 100, //nolint:gomnd - } - var arr []*github.RepositoryRelease - - filters, err := createFilters(pkgInfo) - if err != nil { - return nil - } - - for i := 0; i < 10; i++ { - releases, _, err := c.github.ListReleases(ctx, repoOwner, repoName, opt) - if err != nil { - logerr.WithError(logE, err).WithFields(logrus.Fields{ - "repo_owner": repoOwner, - "repo_name": repoName, - }).Warn("list releases") - return arr - } - for _, release := range releases { - if filterRelease(release, filters) { - arr = append(arr, release) - } - } - if len(releases) != opt.PerPage { - return arr - } - opt.Page++ - } - return arr -} - -func (c *Controller) listAndGetTagName(ctx context.Context, logE *logrus.Entry, pkgInfo *registry.PackageInfo) string { - repoOwner := pkgInfo.RepoOwner - repoName := pkgInfo.RepoName - opt := &github.ListOptions{ - PerPage: 30, //nolint:gomnd - } - - filters, err := createFilters(pkgInfo) - if err != nil { - return "" - } - - for { - releases, _, err := c.github.ListReleases(ctx, repoOwner, repoName, opt) - if err != nil { - logerr.WithError(logE, err).WithFields(logrus.Fields{ - "repo_owner": repoOwner, - "repo_name": repoName, - }).Warn("list releases") - return "" - } - for _, release := range releases { - if filterRelease(release, filters) { - return release.GetTagName() - } - } - if len(releases) != opt.PerPage { - return "" - } - opt.Page++ - } -} - -func filterRelease(release *github.RepositoryRelease, filters []*Filter) bool { - if release.GetPrerelease() { - return false - } - - tagName := release.GetTagName() - - for _, filter := range filters { - if filterTagByFilter(tagName, filter) { - return true - } - } - return false -} - -func filterTagByFilter(tagName string, filter *Filter) bool { - sv := tagName - if filter.Prefix != "" { - if !strings.HasPrefix(tagName, filter.Prefix) { - return false - } - sv = strings.TrimPrefix(tagName, filter.Prefix) - } - if filter.Filter != nil { - if f, err := expr.EvaluateVersionFilter(filter.Filter, tagName); err != nil || !f { - return false - } - } - if filter.Constraint == "" { - return true - } - if f, err := expr.EvaluateVersionConstraints(filter.Constraint, tagName, sv); err == nil && f { - return true - } - return false -} diff --git a/pkg/controller/generate/github_tag.go b/pkg/controller/generate/github_tag.go deleted file mode 100644 index 2053049f6..000000000 --- a/pkg/controller/generate/github_tag.go +++ /dev/null @@ -1,116 +0,0 @@ -package generate - -import ( - "context" - - "github.com/aquaproj/aqua/v2/pkg/config" - "github.com/aquaproj/aqua/v2/pkg/config/registry" - "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" - "github.com/aquaproj/aqua/v2/pkg/github" - "github.com/sirupsen/logrus" - "github.com/suzuki-shunsuke/logrus-error/logerr" -) - -func filterTag(tag *github.RepositoryTag, filters []*Filter) bool { - tagName := tag.GetName() - for _, filter := range filters { - if filterTagByFilter(tagName, filter) { - return true - } - } - return false -} - -// listTags lists GitHub Tags by GitHub API and filter them with `version_filter`. -func (c *Controller) listTags(ctx context.Context, logE *logrus.Entry, pkgInfo *registry.PackageInfo) []*github.RepositoryTag { - // List GitHub Tags by GitHub API - // Filter tags with version_filter - repoOwner := pkgInfo.RepoOwner - repoName := pkgInfo.RepoName - opt := &github.ListOptions{ - PerPage: 100, //nolint:gomnd - } - - filters, err := createFilters(pkgInfo) - if err != nil { - return nil - } - - var arr []*github.RepositoryTag - for i := 0; i < 10; i++ { - tags, _, err := c.github.ListTags(ctx, repoOwner, repoName, opt) - if err != nil { - logerr.WithError(logE, err).WithFields(logrus.Fields{ - "repo_owner": repoOwner, - "repo_name": repoName, - }).Warn("list releases") - return arr - } - for _, tag := range tags { - if filterTag(tag, filters) { - arr = append(arr, tag) - } - } - if len(tags) != opt.PerPage { - return arr - } - opt.Page++ - } - return arr -} - -func (c *Controller) listAndGetTagNameFromTag(ctx context.Context, logE *logrus.Entry, pkgInfo *registry.PackageInfo) string { - // List GitHub Tags by GitHub API - // Filter tags with version_filter - // Get a tag - repoOwner := pkgInfo.RepoOwner - repoName := pkgInfo.RepoName - opt := &github.ListOptions{ - PerPage: 30, //nolint:gomnd - } - filters, err := createFilters(pkgInfo) - if err != nil { - return "" - } - for { - tags, _, err := c.github.ListTags(ctx, repoOwner, repoName, opt) - if err != nil { - logerr.WithError(logE, err).WithFields(logrus.Fields{ - "repo_owner": repoOwner, - "repo_name": repoName, - }).Warn("list tags") - return "" - } - for _, tag := range tags { - if filterTag(tag, filters) { - return tag.GetName() - } - } - if len(tags) != opt.PerPage { - return "" - } - opt.Page++ - } -} - -func (c *Controller) selectVersionFromGitHubTag(ctx context.Context, logE *logrus.Entry, pkgInfo *registry.PackageInfo) string { - tags := c.listTags(ctx, logE, pkgInfo) - versions := make([]fuzzyfinder.Item, len(tags)) - for i, tag := range tags { - versions[i] = &fuzzyfinder.Version{ - Version: tag.GetName(), - } - } - idx, err := c.fuzzyFinder.Find(versions, false) - if err != nil { - return "" - } - return versions[idx].(*fuzzyfinder.Version).Version //nolint:forcetypeassert -} - -func (c *Controller) getVersionFromGitHubTag(ctx context.Context, logE *logrus.Entry, param *config.Param, pkgInfo *registry.PackageInfo) string { - if param.SelectVersion { - return c.selectVersionFromGitHubTag(ctx, logE, pkgInfo) - } - return c.listAndGetTagNameFromTag(ctx, logE, pkgInfo) -} diff --git a/pkg/controller/remove/controller.go b/pkg/controller/remove/controller.go index aa5e6b662..f161e7225 100644 --- a/pkg/controller/remove/controller.go +++ b/pkg/controller/remove/controller.go @@ -20,7 +20,7 @@ type Controller struct { } type FuzzyFinder interface { - FindMulti(pkgs []fuzzyfinder.Item, hasPreview bool) ([]int, error) + FindMulti(pkgs []*fuzzyfinder.Item, hasPreview bool) ([]int, error) } func New(param *config.Param, fs afero.Fs, rt *runtime.Runtime, configFinder ConfigFinder, configReader reader.ConfigReader, registryInstaller rgst.Installer, fuzzyFinder FuzzyFinder) *Controller { diff --git a/pkg/controller/remove/remove.go b/pkg/controller/remove/remove.go index 15311b382..b370c06f5 100644 --- a/pkg/controller/remove/remove.go +++ b/pkg/controller/remove/remove.go @@ -62,18 +62,24 @@ func (c *Controller) Remove(ctx context.Context, logE *logrus.Entry, param *conf } func (c *Controller) removePackagesInteractively(logE *logrus.Entry, param *config.Param, registryContents map[string]*registry.Config) error { - var pkgs []fuzzyfinder.Item + var items []*fuzzyfinder.Item + var pkgs []*fuzzyfinder.Package for registryName, registryContent := range registryContents { for _, pkg := range registryContent.PackageInfos { - pkgs = append(pkgs, &fuzzyfinder.Package{ + fp := &fuzzyfinder.Package{ PackageInfo: pkg, RegistryName: registryName, + } + pkgs = append(pkgs, fp) + items = append(items, &fuzzyfinder.Item{ + Item: fp.Item(), + Preview: fuzzyfinder.PreviewPackage(fp), }) } } // Launch the fuzzy finder - idxes, err := c.fuzzyFinder.FindMulti(pkgs, true) + idxes, err := c.fuzzyFinder.FindMulti(items, true) if err != nil { if errors.Is(err, fuzzyfinder.ErrAbort) { return nil @@ -81,7 +87,7 @@ func (c *Controller) removePackagesInteractively(logE *logrus.Entry, param *conf return fmt.Errorf("find the package: %w", err) } for _, idx := range idxes { - pkg := pkgs[idx].(*fuzzyfinder.Package) //nolint:forcetypeassert + pkg := pkgs[idx] pkgName := pkg.PackageInfo.GetName() logE := logE.WithField("package_name", pkgName) if err := c.removePackage(logE, param.RootDir, pkg.PackageInfo); err != nil { diff --git a/pkg/controller/update/ast/package.go b/pkg/controller/update/ast/package.go new file mode 100644 index 000000000..86cf64aea --- /dev/null +++ b/pkg/controller/update/ast/package.go @@ -0,0 +1,98 @@ +package ast + +import ( + "errors" + "fmt" + "strings" + + "github.com/goccy/go-yaml/ast" + "github.com/sirupsen/logrus" +) + +func UpdatePackages(logE *logrus.Entry, file *ast.File, newVersions map[string]string) (bool, error) { + body := file.Docs[0].Body // DocumentNode + mv, err := findMappingValueFromNode(body, "packages") + if err != nil { + return false, err + } + + seq, ok := mv.Value.(*ast.SequenceNode) + if !ok { + return false, errors.New("the value must be a sequence node") + } + updated := false + for _, value := range seq.Values { + up, err := parsePackageNode(logE, value, newVersions) + if err != nil { + return false, err + } + if up { + updated = up + } + } + return updated, nil +} + +func parsePackageNode(logE *logrus.Entry, node ast.Node, newVersions map[string]string) (bool, error) { //nolint:cyclop,funlen + mvs, err := normalizeMappingValueNodes(node) + if err != nil { + return false, err + } + var registryName string + var pkgName string + var pkgVersion string + var nameNode *ast.StringNode + for _, mvn := range mvs { + switch mvn.Key.String() { + case "registry": + sn, ok := mvn.Value.(*ast.StringNode) + if !ok { + return false, errors.New("registry must be a string") + } + registryName = sn.Value + case "name": + sn, ok := mvn.Value.(*ast.StringNode) + if !ok { + return false, errors.New("name must be a string") + } + nameNode = sn + name, version, ok := strings.Cut(sn.Value, "@") + if !ok { + continue + } + pkgName = name + pkgVersion = version + default: + continue // Ignore unknown fields + } + } + if registryName == "" { + registryName = "standard" + } + if pkgName == "" { + return false, nil + } + newVersion, ok := newVersions[fmt.Sprintf("%s,%s", registryName, pkgName)] + if !ok { + logE.Debug("version isn't found") + return false, nil + } + if pkgVersion == newVersion { + logE.Debug("already latest") + return false, nil + } + if commitHashPattern.MatchString(pkgVersion) { + logE.WithFields(logrus.Fields{ + "current_version": pkgVersion, + "package_name": pkgName, + }).Debug("skip updating a commit hash") + return false, nil + } + logE.WithFields(logrus.Fields{ + "old_version": pkgVersion, + "new_version": newVersion, + "package_name": pkgName, + }).Info("updating a package") + nameNode.Value = fmt.Sprintf("%s@%s", pkgName, newVersion) + return true, nil +} diff --git a/pkg/controller/update/ast/package_test.go b/pkg/controller/update/ast/package_test.go new file mode 100644 index 000000000..ab220a9ab --- /dev/null +++ b/pkg/controller/update/ast/package_test.go @@ -0,0 +1,75 @@ +package ast_test + +import ( + "testing" + + "github.com/aquaproj/aqua/v2/pkg/controller/update/ast" + "github.com/goccy/go-yaml/parser" + "github.com/google/go-cmp/cmp" + "github.com/sirupsen/logrus" +) + +func TestUpdatePackages(t *testing.T) { //nolint:funlen + t.Parallel() + data := []struct { + name string + updated bool + isErr bool + file string + newVersions map[string]string + expFile string + }{ + { + name: "updated", + file: `packages: + - name: cli/cli@v2.0.0 # comment + # foo + - name: suzuki-shunsuke/tfcmt@v4.1.0 + - import: foo.yaml + - name: suzuki-shunsuke/tfcmt@v4.1.0 + registry: custom + - name: suzuki-shunsuke/ci-info + version: v3.0.0 +`, + expFile: `packages: + - name: cli/cli@v2.1.0 # comment + # foo + - name: suzuki-shunsuke/tfcmt@v4.1.0 + - import: foo.yaml + - name: suzuki-shunsuke/tfcmt@v4.6.0 + registry: custom + - name: suzuki-shunsuke/ci-info + version: v3.0.0 +`, + newVersions: map[string]string{ + "standard,cli/cli": "v2.1.0", + "standard,suzuki-shunsuke/ci-info": "v4.0.0", + "custom,suzuki-shunsuke/tfcmt": "v4.6.0", + }, + updated: true, + }, + } + logE := logrus.NewEntry(logrus.New()) + for _, d := range data { + d := d + t.Run(d.name, func(t *testing.T) { + t.Parallel() + file, err := parser.ParseBytes([]byte(d.file), parser.ParseComments) + if err != nil { + t.Fatal(err) + } + updated, err := ast.UpdatePackages(logE, file, d.newVersions) + if err != nil { + if !d.isErr { + t.Fatal(err) + } + } + if updated != d.updated { + t.Fatalf("updated: wanted %v, got %v", d.updated, updated) + } + if diff := cmp.Diff(file.String(), d.expFile); diff != "" { + t.Fatal(diff) + } + }) + } +} diff --git a/pkg/controller/update/ast/registry.go b/pkg/controller/update/ast/registry.go new file mode 100644 index 000000000..3eacc64fc --- /dev/null +++ b/pkg/controller/update/ast/registry.go @@ -0,0 +1,145 @@ +package ast + +import ( + "errors" + + "github.com/goccy/go-yaml/ast" + "github.com/sirupsen/logrus" +) + +const typeStandard = "standard" + +func findMappingValue(values []*ast.MappingValueNode, key string) *ast.MappingValueNode { + for _, value := range values { + if value.Key.String() == key { + return value + } + } + return nil +} + +func normalizeMappingValueNodes(node ast.Node) ([]*ast.MappingValueNode, error) { + switch t := node.(type) { + case *ast.MappingNode: + return t.Values, nil + case *ast.MappingValueNode: + return []*ast.MappingValueNode{t}, nil + } + return nil, errors.New("node must be a mapping node or mapping value node") +} + +func findMappingValueFromNode(body ast.Node, key string) (*ast.MappingValueNode, error) { + values, err := normalizeMappingValueNodes(body) + if err != nil { + return nil, err + } + return findMappingValue(values, key), nil +} + +func updateRegistryVersion(logE *logrus.Entry, refNode *ast.StringNode, rgstName, newVersion string) bool { + if refNode.Value == newVersion { + return false + } + if commitHashPattern.MatchString(refNode.Value) { + logE.WithFields(logrus.Fields{ + "registry_name": rgstName, + "current_version": refNode.Value, + }).Debug("skip updating a commit hash") + return false + } + logE.WithFields(logrus.Fields{ + "old_version": refNode.Value, + "new_version": newVersion, + "registry_name": rgstName, + }).Info("updating a registry") + refNode.Value = newVersion + return true +} + +func parseRegistryNode(logE *logrus.Entry, node ast.Node, newVersions map[string]string) (bool, error) { //nolint:gocognit,cyclop,funlen + mvs, err := normalizeMappingValueNodes(node) + if err != nil { + return false, err + } + var refNode *ast.StringNode + var newVersion string + var rgstName string + for _, mvn := range mvs { + switch mvn.Key.String() { + case "ref": + sn, ok := mvn.Value.(*ast.StringNode) + if !ok { + return false, errors.New("ref must be a string") + } + if newVersion == "" { + refNode = sn + continue + } + return updateRegistryVersion(logE, sn, rgstName, newVersion), nil + case "type": + sn, ok := mvn.Value.(*ast.StringNode) + if !ok { + return false, errors.New("type must be a string") + } + if sn.Value != typeStandard && sn.Value != "github_content" { + break + } + if sn.Value != typeStandard { + continue + } + if rgstName == "" { + rgstName = typeStandard + } + continue + case "name": + sn, ok := mvn.Value.(*ast.StringNode) + if !ok { + return false, errors.New("name must be a string") + } + version, ok := newVersions[sn.Value] + if !ok { + return false, nil + } + if refNode == nil { + rgstName = sn.Value + newVersion = version + continue + } + return updateRegistryVersion(logE, refNode, sn.Value, version), nil + default: + continue // Ignore unknown fields + } + } + if refNode == nil || rgstName == "" { + return false, nil + } + version, ok := newVersions[rgstName] + if !ok { + return false, nil + } + return updateRegistryVersion(logE, refNode, rgstName, version), nil +} + +func UpdateRegistries(logE *logrus.Entry, file *ast.File, newVersions map[string]string) (bool, error) { + body := file.Docs[0].Body // DocumentNode + mv, err := findMappingValueFromNode(body, "registries") + if err != nil { + return false, err + } + + seq, ok := mv.Value.(*ast.SequenceNode) + if !ok { + return false, errors.New("the value must be a sequence node") + } + updated := false + for _, value := range seq.Values { + up, err := parseRegistryNode(logE, value, newVersions) + if err != nil { + return false, err + } + if up { + updated = true + } + } + return updated, nil +} diff --git a/pkg/controller/update/ast/registry_test.go b/pkg/controller/update/ast/registry_test.go new file mode 100644 index 000000000..bc66d3d09 --- /dev/null +++ b/pkg/controller/update/ast/registry_test.go @@ -0,0 +1,82 @@ +package ast_test + +import ( + "testing" + + "github.com/aquaproj/aqua/v2/pkg/controller/update/ast" + "github.com/goccy/go-yaml/parser" + "github.com/google/go-cmp/cmp" + "github.com/sirupsen/logrus" +) + +func TestUpdateRegistries(t *testing.T) { //nolint:funlen + t.Parallel() + data := []struct { + name string + updated bool + isErr bool + file string + newVersions map[string]string + expFile string + }{ + { + name: "updated", + file: `registries: + - type: standard + ref: v4.0.0 + # foo + - name: local + type: local + path: registry.yaml + - name: custom + type: github_content + repo_owner: suzuki-shunsuke + repo_name: aqua-registry + ref: v3.0.0 + path: registry.yaml +`, + expFile: `registries: + - type: standard + ref: v4.5.0 + # foo + - name: local + type: local + path: registry.yaml + - name: custom + type: github_content + repo_owner: suzuki-shunsuke + repo_name: aqua-registry + ref: v4.0.0 + path: registry.yaml +`, + newVersions: map[string]string{ + "standard": "v4.5.0", + "custom": "v4.0.0", + }, + updated: true, + }, + } + logE := logrus.NewEntry(logrus.New()) + for _, d := range data { + d := d + t.Run(d.name, func(t *testing.T) { + t.Parallel() + file, err := parser.ParseBytes([]byte(d.file), parser.ParseComments) + if err != nil { + t.Fatal(err) + } + updated, err := ast.UpdateRegistries(logE, file, d.newVersions) + if err != nil { + if !d.isErr { + t.Fatal(err) + } + } + if updated != d.updated { + t.Fatalf("updated: wanted %v, got %v", d.updated, updated) + } + if diff := cmp.Diff(file.String(), d.expFile); diff != "" { + t.Fatal(diff) + } + }) + } +} diff --git a/pkg/controller/update/ast/var.go b/pkg/controller/update/ast/var.go new file mode 100644 index 000000000..ad5b99d04 --- /dev/null +++ b/pkg/controller/update/ast/var.go @@ -0,0 +1,6 @@ +package ast + +import "regexp" + +// github_archive, github_content, go_build, go_install, http +var commitHashPattern = regexp.MustCompile(`\b[0-9a-f]{40}\b`) diff --git a/pkg/controller/update/controller.go b/pkg/controller/update/controller.go new file mode 100644 index 000000000..a635a48a7 --- /dev/null +++ b/pkg/controller/update/controller.go @@ -0,0 +1,70 @@ +package update + +import ( + "context" + + "github.com/aquaproj/aqua/v2/pkg/config" + "github.com/aquaproj/aqua/v2/pkg/config/aqua" + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/controller/which" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" + "github.com/aquaproj/aqua/v2/pkg/github" + rgst "github.com/aquaproj/aqua/v2/pkg/install-registry" + "github.com/aquaproj/aqua/v2/pkg/runtime" + "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +type Controller struct { + gh RepositoriesService + rootDir string + configFinder ConfigFinder + configReader ConfigReader + registryInstaller rgst.Installer + fs afero.Fs + runtime *runtime.Runtime + requireChecksum bool + fuzzyGetter FuzzyGetter + fuzzyFinder FuzzyFinder + which which.Controller +} + +type FuzzyFinder interface { + Find(items []*fuzzyfinder.Item, hasPreview bool) (int, error) + FindMulti(items []*fuzzyfinder.Item, hasPreview bool) ([]int, error) +} + +type ConfigReader interface { + Read(configFilePath string, cfg *aqua.Config) error + ReadToUpdate(configFilePath string, cfg *aqua.Config) (map[string]*aqua.Config, error) +} + +type FuzzyGetter interface { + Get(ctx context.Context, logE *logrus.Entry, pkg *registry.PackageInfo, currentVersion string, useFinder bool) string +} + +type RepositoriesService interface { + GetLatestRelease(ctx context.Context, repoOwner, repoName string) (*github.RepositoryRelease, *github.Response, error) + ListReleases(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error) + ListTags(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.RepositoryTag, *github.Response, error) +} + +func New(param *config.Param, gh RepositoriesService, configFinder ConfigFinder, configReader ConfigReader, registInstaller rgst.Installer, fs afero.Fs, rt *runtime.Runtime, fuzzyGetter FuzzyGetter, fuzzyFinder FuzzyFinder, whichController which.Controller) *Controller { + return &Controller{ + gh: gh, + rootDir: param.RootDir, + configFinder: configFinder, + configReader: configReader, + registryInstaller: registInstaller, + fs: fs, + runtime: rt, + requireChecksum: param.RequireChecksum, + fuzzyGetter: fuzzyGetter, + fuzzyFinder: fuzzyFinder, + which: whichController, + } +} + +type ConfigFinder interface { + Find(wd, configFilePath string, globalConfigFilePaths ...string) (string, error) +} diff --git a/pkg/controller/update/ignore_commit_hash.go b/pkg/controller/update/ignore_commit_hash.go new file mode 100644 index 000000000..9eb732421 --- /dev/null +++ b/pkg/controller/update/ignore_commit_hash.go @@ -0,0 +1,6 @@ +package update + +import "regexp" + +// github_archive, github_content, go_build, go_install, http +var commitHashPattern = regexp.MustCompile(`\b[0-9a-f]{40}\b`) diff --git a/pkg/controller/update/package.go b/pkg/controller/update/package.go new file mode 100644 index 000000000..218b3153d --- /dev/null +++ b/pkg/controller/update/package.go @@ -0,0 +1,152 @@ +package update + +import ( + "context" + "errors" + "fmt" + + "github.com/aquaproj/aqua/v2/pkg/config" + "github.com/aquaproj/aqua/v2/pkg/config/aqua" + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/controller/update/ast" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" + "github.com/goccy/go-yaml/parser" + "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +func (c *Controller) updatePackages(ctx context.Context, logE *logrus.Entry, param *config.Param, cfgFilePath string, rgstCfgs map[string]*registry.Config) error { + newVersions := map[string]string{} + updatedPkgs := map[string]struct{}{} + if param.Insert { + pkgs, err := c.selectPackages(logE, cfgFilePath) + if err != nil { + return err + } + if pkgs == nil { + return nil + } + updatedPkgs = pkgs + } + cfg := &aqua.Config{} + cfgs, err := c.configReader.ReadToUpdate(cfgFilePath, cfg) + if err != nil { + return fmt.Errorf("read a configuration file: %w", err) + } + cfgs[cfgFilePath] = cfg + for cfgPath, cfg := range cfgs { + if err := c.updatePackagesInFile(ctx, logE, param, cfgPath, cfg, rgstCfgs, updatedPkgs, newVersions); err != nil { + return err + } + } + return nil +} + +func (c *Controller) updatePackagesInFile(ctx context.Context, logE *logrus.Entry, param *config.Param, cfgFilePath string, cfg *aqua.Config, rgstCfgs map[string]*registry.Config, updatedPkgs map[string]struct{}, newVersions map[string]string) error { + pkgs, failed := config.ListPackages(logE, cfg, c.runtime, rgstCfgs) + if len(pkgs) == 0 { + if failed { + return errors.New("list packages") + } + return nil + } + for _, pkg := range pkgs { + logE := logE.WithFields(logrus.Fields{ + "package_name": pkg.Package.Name, + "package_version": pkg.Package.Version, + "registry": pkg.Package.Registry, + }) + if newVersion := c.getPackageNewVersion(ctx, logE, param, updatedPkgs, pkg); newVersion != "" { + newVersions[fmt.Sprintf("%s,%s", pkg.Package.Registry, pkg.PackageInfo.GetName())] = newVersion + newVersions[fmt.Sprintf("%s,%s", pkg.Package.Registry, pkg.Package.Name)] = newVersion + } + } + if err := c.updateFile(logE, cfgFilePath, newVersions); err != nil { + return fmt.Errorf("update a package: %w", err) + } + return nil +} + +func (c *Controller) getPackageNewVersion(ctx context.Context, logE *logrus.Entry, param *config.Param, updatedPkgs map[string]struct{}, pkg *config.Package) string { + if len(updatedPkgs) != 0 { + var item string + if pkg.Package.Registry != "standard" { + item = fmt.Sprintf("%s,%s@%s", pkg.Package.Registry, pkg.Package.Name, pkg.Package.Version) + } else { + item = fmt.Sprintf("%s@%s", pkg.Package.Name, pkg.Package.Version) + } + if _, ok := updatedPkgs[item]; !ok { + return "" + } + } + return c.fuzzyGetter.Get(ctx, logE, pkg.PackageInfo, pkg.Package.Version, param.SelectVersion) +} + +func (c *Controller) selectPackages(logE *logrus.Entry, cfgFilePath string) (map[string]struct{}, error) { + updatedPkgs := map[string]struct{}{} + cfg := &aqua.Config{} + if err := c.configReader.Read(cfgFilePath, cfg); err != nil { + return nil, fmt.Errorf("read a configuration file: %w", err) + } + items := make([]*fuzzyfinder.Item, 0, len(cfg.Packages)) + for _, pkg := range cfg.Packages { + if commitHashPattern.MatchString(pkg.Version) { + logE.WithFields(logrus.Fields{ + "registry_name": pkg.Registry, + "package_name": pkg.Name, + "package_version": pkg.Version, + }).Debug("skip a package whose version is a commit hash") + continue + } + var item string + if pkg.Registry != "standard" { + item = fmt.Sprintf("%s,%s@%s", pkg.Registry, pkg.Name, pkg.Version) + } else { + item = fmt.Sprintf("%s@%s", pkg.Name, pkg.Version) + } + items = append(items, &fuzzyfinder.Item{ + Item: item, + }) + } + idxs, err := c.fuzzyFinder.FindMulti(items, false) + if err != nil { + if errors.Is(err, fuzzyfinder.ErrAbort) { + return nil, nil //nolint:nilnil + } + return nil, fmt.Errorf("select updated packages with fuzzy finder: %w", err) + } + for _, idx := range idxs { + updatedPkgs[items[idx].Item] = struct{}{} + } + return updatedPkgs, nil +} + +func (c *Controller) updateFile(logE *logrus.Entry, cfgFilePath string, newVersions map[string]string) error { + b, err := afero.ReadFile(c.fs, cfgFilePath) + if err != nil { + return fmt.Errorf("read a configuration file: %w", err) + } + + file, err := parser.ParseBytes(b, parser.ParseComments) + if err != nil { + return fmt.Errorf("parse configuration file as YAML: %w", err) + } + + updated, err := ast.UpdatePackages(logE, file, newVersions) + if err != nil { + return fmt.Errorf("parse a file with AST: %w", err) + } + + if !updated { + return nil + } + + stat, err := c.fs.Stat(cfgFilePath) + if err != nil { + return fmt.Errorf("get configuration file stat: %w", err) + } + if err := afero.WriteFile(c.fs, cfgFilePath, []byte(file.String()), stat.Mode()); err != nil { + return fmt.Errorf("write the configuration file: %w", err) + } + return nil +} diff --git a/pkg/controller/update/registry.go b/pkg/controller/update/registry.go new file mode 100644 index 000000000..901560bb7 --- /dev/null +++ b/pkg/controller/update/registry.go @@ -0,0 +1,74 @@ +package update + +import ( + "context" + "fmt" + + "github.com/aquaproj/aqua/v2/pkg/config/aqua" + "github.com/aquaproj/aqua/v2/pkg/controller/update/ast" + "github.com/goccy/go-yaml/parser" + "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +func (c *Controller) newRegistryVersion(ctx context.Context, logE *logrus.Entry, rgst *aqua.Registry) (string, error) { + if rgst.Type == "local" { + return "", nil + } + + logE.Debug("getting the latest release") + release, _, err := c.gh.GetLatestRelease(ctx, rgst.RepoOwner, rgst.RepoName) + if err != nil { + return "", fmt.Errorf("get the latest release by GitHub API: %w", err) + } + // TODO Get the latest tag if the latest release can't be got. + return release.GetTagName(), nil +} + +func (c *Controller) updateRegistries(ctx context.Context, logE *logrus.Entry, cfgFilePath string, cfg *aqua.Config) error { //nolint:cyclop + newVersions := map[string]string{} + for _, rgst := range cfg.Registries { + if commitHashPattern.MatchString(rgst.Ref) { + logE.WithFields(logrus.Fields{ + "registry_name": rgst.Name, + "registry_version": rgst.Ref, + }).Debug("skip a registry whose version is a commit hash") + continue + } + newVersion, err := c.newRegistryVersion(ctx, logE, rgst) + if err != nil { + return err + } + if newVersion == "" { + continue + } + newVersions[rgst.Name] = newVersion + } + + b, err := afero.ReadFile(c.fs, cfgFilePath) + if err != nil { + return fmt.Errorf("read a configuration file: %w", err) + } + + file, err := parser.ParseBytes(b, parser.ParseComments) + if err != nil { + return fmt.Errorf("parse configuration file as YAML: %w", err) + } + + // TODO consider how to update commit hashes + updated, err := ast.UpdateRegistries(logE, file, newVersions) + if err != nil { + return fmt.Errorf("parse a configuration as YAML to update registries: %w", err) + } + + if updated { + stat, err := c.fs.Stat(cfgFilePath) + if err != nil { + return fmt.Errorf("get configuration file stat: %w", err) + } + if err := afero.WriteFile(c.fs, cfgFilePath, []byte(file.String()), stat.Mode()); err != nil { + return fmt.Errorf("write the configuration file: %w", err) + } + } + return nil +} diff --git a/pkg/controller/update/update.go b/pkg/controller/update/update.go new file mode 100644 index 000000000..4f50dc398 --- /dev/null +++ b/pkg/controller/update/update.go @@ -0,0 +1,101 @@ +package update + +import ( + "context" + "fmt" + + "github.com/aquaproj/aqua/v2/pkg/checksum" + "github.com/aquaproj/aqua/v2/pkg/config" + finder "github.com/aquaproj/aqua/v2/pkg/config-finder" + "github.com/aquaproj/aqua/v2/pkg/config/aqua" + "github.com/sirupsen/logrus" +) + +func (c *Controller) Update(ctx context.Context, logE *logrus.Entry, param *config.Param) error { + if err := c.updateCommand(ctx, logE, param); err != nil { + return err + } + if len(param.Args) != 0 { + return nil + } + cfgFilePath, err := c.configFinder.Find(param.PWD, param.ConfigFilePath) + if err != nil { + return fmt.Errorf("find a configuration file: %w", err) + } + if err := c.update(ctx, logE, param, cfgFilePath); err != nil { + return err + } + return nil +} + +func (c *Controller) updateCommand(ctx context.Context, logE *logrus.Entry, param *config.Param) error { + newVersions := map[string]string{} + for _, arg := range param.Args { + findResult, err := c.which.Which(ctx, logE, param, arg) + if err != nil { + return fmt.Errorf("find a command: %w", err) + } + pkg := findResult.Package + if newVersion := c.getPackageNewVersion(ctx, logE, param, nil, pkg); newVersion != "" { + newVersions[fmt.Sprintf("%s,%s", pkg.Package.Registry, pkg.PackageInfo.GetName())] = newVersion + newVersions[fmt.Sprintf("%s,%s", pkg.Package.Registry, pkg.Package.Name)] = newVersion + } + filePath := findResult.ConfigFilePath + if pkg.Package.FilePath != "" { + filePath = pkg.Package.FilePath + } + if err := c.updateFile(logE, filePath, newVersions); err != nil { + return fmt.Errorf("update a package: %w", err) + } + } + return nil +} + +func (c *Controller) update(ctx context.Context, logE *logrus.Entry, param *config.Param, cfgFilePath string) error { //nolint:cyclop + cfg := &aqua.Config{} + if cfgFilePath == "" { + return finder.ErrConfigFileNotFound + } + if err := c.configReader.Read(cfgFilePath, cfg); err != nil { + return fmt.Errorf("read a configuration file: %w", err) + } + + var checksums *checksum.Checksums + if cfg.ChecksumEnabled() { + checksums = checksum.New() + checksumFilePath, err := checksum.GetChecksumFilePathFromConfigFilePath(c.fs, cfgFilePath) + if err != nil { + return err //nolint:wrapcheck + } + if err := checksums.ReadFile(c.fs, checksumFilePath); err != nil { + return fmt.Errorf("read a checksum JSON: %w", err) + } + defer func() { + if err := checksums.UpdateFile(c.fs, checksumFilePath); err != nil { + logE.WithError(err).Error("update a checksum file") + } + }() + } + + // Update packages before registries because if registries are updated before packages the function needs to install new registries then checksums of new registrires aren't added to aqua-checksums.json. + + if !param.OnlyRegistry { + registryConfigs, err := c.registryInstaller.InstallRegistries(ctx, logE, cfg, cfgFilePath, checksums) + if err != nil { + return err //nolint:wrapcheck + } + + if err := c.updatePackages(ctx, logE, param, cfgFilePath, registryConfigs); err != nil { + return err + } + } + + if param.Insert || param.OnlyPackage || len(param.Args) != 0 { + return nil + } + + if err := c.updateRegistries(ctx, logE, cfgFilePath, cfg); err != nil { + return fmt.Errorf("update registries: %w", err) + } + return nil +} diff --git a/pkg/controller/update/update_test.go b/pkg/controller/update/update_test.go new file mode 100644 index 000000000..fa5382de2 --- /dev/null +++ b/pkg/controller/update/update_test.go @@ -0,0 +1,459 @@ +package update_test + +import ( + "context" + "testing" + + "github.com/aquaproj/aqua/v2/pkg/config" + finder "github.com/aquaproj/aqua/v2/pkg/config-finder" + reader "github.com/aquaproj/aqua/v2/pkg/config-reader" + "github.com/aquaproj/aqua/v2/pkg/config/aqua" + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/controller/update" + "github.com/aquaproj/aqua/v2/pkg/controller/which" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" + "github.com/aquaproj/aqua/v2/pkg/github" + rgst "github.com/aquaproj/aqua/v2/pkg/install-registry" + "github.com/aquaproj/aqua/v2/pkg/ptr" + "github.com/aquaproj/aqua/v2/pkg/runtime" + "github.com/aquaproj/aqua/v2/pkg/testutil" + "github.com/aquaproj/aqua/v2/pkg/versiongetter" + "github.com/google/go-cmp/cmp" + "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +func TestController_Update(t *testing.T) { //nolint:funlen,maintidx + t.Parallel() + data := []struct { + name string + isErr bool + files map[string]string + expFiles map[string]string + param *config.Param + releases []*github.RepositoryRelease + tags []*github.RepositoryTag + rt *runtime.Runtime + idxs []int + fuzzyFinderErr error + findResults map[string]*which.FindResult + registries map[string]*registry.Config + versions map[string]string + }{ + { + name: "update commands", + param: &config.Param{ + Args: []string{"tfcmt", "gh"}, + }, + versions: map[string]string{ + "suzuki-shunsuke/tfcmt": "v4.0.0", + "cli/cli": "v2.30.0", + }, + files: map[string]string{ + "/workspace/aqua.yaml": `registries: +- type: standard + ref: v4.0.0 +packages: +- name: suzuki-shunsuke/tfcmt@v3.0.0 +- name: cli/cli@v2.0.0 +`, + }, + expFiles: map[string]string{ + "/workspace/aqua.yaml": `registries: +- type: standard + ref: v4.0.0 +packages: +- name: suzuki-shunsuke/tfcmt@v4.0.0 +- name: cli/cli@v2.30.0 +`, + }, + findResults: map[string]*which.FindResult{ + "tfcmt": { + Package: &config.Package{ + Package: &aqua.Package{ + Name: "suzuki-shunsuke/tfcmt", + Registry: "standard", + Version: "v3.0.0", + }, + PackageInfo: ®istry.PackageInfo{ + Type: "github_release", + RepoOwner: "suzuki-shunsuke", + RepoName: "tfcmt", + Asset: "tfcmt_{{.OS}}_{{.Arch}}.tar.gz", + }, + Registry: &aqua.Registry{ + Name: "standard", + Type: "github_content", + RepoOwner: "aquaproj", + RepoName: "aqua-registry", + Ref: "v4.0.0", + Path: "registry.yaml", + }, + }, + File: ®istry.File{ + Name: "tfcmt", + }, + Config: &aqua.Config{}, + ExePath: "/home/foo/.local/share/aquaproj-aqua/pkgs/github_release/github.com/suzuki-shunsuke/tfcmt/v3.0.0/tfcmt_darwin_arm64.tar.gz/tfcmt", + ConfigFilePath: "/workspace/aqua.yaml", + }, + "gh": { + Package: &config.Package{ + Package: &aqua.Package{ + Name: "cli/cli", + Registry: "standard", + Version: "v2.0.0", + }, + PackageInfo: ®istry.PackageInfo{ + Type: "github_release", + RepoOwner: "cli", + RepoName: "cli", + Asset: "gh_{{trimV .Version}}_{{.OS}}_{{.Arch}}.zip", + Files: []*registry.File{ + { + Name: "gh", + Src: "{{.AssetWithoutExt}}/bin/gh", + }, + }, + }, + Registry: &aqua.Registry{ + Name: "standard", + Type: "github_content", + RepoOwner: "aquaproj", + RepoName: "aqua-registry", + Ref: "v4.0.0", + Path: "registry.yaml", + }, + }, + File: ®istry.File{ + Name: "gh", + Src: "{{.AssetWithoutExt}}/bin/gh", + }, + Config: &aqua.Config{}, + ExePath: "/home/foo/.local/share/aquaproj-aqua/pkgs/github_release/github.com/cli/cli/v2.0.0/gh_2.0.0_macOS_arm64.zip/gh_2.0.0_macOS_arm64/bin/gh", + ConfigFilePath: "/workspace/aqua.yaml", + }, + }, + }, + { + name: "no arg", + rt: &runtime.Runtime{ + GOOS: "darwin", + GOARCH: "arm64", + }, + param: &config.Param{ + PWD: "/workspace", + }, + versions: map[string]string{ + "suzuki-shunsuke/tfcmt": "v4.0.0", + "cli/cli": "v2.30.0", + }, + registries: map[string]*registry.Config{ + "standard": { + PackageInfos: registry.PackageInfos{ + { + Type: "github_release", + RepoOwner: "suzuki-shunsuke", + RepoName: "tfcmt", + Asset: "tfcmt_{{.OS}}_{{.Arch}}.tar.gz", + }, + { + Type: "github_release", + RepoOwner: "cli", + RepoName: "cli", + Asset: "gh_{{trimV .Version}}_{{.OS}}_{{.Arch}}.zip", + Files: []*registry.File{ + { + Name: "gh", + Src: "{{.AssetWithoutExt}}/bin/gh", + }, + }, + }, + }, + }, + }, + files: map[string]string{ + "/workspace/aqua.yaml": `checksum: + enabled: true +registries: +- type: standard + ref: v4.0.0 +packages: +- name: suzuki-shunsuke/tfcmt@v3.0.0 +- name: cli/cli@v2.0.0 +`, + }, + expFiles: map[string]string{ + "/workspace/aqua.yaml": `checksum: + enabled: true +registries: +- type: standard + ref: v4.60.0 +packages: +- name: suzuki-shunsuke/tfcmt@v4.0.0 +- name: cli/cli@v2.30.0 +`, + }, + releases: []*github.RepositoryRelease{ + { + TagName: ptr.String("v4.60.0"), + }, + }, + }, + { + name: "only registry", + param: &config.Param{ + PWD: "/workspace", + OnlyRegistry: true, + }, + files: map[string]string{ + "/workspace/aqua.yaml": `registries: +- type: standard + ref: v4.0.0 +packages: +- name: suzuki-shunsuke/tfcmt@v3.0.0 +- name: cli/cli@v2.0.0 +`, + }, + expFiles: map[string]string{ + "/workspace/aqua.yaml": `registries: +- type: standard + ref: v4.60.0 +packages: +- name: suzuki-shunsuke/tfcmt@v3.0.0 +- name: cli/cli@v2.0.0 +`, + }, + releases: []*github.RepositoryRelease{ + { + TagName: ptr.String("v4.60.0"), + }, + }, + }, + { + name: "only package", + rt: &runtime.Runtime{ + GOOS: "darwin", + GOARCH: "arm64", + }, + param: &config.Param{ + PWD: "/workspace", + OnlyPackage: true, + }, + versions: map[string]string{ + "suzuki-shunsuke/tfcmt": "v4.0.0", + "cli/cli": "v2.30.0", + }, + registries: map[string]*registry.Config{ + "standard": { + PackageInfos: registry.PackageInfos{ + { + Type: "github_release", + RepoOwner: "suzuki-shunsuke", + RepoName: "tfcmt", + Asset: "tfcmt_{{.OS}}_{{.Arch}}.tar.gz", + }, + { + Type: "github_release", + RepoOwner: "cli", + RepoName: "cli", + Asset: "gh_{{trimV .Version}}_{{.OS}}_{{.Arch}}.zip", + Files: []*registry.File{ + { + Name: "gh", + Src: "{{.AssetWithoutExt}}/bin/gh", + }, + }, + }, + }, + }, + }, + files: map[string]string{ + "/workspace/aqua.yaml": `registries: +- type: standard + ref: v4.0.0 +packages: +- name: suzuki-shunsuke/tfcmt@v3.0.0 +- name: cli/cli@v2.0.0 +`, + }, + expFiles: map[string]string{ + "/workspace/aqua.yaml": `registries: +- type: standard + ref: v4.0.0 +packages: +- name: suzuki-shunsuke/tfcmt@v4.0.0 +- name: cli/cli@v2.30.0 +`, + }, + }, + { + name: "select packages", + rt: &runtime.Runtime{ + GOOS: "darwin", + GOARCH: "arm64", + }, + param: &config.Param{ + PWD: "/workspace", + Insert: true, + }, + versions: map[string]string{ + "suzuki-shunsuke/tfcmt": "v4.0.0", + "cli/cli": "v2.30.0", + }, + registries: map[string]*registry.Config{ + "standard": { + PackageInfos: registry.PackageInfos{ + { + Type: "github_release", + RepoOwner: "suzuki-shunsuke", + RepoName: "tfcmt", + Asset: "tfcmt_{{.OS}}_{{.Arch}}.tar.gz", + }, + { + Type: "github_release", + RepoOwner: "cli", + RepoName: "cli", + Asset: "gh_{{trimV .Version}}_{{.OS}}_{{.Arch}}.zip", + Files: []*registry.File{ + { + Name: "gh", + Src: "{{.AssetWithoutExt}}/bin/gh", + }, + }, + }, + }, + }, + }, + idxs: []int{1}, // cli/cli + files: map[string]string{ + "/workspace/aqua.yaml": `registries: +- type: standard + ref: v4.0.0 +packages: +- name: suzuki-shunsuke/tfcmt@v3.0.0 +- name: cli/cli@v2.0.0 +`, + }, + expFiles: map[string]string{ + "/workspace/aqua.yaml": `registries: +- type: standard + ref: v4.0.0 +packages: +- name: suzuki-shunsuke/tfcmt@v3.0.0 +- name: cli/cli@v2.30.0 +`, + }, + }, + { + name: "ignore commit hash", + rt: &runtime.Runtime{ + GOOS: "darwin", + GOARCH: "arm64", + }, + param: &config.Param{ + PWD: "/workspace", + }, + versions: map[string]string{ + "suzuki-shunsuke/tfcmt": "v4.0.0", + "cli/cli": "v2.30.0", + }, + registries: map[string]*registry.Config{ + "standard": { + PackageInfos: registry.PackageInfos{ + { + Type: "github_release", + RepoOwner: "suzuki-shunsuke", + RepoName: "tfcmt", + Asset: "tfcmt_{{.OS}}_{{.Arch}}.tar.gz", + }, + { + Type: "github_release", + RepoOwner: "cli", + RepoName: "cli", + Asset: "gh_{{trimV .Version}}_{{.OS}}_{{.Arch}}.zip", + Files: []*registry.File{ + { + Name: "gh", + Src: "{{.AssetWithoutExt}}/bin/gh", + }, + }, + }, + }, + }, + }, + files: map[string]string{ + "/workspace/aqua.yaml": `checksum: + enabled: true +registries: +- type: standard + ref: 4da26b32f72963f42a04b099d03604dab32c6844 +packages: +- name: suzuki-shunsuke/tfcmt@4da26b32f72963f42a04b099d03604dab32c6844 +- name: cli/cli@v2.0.0 +`, + }, + expFiles: map[string]string{ + "/workspace/aqua.yaml": `checksum: + enabled: true +registries: +- type: standard + ref: 4da26b32f72963f42a04b099d03604dab32c6844 +packages: +- name: suzuki-shunsuke/tfcmt@4da26b32f72963f42a04b099d03604dab32c6844 +- name: cli/cli@v2.30.0 +`, + }, + releases: []*github.RepositoryRelease{ + { + TagName: ptr.String("v4.60.0"), + }, + }, + }, + } + ctx := context.Background() + logE := logrus.NewEntry(logrus.New()) + for _, d := range data { + d := d + t.Run(d.name, func(t *testing.T) { + t.Parallel() + fs, err := testutil.NewFs(d.files) + if err != nil { + t.Fatal(err) + } + gh := &github.MockRepositoriesService{ + Releases: d.releases, + Tags: d.tags, + } + configReader := reader.New(fs, d.param) + configFinder := finder.NewConfigFinder(fs) + registryInstaller := &rgst.MockInstaller{ + M: d.registries, + } + fuzzyFinder := fuzzyfinder.NewMock(d.idxs, d.fuzzyFinderErr) + whichCtrl := &which.MockMultiController{ + FindResults: d.findResults, + } + fuzzyGetter := versiongetter.NewMockFuzzyGetter(d.versions) + ctrl := update.New(d.param, gh, configFinder, configReader, registryInstaller, fs, d.rt, fuzzyGetter, fuzzyFinder, whichCtrl) + if err := ctrl.Update(ctx, logE, d.param); err != nil { + if d.isErr { + return + } + t.Fatal(err) + } + if d.isErr { + t.Fatal("error must be returned") + } + for path, expBody := range d.expFiles { + b, err := afero.ReadFile(fs, path) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expBody, string(b)); diff != "" { + t.Fatal(diff) + } + } + }) + } +} diff --git a/pkg/controller/which/which.go b/pkg/controller/which/which.go index c7cbab2e7..2c0b0b039 100644 --- a/pkg/controller/which/which.go +++ b/pkg/controller/which/which.go @@ -2,6 +2,7 @@ package which import ( "context" + "errors" "fmt" "io" "os" @@ -59,6 +60,18 @@ func (c *MockController) Which(ctx context.Context, logE *logrus.Entry, param *c return c.FindResult, c.Err } +type MockMultiController struct { + FindResults map[string]*FindResult +} + +func (c *MockMultiController) Which(ctx context.Context, logE *logrus.Entry, param *config.Param, exeName string) (*FindResult, error) { + fr, ok := c.FindResults[exeName] + if !ok { + return nil, errors.New("command isn't found") + } + return fr, nil +} + type FindResult struct { Package *config.Package File *registry.File diff --git a/pkg/controller/wire.go b/pkg/controller/wire.go index 9d0063755..0b0809c7b 100644 --- a/pkg/controller/wire.go +++ b/pkg/controller/wire.go @@ -26,6 +26,7 @@ import ( "github.com/aquaproj/aqua/v2/pkg/controller/install" "github.com/aquaproj/aqua/v2/pkg/controller/list" "github.com/aquaproj/aqua/v2/pkg/controller/remove" + "github.com/aquaproj/aqua/v2/pkg/controller/update" "github.com/aquaproj/aqua/v2/pkg/controller/updateaqua" "github.com/aquaproj/aqua/v2/pkg/controller/updatechecksum" "github.com/aquaproj/aqua/v2/pkg/controller/which" @@ -42,6 +43,7 @@ import ( "github.com/aquaproj/aqua/v2/pkg/runtime" "github.com/aquaproj/aqua/v2/pkg/slsa" "github.com/aquaproj/aqua/v2/pkg/unarchive" + "github.com/aquaproj/aqua/v2/pkg/versiongetter" "github.com/google/wire" "github.com/spf13/afero" @@ -151,6 +153,8 @@ func InitializeGenerateCommandController(ctx context.Context, param *config.Para wire.Bind(new(generate.RepositoriesService), new(*github.RepositoriesServiceImpl)), wire.Bind(new(download.GitHubContentAPI), new(*github.RepositoriesServiceImpl)), wire.Bind(new(github.RepositoriesService), new(*github.RepositoriesServiceImpl)), + wire.Bind(new(versiongetter.GitHubTagClient), new(*github.RepositoriesServiceImpl)), + wire.Bind(new(versiongetter.GitHubReleaseClient), new(*github.RepositoriesServiceImpl)), ), wire.NewSet( registry.New, @@ -168,6 +172,7 @@ func InitializeGenerateCommandController(ctx context.Context, param *config.Para wire.NewSet( fuzzyfinder.New, wire.Bind(new(generate.FuzzyFinder), new(*fuzzyfinder.Finder)), + wire.Bind(new(versiongetter.FuzzyFinder), new(*fuzzyfinder.Finder)), ), download.NewHTTPDownloader, wire.NewSet( @@ -195,7 +200,19 @@ func InitializeGenerateCommandController(ctx context.Context, param *config.Para wire.NewSet( cargo.NewClientImpl, wire.Bind(new(cargo.Client), new(*cargo.ClientImpl)), + wire.Bind(new(versiongetter.CargoClient), new(*cargo.ClientImpl)), ), + wire.NewSet( + versiongetter.NewFuzzy, + wire.Bind(new(generate.FuzzyGetter), new(*versiongetter.FuzzyGetter)), + ), + wire.NewSet( + versiongetter.NewGeneralVersionGetter, + wire.Bind(new(versiongetter.VersionGetter), new(*versiongetter.GeneralVersionGetter)), + ), + versiongetter.NewCargo, + versiongetter.NewGitHubRelease, + versiongetter.NewGitHubTag, ) return &generate.Controller{} } @@ -713,6 +730,92 @@ func InitializeUpdateChecksumCommandController(ctx context.Context, param *confi return &updatechecksum.Controller{} } +func InitializeUpdateCommandController(ctx context.Context, param *config.Param, httpClient *http.Client, rt *runtime.Runtime) *update.Controller { + wire.Build( + update.New, + wire.NewSet( + finder.NewConfigFinder, + wire.Bind(new(update.ConfigFinder), new(*finder.ConfigFinder)), + wire.Bind(new(which.ConfigFinder), new(*finder.ConfigFinder)), + ), + wire.NewSet( + reader.New, + wire.Bind(new(reader.ConfigReader), new(*reader.ConfigReaderImpl)), + wire.Bind(new(update.ConfigReader), new(*reader.ConfigReaderImpl)), + ), + wire.NewSet( + registry.New, + wire.Bind(new(registry.Installer), new(*registry.InstallerImpl)), + ), + wire.NewSet( + github.New, + wire.Bind(new(github.RepositoriesService), new(*github.RepositoriesServiceImpl)), + wire.Bind(new(update.RepositoriesService), new(*github.RepositoriesServiceImpl)), + wire.Bind(new(download.GitHubContentAPI), new(*github.RepositoriesServiceImpl)), + wire.Bind(new(versiongetter.GitHubTagClient), new(*github.RepositoriesServiceImpl)), + wire.Bind(new(versiongetter.GitHubReleaseClient), new(*github.RepositoriesServiceImpl)), + ), + wire.NewSet( + download.NewGitHubContentFileDownloader, + wire.Bind(new(domain.GitHubContentFileDownloader), new(*download.GitHubContentFileDownloader)), + ), + download.NewHTTPDownloader, + wire.NewSet( + download.NewDownloader, + wire.Bind(new(download.ClientAPI), new(*download.Downloader)), + ), + afero.NewOsFs, + wire.NewSet( + cosign.NewVerifier, + wire.Bind(new(cosign.Verifier), new(*cosign.VerifierImpl)), + ), + wire.NewSet( + exec.New, + wire.Bind(new(cosign.Executor), new(*exec.Executor)), + wire.Bind(new(slsa.CommandExecutor), new(*exec.Executor)), + ), + wire.NewSet( + slsa.New, + wire.Bind(new(slsa.Verifier), new(*slsa.VerifierImpl)), + ), + wire.NewSet( + slsa.NewExecutor, + wire.Bind(new(slsa.Executor), new(*slsa.ExecutorImpl)), + ), + wire.NewSet( + versiongetter.NewFuzzy, + wire.Bind(new(update.FuzzyGetter), new(*versiongetter.FuzzyGetter)), + ), + wire.NewSet( + fuzzyfinder.New, + wire.Bind(new(update.FuzzyFinder), new(*fuzzyfinder.Finder)), + wire.Bind(new(versiongetter.FuzzyFinder), new(*fuzzyfinder.Finder)), + ), + wire.NewSet( + versiongetter.NewGeneralVersionGetter, + wire.Bind(new(versiongetter.VersionGetter), new(*versiongetter.GeneralVersionGetter)), + ), + versiongetter.NewCargo, + versiongetter.NewGitHubRelease, + versiongetter.NewGitHubTag, + wire.NewSet( + cargo.NewClientImpl, + wire.Bind(new(cargo.Client), new(*cargo.ClientImpl)), + wire.Bind(new(versiongetter.CargoClient), new(*cargo.ClientImpl)), + ), + wire.NewSet( + which.New, + wire.Bind(new(which.Controller), new(*which.ControllerImpl)), + ), + wire.NewSet( + link.New, + wire.Bind(new(domain.Linker), new(*link.Linker)), + ), + osenv.New, + ) + return &update.Controller{} +} + func InitializeAllowPolicyCommandController(ctx context.Context, param *config.Param) *allowpolicy.Controller { wire.Build( allowpolicy.New, diff --git a/pkg/controller/wire_gen.go b/pkg/controller/wire_gen.go index 93d5df5df..1273edbb4 100644 --- a/pkg/controller/wire_gen.go +++ b/pkg/controller/wire_gen.go @@ -26,6 +26,7 @@ import ( "github.com/aquaproj/aqua/v2/pkg/controller/install" "github.com/aquaproj/aqua/v2/pkg/controller/list" "github.com/aquaproj/aqua/v2/pkg/controller/remove" + "github.com/aquaproj/aqua/v2/pkg/controller/update" "github.com/aquaproj/aqua/v2/pkg/controller/updateaqua" "github.com/aquaproj/aqua/v2/pkg/controller/updatechecksum" "github.com/aquaproj/aqua/v2/pkg/controller/which" @@ -41,6 +42,7 @@ import ( "github.com/aquaproj/aqua/v2/pkg/runtime" "github.com/aquaproj/aqua/v2/pkg/slsa" "github.com/aquaproj/aqua/v2/pkg/unarchive" + "github.com/aquaproj/aqua/v2/pkg/versiongetter" "github.com/spf13/afero" "github.com/suzuki-shunsuke/go-osenv/osenv" "io" @@ -103,7 +105,12 @@ func InitializeGenerateCommandController(ctx context.Context, param *config.Para installerImpl := registry.New(param, gitHubContentFileDownloader, fs, rt, verifierImpl, slsaVerifierImpl) fuzzyfinderFinder := fuzzyfinder.New() clientImpl := cargo.NewClientImpl(httpClient) - controller := generate.New(configFinder, configReaderImpl, installerImpl, repositoriesService, fs, fuzzyfinderFinder, clientImpl) + cargoVersionGetter := versiongetter.NewCargo(clientImpl) + gitHubTagVersionGetter := versiongetter.NewGitHubTag(repositoriesService) + gitHubReleaseVersionGetter := versiongetter.NewGitHubRelease(repositoriesService) + generalVersionGetter := versiongetter.NewGeneralVersionGetter(cargoVersionGetter, gitHubTagVersionGetter, gitHubReleaseVersionGetter) + fuzzyGetter := versiongetter.NewFuzzy(fuzzyfinderFinder, generalVersionGetter) + controller := generate.New(configFinder, configReaderImpl, installerImpl, repositoriesService, fs, fuzzyfinderFinder, clientImpl, fuzzyGetter) return controller } @@ -257,6 +264,33 @@ func InitializeUpdateChecksumCommandController(ctx context.Context, param *confi return controller } +func InitializeUpdateCommandController(ctx context.Context, param *config.Param, httpClient *http.Client, rt *runtime.Runtime) *update.Controller { + repositoriesService := github.New(ctx) + fs := afero.NewOsFs() + configFinder := finder.NewConfigFinder(fs) + configReaderImpl := reader.New(fs, param) + httpDownloader := download.NewHTTPDownloader(httpClient) + gitHubContentFileDownloader := download.NewGitHubContentFileDownloader(repositoriesService, httpDownloader) + executor := exec.New() + downloader := download.NewDownloader(repositoriesService, httpDownloader) + verifierImpl := cosign.NewVerifier(executor, fs, downloader, param) + executorImpl := slsa.NewExecutor(executor, param) + slsaVerifierImpl := slsa.New(downloader, fs, executorImpl) + installerImpl := registry.New(param, gitHubContentFileDownloader, fs, rt, verifierImpl, slsaVerifierImpl) + fuzzyfinderFinder := fuzzyfinder.New() + clientImpl := cargo.NewClientImpl(httpClient) + cargoVersionGetter := versiongetter.NewCargo(clientImpl) + gitHubTagVersionGetter := versiongetter.NewGitHubTag(repositoriesService) + gitHubReleaseVersionGetter := versiongetter.NewGitHubRelease(repositoriesService) + generalVersionGetter := versiongetter.NewGeneralVersionGetter(cargoVersionGetter, gitHubTagVersionGetter, gitHubReleaseVersionGetter) + fuzzyGetter := versiongetter.NewFuzzy(fuzzyfinderFinder, generalVersionGetter) + osEnv := osenv.New() + linker := link.New() + controllerImpl := which.New(param, configFinder, configReaderImpl, installerImpl, rt, osEnv, fs, linker) + controller := update.New(param, repositoriesService, configFinder, configReaderImpl, installerImpl, fs, rt, fuzzyGetter, fuzzyfinderFinder, controllerImpl) + return controller +} + func InitializeAllowPolicyCommandController(ctx context.Context, param *config.Param) *allowpolicy.Controller { fs := afero.NewOsFs() configFinderImpl := policy.NewConfigFinder(fs) diff --git a/pkg/fuzzyfinder/finder.go b/pkg/fuzzyfinder/finder.go index c555f534f..16c797d12 100644 --- a/pkg/fuzzyfinder/finder.go +++ b/pkg/fuzzyfinder/finder.go @@ -8,9 +8,9 @@ import ( var ErrAbort = fuzzyfinder.ErrAbort -type Item interface { - Item() string - Preview(w int) string +type Item struct { + Item string + Preview string } type Finder struct{} @@ -19,7 +19,7 @@ func New() *Finder { return &Finder{} } -func (f *Finder) Find(items []Item, hasPreview bool) (int, error) { +func (f *Finder) Find(items []*Item, hasPreview bool) (int, error) { var opts []fuzzyfinder.Option if hasPreview { opts = []fuzzyfinder.Option{ @@ -27,16 +27,16 @@ func (f *Finder) Find(items []Item, hasPreview bool) (int, error) { if i < 0 { return "No item matches" } - return items[i].Preview(w) + return formatPreview(items[i].Preview, w) }), } } return fuzzyfinder.Find(items, func(i int) string { //nolint:wrapcheck - return items[i].Item() + return items[i].Item }, opts...) } -func (f *Finder) FindMulti(items []Item, hasPreview bool) ([]int, error) { +func (f *Finder) FindMulti(items []*Item, hasPreview bool) ([]int, error) { var opts []fuzzyfinder.Option if hasPreview { opts = []fuzzyfinder.Option{ @@ -44,12 +44,12 @@ func (f *Finder) FindMulti(items []Item, hasPreview bool) ([]int, error) { if i < 0 { return "No item matches" } - return items[i].Preview(w) + return formatPreview(items[i].Preview, w) }), } } return fuzzyfinder.FindMulti(items, func(i int) string { //nolint:wrapcheck - return items[i].Item() + return items[i].Item }, opts...) } @@ -70,7 +70,7 @@ func formatLine(line string, w int) string { return strings.Join(descArr, "\n") } -func formatDescription(desc string, w int) string { +func formatPreview(desc string, w int) string { lines := strings.Split(desc, "\n") arr := make([]string, len(lines)) for i, line := range lines { diff --git a/pkg/fuzzyfinder/mock.go b/pkg/fuzzyfinder/mock.go index 0f7a59436..e727c626a 100644 --- a/pkg/fuzzyfinder/mock.go +++ b/pkg/fuzzyfinder/mock.go @@ -12,10 +12,10 @@ func NewMock(idxs []int, err error) *MockFuzzyFinder { } } -func (f *MockFuzzyFinder) Find(items []Item, hasPreview bool) (int, error) { +func (f *MockFuzzyFinder) Find(items []*Item, hasPreview bool) (int, error) { return f.idxs[0], f.err } -func (f *MockFuzzyFinder) FindMulti(items []Item, hasPreview bool) ([]int, error) { +func (f *MockFuzzyFinder) FindMulti(items []*Item, hasPreview bool) ([]int, error) { return f.idxs, f.err } diff --git a/pkg/fuzzyfinder/package.go b/pkg/fuzzyfinder/package.go index 2563408d8..24e8d9e61 100644 --- a/pkg/fuzzyfinder/package.go +++ b/pkg/fuzzyfinder/package.go @@ -13,11 +13,18 @@ type Package struct { Version string } +func PreviewPackage(p *Package) string { + return fmt.Sprintf("%s\n\n%s\n%s", + p.PackageInfo.GetName(), + p.PackageInfo.GetLink(), + p.PackageInfo.Description) +} + func (p *Package) Preview(w int) string { return fmt.Sprintf("%s\n\n%s\n%s", p.PackageInfo.GetName(), p.PackageInfo.GetLink(), - formatDescription(p.PackageInfo.Description, w/2-8)) //nolint:gomnd + formatPreview(p.PackageInfo.Description, w/2-8)) //nolint:gomnd } func (p *Package) Item() string { diff --git a/pkg/fuzzyfinder/version.go b/pkg/fuzzyfinder/version.go index 0b578296a..4529cd363 100644 --- a/pkg/fuzzyfinder/version.go +++ b/pkg/fuzzyfinder/version.go @@ -11,6 +11,23 @@ type Version struct { URL string } +func PreviewVersion(v *Version) string { + s := v.Version + if v.Name != "" && v.Name != v.Version { + s += fmt.Sprintf(" (%s)", v.Name) + } + if v.URL != "" || v.Description != "" { + s += "\n" + } + if v.URL != "" { + s += fmt.Sprintf("\n%s", v.URL) + } + if v.URL != "" { + s += fmt.Sprintf("\n%s", v.Description) + } + return s +} + func (v *Version) Preview(w int) string { s := v.Version if v.Name != "" && v.Name != v.Version { @@ -23,7 +40,7 @@ func (v *Version) Preview(w int) string { s += fmt.Sprintf("\n%s", v.URL) } if v.URL != "" { - s += fmt.Sprintf("\n%s", formatDescription(v.Description, w/2-8)) //nolint:gomnd + s += fmt.Sprintf("\n%s", formatPreview(v.Description, w/2-8)) //nolint:gomnd } return s } @@ -32,12 +49,12 @@ func (v *Version) Item() string { return v.Version } -func ConvertStringsToVersions(arr []string) []Item { - versions := make([]Item, len(arr)) +func ConvertStringsToItems(arr []string) []*Item { + items := make([]*Item, len(arr)) for i, a := range arr { - versions[i] = &Version{ - Version: a, + items[i] = &Item{ + Item: a, } } - return versions + return items } diff --git a/pkg/versiongetter/cargo.go b/pkg/versiongetter/cargo.go new file mode 100644 index 000000000..3ef1fb040 --- /dev/null +++ b/pkg/versiongetter/cargo.go @@ -0,0 +1,36 @@ +package versiongetter + +import ( + "context" + "fmt" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" +) + +type CargoClient interface { + ListVersions(ctx context.Context, crate string) ([]string, error) + GetLatestVersion(ctx context.Context, crate string) (string, error) +} + +type CargoVersionGetter struct { + client CargoClient +} + +func NewCargo(client CargoClient) *CargoVersionGetter { + return &CargoVersionGetter{ + client: client, + } +} + +func (c *CargoVersionGetter) Get(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) (string, error) { + return c.client.GetLatestVersion(ctx, pkg.Crate) //nolint:wrapcheck +} + +func (c *CargoVersionGetter) List(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) ([]*fuzzyfinder.Item, error) { + versionStrings, err := c.client.ListVersions(ctx, pkg.Crate) + if err != nil { + return nil, fmt.Errorf("list versions of the crate: %w", err) + } + return fuzzyfinder.ConvertStringsToItems(versionStrings), nil +} diff --git a/pkg/versiongetter/cargo_test.go b/pkg/versiongetter/cargo_test.go new file mode 100644 index 000000000..59fd40890 --- /dev/null +++ b/pkg/versiongetter/cargo_test.go @@ -0,0 +1,127 @@ +package versiongetter_test + +import ( + "context" + "testing" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" + "github.com/aquaproj/aqua/v2/pkg/versiongetter" + "github.com/google/go-cmp/cmp" +) + +func TestCargoVersionGetter_Get(t *testing.T) { + t.Parallel() + data := []struct { + name string + versions map[string][]string + pkg *registry.PackageInfo + filters []*versiongetter.Filter + isErr bool + version string + }{ + { + name: "normal", + filters: []*versiongetter.Filter{ + {}, + }, + versions: map[string][]string{ + "crates.io/skim": { + "3.0.0", + "2.0.0", + "1.0.0", + }, + }, + pkg: ®istry.PackageInfo{ + Crate: "crates.io/skim", + }, + version: "3.0.0", + }, + } + + ctx := context.Background() + for _, d := range data { + d := d + t.Run(d.name, func(t *testing.T) { + t.Parallel() + cargoClient := versiongetter.NewMockCargoClient(d.versions) + cargoGetter := versiongetter.NewCargo(cargoClient) + version, err := cargoGetter.Get(ctx, d.pkg, d.filters) + if err != nil { + if d.isErr { + return + } + t.Fatal(err) + } + if d.isErr { + t.Fatal("error must be returned") + } + if version != d.version { + t.Fatalf("wanted %s, got %s", d.version, version) + } + }) + } +} + +func TestCargoVersionGetter_List(t *testing.T) { + t.Parallel() + data := []struct { + name string + versions map[string][]string + pkg *registry.PackageInfo + filters []*versiongetter.Filter + isErr bool + items []*fuzzyfinder.Item + }{ + { + name: "normal", + filters: []*versiongetter.Filter{ + {}, + }, + versions: map[string][]string{ + "crates.io/skim": { + "3.0.0", + "2.0.0", + "1.0.0", + }, + }, + pkg: ®istry.PackageInfo{ + Crate: "crates.io/skim", + }, + items: []*fuzzyfinder.Item{ + { + Item: "3.0.0", + }, + { + Item: "2.0.0", + }, + { + Item: "1.0.0", + }, + }, + }, + } + + ctx := context.Background() + for _, d := range data { + d := d + t.Run(d.name, func(t *testing.T) { + t.Parallel() + cargoClient := versiongetter.NewMockCargoClient(d.versions) + cargoGetter := versiongetter.NewCargo(cargoClient) + items, err := cargoGetter.List(ctx, d.pkg, d.filters) + if err != nil { + if d.isErr { + return + } + t.Fatal(err) + } + if d.isErr { + t.Fatal("error must be returned") + } + if diff := cmp.Diff(items, d.items); diff != "" { + t.Fatalf(diff) + } + }) + } +} diff --git a/pkg/versiongetter/filter.go b/pkg/versiongetter/filter.go new file mode 100644 index 000000000..6ba8658cd --- /dev/null +++ b/pkg/versiongetter/filter.go @@ -0,0 +1,51 @@ +package versiongetter + +import ( + "github.com/antonmedv/expr/vm" + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/expr" +) + +type Filter struct { + Prefix string + Filter *vm.Program + Constraint string +} + +func createFilters(pkgInfo *registry.PackageInfo) ([]*Filter, error) { + filters := make([]*Filter, 0, 1+len(pkgInfo.VersionOverrides)) + topFilter := &Filter{} + if pkgInfo.VersionFilter != "" { + f, err := expr.CompileVersionFilter(pkgInfo.VersionFilter) + if err != nil { + return nil, err //nolint:wrapcheck + } + topFilter.Filter = f + } + topFilter.Constraint = pkgInfo.VersionConstraints + if pkgInfo.VersionPrefix != "" { + topFilter.Prefix = pkgInfo.VersionPrefix + } + filters = append(filters, topFilter) + + for _, vo := range pkgInfo.VersionOverrides { + flt := &Filter{ + Prefix: topFilter.Prefix, + Filter: topFilter.Filter, + Constraint: topFilter.Constraint, + } + if vo.VersionFilter != nil { + f, err := expr.CompileVersionFilter(*vo.VersionFilter) + if err != nil { + return nil, err //nolint:wrapcheck + } + flt.Filter = f + } + flt.Constraint = vo.VersionConstraints + if vo.VersionPrefix != nil { + flt.Prefix = *vo.VersionPrefix + } + filters = append(filters, flt) + } + return filters, nil +} diff --git a/pkg/versiongetter/fuzzy_getter.go b/pkg/versiongetter/fuzzy_getter.go new file mode 100644 index 000000000..7669cbc4a --- /dev/null +++ b/pkg/versiongetter/fuzzy_getter.go @@ -0,0 +1,68 @@ +package versiongetter + +import ( + "context" + "strings" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" + "github.com/sirupsen/logrus" +) + +type FuzzyGetter struct { + fuzzyFinder FuzzyFinder + getter VersionGetter +} + +func NewFuzzy(finder FuzzyFinder, getter VersionGetter) *FuzzyGetter { + return &FuzzyGetter{ + fuzzyFinder: finder, + getter: getter, + } +} + +type FuzzyFinder interface { + Find(items []*fuzzyfinder.Item, hasPreview bool) (int, error) + FindMulti(items []*fuzzyfinder.Item, hasPreview bool) ([]int, error) +} + +func (g *FuzzyGetter) Get(ctx context.Context, _ *logrus.Entry, pkg *registry.PackageInfo, currentVersion string, useFinder bool) string { //nolint:cyclop + filters, err := createFilters(pkg) + if err != nil { + return "" + } + + if useFinder { //nolint:nestif + versions, err := g.getter.List(ctx, pkg, filters) + if err != nil { + return "" + } + if versions == nil { + return "" + } + currentVersionIndex := 0 + if currentVersion != "" { + for i, version := range versions { + if version.Item == currentVersion { + version.Item += " (*)" + currentVersionIndex = i + break + } + } + } + idx, err := g.fuzzyFinder.Find(versions, true) + if err != nil { + return "" + } + if idx == currentVersionIndex { + return strings.TrimSuffix(versions[idx].Item, " (*)") + } + return versions[idx].Item + } + + version, err := g.getter.Get(ctx, pkg, filters) + if err != nil { + return "" + } + return version +} diff --git a/pkg/versiongetter/fuzzy_getter_test.go b/pkg/versiongetter/fuzzy_getter_test.go new file mode 100644 index 000000000..4eb6f2080 --- /dev/null +++ b/pkg/versiongetter/fuzzy_getter_test.go @@ -0,0 +1,85 @@ +package versiongetter_test + +import ( + "context" + "testing" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" + "github.com/aquaproj/aqua/v2/pkg/versiongetter" + "github.com/sirupsen/logrus" +) + +func TestFuzzyGetter_Get(t *testing.T) { //nolint:funlen + t.Parallel() + data := []struct { + name string + pkg *registry.PackageInfo + currentVersion string + useFinder bool + version string + idxs []int + versions map[string][]*fuzzyfinder.Item + }{ + { + name: "normal", + pkg: ®istry.PackageInfo{ + RepoOwner: "suzuki-shunsuke", + RepoName: "tfcmt", + }, + currentVersion: "v2.0.0", + version: "v4.6.0", + versions: map[string][]*fuzzyfinder.Item{ + "suzuki-shunsuke/tfcmt": { + { + Item: "v4.6.0", + }, + { + Item: "v3.0.0", + }, + { + Item: "v2.0.0", + }, + }, + }, + }, + { + name: "finder", + pkg: ®istry.PackageInfo{ + RepoOwner: "suzuki-shunsuke", + RepoName: "tfcmt", + }, + useFinder: true, + idxs: []int{1}, + currentVersion: "v2.0.0", + version: "v3.0.0", + versions: map[string][]*fuzzyfinder.Item{ + "suzuki-shunsuke/tfcmt": { + { + Item: "v4.6.0", + }, + { + Item: "v3.0.0", + }, + { + Item: "v2.0.0", + }, + }, + }, + }, + } + logE := logrus.NewEntry(logrus.New()) + for _, d := range data { + d := d + t.Run(d.name, func(t *testing.T) { + t.Parallel() + finder := fuzzyfinder.NewMock(d.idxs, nil) + vg := versiongetter.NewMockVersionGetter(d.versions) + fg := versiongetter.NewFuzzy(finder, vg) + version := fg.Get(context.Background(), logE, d.pkg, d.currentVersion, d.useFinder) + if version != d.version { + t.Fatalf("wanted %s, got %s", d.version, version) + } + }) + } +} diff --git a/pkg/versiongetter/general.go b/pkg/versiongetter/general.go new file mode 100644 index 000000000..5d317021d --- /dev/null +++ b/pkg/versiongetter/general.go @@ -0,0 +1,54 @@ +package versiongetter + +import ( + "context" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" +) + +type GeneralVersionGetter struct { + cargo *CargoVersionGetter + ghTag *GitHubTagVersionGetter + ghRelease *GitHubReleaseVersionGetter +} + +func NewGeneralVersionGetter(cargo *CargoVersionGetter, ghTag *GitHubTagVersionGetter, ghRelease *GitHubReleaseVersionGetter) *GeneralVersionGetter { + return &GeneralVersionGetter{ + cargo: cargo, + ghTag: ghTag, + ghRelease: ghRelease, + } +} + +func (g *GeneralVersionGetter) Get(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) (string, error) { + getter := g.get(pkg) + if getter == nil { + return "", nil + } + return getter.Get(ctx, pkg, filters) //nolint:wrapcheck +} + +func (g *GeneralVersionGetter) List(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) ([]*fuzzyfinder.Item, error) { + getter := g.get(pkg) + if getter == nil { + return nil, nil + } + return getter.List(ctx, pkg, filters) //nolint:wrapcheck +} + +func (g *GeneralVersionGetter) get(pkg *registry.PackageInfo) VersionGetter { + if pkg.Type == "cargo" { + return g.cargo + } + if g.ghTag == nil { + return nil + } + if !pkg.HasRepo() { + return nil + } + if pkg.VersionSource == "github_tag" { + return g.ghTag + } + return g.ghRelease +} diff --git a/pkg/versiongetter/github_release.go b/pkg/versiongetter/github_release.go new file mode 100644 index 000000000..cfd64edd6 --- /dev/null +++ b/pkg/versiongetter/github_release.go @@ -0,0 +1,135 @@ +package versiongetter + +import ( + "context" + "fmt" + "strings" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/expr" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" + "github.com/aquaproj/aqua/v2/pkg/github" +) + +type GitHubReleaseVersionGetter struct { + gh GitHubReleaseClient +} + +func NewGitHubRelease(gh GitHubReleaseClient) *GitHubReleaseVersionGetter { + return &GitHubReleaseVersionGetter{ + gh: gh, + } +} + +type GitHubReleaseClient interface { + GetLatestRelease(ctx context.Context, repoOwner, repoName string) (*github.RepositoryRelease, *github.Response, error) + ListReleases(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error) +} + +func (g *GitHubReleaseVersionGetter) Get(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) (string, error) { + repoOwner := pkg.RepoOwner + repoName := pkg.RepoName + + if len(filters) == 0 { + release, _, err := g.gh.GetLatestRelease(ctx, repoOwner, repoName) + if err != nil { + return "", fmt.Errorf("get the latest GitHub Release: %w", err) + } + return release.GetTagName(), nil + } + + opt := &github.ListOptions{ + PerPage: 30, //nolint:gomnd + } + for { + releases, _, err := g.gh.ListReleases(ctx, repoOwner, repoName, opt) + if err != nil { + return "", fmt.Errorf("list tags: %w", err) + } + for _, release := range releases { + if filterRelease(release, filters) { + return release.GetTagName(), nil + } + } + if len(releases) != opt.PerPage { + return "", nil + } + opt.Page++ + } +} + +func (g *GitHubReleaseVersionGetter) List(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) ([]*fuzzyfinder.Item, error) { + repoOwner := pkg.RepoOwner + repoName := pkg.RepoName + opt := &github.ListOptions{ + PerPage: 30, //nolint:gomnd + } + var items []*fuzzyfinder.Item + tags := map[string]struct{}{} + for { + releases, _, err := g.gh.ListReleases(ctx, repoOwner, repoName, opt) + if err != nil { + return nil, fmt.Errorf("list tags: %w", err) + } + for _, release := range releases { + tagName := release.GetTagName() + if _, ok := tags[tagName]; ok { + continue + } + tags[tagName] = struct{}{} + if filterRelease(release, filters) { + v := &fuzzyfinder.Version{ + Name: release.GetName(), + Version: tagName, + Description: release.GetBody(), + URL: release.GetHTMLURL(), + } + items = append(items, &fuzzyfinder.Item{ + Item: tagName, + Preview: fuzzyfinder.PreviewVersion(v), + }) + } + } + if len(releases) != opt.PerPage { + return items, nil + } + opt.Page++ + } +} + +func filterRelease(release *github.RepositoryRelease, filters []*Filter) bool { + if release.GetPrerelease() { + return false + } + + tagName := release.GetTagName() + + for _, filter := range filters { + if filterTagByFilter(tagName, filter) { + return true + } + } + return false +} + +func filterTagByFilter(tagName string, filter *Filter) bool { + sv := tagName + if filter.Prefix != "" { + if !strings.HasPrefix(tagName, filter.Prefix) { + return false + } + sv = strings.TrimPrefix(tagName, filter.Prefix) + } + if filter.Filter != nil { + if f, err := expr.EvaluateVersionFilter(filter.Filter, tagName); err != nil || !f { + return false + } + } + if filter.Constraint == "" { + return true + } + if f, err := expr.EvaluateVersionConstraints(filter.Constraint, tagName, sv); err == nil && f { + return true + } + return false +} diff --git a/pkg/controller/generate/github_release_internal_test.go b/pkg/versiongetter/github_release_internal_test.go similarity index 98% rename from pkg/controller/generate/github_release_internal_test.go rename to pkg/versiongetter/github_release_internal_test.go index 5dc87e30f..62f38463d 100644 --- a/pkg/controller/generate/github_release_internal_test.go +++ b/pkg/versiongetter/github_release_internal_test.go @@ -1,4 +1,4 @@ -package generate +package versiongetter import ( "testing" diff --git a/pkg/versiongetter/github_release_test.go b/pkg/versiongetter/github_release_test.go new file mode 100644 index 000000000..98c5372b5 --- /dev/null +++ b/pkg/versiongetter/github_release_test.go @@ -0,0 +1,161 @@ +package versiongetter_test + +import ( + "context" + "testing" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" + "github.com/aquaproj/aqua/v2/pkg/github" + "github.com/aquaproj/aqua/v2/pkg/ptr" + "github.com/aquaproj/aqua/v2/pkg/versiongetter" + "github.com/google/go-cmp/cmp" +) + +func TestGitHubReleaseVersionGetter_Get(t *testing.T) { //nolint:dupl + t.Parallel() + data := []struct { + name string + releases map[string][]*github.RepositoryRelease + pkg *registry.PackageInfo + filters []*versiongetter.Filter + isErr bool + version string + }{ + { + name: "normal", + filters: []*versiongetter.Filter{ + {}, + }, + releases: map[string][]*github.RepositoryRelease{ + "suzuki-shunsuke/tfcmt": { + { + TagName: ptr.String("v3.0.0"), + }, + { + TagName: ptr.String("v2.0.0"), + }, + { + TagName: ptr.String("v1.0.0"), + }, + }, + }, + pkg: ®istry.PackageInfo{ + RepoOwner: "suzuki-shunsuke", + RepoName: "tfcmt", + }, + version: "v3.0.0", + }, + } + + ctx := context.Background() + for _, d := range data { + d := d + t.Run(d.name, func(t *testing.T) { + t.Parallel() + ghReleaseClient := versiongetter.NewMockGitHubReleaseClient(d.releases) + ghReleaseGetter := versiongetter.NewGitHubRelease(ghReleaseClient) + version, err := ghReleaseGetter.Get(ctx, d.pkg, d.filters) + if err != nil { + if d.isErr { + return + } + t.Fatal(err) + } + if d.isErr { + t.Fatal("error must be returned") + } + if version != d.version { + t.Fatalf("wanted %s, got %s", d.version, version) + } + }) + } +} + +func TestGitHubReleaseVersionGetter_List(t *testing.T) { //nolint:funlen + t.Parallel() + data := []struct { + name string + releases map[string][]*github.RepositoryRelease + pkg *registry.PackageInfo + filters []*versiongetter.Filter + isErr bool + items []*fuzzyfinder.Item + }{ + { + name: "normal", + filters: []*versiongetter.Filter{ + {}, + }, + releases: map[string][]*github.RepositoryRelease{ + "suzuki-shunsuke/tfcmt": { + { + TagName: ptr.String("v3.0.0"), + Body: ptr.String("body(v3)"), + HTMLURL: ptr.String("https://github.com/suzuki-shunsuke/tfcmt/releases/tag/v3.0.0"), + }, + { + TagName: ptr.String("v2.0.0"), + Body: ptr.String("body(v2)"), + HTMLURL: ptr.String("https://github.com/suzuki-shunsuke/tfcmt/releases/tag/v2.0.0"), + }, + { + TagName: ptr.String("v1.0.0"), + Body: ptr.String("body(v1)"), + HTMLURL: ptr.String("https://github.com/suzuki-shunsuke/tfcmt/releases/tag/v1.0.0"), + }, + }, + }, + pkg: ®istry.PackageInfo{ + RepoOwner: "suzuki-shunsuke", + RepoName: "tfcmt", + }, + items: []*fuzzyfinder.Item{ + { + Item: "v3.0.0", + Preview: `v3.0.0 + +https://github.com/suzuki-shunsuke/tfcmt/releases/tag/v3.0.0 +body(v3)`, + }, + { + Item: "v2.0.0", + Preview: `v2.0.0 + +https://github.com/suzuki-shunsuke/tfcmt/releases/tag/v2.0.0 +body(v2)`, + }, + { + Item: "v1.0.0", + Preview: `v1.0.0 + +https://github.com/suzuki-shunsuke/tfcmt/releases/tag/v1.0.0 +body(v1)`, + }, + }, + }, + } + + ctx := context.Background() + for _, d := range data { + d := d + t.Run(d.name, func(t *testing.T) { + t.Parallel() + ghReleaseClient := versiongetter.NewMockGitHubReleaseClient(d.releases) + ghReleaseGetter := versiongetter.NewGitHubRelease(ghReleaseClient) + items, err := ghReleaseGetter.List(ctx, d.pkg, d.filters) + if err != nil { + if d.isErr { + return + } + t.Fatal(err) + } + if d.isErr { + t.Fatal("error must be returned") + } + if diff := cmp.Diff(items, d.items); diff != "" { + t.Fatalf(diff) + } + }) + } +} diff --git a/pkg/versiongetter/github_tag.go b/pkg/versiongetter/github_tag.go new file mode 100644 index 000000000..4da67a3f8 --- /dev/null +++ b/pkg/versiongetter/github_tag.go @@ -0,0 +1,87 @@ +package versiongetter + +import ( + "context" + "fmt" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" + "github.com/aquaproj/aqua/v2/pkg/github" +) + +type GitHubTagVersionGetter struct { + gh GitHubTagClient +} + +func NewGitHubTag(gh GitHubTagClient) *GitHubTagVersionGetter { + return &GitHubTagVersionGetter{ + gh: gh, + } +} + +type GitHubTagClient interface { + ListTags(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.RepositoryTag, *github.Response, error) +} + +func (g *GitHubTagVersionGetter) Get(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) (string, error) { + repoOwner := pkg.RepoOwner + repoName := pkg.RepoName + opt := &github.ListOptions{ + PerPage: 30, //nolint:gomnd + } + for { + tags, _, err := g.gh.ListTags(ctx, repoOwner, repoName, opt) + if err != nil { + return "", fmt.Errorf("list tags: %w", err) + } + for _, tag := range tags { + if filterTag(tag, filters) { + return tag.GetName(), nil + } + } + if len(tags) != opt.PerPage { + return "", nil + } + opt.Page++ + } +} + +func (g *GitHubTagVersionGetter) List(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) ([]*fuzzyfinder.Item, error) { + repoOwner := pkg.RepoOwner + repoName := pkg.RepoName + opt := &github.ListOptions{ + PerPage: 30, //nolint:gomnd + } + var versions []string + tagNames := map[string]struct{}{} + for { + tags, _, err := g.gh.ListTags(ctx, repoOwner, repoName, opt) + if err != nil { + return nil, fmt.Errorf("list tags: %w", err) + } + for _, tag := range tags { + tagName := tag.GetName() + if _, ok := tagNames[tagName]; ok { + continue + } + tagNames[tagName] = struct{}{} + if filterTag(tag, filters) { + versions = append(versions, tagName) + } + } + if len(tags) != opt.PerPage { + return fuzzyfinder.ConvertStringsToItems(versions), nil + } + opt.Page++ + } +} + +func filterTag(tag *github.RepositoryTag, filters []*Filter) bool { + tagName := tag.GetName() + for _, filter := range filters { + if filterTagByFilter(tagName, filter) { + return true + } + } + return false +} diff --git a/pkg/versiongetter/github_tag_test.go b/pkg/versiongetter/github_tag_test.go new file mode 100644 index 000000000..61c044fd4 --- /dev/null +++ b/pkg/versiongetter/github_tag_test.go @@ -0,0 +1,143 @@ +package versiongetter_test + +import ( + "context" + "testing" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" + "github.com/aquaproj/aqua/v2/pkg/github" + "github.com/aquaproj/aqua/v2/pkg/ptr" + "github.com/aquaproj/aqua/v2/pkg/versiongetter" + "github.com/google/go-cmp/cmp" +) + +func TestGitHubTagVersionGetter_Get(t *testing.T) { //nolint:dupl + t.Parallel() + data := []struct { + name string + tags map[string][]*github.RepositoryTag + pkg *registry.PackageInfo + filters []*versiongetter.Filter + isErr bool + version string + }{ + { + name: "normal", + filters: []*versiongetter.Filter{ + {}, + }, + tags: map[string][]*github.RepositoryTag{ + "suzuki-shunsuke/tfcmt": { + { + Name: ptr.String("v3.0.0"), + }, + { + Name: ptr.String("v2.0.0"), + }, + { + Name: ptr.String("v1.0.0"), + }, + }, + }, + pkg: ®istry.PackageInfo{ + RepoOwner: "suzuki-shunsuke", + RepoName: "tfcmt", + }, + version: "v3.0.0", + }, + } + + ctx := context.Background() + for _, d := range data { + d := d + t.Run(d.name, func(t *testing.T) { + t.Parallel() + ghTagClient := versiongetter.NewMockGitHubTagClient(d.tags) + ghTagGetter := versiongetter.NewGitHubTag(ghTagClient) + version, err := ghTagGetter.Get(ctx, d.pkg, d.filters) + if err != nil { + if d.isErr { + return + } + t.Fatal(err) + } + if d.isErr { + t.Fatal("error must be returned") + } + if version != d.version { + t.Fatalf("wanted %s, got %s", d.version, version) + } + }) + } +} + +func TestGitHubTagVersionGetter_List(t *testing.T) { //nolint:funlen + t.Parallel() + data := []struct { + name string + tags map[string][]*github.RepositoryTag + pkg *registry.PackageInfo + filters []*versiongetter.Filter + isErr bool + items []*fuzzyfinder.Item + }{ + { + name: "normal", + filters: []*versiongetter.Filter{ + {}, + }, + tags: map[string][]*github.RepositoryTag{ + "suzuki-shunsuke/tfcmt": { + { + Name: ptr.String("v3.0.0"), + }, + { + Name: ptr.String("v2.0.0"), + }, + { + Name: ptr.String("v1.0.0"), + }, + }, + }, + pkg: ®istry.PackageInfo{ + RepoOwner: "suzuki-shunsuke", + RepoName: "tfcmt", + }, + items: []*fuzzyfinder.Item{ + { + Item: "v3.0.0", + }, + { + Item: "v2.0.0", + }, + { + Item: "v1.0.0", + }, + }, + }, + } + + ctx := context.Background() + for _, d := range data { + d := d + t.Run(d.name, func(t *testing.T) { + t.Parallel() + ghTagClient := versiongetter.NewMockGitHubTagClient(d.tags) + ghTagGetter := versiongetter.NewGitHubTag(ghTagClient) + items, err := ghTagGetter.List(ctx, d.pkg, d.filters) + if err != nil { + if d.isErr { + return + } + t.Fatal(err) + } + if d.isErr { + t.Fatal("error must be returned") + } + if diff := cmp.Diff(items, d.items); diff != "" { + t.Fatalf(diff) + } + }) + } +} diff --git a/pkg/versiongetter/mock_cargo.go b/pkg/versiongetter/mock_cargo.go new file mode 100644 index 000000000..671c5d0ce --- /dev/null +++ b/pkg/versiongetter/mock_cargo.go @@ -0,0 +1,32 @@ +package versiongetter + +import ( + "context" + "errors" +) + +type MockCargoClient struct { + versions map[string][]string +} + +func NewMockCargoClient(versions map[string][]string) *MockCargoClient { + return &MockCargoClient{ + versions: versions, + } +} + +func (g *MockCargoClient) ListVersions(ctx context.Context, crate string) ([]string, error) { + versions, ok := g.versions[crate] + if !ok { + return nil, errors.New("crate isn't found") + } + return versions, nil +} + +func (g *MockCargoClient) GetLatestVersion(ctx context.Context, crate string) (string, error) { + versions, ok := g.versions[crate] + if !ok { + return "", errors.New("crate isn't found") + } + return versions[0], nil +} diff --git a/pkg/versiongetter/mock_fuzzy_getter.go b/pkg/versiongetter/mock_fuzzy_getter.go new file mode 100644 index 000000000..d256bd35f --- /dev/null +++ b/pkg/versiongetter/mock_fuzzy_getter.go @@ -0,0 +1,22 @@ +package versiongetter + +import ( + "context" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/sirupsen/logrus" +) + +type MockFuzzyGetter struct { + versions map[string]string +} + +func NewMockFuzzyGetter(versions map[string]string) *MockFuzzyGetter { + return &MockFuzzyGetter{ + versions: versions, + } +} + +func (g *MockFuzzyGetter) Get(ctx context.Context, _ *logrus.Entry, pkg *registry.PackageInfo, currentVersion string, useFinder bool) string { + return g.versions[pkg.GetName()] +} diff --git a/pkg/versiongetter/mock_github_release.go b/pkg/versiongetter/mock_github_release.go new file mode 100644 index 000000000..b3131a528 --- /dev/null +++ b/pkg/versiongetter/mock_github_release.go @@ -0,0 +1,35 @@ +package versiongetter + +import ( + "context" + "errors" + "fmt" + + "github.com/aquaproj/aqua/v2/pkg/github" +) + +type MockGitHubReleaseClient struct { + releases map[string][]*github.RepositoryRelease +} + +func NewMockGitHubReleaseClient(releases map[string][]*github.RepositoryRelease) *MockGitHubReleaseClient { + return &MockGitHubReleaseClient{ + releases: releases, + } +} + +func (g *MockGitHubReleaseClient) GetLatestRelease(ctx context.Context, repoOwner, repoName string) (*github.RepositoryRelease, *github.Response, error) { + releases, ok := g.releases[fmt.Sprintf("%s/%s", repoOwner, repoName)] + if !ok { + return nil, nil, errors.New("repository isn't found") + } + return releases[0], nil, nil +} + +func (g *MockGitHubReleaseClient) ListReleases(ctx context.Context, owner, repo string, opts *github.ListOptions) ([]*github.RepositoryRelease, *github.Response, error) { + releases, ok := g.releases[fmt.Sprintf("%s/%s", owner, repo)] + if !ok { + return nil, nil, errors.New("repository isn't found") + } + return releases, nil, nil +} diff --git a/pkg/versiongetter/mock_github_tag.go b/pkg/versiongetter/mock_github_tag.go new file mode 100644 index 000000000..23d33ed6a --- /dev/null +++ b/pkg/versiongetter/mock_github_tag.go @@ -0,0 +1,31 @@ +package versiongetter + +import ( + "context" + "errors" + "fmt" + + "github.com/aquaproj/aqua/v2/pkg/github" +) + +type MockGitHubTagClient struct { + tags map[string][]*github.RepositoryTag +} + +func NewMockGitHubTagClient(tags map[string][]*github.RepositoryTag) *MockGitHubTagClient { + return &MockGitHubTagClient{ + tags: tags, + } +} + +func (g *MockGitHubTagClient) ListTags(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.RepositoryTag, *github.Response, error) { + tags, ok := g.tags[fmt.Sprintf("%s/%s", owner, repo)] + if !ok { + return nil, nil, errors.New("repository is not found") + } + m := (opts.Page + 1) * opts.PerPage + if m > len(tags) { + m = len(tags) + } + return tags[opts.Page*opts.PerPage : m], nil, nil +} diff --git a/pkg/versiongetter/mock_version_getter.go b/pkg/versiongetter/mock_version_getter.go new file mode 100644 index 000000000..59ce45acd --- /dev/null +++ b/pkg/versiongetter/mock_version_getter.go @@ -0,0 +1,35 @@ +package versiongetter + +import ( + "context" + "errors" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" +) + +type MockVersionGetter struct { + versions map[string][]*fuzzyfinder.Item +} + +func NewMockVersionGetter(versions map[string][]*fuzzyfinder.Item) *MockVersionGetter { + return &MockVersionGetter{ + versions: versions, + } +} + +func (g *MockVersionGetter) Get(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) (string, error) { + versions, ok := g.versions[pkg.GetName()] + if !ok { + return "", errors.New("version isn't found") + } + return versions[0].Item, nil +} + +func (g *MockVersionGetter) List(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) ([]*fuzzyfinder.Item, error) { + versions, ok := g.versions[pkg.GetName()] + if !ok { + return nil, errors.New("version isn't found") + } + return versions, nil +} diff --git a/pkg/versiongetter/version_getter.go b/pkg/versiongetter/version_getter.go new file mode 100644 index 000000000..271fef4c7 --- /dev/null +++ b/pkg/versiongetter/version_getter.go @@ -0,0 +1,13 @@ +package versiongetter + +import ( + "context" + + "github.com/aquaproj/aqua/v2/pkg/config/registry" + "github.com/aquaproj/aqua/v2/pkg/fuzzyfinder" +) + +type VersionGetter interface { + Get(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) (string, error) + List(ctx context.Context, pkg *registry.PackageInfo, filters []*Filter) ([]*fuzzyfinder.Item, error) +} diff --git a/renovate.json5 b/renovate.json5 index 1ec83aa95..e3614c7d5 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -9,7 +9,9 @@ "github>aquaproj/aqua-renovate-config:file#1.11.0(aqua/imports/.*\\.ya?ml)", "github>aquaproj/aqua-renovate-config:installer-script#1.11.0(Dockerfile-prebuilt)" ], - ignorePaths: [], + ignorePaths: [ + "tests/update/**", + ], regexManagers: [ { fileMatch: [".*\\.go"], diff --git a/tests/update/aqua.yaml b/tests/update/aqua.yaml new file mode 100644 index 000000000..b1f138ace --- /dev/null +++ b/tests/update/aqua.yaml @@ -0,0 +1,15 @@ +--- +# aqua - Declarative CLI Version Manager +# https://aquaproj.github.io/ +# checksum: +# enabled: true +# require_checksum: true +# supported_envs: +# - all +registries: +- type: standard + ref: v4.60.0 # renovate: depName=aquaproj/aqua-registry +packages: + - name: suzuki-shunsuke/tfcmt@v4.6.0 + - import: ci-info.yaml + - import: imports/*.yaml diff --git a/tests/update/ci-info.yaml b/tests/update/ci-info.yaml new file mode 100644 index 000000000..2b6d7face --- /dev/null +++ b/tests/update/ci-info.yaml @@ -0,0 +1,2 @@ +packages: + - name: suzuki-shunsuke/ci-info@v2.1.0 diff --git a/tests/update/imports/github-comment.yaml b/tests/update/imports/github-comment.yaml new file mode 100644 index 000000000..c033d5c86 --- /dev/null +++ b/tests/update/imports/github-comment.yaml @@ -0,0 +1,2 @@ +packages: + - name: suzuki-shunsuke/github-comment@v6.0.0