Skip to main content

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!
note

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
tip

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.

  1. Initialize a new module:

    dagger init --name=trivy --sdk=go trivy
    cd trivy
  2. 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 (like Container, 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's Container().From() method to initialize a new container from a base image. In this example, the base image is the official Trivy image aquasec/trivy:latest. This method returns a Container 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, the trivy image command for image scanning. It also passes the optional parameters to the command. The WithExec() method returns a revised Container 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.
  3. 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: