Skip to content

Commit

Permalink
cue/load: support injection of system variables
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
mpvl committed May 5, 2021
1 parent 1f618f0 commit bcdf277
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 23 deletions.
3 changes: 3 additions & 0 deletions cmd/cue/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,9 @@ func (p *buildPlan) matchFile(file string) bool {
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
}

Expand Down
25 changes: 14 additions & 11 deletions cmd/cue/cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,18 @@ import (

// 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"
Expand Down Expand Up @@ -90,6 +91,8 @@ func addOrphanFlags(f *pflag.FlagSet) {
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
Expand Down
25 changes: 25 additions & 0 deletions cmd/cue/cmd/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,31 @@ field. For instance
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
`,
}

Expand Down
1 change: 1 addition & 0 deletions cmd/cue/cmd/testdata/script/help_cmd.txt
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ Available Commands:
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
Expand Down
1 change: 1 addition & 0 deletions cmd/cue/cmd/testdata/script/help_cmd_flags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ Usage:
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
Expand Down
52 changes: 51 additions & 1 deletion cmd/cue/cmd/testdata/script/inject.txt
Original file line number Diff line number Diff line change
@@ -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 --
Expand All @@ -11,3 +25,39 @@ cmp stdout expect-stdout

-- 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
6 changes: 6 additions & 0 deletions cue/load/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ type Config struct {
// 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

Expand Down
3 changes: 2 additions & 1 deletion cue/load/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func Instances(args []string, c *Config) []*build.Instance {
for _, p := range a {
p.ReportError(err)
}
return a
}

if l.replacements == nil {
Expand Down Expand Up @@ -135,7 +136,7 @@ const (
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
}
Expand Down
130 changes: 120 additions & 10 deletions cue/load/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -27,6 +33,72 @@ import (
"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
Expand All @@ -45,14 +117,17 @@ import (
// 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)
Expand Down Expand Up @@ -85,27 +160,33 @@ func parseTag(pos token.Pos, body string) (t tag, err errors.Error) {
}
}

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 {
Expand Down Expand Up @@ -190,6 +271,35 @@ func injectTags(tags []string, l *loader) errors.Error {
}
}
}

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
}

Expand Down

0 comments on commit bcdf277

Please sign in to comment.