Interactive Debugging
Pipeline failures can be both frustrating and lead to wasted resources as the team seeks to understand what went wrong and why. Dagger's interactive debugging feature is an invaluable tool in this situation.
Dagger lets users drop in to an interactive shell when a pipeline run fails, with all the context at the point of failure. This is similar to a debugger experience, but without needing to set breakpoints explicitly. No changes are required to your code.
Here's an example of a pipeline run failing, and Dagger opening an interactive terminal at the point of failure:
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger.io/dagger/dag"
)
type MyModule struct{}
func (m *MyModule) Foo(ctx context.Context) (string, error) {
return dag.Container().
From("alpine:latest").
WithExec([]string{"sh", "-c", "echo hello world > /foo"}).
WithExec([]string{"cat", "/FOO"}). // deliberate error
Stdout(ctx)
}
// run with dagger call --interactive foo
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def foo(self) -> str:
return await (
dag.container()
.from_("alpine:latest")
.with_exec(["sh", "-c", "echo hello world > /foo"])
.with_exec(["cat", "/FOO"]) # deliberate error
.stdout()
)
# run with dagger call --interactive foo
import { dag, object, Directory, Container, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
async foo(): Promise<string> {
return await dag
.container()
.from("alpine:latest")
.withExec(["sh", "-c", "echo hello world > /foo"])
.withExec(["cat", "/FOO"]) // deliberate error
.stdout()
}
}
// run with dagger call --interactive foo
See it in action:
You can also set one or more explicit breakpoints in your pipeline, which then starts an interactive terminal session at each breakpoint. This lets you inspect a directory or a container at any point in your pipeline run, with all the necessary context available to you.
Here's another example, which opens an interactive terminal at two different points in a pipeline run to inspect the built container:
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
"dagger.io/dagger/dag"
)
type MyModule struct{}
func (m *MyModule) Foo(ctx context.Context) *dagger.Container {
return dag.Container().
From("alpine:latest").
Terminal().
WithExec([]string{"sh", "-c", "echo hello world > /foo"}).
Terminal()
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def foo(self) -> dagger.Container:
return await (
dag.container()
.from_("alpine:latest")
.terminal()
.with_exec(["sh", "-c", "echo hello world > /foo"])
.terminal()
)
import { dag, object, Directory, Container, func } from "@dagger.io/dagger"
@object()
class MyModule {
@func()
async foo(): Container {
return await dag
.container()
.from("alpine:latest")
.terminal()
.withExec(["sh", "-c", "echo hello world > /foo"])
.terminal()
}
}
See it in action: