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)
}
}
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:
- Linux/macOS
- Windows
dagger.#Plan & {
client: network: "unix:///var/run/docker.sock": connect: dagger.#Socket
actions: {
image: alpine.#Build & {
packages: "docker-cli": {}
}
run: docker.#Run & {
input: image.output
mounts: docker: {
dest: "/var/run/docker.sock"
contents: client.network."unix:///var/run/docker.sock".connect
}
command: {
name: "docker"
args: ["info"]
}
}
}
}
dagger.#Plan & {
client: network: "npipe:////./pipe/docker_engine": connect: dagger.#Socket
actions: {
image: alpine.#Build & {
packages: "docker-cli": {}
}
run: docker.#Run & {
input: image.output
mounts: docker: {
dest: "/var/run/docker.sock"
contents: client.network."npipe:////./pipe/docker_engine".connect
}
command: {
name: "docker"
args: ["info"]
}
}
}
}
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
}
}
➜ dagger do test
[✔] client.commands.arch
[✔] client.commands.os
[✔] actions.test
Field Value
os "Darwin"
arch "x86_64"
There's a more portable way to find the OS and CPU architecture, just use the client's platform.
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
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
}
}
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
}
}
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
}
}
INFO actions.test._run._exec | #4 0.209 Platform: darwin / amd64
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).
If client: _
confuses you, see Use top to match anything.
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
...
}