Dagger
Search

expense-tracker

the shared z5labs GoApp archetype, so CI and local builds run the same
opinionated pipeline (fmt + vet + lint + test -race, scratch image,
standardized image tags) and every binary shares one build recipe.

The recipe lives entirely in the pinned z5labs dependency (see
dagger.json); the functions here differ only by package path and binary
name. Common commands:

dagger call check # fmt + vet + lint + test -race (no build)
dagger call templ-check # generated *_templ.go is in sync with the .templ
dagger call server-binary -o ./bin/server # the exact CI artifact, single-arch, locally
dagger call importer-binary -o ./bin/importer
dagger call server-image # the scratch image CI would publish
dagger call image-smoke-test # run that image and check it actually serves
dagger call ci # full pipeline for both binaries (publishes on match)

It also owns the app's run configuration and the dependencies that go
with it — this repo's replacement for a compose file:

dagger call run-against local up --ports 8080:8080 # the app + a seeded emulator, on localhost:8080
dagger call emulator --seed up --ports 8085:8085 # just the emulator, for a `go run ./cmd/server` loop
dagger call integration-test # `go test -tags integration` against its own emulator

...and, on the same principle, the cloud the app runs on: the Terraform in
deploy/ is run from a pinned container with an explicit short-lived token,
never from a `terraform` on someone's PATH (see terraform.go):

dagger call terraform validate # the CI gate; no cloud, no credentials
dagger call terraform --project=… … plan # and state-bucket / apply / output

The z5labs pipeline is deliberately narrow: it has no Firestore to talk
to, and it does not run templ. So the emulator-backed tests are
build-tagged out of it, and both they and the templ-diff check are their
own functions — which is also how CI runs them, one `dagger call` each.

Dagger does not allow a module function to return a dependency's type,
so each function is terminal: it constructs the z5labs GoApp internally
and returns a File, Container, or error.

Installation

dagger install github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145

Entrypoint

Return Type
ExpenseTracker
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
func (m *MyModule) Example() *dagger.ExpenseTracker  {
	return dag.
			Expensetracker()
}
@function
def example() -> dagger.ExpenseTracker:
	return (
		dag.expense_tracker()
	)
@func()
example(): ExpenseTracker {
	return dag
		.expenseTracker()
}

Types

ExpenseTracker 🔗

ExpenseTracker is this repository’s Dagger module.

check() 🔗

Check runs the shared z5labs check stages once for the whole module: gofmt, go vet, golangci-lint, and go test -race. It builds nothing, so it is the fast pre-commit / PR gate.

Return Type
Void !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 check
func (m *MyModule) Example(ctx context.Context)   {
	return dag.
			Expensetracker().
			Check(ctx)
}
@function
async def example() -> None:
	return await (
		dag.expense_tracker()
		.check()
	)
@func()
async example(): Promise<void> {
	return dag
		.expenseTracker()
		.check()
}

ci() 🔗

Ci runs the full standardized pipeline for BOTH binaries: the shared checks, a multi-arch scratch build, and — when registry is set and the source’s HEAD ref matches z5labs’ publishOn filter (default main) — a publish. With no registry it is a checks + build gate safe to run anywhere.

Return Type
Void !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
registryString -

