Skip to main content

Interacting with the client

dagger.#Plan has a client field that allows interaction with the local machine where the dagger command line client is run. You can:

  • Read and write files and directories;
  • Use local sockets;
  • Load environment variables;
  • Run commands;
  • Get current platform.

Accessing the file system

You may need to load a local directory as a dagger.#FS type in your plan:

dagger.#Plan & {
// Path may be absolute, or relative to current working directory
client: filesystem: ".": read: {
// CUE type defines expected content
contents: dagger.#FS
exclude: ["node_modules"]
}

actions: {
copy: docker.#Copy & {
contents: client.filesystem.".".read.contents
}
// ...
}
}

It’s also easy to write a file locally.

Strings can be written to local files like this:

import (
"encoding/yaml"
// ...
)

dagger.#Plan & {
client: filesystem: "config.yaml": write: {
// Convert a CUE value into a YAML formatted string
contents: yaml.Marshal(actions.pull.output.config)
}
}
caution

Strings in CUE are UTF-8 encoded, so the above example should never be used when handling arbitrary binary data. There is also a limit on the size of these strings (current 16MB). The next example of exporting a dagger.#FS shows how to handle the export of files of arbitrary size and encoding.

Files and directories (in the form of a dagger.#FS) can be exported to the local filesystem too:

package main

import (
"dagger.io/dagger"
"dagger.io/dagger/core"

"universe.dagger.io/go"
)

dagger.#Plan & {
client: filesystem: output: write: contents: actions.buildhello.output

actions: buildhello: {
_source: core.#WriteFile & {
input: dagger.#Scratch
path: "/helloworld.go"
contents: """
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
"""
}
go.#Build & {
source: _source.output
packages: ["/src/helloworld.go"]
}
}
}

Using a local socket

You can use a local socket in an action:

Environment variables

Environment variables can be read from the local machine as strings or secrets, just specify the type:

package main

import (
"dagger.io/dagger"
"universe.dagger.io/docker"
)

dagger.#Plan & {
client: env: {
// load as a string
REGISTRY_USER: string
// load as a secret
REGISTRY_TOKEN: dagger.#Secret
}

actions: pull: docker.#Pull & {
source: "registry.example.com/image"
auth: {
username: client.env.REGISTRY_USER
secret: client.env.REGISTRY_TOKEN
}
}
}

You can provide a default value for strings, or mark any environment variable as optional so they don't fail if not defined in the host:

package main

import (
"dagger.io/dagger"
"universe.dagger.io/docker"
)

dagger.#Plan & {
client: env: {
// load as a string, using a default if not defined
REGISTRY_USER: string | *"_token_"
// load as a secret, but don't fail if not defined
REGISTRY_TOKEN?: dagger.#Secret
}

actions: pull: docker.#Pull & {
source: "registry.example.com/image"
if client.env.REGISTRY_TOKEN != _|_ {
auth: {
username: client.env.REGISTRY_USER
secret: client.env.REGISTRY_TOKEN
}
}
}
}

Running commands

Sometimes you need something more advanced that only a local command can give you:

package main

import (
"strings"
"dagger.io/dagger"
"dagger.io/dagger/core"
)

dagger.#Plan & {
client: commands: {
os: {
// notice: this command isn't available on Windows
name: "uname"
args: ["-s"]
}
arch: {
name: "uname"
args: ["-m"]
}
}

actions: test: {
// using #Nop because we need an action for the outputs
_os: core.#Nop & {
// command outputs usually add a new line, you can trim it
input: strings.TrimSpace(client.commands.os.stdout)
}
_arch: core.#Nop & {
// we access the command's output via the `stdout` field
input: strings.TrimSpace(client.commands.arch.stdout)
}
// action outputs for debugging
os: _os.output
arch: _arch.output
}
}
Output
➜  dagger do test
[] client.commands.arch
[] client.commands.os
[] actions.test
Field Value
os "Darwin"
arch "x86_64"
tip

There's a more portable way to find the OS and CPU architecture, just use the client's platform.

tip

To learn more about controlling action outputs, see the Handling action outputs guide.

Standard input

If your command needs to read from the standard input stream, you can use stdin:

package main

import (
"strings"
"dagger.io/dagger"
"dagger.io/dagger/core"
)

dagger.#Plan & {
client: commands: rev: {
// Print stdin in reverse
// Same as `rev <(echo olleh)` or `echo olleh | rev`
name: "rev"
stdin: "olleh"
}

actions: test: {
_op: core.#Nop & {
input: strings.TrimSpace(client.commands.rev.stdout)
}
verify: _op.output & "hello"
}
}

Capturing errors

Attention

A failing exit code will fail the plan, so if you need to further debug the cause of a failed command, you can just try running it directly in your computer. Some commands print to stderr for messages that aren't fatal. This is for those cases.

If you need the stderr output of a command in an action, you can capture it with stderr:

package main

import (
"strings"
"dagger.io/dagger"
"dagger.io/dagger/core"
)

dagger.#Plan & {
client: commands: cat: {
name: "sh"
// simulate error output without failed exit status
flags: "-c": """
cat /foobar
echo ok
"""
}

actions: test: {
_op: core.#Nop & {
input: strings.TrimSpace(client.commands.cat.stderr)
}
error: _op.output
}
}
Output
Field  Value
error "cat: /foobar: No such file or directory"```

Secrets

All input/output streams (stdout, stderr and stdin) accept a dagger.#Secret instead of a string. You can see a simple example using SOPS.

It may be useful to use a secret as an input to a command as well:

package main

import (
"strings"
"dagger.io/dagger"
"dagger.io/dagger/core"
)

dagger.#Plan & {
client: {
env: PRIVATE_KEY: string | *"/home/user/.ssh/id_rsa"
commands: {
pkey: {
name: "cat"
args: [env.PRIVATE_KEY]
stdout: dagger.#Secret
}
digest: {
name: "openssl"
args: ["dgst", "-sha256"]
stdin: pkey.stdout // a secret
}
}
}

actions: test: {
_op: core.#Nop & {
input: strings.TrimSpace(client.commands.digest.stdout)
}
digest: _op.output
}
}
Output
Field   Value
digest "aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f"

Another use case is needing to provide a password from input to a command.

Platform

If you need the current platform, there’s a more portable way than running the uname command:

package main

import (
"dagger.io/dagger"
"universe.dagger.io/python"
)

dagger.#Plan & {
client: _

actions: test: python.#Run & {
script: contents: "print('Platform: \(client.platform.os) / \(client.platform.arch)')"
always: true
}
}
dagger --log-format plain do test
INFO  actions.test._run._exec | #4 0.209 Platform: darwin / amd64
Remember

This is the platform where the dagger binary is being run (a.k.a client), which is different from the environment where the action is actually run (i.e., BuildKit, a.k.a server).

tip

If client: _ confuses you, see Use top to match anything.

tip

You can see an example of this being used in our own CI dagger plan in the build action, to specify the os and arch fields in go.#Build:

build: go.#Build & {
source: _source
os: client.platform.os
arch: client.platform.arch
...
}