Armada Rust Client
Rust gRPC client for the Armada batch job scheduler.
Provides two operations:
submit— submit a batch of jobs to a queuewatch— stream events for a job set
Prerequisites
| Tool | Purpose | Install |
|---|---|---|
| Rust ≥ 1.85 | Build toolchain | curl https://sh.rustup.rs | sh |
protoc |
Proto code generation at build time | brew install protobuf (macOS) / apt install protobuf-compiler (Linux) |
Verify both are available:
Building
The crate lives inside the Armada monorepo. It does not participate in a Cargo workspace — all commands use --manifest-path.
Step 1 — fetch vendored proto files
build.rs requires k8s and Google API proto files that are not committed to the repository. Fetch them once before the first build (and whenever you want to update them):
# from the repo root
# kubernetes/api
# kubernetes/apimachinery
# google/api annotations
Step 2 — build
# from the repo root
What happens during cargo build:
build.rsrunstonic-buildin two passes:- Pass 1 — compiles vendored k8s protos (
client/rust/proto/k8s.io/…) and generates their Rust types into$OUT_DIR. - Pass 2 — compiles the Armada API protos (
pkg/api/*.proto) withextern_pathentries that redirect k8s type references to the types generated in Pass 1, avoiding duplicate definitions.
- Pass 1 — compiles vendored k8s protos (
- The generated
.rsfiles are included into the crate viatonic::include_proto!insrc/lib.rs.
Running Tests
Expected output:
running 12 tests
test auth::tests::basic_provider_returns_basic_header ... ok
test auth::tests::static_provider_empty_token_returns_empty ... ok
test auth::tests::static_provider_returns_bearer_header ... ok
test builder::tests::builder_with_pod_specs_builds_correctly ... ok
test builder::tests::label_and_annotation_helpers ... ok
test builder::tests::optional_fields_default_to_empty ... ok
test builder::tests::pod_spec_called_twice_replaces_previous ... ok
test builder::tests::pod_spec_singular_shorthand ... ok
test error::tests::auth_constructor ... ok
test error::tests::from_invalid_metadata ... ok
test error::tests::from_status ... ok
test error::tests::invalid_uri_holds_message ... ok
test result: ok. 12 passed; 0 failed
Doc-tests are disabled (doctest = false) because the vendored k8s and google API proto files contain Go/YAML syntax in their doc comments that is not valid Rust.
Running the Examples
Two runnable examples live in examples/. Both read configuration from environment variables.
| Variable | Default | Description |
|---|---|---|
ARMADA_ENDPOINT |
http://localhost:50051 |
gRPC endpoint |
ARMADA_TOKEN |
(empty) | Bearer token; leave empty for unauthenticated clusters |
ARMADA_QUEUE |
test |
Queue name (must exist) |
ARMADA_JOB_SET |
rust-smoke-test |
Job set ID (arbitrary string) |
1. Create a queue
Use the Armada REST API (port 8080 / NodePort 30001 in kind):
2. Submit jobs
Submits one job and prints the assigned job ID, then prints the command to watch it:
ARMADA_ENDPOINT=http://localhost:30002 \
ARMADA_QUEUE=rust-test \
ARMADA_JOB_SET=rust-smoke- \
Expected output:
Submitting job to queue 'rust-test', job set 'rust-smoke-1234567890'...
Submitted 1 job(s):
job_id=01kjsxm5ksstr57prfqjtgakq0
To watch this job set:
ARMADA_ENDPOINT=http://localhost:30002 ARMADA_QUEUE=rust-test ARMADA_JOB_SET=rust-smoke-1234567890 \
cargo run --manifest-path client/rust/Cargo.toml --example watch
3. Watch a job set
Streams events for a job set until the server closes the connection (Ctrl-C to exit):
ARMADA_ENDPOINT=http://localhost:30002 \
ARMADA_QUEUE=rust-test \
ARMADA_JOB_SET=rust-smoke-1234567890 \
Expected output:
Watching job set 'rust-smoke-1234567890' on queue 'rust-test'...
event id=… message=Some(EventMessage { events: Some(Submitted(…)) })
event id=… message=Some(EventMessage { events: Some(Queued(…)) })
event id=… message=Some(EventMessage { events: Some(Leased(…)) })
event id=… message=Some(EventMessage { events: Some(Pending(…)) })
event id=… message=Some(EventMessage { events: Some(Running(…)) })
event id=… message=Some(EventMessage { events: Some(Succeeded(…)) })
Using the Client in Your Code
Add the crate as a path dependency (until it is published to crates.io):
[]
= { = "../armada/client/rust" }
= { = "1", = ["rt-multi-thread", "macros"] }
= "0.3"
Connect
use ;
// Plaintext (development / in-cluster without TLS)
let client = connect.await?;
// TLS (production — uses system root certificates)
let client = connect_tls.await?;
// Optional: apply a deadline to every RPC
use Duration;
let client = client.with_timeout;
ArmadaClient is Clone — all clones share the same underlying channel, so cloning is the right way to distribute the client across tasks.
Submit jobs
use ;
use ;
use Quantity;
// k8s protos use proto2 syntax — optional fields are Option<T>
let cpu = Quantity ;
let memory = Quantity ;
let container = Container ;
let pod_spec = PodSpec ;
// Typestate builder: .build() only compiles after .pod_spec() / .pod_specs() is called
let item = new
.namespace
.priority
.label
.pod_spec
.build;
let response = client.submit.await?;
for r in &response.job_response_items
Watch a job set
use StreamExt;
let mut stream = client.watch.await?;
while let Some = stream.next.await
Pass a from_message_id to resume from a known cursor after reconnecting:
let mut stream = client.watch.await?;
API Reference
ArmadaClient
| Method | Signature | Description |
|---|---|---|
connect |
(endpoint, provider) -> Result<Self> |
Open a plaintext gRPC channel |
connect_tls |
(endpoint, provider) -> Result<Self> |
Open a TLS channel (system roots) |
with_timeout |
(Duration) -> Self |
Set a per-call deadline (chainable) |
submit |
(JobSubmitRequest) -> Result<JobSubmitResponse> |
Submit one or more jobs |
watch |
(queue, job_set_id, from_message_id) -> Result<BoxStream<…>> |
Stream job set events |
JobRequestItemBuilder<S>
Typestate builder. All setters are available in any state. .build() requires the HasPodSpec state, which is entered by calling .pod_spec(spec) or .pod_specs(specs).
| Setter | Type | Default |
|---|---|---|
.namespace(s) |
impl Into<String> |
"" |
.priority(p) |
f64 |
0.0 |
.client_id(s) |
impl Into<String> |
"" |
.label(k, v) |
impl Into<String> × 2 |
— adds one label |
.labels(m) |
HashMap<String, String> |
empty |
.annotation(k, v) |
impl Into<String> × 2 |
— adds one annotation |
.annotations(m) |
HashMap<String, String> |
empty |
.scheduler(s) |
impl Into<String> |
"" |
.add_ingress(i) |
IngressConfig |
— appends one config |
.ingress(v) |
Vec<IngressConfig> |
empty |
.add_service(s) |
ServiceConfig |
— appends one config |
.services(v) |
Vec<ServiceConfig> |
empty |
.pod_spec(s) |
PodSpec |
— transitions to HasPodSpec |
.pod_specs(v) |
Vec<PodSpec> |
— transitions to HasPodSpec |
StaticTokenProvider
Implements TokenProvider with a fixed token string. Its Debug output redacts the token value.
let p = new;
println!; // StaticTokenProvider { token: "[redacted]" }
Error
| Variant | Cause |
|---|---|
Transport(tonic::transport::Error) |
TCP/TLS connection failure |
Grpc(Box<tonic::Status>) |
Server returned a non-OK gRPC status |
Auth(String) |
Token provider returned an error |
InvalidUri(String) |
Malformed endpoint string |
InvalidMetadata(…) |
Token contains characters invalid in HTTP headers |
Project Structure
client/rust/
├── build.rs # tonic-build: two-pass proto compilation
├── Cargo.toml
├── proto/ # Vendored protos (k8s + google.api)
│ ├── google/api/ # google.api HTTP annotations
│ └── k8s.io/ # k8s API + apimachinery types
├── src/
│ ├── lib.rs # Module layout + public re-exports
│ ├── auth.rs # TokenProvider trait, StaticTokenProvider
│ ├── builder.rs # JobRequestItemBuilder (typestate)
│ ├── client.rs # ArmadaClient
│ └── error.rs # Error enum
└── examples/
├── submit.rs # Submit a job and print the job ID
└── watch.rs # Stream events for a job set
CI
GitHub Actions workflow: .github/workflows/rust-client.yml
Runs on every push/PR that touches client/rust/**:
cargo fmt -- --checkcargo buildcargo testcargo clippy -- -D warnings
protoc is installed in CI via arduino/setup-protoc@v3 before the build step.