Container registry to publish to (e.g. “us-docker.pkg.dev//”). Empty disables publishing.

authSecret -

Registry password/token; required by z5labs when registry is set.

Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 ci
func (m *MyModule) Example(ctx context.Context)   {
	return dag.
			Expensetracker().
			Ci(ctx)
}
@function
async def example() -> None:
	return await (
		dag.expense_tracker()
		.ci()
	)
@func()
async example(): Promise<void> {
	return dag
		.expenseTracker()
		.ci()
}

deploy() 🔗

Deploy publishes this commit’s images through the z5labs pipeline and rolls the server image out to Cloud Run. It returns the service’s URL.

One token does both halves: Artifact Registry takes it as the password for the oauth2accesstoken user, and gcloud reads it from the environment.

Not cached, and the exec inside carries a nonce for the same reason terraform.go’s do: what a deploy changes lives in Google’s API, which Dagger cannot see, so an identical second call must actually run again rather than replay the first one’s output.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
projectString !-

Google Cloud project that owns the registry and the service.

accessTokenSecret !-

A short-lived Google OAuth access token — it authenticates both the registry push and gcloud:

--access-token=cmd://"gcloud auth print-access-token"
regionString "us-central1"

Region of the Cloud Run service and the registry. Must match the region deploy/ was applied with.

serviceString "expense-tracker"

Cloud Run service to roll the image out to; deploy/variables.tf’s service_name.

artifactRegistryRepositoryString "containers"

Artifact Registry repository the images are published to; deploy/variables.tf’s artifact_registry_repository.

Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 deploy --project string --access-token env:MYSECRET
func (m *MyModule) Example(ctx context.Context, project string, accessToken *dagger.Secret) string  {
	return dag.
			Expensetracker().
			Deploy(ctxproject, accessToken)
}
@function
async def example(project: str, accesstoken: dagger.Secret) -> str:
	return await (
		dag.expense_tracker()
		.deploy(project, accesstoken)
	)
@func()
async example(project: string, accessToken: Secret): Promise<string> {
	return dag
		.expenseTracker()
		.deploy(project, accessToken)
}

emulator() 🔗

Emulator is a Firestore emulator on its own, for the fast edit loop — publish it to the host and run the app from source against it:

dagger call emulator --seed up --ports 8085:8085
FIRESTORE_EMULATOR_HOST=localhost:8085 GCP_PROJECT=demo-expense-tracker \
  OWNER_EMAIL=you@example.com \
  OAUTH_CLIENT_ID=local.apps.googleusercontent.com OAUTH_CLIENT_SECRET=x \
  SESSION_KEY="$(openssl rand -base64 32)" go run ./cmd/server

For the whole app, dependencies and all, use run-against local.

Its data lives only as long as the service does: stop it and the log is gone. –seed fills a fresh one with a couple of months of sample events, so there is something to look at.

Return Type
Service !
Arguments
NameTypeDefault ValueDescription
portInteger 8085

Port to listen on, inside the container.

seedBoolean -

Write a sample event log into it once it is up.

projectString "demo-expense-tracker"

Project id the sample events are written under; must match the GCP_PROJECT the app runs with, since it namespaces the data.

Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 emulator
func (m *MyModule) Example() *dagger.Service  {
	return dag.
			Expensetracker().
			Emulator()
}
@function
def example() -> dagger.Service:
	return (
		dag.expense_tracker()
		.emulator()
	)
@func()
example(): Service {
	return dag
		.expenseTracker()
		.emulator()
}

imageSmokeTest() 🔗

ImageSmokeTest starts the scratch image that server-image builds — the exact artifact CI publishes — with a Firestore emulator behind it, and checks that the running container serves liveness, readiness, redirects protected routes to sign in, and mounts the sign-in flow on $PORT.

It is the only thing that tests the deployable rather than the source, which is why CI runs it as its own leg.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 image-smoke-test
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Imagesmoketest(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.imagesmoketest()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.imageSmokeTest()
}

imageTag() 🔗

ImageTag is the tag this commit’s images are published under — “-”, the z5labs pipeline’s scheme for a branch build:

dagger call image-tag
# 4f2a1c9-2026-07-12T14-03-22-05-00

The pipeline does not tell anyone what it pushed, so the deploy has to derive it. That is a duplicated assumption, and the honest thing to do with one is to state it and then check it: Deploy asks the registry whether that exact tag is really there before it rolls anything out, so a change to z5labs’ scheme fails the deploy with a message that says so, rather than deploying a stale image that happens to still exist.

It reads git, so — like ci — it needs a real .git directory and does not work from inside a git worktree.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 image-tag
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Imagetag(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.imagetag()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.imageTag()
}

importerBinary() 🔗

ImporterBinary compiles cmd/importer into the same shape of artifact as ServerBinary, single-arch for the host.

Return Type
File !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 importer-binary
func (m *MyModule) Example() *dagger.File  {
	return dag.
			Expensetracker().
			Importerbinary()
}
@function
def example() -> dagger.File:
	return (
		dag.expense_tracker()
		.importerbinary()
	)
@func()
example(): File {
	return dag
		.expenseTracker()
		.importerBinary()
}

importerImage() 🔗

ImporterImage builds the scratch image CI would publish for cmd/importer, single-arch for the host.

Return Type
Container !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 importer-image
func (m *MyModule) Example() *dagger.Container  {
	return dag.
			Expensetracker().
			Importerimage()
}
@function
def example() -> dagger.Container:
	return (
		dag.expense_tracker()
		.importerimage()
	)
@func()
example(): Container {
	return dag
		.expenseTracker()
		.importerImage()
}

integrationTest() 🔗

IntegrationTest runs the emulator-backed tests — the ones tagged integration, which the z5labs go test -race stage skips because it has no database — against an emulator bound into the test container. It is self-contained: no host emulator, no host ports.

Return Type
String !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 integration-test
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Integrationtest(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.integrationtest()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.integrationTest()
}

runAgainst() 🔗

RunAgainst starts the run-configuration chain. Source is contextual, so dagger call run-against local needs no arguments.

Return Type
ExpenseTrackerRunAgainst !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 run-against
func (m *MyModule) Example() *dagger.ExpenseTrackerRunAgainst  {
	return dag.
			Expensetracker().
			Runagainst()
}
@function
def example() -> dagger.ExpenseTrackerRunAgainst:
	return (
		dag.expense_tracker()
		.runagainst()
	)
@func()
example(): ExpenseTrackerRunAgainst {
	return dag
		.expenseTracker()
		.runAgainst()
}

serverBinary() 🔗

ServerBinary compiles cmd/server into the exact binary CI publishes (CGO disabled, -trimpath, -s -w), single-arch for the host. Export it with -o:

dagger call server-binary -o ./bin/server
Return Type
File !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 server-binary
func (m *MyModule) Example() *dagger.File  {
	return dag.
			Expensetracker().
			Serverbinary()
}
@function
def example() -> dagger.File:
	return (
		dag.expense_tracker()
		.serverbinary()
	)
@func()
example(): File {
	return dag
		.expenseTracker()
		.serverBinary()
}

serverImage() 🔗

ServerImage builds the scratch image CI would publish for cmd/server, single-arch for the host. Run it, export it, or inspect it:

dagger call server-image export --path server.tar
Return Type
Container !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 server-image
func (m *MyModule) Example() *dagger.Container  {
	return dag.
			Expensetracker().
			Serverimage()
}
@function
def example() -> dagger.Container:
	return (
		dag.expense_tracker()
		.serverimage()
	)
@func()
example(): Container {
	return dag
		.expenseTracker()
		.serverImage()
}

templCheck() 🔗

TemplCheck re-runs templ generate and fails if the result differs from what is committed. The z5labs pipeline does not run templ, and the generated *_templ.go is committed precisely so it doesn’t have to — a fresh checkout has to already compile. That trade only holds if something enforces the two staying in sync; this is that something.

It compares content, not timestamps: templ leaves a file alone when the output is unchanged, but a check that keyed on writes rather than bytes would rot the moment that stopped being true.

Return Type
Void !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -No description provided
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 templ-check
func (m *MyModule) Example(ctx context.Context)   {
	return dag.
			Expensetracker().
			Templcheck(ctx)
}
@function
async def example() -> None:
	return await (
		dag.expense_tracker()
		.templcheck()
	)
@func()
async example(): Promise<void> {
	return dag
		.expenseTracker()
		.templCheck()
}

terraform() 🔗

Terraform builds the root module’s command surface. Only validate works with no arguments — it is the one command that touches no cloud, which is exactly why CI can run it on every pull request.

Return Type
ExpenseTrackerTerraform !
Arguments
NameTypeDefault ValueDescription
sourceDirectory -

The root module. Only deploy/ is uploaded — the Go sources have no bearing on the infrastructure, and a source change that could not possibly alter a plan should not invalidate one.

projectString -

Google Cloud project id. Required by everything except validate.

regionString "us-central1"No description provided
ownerEmailString -

The owner’s Google account. Required by plan and apply.

oauthClientIdString -

The Google Sign-In client id (“….apps.googleusercontent.com”). Required by plan and apply.

stateBucketString -

GCS bucket for the remote state. Defaults to “-tfstate”.

allowUnauthenticatedBoolean -

Grant roles/run.invoker to allUsers. Leave it off until the app checks who is calling.

accessTokenSecret -

A short-lived Google OAuth access token:

--access-token=cmd://"gcloud auth print-access-token"
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform
func (m *MyModule) Example() *dagger.ExpenseTrackerTerraform  {
	return dag.
			Expensetracker().
			Terraform()
}
@function
def example() -> dagger.ExpenseTrackerTerraform:
	return (
		dag.expense_tracker()
		.terraform()
	)
@func()
example(): ExpenseTrackerTerraform {
	return dag
		.expenseTracker()
		.terraform()
}

ExpenseTrackerRunAgainst 🔗

RunAgainst is the app’s run configuration, as code. It answers “where does this thing run” the way an IDE’s run configuration would, except it is reproducible and shared: Local() stands the whole stack up on the local engine, which is what this repo has instead of a compose file. A NonProd() sibling — the same app container, pointed at services that already exist somewhere — is the natural next one.

source() 🔗

Source is the repository: the app that gets built and run.

Return Type
Directory !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 run-against \
 source
func (m *MyModule) Example() *dagger.Directory  {
	return dag.
			Expensetracker().
			Runagainst().
			Source()
}
@function
def example() -> dagger.Directory:
	return (
		dag.expense_tracker()
		.runagainst()
		.source()
	)
@func()
example(): Directory {
	return dag
		.expenseTracker()
		.runAgainst()
		.source()
}

local() 🔗

Local runs the app against a complete local stack: a Firestore emulator with a sample event log already in it, and the server — the very container CI builds and publishes, not go run — wired to it. One command brings up every dependency and the app:

dagger call run-against local up --ports 8080:8080

Then the app is on localhost:8080, backed by the emulator. Nothing is published to the host but the app itself; the emulator is reachable only from inside, which is where the app is.

Not cached, and it must not be: a cached call would hand back the previous session’s service without running any of this — without starting an emulator, without seeding it — and the app would come up against a database that does not exist.

Return Type
Service !
Arguments
NameTypeDefault ValueDescription
portInteger 8080

Port the app listens on, in the container and on the host.

projectString "demo-expense-tracker"

Google Cloud project id. Against the emulator it only namespaces the data, so any value will do — but the app and the seed have to agree on it.

ownerEmailString "owner@example.com"

The owner allowlist’s single address. Nothing enforces it until the auth stories land.

seedBoolean true

Write a sample event log into the emulator before the app starts. Off means an empty database — the app runs, there is nothing in it.

oauthClientIdString "local-client-id.apps.googleusercontent.com"

Google Sign-In credentials. The defaults are placeholders — enough for the app to boot and serve, which is all a run against a fake database needs. To actually sign in locally, pass a real client and register http://localhost:8080/auth/callback as one of its authorized redirect URIs:

dagger call run-against local --oauth-client-id=… \
  --oauth-client-secret=env:OAUTH_CLIENT_SECRET up --ports 8080:8080
oauthClientSecretSecret -No description provided
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 run-against \
 local
func (m *MyModule) Example() *dagger.Service  {
	return dag.
			Expensetracker().
			Runagainst().
			Local()
}
@function
def example() -> dagger.Service:
	return (
		dag.expense_tracker()
		.runagainst()
		.local()
	)
@func()
example(): Service {
	return dag
		.expenseTracker()
		.runAgainst()
		.local()
}

ExpenseTrackerTerraform 🔗

Terraform is the deploy/ root module together with everything it needs to talk to a project. Constructed by ExpenseTracker.Terraform; every command is a method on it, so --project and friends are given once:

dagger call terraform --project=p --owner-email=o --access-token=… plan

source() 🔗

Source is deploy/ — the root module, and nothing else in the repo.

Return Type
Directory !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 source
func (m *MyModule) Example() *dagger.Directory  {
	return dag.
			Expensetracker().
			Terraform().
			Source()
}
@function
def example() -> dagger.Directory:
	return (
		dag.expense_tracker()
		.terraform()
		.source()
	)
@func()
example(): Directory {
	return dag
		.expenseTracker()
		.terraform()
		.source()
}

project() 🔗

Project is the Google Cloud project every resource is created in.

Return Type
String !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 project
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Terraform().
			Project(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.terraform()
		.project()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.terraform()
		.project()
}

region() 🔗

Region carries Cloud Run, Artifact Registry, and the state bucket.

Return Type
String !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 region
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Terraform().
			Region(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.terraform()
		.region()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.terraform()
		.region()
}

ownerEmail() 🔗

OwnerEmail is the app’s allowlist and, until the service goes public, the one principal that may invoke it.

Return Type
String !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 owner-email
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Terraform().
			Owneremail(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.terraform()
		.owneremail()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.terraform()
		.ownerEmail()
}

oauthClientId() 🔗

OAuthClientID is the Google Sign-In client the app signs people in with. Required by plan and apply for the same reason OwnerEmail is: it goes into the service’s environment, and the app refuses to boot without it. The matching secret never passes through here — it is added to Secret Manager out of band.

Return Type
String !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 oauth-client-id
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Terraform().
			Oauthclientid(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.terraform()
		.oauthclientid()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.terraform()
		.oauthClientId()
}

bucket() 🔗

Bucket holds the remote state. Empty means “-tfstate”. It is not called StateBucket because the function that creates it is, and a Dagger object cannot expose a field and a method under one name.

Return Type
String !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 bucket
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Terraform().
			Bucket(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.terraform()
		.bucket()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.terraform()
		.bucket()
}

allowUnauthenticated() 🔗

AllowUnauthenticated opens the service to allUsers. Off until the app authenticates its own callers — see deploy/variables.tf.

Return Type
Boolean !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 allow-unauthenticated
func (m *MyModule) Example(ctx context.Context) bool  {
	return dag.
			Expensetracker().
			Terraform().
			Allowunauthenticated(ctx)
}
@function
async def example() -> bool:
	return await (
		dag.expense_tracker()
		.terraform()
		.allowunauthenticated()
	)
@func()
async example(): Promise<boolean> {
	return dag
		.expenseTracker()
		.terraform()
		.allowUnauthenticated()
}

accessToken() 🔗

AccessToken is a short-lived Google OAuth token. Both the provider and the GCS backend read it from GOOGLE_OAUTH_ACCESS_TOKEN, and gcloud from CLOUDSDK_AUTH_ACCESS_TOKEN, so one secret authenticates everything here.

Return Type
Secret !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 access-token
func (m *MyModule) Example() *dagger.Secret  {
	return dag.
			Expensetracker().
			Terraform().
			Accesstoken()
}
@function
def example() -> dagger.Secret:
	return (
		dag.expense_tracker()
		.terraform()
		.accesstoken()
	)
@func()
example(): Secret {
	return dag
		.expenseTracker()
		.terraform()
		.accessToken()
}

apply() 🔗

Apply makes it so.

-auto-approve, because there is no terminal here to approve at: a Dagger function’s exec has no stdin. plan is the review step, and it is not optional in the way an interactive prompt lets you pretend it is — read it.

Return Type
String !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 apply
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Terraform().
			Apply(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.terraform()
		.apply()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.terraform()
		.apply()
}

output() 🔗

Output returns the root module’s outputs as JSON — the registry to publish to, the service to deploy, the identities to impersonate. It is how the deploy story (#7) learns what this story created, instead of repeating the names in a workflow file where they can go stale.

Return Type
String !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 output
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Terraform().
			Output(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.terraform()
		.output()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.terraform()
		.output()
}

plan() 🔗

Plan reports what an apply would change, and changes nothing. Run it first — always, but especially on this module: an apply that recreates the Firestore database is a plan you want to have read.

Return Type
String !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 plan
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Terraform().
			Plan(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.terraform()
		.plan()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.terraform()
		.plan()
}

stateBucket() 🔗

StateBucket creates the GCS bucket the remote state lives in, and is idempotent — run it once per project, or every time, it makes no difference.

It exists because of a genuine ordering problem, not for want of a resource: Terraform initializes its backend before it evaluates any configuration, so a bucket declared in this root module could never be the bucket this root module stores its state in. Somebody has to create it out of band. The usual “somebody” is a human following a README, which is how state buckets end up with no versioning; this is that step, written down and executable.

Versioning is the point of the exercise. A corrupted or truncated state file is recoverable from an earlier generation; without it, the only record of what Terraform believes it created is gone, and every resource has to be imported by hand.

Return Type
String !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 state-bucket
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Terraform().
			Statebucket(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.terraform()
		.statebucket()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.terraform()
		.stateBucket()
}

validate() 🔗

Validate is the pull-request gate: the module is formatted, its syntax and its references hold, and every provider it names can be resolved.

It initializes with -backend=false, so it needs no bucket, no project, and no credentials — which is the only reason CI can run it at all. A validate that required cloud access would either not run on pull requests or would hand every fork’s pull request a token.

What it therefore cannot catch is anything only the API knows: a quota, a name already taken, an invalid region. That is what plan is for.

Return Type
String !
Example
dagger -m github.com/Zaba505/expense-tracker@c67b6e164c86155d0226a1d72ed99d39c145d145 call \
 terraform \
 validate
func (m *MyModule) Example(ctx context.Context) string  {
	return dag.
			Expensetracker().
			Terraform().
			Validate(ctx)
}
@function
async def example() -> str:
	return await (
		dag.expense_tracker()
		.terraform()
		.validate()
	)
@func()
async example(): Promise<string> {
	return dag
		.expenseTracker()
		.terraform()
		.validate()
}