foundation_deployment
Trait-based, multi-provider infrastructure deployment. You describe infrastructure
as ordinary Rust types; a type that implements [Deployable] can be deployed
and destroyed uniformly — no YAML, no TOML, no DSL.
- The
Deployablecontract - Why boxed futures (and not
async_traitor RPITIT) - Writing a provider
- State persistence
- Unique underlying mechanics — the Docker case
- Driving a deploy
The Deployable contract
deploy and destroy are async. Each returns a
pub type BoxFuture<'a, T> = ;
which the caller .awaits (or drives on valtron via from_future). instance_id
lets one Deployable describe many instances of the same resource; client
carries the state store and (for HTTP-first providers) an API client.
Why boxed futures
deploy/destroy return a boxed, Send future rather than async fn in
the trait, an #[async_trait] macro, or return-position impl Trait (RPITIT).
That is a deliberate trade:
| Approach | Object-safe (dyn Deployable) |
Send guarantee |
Cost |
|---|---|---|---|
#[async_trait] |
✅ | ✅ (boxes) | proc-macro dep + one alloc/call |
RPITIT -> impl Future + Send |
❌ | inferred (fragile) | zero-alloc |
-> BoxFuture (this crate) |
✅ | ✅ (explicit) | one alloc/call |
For deploy/destroy — coarse-grained, I/O-bound, called rarely — the single
Box::pin allocation is nanoseconds against a millisecond-scale network call, so
the cost is irrelevant. In exchange we get:
- Object safety.
Vec<Box<dyn Deployable<…>>>works, so an orchestrator can hold and deploy a heterogeneous set of resources. RPITIT would forbid this. Sendstated in the type, not inferred — no per-impl "the future isn'tSend" surprises (the fragility that bites RPITIT/async fn-in-trait).- A nameable type you can store in fields,
Vecs, and channels.
Any valtron task or stream can be adapted into a BoxFuture, so this is the one
async currency the deployment layer speaks — no macro, no unstable features.
Writing a provider
Write the real work as an async move block and Box::pin it:
use ;
use ProviderClient;
use FileStateStore;
use SystemDnsResolver;
State persistence
self.store(&client) returns a [NamespacedStore] scoped to NAMESPACE, so
every key is isolated per-deployable. The pattern is:
deployrecords what it created (store.store_typed(&id.to_string(), &output)).destroyreads it back (store.get_typed(&id.to_string())) to know exactly what to tear down, then clears it (store.remove(...)).
store_typed / get_typed / remove are synchronous (they drive the store's
stream internally), so they compose fine inside the async block.
Unique underlying mechanics
ProviderClient carries a TCP+DNS HTTP client (http_client: SimpleHttpClient<R>)
tuned for cloud REST APIs. A provider whose transport is different is free to
use its own client and treat ProviderClient purely as the state-persistence
handle. deploy/destroy are just async blocks — they can dial anything.
The reference case is foundation_deployment_docker's ContainerDeployment.
Docker's Engine API is served over a Unix socket (/var/run/docker.sock),
which does not fit ProviderClient's TCP+DNS client at all. So its impl:
destroy mirrors it: get_typed the container id, stop + remove it over the
same self-built Unix-socket client, then remove the state key.
This is the intended shape whenever the mechanics are unique: own the
transport inside the future, use ProviderClient for state and namespacing.
The Deployable surface stays uniform; how a provider talks to its backend is
entirely its business.
Driving a deploy
Because deploy/destroy are plain futures, callers choose how to run them:
// Inside any async context (e.g. a #[valtron_test] or another future):
let output = deployment.deploy.await?;
// ...
deployment.destroy.await?;
// From sync code, on the valtron pool:
let task = from_future;
// drive `task` to completion via drive_iterator / run_until_next_state
Object safety means you can also fan out over a collection:
let resources: = /* … */;
for r in &resources