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 Python SDK. If not, refer to the Python 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=python potato
cd potato

This will generate a dagger.json module file, initial dagger/src/main/__init__.py, dagger/pyproject.toml and dagger/requirements.lock files, as well as a generated dagger/sdk folder for local development.

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.

Replace the auto-generated examples in dagger/src/main/__init__.py with your own, simpler Dagger Function:

from dagger import function, object_type


@object_type
class Potato:
@function
def hello_world(self) -> str:
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.

Update the Dagger Function to accept multiple input parameters (some of which are optional):

from dagger import function, object_type


@object_type
class Potato:
@function
def hello_world(self, count: int, mashed: bool = False) -> str:
if mashed:
return f"Hello Daggernauts, I have mashed {count} potatoes"
return f"Hello Daggernauts, I have {count} potatoes"

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:

from dagger import field, function, object_type


@object_type
class PotatoMessage:
message: str = field()
from_: str = field(name="from")


@object_type
class Potato:
@function
def hello_world(
self,
count: int,
mashed: bool = False,
) -> PotatoMessage:
if mashed:
message = f"Hello Daggernauts, I have mashed {count} potatoes"
else:
message = f"Hello Daggernauts, I have {count} potatoes"

return PotatoMessage(
message=message,
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 --sdk=python trivy
    cd trivy
  2. Replace the generated dagger/src/main/__init__.py file with the following code:

    """Security scanning with Trivy."""
    from typing import Annotated

    from dagger import Doc, dag, function, object_type


    @object_type
    class Trivy:
    """Functions for scanning images for vulnerabilities using Trivy"""

    @function
    async def scan_image(
    self,
    image_ref: Annotated[
    str,
    Doc("The image reference to scan"),
    ],
    severity: Annotated[
    str,
    Doc("Severity levels to scan for"),
    ] = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL",
    exit_code: Annotated[
    int,
    Doc("The exit code to return if vulnerabilities are found"),
    ] = 0,
    format: Annotated[
    str,
    Doc("The output format to use for the scan results"),
    ] = "table",
    ) -> str:
    """Scan the specified image for vulnerabilities."""
    return await (
    dag.container()
    .from_("aquasec/trivy:latest")
    .with_exec(
    [
    "image",
    "--quiet",
    "--severity",
    severity,
    "--exit-code",
    str(exit_code),
    "--format",
    format,
    image_ref,
    ]
    )
    .stdout()
    )

    In this example, the scan_image() function accepts four parameters:

    • 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).

    The function code performs the following operations:

    • It uses the default 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.with_exec() 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 with_exec() 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: