diff --git a/cmd/dagger/cmd/common/common.go b/cmd/dagger/cmd/common/common.go index cac81e68..a83560e8 100644 --- a/cmd/dagger/cmd/common/common.go +++ b/cmd/dagger/cmd/common/common.go @@ -2,10 +2,13 @@ package common import ( "context" + "fmt" + "strings" "github.com/rs/zerolog/log" "github.com/spf13/viper" "go.dagger.io/dagger/client" + "go.dagger.io/dagger/compiler" "go.dagger.io/dagger/environment" "go.dagger.io/dagger/solver" "go.dagger.io/dagger/state" @@ -97,3 +100,49 @@ func EnvironmentUp(ctx context.Context, state *state.State, noCache bool) *envir } return result } + +// FormatValue returns the String representation of the cue value +func FormatValue(val *compiler.Value) string { + if val.HasAttr("artifact") { + return "dagger.#Artifact" + } + if val.HasAttr("secret") { + return "dagger.#Secret" + } + if val.IsConcreteR() != nil { + return val.Cue().IncompleteKind().String() + } + // value representation in Cue + valStr := fmt.Sprintf("%v", val.Cue()) + // escape \n + return strings.ReplaceAll(valStr, "\n", "\\n") +} + +// ValueDocString returns the value doc from the comment lines +func ValueDocString(val *compiler.Value) string { + docs := []string{} + for _, c := range val.Cue().Doc() { + docs = append(docs, strings.TrimSpace(c.Text())) + } + doc := strings.Join(docs, " ") + + lines := strings.Split(doc, "\n") + + // Strip out FIXME, TODO, and INTERNAL comments + docs = []string{} + for _, line := range lines { + if strings.HasPrefix(line, "FIXME: ") || + strings.HasPrefix(line, "TODO: ") || + strings.HasPrefix(line, "INTERNAL: ") { + continue + } + if len(line) == 0 { + continue + } + docs = append(docs, line) + } + if len(docs) == 0 { + return "-" + } + return strings.Join(docs, " ") +} diff --git a/cmd/dagger/cmd/input/list.go b/cmd/dagger/cmd/input/list.go index aae85878..a4c8130d 100644 --- a/cmd/dagger/cmd/input/list.go +++ b/cmd/dagger/cmd/input/list.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "strings" "text/tabwriter" "go.dagger.io/dagger/client" @@ -53,18 +52,20 @@ var listCmd = &cobra.Command{ } w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) - fmt.Fprintln(w, "Input\tType\tValue\tSet by user\tDescription") + fmt.Fprintln(w, "Input\tValue\tSet by user\tDescription") for _, inp := range inputs { isConcrete := (inp.IsConcreteR() == nil) _, hasDefault := inp.Default() - valStr := "-" - if isConcrete { - valStr, _ = inp.Cue().String() - } - if hasDefault { - valStr = fmt.Sprintf("%s (default)", valStr) - } + // valStr := "-" + // if isConcrete { + // valStr, _ = inp.Cue().String() + // } + // if hasDefault { + // valStr = fmt.Sprintf("%s (default)", valStr) + // } + + // valStr = strings.ReplaceAll(valStr, "\n", "\\n") if !viper.GetBool("all") { // skip input that is not overridable @@ -73,12 +74,11 @@ var listCmd = &cobra.Command{ } } - fmt.Fprintf(w, "%s\t%s\t%s\t%t\t%s\n", + fmt.Fprintf(w, "%s\t%s\t%t\t%s\n", inp.Path(), - getType(inp), - valStr, + common.FormatValue(inp), isUserSet(st, inp), - getDocString(inp), + common.ValueDocString(inp), ) } @@ -103,44 +103,6 @@ func isUserSet(env *state.State, val *compiler.Value) bool { return false } -func getType(val *compiler.Value) string { - if val.HasAttr("artifact") { - return "dagger.#Artifact" - } - if val.HasAttr("secret") { - return "dagger.#Secret" - } - return val.Cue().IncompleteKind().String() -} - -func getDocString(val *compiler.Value) string { - docs := []string{} - for _, c := range val.Cue().Doc() { - docs = append(docs, strings.TrimSpace(c.Text())) - } - doc := strings.Join(docs, " ") - - lines := strings.Split(doc, "\n") - - // Strip out FIXME, TODO, and INTERNAL comments - docs = []string{} - for _, line := range lines { - if strings.HasPrefix(line, "FIXME: ") || - strings.HasPrefix(line, "TODO: ") || - strings.HasPrefix(line, "INTERNAL: ") { - continue - } - if len(line) == 0 { - continue - } - docs = append(docs, line) - } - if len(docs) == 0 { - return "-" - } - return strings.Join(docs, " ") -} - func init() { listCmd.Flags().BoolP("all", "a", false, "List all inputs (include non-overridable)") diff --git a/cmd/dagger/cmd/output/list.go b/cmd/dagger/cmd/output/list.go new file mode 100644 index 00000000..502d7b10 --- /dev/null +++ b/cmd/dagger/cmd/output/list.go @@ -0,0 +1,86 @@ +package output + +import ( + "context" + "fmt" + "os" + "text/tabwriter" + + "go.dagger.io/dagger/client" + "go.dagger.io/dagger/cmd/dagger/cmd/common" + "go.dagger.io/dagger/cmd/dagger/logger" + "go.dagger.io/dagger/environment" + "go.dagger.io/dagger/solver" + "go.dagger.io/dagger/state" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var listCmd = &cobra.Command{ + Use: "list [TARGET] [flags]", + Short: "List the outputs of an environment", + 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()) + + workspace := common.CurrentWorkspace(ctx) + st := common.CurrentEnvironmentState(ctx, workspace) + + ListOutputs(ctx, st, true) + }, +} + +func ListOutputs(ctx context.Context, st *state.State, isTTY bool) { + lg := log.Ctx(ctx).With(). + Str("environment", st.Name). + Logger() + + c, err := client.New(ctx, "", false) + if err != nil { + lg.Fatal().Err(err).Msg("unable to create client") + } + + _, err = c.Do(ctx, st, func(ctx context.Context, env *environment.Environment, s solver.Solver) error { + outputs, err := env.ScanOutputs(ctx) + if err != nil { + return err + } + + if !isTTY { + for _, out := range outputs { + lg.Info().Str("name", out.Path().String()). + Str("value", fmt.Sprintf("%v", out.Cue())). + Msg("output") + } + return nil + } + + w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) + fmt.Fprintln(w, "Output\tValue\tDescription") + + for _, out := range outputs { + fmt.Fprintf(w, "%s\t%s\t%s\n", + out.Path(), + common.FormatValue(out), + common.ValueDocString(out), + ) + } + + w.Flush() + return nil + }) + + if err != nil { + lg.Fatal().Err(err).Msg("failed to query environment") + } +} diff --git a/cmd/dagger/cmd/output/root.go b/cmd/dagger/cmd/output/root.go index d3c87f01..c765c55d 100644 --- a/cmd/dagger/cmd/output/root.go +++ b/cmd/dagger/cmd/output/root.go @@ -9,7 +9,6 @@ var Cmd = &cobra.Command{ } func init() { - Cmd.AddCommand( - dirCmd, - ) + // Cmd.AddCommand(dirCmd) + Cmd.AddCommand(listCmd) } diff --git a/cmd/dagger/cmd/up.go b/cmd/dagger/cmd/up.go index 4d26a695..8916e515 100644 --- a/cmd/dagger/cmd/up.go +++ b/cmd/dagger/cmd/up.go @@ -7,6 +7,7 @@ import ( "cuelang.org/go/cue" "go.dagger.io/dagger/client" "go.dagger.io/dagger/cmd/dagger/cmd/common" + "go.dagger.io/dagger/cmd/dagger/cmd/output" "go.dagger.io/dagger/cmd/dagger/logger" "go.dagger.io/dagger/compiler" "go.dagger.io/dagger/environment" @@ -46,6 +47,8 @@ var upCmd = &cobra.Command{ if err := workspace.Save(ctx, st); err != nil { lg.Fatal().Err(err).Msg("failed to update environment") } + + output.ListOutputs(ctx, st, term.IsTerminal(int(os.Stdout.Fd()))) }, } diff --git a/environment/environment.go b/environment/environment.go index 7cb0a45a..a0aa24b7 100644 --- a/environment/environment.go +++ b/environment/environment.go @@ -322,3 +322,23 @@ func (e *Environment) ScanInputs(ctx context.Context, mergeUserInputs bool) ([]* return scanInputs(ctx, src), nil } + +func (e *Environment) ScanOutputs(ctx context.Context) ([]*compiler.Value, error) { + src, err := e.prepare(ctx) + if err != nil { + return nil, err + } + + if e.state.Computed != "" { + computed, err := compiler.DecodeJSON("", []byte(e.state.Computed)) + if err != nil { + return nil, err + } + + if err := src.FillPath(cue.MakePath(), computed); err != nil { + return nil, err + } + } + + return scanOutputs(ctx, src), nil +} diff --git a/environment/inputs_scan.go b/environment/inputs_scan.go index 9fcea5b0..0939a9ac 100644 --- a/environment/inputs_scan.go +++ b/environment/inputs_scan.go @@ -66,3 +66,23 @@ func scanInputs(ctx context.Context, value *compiler.Value) []*compiler.Value { return inputs } + +func scanOutputs(ctx context.Context, value *compiler.Value) []*compiler.Value { + lg := log.Ctx(ctx) + inputs := []*compiler.Value{} + + value.Walk( + func(val *compiler.Value) bool { + if !val.HasAttr("output") { + return true + } + + lg.Debug().Str("value.Path", val.Path().String()).Msg("found output") + inputs = append(inputs, val) + + return true + }, nil, + ) + + return inputs +} diff --git a/examples/jamstack/backend.cue b/examples/jamstack/backend.cue index 4932502f..200d389c 100644 --- a/examples/jamstack/backend.cue +++ b/examples/jamstack/backend.cue @@ -9,40 +9,46 @@ import ( // Backend configuration backend: { // Source code to build this container - source: git.#Repository | dagger.#Artifact + source: git.#Repository | dagger.#Artifact @dagger(input) // Container environment variables - environment: [string]: string + environment: { + [string]: string @dagger(input) + } // Public hostname (need to match the master domain configures on the loadbalancer) - hostname: string + hostname: string @dagger(input) // Container configuration container: { // Desired number of running containers - desiredCount: *1 | int + desiredCount: *1 | int @dagger(input) // Time to wait for the HTTP timeout to complete - healthCheckTimeout: *10 | int + healthCheckTimeout: *10 | int @dagger(input) // HTTP Path to perform the healthcheck request (HTTP Get) - healthCheckPath: *"/" | string + healthCheckPath: *"/" | string @dagger(input) // Number of times the health check needs to fail before recycling the container - healthCheckUnhealthyThreshold: *2 | int + healthCheckUnhealthyThreshold: *2 | int @dagger(input) // Port used by the process inside the container - port: *80 | int + port: *80 | int @dagger(input) // Memory to allocate - memory: *1024 | int + memory: *1024 | int @dagger(input) // Override the default container command - command: [...string] + command: [...string] @dagger(input) // Custom dockerfile path - dockerfilePath: *"" | string + dockerfilePath: *"" | string @dagger(input) // docker build args - dockerBuildArgs: [string]: string + dockerBuildArgs: { + [string]: string @dagger(input) + } } // Init container runs only once when the main container starts initContainer: { - command: [...string] - environment: [string]: string + command: [...string] @dagger(input) + environment: { + [string]: string @dagger(input) + } } } diff --git a/examples/jamstack/database.cue b/examples/jamstack/database.cue index a2ed656e..ab3f470b 100644 --- a/examples/jamstack/database.cue +++ b/examples/jamstack/database.cue @@ -7,7 +7,7 @@ import ( database: { let slug = name - dbType: "mysql" | "postgresql" + dbType: "mysql" | "postgresql" @dagger(input) db: rds.#CreateDB & { config: infra.awsConfig diff --git a/examples/jamstack/frontend.cue b/examples/jamstack/frontend.cue index fe96e014..d6596e6a 100644 --- a/examples/jamstack/frontend.cue +++ b/examples/jamstack/frontend.cue @@ -9,24 +9,30 @@ import ( frontend: { // Source code to build the app - source: git.#Repository | dagger.#Artifact + source: git.#Repository | dagger.#Artifact @dagger(input) - writeEnvFile?: string + writeEnvFile?: string @dagger(input) // Yarn Build yarn: { // Run this yarn script - script: string | *"build" + script: string | *"build" @dagger(input) // Read build output from this directory // (path must be relative to working directory). - buildDir: string | *"build" + buildDir: string | *"build" @dagger(input) } // Build environment variables - environment: [string]: string - environment: NODE_ENV: string | *"production" - environment: APP_URL: "https://\(name).netlify.app/" + environment: { + [string]: string @dagger(input) + } + environment: { + NODE_ENV: string | *"production" @dagger(input) + } + environment: { + APP_URL: "https://\(name).netlify.app/" @dagger(input) + } } frontend: { diff --git a/examples/jamstack/infra.cue b/examples/jamstack/infra.cue index 9b04eccc..db076093 100644 --- a/examples/jamstack/infra.cue +++ b/examples/jamstack/infra.cue @@ -10,26 +10,26 @@ infra: { awsConfig: aws.#Config // VPC Id - vpcId: string + vpcId: string @dagger(input) // ECR Image repository - ecrRepository: string + ecrRepository: string @dagger(input) // ECS cluster name - ecsClusterName: string + ecsClusterName: string @dagger(input) // Execution Role ARN used for all tasks running on the cluster - ecsTaskRoleArn?: string + ecsTaskRoleArn?: string @dagger(input) // ELB listener ARN - elbListenerArn: string + elbListenerArn: string @dagger(input) // Secret ARN for the admin password of the RDS Instance - rdsAdminSecretArn: string + rdsAdminSecretArn: string @dagger(input) // ARN of the RDS Instance - rdsInstanceArn: string + rdsInstanceArn: string @dagger(input) // Netlify credentials - netlifyAccount: netlify.#Account + netlifyAccount: netlify.#Account @dagger(input) } diff --git a/examples/jamstack/main.cue b/examples/jamstack/main.cue index e9e4d2ab..05b40ef1 100644 --- a/examples/jamstack/main.cue +++ b/examples/jamstack/main.cue @@ -1,7 +1,7 @@ package main // Name of the application -name: string & =~"[a-z0-9-]+" +name: string & =~"[a-z0-9-]+" @dagger(input) // Inject db info in the container environment backend: environment: { @@ -17,6 +17,6 @@ backend: environment: { frontend: environment: APP_URL_API: url.backendURL url: { - frontendURL: frontend.site.url - backendURL: "https://\(backend.hostname)/" + frontendURL: frontend.site.url @dagger(output) + backendURL: "https://\(backend.hostname)/" @dagger(output) } diff --git a/examples/monitoring/main.cue b/examples/monitoring/main.cue index 1021cdbf..49469f2d 100644 --- a/examples/monitoring/main.cue +++ b/examples/monitoring/main.cue @@ -6,14 +6,14 @@ import ( // AWS account: credentials and region awsConfig: aws.#Config & { - region: *"us-east-1" | string + region: *"us-east-1" | string @dagger(input) } // URL of the website to monitor -website: string | *"https://www.google.com" +website: string | *"https://www.google.com" @dagger(input) // Email address to notify of monitoring alerts -email: string +email: string @dagger(input) // The monitoring service running on AWS Cloudwatch monitor: #HTTPMonitor & { diff --git a/examples/react/main.cue b/examples/react/main.cue index 1e4cb1df..44c60c9e 100644 --- a/examples/react/main.cue +++ b/examples/react/main.cue @@ -15,7 +15,7 @@ repo: git.#Repository & { // Host the application with Netlify www: netlify.#Site & { // Site name can be overridden - name: string | *"dagger-examples-react" + name: string | *"dagger-examples-react" @dagger(input) // Deploy the output of yarn build // (Netlify build feature is not used, to avoid extra cost). diff --git a/examples/simple-s3/main.cue b/examples/simple-s3/main.cue index 848ffc96..3301655b 100644 --- a/examples/simple-s3/main.cue +++ b/examples/simple-s3/main.cue @@ -16,7 +16,9 @@ bucket: *"dagger-io-examples" | string @dagger(input) // Source code to deploy source: dagger.#Artifact @dagger(input) -url: "\(deploy.url)index.html" + +// Deployed URL +url: "\(deploy.url)index.html" @dagger(output) deploy: s3.#Put & { always: true diff --git a/stdlib/aws/s3/s3.cue b/stdlib/aws/s3/s3.cue index 9555f419..5207066a 100644 --- a/stdlib/aws/s3/s3.cue +++ b/stdlib/aws/s3/s3.cue @@ -76,7 +76,7 @@ import ( opts="--content-type $CONTENT_TYPE" fi aws s3 $op $opts /source "$TARGET" - echo "$TARGET" \ + echo -n "$TARGET" \ | sed -E 's=^s3://([^/]*)/=https://\1.s3.amazonaws.com/=' \ > /url """#, diff --git a/tests/cli.bats b/tests/cli.bats index e5e0f81f..a0ddcff6 100644 --- a/tests/cli.bats +++ b/tests/cli.bats @@ -296,27 +296,53 @@ setup() { outAll="$("$DAGGER" input list --all -e "list")" #note: this is the recommended way to use pipes with bats - run bash -c "echo \"$out\" | grep awsConfig.accessKey | grep '#Secret' | grep false" + run bash -c "echo \"$out\" | grep awsConfig.accessKey | grep 'dagger.#Secret' | grep 'AWS access key'" assert_success - run bash -c "echo \"$out\" | grep cfgInline.source | grep '#Artifact' | grep false | grep 'source dir'" + run bash -c "echo \"$out\" | grep cfgInline.source | grep 'dagger.#Artifact' | grep false | grep 'source dir'" assert_success run bash -c "echo \"$outAll\" | grep cfg2" assert_failure - run bash -c "echo \"$out\" | grep cfgInline.strDef | grep string | grep 'yolo (default)' | grep false" + run bash -c "echo \"$out\" | grep cfgInline.strDef | grep '*yolo | string' | grep false" assert_success run bash -c "echo \"$out\" | grep cfg.num" assert_failure - run bash -c "echo \"$outAll\" | grep cfg.num | grep int" + run bash -c "echo \"$outAll\" | grep cfg.num | grep 21 | grep -v int" assert_success run bash -c "echo \"$out\" | grep cfg.strSet" assert_failure - run bash -c "echo \"$outAll\" | grep cfg.strSet | grep string | grep pipo" + run bash -c "echo \"$outAll\" | grep cfg.strSet | grep pipo" + assert_success +} + +@test "dagger output list" { + "$DAGGER" init + + dagger_new_with_plan list "$TESTDIR"/cli/output/list + + out="$("$DAGGER" output list -e "list")" + + run bash -c "echo \"$out\" | grep cfgInline.url | grep 'http://this.is.a.test/' | grep 'test url description'" + assert_success + + run bash -c "echo \"$out\" | grep cfg.url | grep 'http://this.is.a.test/' | grep 'test url description'" + assert_success + + run bash -c "echo \"$out\" | grep cfg2.url | grep 'http://this.is.a.test/' | grep 'test url description'" + assert_success + + run bash -c "echo \"$out\" | grep cfg.foo | grep '*42 | int'" + assert_success + + run bash -c "echo \"$out\" | grep cfg2.bar | grep 'dagger.#Artifact'" + assert_success + + run bash -c "echo \"$out\" | grep cfg2.str | grep 'string'" assert_success } diff --git a/tests/cli/output/list/main.cue b/tests/cli/output/list/main.cue new file mode 100644 index 00000000..a19ae14d --- /dev/null +++ b/tests/cli/output/list/main.cue @@ -0,0 +1,26 @@ +package main + +import ( + "dagger.io/dagger" +) + +#A: { + // a string + str: string @dagger(output) + strSet: "pipo" @dagger(input) + strDef: *"yolo" | string @dagger(input) + + // test url description + url: "http://this.is.a.test/" @dagger(output) + url2: url + foo: int | *42 @dagger(output) + + bar: dagger.#Artifact @dagger(output) +} + +cfgInline: { + #A +} + +cfg: #A +cfg2: cfg