4. Daggerverse and Modules

Daggerverse and Modules

Until here, we have successfully daggerized an application using the Dagger API. But there is more: Dagger allows you to reuse Dagger Functions developed by others, which were published to the Daggerverse !

So let’s visit the Daggerverse and explore it a bit. Here we find hundreds of ready to use Dagger Modules - and each one of them extends Dagger with one or more additional Functions! We can also navigate to the (Git) repositories and inspect the source code of each published Module.

Let’s search for Trivy , a very popular open-source vulnerability scann tool At the time of writing, a search for trivy reveals six Modules (+1 just containing examples) - which leaves us with six different solutions to one problem :)

Task 4.1: Install a Module from Daggerverse

For our task, we chose the github.com/sagikazarmark/daggerverse/trivy Module.

Explore its page, have a look at the available functions which are documented on the left side.

After that, add it to our project by installing it

show solution
dagger install github.com/sagikazarmark/daggerverse/trivy@v0.5.0

Dagger downloaded the Module and added it as dependency to our dagger.json:

{
  "name": "ClassQuiz",
  "engineVersion": "v0.18.0",
  "sdk": {
    "source": "python"
  },
  "dependencies": [
    {
      "name": "trivy",
      "source": "github.com/sagikazarmark/daggerverse/trivy@trivy/v0.5.0",
      "pin": "5b826062b6bc1bfbd619aa5d0fba117190c85aba"
    }
  ],
  "source": "ci"
}

This way, all the functions provided by the module are available directly in our code - no need to add further imports or anything like that!

The Challenge

We already know how to spin up our app locally, but now it’s time to do some security tests.
So instead of starting the app, we want to build the container images and scan them for vulnerabilities.

We could simply create and start a Trivy Dagger Container using the Dagger API like we did for Redis & Co.
But after what we learned previously, we will, of course, use the functions of the Trivy Module !

Extending our codebase

The Trivy Module has a container() function, which expects Container as argument. As our existing frontend() and backend() return a Service , we need an additional functions.

Since the only difference in creating these containers is the path in which they are built, we will combine them into a single function:

    @function
    async def build(self, context: dagger.Directory) -> dagger.Container:
        """Returns a container built with the given context."""
        return await dag.container().build(context)

For better scalability, we have defined the function as asynchronous.

Add this build method to your module.

Task 4.2: Add Trivy scan

Now that everything is prepared, it’s time to add the actual Trivy scan:

Add a vulnerability_scan function, which returns a directory containing the scan results.

show solution
    @function
    async def vulnerability_scan(self, context: dagger.Directory) -> dagger.Directory:
        """Builds the front- and backend, performs a Trivy scan and returns the directory containing the reports."""
        trivy = dag.trivy()

        directory = (
            dag.directory()
            .with_file("scans/backend.sarif", trivy.container(await self.build(context)).report("sarif"))
            .with_file("scans/frontend.sarif", trivy.container(await self.build(context.directory("frontend"))).report("sarif"))
        )
        return directory

Task 4.3: Run the scan from CLI

Let’s see if we can run it from the CLI and have a look at the results:

show solution
dagger call vulnerability-scan --context=. export --path=.tmp

If everything went well, the scan results should be found in the directory .tmp/scans/.

Complete Solution

ci/src/class_quiz/main.py:

import dagger
from dagger import dag, function, object_type

@object_type
class ClassQuiz:

    @function
    async def build(self, context: dagger.Directory) -> dagger.Container:
        """Returns a container built with the given context."""
        return await dag.container().build(context)

    @function
    async def vulnerability_scan(self, context: dagger.Directory) -> dagger.Directory:
        """Builds the front- and backend, performs a Trivy scan and returns the directory containing the reports."""
        trivy = dag.trivy()

        directory = (
            dag.directory()
            .with_file("scans/backend.sarif", trivy.container(await self.build(context)).report("sarif"))
            .with_file("scans/frontend.sarif", trivy.container(await self.build(context.directory("frontend"))).report("sarif"))
        )
        return directory

    @function
    def frontend(self, context: dagger.Directory) -> dagger.Container:
        """Returns a frontend container built with the given context and params."""
        return (
            dag.container()
            .with_env_variable("API_URL", "http://api:8081")
            .with_env_variable("REDIS_URL", "redis://redisd:6379/0?decode_responses=True")
            .build(context)
        )

    @function
    def backend(self, context: dagger.Directory) -> dagger.Container:
        """Returns a backend container built with the given context, params and service bindings."""
        return (
            dag.container()
            .with_env_variable("MAX_WORKERS", "1")
            .with_env_variable("PORT", "8081")
            .with_env_variable("REDIS", "redis://redisd:6379/0?decode_responses=True")
            .with_env_variable("SKIP_EMAIL_VERIFICATION", "True")
            .with_env_variable("DB_URL", "postgresql://postgres:classquiz@postgresd:5432/classquiz")
            .with_env_variable("MAIL_ADDRESS", "some@example.org")
            .with_env_variable("MAIL_PASSWORD", "some@example.org")
            .with_env_variable("MAIL_USERNAME", "some@example.org")
            .with_env_variable("MAIL_SERVER", "some.example.org")
            .with_env_variable("MAIL_PORT", "525")
            .with_env_variable("SECRET_KEY", "secret")
            .with_env_variable("MEILISEARCH_URL", "http://meilisearchd:7700")
            .with_env_variable("STORAGE_BACKEND", "local")
            .with_env_variable("STORAGE_PATH", "/app/data")
            .with_service_binding("postgresd", self.postgres())
            .with_service_binding("meilisearchd", self.meilisearch())
            .with_service_binding("redisd", self.redis())
            .build(context)
        )

    @function
    def redis(self) -> dagger.Service:
        """Returns a redis service from a container built with the given params."""
        return (
            dag.container()
            .from_("redis:alpine")
            .with_exposed_port(6379)
            .as_service()
        )

    @function
    def postgres(self) -> dagger.Service:
        """Returns a postgres database service from a container built with the given params."""
        return (
            dag.container()
            .from_("postgres:14-alpine")
            .with_env_variable("POSTGRES_PASSWORD", "classquiz")
            .with_env_variable("POSTGRES_DB", "classquiz")
            .with_env_variable("POSTGRES_USER", "postgres")
            .with_exposed_port(5432)
            .as_service()
        )

    @function
    def meilisearch(self) -> dagger.Service:
        """Returns a meilisearch service from a container built with the given params."""
        return (
            dag.container()
            .from_("getmeili/meilisearch:v0.28.0")
            .with_exposed_port(7700)
            .as_service()
        )

    @function
    def proxy(self, context: dagger.Directory, proxy_config: dagger.File) -> dagger.Service:
        """Returns a caddy proxy service encapsulating the front and backend services. This service must be bound to port 8000 in order to match some hard coded configuration: --ports 8000:8080"""
        return (
            dag.container()
            .from_("caddy:alpine")
            .with_service_binding("frontend", self.frontend(context.directory("frontend")).as_service())
            .with_service_binding("api", self.backend(context).as_service())
            .with_file("/etc/caddy/Caddyfile", proxy_config)
            .with_exposed_port(8080)
            .as_service()
        )