Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ same for REST workers, dashboard reads, and Python submitters; future Postgres
or managed/cloud storage backends can replace the same storage boundary without
changing submitter contracts.

Cloud-ready backend, artifact, worker identity, and scheduler extension
contracts are documented in
[docs/extension-contracts.md](docs/extension-contracts.md).

## Release

The release workflow builds Linux, Windows, and macOS wheels plus an sdist. PyPI publishing uses Trusted Publishing through the `pypi` GitHub environment.
Expand Down
14 changes: 14 additions & 0 deletions crates/farm-core/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,8 @@ pub struct WorkerRegister {
pub labels: HashMap<String, String>,
#[serde(default)]
pub capacity: WorkerCapacity,
#[serde(default)]
pub identity: Option<WorkerIdentity>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -634,11 +636,23 @@ pub struct WorkerInfo {
pub name: String,
pub labels: HashMap<String, String>,
pub capacity: WorkerCapacity,
#[serde(default)]
pub identity: Option<WorkerIdentity>,
pub state: WorkerState,
pub registered_at: DateTime<Utc>,
pub last_seen_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerIdentity {
pub provider: String,
pub subject: String,
#[serde(default)]
pub attributes: HashMap<String, String>,
#[serde(default)]
pub expires_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerLogBatch {
#[serde(default)]
Expand Down
57 changes: 56 additions & 1 deletion crates/farm-core/src/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ impl InMemoryScheduler {
name: registration.name,
labels: registration.labels,
capacity: normalize_capacity(registration.capacity),
identity: registration.identity,
state: WorkerState::Online,
registered_at: now,
last_seen_at: now,
Expand Down Expand Up @@ -1782,7 +1783,7 @@ mod tests {
ArtifactKind, AuditEventInput, AuditOutcome, CommandSpec, LogLevel,
OpenJdAmountRequirement, OpenJdAttributeRequirement, ResourceLimitDefinition, TaskArtifact,
TaskAttemptState, TaskLease, TaskLeaseRenewal, TaskRequirements, TaskStarted,
WorkerCapacity, WorkerId, WorkerInfo, WorkerLogBatch, WorkerLogInput,
WorkerCapacity, WorkerId, WorkerIdentity, WorkerInfo, WorkerLogBatch, WorkerLogInput,
};

use super::*;
Expand Down Expand Up @@ -1825,6 +1826,7 @@ mod tests {
name: "local".to_string(),
labels: HashMap::new(),
capacity: WorkerCapacity::default(),
identity: None,
})
.expect("worker should register");

Expand Down Expand Up @@ -1880,6 +1882,7 @@ mod tests {
name: "render-node-01".to_string(),
labels: HashMap::new(),
capacity: WorkerCapacity { slots: 4 },
identity: None,
})
.expect("worker should register");

Expand All @@ -1896,6 +1899,36 @@ mod tests {
assert_eq!(snapshot.workers[0].name, "render-node-01");
}

#[test]
fn worker_registration_preserves_optional_identity() {
let scheduler = InMemoryScheduler::default();
let expires_at = Utc::now() + Duration::minutes(30);
let identity = WorkerIdentity {
provider: "aws-sts".to_string(),
subject: "i-0123456789abcdef0".to_string(),
attributes: HashMap::from([("region".to_string(), "us-west-2".to_string())]),
expires_at: Some(expires_at),
};

let worker = scheduler
.register_worker(WorkerRegister {
name: "cloud-worker".to_string(),
labels: HashMap::from([("pool".to_string(), "burst".to_string())]),
capacity: WorkerCapacity { slots: 8 },
identity: Some(identity.clone()),
})
.expect("worker should register");

assert_eq!(worker.identity.as_ref(), Some(&identity));
let listed_worker = scheduler
.list_workers()
.expect("workers should list")
.pop()
.expect("worker should be present");
assert_eq!(listed_worker.identity.as_ref(), Some(&identity));
assert_eq!(listed_worker.capacity.slots, 8);
}

#[test]
fn metrics_snapshot_reports_operational_counters() {
let scheduler = InMemoryScheduler::default();
Expand Down Expand Up @@ -2025,11 +2058,18 @@ mod tests {
openjd: None,
})
.expect("job should submit");
let identity = WorkerIdentity {
provider: "cloud-runner".to_string(),
subject: "worker-group/render-node-01".to_string(),
attributes: HashMap::from([("lifecycle".to_string(), "spot".to_string())]),
expires_at: Some(Utc::now() + Duration::minutes(15)),
};
let worker = scheduler
.register_worker(WorkerRegister {
name: "render-node-01".to_string(),
labels: HashMap::new(),
capacity: WorkerCapacity { slots: 2 },
identity: Some(identity.clone()),
})
.expect("worker should register");
let lease = scheduler
Expand Down Expand Up @@ -2057,6 +2097,10 @@ mod tests {
assert_eq!(restored.tasks[0].state, TaskState::Pending);
assert_eq!(restored.tasks[0].stdout_tail.as_deref(), Some("stdout"));
assert_eq!(reopened.list_workers().unwrap()[0].name, "render-node-01");
assert_eq!(
reopened.list_workers().unwrap()[0].identity.as_ref(),
Some(&identity)
);
let _ = std::fs::remove_file(database_path);
}

Expand Down Expand Up @@ -2142,6 +2186,7 @@ mod tests {
name: "durable-worker".to_string(),
labels: HashMap::new(),
capacity: WorkerCapacity::default(),
identity: None,
})
.expect("worker should register");
let job = scheduler
Expand Down Expand Up @@ -2218,6 +2263,7 @@ mod tests {
name: "worker".to_string(),
labels: HashMap::new(),
capacity: WorkerCapacity::default(),
identity: None,
})
.expect("worker should register");
let lease = scheduler
Expand Down Expand Up @@ -2276,6 +2322,7 @@ mod tests {
name: "multi-slot".to_string(),
labels: HashMap::new(),
capacity: WorkerCapacity { slots: 2 },
identity: None,
})
.expect("worker should register");

