Skip to content

Commit 2ea6a75

Browse files
feat: add vz-stack crate with typed spec model and sqlite state store (B16)
New crate providing the stack control plane foundation: - StackSpec/ServiceSpec/NetworkSpec/VolumeSpec typed domain model - SQLite-backed StateStore for desired state, observed state, and events - No-op apply() entrypoint that persists state and emits lifecycle events - 26 tests covering round-trip serialization, persistence, idempotency, and event streaming Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bf44ecb commit 2ea6a75

9 files changed

Lines changed: 1256 additions & 0 deletions

File tree

crates/Cargo.lock

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ members = [
77
"vz-guest-agent",
88
"vz-linux",
99
"vz-oci",
10+
"vz-stack",
1011
]
1112

1213
[workspace.package]

crates/vz-stack/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "vz-stack"
3+
version = "0.1.0"
4+
description = "Stack control plane for multi-service OCI workloads"
5+
edition.workspace = true
6+
rust-version.workspace = true
7+
license.workspace = true
8+
repository.workspace = true
9+
10+
[dependencies]
11+
serde = { workspace = true }
12+
serde_json = { workspace = true }
13+
thiserror = { workspace = true }
14+
tracing = { workspace = true }
15+
rusqlite = { version = "0.34", features = ["bundled"] }
16+
17+
[lints]
18+
workspace = true

crates/vz-stack/src/error.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// Errors produced by `vz-stack` operations.
2+
#[derive(Debug, thiserror::Error)]
3+
pub enum StackError {
4+
/// State store operation failed.
5+
#[error("state store error: {0}")]
6+
Store(#[from] rusqlite::Error),
7+
8+
/// Serialization or deserialization failed.
9+
#[error("serialization error: {0}")]
10+
Serialization(#[from] serde_json::Error),
11+
12+
/// Invalid stack specification.
13+
#[error("invalid stack spec: {0}")]
14+
InvalidSpec(String),
15+
}

crates/vz-stack/src/events.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//! Structured event types for stack lifecycle observability.
2+
3+
use serde::{Deserialize, Serialize};
4+
5+
/// Structured event emitted during stack lifecycle operations.
6+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7+
#[serde(tag = "type")]
8+
pub enum StackEvent {
9+
/// Reconciliation started for a stack.
10+
#[serde(rename = "stack_apply_started")]
11+
StackApplyStarted {
12+
/// Stack name.
13+
stack_name: String,
14+
/// Number of services in the spec.
15+
services_count: usize,
16+
},
17+
/// Reconciliation completed for a stack.
18+
#[serde(rename = "stack_apply_completed")]
19+
StackApplyCompleted {
20+
/// Stack name.
21+
stack_name: String,
22+
/// Number of actions that succeeded.
23+
succeeded: usize,
24+
/// Number of actions that failed.
25+
failed: usize,
26+
},
27+
/// Reconciliation failed for a stack.
28+
#[serde(rename = "stack_apply_failed")]
29+
StackApplyFailed {
30+
/// Stack name.
31+
stack_name: String,
32+
/// Error description.
33+
error: String,
34+
},
35+
/// A service is being created.
36+
#[serde(rename = "service_creating")]
37+
ServiceCreating {
38+
/// Stack name.
39+
stack_name: String,
40+
/// Service name.
41+
service_name: String,
42+
},
43+
/// A service is ready and running.
44+
#[serde(rename = "service_ready")]
45+
ServiceReady {
46+
/// Stack name.
47+
stack_name: String,
48+
/// Service name.
49+
service_name: String,
50+
/// Runtime container identifier.
51+
runtime_id: String,
52+
},
53+
/// A service failed to start or crashed.
54+
#[serde(rename = "service_failed")]
55+
ServiceFailed {
56+
/// Stack name.
57+
stack_name: String,
58+
/// Service name.
59+
service_name: String,
60+
/// Error description.
61+
error: String,
62+
},
63+
}
64+
65+
#[cfg(test)]
66+
mod tests {
67+
#![allow(clippy::unwrap_used)]
68+
69+
use super::*;
70+
71+
#[test]
72+
fn event_round_trip() {
73+
let events = vec![
74+
StackEvent::StackApplyStarted {
75+
stack_name: "myapp".to_string(),
76+
services_count: 3,
77+
},
78+
StackEvent::StackApplyCompleted {
79+
stack_name: "myapp".to_string(),
80+
succeeded: 2,
81+
failed: 1,
82+
},
83+
StackEvent::ServiceReady {
84+
stack_name: "myapp".to_string(),
85+
service_name: "web".to_string(),
86+
runtime_id: "ctr-123".to_string(),
87+
},
88+
];
89+
90+
for event in events {
91+
let json = serde_json::to_string(&event).unwrap();
92+
let deserialized: StackEvent = serde_json::from_str(&json).unwrap();
93+
assert_eq!(deserialized, event);
94+
}
95+
}
96+
97+
#[test]
98+
fn event_tag_serialization() {
99+
let event = StackEvent::StackApplyStarted {
100+
stack_name: "test".to_string(),
101+
services_count: 1,
102+
};
103+
let json = serde_json::to_string(&event).unwrap();
104+
assert!(json.contains("\"type\":\"stack_apply_started\""));
105+
}
106+
}

crates/vz-stack/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//! Stack control plane for multi-service OCI workloads.
2+
//!
3+
//! Provides a typed [`StackSpec`] model, a durable SQLite-backed
4+
//! [`StateStore`], and an [`apply`] entrypoint that persists desired
5+
//! and observed state for idempotent reconciliation.
6+
7+
#![forbid(unsafe_code)]
8+
9+
mod error;
10+
mod events;
11+
mod reconcile;
12+
mod spec;
13+
mod state_store;
14+
15+
pub use error::StackError;
16+
pub use events::StackEvent;
17+
pub use reconcile::{ApplyResult, apply};
18+
pub use spec::{
19+
HealthCheckSpec, MountSpec, NetworkSpec, PortSpec, ResourcesSpec, RestartPolicy, ServiceSpec,
20+
StackSpec, VolumeSpec,
21+
};
22+
pub use state_store::{ServiceObservedState, ServicePhase, StateStore};

0 commit comments

Comments
 (0)