Your First Module
Introduction
Welcome to Dagger, a programmable tool that lets you replace your software project's artisanal scripts with a modern API and cross-language scripting engine.
Dagger lets you encapsulate all your project's tasks and workflows into simple Dagger Modules, written in your programming language of choice.
This guide introduces you to Dagger Modules and walks you, step by step, through the process of creating your first Dagger Module.
Requirements
This guide assumes that:
- You have a good understanding of the Dagger Go SDK. If not, refer to the Go SDK reference.
- You have the Dagger CLI installed. If not, install Dagger.
- You have Docker installed and running on the host system. If not, install Docker.
Step 1: Initialize a new Dagger Module
Create a new directory on your filesystem and run dagger init
to bootstrap your first Dagger Module. This guide calls it potato
, but you can choose your favorite food.
# initialize Dagger module
dagger init --sdk=go potato
cd potato/
This will generate a dagger.json
module file, an initial dagger/main.go
source file, as well as a dagger/dagger.gen.go
file and dagger/internal/
directory.
The auto-generated Dagger Module template comes with some example Dagger Functions. Test it with the dagger call
command:
dagger call container-echo --string-arg='Hello Daggernauts!' stdout
The result will be:
Hello Daggernauts!
When using dagger call
, all names (functions, arguments, struct fields, etc) are converted into a shell-friendly "kebab-case" style.
Step 2: Create a simple Dagger Function
Dagger Modules contain one or more Dagger Functions. You saw one of them in the previous section, but you can also add your own.
This Dagger Module is named potato
, so that means all methods on the Potato
type are published as Dagger Functions. Replace the auto-generated examples in dagger/main.go
with your own, simpler Dagger Function:
package main
type Potato struct{}
func (m *Potato) HelloWorld() string {
return "Hello Daggernauts!"
}
Test the new Dagger Function, once again using dagger call
:
dagger call hello-world
The result will be:
Hello Daggernauts!
Step 3: Add arguments to your Dagger Function
Dagger Functions can accept and return multiple different types, not just basic built-in types.
For example, you can include an optional context.Context
input, and an optional error
return type. These are all valid variations of the previous Dagger Function:
func (m *Potato) HelloWorld() string
func (m *Potato) HelloWorld() (string, error)
func (m *Potato) HelloWorld(ctx context.Context) string
func (m *Potato) HelloWorld(ctx context.Context) (string, error)
Update the Dagger Function to accept multiple input parameters (some of which are optional):
package main
import "fmt"
type Potato struct{}
func (m *Potato) HelloWorld(
// the number of potatoes to process
count int,
// whether the potatoes are mashed
// +optional
mashed bool,
) string {
if mashed {
return fmt.Sprintf("Hello Daggernauts, I have mashed %d potatoes", count)
}
return fmt.Sprintf("Hello Daggernauts, I have %d potatoes", count)
}
The optional parameters are specified using the special // +optional
comment, which Dagger uses to mark these as optional in the generated API. These comments must appear in the comment block before the parameter.
Here's an example of calling the Dagger Function with optional parameters:
dagger call hello-world --count=10 --mashed
The result will be:
Hello Daggernauts, I have mashed 10 potatoes
Use dagger call --help
at any point to get help on the commands and flags available.
Step 4: Add return values to your Dagger Function
Update the Dagger Function to return a custom PotatoMessage
type:
package main
import "fmt"
type Potato struct{}
type PotatoMessage struct {
Message string
From string
}
func (m *Potato) HelloWorld(
// the number of potatoes to process
count int,
// whether the potatoes are mashed
// +optional
mashed bool,
) PotatoMessage {
if mashed {
return PotatoMessage{
Message: fmt.Sprintf("Hello Daggernauts, I have mashed %d potatoes", count),
From: "potato@example.com",
}
}
return PotatoMessage{
Message: fmt.Sprintf("Hello Daggernauts, I have %d potatoes", count),
From: "potato@example.com",
}
}
Test it using dagger call
:
dagger call hello-world --count=10 message
dagger call hello-world --count=10 from
The result will be:
Hello Daggernauts, I have 10 potatoes
potato@example.com
Step 5: Create a more complex Dagger Function
Now, put everything you've learnt to the test, by building a Dagger Module and Dagger Function for a real-world use case: scanning a container image for vulnerabilities with Trivy.
-
Initialize a new module:
dagger init --name=trivy --sdk=go trivy
cd trivy -
Replace the generated
dagger/main.go
file with the following code:package main
import (
"context"
"strconv"
)
type Trivy struct{}
// pull the official Trivy image
// send the trivy CLI an image ref to scan
func (t *Trivy) ScanImage(
ctx context.Context,
imageRef string,
// +optional
// +default="UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL"
severity string,
// +optional
exitCode int,
// +optional
// +default="table"
format string,
) (string, error) {
return dag.
Container().
From("aquasec/trivy:latest").
WithExec([]string{
"image",
"--quiet",
"--severity", severity,
"--exit-code", strconv.Itoa(exitCode),
"--format", format,
imageRef,
}).
Stdout(ctx)
}In this example, the
ScanImage()
function accepts four parameters (apart from the context):- A reference to the container image to be scanned (required);
- A severity filter (optional);
- The exit code to use if scanning finds vulnerabilities (optional);
- The reporting format (optional).
dag
is the Dagger client, which is pre-initialized. It contains all the core types (likeContainer
,Directory
, etc.), as well as bindings to any dependencies your module has declared.The function code performs the following operations:
- It uses the
dag
client'sContainer().From()
method to initialize a new container from a base image. In this example, the base image is the official Trivy imageaquasec/trivy:latest
. This method returns aContainer
representing an OCI-compatible container image. - It uses the
Container.WithExec()
method to define the command to be executed in the container - in this case, thetrivy image
command for image scanning. It also passes the optional parameters to the command. TheWithExec()
method returns a revisedContainer
with the results of command execution. - It retrieves the output stream of the command with the
Container.Stdout()
method and prints the result to the console.
-
Test the function using
dagger call
:dagger call scan-image --image-ref alpine:latest
Here's an example of the output:
alpine:latest (alpine 3.19.1)
=============================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
Conclusion
This guide taught you the basics of writing a Dagger Module. It showed you how to initialize a Dagger Module, add Dagger Functions to it, and work with arguments and custom return values. It also worked through a real-world use case: a Dagger Module to scan container images with Trivy.
Continue your journey into Dagger programming with the following resources: