1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//! Federation-wide scenario activation model.
//!
//! Tracks which scenario is currently active on a federation and the
//! per-service override state that has been pushed to workspaces. Snapshotting
//! the manifest here (rather than storing only a `scenario_id`) ensures
//! deactivation/rollback still works after the source scenario is edited or
//! removed.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
/// Lifecycle status for a federation-wide activation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FederationScenarioActivationStatus {
/// Scenario is active — workspaces should observe the overrides.
Active,
/// Scenario was deactivated by a user; overrides should be reverted.
Deactivated,
/// Activation failed mid-apply; overrides may be in an inconsistent state.
Failed,
}
impl FederationScenarioActivationStatus {
/// Serialized form used when writing to the database.
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Active => "active",
Self::Deactivated => "deactivated",
Self::Failed => "failed",
}
}
/// Parse a status string from the database.
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s {
"active" => Some(Self::Active),
"deactivated" => Some(Self::Deactivated),
"failed" => Some(Self::Failed),
_ => None,
}
}
}
/// Per-service runtime state for a federation activation.
///
/// One entry per service in the federation. The registry writes these as
/// `pending`, then flips them to `applied` / `failed` as the runtime poll
/// endpoint confirms workspaces have observed the overrides.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerServiceActivationState {
/// Service name (matches `ServiceBoundary.name`).
pub service_name: String,
/// Workspace the service is bound to.
pub workspace_id: Uuid,
/// State machine: `pending` → `applied` | `failed`.
pub status: String,
/// Error message when `status == "failed"`.
#[serde(default)]
pub error: Option<String>,
/// Timestamp the workspace last confirmed the activation.
#[serde(default)]
pub last_observed_at: Option<DateTime<Utc>>,
}
/// Federation-wide scenario activation record.
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
pub struct FederationScenarioActivation {
pub id: Uuid,
pub federation_id: Uuid,
/// Source scenario — nullable because an admin may activate an inline
/// manifest that is not stored in the scenarios table.
pub scenario_id: Option<Uuid>,
/// Human-readable name captured for audit even if `scenario_id` is later
/// nulled out.
pub scenario_name: String,
/// Full manifest JSON snapshot at activation time.
pub manifest_snapshot: serde_json::Value,
/// Per-service overrides applied on top of the manifest; shape is
/// `{ service_name: { ... } }`.
pub service_overrides: serde_json::Value,
/// Lifecycle status. Stored as TEXT in both Postgres and SQLite — see
/// `FederationScenarioActivationStatus::as_str`.
pub status: String,
/// Per-service state; JSON array of `PerServiceActivationState`.
pub per_service_state: serde_json::Value,
pub activated_by: Uuid,
pub activated_at: DateTime<Utc>,
pub deactivated_at: Option<DateTime<Utc>>,
}
impl FederationScenarioActivation {
/// Parse `status` into a typed enum.
#[must_use]
pub fn typed_status(&self) -> Option<FederationScenarioActivationStatus> {
FederationScenarioActivationStatus::parse(&self.status)
}
/// Parse `per_service_state` into typed entries.
///
/// # Errors
///
/// Returns an error if the stored JSON is not an array of
/// `PerServiceActivationState` records.
pub fn parse_per_service_state(
&self,
) -> Result<Vec<PerServiceActivationState>, serde_json::Error> {
serde_json::from_value(self.per_service_state.clone())
}
}