Cookbook
Filesystem
List host directory contents
The following code listing obtains a reference to the host working directory and lists the directory's contents.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"log"
"os"
"dagger.io/dagger"
)
func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
log.Println(err)
return
}
defer client.Close()
entries, err := client.Host().Directory(".").Entries(ctx)
if err != nil {
log.Println(err)
return
}
fmt.Println(entries)
}
import Client, { connect } from "@dagger.io/dagger"
connect(async (client: Client) => {
const entries = await client.host().directory(".").entries()
console.log(entries)
}, {LogOutput: process.stderr})
import sys
import anyio
import dagger
async def main():
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
entries = await client.host().directory(".").entries()
print(entries)
anyio.run(main)
When the Dagger pipeline code is in a sub-directory, it may be more useful to set the parent directory (the project's root directory) as the working directory.
The following listing revises the previous one, obtaining a reference to the parent directory on the host and listing its contents.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"log"
"os"
"dagger.io/dagger"
)
func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr), dagger.WithWorkdir(".."))
if err != nil {
log.Println(err)
return
}
defer client.Close()
entries, err := client.Host().Directory(".").Entries(ctx)
if err != nil {
log.Println(err)
return
}
fmt.Println(entries)
}
import Client, { connect } from "@dagger.io/dagger"
connect(async (client: Client) => {
const entries = await client.host().directory(".").entries()
console.log(entries)
}, {LogOutput: process.stderr, Workdir: ".."})
import sys
import anyio
import dagger
async def main():
async with dagger.Connection(dagger.Config(log_output=sys.stderr, workdir="..")) as client:
entries = await client.host().directory(".").entries()
print(entries)
anyio.run(main)
Mount host directory in container
The following code listing mounts a host directory in a container at the /host
container path and then executes a command in the container referencing the mounted directory.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"log"
"os"
"dagger.io/dagger"
)
func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
log.Println(err)
return
}
defer client.Close()
contents, err := client.Container().
From("alpine:latest").
WithDirectory("/host", client.Host().Directory(".")).
WithExec([]string{"ls", "/host"}).
Stdout(ctx)
if err != nil {
log.Println(err)
return
}
fmt.Println(contents)
}
import Client, { connect } from "@dagger.io/dagger"
connect(async (client: Client) => {
const contents = await client.container()
.from("alpine:latest")
.withDirectory("/host", client.host().directory("."))
.withExec(["ls", "/host"])
.stdout()
console.log(contents)
}, {LogOutput: process.stderr})
import sys
import anyio
import dagger
async def main():
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
out = await (
client.container()
.from_("alpine:latest")
.with_directory("/host", client.host().directory("."))
.with_exec(["ls", "/host"])
.stdout()
)
print(out)
anyio.run(main)
Get host directory with exclusions
The following code listing obtains a reference to the host working directory containing all files except *.txt
files.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"dagger.io/dagger"
)
func main() {
dir := os.TempDir()
os.WriteFile(filepath.Join(dir, "foo.txt"), []byte("1"), 0600)
os.WriteFile(filepath.Join(dir, "bar.txt"), []byte("2"), 0600)
os.WriteFile(filepath.Join(dir, "baz.rar"), []byte("3"), 0600)
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr), dagger.WithWorkdir(dir))
if err != nil {
log.Println(err)
return
}
defer client.Close()
entries, err := client.Host().Directory(".", dagger.HostDirectoryOpts{
Exclude: []string{"*.txt"},
}).Entries(ctx)
if err != nil {
log.Println(err)
return
}
fmt.Println(entries)
}
import Client, { connect } from "@dagger.io/dagger"
import * as os from "os"
import * as path from "path"
import * as fs from "fs"
const dir = os.tmpdir()
const files = ["foo.txt", "bar.txt", "baz.rar"]
let count = 1
for (const file of files) {
fs.writeFileSync(path.join(dir, file), count.toString())
count = count+1
}
connect(async (client: Client) => {
const entries = await client.host().directory(".", {"exclude":["*.txt"]}).entries()
console.log(entries)
}, {LogOutput: process.stderr, Workdir: dir})
import sys
import tempfile
import anyio
import dagger
async def main(workdir: anyio.Path):
for i, file in enumerate(["foo.txt", "bar.txt", "baz.rar"]):
await (workdir / file).write_text(str(i + 1))
cfg = dagger.Config(log_output=sys.stderr, workdir=workdir)
async with dagger.Connection(cfg) as client:
entries = await client.host().directory(".", exclude=["*.txt"]).entries()
print(entries)
with tempfile.TemporaryDirectory() as workdir:
anyio.run(main, anyio.Path(workdir))
Get host directory with inclusions
The following code listing obtains a reference to the host working directory containing only *.rar
files.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"dagger.io/dagger"
)
func main() {
dir := os.TempDir()
os.WriteFile(filepath.Join(dir, "foo.txt"), []byte("1"), 0600)
os.WriteFile(filepath.Join(dir, "bar.txt"), []byte("2"), 0600)
os.WriteFile(filepath.Join(dir, "baz.rar"), []byte("3"), 0600)
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr), dagger.WithWorkdir(dir))
if err != nil {
log.Println(err)
return
}
defer client.Close()
entries, err := client.Host().Directory(".", dagger.HostDirectoryOpts{
Include: []string{"*.rar"},
}).Entries(ctx)
if err != nil {
log.Println(err)
return
}
fmt.Println(entries)
}
import Client, { connect } from "@dagger.io/dagger"
import * as os from "os"
import * as path from "path"
import * as fs from "fs"
const dir = os.tmpdir()
const files = ["foo.txt", "bar.txt", "baz.rar"]
let count = 1
for (const file of files) {
fs.writeFileSync(path.join(dir, file), count.toString())
count = count+1
}
connect(async (client: Client) => {
const entries = await client.host().directory(".", {"include":["*.rar"]}).entries()
console.log(entries)
}, {LogOutput: process.stderr, Workdir: dir})
import sys
import tempfile
import anyio
import dagger
async def main(workdir: anyio.Path):
for i, file in enumerate(["foo.txt", "bar.txt", "baz.rar"]):
await (workdir / file).write_text(str(i + 1))
cfg = dagger.Config(log_output=sys.stderr, workdir=workdir)
async with dagger.Connection(cfg) as client:
entries = await client.host().directory(".", include=["*.rar"]).entries()
print(entries)
with tempfile.TemporaryDirectory() as workdir:
anyio.run(main, anyio.Path(workdir))
Get host directory with exclusions and inclusions
The following code listing obtains a reference to the host working directory containing all files except *.rar
files.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"dagger.io/dagger"
)
func main() {
dir := os.TempDir()
os.WriteFile(filepath.Join(dir, "foo.txt"), []byte("1"), 0600)
os.WriteFile(filepath.Join(dir, "bar.txt"), []byte("2"), 0600)
os.WriteFile(filepath.Join(dir, "baz.rar"), []byte("3"), 0600)
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr), dagger.WithWorkdir(dir))
if err != nil {
log.Println(err)
return
}
defer client.Close()
entries, err := client.Host().Directory(".", dagger.HostDirectoryOpts{
Include: []string{"*.*"},
Exclude: []string{"*.rar"},
}).Entries(ctx)
if err != nil {
log.Println(err)
return
}
fmt.Println(entries)
}
import Client, { connect } from "@dagger.io/dagger"
import * as os from "os"
import * as path from "path"
import * as fs from "fs"
const dir = os.tmpdir()
const files = ["foo.txt", "bar.txt", "baz.rar"]
let count = 1
for (const file of files) {
fs.writeFileSync(path.join(dir, file), count.toString())
count = count+1
}
connect(async (client: Client) => {
const entries = await client.host().directory(".", {"include":["*.*"], "exclude":["*.rar"]}).entries()
console.log(entries)
}, {LogOutput: process.stderr, Workdir: dir})
import sys
import tempfile
import anyio
import dagger
async def main(workdir: anyio.Path):
for i, file in enumerate(["foo.txt", "bar.txt", "baz.rar"]):
await (workdir / file).write_text(str(i + 1))
cfg = dagger.Config(log_output=sys.stderr, workdir=workdir)
async with dagger.Connection(cfg) as client:
entries = await client.host().directory(".", exclude=["*.rar"], include=["*.*"]).entries()
print(entries)
with tempfile.TemporaryDirectory() as workdir:
anyio.run(main, anyio.Path(workdir))
Builds
Perform multi-stage build
The following code listing performs a multi-stage build.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
// create dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// get host directory
project := client.Host().Directory(".")
// build app
builder := client.Container().
From("golang:latest").
WithDirectory("/src", project).
WithWorkdir("/src").
WithEnvVariable("CGO_ENABLED", "0").
WithExec([]string{"go", "build", "-o", "myapp"})
// publish binary on alpine base
prodImage := client.Container().
From("alpine").
WithFile("/bin/myapp", builder.File("/src/myapp")).
WithEntrypoint([]string{"/bin/myapp"})
addr, err := prodImage.Publish(ctx, "localhost:5000/multistage")
if err != nil {
panic(err)
}
fmt.Println(addr)
}
import Client, { connect } from "@dagger.io/dagger"
connect(async (client: Client) => {
// get host directory
const project = client.host().directory(".")
// build app
const builder = client.container()
.from("golang:latest")
.withDirectory("/src", project)
.withWorkdir("/src")
.withEnvVariable("CGO_ENABLED", "0")
.withExec(["go", "build", "-o", "myapp"])
// publish binary on alpine base
const prod = client.container()
.from("alpine")
.withFile("/bin/myapp", builder.file("/src/myapp"))
.withEntrypoint(["/bin/myapp"])
const addr = await prod.publish("localhost:5000/multistage")
console.log(addr)
}, {LogOutput: process.stderr})
import sys
import anyio
import dagger
async def main():
# create Dagger client
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# get host directory
project = client.host().directory(".")
# build app
builder = (
client
.container()
.from_("golang:latest")
.with_directory("/src", project)
.with_workdir("/src")
.with_env_variable("CGO_ENABLED", "0")
.with_exec(["go", "build", "-o", "myapp"])
)
# publish binary on alpine base
prod = (
client
.container()
.from_("alpine")
.with_file("/bin/myapp", builder.file("/src/myapp"))
.with_entrypoint(["/bin/myapp"])
)
addr = await prod.publish("localhost:5000/multistage")
print(addr)
anyio.run(main)
Perform matrix build
The following code listing builds separate images for multiple OS and CPU architecture combinations.
// Create a multi-build pipeline for a Go application.
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
println("Building with Dagger")
// define build matrix
geese := []string{"linux", "darwin"}
goarches := []string{"amd64", "arm64"}
ctx := context.Background()
// initialize dagger client
c, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
// get reference to the local project
src := c.Host().Directory(".")
// create empty directory to put build outputs
outputs := c.Directory()
golang := c.Container().
// get golang image
From("golang:latest").
// mount source code into golang image
WithDirectory("/src", src).
WithWorkdir("/src")
for _, goos := range geese {
for _, goarch := range goarches {
// create a directory for each OS and architecture
path := fmt.Sprintf("build/%s/%s/", goos, goarch)
build := golang.
// set GOARCH and GOOS in the build environment
WithEnvVariable("GOOS", goos).
WithEnvVariable("GOARCH", goarch).
WithExec([]string{"go", "build", "-o", path})
// add build to outputs
outputs = outputs.WithDirectory(path, build.Directory(path))
}
}
// write build artifacts to host
ok, err := outputs.Export(ctx, ".")
if err != nil {
panic(err)
}
if !ok {
panic("did not export files")
}
}
import { connect } from "@dagger.io/dagger"
// Create a multi-build pipeline for a Go application.
// define build matrix
const oses = ["linux", "darwin"]
const arches = ["amd64", "arm64"]
// initialize dagger client
connect(async (client) => {
console.log("Building with Dagger")
// get reference to the local project
const src = client
.host()
.directory(".")
// create empty directory to put build outputs
var outputs = client.directory()
const golang = client.container()
// get golang image
.from("golang:latest")
// mount source code into golang image
.withDirectory("/src", src)
.withWorkdir("/src")
for (const os of oses) {
for (const arch of arches) {
// create a directory for each OS and architecture
const path = `build/${os}/${arch}/`
const build = golang
// set GOARCH and GOOS in the build environment
.withEnvVariable("GOOS", os)
.withEnvVariable("GOARCH", arch)
.withExec(["go", "build", "-o", path])
// add build to outputs
outputs = outputs.withDirectory(path, build.directory(path))
}
}
// write build artifacts to host
await outputs.export(".")
}, {LogOutput: process.stderr});
"""Create a multi-build pipeline for a Go application."""
import itertools
import sys
import anyio
import dagger
async def main():
print("Building with Dagger")
# define build matrix
oses = ["linux", "darwin"]
arches = ["amd64", "arm64"]
# initialize dagger client
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# get reference to the local project
src = client.host().directory(".")
# create empty directory to put build outputs
outputs = client.directory()
golang = (
# get `golang` image
client.container()
.from_("golang:latest")
# mount source code into `golang` image
.with_directory("/src", src)
.with_workdir("/src")
)
for goos, goarch in itertools.product(oses, arches):
# create a directory for each OS and architecture
path = f"build/{goos}/{goarch}/"
build = (
golang
# set GOARCH and GOOS in the build environment
.with_env_variable("GOOS", goos)
.with_env_variable("GOARCH", goarch)
.with_exec(["go", "build", "-o", path])
)
# add build to outputs
outputs = outputs.with_directory(path, build.directory(path))
# write build artifacts to host
await outputs.export(".")
anyio.run(main)
Build multi-arch image
The following code listing builds a single image for different CPU architectures using native emulation.
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
// the platforms to build for and push in a multi-platform image
var platforms = []dagger.Platform{
"linux/amd64", // a.k.a. x86_64
"linux/arm64", // a.k.a. aarch64
"linux/s390x", // a.k.a. IBM S/390
}
// the container registry for the multi-platform image
const imageRepo = "localhost/testrepo:latest"
func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// the git repository containing code for the binary to be built
gitRepo := client.Git("https://github.com/dagger/dagger.git").
Branch("086862926433e19e1f24cd709e6165c36bdb2633").
Tree()
platformVariants := make([]*dagger.Container, 0, len(platforms))
for _, platform := range platforms {
// pull the golang image for this platform
ctr := client.Container(dagger.ContainerOpts{Platform: platform})
ctr = ctr.From("golang:1.20-alpine")
// mount in source code
ctr = ctr.WithDirectory("/src", gitRepo)
// mount in an empty dir where the built binary will live
ctr = ctr.WithDirectory("/output", client.Directory())
// ensure the binary will be statically linked and thus executable
// in the final image
ctr = ctr.WithEnvVariable("CGO_ENABLED", "0")
// build the binary and put the result at the mounted output
// directory
ctr = ctr.WithWorkdir("/src")
ctr = ctr.WithExec([]string{
"go", "build",
"-o", "/output/dagger",
"/src/cmd/dagger",
})
// select the output directory
outputDir := ctr.Directory("/output")
// wrap the output directory in a new empty container marked
// with the same platform
binaryCtr := client.
Container(dagger.ContainerOpts{Platform: platform}).
WithRootfs(outputDir)
platformVariants = append(platformVariants, binaryCtr)
}
// publishing the final image uses the same API as single-platform
// images, but now additionally specify the `PlatformVariants`
// option with the containers built before.
imageDigest, err := client.
Container().
Publish(ctx, imageRepo, dagger.ContainerPublishOpts{
PlatformVariants: platformVariants,
})
if err != nil {
panic(err)
}
fmt.Println("Pushed multi-platform image w/ digest: ", imageDigest)
}
Build multi-arch image with cross-compilation
The following code listing builds a single image for different CPU architectures using cross-compilation.
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
platformFormat "github.com/containerd/containerd/platforms"
)
var platforms = []dagger.Platform{
"linux/amd64", // a.k.a. x86_64
"linux/arm64", // a.k.a. aarch64
"linux/s390x", // a.k.a. IBM S/390
}
// the container registry for the multi-platform image
const imageRepo = "localhost/testrepo:latest"
// util that returns the architecture of the provided platform
func architectureOf(platform dagger.Platform) string {
return platformFormat.MustParse(string(platform)).Architecture
}
func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
gitRepo := client.Git("https://github.com/dagger/dagger.git").
Branch("086862926433e19e1f24cd709e6165c36bdb2633").
Tree()
platformVariants := make([]*dagger.Container, 0, len(platforms))
for _, platform := range platforms {
// pull the golang image for the *host platform*. This is
// accomplished by just not specifying a platform; the default
// is that of the host.
ctr := client.Container()
ctr = ctr.From("golang:1.20-alpine")
// mount in our source code
ctr = ctr.WithDirectory("/src", gitRepo)
// mount in an empty dir to put the built binary
ctr = ctr.WithDirectory("/output", client.Directory())
// ensure the binary will be statically linked and thus executable
// in the final image
ctr = ctr.WithEnvVariable("CGO_ENABLED", "0")
// configure the go compiler to use cross-compilation targeting the
// desired platform
ctr = ctr.WithEnvVariable("GOOS", "linux")
ctr = ctr.WithEnvVariable("GOARCH", architectureOf(platform))
// build the binary and put the result at the mounted output
// directory
ctr = ctr.WithWorkdir("/src")
ctr = ctr.WithExec([]string{
"go", "build",
"-o", "/output/dagger",
"/src/cmd/dagger",
})
// select the output directory
outputDir := ctr.Directory("/output")
// wrap the output directory in a new empty container marked
// with the platform
binaryCtr := client.
Container(dagger.ContainerOpts{Platform: platform}).
WithRootfs(outputDir)
platformVariants = append(platformVariants, binaryCtr)
}
// publishing the final image uses the same API as single-platform
// images, but now additionally specify the `PlatformVariants`
// option with the containers built before.
imageDigest, err := client.
Container().
Publish(ctx, imageRepo, dagger.ContainerPublishOpts{
PlatformVariants: platformVariants,
})
if err != nil {
panic(err)
}
fmt.Println("published multi-platform image with digest", imageDigest)
}
Build image from Dockerfile
The following code listing builds an image using an existing Dockerfile.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"math"
"math/rand"
"os"
"dagger.io/dagger"
)
func main() {
ctx := context.Background()
// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
contextDir := client.Host().Directory(".")
ref, err := contextDir.
DockerBuild().
Publish(ctx, fmt.Sprintf("ttl.sh/hello-dagger-%.0f", math.Floor(rand.Float64()*10000000))) //#nosec
if err != nil {
panic(err)
}
fmt.Printf("Published image to :%s\n", ref)
}
import { connect } from "@dagger.io/dagger"
connect(async (client) => {
// set build context
const contextDir = client.host().directory(".")
// build using Dockerfile
// publish the resulting container to a registry
const imageRef = await contextDir
.dockerBuild()
.publish('ttl.sh/hello-dagger-' + Math.floor(Math.random() * 10000000))
console.log(`Published image to: ${imageRef}`)
}, { LogOutput: process.stderr })
import random
import sys
import anyio
import dagger
async def main():
config = dagger.Config(log_output=sys.stdout)
async with dagger.Connection(config) as client:
# set build context
context_dir = client.host().directory(".")
# build using Dockerfile
# publish the resulting container to a registry
image_ref = (
await context_dir.docker_build()
.publish(f"ttl.sh/hello-dagger-{random.randint(0, 10000000)}")
)
print(f"Published image to: {image_ref}")
anyio.run(main)
Add OCI annotations to image
The following code listing adds OpenContainer Initiative (OCI) annotations to an image.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
"time"
"dagger.io/dagger"
)
func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// create and publish image with annotations
ctr := client.Container().
From("alpine").
WithLabel("org.opencontainers.image.title", "my-alpine").
WithLabel("org.opencontainers.image.version", "1.0").
WithLabel("org.opencontainers.image.created", time.Now().String()).
WithLabel("org.opencontainers.image.source", "https://github.com/alpinelinux/docker-alpine").
WithLabel("org.opencontainers.image.licenses", "MIT")
addr, err := ctr.Publish(ctx, "ttl.sh/my-alpine")
if err != nil {
panic(err)
}
fmt.Println(addr)
}
import { connect } from "@dagger.io/dagger"
// create Dagger client
connect(async (client) => {
// create and publish image with annotations
const container = client.container().
from("alpine").
withLabel("org.opencontainers.image.title", "my-alpine").
withLabel("org.opencontainers.image.version", "1.0").
withLabel("org.opencontainers.image.created", new Date()).
WithLabel("org.opencontainers.image.source", "https://github.com/alpinelinux/docker-alpine").
WithLabel("org.opencontainers.image.licenses", "MIT")
const addr = await container.publish("ttl.sh/my-alpine")
console.log(addr)
}, {LogOutput: process.stderr})
import sys
import anyio
import datetime
import dagger
async def main():
# create Dagger client
async with dagger.Connection(dagger.Config(log_output=sys.stderr, workdir=".")) as client:
# publish app on alpine base
container = (
client
.container()
.from_("alpine")
.with_label("org.opencontainers.image.title", "my-alpine")
.with_label("org.opencontainers.image.version", "1.0")
.with_label("org.opencontainers.image.created", datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f%z"))
.with_label("org.opencontainers.image.source", "https://github.com/alpinelinux/docker-alpine")
.with_label("org.opencontainers.image.licenses", "MIT")
)
addr = await container.publish("ttl.sh/my-alpine")
print(addr)
anyio.run(main)
Define build-time variables
The following code listing defines various environment variables for build purposes.
// Create a multi-build pipeline for a Go application.
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
println("Building with Dagger")
// define build matrix
geese := []string{"linux", "darwin"}
goarches := []string{"amd64", "arm64"}
ctx := context.Background()
// initialize dagger client
c, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
// get reference to the local project
src := c.Host().Directory(".")
// create empty directory to put build outputs
outputs := c.Directory()
golang := c.Container().
// get golang image
From("golang:latest").
// mount source code into golang image
WithDirectory("/src", src).
WithWorkdir("/src")
for _, goos := range geese {
for _, goarch := range goarches {
// create a directory for each OS and architecture
path := fmt.Sprintf("build/%s/%s/", goos, goarch)
build := golang.
// set GOARCH and GOOS in the build environment
WithEnvVariable("GOOS", goos).
WithEnvVariable("GOARCH", goarch).
WithExec([]string{"go", "build", "-o", path})
// add build to outputs
outputs = outputs.WithDirectory(path, build.Directory(path))
}
}
// write build artifacts to host
ok, err := outputs.Export(ctx, ".")
if err != nil {
panic(err)
}
if !ok {
panic("did not export files")
}
}
import { connect } from "@dagger.io/dagger"
// Create a multi-build pipeline for a Go application.
// define build matrix
const oses = ["linux", "darwin"]
const arches = ["amd64", "arm64"]
// initialize dagger client
connect(async (client) => {
console.log("Building with Dagger")
// get reference to the local project
const src = client
.host()
.directory(".")
// create empty directory to put build outputs
var outputs = client.directory()
const golang = client.container()
// get golang image
.from("golang:latest")
// mount source code into golang image
.withDirectory("/src", src)
.withWorkdir("/src")
for (const os of oses) {
for (const arch of arches) {
// create a directory for each OS and architecture
const path = `build/${os}/${arch}/`
const build = golang
// set GOARCH and GOOS in the build environment
.withEnvVariable("GOOS", os)
.withEnvVariable("GOARCH", arch)
.withExec(["go", "build", "-o", path])
// add build to outputs
outputs = outputs.withDirectory(path, build.directory(path))
}
}
// write build artifacts to host
await outputs.export(".")
}, {LogOutput: process.stderr});
"""Create a multi-build pipeline for a Go application."""
import itertools
import sys
import anyio
import dagger
async def main():
print("Building with Dagger")
# define build matrix
oses = ["linux", "darwin"]
arches = ["amd64", "arm64"]
# initialize dagger client
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# get reference to the local project
src = client.host().directory(".")
# create empty directory to put build outputs
outputs = client.directory()
golang = (
# get `golang` image
client.container()
.from_("golang:latest")
# mount source code into `golang` image
.with_directory("/src", src)
.with_workdir("/src")
)
for goos, goarch in itertools.product(oses, arches):
# create a directory for each OS and architecture
path = f"build/{goos}/{goarch}/"
build = (
golang
# set GOARCH and GOOS in the build environment
.with_env_variable("GOOS", goos)
.with_env_variable("GOARCH", goarch)
.with_exec(["go", "build", "-o", path])
)
# add build to outputs
outputs = outputs.with_directory(path, build.directory(path))
# write build artifacts to host
await outputs.export(".")
anyio.run(main)
Access private Git repository
The following code listing demonstrates how to access a private Git repository using SSH.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx)
if err != nil {
panic(err)
}
defer client.Close()
// Retrieve path of authentication agent socket from host
sshAgentPath := os.Getenv("SSH_AUTH_SOCK")
// Private repository with a README.md file at the root.
readme, err := client.
Git("git@private-repository.git").
Branch("main").
Tree(
dagger.GitRefTreeOpts{
SSHAuthSocket: client.Host().UnixSocket(sshAgentPath),
},
).
File("README.md").
Contents(ctx)
if err != nil {
panic(err)
}
fmt.Println("readme", readme)
}
import Client, { connect } from "@dagger.io/dagger"
import process from "process"
// initialize Dagger client
connect(async (client: Client) => {
// Collect value of SSH_AUTH_SOCK env var, to retrieve authentication socket path
const sshAuthSockPath = process.env.SSH_AUTH_SOCK?.toString() || ""
// Retrieve authentication socket ID from host
const sshAgentSocketID = await client.host().unixSocket(sshAuthSockPath).id()
const repo = client
// Retrieve the repository
.git("git@private-repository.git")
// Select the main branch, and the filesystem tree associated
.branch("main")
.tree({
sshAuthSocket: sshAgentSocketID,
})
// Select the README.md file
.file("README.md")
// Retrieve the content of the README file
const file = await repo.contents()
console.log(file)
})
"""Clone a Private Git Repository and print the content of the README.md file."""
import os
import sys
import anyio
import dagger
async def main():
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# Collect value of SSH_AUTH_SOCK env var, to retrieve auth socket path
ssh_auth_path = os.environ.get("SSH_AUTH_SOCK", "")
# Retrieve authentication socket from host
ssh_agent_socket = client.host().unix_socket(ssh_auth_path)
repo = (
client
# Retrieve the repository
.git("git@private-repository.git")
# Select the main branch, and the filesystem tree associated
.branch("main").tree(ssh_auth_socket=ssh_agent_socket)
# Select the README.md file
.file("README.md")
)
# Retrieve the content of the README file
file = await repo.contents()
print(file)
anyio.run(main)
Use transient database for application tests
The following code listing creates a temporary MariaDB database service and binds it to an application container for unit/integration testing.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
ctx := context.Background()
// create Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// get MariaDB base image
mariadb := client.Container().
From("mariadb:10.11.2").
WithEnvVariable("MARIADB_USER", "user").
WithEnvVariable("MARIADB_PASSWORD", "password").
WithEnvVariable("MARIADB_DATABASE", "drupal").
WithEnvVariable("MARIADB_ROOT_PASSWORD", "root").
WithExposedPort(3306)
// get Drupal base image
// install additional dependencies
drupal := client.Container().
From("drupal:10.0.7-php8.2-fpm").
WithExec([]string{"composer", "require", "drupal/core-dev", "--dev", "--update-with-all-dependencies"})
// add service binding for MariaDB
// run kernel tests using PHPUnit
test, err := drupal.
WithServiceBinding("db", mariadb).
WithEnvVariable("SIMPLETEST_DB", "mysql://user:password@db/drupal").
WithEnvVariable("SYMFONY_DEPRECATIONS_HELPER", "disabled").
WithWorkdir("/opt/drupal/web/core").
WithExec([]string{"../../vendor/bin/phpunit", "-v", "--group", "KernelTests"}).
Stdout(ctx)
if err != nil {
panic(err)
}
fmt.Println(test)
}
import Client, { connect } from '@dagger.io/dagger';
connect(
async (client: Client) => {
// get MariaDB base image
const mariadb = client
.container()
.from("mariadb:10.11.2")
.withEnvVariable("MARIADB_USER", "user")
.withEnvVariable("MARIADB_PASSWORD", "password")
.withEnvVariable("MARIADB_DATABASE", "drupal")
.withEnvVariable("MARIADB_ROOT_PASSWORD", "root")
.withExposedPort(3306);
// get Drupal base image
// install additional dependencies
const drupal = client
.container()
.from("drupal:10.0.7-php8.2-fpm")
.withExec(["composer", "require", "drupal/core-dev", "--dev", "--update-with-all-dependencies"]);
// add service binding for MariaDB
// run unit tests using PHPUnit
const test = await drupal
.withServiceBinding("db", mariadb)
.withEnvVariable("SIMPLETEST_DB", "mysql://user:password@db/drupal")
.withEnvVariable("SYMFONY_DEPRECATIONS_HELPER", "disabled")
.withWorkdir("/opt/drupal/web/core")
.withExec(["../../vendor/bin/phpunit", "-v", "--group", "KernelTests"])
.stdout();
// print ref
console.log(test);
}, { LogOutput: process.stderr }
);
import sys
import anyio
import dagger
async def main():
# create Dagger client
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# get MariaDB base image
mariadb = (
client
.container()
.from_("mariadb:10.11.2")
.with_env_variable("MARIADB_USER", "user")
.with_env_variable("MARIADB_PASSWORD", "password")
.with_env_variable("MARIADB_DATABASE", "drupal")
.with_env_variable("MARIADB_ROOT_PASSWORD", "root")
.with_exposed_port(3306)
)
# get Drupal base image
# install additional dependencies
drupal = (
client
.container()
.from_("drupal:10.0.7-php8.2-fpm")
.with_exec(["composer", "require", "drupal/core-dev", "--dev", "--update-with-all-dependencies"])
)
# add service binding for MariaDB
# run unit tests using PHPUnit
test = await (
drupal
.with_service_binding("db", mariadb)
.with_env_variable("SIMPLETEST_DB", "mysql://user:password@db/drupal")
.with_env_variable("SYMFONY_DEPRECATIONS_HELPER", "disabled")
.with_workdir("/opt/drupal/web/core")
.with_exec(["../../vendor/bin/phpunit", "-v", "--group", "KernelTests"])
.stdout()
)
# print ref
print(test)
anyio.run(main)
Invalidate cache
The following code listing demonstrates how to invalidate the Dagger cache and thereby force execution of subsequent pipeline steps, by introducing a volatile time variable at a specific point in the Dagger pipeline.
This is a temporary workaround until cache invalidation support is officially added to Dagger.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
"time"
"dagger.io/dagger"
)
func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// invalidate cache to force execution
// of second WithExec() operation
output, err := client.Pipeline("test").
Container().
From("alpine").
WithExec([]string{"apk", "add", "curl"}).
WithEnvVariable("CACHEBUSTER", time.Now().String()).
WithExec([]string{"apk", "add", "zip"}).
Stdout(ctx)
if err != nil {
panic(err)
}
fmt.Println(output)
}
import { connect } from "@dagger.io/dagger"
// create Dagger client
connect(async (client) => {
// invalidate cache to force execution
// of second withExec() operation
const output = await client.pipeline("test").
container().
from("alpine").
withExec(["apk", "add", "curl"]).
withEnvVariable("CACHEBUSTER", Date.now().toString()).
withExec(["apk", "add", "zip"]).
stdout()
console.log(output)
}, {LogOutput: process.stderr})
import sys
import anyio
from datetime import datetime
import dagger
async def main():
# create Dagger client
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# invalidate cache to force execution
# of second with_exec() operation
output = await (
client.pipeline("test").
container().
from_("alpine").
with_exec(["apk", "add", "curl"]).
with_env_variable("CACHEBUSTER", str(datetime.now())).
with_exec(["apk", "add", "zip"]).
stdout()
)
print(output)
anyio.run(main)
Outputs
Publish image to registry
The following code listing publishes a container image to a remote registry (Docker Hub). Replace the DOCKER-HUB-USERNAME
and DOCKER-HUB-PASSWORD
placeholders with your Docker Hub username and password respectively.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
// initialize Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// set secret as string value
secret := client.SetSecret("password", "DOCKER-HUB-PASSWORD")
// create container
c := client.Container(dagger.ContainerOpts{Platform: "linux/amd64"}).
From("nginx:1.23-alpine").
WithNewFile("/usr/share/nginx/html/index.html", dagger.ContainerWithNewFileOpts{
Contents: "Hello from Dagger!",
Permissions: 0o400,
})
// use secret for registry authentication
addr, err := c.
WithRegistryAuth("docker.io", "DOCKER-HUB-USERNAME", secret).
Publish(ctx, "DOCKER-HUB-USERNAME/my-nginx")
if err != nil {
panic(err)
}
// print result
fmt.Println("Published at:", addr)
}
import { connect } from "@dagger.io/dagger"
// initialize Dagger client
connect(async (client) => {
// set secret as string value
const secret = client.setSecret("password", "DOCKER-HUB-PASSWORD")
// create container
const c = client
.container()
.from("nginx:1.23-alpine")
.withNewFile("/usr/share/nginx/html/index.html", {
contents: "Hello from Dagger!",
permissions: 0o400,
})
// use secret for registry authentication
const addr = await c.
withRegistryAuth("docker.io", "DOCKER-HUB-USERNAME", secret).
publish("DOCKER-HUB-USERNAME/my-nginx")
// print result
console.log(`Published at: ${addr}`)
}, {LogOutput: process.stderr})
import sys
import anyio
import dagger
async def main():
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# set secret as string value
secret = client.set_secret("password", "DOCKER-HUB-PASSWORD")
# create container
ctr = (
client.container(platform=dagger.Platform("linux/amd64"))
.from_("nginx:1.23-alpine")
.with_new_file("/usr/share/nginx/html/index.html", "Hello from Dagger!", 0o400)
)
# use secret for registry authentication
addr = await (
ctr
.with_registry_auth("docker.io", "DOCKER-HUB-USERNAME", secret)
.publish("DOCKER-HUB-USERNAME/my-nginx")
)
# print result
print(f"Published at: {addr}")
anyio.run(main)
Export image to host
The following code listing exports a container image from a Dagger pipeline to the host.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
// initialize Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// use NGINX container
// add new webserver index page
c := client.Container(dagger.ContainerOpts{Platform: "linux/amd64"}).
From("nginx:1.23-alpine").
WithNewFile("/usr/share/nginx/html/index.html", dagger.ContainerWithNewFileOpts{
Contents: "Hello from Dagger!",
Permissions: 0o400,
})
// export to host filesystem
val, err := c.Export(ctx, "/tmp/my-nginx.tar")
if err != nil {
panic(err)
}
// print result
fmt.Println("Exported image: ", val)
}
import { connect } from "@dagger.io/dagger"
// initialize Dagger client
connect(async (client) => {
// use NGINX container
// add new webserver index page
const ctr = client
.container({ platform: "linux/amd64" })
.from("nginx:1.23-alpine")
.withNewFile("/usr/share/nginx/html/index.html", {
contents: "Hello from Dagger!",
permissions: 0o400,
})
// export to host filesystem
const result = await ctr
.export("/tmp/my-nginx.tar")
// print result
console.log(`Exported image: ${result}`)
}, {LogOutput: process.stderr})
import sys
import anyio
import dagger
async def main():
# initialize Dagger client
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# use NGINX container
# add new webserver index page
ctr = (
client
.container(platform=dagger.Platform("linux/amd64"))
.from_("nginx:1.23-alpine")
.with_new_file("/usr/share/nginx/html/index.html", "Hello from Dagger!", 0o400)
)
# export to host filesystem
val = await ctr.export("/tmp/my-nginx.tar")
# print result
print(f"Exported image: {val}")
anyio.run(main)
Export container directory to host
The following code listing exports the contents of a container directory to the host's temporary directory.
- Go
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"dagger.io/dagger"
)
func main() {
hostdir := os.TempDir()
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
log.Println(err)
return
}
defer client.Close()
_, err = client.Container().From("alpine:latest").
WithWorkdir("/tmp").
WithExec([]string{"wget", "https://dagger.io"}).
Directory(".").
Export(ctx, hostdir)
if err != nil {
log.Println(err)
return
}
contents, err := os.ReadFile(filepath.Join(hostdir, "index.html"))
if err != nil {
log.Println(err)
return
}
fmt.Println(string(contents))
}
Publish image to registry with multiple tags
The following code listing tags a container image multiple times and publishes it to a remote registry (Docker Hub). Set the Docker Hub username and password as host environment variables named DOCKERHUB_USERNAME
and DOCKERHUB_PASSWORD
respectively.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
// create Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// load registry credentials from environment variables
username := os.Getenv("DOCKERHUB_USERNAME")
if username == "" {
panic("DOCKERHUB_USERNAME env var must be set")
}
passwordPlaintext := os.Getenv("DOCKERHUB_PASSWORD")
if passwordPlaintext == "" {
panic("DOCKERHUB_PASSWORD env var must be set")
}
password := client.SetSecret("password", passwordPlaintext)
// define multiple image tags
tags := [4]string{"latest", "1.0-alpine", "1.0", "1.0.0"}
// create and publish image with multiple tags
ctr := client.Container().
From("alpine").
WithRegistryAuth("docker.io", username, password)
for _, tag := range tags {
addr, err := ctr.Publish(ctx, fmt.Sprintf("%s/alpine:%s", username, tag))
if err != nil {
panic(err)
}
fmt.Println("Published: ", addr)
}
}
import { connect } from "@dagger.io/dagger"
// create Dagger client
connect(async (client) => {
// define tags
const tags = ["latest", "1.0-alpine", "1.0", "1.0.0"]
if(!process.env.DOCKERHUB_USERNAME) {
console.log('DOCKERHUB_USERNAME environment variable must be set');
process.exit();
}
if(!process.env.DOCKERHUB_PASSWORD) {
console.log('DOCKERHUB_PASSWORD environment variable must be set');
process.exit();
}
const username = process.env.DOCKERHUB_USERNAME;
const password = process.env.DOCKERHUB_PASSWORD;
// set secret as string value
const secret = client.setSecret("password", password);
// create and publish image with multiple tags
const container = client.container().
from("alpine")
for (var tag in tags) {
let addr = await container.
withRegistryAuth("docker.io", username, secret).
publish(`${username}/my-alpine:${tags[tag]}`)
console.log(`Published at: ${addr}`)
}
}, {LogOutput: process.stderr})
import sys
import anyio
import datetime
import os
import dagger
async def main():
# define tags
tags = ["latest", "1.0-alpine", "1.0", "1.0.0"]
if "DOCKERHUB_USERNAME" not in os.environ:
raise EnvironmentError("DOCKERHUB_USERNAME environment variable must be set")
if "DOCKERHUB_PASSWORD" not in os.environ:
raise EnvironmentError("DOCKERHUB_PASSWORD environment variable must be set")
username = os.environ.get("DOCKERHUB_USERNAME")
password = os.environ.get("DOCKERHUB_PASSWORD")
# create Dagger client
async with dagger.Connection(dagger.Config(log_output=sys.stderr, workdir=".")) as client:
# set secret as string value
secret = client.set_secret("password", password)
# create and publish image with multiple tags
container = (
client
.container()
.from_("alpine")
)
for tag in tags:
addr = await (
container
.with_registry_auth("docker.io", username, secret)
.publish(f"{username}/my-alpine:{tag}")
)
print(f"Published at: {addr}")
anyio.run(main)
Secrets
Expose secret via environment variable
The following code listing demonstrates how to inject an environment variable in a container as a secret.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
// initialize Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// read secret from host variable
secret := client.SetSecret("gh-secret", os.Getenv("GH_SECRET"))
// use secret in container environment
out, err := client.
Container().
From("alpine:3.17").
WithSecretVariable("GITHUB_API_TOKEN", secret).
WithExec([]string{"apk", "add", "curl"}).
WithExec([]string{"sh", "-c", `curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN"`}).
Stdout(ctx)
if err != nil {
panic(err)
}
fmt.Println(out)
}
import { connect } from "@dagger.io/dagger"
// initialize Dagger client
connect(async (client) => {
// read secret from host variable
const secret = client.setSecret("gh-secret", process.env["GH_SECRET"])
// use secret in container environment
const out = await client
.container()
.from("alpine:3.17")
.withSecretVariable("GITHUB_API_TOKEN", secret)
.withExec(["apk", "add", "curl"])
.withExec(["sh", "-c", `curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN"`])
.stdout()
// print result
console.log(out)
}, {LogOutput: process.stderr})
import sys
import anyio
import dagger
async def main():
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# read secret from host variable
secret = client.set_secret("gh-secret", os.environ["GH_SECRET"])
# use secret in container environment
out = await (
client.container(platform=dagger.Platform("linux/amd64"))
.from_("alpine:3.17")
.with_secret_variable("GITHUB_API_TOKEN", secret)
.with_exec(["apk", "add", "curl"])
.with_exec(["sh", "-c", """curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN" """])
.stdout()
)
# print result
print(out)
anyio.run(main)
Expose secret via file
The following code listing demonstrates how to inject a file in a container as a secret.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
)
func main() {
// initialize Dagger client
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// read file
config, err := os.ReadFile("/home/USER/.config/gh/hosts.yml")
if err != nil {
panic(err)
}
// set secret to file contents
secret := client.SetSecret("ghConfig", string(config))
// mount secret as file in container
out, err := client.
Container().
From("alpine:3.17").
WithExec([]string{"apk", "add", "github-cli"}).
WithMountedSecret("/root/.config/gh/hosts.yml", secret).
WithWorkdir("/root").
WithExec([]string{"gh", "auth", "status"}).
Stdout(ctx)
if err != nil {
panic(err)
}
fmt.Println(out)
}
import { connect } from "@dagger.io/dagger"
import { readFile } from "fs/promises"
// initialize Dagger client
connect(async (client) => {
// read file
const config = await readFile("/home/USER/.config/gh/hosts.yml")
// set secret to file contents
const secret = client.setSecret("ghConfig", config.toString())
// mount secret as file in container
const out = await client
.container()
.from("alpine:3.17")
.withExec(["apk", "add", "github-cli"])
.withMountedSecret("/root/.config/gh/hosts.yml", secret)
.withWorkdir("/root")
.withExec(["gh", "auth", "status"])
.stdout()
// print result
console.log(out)
}, {LogOutput: process.stderr})
import sys
import anyio
import dagger
async def main():
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# read file
config = await anyio.Path("/home/USER/.config/gh/hosts.yml").read_text()
# set secret to file contents
secret = client.set_secret("ghConfig", config)
# mount secret as file in container
out = await (
client.container(platform=dagger.Platform("linux/amd64"))
.from_("alpine:3.17")
.with_exec(["apk", "add", "github-cli"])
.with_mounted_secret("/root/.config/gh/hosts.yml", secret)
.with_workdir("/root")
.with_exec(["gh", "auth", "status"])
.stdout()
)
# print result
print(out)
anyio.run(main)
Load secret from Google Cloud Secret Manager
The following code listing reads a secret (a GitHub API token) from Google Cloud Secret Manager and uses it in a Dagger pipeline to interact with the GitHub API.
Set up Application Default Credentials (ADC) and replace the PROJECT-ID
and SECRET-ID
placeholders with your Google Cloud project and secret identifiers respectively.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"dagger.io/dagger"
)
func main() {
ctx := context.Background()
// get secret from Google Cloud Secret Manager
secretPlaintext, err := gcpGetSecretPlaintext(ctx, "PROJECT-ID", "SECRET-ID")
if err != nil {
panic(err)
}
// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// load secret into Dagger
secret := client.SetSecret("ghApiToken", string(secretPlaintext))
// use secret in container environment
out, err := client.
Container().
From("alpine:3.17").
WithSecretVariable("GITHUB_API_TOKEN", secret).
WithExec([]string{"apk", "add", "curl"}).
WithExec([]string{"sh", "-c", `curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN"`}).
Stdout(ctx)
if err != nil {
panic(err)
}
// print result
fmt.Println(out)
}
func gcpGetSecretPlaintext(ctx context.Context, projectID, secretID string) (string, error) {
secretUri := fmt.Sprintf("projects/%s/secrets/%s/versions/latest", projectID, secretID)
// initialize Google Cloud API client
gcpClient, err := secretmanager.NewClient(ctx)
if err != nil {
panic(err)
}
defer gcpClient.Close()
// retrieve secret
secReq := &secretmanagerpb.AccessSecretVersionRequest{
Name: secretUri,
}
res, err := gcpClient.AccessSecretVersion(ctx, secReq)
if err != nil {
panic(err)
}
secretPlaintext := res.Payload.Data
return string(secretPlaintext), nil
}
import { connect } from "@dagger.io/dagger"
import { SecretManagerServiceClient } from "@google-cloud/secret-manager"
// initialize Dagger client
connect(async (client) => {
// get secret from Google Cloud Secret Manager
const secretPlaintext = await gcpGetSecretPlaintext("PROJECT-ID", "SECRET-ID")
// load secret into Dagger
const secret = client.setSecret("ghApiToken", secretPlaintext)
// use secret in container environment
const out = await client
.container()
.from("alpine:3.17")
.withSecretVariable("GITHUB_API_TOKEN", secret)
.withExec(["apk", "add", "curl"])
.withExec(["sh", "-c", `curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN"`])
.stdout()
// print result
console.log(out)
}, {LogOutput: process.stderr})
async function gcpGetSecretPlaintext(projectID, secretID) {
// initialize Google Cloud API client
const client = new SecretManagerServiceClient();
const secretUri = `projects/${projectID}/secrets/${secretID}/versions/latest`
// retrieve secret
const [accessResponse] = await client.accessSecretVersion({
name: secretUri,
});
const secretPlaintext = accessResponse.payload.data.toString('utf8');
return secretPlaintext;
}
import sys
import anyio
import dagger
from google.cloud import secretmanager
async def main():
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# get secret from Google Cloud Secret Manager
secretPlaintext = await gcp_get_secret_plaintext("PROJECT-ID", "SECRET-ID")
# read secret from host variable
secret = client.set_secret("ghApiToken", secretPlaintext)
# use secret in container environment
out = await (
client.container(platform=dagger.Platform("linux/amd64"))
.from_("alpine:3.17")
.with_secret_variable("GITHUB_API_TOKEN", secret)
.with_exec(["apk", "add", "curl"])
.with_exec(["sh", "-c", """curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN" """])
.stdout()
)
# print result
print(out)
async def gcp_get_secret_plaintext(project_id, secret_id):
secret_uri = f"projects/{project_id}/secrets/{secret_id}/versions/latest"
# initialize Google Cloud API client
client = secretmanager.SecretManagerServiceAsyncClient()
# retrieve secret
response = await client.access_secret_version(request={"name": secret_uri})
secret_plaintext = response.payload.data.decode("UTF-8")
return secret_plaintext
anyio.run(main)
Load secret from Hashicorp Vault
The following code listing reads a secret (a GitHub API token) from a Hashicorp Vault Key/Value v2 engine and uses it in a Dagger pipeline to interact with the GitHub API.
Set the Hashicorp Vault URI, namespace, role and secret identifiers as host environment variables named VAULT_ADDRESS
, VAULT_NAMESPACE
, VAULT_ROLE_ID
and VAULT_SECRET_ID
respectively. Replace the MOUNT-PATH
, SECRET-ID
and SECRET-KEY
placeholders with your Hashicorp Vault mount point, secret identifier and key respectively.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"os"
"dagger.io/dagger"
"github.com/hashicorp/vault-client-go"
"github.com/hashicorp/vault-client-go/schema"
)
func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
if err != nil {
panic(err)
}
defer client.Close()
// get secret from Vault
secretPlaintext, err := getVaultSecret("MOUNT-PATH", "SECRET-ID", "SECRET-KEY")
if err != nil {
panic(err)
}
// load secret into Dagger
secret := client.SetSecret("ghApiToken", secretPlaintext)
// use secret in container environment
out, err := client.Container().
From("alpine:3.17").
WithSecretVariable("GITHUB_API_TOKEN", secret).
WithExec([]string{"apk", "add", "curl"}).
WithExec([]string{"sh", "-c", "curl \"https://api.github.com/repos/dagger/dagger/issues\" --header \"Accept: application/vnd.github+json\" --header \"Authorization: Bearer $GITHUB_API_TOKEN\""}).
Stdout(ctx)
// print result
fmt.Println(out)
}
func getVaultSecret(mountPath, secretID, secretKey string) (string, error) {
ctx := context.Background()
// check for required variables in host environment
address := os.Getenv("VAULT_ADDRESS")
role_id := os.Getenv("VAULT_ROLE_ID")
secret_id := os.Getenv("VAULT_SECRET_ID")
// create Vault client
client, err := vault.New(
vault.WithAddress(address),
)
if err != nil {
return "", err
}
// log in to Vault
resp, err := client.Auth.AppRoleLogin(
ctx,
schema.AppRoleLoginRequest{
RoleId: role_id,
SecretId: secret_id,
},
vault.WithMountPath(mountPath),
)
if err != nil {
return "", err
}
if err := client.SetToken(resp.Auth.ClientToken); err != nil {
return "", err
}
// read and return secret
secret, err := client.Secrets.KvV2Read(
ctx,
secretID,
vault.WithMountPath(mountPath),
)
if err != nil {
return "", err
}
return fmt.Sprintf("%s", secret.Data.Data[secretKey]), nil
}
import { connect } from "@dagger.io/dagger"
import fetch from "node-fetch"
// initialize Dagger client
connect(async (client) => {
// get secret from Vault
const secretPlaintext = await getVaultSecret("MOUNT-PATH", "SECRET-ID", "SECRET-KEY")
// load secret into Dagger
const secret = client.setSecret("ghApiToken", secretPlaintext)
// use secret in container environment
const out = await client
.container()
.from("alpine:3.17")
.withSecretVariable("GITHUB_API_TOKEN", secret)
.withExec(["apk", "add", "curl"])
.withExec(["sh", "-c", `curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN"`])
.stdout()
// print result
console.log(out)
}, {LogOutput: process.stderr})
async function getVaultSecret(mountPath, secretID, secretKey) {
// check for required variables in host environment
const vars = ["VAULT_ADDRESS", "VAULT_NAMESPACE", "VAULT_ROLE_ID", "VAULT_SECRET_ID"];
vars.forEach(v => {
if(!process.env[v]) {
console.log(`${v} variable must be set`);
process.exit();
}
});
const address = process.env.VAULT_ADDRESS;
const namespace = process.env.VAULT_NAMESPACE;
const role = process.env.VAULT_ROLE_ID;
const secret = process.env.VAULT_SECRET_ID;
// request client token
let url = `${address}/v1/auth/approle/login`;
let body = {"role_id": role, "secret_id": secret};
let options = {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-Vault-Namespace': `${namespace}`
},
body: JSON.stringify(body)
};
// read client token
let tokenResponse = await fetch(url, options)
.then(res => res.json())
.catch(err => console.error('Error: ' + err));
const token = tokenResponse.auth.client_token;
// request secret
url = `${address}/v1/${mountPath}/data/${secretID}`;
options = {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Vault-Namespace': `${namespace}`,
'X-Vault-Token': `${token}`,
}
};
// return secret
let secretResponse = await fetch(url, options)
.then(res => res.json())
.catch(err => console.error('Error: ' + err));
return secretResponse.data.data[secretKey];
}
import sys
import anyio
import os
import hvac
import dagger
async def main():
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
# get secret from Vault
secretPlaintext = await get_vault_secret("MOUNT-PATH", "SECRET-ID", "SECRET-KEY")
# load secret into Dagger
secret = client.set_secret("ghApiToken", secretPlaintext)
# use secret in container environment
out = await (
client.container(platform=dagger.Platform("linux/amd64"))
.from_("alpine:3.17")
.with_secret_variable("GITHUB_API_TOKEN", secret)
.with_exec(["apk", "add", "curl"])
.with_exec(["sh", "-c", """curl "https://api.github.com/repos/dagger/dagger/issues" --header "Accept: application/vnd.github+json" --header "Authorization: Bearer $GITHUB_API_TOKEN" """])
.stdout()
)
# print result
print(out)
async def get_vault_secret(mount_path, secret_id, secret_key):
# check for required variables in host environment
for var in ["VAULT_ADDRESS", "VAULT_NAMESPACE", "VAULT_ROLE_ID", "VAULT_SECRET_ID"]:
if var not in os.environ:
raise EnvironmentError('"%s" environment variable must be set' % var)
# create Vault client
client = hvac.Client(
url=os.environ.get("VAULT_ADDRESS"),
namespace=os.environ.get("VAULT_NAMESPACE")
)
# log in to Vault
client.auth.approle.login(
role_id=os.environ.get("VAULT_ROLE_ID"),
secret_id=os.environ.get("VAULT_SECRET_ID"),
use_token=True
)
# read and return secret
read_response = client.secrets.kv.read_secret_version(
path=secret_id,
mount_point=mount_path,
raise_on_deleted_version=True
)
return read_response['data']['data'][secret_key]
anyio.run(main)
Optimizations
Cache dependencies
The following code listing uses a cache volume for application dependencies. This enables Dagger to reuse the contents of the cache every time the pipeline runs, and thereby speed up pipeline operations.
- Go
- Node.js
- Python
package main
import (
"context"
"fmt"
"math"
"math/rand"
"os"
"dagger.io/dagger"
)
func main() {
ctx := context.Background()
// initialize Dagger client
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer client.Close()
// create a cache volume
nodeCache := client.CacheVolume("node")
// use a node:16-slim container
// mount the source code directory on the host
// at /src in the container
// mount the cache volume to persist dependencies
source := client.Container().
From("node:16-slim").
WithDirectory("/src", client.Host().Directory("."), dagger.ContainerWithDirectoryOpts{
Exclude: []string{"node_modules/", "ci/"},
}).
WithMountedCache("/src/node_modules", nodeCache)
// set the working directory in the container
// install application dependencies
runner := source.WithWorkdir("/src").
WithExec([]string{"npm", "install"})