cue/load: support injection of system variables
For reviewer: tests and naming are the big ticket
items to review here.
For instance:
wd: string @tag(wd,var=cwd)
Fixes #222
os-specific filepath functionality available by passing
the "os" injection variable as second argument
Fixes #135
username: available as the username injection variable
see cue help injection
Change-Id: I33c04f5f8dff34a1b6a4333a6674b3f36be48d34
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/9578
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/common.go b/cmd/cue/cmd/common.go
index 9ad8a7c..1c4381c 100644
--- a/cmd/cue/cmd/common.go
+++ b/cmd/cue/cmd/common.go
@@ -416,6 +416,9 @@
func setTags(f *pflag.FlagSet, cfg *load.Config) error {
tags, _ := f.GetStringArray(string(flagInject))
cfg.Tags = tags
+ if b, _ := f.GetBool(string(flagInjectVars)); b {
+ cfg.TagVars = load.DefaultTagVars()
+ }
return nil
}
diff --git a/cmd/cue/cmd/flags.go b/cmd/cue/cmd/flags.go
index a9edfe4..e616eac 100644
--- a/cmd/cue/cmd/flags.go
+++ b/cmd/cue/cmd/flags.go
@@ -21,17 +21,18 @@
// Common flags
const (
- flagAll flagName = "all"
- flagDryrun flagName = "dryrun"
- flagVerbose flagName = "verbose"
- flagAllErrors flagName = "all-errors"
- flagTrace flagName = "trace"
- flagForce flagName = "force"
- flagIgnore flagName = "ignore"
- flagStrict flagName = "strict"
- flagSimplify flagName = "simplify"
- flagPackage flagName = "package"
- flagInject flagName = "inject"
+ flagAll flagName = "all"
+ flagDryrun flagName = "dryrun"
+ flagVerbose flagName = "verbose"
+ flagAllErrors flagName = "all-errors"
+ flagTrace flagName = "trace"
+ flagForce flagName = "force"
+ flagIgnore flagName = "ignore"
+ flagStrict flagName = "strict"
+ flagSimplify flagName = "simplify"
+ flagPackage flagName = "package"
+ flagInject flagName = "inject"
+ flagInjectVars flagName = "inject-vars"
flagExpression flagName = "expression"
flagSchema flagName = "schema"
@@ -90,6 +91,8 @@
func addInjectionFlags(f *pflag.FlagSet, auto bool) {
f.StringArrayP(string(flagInject), "t", nil,
"set the value of a tagged field")
+ f.BoolP(string(flagInjectVars), "T", auto,
+ "inject system variables in tags")
}
type flagName string
diff --git a/cmd/cue/cmd/help.go b/cmd/cue/cmd/help.go
index aadbca6..d1d8578 100644
--- a/cmd/cue/cmd/help.go
+++ b/cmd/cue/cmd/help.go
@@ -331,6 +331,31 @@
environment: "prod" | "staging" @tag(env,short=prod|staging)
ensures the user may only specify "prod" or "staging".
+
+
+Tag variables
+
+The injection mechanism allows for the injection of system variables:
+when variable injection is enabled, tags of the form
+
+ @tag(dir,var=cwd)
+
+will inject the named variable (here cwd) into the tag. An explicitly
+set value for a tag using --inject/-t takes precedence over an
+available tag variable.
+
+The following variables are supported:
+
+ now current time in RFC3339 format.
+ os OS identifier of the current system. Valid values:
+ aix android darwin dragonfly
+ freebsd illumos ios js (wasm)
+ linux netbsd openbsd plan9
+ solaris windows
+ cwd working directory
+ username current username
+ hostname current hostname
+ rand a random 128-bit integer
`,
}
diff --git a/cmd/cue/cmd/testdata/script/help_cmd.txt b/cmd/cue/cmd/testdata/script/help_cmd.txt
index ab7de8e..2b76d25 100644
--- a/cmd/cue/cmd/testdata/script/help_cmd.txt
+++ b/cmd/cue/cmd/testdata/script/help_cmd.txt
@@ -140,6 +140,7 @@
Flags:
-h, --help help for cmd
-t, --inject stringArray set the value of a tagged field
+ -T, --inject-vars inject system variables in tags (default true)
Global Flags:
-E, --all-errors print all available errors
diff --git a/cmd/cue/cmd/testdata/script/help_cmd_flags.txt b/cmd/cue/cmd/testdata/script/help_cmd_flags.txt
index 8cad548..8993fed 100644
--- a/cmd/cue/cmd/testdata/script/help_cmd_flags.txt
+++ b/cmd/cue/cmd/testdata/script/help_cmd_flags.txt
@@ -138,6 +138,7 @@
Flags:
-h, --help help for cmd
-t, --inject stringArray set the value of a tagged field
+ -T, --inject-vars inject system variables in tags (default true)
Global Flags:
-E, --all-errors print all available errors
diff --git a/cmd/cue/cmd/testdata/script/inject.txt b/cmd/cue/cmd/testdata/script/inject.txt
index b2f4403..a6b9a3e 100644
--- a/cmd/cue/cmd/testdata/script/inject.txt
+++ b/cmd/cue/cmd/testdata/script/inject.txt
@@ -1,7 +1,21 @@
cue eval test.cue -t env=prod
-
cmp stdout expect-stdout
+cue eval vars.cue -T
+cmp stdout expect-stdout-vars
+
+cue eval vars.cue -T -t dir=xxx
+cmp stdout expect-stdout-override
+
+cue eval vars.cue
+cmp stdout expect-stdout-novars
+
+! cue eval -T err.cue
+cmp stderr expect-stderr-err
+
+cue cmd user vars.cue vars_tool.cue
+cmp stdout expect-stdout-tool
+
# TODO: report errors for invalid tags?
-- test.cue --
@@ -11,3 +25,39 @@
-- expect-stdout --
environment: "prod"
+-- vars.cue --
+import "path"
+
+_os: string @tag(os,var=os)
+_dir: string @tag(dir,var=cwd)
+
+base: path.Base(_dir, _os)
+
+-- err.cue --
+dir: string @tag(dir,var=userz)
+
+-- vars_tool.cue --
+import (
+ "path"
+ "tool/cli"
+)
+
+wd: string @tag(wd,var=cwd)
+_os: string @tag(os,var=os)
+
+command: user: {
+ base: cli.Print & { text: path.Base(wd, _os) }
+}
+
+-- expect-stdout-vars --
+base: "script-inject"
+-- expect-stderr-err --
+tag variable 'userz' not found
+-- expect-stdout-override --
+base: "xxx"
+-- expect-stdout-novars --
+import "path"
+
+base: path.Base(_dir, _os)
+-- expect-stdout-tool --
+script-inject
diff --git a/cue/load/config.go b/cue/load/config.go
index 847e447..517346e 100644
--- a/cue/load/config.go
+++ b/cue/load/config.go
@@ -218,6 +218,12 @@
// ensures the user may only specify "prod" or "staging".
Tags []string
+ // TagVars defines a set of key value pair the values of which may be
+ // referenced by tags.
+ //
+ // Use DefaultTagVars to get a pre-loaded map with supported values.
+ TagVars map[string]TagVar
+
// Include all files, regardless of tags.
AllCUEFiles bool
diff --git a/cue/load/loader.go b/cue/load/loader.go
index 1cdd0c1..72f58be 100644
--- a/cue/load/loader.go
+++ b/cue/load/loader.go
@@ -97,6 +97,7 @@
for _, p := range a {
p.ReportError(err)
}
+ return a
}
if l.replacements == nil {
@@ -135,7 +136,7 @@
type loader struct {
cfg *Config
stk importStack
- tags []tag // tags found in files
+ tags []*tag // tags found in files
buildTags map[string]bool
replacements map[ast.Node]ast.Node
}
diff --git a/cue/load/tags.go b/cue/load/tags.go
index 6692ac2..a9ab269 100644
--- a/cue/load/tags.go
+++ b/cue/load/tags.go
@@ -15,7 +15,13 @@
package load
import (
+ "crypto/rand"
+ "encoding/hex"
+ "os"
+ "os/user"
+ "runtime"
"strings"
+ "time"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
@@ -27,6 +33,72 @@
"cuelang.org/go/internal/cli"
)
+// A TagVar represents an injection variable.
+type TagVar struct {
+ // Func returns an ast for a tag variable. It is only called once
+ // per evaluation of a configuration.
+ Func func() (ast.Expr, error)
+
+ // Description documents this TagVar.
+ Description string
+}
+
+const rfc3339 = "2006-01-02T15:04:05.999999999Z"
+
+// DefaultTagVars creates a new map with a set of supported injection variables.
+func DefaultTagVars() map[string]TagVar {
+ return map[string]TagVar{
+ "now": {
+ Func: func() (ast.Expr, error) {
+ return ast.NewString(time.Now().UTC().Format(rfc3339)), nil
+ },
+ },
+ "os": {
+ Func: func() (ast.Expr, error) {
+ return ast.NewString(runtime.GOOS), nil
+ },
+ },
+ "cwd": {
+ Func: func() (ast.Expr, error) {
+ return varToString(os.Getwd())
+ },
+ },
+ "username": {
+ Func: func() (ast.Expr, error) {
+ u, err := user.Current()
+ return varToString(u.Username, err)
+ },
+ },
+ "hostname": {
+ Func: func() (ast.Expr, error) {
+ return varToString(os.Hostname())
+ },
+ },
+ "rand": {
+ Func: func() (ast.Expr, error) {
+ var b [16]byte
+ _, err := rand.Read(b[:])
+ if err != nil {
+ return nil, err
+ }
+ var hx [34]byte
+ hx[0] = '0'
+ hx[1] = 'x'
+ hex.Encode(hx[2:], b[:])
+ return ast.NewLit(token.INT, string(hx[:])), nil
+ },
+ },
+ }
+}
+
+func varToString(s string, err error) (ast.Expr, error) {
+ if err != nil {
+ return nil, err
+ }
+ x := ast.NewString(s)
+ return x, nil
+}
+
// A tag binds an identifier to a field to allow passing command-line values.
//
// A tag is of the form
@@ -45,14 +117,17 @@
// mechanism that would assign different values to different fields based on the
// same shorthand, duplicating functionality that is already available in CUE.
type tag struct {
- key string
- kind cue.Kind
- shorthands []string
+ key string
+ kind cue.Kind
+ shorthands []string
+ vars string // -T flag
+ hasReplacement bool
field *ast.Field
}
-func parseTag(pos token.Pos, body string) (t tag, err errors.Error) {
+func parseTag(pos token.Pos, body string) (t *tag, err errors.Error) {
+ t = &tag{}
t.kind = cue.StringKind
a := internal.ParseAttrBody(pos, body)
@@ -85,27 +160,33 @@
}
}
+ if s, ok, _ := a.Lookup(1, "var"); ok {
+ t.vars = s
+ }
+
return t, nil
}
func (t *tag) inject(value string, l *loader) errors.Error {
e, err := cli.ParseValue(token.NoPos, t.key, value, t.kind)
- if err != nil {
- return err
- }
- injected := ast.NewBinExpr(token.AND, t.field.Value, e)
+ t.injectValue(e, l)
+ return err
+}
+
+func (t *tag) injectValue(x ast.Expr, l *loader) {
+ injected := ast.NewBinExpr(token.AND, t.field.Value, x)
if l.replacements == nil {
l.replacements = map[ast.Node]ast.Node{}
}
l.replacements[t.field.Value] = injected
t.field.Value = injected
- return nil
+ t.hasReplacement = true
}
// findTags defines which fields may be associated with tags.
//
// TODO: should we limit the depth at which tags may occur?
-func findTags(b *build.Instance) (tags []tag, errs errors.Error) {
+func findTags(b *build.Instance) (tags []*tag, errs errors.Error) {
findInvalidTags := func(x ast.Node, msg string) {
ast.Walk(x, nil, func(n ast.Node) {
if f, ok := n.(*ast.Field); ok {
@@ -190,6 +271,35 @@
}
}
}
+
+ if l.cfg.TagVars != nil {
+ vars := map[string]ast.Expr{}
+
+ // Inject tag variables if the tag wasn't already set.
+ for _, t := range l.tags {
+ if t.hasReplacement || t.vars == "" {
+ continue
+ }
+ x, ok := vars[t.vars]
+ if !ok {
+ tv, ok := l.cfg.TagVars[t.vars]
+ if !ok {
+ return errors.Newf(token.NoPos,
+ "tag variable '%s' not found", t.vars)
+ }
+ tag, err := tv.Func()
+ if err != nil {
+ return errors.Wrapf(err, token.NoPos,
+ "error getting tag variable '%s'", t.vars)
+ }
+ x = tag
+ vars[t.vars] = tag
+ }
+ if x != nil {
+ t.injectValue(x, l)
+ }
+ }
+ }
return nil
}
diff --git a/cue/load/tags_test.go b/cue/load/tags_test.go
new file mode 100644
index 0000000..122495f
--- /dev/null
+++ b/cue/load/tags_test.go
@@ -0,0 +1,126 @@
+// Copyright 2021 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package load
+
+import (
+ "bytes"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/cuecontext"
+ "cuelang.org/go/cue/token"
+ "cuelang.org/go/internal/diff"
+)
+
+var testTagVars = map[string]TagVar{
+ "now": stringVar("2006-01-02T15:04:05.999999999Z"),
+ "os": stringVar("m1"),
+ "cwd": stringVar("home"),
+ "username": stringVar("cueser"),
+ "hostname": stringVar("cuebe"),
+ "rand": {Func: func() (ast.Expr, error) {
+ return ast.NewLit(token.INT, "112950970371208119678246559335704039641"), nil
+ }},
+}
+
+func stringVar(s string) TagVar {
+ return TagVar{Func: func() (ast.Expr, error) { return ast.NewString(s), nil }}
+}
+
+func TestTags(t *testing.T) {
+ dir, _ := ioutil.TempDir("", "")
+ defer os.RemoveAll(dir)
+
+ testCases := []struct {
+ in string
+ out string
+ err string
+ }{{
+ in: `
+ rand: int @tag(foo,var=rand)
+ time: string @tag(bar,var=now)
+ host: string @tag(bar,var=hostname)
+ user: string @tag(bar,var=username)
+ cwd: string @tag(bar,var=cwd)
+ `,
+
+ out: `{
+ rand: 112950970371208119678246559335704039641
+ time: "2006-01-02T15:04:05.999999999Z"
+ host: "cuebe"
+ user: "cueser"
+ cwd: "home"
+ }`,
+ }, {
+ in: `
+ time: int @tag(bar,var=now)
+ `,
+ err: `time: conflicting values int and "2006-01-02T15:04:05.999999999Z" (mismatched types int and string)`,
+ }, {
+ // Auto inject only on marked places
+ // TODO: Is this the right thing to do?
+ in: `
+ u1: string @tag(bar,var=username)
+ u2: string @tag(bar)
+ `,
+ out: `{
+ u1: "cueser"
+ u2: string // not filled
+ }`,
+ }, {
+ in: `
+ u1: string @tag(bar,var=user)
+ `,
+ err: `tag variable 'user' not found`,
+ }}
+
+ for _, tc := range testCases {
+ t.Run("", func(t *testing.T) {
+ cfg := &Config{
+ Dir: dir,
+ Overlay: map[string]Source{
+ filepath.Join(dir, "foo.cue"): FromString(tc.in),
+ },
+ TagVars: testTagVars,
+ }
+ b := Instances([]string{"foo.cue"}, cfg)[0]
+
+ c := cuecontext.New()
+ got := c.BuildInstance(b)
+ switch err := got.Err(); {
+ case (err == nil) != (tc.err == ""):
+ t.Fatalf("error: got %v; want %v", err, tc.err)
+
+ case err != nil:
+ got := err.Error()
+ if got != tc.err {
+ t.Fatalf("error: got %v; want %v", got, tc.err)
+ }
+
+ default:
+ want := c.CompileString(tc.out)
+ if !got.Equals(want) {
+ _, es := diff.Diff(got, want)
+ b := &bytes.Buffer{}
+ diff.Print(b, es)
+ t.Error(b)
+ }
+ }
+ })
+ }
+}