diff --git a/.github/workflows/wc-integration-test.yaml b/.github/workflows/wc-integration-test.yaml index 711a725f5..cb097223f 100644 --- a/.github/workflows/wc-integration-test.yaml +++ b/.github/workflows/wc-integration-test.yaml @@ -91,6 +91,16 @@ jobs: run: diff aqua.yaml expect.yaml working-directory: tests/insert + - name: Test version_expr readFile + run: aqua which -v terraform + working-directory: tests/version_expr_file + - name: Test version_expr readJSON + run: aqua which -v terraform + working-directory: tests/version_expr_json + - name: Test version_expr readYAML + run: aqua which -v terraform + working-directory: tests/version_expr_yaml + - run: aqua g -i suzuki-shunsuke/tfcmt working-directory: tests/main - name: add duplicated package diff --git a/go.mod b/go.mod index 1d0a84397..28477d5dc 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( golang.org/x/oauth2 v0.24.0 golang.org/x/sys v0.28.0 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -77,5 +78,4 @@ require ( golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/json-schema/aqua-yaml.json b/json-schema/aqua-yaml.json index d05142f45..70a0c5c0e 100644 --- a/json-schema/aqua-yaml.json +++ b/json-schema/aqua-yaml.json @@ -97,6 +97,9 @@ "go_version_file": { "type": "string" }, + "version_expr": { + "type": "string" + }, "vars": { "type": "object" }, diff --git a/pkg/config-reader/reader.go b/pkg/config-reader/reader.go index 1871edb1f..0f3e2c1d9 100644 --- a/pkg/config-reader/reader.go +++ b/pkg/config-reader/reader.go @@ -10,6 +10,7 @@ import ( "github.com/aquaproj/aqua/v2/pkg/config" "github.com/aquaproj/aqua/v2/pkg/config/aqua" + "github.com/aquaproj/aqua/v2/pkg/expr" "github.com/aquaproj/aqua/v2/pkg/osfile" "github.com/sirupsen/logrus" "github.com/spf13/afero" @@ -97,6 +98,18 @@ func (r *ConfigReader) readPackage(logE *logrus.Entry, configFilePath string, pk } return nil, nil } + if pkg.VersionExpr != "" { + // version_expr + dir := filepath.Dir(configFilePath) + s, err := expr.EvalVersionExpr(r.fs, dir, pkg.VersionExpr) + if err != nil { + return nil, fmt.Errorf("evaluate a version_expr: %w", logerr.WithFields(err, logrus.Fields{ + "version_expr": pkg.VersionExpr, + })) + } + pkg.Version = pkg.VersionExprPrefix + s + return nil, nil + } if pkg.Import == "" { // version return nil, nil diff --git a/pkg/config/aqua/config.go b/pkg/config/aqua/config.go index a78aa7c83..df05357f5 100644 --- a/pkg/config/aqua/config.go +++ b/pkg/config/aqua/config.go @@ -7,18 +7,20 @@ 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"` - CommandAliases []*CommandAlias `json:"command_aliases,omitempty" yaml:"command_aliases,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"` + VersionExpr string `json:"version_expr,omitempty" yaml:"version_expr,omitempty"` + VersionExprPrefix string `json:"version_expr_prefix,omitempty" yaml:"version_expr_prefix,omitempty"` + Vars map[string]any `json:"vars,omitempty" yaml:",omitempty"` + CommandAliases []*CommandAlias `json:"command_aliases,omitempty" yaml:"command_aliases,omitempty"` } type CommandAlias struct { diff --git a/pkg/expr/error.go b/pkg/expr/error.go index 0e7aaf721..fbd045018 100644 --- a/pkg/expr/error.go +++ b/pkg/expr/error.go @@ -2,4 +2,7 @@ package expr import "errors" -var errMustBeBoolean = errors.New("the evaluation result must be a boolean") +var ( + errMustBeBoolean = errors.New("the evaluation result must be a boolean") + errMustBeString = errors.New("the evaluation result must be a string") +) diff --git a/pkg/expr/version_expr.go b/pkg/expr/version_expr.go new file mode 100644 index 000000000..841edbaf9 --- /dev/null +++ b/pkg/expr/version_expr.go @@ -0,0 +1,94 @@ +package expr + +import ( + "encoding/json" + "errors" + "fmt" + "path/filepath" + "regexp" + "strings" + + "github.com/expr-lang/expr" + "github.com/spf13/afero" + "gopkg.in/yaml.v3" +) + +type Reader struct { + pwd string + fs afero.Fs +} + +const safeVersionPattern = `^v?\d+\.\d+(\.\d+)*[.-]?((alpha|beta|dev|rc)[.-]?)?\d*` + +var safeVersionRegexp = regexp.MustCompile(safeVersionPattern) + +func EvalVersionExpr(fs afero.Fs, pwd string, expression string) (string, error) { + r := Reader{fs: fs, pwd: pwd} + compiled, err := expr.Compile(expression, expr.Env(map[string]any{ + "readFile": r.readFile, + "readJSON": r.readJSON, + "readYAML": r.readYAML, + })) + if err != nil { + return "", fmt.Errorf("parse the expression: %w", err) + } + a, err := expr.Run(compiled, map[string]any{ + "readFile": r.readFile, + "readJSON": r.readJSON, + "readYAML": r.readYAML, + }) + if err != nil { + // Don't output error to prevent leaking sensitive information + // Maybe malicious users tries to read a secret file + return "", errors.New("evaluate the expression") + } + s, ok := a.(string) + if !ok { + return "", errMustBeString + } + // Restrict the value of version_expr to a semver for security reason. + // This prevents secrets from being exposed. + if !safeVersionRegexp.MatchString(s) { + // Don't output the valuof of version_expr to prevent leaking sensitive information + // Maybe malicious users tries to read a secret file + return "", errors.New("the evaluation result of version_expr must match with " + safeVersionPattern) + } + return s, nil +} + +func (r *Reader) read(s string) []byte { + if !filepath.IsAbs(s) { + s = filepath.Join(r.pwd, s) + } + b, err := afero.ReadFile(r.fs, s) + if err != nil { + panic(err) + } + return b +} + +func (r *Reader) readFile(s string) string { + return strings.TrimSpace(string(r.read(s))) +} + +func (r *Reader) readJSON(s string) any { + b := r.read(s) + var a any + if err := json.Unmarshal(b, &a); err != nil { + // Don't output error to prevent leaking sensitive information + // Maybe malicious users tries to read a secret file + panic("failed to unmarshal JSON") + } + return a +} + +func (r *Reader) readYAML(s string) any { + b := r.read(s) + var a any + if err := yaml.Unmarshal(b, &a); err != nil { + // Don't output error to prevent leaking sensitive information + // Maybe malicious users tries to read a secret file + panic("failed to unmarshal YAML") + } + return a +} diff --git a/tests/version_expr_file/.terraform-version b/tests/version_expr_file/.terraform-version new file mode 100644 index 000000000..5ad2491cf --- /dev/null +++ b/tests/version_expr_file/.terraform-version @@ -0,0 +1 @@ +1.10.2 diff --git a/tests/version_expr_file/aqua.yaml b/tests/version_expr_file/aqua.yaml new file mode 100644 index 000000000..32826df19 --- /dev/null +++ b/tests/version_expr_file/aqua.yaml @@ -0,0 +1,18 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/aquaproj/aqua/main/json-schema/aqua-yaml.json +# aqua - Declarative CLI Version Manager +# https://aquaproj.github.io/ +# checksum: +# enabled: true +# require_checksum: true +# supported_envs: +# - all +registries: +- type: standard + ref: v4.276.0 # renovate: depName=aquaproj/aqua-registry +packages: +- name: hashicorp/terraform + version_expr: | + "v" + readFile('.terraform-version') + # version_template: v{{readFile '.terraform-version'}} + # version_template: v{{(readYAML 'foo.yaml').version}} diff --git a/tests/version_expr_json/aqua.yaml b/tests/version_expr_json/aqua.yaml new file mode 100644 index 000000000..5ca7efbcd --- /dev/null +++ b/tests/version_expr_json/aqua.yaml @@ -0,0 +1,16 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/aquaproj/aqua/main/json-schema/aqua-yaml.json +# aqua - Declarative CLI Version Manager +# https://aquaproj.github.io/ +# checksum: +# enabled: true +# require_checksum: true +# supported_envs: +# - all +registries: +- type: standard + ref: v4.276.0 # renovate: depName=aquaproj/aqua-registry +packages: +- name: hashicorp/terraform + version_expr: | + readJSON('version.json').version diff --git a/tests/version_expr_json/version.json b/tests/version_expr_json/version.json new file mode 100644 index 000000000..3f6d4e3ee --- /dev/null +++ b/tests/version_expr_json/version.json @@ -0,0 +1,3 @@ +{ + "version": "1.10.1" +} diff --git a/tests/version_expr_yaml/aqua.yaml b/tests/version_expr_yaml/aqua.yaml new file mode 100644 index 000000000..d195699f5 --- /dev/null +++ b/tests/version_expr_yaml/aqua.yaml @@ -0,0 +1,16 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/aquaproj/aqua/main/json-schema/aqua-yaml.json +# aqua - Declarative CLI Version Manager +# https://aquaproj.github.io/ +# checksum: +# enabled: true +# require_checksum: true +# supported_envs: +# - all +registries: +- type: standard + ref: v4.276.0 # renovate: depName=aquaproj/aqua-registry +packages: +- name: hashicorp/terraform + version_expr: | + readYAML('version.yaml').version diff --git a/tests/version_expr_yaml/version.yaml b/tests/version_expr_yaml/version.yaml new file mode 100644 index 000000000..4f6ac6820 --- /dev/null +++ b/tests/version_expr_yaml/version.yaml @@ -0,0 +1 @@ +version: 1.10.1