From 9e584e927a158fe5f65271195964cd500d2d6cb8 Mon Sep 17 00:00:00 2001 From: Shunsuke Suzuki Date: Sat, 2 Nov 2024 15:31:38 +0900 Subject: [PATCH] feat: allow command aliases (#3224) * feat: allow command aliases * ci: add an integration test * refactor: split functions * refactor: split functions * docs: update JSON Schema * fix: fix a log * fix: fix cp command to support command aliases * fix: create links using original command names --- .github/workflows/wc-integration-test.yaml | 7 ++ json-schema/aqua-yaml.json | 25 +++++ pkg/config/aqua/config.go | 29 +++--- pkg/controller/cp/controller.go | 2 +- pkg/controller/cp/copy.go | 5 +- pkg/controller/which/which.go | 54 ++++++---- pkg/installpackage/check_file.go | 15 ++- pkg/installpackage/link.go | 109 +++++++++++++++------ tests/aliases/aqua.yaml | 19 ++++ 9 files changed, 198 insertions(+), 67 deletions(-) create mode 100644 tests/aliases/aqua.yaml diff --git a/.github/workflows/wc-integration-test.yaml b/.github/workflows/wc-integration-test.yaml index 94b4d8a2e..bdafdc887 100644 --- a/.github/workflows/wc-integration-test.yaml +++ b/.github/workflows/wc-integration-test.yaml @@ -39,6 +39,13 @@ jobs: run: aqua -c aqua.yaml g -i kubernetes-sigs/kustomize working-directory: tests/main + - name: Test command aliases + run: aqua i -l + working-directory: tests/aliases + - name: Test command aliases + run: terraform-013 version + working-directory: tests/aliases + - run: aqua list - run: aqua list -installed - run: aqua list -installed -a diff --git a/json-schema/aqua-yaml.json b/json-schema/aqua-yaml.json index 7145b0dc5..d05142f45 100644 --- a/json-schema/aqua-yaml.json +++ b/json-schema/aqua-yaml.json @@ -18,6 +18,25 @@ "additionalProperties": false, "type": "object" }, + "CommandAlias": { + "properties": { + "command": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "no_link": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "command", + "alias" + ] + }, "Config": { "properties": { "packages": { @@ -80,6 +99,12 @@ }, "vars": { "type": "object" + }, + "command_aliases": { + "items": { + "$ref": "#/$defs/CommandAlias" + }, + "type": "array" } }, "additionalProperties": false, diff --git a/pkg/config/aqua/config.go b/pkg/config/aqua/config.go index b0be57e81..2de013d7e 100644 --- a/pkg/config/aqua/config.go +++ b/pkg/config/aqua/config.go @@ -7,17 +7,24 @@ import ( ) type Package struct { - Name string `validate:"required" json:"name,omitempty"` - Registry string `validate:"required" yaml:",omitempty" json:"registry,omitempty" jsonschema:"description=Registry name,example=foo,example=local,default=standard"` - Version string `validate:"required" yaml:",omitempty" json:"version,omitempty"` - Import string `yaml:",omitempty" json:"import,omitempty"` - Tags []string `yaml:",omitempty" json:"tags,omitempty"` - Description string `yaml:",omitempty" json:"description,omitempty"` - Link string `yaml:",omitempty" json:"link,omitempty"` - Update *Update `yaml:",omitempty" json:"update,omitempty"` - FilePath string `json:"-" yaml:"-"` - GoVersionFile string `json:"go_version_file,omitempty" yaml:"go_version_file,omitempty"` - Vars map[string]any `json:"vars,omitempty" yaml:",omitempty"` + Name string `validate:"required" json:"name,omitempty"` + Registry string `validate:"required" yaml:",omitempty" json:"registry,omitempty" jsonschema:"description=Registry name,example=foo,example=local,default=standard"` + Version string `validate:"required" yaml:",omitempty" json:"version,omitempty"` + Import string `yaml:",omitempty" json:"import,omitempty"` + Tags []string `yaml:",omitempty" json:"tags,omitempty"` + Description string `yaml:",omitempty" json:"description,omitempty"` + Link string `yaml:",omitempty" json:"link,omitempty"` + Update *Update `yaml:",omitempty" json:"update,omitempty"` + FilePath string `json:"-" yaml:"-"` + GoVersionFile string `json:"go_version_file,omitempty" yaml:"go_version_file,omitempty"` + Vars map[string]any `json:"vars,omitempty" yaml:",omitempty"` + CommandAliases []*CommandAlias `json:"command_aliases,omitempty" yaml:"command_aliases,omitempty"` +} + +type CommandAlias struct { + Command string `validate:"required" json:"command"` + Alias string `validate:"required" json:"alias"` + NoLink bool `yaml:"no_link,omitempty" json:"no_link,omitempty"` } type Update struct { diff --git a/pkg/controller/cp/controller.go b/pkg/controller/cp/controller.go index c01ba993c..fd230dfd4 100644 --- a/pkg/controller/cp/controller.go +++ b/pkg/controller/cp/controller.go @@ -133,7 +133,7 @@ func (c *Controller) installAndCopy(ctx context.Context, logE *logrus.Entry, par } } - if err := c.copy(logE, param, findResult, exeName); err != nil { + if err := c.copy(logE, param, findResult.ExePath, exeName); err != nil { return err } return nil diff --git a/pkg/controller/cp/copy.go b/pkg/controller/cp/copy.go index 4654af9f6..d9bf90ab1 100644 --- a/pkg/controller/cp/copy.go +++ b/pkg/controller/cp/copy.go @@ -5,11 +5,10 @@ import ( "path/filepath" "github.com/aquaproj/aqua/v2/pkg/config" - "github.com/aquaproj/aqua/v2/pkg/controller/which" "github.com/sirupsen/logrus" ) -func (c *Controller) copy(logE *logrus.Entry, param *config.Param, findResult *which.FindResult, exeName string) error { +func (c *Controller) copy(logE *logrus.Entry, param *config.Param, exePath string, exeName string) error { p := filepath.Join(param.Dest, exeName) if c.runtime.GOOS == "windows" && filepath.Ext(exeName) == "" { p += ".exe" @@ -18,7 +17,7 @@ func (c *Controller) copy(logE *logrus.Entry, param *config.Param, findResult *w "exe_name": exeName, "dest": p, }).Info("coping a file") - if err := c.packageInstaller.Copy(p, findResult.ExePath); err != nil { + if err := c.packageInstaller.Copy(p, exePath); err != nil { return fmt.Errorf("copy a file: %w", err) } return nil diff --git a/pkg/controller/which/which.go b/pkg/controller/which/which.go index dd58ccbd6..013f03fa8 100644 --- a/pkg/controller/which/which.go +++ b/pkg/controller/which/which.go @@ -157,25 +157,45 @@ func (c *Controller) findExecFileFromPkg(logE *logrus.Entry, registries map[stri } for _, file := range pkgInfo.GetFiles() { - if file.Name == exeName { - findResult := &FindResult{ - Package: &config.Package{ - Package: pkg, - PackageInfo: pkgInfo, - }, - File: file, - } - if err := findResult.Package.ApplyVars(); err != nil { - return nil, fmt.Errorf("apply package variables: %w", err) - } - exePath, err := c.getExePath(findResult) - if err != nil { - logE.WithError(err).Error("get the execution file path") - return nil, nil //nolint:nilnil - } - findResult.ExePath = exePath + findResult, err := c.findExecFileFromFile(logE, exeName, pkg, pkgInfo, file) + if err != nil { + return nil, err + } + if findResult != nil { return findResult, nil } } return nil, nil //nolint:nilnil } + +func (c *Controller) findExecFileFromFile(logE *logrus.Entry, exeName string, pkg *aqua.Package, pkgInfo *registry.PackageInfo, file *registry.File) (*FindResult, error) { + cmds := map[string]struct{}{ + file.Name: {}, + } + for _, alias := range pkg.CommandAliases { + if file.Name != alias.Command { + continue + } + cmds[alias.Alias] = struct{}{} + } + if _, ok := cmds[exeName]; !ok { + return nil, nil //nolint:nilnil + } + findResult := &FindResult{ + Package: &config.Package{ + Package: pkg, + PackageInfo: pkgInfo, + }, + File: file, + } + if err := findResult.Package.ApplyVars(); err != nil { + return nil, fmt.Errorf("apply package variables: %w", err) + } + exePath, err := c.getExePath(findResult) + if err != nil { + logE.WithError(err).Error("get the execution file path") + return nil, nil //nolint:nilnil + } + findResult.ExePath = exePath + return findResult, nil +} diff --git a/pkg/installpackage/check_file.go b/pkg/installpackage/check_file.go index 70868fc68..19afec278 100644 --- a/pkg/installpackage/check_file.go +++ b/pkg/installpackage/check_file.go @@ -59,10 +59,19 @@ func (is *Installer) checkAndCopyFile(ctx context.Context, logE *logrus.Entry, p return nil } logE.Info("copying an executable file") - if err := is.Copy(filepath.Join(is.copyDir, file.Name), exePath); err != nil { - return err + exeNames := map[string]struct{}{ + file.Name: {}, + } + for _, alias := range pkg.Package.CommandAliases { + if alias.Command == file.Name { + exeNames[alias.Alias] = struct{}{} + } + } + for exeName := range exeNames { + if err := is.Copy(filepath.Join(is.copyDir, exeName), exePath); err != nil { + return err + } } - return nil } diff --git a/pkg/installpackage/link.go b/pkg/installpackage/link.go index 16c40f7ae..b3a7be76e 100644 --- a/pkg/installpackage/link.go +++ b/pkg/installpackage/link.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/aquaproj/aqua/v2/pkg/config" + "github.com/aquaproj/aqua/v2/pkg/config/registry" "github.com/sirupsen/logrus" "github.com/spf13/afero" "github.com/suzuki-shunsuke/logrus-error/logerr" @@ -27,40 +28,86 @@ func (is *Installer) createLinks(logE *logrus.Entry, pkgs []*config.Package) boo } for _, pkg := range pkgs { - pkgInfo := pkg.PackageInfo - for _, file := range pkgInfo.GetFiles() { - if is.realRuntime.IsWindows() { - hardLink := filepath.Join(is.rootDir, "bin", file.Name+".exe") - if f, err := afero.Exists(is.fs, hardLink); err != nil { - logerr.WithError(logE, err).WithFields(logrus.Fields{ - "command": file.Name, - }).Error("check if a hard link to aqua-proxy exists") - failed = true - continue - } else if f { - continue - } - logE.WithFields(logrus.Fields{ - "command": file.Name, - }).Info("creating a hard link to aqua-proxy") - if err := is.linker.Hardlink(aquaProxyPathOnWindows, hardLink); err != nil { - logerr.WithError(logE, err).WithFields(logrus.Fields{ - "command": file.Name, - }).Error("create a hard link to aqua-proxy") - failed = true - } - continue - } - if err := is.createLink(logE, filepath.Join(is.rootDir, "bin", file.Name), filepath.Join("..", proxyName)); err != nil { - logerr.WithError(logE, err).Error("create the symbolic link") - failed = true - continue - } + logE := logE.WithFields(logrus.Fields{ + "package_name": pkg.Package.Name, + "package_version": pkg.Package.Version, + }) + if is.createPackageLinks(logE, pkg, aquaProxyPathOnWindows) { + failed = true + } + } + return failed +} + +func (is *Installer) createPackageLinks(logE *logrus.Entry, pkg *config.Package, aquaProxyPathOnWindows string) bool { + failed := false + pkgInfo := pkg.PackageInfo + for _, file := range pkgInfo.GetFiles() { + logE := logE.WithFields(logrus.Fields{ + "command": file.Name, + }) + if is.createFileLinks(logE, pkg, file, aquaProxyPathOnWindows) { + failed = true } } return failed } +func (is *Installer) createFileLinks(logE *logrus.Entry, pkg *config.Package, file *registry.File, aquaProxyPathOnWindows string) bool { + failed := false + cmds := map[string]struct{}{ + file.Name: {}, + } + for _, alias := range pkg.Package.CommandAliases { + if file.Name != alias.Command { + continue + } + if alias.NoLink { + continue + } + cmds[alias.Alias] = struct{}{} + } + for cmd := range cmds { + if err := is.createCmdLink(logE, file, cmd, aquaProxyPathOnWindows); err != nil { + logerr.WithError(logE, err).Error("create a link to aqua-proxy") + failed = true + } + } + return failed +} + +func (is *Installer) createCmdLink(logE *logrus.Entry, file *registry.File, cmd string, aquaProxyPathOnWindows string) error { + if cmd != file.Name { + logE = logE.WithFields(logrus.Fields{ + "command_alias": cmd, + }) + } + if is.realRuntime.IsWindows() { + if err := is.createHardLink(logE, cmd, aquaProxyPathOnWindows); err != nil { + return fmt.Errorf("create a hard link to aqua-proxy: %w", err) + } + return nil + } + if err := is.createLink(logE, filepath.Join(is.rootDir, "bin", cmd), filepath.Join("..", proxyName)); err != nil { + return fmt.Errorf("create a symbolic link: %w", err) + } + return nil +} + +func (is *Installer) createHardLink(logE *logrus.Entry, cmd string, aquaProxyPathOnWindows string) error { + hardLink := filepath.Join(is.rootDir, "bin", cmd+".exe") + if f, err := afero.Exists(is.fs, hardLink); err != nil { + return fmt.Errorf("check if a hard link to aqua-proxy exists: %w", err) + } else if f { + return nil + } + logE.Info("creating a hard link to aqua-proxy") + if err := is.linker.Hardlink(aquaProxyPathOnWindows, hardLink); err != nil { + return fmt.Errorf("create a hard link to aqua-proxy: %w", err) + } + return nil +} + func (is *Installer) recreateHardLinks() error { binDir := filepath.Join(is.rootDir, "bin") infos, err := afero.ReadDir(is.fs, binDir) @@ -116,9 +163,7 @@ func (is *Installer) createLink(logE *logrus.Entry, linkPath, linkDest string) e return fmt.Errorf("unexpected file mode %s: %s", linkPath, mode.String()) } } - logE.WithFields(logrus.Fields{ - "command": filepath.Base(linkPath), - }).Info("create a symbolic link") + logE.Info("create a symbolic link") if err := is.linker.Symlink(linkDest, linkPath); err != nil { return fmt.Errorf("create a symbolic link: %w", err) } diff --git a/tests/aliases/aqua.yaml b/tests/aliases/aqua.yaml new file mode 100644 index 000000000..fe33acc0f --- /dev/null +++ b/tests/aliases/aqua.yaml @@ -0,0 +1,19 @@ +--- +# aqua - Declarative CLI Version Manager +# https://aquaproj.github.io/ +# checksum: +# enabled: true +# require_checksum: true +# supported_envs: +# - all +registries: +- type: standard + ref: v4.246.0 # renovate: depName=aquaproj/aqua-registry +packages: +- name: hashicorp/terraform@v1.9.8 +- name: hashicorp/terraform + version: v0.13.7 + command_aliases: + - command: terraform + alias: terraform-013 + # no_link: true