act/pkg/runner/expression.go
Torbjørn Vatn a149cf8ca2
Make envs available in if conditionals (#225)
* Ignore .idea

* Add Env to the RunContext vm so we can Evaluate and Interpolate `env.xx`

* Make EvalBool support expressions more in line with the github runner

* Turns out Boolean(value) is what github is doing after all

* Add test for github context as well
2020-05-04 12:18:13 -07:00

330 lines
7.1 KiB
Go

package runner
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/robertkrimen/otto"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
"gopkg.in/godo.v2/glob"
)
const prefix = "${{"
const suffix = "}}"
var pattern *regexp.Regexp
func init() {
pattern = regexp.MustCompile(fmt.Sprintf("\\%s.+?%s", prefix, suffix))
}
// NewExpressionEvaluator creates a new evaluator
func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
vm := rc.newVM()
return &expressionEvaluator{
vm,
}
}
// NewExpressionEvaluator creates a new evaluator
func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator {
vm := sc.RunContext.newVM()
configers := []func(*otto.Otto){
sc.vmEnv(),
sc.vmInputs(),
}
for _, configer := range configers {
configer(vm)
}
return &expressionEvaluator{
vm,
}
}
// ExpressionEvaluator is the interface for evaluating expressions
type ExpressionEvaluator interface {
Evaluate(string) (string, error)
Interpolate(string) string
}
type expressionEvaluator struct {
vm *otto.Otto
}
func (ee *expressionEvaluator) Evaluate(in string) (string, error) {
val, err := ee.vm.Run(in)
if err != nil {
return "", err
}
if val.IsNull() || val.IsUndefined() {
return "", nil
}
return val.ToString()
}
func (ee *expressionEvaluator) Interpolate(in string) string {
errList := make([]error, 0)
out := in
for {
out = pattern.ReplaceAllStringFunc(in, func(match string) string {
expression := strings.TrimPrefix(strings.TrimSuffix(match, suffix), prefix)
evaluated, err := ee.Evaluate(expression)
if err != nil {
errList = append(errList, err)
}
return evaluated
})
if len(errList) > 0 {
logrus.Errorf("Unable to interpolate string '%s' - %v", in, errList)
break
}
if out == in {
// no replacement occurred, we're done!
break
}
in = out
}
return out
}
func (rc *RunContext) newVM() *otto.Otto {
configers := []func(*otto.Otto){
vmContains,
vmStartsWith,
vmEndsWith,
vmFormat,
vmJoin,
vmToJSON,
vmAlways,
rc.vmCancelled(),
rc.vmSuccess(),
rc.vmFailure(),
rc.vmHashFiles(),
rc.vmGithub(),
rc.vmJob(),
rc.vmSteps(),
rc.vmRunner(),
rc.vmSecrets(),
rc.vmStrategy(),
rc.vmMatrix(),
rc.vmEnv(),
}
vm := otto.New()
for _, configer := range configers {
configer(vm)
}
return vm
}
func vmContains(vm *otto.Otto) {
_ = vm.Set("contains", func(searchString interface{}, searchValue string) bool {
if searchStringString, ok := searchString.(string); ok {
return strings.Contains(strings.ToLower(searchStringString), strings.ToLower(searchValue))
} else if searchStringArray, ok := searchString.([]string); ok {
for _, s := range searchStringArray {
if strings.EqualFold(s, searchValue) {
return true
}
}
}
return false
})
}
func vmStartsWith(vm *otto.Otto) {
_ = vm.Set("startsWith", func(searchString string, searchValue string) bool {
return strings.HasPrefix(strings.ToLower(searchString), strings.ToLower(searchValue))
})
}
func vmEndsWith(vm *otto.Otto) {
_ = vm.Set("endsWith", func(searchString string, searchValue string) bool {
return strings.HasSuffix(strings.ToLower(searchString), strings.ToLower(searchValue))
})
}
func vmFormat(vm *otto.Otto) {
_ = vm.Set("format", func(s string, vals ...string) string {
for i, v := range vals {
s = strings.ReplaceAll(s, fmt.Sprintf("{%d}", i), v)
}
return s
})
}
func vmJoin(vm *otto.Otto) {
_ = vm.Set("join", func(element interface{}, optionalElem string) string {
slist := make([]string, 0)
if elementString, ok := element.(string); ok {
slist = append(slist, elementString)
} else if elementArray, ok := element.([]string); ok {
slist = append(slist, elementArray...)
}
if optionalElem != "" {
slist = append(slist, optionalElem)
}
return strings.Join(slist, " ")
})
}
func vmToJSON(vm *otto.Otto) {
toJSON := func(o interface{}) string {
rtn, err := json.MarshalIndent(o, "", " ")
if err != nil {
logrus.Errorf("Unable to marsal: %v", err)
return ""
}
return string(rtn)
}
_ = vm.Set("toJSON", toJSON)
_ = vm.Set("toJson", toJSON)
}
func (rc *RunContext) vmHashFiles() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("hashFiles", func(path string) string {
files, _, err := glob.Glob([]string{filepath.Join(rc.Config.Workdir, path)})
if err != nil {
logrus.Errorf("Unable to glob.Glob: %v", err)
return ""
}
hasher := sha256.New()
for _, file := range files {
f, err := os.Open(file.Path)
if err != nil {
logrus.Errorf("Unable to os.Open: %v", err)
}
defer f.Close()
if _, err := io.Copy(hasher, f); err != nil {
logrus.Errorf("Unable to io.Copy: %v", err)
}
}
return hex.EncodeToString(hasher.Sum(nil))
})
}
}
func (rc *RunContext) vmSuccess() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("success", func() bool {
return rc.getJobContext().Status == "success"
})
}
}
func (rc *RunContext) vmFailure() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("failure", func() bool {
return rc.getJobContext().Status == "failure"
})
}
}
func vmAlways(vm *otto.Otto) {
_ = vm.Set("always", func() bool {
return true
})
}
func (rc *RunContext) vmCancelled() func(vm *otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("cancelled", func() bool {
return rc.getJobContext().Status == "cancelled"
})
}
}
func (rc *RunContext) vmGithub() func(*otto.Otto) {
github := rc.getGithubContext()
return func(vm *otto.Otto) {
_ = vm.Set("github", github)
}
}
func (rc *RunContext) vmEnv() func(*otto.Otto) {
return func(vm *otto.Otto) {
env := rc.GetEnv()
log.Debugf("context env => %v", env)
_ = vm.Set("env", env)
}
}
func (sc *StepContext) vmEnv() func(*otto.Otto) {
return func(vm *otto.Otto) {
log.Debugf("context env => %v", sc.Env)
_ = vm.Set("env", sc.Env)
}
}
func (sc *StepContext) vmInputs() func(*otto.Otto) {
inputs := make(map[string]string)
for k, v := range sc.Step.With {
inputs[k] = v
}
return func(vm *otto.Otto) {
_ = vm.Set("inputs", inputs)
}
}
func (rc *RunContext) vmJob() func(*otto.Otto) {
job := rc.getJobContext()
return func(vm *otto.Otto) {
_ = vm.Set("job", job)
}
}
func (rc *RunContext) vmSteps() func(*otto.Otto) {
steps := rc.getStepsContext()
return func(vm *otto.Otto) {
_ = vm.Set("steps", steps)
}
}
func (rc *RunContext) vmRunner() func(*otto.Otto) {
runner := map[string]interface{}{
"os": "Linux",
"temp": "/tmp",
"tool_cache": "/opt/hostedtoolcache",
}
return func(vm *otto.Otto) {
_ = vm.Set("runner", runner)
}
}
func (rc *RunContext) vmSecrets() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("secrets", rc.Config.Secrets)
}
}
func (rc *RunContext) vmStrategy() func(*otto.Otto) {
job := rc.Run.Job()
strategy := make(map[string]interface{})
if job.Strategy != nil {
strategy["fail-fast"] = job.Strategy.FailFast
strategy["max-parallel"] = job.Strategy.MaxParallel
}
return func(vm *otto.Otto) {
_ = vm.Set("strategy", strategy)
}
}
func (rc *RunContext) vmMatrix() func(*otto.Otto) {
return func(vm *otto.Otto) {
_ = vm.Set("matrix", rc.Matrix)
}
}