1use crate::plugin_protocol::{
2 MAX_EVENTS, MAX_MESSAGE_BYTES, PROTOCOL_VERSION, v1, validate_canonical_json, validate_context,
3};
4use serde_json::Value;
5use std::collections::BTreeMap;
6use std::fs;
7use std::path::PathBuf;
8use std::pin::Pin;
9use std::sync::Mutex;
10use std::time::Duration;
11use tokio_stream::Stream;
12use tonic::{Request, Response, Status};
13use v1::provider_service_server::ProviderService;
14
15#[derive(Debug)]
16pub struct FixtureProvider {
17 mode: String,
18 state: Mutex<BTreeMap<String, Value>>,
19 state_file: Option<PathBuf>,
21}
22
23impl FixtureProvider {
24 pub fn new(mode: impl Into<String>) -> Self {
25 Self { mode: mode.into(), state: Mutex::new(BTreeMap::new()), state_file: None }
26 }
27
28 pub fn with_state_file(mode: impl Into<String>, state_file: PathBuf) -> Self {
29 let state = if state_file.is_file() {
30 fs::read_to_string(&state_file)
31 .ok()
32 .and_then(|content| serde_json::from_str(&content).ok())
33 .unwrap_or_default()
34 } else {
35 BTreeMap::new()
36 };
37 Self { mode: mode.into(), state: Mutex::new(state), state_file: Some(state_file) }
38 }
39
40 async fn before_request(&self) {
41 if self.mode == "hang" {
42 tokio::time::sleep(Duration::from_secs(30)).await;
43 }
44 }
45
46 fn context(context: Option<&v1::RequestContext>) -> Result<(), Status> {
47 validate_context(context).map_err(|error| Status::failed_precondition(format!("{error:?}")))
48 }
49
50 fn resources(input: &[u8]) -> Result<BTreeMap<String, Value>, Status> {
51 let value = validate_canonical_json(input)
52 .map_err(|error| Status::invalid_argument(format!("{error:?}")))?;
53 serde_json::from_value(
54 value.get("resources").cloned().unwrap_or_else(|| Value::Object(Default::default())),
55 )
56 .map_err(|_| Status::invalid_argument("resources must be an object"))
57 }
58
59 fn persist_locked(&self, state: &BTreeMap<String, Value>) -> Result<(), Status> {
60 let Some(path) = &self.state_file else {
61 return Ok(());
62 };
63 let encoded =
64 serde_json::to_string(state).map_err(|_| Status::internal("state serialization failed"))?;
65 fs::write(path, encoded).map_err(|_| Status::internal("state file write failed"))?;
66 Ok(())
67 }
68
69 fn require_session_secret(
72 &self,
73 secrets: &std::collections::HashMap<String, Vec<u8>>,
74 ) -> Result<(), Status> {
75 if self.mode != "session-secret" {
76 return Ok(());
77 }
78 match secrets.get("secret/session") {
79 Some(value) if value.as_slice() == b"fixture-session-secret" => Ok(()),
80 _ => Err(Status::permission_denied("required provider session secret was not supplied")),
81 }
82 }
83}
84
85#[tonic::async_trait]
86impl ProviderService for FixtureProvider {
87 async fn get_metadata(
88 &self,
89 request: Request<v1::GetMetadataRequest>,
90 ) -> Result<Response<v1::GetMetadataResponse>, Status> {
91 self.before_request().await;
92 Self::context(request.get_ref().context.as_ref())?;
93 Ok(Response::new(v1::GetMetadataResponse {
94 protocol_version: if self.mode == "incompatible" {
95 "2.0".to_string()
96 } else {
97 PROTOCOL_VERSION.to_string()
98 },
99 provider_kind: "synthetic-fixture".to_string(),
100 provider_version: "0.1.0".to_string(),
101 capabilities: vec![
102 "observe".to_string(),
103 "plan".to_string(),
104 "apply".to_string(),
105 "verify".to_string(),
106 "capture".to_string(),
107 "cancel".to_string(),
108 ],
109 max_message_bytes: MAX_MESSAGE_BYTES as u32,
110 max_events: MAX_EVENTS as u32,
111 resource_kinds: vec![],
112 }))
113 }
114
115 async fn observe(
116 &self,
117 request: Request<v1::ObserveRequest>,
118 ) -> Result<Response<v1::ObserveResponse>, Status> {
119 self.before_request().await;
120 let request = request.into_inner();
121 Self::context(request.context.as_ref())?;
122 self.require_session_secret(&request.secrets)?;
123 if self.mode == "malformed" {
124 return Ok(Response::new(v1::ObserveResponse { observations_json: b"{".to_vec() }));
125 }
126 let state = self.state.lock().map_err(|_| Status::internal("state lock failed"))?;
127 let observations_json = serde_json::to_vec(&serde_json::json!({"resources": &*state}))
128 .map_err(|_| Status::internal("state serialization failed"))?;
129 Ok(Response::new(v1::ObserveResponse { observations_json }))
130 }
131
132 async fn capture(
133 &self,
134 request: Request<v1::CaptureRequest>,
135 ) -> Result<Response<v1::CaptureResponse>, Status> {
136 self.before_request().await;
137 let request = request.into_inner();
138 Self::context(request.context.as_ref())?;
139 self.require_session_secret(&request.secrets)?;
140 let selected = request.resource_ids.into_iter().collect::<std::collections::BTreeSet<_>>();
141 let state = self.state.lock().map_err(|_| Status::internal("state lock failed"))?;
142 let mut artifacts = Vec::new();
143 for resource_id in selected {
144 let Some(value) = state.get(&resource_id) else {
145 continue;
146 };
147 let content =
148 serde_json::to_vec(value).map_err(|_| Status::internal("capture serialization failed"))?;
149 artifacts.push(v1::CaptureArtifact {
150 resource_id: resource_id.clone(),
151 relative_path: format!("resources/{}.json", resource_id.replace('/', "_")),
152 content_digest: format!("sha256:{}", crate::plan::sha256_hex_bytes(&content)),
153 content,
154 mandatory: true,
155 media_type: "application/json".into(),
156 });
157 }
158 Ok(Response::new(v1::CaptureResponse { artifacts }))
159 }
160
161 async fn plan(
162 &self,
163 request: Request<v1::PlanRequest>,
164 ) -> Result<Response<v1::PlanResponse>, Status> {
165 self.before_request().await;
166 let request = request.into_inner();
167 Self::context(request.context.as_ref())?;
168 self.require_session_secret(&request.secrets)?;
169 let desired = Self::resources(&request.desired_json)?;
170 let observed = Self::resources(&request.observations_json)?;
171 let mut actions = Vec::new();
172 for (resource_id, value) in &desired {
173 if observed.get(resource_id) != Some(value) {
174 let operation = if observed.contains_key(resource_id) { "update" } else { "create" };
175 let details = serde_json::json!({"value": value});
176 let outputs = if self.mode == "confidential-output" {
177 vec![v1::ActionOutputDecl {
178 binding_id: format!("run/{operation}:{resource_id}/generated"),
179 output_name: "generated".into(),
180 classification: v1::OutputClassification::Confidential.into(),
181 max_bytes: 4096,
182 value_type: v1::BindingValueType::OpaqueBytes.into(),
183 sink_binding: "secret/generated".into(),
184 allow_overwrite: false,
185 }]
186 } else {
187 vec![]
188 };
189 actions.push(v1::PlannedAction {
190 action_id: format!("{operation}:{resource_id}"),
191 resource_id: resource_id.clone(),
192 operation: operation.to_string(),
193 risk: v1::Risk::Reversible.into(),
194 details_json: serde_json::to_vec(&details)
195 .map_err(|_| Status::internal("action serialization failed"))?,
196 secret_references: Vec::new(),
197 depends_on: Vec::new(),
198 contract: Some(v1::ActionContract {
199 deployment_target: String::new(),
200 target_placement: v1::TargetPlacement::Unspecified.into(),
201 outputs,
202 inputs: vec![],
203 timeout_seconds: 600,
204 }),
205 });
206 }
207 }
208 for resource_id in observed.keys().filter(|id| !desired.contains_key(*id)) {
209 actions.push(v1::PlannedAction {
210 action_id: format!("delete:{resource_id}"),
211 resource_id: resource_id.clone(),
212 operation: "delete".to_string(),
213 risk: v1::Risk::Destructive.into(),
214 details_json: b"{}".to_vec(),
215 secret_references: Vec::new(),
216 depends_on: Vec::new(),
217 contract: Some(v1::ActionContract {
218 deployment_target: String::new(),
219 target_placement: v1::TargetPlacement::Unspecified.into(),
220 outputs: vec![],
221 inputs: vec![],
222 timeout_seconds: 600,
223 }),
224 });
225 }
226 actions.sort_by(|left, right| left.action_id.cmp(&right.action_id));
227 Ok(Response::new(v1::PlanResponse { actions }))
228 }
229
230 type ApplyStream =
231 Pin<Box<dyn Stream<Item = Result<v1::ApplyResponse, Status>> + Send + 'static>>;
232
233 async fn apply(
234 &self,
235 request: Request<v1::ApplyRequest>,
236 ) -> Result<Response<Self::ApplyStream>, Status> {
237 self.before_request().await;
238 let request = request.into_inner();
239 Self::context(request.context.as_ref())?;
240 let action = request.action.ok_or_else(|| Status::invalid_argument("action is required"))?;
241 let mut state = self.state.lock().map_err(|_| Status::internal("state lock failed"))?;
242 match action.operation.as_str() {
243 "create" | "update" => {
244 let details = validate_canonical_json(&action.details_json)
245 .map_err(|error| Status::invalid_argument(format!("{error:?}")))?;
246 let value = details
247 .get("value")
248 .cloned()
249 .ok_or_else(|| Status::invalid_argument("action value is required"))?;
250 state.insert(action.resource_id.clone(), value);
251 }
252 "delete" => {
253 state.remove(&action.resource_id);
254 }
255 _ => return Err(Status::invalid_argument("unsupported operation")),
256 }
257 self.persist_locked(&state)?;
258 drop(state);
259 let count =
261 if self.mode == "flood" { MAX_EVENTS.saturating_mul(64).saturating_add(2) } else { 2 };
262 let mode = self.mode.clone();
263 let events = (0..count).map(move |index| {
264 let confidential_outputs = if mode == "confidential-output" && index + 1 == count {
265 [("generated".to_string(), b"fixture-confidential".to_vec())].into_iter().collect()
266 } else {
267 Default::default()
268 };
269 let safe_message = if mode == "unsafe-progress" {
270 "ordinary provider detail\ntoken=field-secret\nopaque-action-secret".to_string()
271 } else {
272 "synthetic fixture event".to_string()
273 };
274 Ok(v1::ApplyResponse {
275 sequence: (index + 1) as u32,
276 phase: if index + 1 == count {
277 v1::EventPhase::Completed.into()
278 } else {
279 v1::EventPhase::Progress.into()
280 },
281 action_id: action.action_id.clone(),
282 safe_message,
283 error: None,
284 detail: false,
285 confidential_outputs,
286 public_outputs: Default::default(),
287 })
288 });
289 Ok(Response::new(Box::pin(tokio_stream::iter(events))))
290 }
291
292 async fn verify(
293 &self,
294 request: Request<v1::VerifyRequest>,
295 ) -> Result<Response<v1::VerifyResponse>, Status> {
296 self.before_request().await;
297 let request = request.into_inner();
298 Self::context(request.context.as_ref())?;
299 self.require_session_secret(&request.secrets)?;
300 let action = request.action.ok_or_else(|| Status::invalid_argument("action is required"))?;
301 let state = self.state.lock().map_err(|_| Status::internal("state lock failed"))?;
302 let satisfied = match action.operation.as_str() {
303 "create" | "update" => state.contains_key(&action.resource_id),
304 "delete" => !state.contains_key(&action.resource_id),
305 _ => false,
306 };
307 Ok(Response::new(v1::VerifyResponse {
308 satisfied,
309 observation_json: serde_json::to_vec(&serde_json::json!({
310 "resource": state.get(&action.resource_id)
311 }))
312 .map_err(|_| Status::internal("verification serialization failed"))?,
313 error: None,
314 }))
315 }
316
317 async fn cancel(
318 &self,
319 request: Request<v1::CancelRequest>,
320 ) -> Result<Response<v1::CancelResponse>, Status> {
321 Self::context(request.get_ref().context.as_ref())?;
322 Ok(Response::new(v1::CancelResponse { accepted: true }))
323 }
324}