Simpler Reusability with the Global Client
January 22, 2024
Jan 22, 2024
Helder Correia
None
Engineering
Passing a dagger.Client
instance around when making pipelines is no longer required since there’s now a globally available one. By switching to the global client, your code will be less verbose and simpler to reuse. In this post, we’ll explore the global client feature with Python, Typescript, and Go SDK examples.
Background
The usual first step for building a Dagger pipeline is to establish a connection, which returns a dagger.Client
instance that must be passed around to access the API:
import anyio
import dagger
def base(client: dagger.Client) -> dagger.Container:
return client.container().from_("alpine")
async def test(client: dagger.Client) -> str:
return await base(client).with_exec(["echo", "hello world"]).stdout()
async def main():
async with dagger.Connection() as client:
out = await test(client)
print(out)
anyio.run(main)
In larger projects, this can become quite cumbersome, especially when using a tool like Click, that creates a command line interface from functions in code, since one has to conform to the tool’s API to pass the client instance around, while making sure the connection isn’t closed prematurely (for example, by exiting the with
block).
The global client: dag
Users usually only need one client connection during a dagger run
call. We can simplify by providing a global client instance that can be referred from anywhere.
The Python SDK introduced this as an experimental opt-in feature back in v0.8.5 until its maturity in v0.9.4 where the name dag
was adopted as a convention for the global client instance across SDKs.
Basic usage:
import dagger
from dagger import dag
async def main():
async with dagger.connection():
src = dag.host().directory(".")
...
Notice the difference in casing:
dagger.Connection
(older class): Returns a connecteddagger.Client
instancedagger.connection
(new function): Establishes a connection in the globaldag
client instance
Example
We can now simplify the initial example:
import anyio
import dagger
from dagger import dag
def base() -> dagger.Container:
return dag.container().from_("alpine")
async def test() -> str:
return await base().with_exec(["echo", "hello world"]).stdout()
async def main():
async with dagger.connection():
out = await test()
print(out)
anyio.run(main)
In summary:
Import
dag
Replace
dagger.Connection() as client
withdagger.connection()
Remove
client
from function argumentsRename remaining
client
references todag
Simplified utility factories
In a lot of cases with
utilities need to be wrapped in a factory function to pass a single client instance argument:
def base(client: dagger.Client):
return client.container().with_(source(client))
def source(client: dagger.Client):
def _source(ctr: dagger.Container):
src = client.host().directory("src")
return ctr.with_directory("/src", src)
return _source
With the global client, a factory is no longer necessary:
def base():
return dag.container().with_(source)
def source(ctr: dagger.Container):
src = dag.host().directory("src")
return ctr.with_directory("/src", src)
Simplified reusability
For lazy API fields (those that don’t require an await
), having an established connection first is no longer necessary, which can make it much simpler to manage the pipeline setup in certain use cases:
import os
import anyio
import dagger
from dagger import dag
async def main():
token = dag.set_secret("token", os.getenv("API_TOKEN"))
src = dag.host().directory("src")
ctr = (
dag.container()
.from_("alpine")
.with_secret_variable("token", token)
.with_directory("/src", src)
.with_exec([...])
)
# only need a connection when making requests
async with dagger.connection():
out = await ctr.stdout()
print(out)
anyio.run(main)
Lazy connection
If a dagger.Config
isn’t needed (and it mostly isn’t when using dagger run
), the global client can also automatically connect when it needs to make the first request. Removing the need to connect explicitly can further simplify some setups, but you need to make sure to close the connection in the end cleanly:
import anyio
import dagger
from dagger import dag
async def run():
ctr = (
dag.container()
.from_("alpine")
.with_exec(["cat", "/etc/alpine-release"])
)
print(await ctr.stdout()) # automatically connected here
async def main():
try:
await run()
finally:
# make sure `close` is called
await dagger.close()
anyio.run(main)
Notice that you must guard against exceptions that may be raised by the pipeline anyway; otherwise, dagger.close()
won’t be called.
Another downside is that if there’s an error during connection, it’s possible that it’s being made during an implicit conversion from object to ID that the SDKs make internally (for example, a Directory
instance in Container.with_directory
to a DirectoryID
required by the API) so you may be confused by the error message’s origin.
For those reasons, we recommend being more explicit and simply using a with dagger.connection()
block as in the previous examples.
Typescript SDK
The global client has been available in the Typescript SDK since v0.9.4 via dagger.connection()
:
import { dag } from '@dagger.io/dagger'
import * as dagger from '@dagger.io/dagger'
await dagger.connection(async () => {
const out = await dag
.container()
.from("alpine")
.withExec(["echo"," hello world"])
.stdout()
console.log(out)
})
In summary:
Import
dag
Replace
connect
withdagger.connection
Remove
client
from function argumentsRename remaining
client
references todag
To connect lazily, you can replace dagger.connection(main)
with a manual call to dagger.close()
as well:
main().finally(() => dagger.close())
Go SDK
The Go SDK added support more recently in v0.9.6:
package main
import (
"context"
"fmt"
"dagger.io/dagger/dag"
)
func main() {
ctx := context.Background()
defer dag.Close()
out, err := dag.
Container().
From("alpine").
WithExec([]string{"echo", "hello world"}).
Stdout(ctx)
if err != nil {
panic(err)
}
fmt.Println(out
In Go, there’s no way to pass configuration for the connection (you probably don't need it anyway if using dagger run
). It only supports a lazy connection, but the ergonomics for closing it is the same (with defer dag.Close()
).
In summary:
Import
dag
Remove
dagger.Connection(ctx)
Replace
client.Close()
withdag.Close()
Remove
client
from function arguments;Rename remaining
client
references todag
Conclusion
We’ve seen that the Python, Typescript, and Go SDKs have a new convention for a global client under the name dag
, which can be called from anywhere, even before establishing a connection. As well as how adopting it can reduce verbosity and help simplify the setup for running pipelines in non-trivial cases where greater care needs to be taken to manage the client connection to the Dagger API.
In addition, our upcoming Zenith release will feature support for what we call Dagger modules (currently available as experimental), which use the global client in all SDKs but where the connection is managed for you. So, adopting the global client now will also make it much easier to adapt to a module later.
Check out the global client today, and let us know if you have any feedback! If you get stuck, have feedback to share, or want to discuss your use case, chat with us on Discord.
Passing a dagger.Client
instance around when making pipelines is no longer required since there’s now a globally available one. By switching to the global client, your code will be less verbose and simpler to reuse. In this post, we’ll explore the global client feature with Python, Typescript, and Go SDK examples.
Background
The usual first step for building a Dagger pipeline is to establish a connection, which returns a dagger.Client
instance that must be passed around to access the API:
import anyio
import dagger
def base(client: dagger.Client) -> dagger.Container:
return client.container().from_("alpine")
async def test(client: dagger.Client) -> str:
return await base(client).with_exec(["echo", "hello world"]).stdout()
async def main():
async with dagger.Connection() as client:
out = await test(client)
print(out)
anyio.run(main)
In larger projects, this can become quite cumbersome, especially when using a tool like Click, that creates a command line interface from functions in code, since one has to conform to the tool’s API to pass the client instance around, while making sure the connection isn’t closed prematurely (for example, by exiting the with
block).
The global client: dag
Users usually only need one client connection during a dagger run
call. We can simplify by providing a global client instance that can be referred from anywhere.
The Python SDK introduced this as an experimental opt-in feature back in v0.8.5 until its maturity in v0.9.4 where the name dag
was adopted as a convention for the global client instance across SDKs.
Basic usage:
import dagger
from dagger import dag
async def main():
async with dagger.connection():
src = dag.host().directory(".")
...
Notice the difference in casing:
dagger.Connection
(older class): Returns a connecteddagger.Client
instancedagger.connection
(new function): Establishes a connection in the globaldag
client instance
Example
We can now simplify the initial example:
import anyio
import dagger
from dagger import dag
def base() -> dagger.Container:
return dag.container().from_("alpine")
async def test() -> str:
return await base().with_exec(["echo", "hello world"]).stdout()
async def main():
async with dagger.connection():
out = await test()
print(out)
anyio.run(main)
In summary:
Import
dag
Replace
dagger.Connection() as client
withdagger.connection()
Remove
client
from function argumentsRename remaining
client
references todag
Simplified utility factories
In a lot of cases with
utilities need to be wrapped in a factory function to pass a single client instance argument:
def base(client: dagger.Client):
return client.container().with_(source(client))
def source(client: dagger.Client):
def _source(ctr: dagger.Container):
src = client.host().directory("src")
return ctr.with_directory("/src", src)
return _source
With the global client, a factory is no longer necessary:
def base():
return dag.container().with_(source)
def source(ctr: dagger.Container):
src = dag.host().directory("src")
return ctr.with_directory("/src", src)
Simplified reusability
For lazy API fields (those that don’t require an await
), having an established connection first is no longer necessary, which can make it much simpler to manage the pipeline setup in certain use cases:
import os
import anyio
import dagger
from dagger import dag
async def main():
token = dag.set_secret("token", os.getenv("API_TOKEN"))
src = dag.host().directory("src")
ctr = (
dag.container()
.from_("alpine")
.with_secret_variable("token", token)
.with_directory("/src", src)
.with_exec([...])
)
# only need a connection when making requests
async with dagger.connection():
out = await ctr.stdout()
print(out)
anyio.run(main)
Lazy connection
If a dagger.Config
isn’t needed (and it mostly isn’t when using dagger run
), the global client can also automatically connect when it needs to make the first request. Removing the need to connect explicitly can further simplify some setups, but you need to make sure to close the connection in the end cleanly:
import anyio
import dagger
from dagger import dag
async def run():
ctr = (
dag.container()
.from_("alpine")
.with_exec(["cat", "/etc/alpine-release"])
)
print(await ctr.stdout()) # automatically connected here
async def main():
try:
await run()
finally:
# make sure `close` is called
await dagger.close()
anyio.run(main)
Notice that you must guard against exceptions that may be raised by the pipeline anyway; otherwise, dagger.close()
won’t be called.
Another downside is that if there’s an error during connection, it’s possible that it’s being made during an implicit conversion from object to ID that the SDKs make internally (for example, a Directory
instance in Container.with_directory
to a DirectoryID
required by the API) so you may be confused by the error message’s origin.
For those reasons, we recommend being more explicit and simply using a with dagger.connection()
block as in the previous examples.
Typescript SDK
The global client has been available in the Typescript SDK since v0.9.4 via dagger.connection()
:
import { dag } from '@dagger.io/dagger'
import * as dagger from '@dagger.io/dagger'
await dagger.connection(async () => {
const out = await dag
.container()
.from("alpine")
.withExec(["echo"," hello world"])
.stdout()
console.log(out)
})
In summary:
Import
dag
Replace
connect
withdagger.connection
Remove
client
from function argumentsRename remaining
client
references todag
To connect lazily, you can replace dagger.connection(main)
with a manual call to dagger.close()
as well:
main().finally(() => dagger.close())
Go SDK
The Go SDK added support more recently in v0.9.6:
package main
import (
"context"
"fmt"
"dagger.io/dagger/dag"
)
func main() {
ctx := context.Background()
defer dag.Close()
out, err := dag.
Container().
From("alpine").
WithExec([]string{"echo", "hello world"}).
Stdout(ctx)
if err != nil {
panic(err)
}
fmt.Println(out
In Go, there’s no way to pass configuration for the connection (you probably don't need it anyway if using dagger run
). It only supports a lazy connection, but the ergonomics for closing it is the same (with defer dag.Close()
).
In summary:
Import
dag
Remove
dagger.Connection(ctx)
Replace
client.Close()
withdag.Close()
Remove
client
from function arguments;Rename remaining
client
references todag
Conclusion
We’ve seen that the Python, Typescript, and Go SDKs have a new convention for a global client under the name dag
, which can be called from anywhere, even before establishing a connection. As well as how adopting it can reduce verbosity and help simplify the setup for running pipelines in non-trivial cases where greater care needs to be taken to manage the client connection to the Dagger API.
In addition, our upcoming Zenith release will feature support for what we call Dagger modules (currently available as experimental), which use the global client in all SDKs but where the connection is managed for you. So, adopting the global client now will also make it much easier to adapt to a module later.
Check out the global client today, and let us know if you have any feedback! If you get stuck, have feedback to share, or want to discuss your use case, chat with us on Discord.
Passing a dagger.Client
instance around when making pipelines is no longer required since there’s now a globally available one. By switching to the global client, your code will be less verbose and simpler to reuse. In this post, we’ll explore the global client feature with Python, Typescript, and Go SDK examples.
Background
The usual first step for building a Dagger pipeline is to establish a connection, which returns a dagger.Client
instance that must be passed around to access the API:
import anyio
import dagger
def base(client: dagger.Client) -> dagger.Container:
return client.container().from_("alpine")
async def test(client: dagger.Client) -> str:
return await base(client).with_exec(["echo", "hello world"]).stdout()
async def main():
async with dagger.Connection() as client:
out = await test(client)
print(out)
anyio.run(main)
In larger projects, this can become quite cumbersome, especially when using a tool like Click, that creates a command line interface from functions in code, since one has to conform to the tool’s API to pass the client instance around, while making sure the connection isn’t closed prematurely (for example, by exiting the with
block).
The global client: dag
Users usually only need one client connection during a dagger run
call. We can simplify by providing a global client instance that can be referred from anywhere.
The Python SDK introduced this as an experimental opt-in feature back in v0.8.5 until its maturity in v0.9.4 where the name dag
was adopted as a convention for the global client instance across SDKs.
Basic usage:
import dagger
from dagger import dag
async def main():
async with dagger.connection():
src = dag.host().directory(".")
...
Notice the difference in casing:
dagger.Connection
(older class): Returns a connecteddagger.Client
instancedagger.connection
(new function): Establishes a connection in the globaldag
client instance
Example
We can now simplify the initial example:
import anyio
import dagger
from dagger import dag
def base() -> dagger.Container:
return dag.container().from_("alpine")
async def test() -> str:
return await base().with_exec(["echo", "hello world"]).stdout()
async def main():
async with dagger.connection():
out = await test()
print(out)
anyio.run(main)
In summary:
Import
dag
Replace
dagger.Connection() as client
withdagger.connection()
Remove
client
from function argumentsRename remaining
client
references todag
Simplified utility factories
In a lot of cases with
utilities need to be wrapped in a factory function to pass a single client instance argument:
def base(client: dagger.Client):
return client.container().with_(source(client))
def source(client: dagger.Client):
def _source(ctr: dagger.Container):
src = client.host().directory("src")
return ctr.with_directory("/src", src)
return _source
With the global client, a factory is no longer necessary:
def base():
return dag.container().with_(source)
def source(ctr: dagger.Container):
src = dag.host().directory("src")
return ctr.with_directory("/src", src)
Simplified reusability
For lazy API fields (those that don’t require an await
), having an established connection first is no longer necessary, which can make it much simpler to manage the pipeline setup in certain use cases:
import os
import anyio
import dagger
from dagger import dag
async def main():
token = dag.set_secret("token", os.getenv("API_TOKEN"))
src = dag.host().directory("src")
ctr = (
dag.container()
.from_("alpine")
.with_secret_variable("token", token)
.with_directory("/src", src)
.with_exec([...])
)
# only need a connection when making requests
async with dagger.connection():
out = await ctr.stdout()
print(out)
anyio.run(main)
Lazy connection
If a dagger.Config
isn’t needed (and it mostly isn’t when using dagger run
), the global client can also automatically connect when it needs to make the first request. Removing the need to connect explicitly can further simplify some setups, but you need to make sure to close the connection in the end cleanly:
import anyio
import dagger
from dagger import dag
async def run():
ctr = (
dag.container()
.from_("alpine")
.with_exec(["cat", "/etc/alpine-release"])
)
print(await ctr.stdout()) # automatically connected here
async def main():
try:
await run()
finally:
# make sure `close` is called
await dagger.close()
anyio.run(main)
Notice that you must guard against exceptions that may be raised by the pipeline anyway; otherwise, dagger.close()
won’t be called.
Another downside is that if there’s an error during connection, it’s possible that it’s being made during an implicit conversion from object to ID that the SDKs make internally (for example, a Directory
instance in Container.with_directory
to a DirectoryID
required by the API) so you may be confused by the error message’s origin.
For those reasons, we recommend being more explicit and simply using a with dagger.connection()
block as in the previous examples.
Typescript SDK
The global client has been available in the Typescript SDK since v0.9.4 via dagger.connection()
:
import { dag } from '@dagger.io/dagger'
import * as dagger from '@dagger.io/dagger'
await dagger.connection(async () => {
const out = await dag
.container()
.from("alpine")
.withExec(["echo"," hello world"])
.stdout()
console.log(out)
})
In summary:
Import
dag
Replace
connect
withdagger.connection
Remove
client
from function argumentsRename remaining
client
references todag
To connect lazily, you can replace dagger.connection(main)
with a manual call to dagger.close()
as well:
main().finally(() => dagger.close())
Go SDK
The Go SDK added support more recently in v0.9.6:
package main
import (
"context"
"fmt"
"dagger.io/dagger/dag"
)
func main() {
ctx := context.Background()
defer dag.Close()
out, err := dag.
Container().
From("alpine").
WithExec([]string{"echo", "hello world"}).
Stdout(ctx)
if err != nil {
panic(err)
}
fmt.Println(out
In Go, there’s no way to pass configuration for the connection (you probably don't need it anyway if using dagger run
). It only supports a lazy connection, but the ergonomics for closing it is the same (with defer dag.Close()
).
In summary:
Import
dag
Remove
dagger.Connection(ctx)
Replace
client.Close()
withdag.Close()
Remove
client
from function arguments;Rename remaining
client
references todag
Conclusion
We’ve seen that the Python, Typescript, and Go SDKs have a new convention for a global client under the name dag
, which can be called from anywhere, even before establishing a connection. As well as how adopting it can reduce verbosity and help simplify the setup for running pipelines in non-trivial cases where greater care needs to be taken to manage the client connection to the Dagger API.
In addition, our upcoming Zenith release will feature support for what we call Dagger modules (currently available as experimental), which use the global client in all SDKs but where the connection is managed for you. So, adopting the global client now will also make it much easier to adapt to a module later.
Check out the global client today, and let us know if you have any feedback! If you get stuck, have feedback to share, or want to discuss your use case, chat with us on Discord.