Return Values
In addition to returning basic types (string, boolean, ...), Dagger Functions can also return any of Dagger's core types, such as Directory
, Container
, Service
, Secret
, and many more.
This opens powerful applications to Dagger Functions. For example, a Dagger Function that builds binaries could take a directory with the source code as argument and return another directory (a "just-in-time" directory) containing just binaries or a container image (a "just-in-time" container) with the binaries included.
If a function doesn't have a return type annotation, it'll be translated to the dagger.Void type in the API.
String return values
Here is an example of a Dagger Function that returns operating system information for the container as a string:
- Go
- Python
- TypeScript
package main
import (
"context"
"main/internal/dagger"
)
type MyModule struct{}
func (m *MyModule) OsInfo(ctx context.Context, ctr *dagger.Container) (string, error) {
return ctr.
WithExec([]string{"uname", "-a"}).
Stdout(ctx)
}
import dagger
from dagger import function, object_type
@object_type
class MyModule:
@function
async def os_info(self, ctr: dagger.Container) -> str:
return await ctr.with_exec(["uname", "-a"]).stdout()
import { Container, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
async osInfo(ctr: Container): Promise<string> {
return ctr.withExec(["uname", "-a"]).stdout()
}
}
Here is an example call for this Dagger Function:
dagger call os-info --ctr=ubuntu:latest
The result will look like this:
Linux buildkitsandbox 6.1.0-22-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.94-1 (2024-06-21) x86_64 x86_64 x86_64 GNU/Linux
Directory return values
Directory return values might be produced by a Dagger Function that:
- Builds language-specific binaries
- Downloads source code from git or another remote source
- Processes source code (for example, generating documentation or linting code)
- Downloads or processes datasets
- Downloads machine learning models
Here is an example of a Go builder Dagger Function that accepts a remote Git address as a Directory
argument, builds a Go binary from the source code in that repository, and returns the build directory containing the compiled binary:
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
func (m *MyModule) GoBuilder(ctx context.Context, src *dagger.Directory, arch string, os string) *dagger.Directory {
return dag.Container().
From("golang:1.21").
WithMountedDirectory("/src", src).
WithWorkdir("/src").
WithEnvVariable("GOARCH", arch).
WithEnvVariable("GOOS", os).
WithEnvVariable("CGO_ENABLED", "0").
WithExec([]string{"go", "build", "-o", "build/"}).
Directory("/src/build")
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def go_builder(self, src: dagger.Directory, arch: str, os: str) -> dagger.Directory:
return (
dag.container()
.from_("golang:1.21")
.with_mounted_directory("/src", src)
.with_workdir("/src")
.with_env_variable("GOARCH", arch)
.with_env_variable("GOOS", os)
.with_env_variable("CGO_ENABLED", "0")
.with_exec(["go", "build", "-o", "build/"])
.directory("/src/build")
)
import { dag, Directory, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
goBuilder(src: Directory, arch: string, os: string): Directory {
return dag
.container()
.from("golang:1.21")
.withMountedDirectory("/src", src)
.withWorkdir("/src")
.withEnvVariable("GOARCH", arch)
.withEnvVariable("GOOS", os)
.withEnvVariable("CGO_ENABLED", "0")
.withExec(["go", "build", "-o", "build/"])
.directory("/src/build")
}
}
Here is an example call for this Dagger Function:
dagger call go-builder --src=https://github.com/golang/example#master:/hello --arch=amd64 --os=linux
Once the command completes, you should see this output:
_type: Directory
entries:
- hello
This means that the build succeeded, and a Directory
type representing the build directory was returned. This Directory
is called a "just-in-time" directory: a dynamically-produced artifact of a Dagger pipeline.
File return values
Similar to just-in-time directories, Dagger Functions can produce just-in-time files by returning the File
type.
Just-in-time files might be produced by a Dagger Function that:
- Builds language-specific binaries
- Combines multiple input files into a single output file, such as a composite video or a compressed archive
Here is an example of a Dagger Function that accepts a filesystem path or remote Git address as a Directory
argument and returns a ZIP archive of that directory:
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
func (m *MyModule) Archiver(ctx context.Context, src *dagger.Directory) *dagger.File {
return dag.Container().
From("alpine:latest").
WithExec([]string{"apk", "add", "zip"}).
WithMountedDirectory("/src", src).
WithWorkdir("/src").
WithExec([]string{"sh", "-c", "zip -p -r out.zip *.*"}).
File("/src/out.zip")
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def archiver(self, src: dagger.Directory) -> dagger.File:
return (
dag.container()
.from_("alpine:latest")
.with_exec(["apk", "add", "zip"])
.with_mounted_directory("/src", src)
.with_workdir("/src")
.with_exec(["sh", "-c", "zip -p -r out.zip *.*"])
.file("/src/out.zip")
)
import { dag, Directory, File, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
archiver(src: Directory): File {
return dag
.container()
.from("alpine:latest")
.withExec(["apk", "add", "zip"])
.withMountedDirectory("/src", src)
.withWorkdir("/src")
.withExec(["sh", "-c", "zip -p -r out.zip *.*"])
.file("/src/out.zip")
}
}
Here is an example call for this Dagger Function:
dagger call archiver --src=https://github.com/dagger/dagger#main:./docs/current_docs/quickstart
Once the command completes, you should see this output:
_type: File
name: out.zip
size: 13744
This means that the build succeeded, and a File
type representing the ZIP archive was returned.
Container return values
Similar to directories and files, just-in-time containers are produced by calling a Dagger Function that returns the Container
type. This type provides a complete API for building, running and distributing containers.
Just-in-time containers might be produced by a Dagger Function that:
- Builds a container
- Minifies a container
- Downloads a container image from a running registry
- Exports a container from Docker or other container runtimes
- Snapshots the state of a running container
You can think of a just-in-time container, and the Container
type that represents it, as a build stage in Dockerfile. Each operation produces a new immutable state, which can be further processed, or exported as an OCI image. Dagger Functions can accept, return and pass containers between themselves, just like regular variables.
Here's an example of a Dagger Function that returns a base alpine
container image with a list of additional specified packages:
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
func (m *MyModule) AlpineBuilder(ctx context.Context, packages []string) *dagger.Container {
ctr := dag.Container().
From("alpine:latest")
for _, pkg := range packages {
ctr = ctr.WithExec([]string{"apk", "add", pkg})
}
return ctr
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def alpine_builder(self, packages: list[str]) -> dagger.Container:
ctr = dag.container().from_("alpine:latest")
for pkg in packages:
ctr = ctr.with_exec(["apk", "add", pkg])
return ctr
import { dag, Container, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
alpineBuilder(packages: string[]): Container {
let ctr = dag.container().from("alpine:latest")
for (const pkg in packages) {
ctr = ctr.withExec(["apk", "add", pkg])
}
return ctr
}
}
Here is an example call for this Dagger Function:
dagger call alpine-builder --packages=curl,openssh
Once the command completes, you should see this output:
_type: Container
defaultArgs:
- /bin/sh
entrypoint: []
mounts: []
platform: linux/amd64
user: ""
workdir: ""
This means that the build succeeded, and a Container
type representing the built container image was returned.
When calling Dagger Functions that produce a just-in-time artifact, you can use the Dagger CLI to add more functions to the pipeline for further processing - for example, inspecting the contents of a directory artifact, exporting a file artifact to the local filesystem, publishing a container artifact to a registry, and so on. This is called "function chaining", and it is one of Dagger's most powerful features.
Chaining
So long as a Dagger Function returns an object that can be JSON-serialized, its state will be preserved and passed to the next function in the chain. This makes it possible to write custom Dagger Functions that support function chaining in the same style as the Dagger API.
Here is an example module with support for function chaining:
- Go
- Python
- TypeScript
// A Dagger module for saying hello world!
package main
import (
"context"
"fmt"
)
type MyModule struct {
Greeting string
Name string
}
func (hello *MyModule) WithGreeting(ctx context.Context, greeting string) (*MyModule, error) {
hello.Greeting = greeting
return hello, nil
}
func (hello *MyModule) WithName(ctx context.Context, name string) (*MyModule, error) {
hello.Name = name
return hello, nil
}
func (hello *MyModule) Message(ctx context.Context) (string, error) {
var (
greeting = hello.Greeting
name = hello.Name
)
if greeting == "" {
greeting = "Hello"
}
if name == "" {
name = "World"
}
return fmt.Sprintf("%s, %s!", greeting, name), nil
}
"""A simple chaining example module."""
from typing import Self
from dagger import function, object_type
@object_type
class MyModule:
"""Functions that chain together."""
greeting: str = "Hello"
name: str = "World"
@function
def with_greeting(self, greeting: str) -> Self:
self.greeting = greeting
return self
@function
def with_name(self, name: str) -> Self:
self.name = name
return self
@function
def message(self) -> str:
return f"{self.greeting}, {self.name}!"
import { object, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
greeting = "Hello"
@func()
name = "World"
@func()
withGreeting(greeting: string): MyModule {
this.greeting = greeting
return this
}
@func()
withName(name: string): MyModule {
this.name = name
return this
}
@func()
message(): string {
return `${this.greeting} ${this.name}`
}
}
And here is an example call for this module:
dagger call with-name --name=Monde with-greeting --greeting=Bonjour message
The result will be:
Bonjour, Monde!