diff --git a/pkg/dagger.io/dagger/engine/plan.cue b/pkg/dagger.io/dagger/engine/plan.cue index 20ebce6a..9e11d527 100644 --- a/pkg/dagger.io/dagger/engine/plan.cue +++ b/pkg/dagger.io/dagger/engine/plan.cue @@ -17,8 +17,10 @@ package engine // Send outputs to the client outputs: { - @dagger(notimplemented) + // Export an #FS to the client directories: [name=string]: _#outputDirectory + // Export a string to a file + files: [name=string]: _#outputFile } // Forward network services to and from the client @@ -110,6 +112,19 @@ _#outputDirectory: { dest: string } +_#outputFile: { + $dagger: task: _name: "OutputFile" + + // File contents to export + contents: string + + // Export to this path ON THE CLIENT MACHINE + dest: string + + // Permissions of the file (defaults to 0o644) + permissions?: int +} + // Forward a network endpoint to and from the client _#proxyEndpoint: { $dagger: task: _name: "ProxyEndpoint" diff --git a/plan/task/outputfile.go b/plan/task/outputfile.go new file mode 100644 index 00000000..db42a6f1 --- /dev/null +++ b/plan/task/outputfile.go @@ -0,0 +1,63 @@ +package task + +import ( + "context" + "fmt" + "io/fs" + "os" + + "cuelang.org/go/cue" + "go.dagger.io/dagger/compiler" + "go.dagger.io/dagger/plancontext" + "go.dagger.io/dagger/solver" +) + +func init() { + Register("OutputFile", func() Task { return &outputFileTask{} }) +} + +type outputFileTask struct { +} + +func (c outputFileTask) Run(ctx context.Context, pctx *plancontext.Context, s solver.Solver, v *compiler.Value) (*compiler.Value, error) { + var contents []byte + var err error + + switch kind := v.Lookup("contents").Kind(); kind { + case cue.StringKind: + var str string + str, err = v.Lookup("contents").String() + if err == nil { + contents = []byte(str) + } + case cue.BottomKind: + err = fmt.Errorf("contents is not set") + default: + err = fmt.Errorf("unhandled data type in contents: %s", kind) + } + + if err != nil { + return nil, err + } + + dest, err := v.Lookup("dest").AbsPath() + if err != nil { + return nil, err + } + + perm := fs.FileMode(0644) // default permission + if v.Lookup("permissions").Exists() { + permissions, err := v.Lookup("permissions").Int64() + if err != nil { + return nil, err + } + perm = fs.FileMode(permissions) + } + + err = os.WriteFile(dest, contents, perm) + if err != nil { + return nil, err + } + + return compiler.NewValue(), nil +} diff --git a/tests/plan.bats b/tests/plan.bats index f7f962f3..a520513b 100644 --- a/tests/plan.bats +++ b/tests/plan.bats @@ -69,11 +69,11 @@ setup() { cd "$TESTDIR" "$DAGGER" --europa up ./plan/inputs/secrets/exec.cue "$DAGGER" --europa up ./plan/inputs/secrets/exec_relative.cue - + run "$DAGGER" --europa up ./plan/inputs/secrets/invalid_command.cue assert_failure assert_output --partial 'failed: exec: "rtyet": executable file not found' - + run "$DAGGER" --europa up ./plan/inputs/secrets/invalid_command_options.cue assert_failure assert_output --partial 'option' @@ -83,30 +83,74 @@ setup() { cd "$TESTDIR" "$DAGGER" --europa up --with 'inputs: params: foo:"bar"' ./plan/with/params.cue "$DAGGER" --europa up --with 'actions: verify: env: FOO: "bar"' ./plan/with/actions.cue - + run "$DAGGER" --europa up --with 'inputs: params: foo:1' ./plan/with/params.cue assert_failure assert_output --partial "conflicting values string and 1" - + run "$DAGGER" --europa up ./plan/with/params.cue assert_failure assert_output --partial "actions.verify.env.FOO: non-concrete value string" } -@test "plan/outputs" { - cd "$TESTDIR"/plan/outputs +@test "plan/outputs/directories" { + cd "$TESTDIR"/plan/outputs/directories rm -f "./out/test" "$DAGGER" --europa up ./outputs.cue assert [ -f "./out/test" ] } -@test "plan/outputs relative paths" { +@test "plan/outputs/directories relative paths" { cd "$TESTDIR"/plan - rm -f "./outputs/out/test" - "$DAGGER" --europa up ./outputs/outputs.cue - assert [ -f "./outputs/out/test" ] + rm -f "./outputs/directories/out/test" + "$DAGGER" --europa up ./outputs/directories/outputs.cue + assert [ -f "./outputs/directories/out/test" ] +} + +@test "plan/outputs/files normal usage" { + cd "$TESTDIR"/plan/outputs/files + + "$DAGGER" --europa up ./usage.cue + + run ./test.sh + assert_output "Hello World!" + + run ls -l "./test.sh" + assert_output --partial "-rwxr-x---" + + rm -f "./test.sh" +} + +@test "plan/outputs/files relative path" { + cd "$TESTDIR"/plan + + "$DAGGER" --europa up ./outputs/files/usage.cue + assert [ -f "./outputs/files/test.sh" ] + + rm -f "./outputs/files/test.sh" +} + +@test "plan/outputs/files default permissions" { + cd "$TESTDIR"/plan/outputs/files + + "$DAGGER" --europa up ./default_permissions.cue + + run ls -l "./test" + assert_output --partial "-rw-r--r--" + + rm -f "./test" +} + +@test "plan/outputs/files no contents" { + cd "$TESTDIR"/plan/outputs/files + + run "$DAGGER" --europa up ./no_contents.cue + assert_failure + assert_output --partial "contents is not set" + + assert [ ! -f "./test" ] } @test "plan/platform" { @@ -121,4 +165,4 @@ setup() { # Run with invalid platform run "$DAGGER" --europa up ./plan/platform/config_platform_failure_invalid_platform.cue assert_failure -} \ No newline at end of file +} diff --git a/tests/plan/outputs/.gitignore b/tests/plan/outputs/directories/.gitignore similarity index 100% rename from tests/plan/outputs/.gitignore rename to tests/plan/outputs/directories/.gitignore diff --git a/tests/plan/outputs/outputs.cue b/tests/plan/outputs/directories/outputs.cue similarity index 100% rename from tests/plan/outputs/outputs.cue rename to tests/plan/outputs/directories/outputs.cue diff --git a/tests/plan/outputs/files/default_permissions.cue b/tests/plan/outputs/files/default_permissions.cue new file mode 100644 index 00000000..3dceba5e --- /dev/null +++ b/tests/plan/outputs/files/default_permissions.cue @@ -0,0 +1,10 @@ +package main + +import "dagger.io/dagger" + +dagger.#Plan & { + outputs: files: test: { + contents: "foobar" + dest: "./test" + } +} diff --git a/tests/plan/outputs/files/no_contents.cue b/tests/plan/outputs/files/no_contents.cue new file mode 100644 index 00000000..c7fc9d55 --- /dev/null +++ b/tests/plan/outputs/files/no_contents.cue @@ -0,0 +1,7 @@ +package main + +import "dagger.io/dagger" + +dagger.#Plan & { + outputs: files: test: dest: "./test" +} diff --git a/tests/plan/outputs/files/usage.cue b/tests/plan/outputs/files/usage.cue new file mode 100644 index 00000000..ac2bdd11 --- /dev/null +++ b/tests/plan/outputs/files/usage.cue @@ -0,0 +1,18 @@ +package main + +import "dagger.io/dagger" + +dagger.#Plan & { + outputs: files: { + [path=string]: dest: path + "test.sh": { + contents: """ + #!/bin/bash + set -euo pipefail + echo "Hello World!" + + """ + permissions: 0o750 + } + } +}