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.
Note
There are also several Modules published by PuzzleLet’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 :)
Note
As Dagger Functions can call other functions across languages, the language a Module is written in doesn’t matter!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!
Note
You may wonder why the dependency containstrivy@5b826062b6bc1bfbd619aa5d0fba117190c85aba
while we wanted to install trivy@v0.5.0
?
This is not a mistake: Dagger enforces version pinning, which guarantees that the module version to be installed always remains exactly the same!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
Note
When using dagger call, all names (functions, arguments, struct fields, etc) are converted into a shell-friendly “kebab-case” style.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()
)