Expand Down Expand Up @@ -2328,6 +2375,7 @@ mod tests {
name: "single-slot".to_string(),
labels: HashMap::new(),
capacity: WorkerCapacity { slots: 1 },
identity: None,
})
.expect("worker should register");

Expand Down Expand Up @@ -2412,6 +2460,7 @@ mod tests {
("pool".to_string(), "lighting".to_string()),
]),
capacity: WorkerCapacity::default(),
identity: None,
})
.expect("worker should register");

Expand Down Expand Up @@ -2449,6 +2498,7 @@ mod tests {
name: "plain-worker".to_string(),
labels: HashMap::new(),
capacity: WorkerCapacity::default(),
identity: None,
})
.expect("worker should register");

Expand Down Expand Up @@ -2494,6 +2544,7 @@ mod tests {
name: "small-linux".to_string(),
labels: HashMap::from([("ATTR.WORKER.OS.FAMILY".to_string(), "linux".to_string())]),
capacity: WorkerCapacity { slots: 1 },
identity: None,
})
.expect("worker should register");
assert!(scheduler
Expand All @@ -2506,6 +2557,7 @@ mod tests {
name: "large-linux".to_string(),
labels: HashMap::from([("attr.worker.os.family".to_string(), "linux".to_string())]),
capacity: WorkerCapacity { slots: 2 },
identity: None,
})
.expect("worker should register");
let lease = scheduler
Expand Down Expand Up @@ -2541,6 +2593,7 @@ mod tests {
name: "local".to_string(),
labels: HashMap::new(),
capacity: WorkerCapacity::default(),
identity: None,
})
.expect("worker should register");

Expand Down Expand Up @@ -2615,6 +2668,7 @@ mod tests {
name: "local".to_string(),
labels: HashMap::new(),
capacity: WorkerCapacity::default(),
identity: None,
})
.expect("worker should register");
let lease = scheduler.lease_task(worker.id).unwrap().unwrap();
Expand Down Expand Up @@ -2767,6 +2821,7 @@ mod tests {
name: "local".to_string(),
labels: HashMap::new(),
capacity: WorkerCapacity::default(),
identity: None,
})
.expect("worker should register")
}
Expand Down
1 change: 1 addition & 0 deletions crates/farm-worker/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ async fn register_worker(
capacity: WorkerCapacity {
slots: args.slots.max(1),
},
identity: None,
})
.send()
.await?
Expand Down
8 changes: 8 additions & 0 deletions dashboard/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,19 @@ export interface ApiWorker {
name: string;
labels: Record<string, string>;
capacity: { slots: number };
identity?: ApiWorkerIdentity | null;
state: "online" | "offline";
registered_at: string;
last_seen_at: string;
}

export interface ApiWorkerIdentity {
provider: string;
subject: string;
attributes?: Record<string, string>;
expires_at?: string | null;
}

export interface FarmLog {
id: string;
timestamp: string;
Expand Down
10 changes: 10 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ Core endpoints:
- `GET /v1/tasks/{task_id}/attempts/{attempt_id}/logs`: read logs for one durable attempt record.
- `GET /v1/tasks/{task_id}/artifacts/{artifact_index}`: download a captured task artifact.

Backend, artifact, identity, and scheduler extension boundaries are defined in
[extension-contracts.md](extension-contracts.md). That note separates stable
client contracts from experimental backend and cloud-provider implementation
points.

Task leases have a configurable controller-side TTL. Workers renew leases while
direct command and OpenJD tasks are still executing, so long-running work is not
dispatched twice while its worker remains healthy. If a worker disappears and no
Expand All @@ -57,6 +62,11 @@ worker. OpenJD `hostRequirements` are carried through the same scheduler path:
standard amount capabilities are read from worker labels or slot capacity, and
standard attribute capabilities are read from worker labels.

Worker registration accepts optional identity metadata for provisioned or
short-lived workers. Labels continue to describe scheduling capabilities;
identity remains provenance and authorization context so the scheduler can keep
task placement independent from cloud providers.

Lifecycle actions are idempotent where repeating the same request is safe, and
invalid state transitions return a conflict response with a descriptive error.

Expand Down
Loading
Loading