diff --git a/cmd/dagger/cmd/input/list.go b/cmd/dagger/cmd/input/list.go new file mode 100644 index 00000000..0ad4c701 --- /dev/null +++ b/cmd/dagger/cmd/input/list.go @@ -0,0 +1,120 @@ +package input + +import ( + "context" + "fmt" + "os" + "text/tabwriter" + + "dagger.io/go/cmd/dagger/cmd/common" + "dagger.io/go/cmd/dagger/logger" + "dagger.io/go/dagger" + + "cuelang.org/go/cue/ast" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var listCmd = &cobra.Command{ + Use: "list [TARGET] [flags]", + Short: "List for the inputs of a deployment", + Args: cobra.MaximumNArgs(1), + PreRun: func(cmd *cobra.Command, args []string) { + // Fix Viper bug for duplicate flags: + // https://github.com/spf13/viper/issues/233 + if err := viper.BindPFlags(cmd.Flags()); err != nil { + panic(err) + } + }, + Run: func(cmd *cobra.Command, args []string) { + lg := logger.New() + ctx := lg.WithContext(cmd.Context()) + + store, err := dagger.DefaultStore() + if err != nil { + lg.Fatal().Err(err).Msg("failed to load store") + } + + deployment := common.GetCurrentDeploymentState(ctx, store) + + // print any persisted inputs + if len(deployment.Inputs) > 0 { + fmt.Println("Saved Inputs:") + for _, input := range deployment.Inputs { + // Todo, how to pull apart an input to print relevant information + fmt.Printf("%s: %v\n", input.Key, input.Value) + } + // add some space + fmt.Println() + } + + lg = lg.With(). + Str("deploymentName", deployment.Name). + Str("deploymentId", deployment.ID). + Logger() + + c, err := dagger.NewClient(ctx, "", false) + if err != nil { + lg.Fatal().Err(err).Msg("unable to create client") + } + + _, err = c.Do(ctx, deployment, func(lCtx context.Context, lDeploy *dagger.Deployment, lSolver dagger.Solver) error { + inputs, err := lDeploy.ScanInputs() + if err != nil { + return err + } + + fmt.Println("Plan Inputs:") + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + fmt.Fprintln(w, "Path\tFrom\tType") + + for _, val := range inputs { + // check for references + // this is here because it has issues + // so we wrap it in a flag to control its usage while debugging + _, vals := val.Expr() + if !viper.GetBool("keep-references") { + foundRef := false + for _, ve := range vals { + s := ve.Source() + switch s.(type) { + case *ast.Ident: + foundRef = true + } + } + if foundRef { + continue + } + } + + // Construct output as a tab-table + // get path / pkg import (if available) + inst, _ := val.Reference() + pkg := "(plan)" + if inst != nil { + pkg = inst.ImportPath + } + + fmt.Fprintf(w, "%s\t%s\t%v\n", val.Path(), pkg, val) + + } + // ensure we flush the output buf + w.Flush() + + return nil + }) + + if err != nil { + lg.Fatal().Err(err).Msg("failed to query deployment") + } + + }, +} + +func init() { + listCmd.Flags().BoolP("keep-references", "R", false, "Try to eliminate references") + + if err := viper.BindPFlags(listCmd.Flags()); err != nil { + panic(err) + } +} diff --git a/cmd/dagger/cmd/input/root.go b/cmd/dagger/cmd/input/root.go index 07ad939e..3daae1f6 100644 --- a/cmd/dagger/cmd/input/root.go +++ b/cmd/dagger/cmd/input/root.go @@ -27,6 +27,7 @@ func init() { textCmd, jsonCmd, yamlCmd, + listCmd, ) } diff --git a/dagger/deployment.go b/dagger/deployment.go index 603feb57..12226263 100644 --- a/dagger/deployment.go +++ b/dagger/deployment.go @@ -10,6 +10,7 @@ import ( "cuelang.org/go/cue" cueflow "cuelang.org/go/tools/flow" "dagger.io/go/dagger/compiler" + "dagger.io/go/pkg/cuetils" "dagger.io/go/stdlib" "github.com/opentracing/opentracing-go" @@ -290,3 +291,12 @@ func newPipelineRunner(inst *cue.Instance, computed *compiler.Value, s Solver) c return nil }) } + +func (d *Deployment) ScanInputs() ([]cue.Value, error) { + vals, err := cuetils.ScanForInputs(d.plan.Cue()) + if err != nil { + return nil, err + } + + return vals, nil +} diff --git a/pkg/cuetils/scan.go b/pkg/cuetils/scan.go new file mode 100644 index 00000000..499944bd --- /dev/null +++ b/pkg/cuetils/scan.go @@ -0,0 +1,136 @@ +package cuetils + +import ( + "cuelang.org/go/cue" +) + +// ScanForInputs walks a Value looking for potential inputs +// - non-concrete values or values with defaults +// - exclude @dagger(computed) and #up +// - exclude values which have references +func ScanForInputs(value cue.Value) ([]cue.Value, error) { + var ( + vals []cue.Value + err error + ) + + // walk before function, bool return is if the walk should recurse again + before := func(v cue.Value) (bool, error) { + // explicit phase + // look for #up + label, _ := v.Label() + if label == "#up" { + return false, nil + } + + // look to exclude any @dagger(computed) + attrs := v.Attributes(cue.ValueAttr) + for _, attr := range attrs { + name := attr.Name() + // match `@dagger(...)` + if name == "dagger" { + // loop over args (CSV content in attribute) + for i := 0; i < attr.NumArgs(); i++ { + key, _ := attr.Arg(i) + + // we found an explicit computed value + if key == "computed" { + return false, nil + } + } + } + } + + // inference phase + switch v.IncompleteKind() { + case cue.StructKind: + return true, nil + + case cue.ListKind: + if !v.IsConcrete() { + vals = append(vals, v) + return false, nil + } + return true, nil + + default: + + // a leaf with default? + _, has := v.Default() + if has { + vals = append(vals, v) + // recurse here? + return false, nil + } + + // is this leaf not concrete? (should cause an error) + if v.Validate(cue.Concrete(true), cue.Optional(true)) != nil { + vals = append(vals, v) + } + + return false, nil + } + } + + // walk + err = walkValue(value, before, nil) + if err != nil { + return nil, err + } + + return vals, nil +} + +// walkValue is a custome walk function so that we recurse into more types than CUE's buildin walk +// specificially, we need to customize the options to val.Fields when val is a struct +func walkValue(val cue.Value, before func(cue.Value) (bool, error), after func(cue.Value) error) error { + if before != nil { + recurse, err := before(val) + if err != nil { + return err + } + + // should we recurse into fields + if recurse { + switch val.IncompleteKind() { + case cue.StructKind: + // provide custom args to ensure we walk nested defs + // and that optionals are included + iter, err := val.Fields( + cue.Definitions(true), + cue.Optional(true), + ) + if err != nil { + return err + } + for iter.Next() { + err := walkValue(iter.Value(), before, after) + if err != nil { + return err + } + } + + case cue.ListKind: + iter, err := val.List() + if err != nil { + return err + } + for iter.Next() { + err := walkValue(iter.Value(), before, after) + if err != nil { + return err + } + } + } + } + } + + if after != nil { + err := after(val) + if err != nil { + return err + } + } + + return nil +} diff --git a/tests/cli.bats b/tests/cli.bats index 907a3c45..881f5af0 100644 --- a/tests/cli.bats +++ b/tests/cli.bats @@ -217,3 +217,10 @@ setup() { "source": {} }' } + +@test "dagger input scan" { + "$DAGGER" new --plan-dir "$TESTDIR"/cli/input/scan "scan" + run "$DAGGER" input scan -d "input" + assert_success + +} diff --git a/tests/cli/input/scan/main.cue b/tests/cli/input/scan/main.cue new file mode 100644 index 00000000..5405c218 --- /dev/null +++ b/tests/cli/input/scan/main.cue @@ -0,0 +1,50 @@ +package main + +foo: string + +name: string | *"world" +message: "Hello, \(name)!" + +optional?: string + +missing: [string]: string + +bar: { + a: string + #c: string + b: int @dagger(computed) +} + +// may be missing +#inputs: { + hello: string + missing: *"" | string +} + +// substitute +let A = string +let B = bar.a + +//let Ba = bar.a +//let Bb = bar.b +let D = "hello" + +refd: { + a: string + b: { + ref1: a + ref2: A + aa: B + bb: D + } + #c: C: string +} + +#fld1: string + +exec: { + cmd: string + #up: [{foo: string}] +} + +list: [...string]