Skip to content

Commit

Permalink
feat: allow command aliases (#3224)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
suzuki-shunsuke authored Nov 2, 2024
1 parent 2a7788e commit 9e584e9
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 67 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/wc-integration-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions json-schema/aqua-yaml.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -80,6 +99,12 @@
},
"vars": {
"type": "object"
},
"command_aliases": {
"items": {
"$ref": "#/$defs/CommandAlias"
},
"type": "array"
}
},
"additionalProperties": false,
Expand Down
29 changes: 18 additions & 11 deletions pkg/config/aqua/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/cp/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions pkg/controller/cp/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
54 changes: 37 additions & 17 deletions pkg/controller/which/which.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
15 changes: 12 additions & 3 deletions pkg/installpackage/check_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
109 changes: 77 additions & 32 deletions pkg/installpackage/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down
19 changes: 19 additions & 0 deletions tests/aliases/aqua.yaml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
- name: hashicorp/terraform
version: v0.13.7
command_aliases:
- command: terraform
alias: terraform-013
# no_link: true

0 comments on commit 9e584e9

Please sign in to comment.