1use crate::{ApplyReport, BackendId, Op, ProvisionReport, StateData};
4use alembic_core::{JsonMap, Key, Schema, TypeName};
5use anyhow::Result;
6use serde::{Deserialize, Serialize};
7use std::io::{self, BufReader, Read, Write};
8
9pub const EXTERNAL_PROTOCOL_VERSION: u8 = 1;
11
12#[derive(Debug, Serialize, Deserialize)]
14pub struct ExternalEnvelope {
15 pub version: u8,
17 pub setup: serde_yaml::Value,
19 #[serde(flatten)]
21 pub request: ExternalRequest,
22}
23
24#[derive(Debug, Serialize, Deserialize)]
26#[serde(tag = "method", rename_all = "snake_case")]
27pub enum ExternalRequest {
28 Read {
30 schema: Schema,
31 types: Vec<TypeName>,
32 state: StateData,
33 },
34 Write {
36 schema: Schema,
37 ops: Vec<Op>,
38 state: StateData,
39 },
40 EnsureSchema { schema: Schema },
42 PreviewSchema { schema: Schema },
44}
45
46#[derive(Debug, Serialize)]
48pub struct ExternalEnvelopeRef<'a> {
49 pub version: u8,
51 pub setup: serde_yaml::Value,
53 #[serde(flatten)]
55 pub request: ExternalRequestRef<'a>,
56}
57
58#[derive(Debug, Serialize)]
60#[serde(tag = "method", rename_all = "snake_case")]
61pub enum ExternalRequestRef<'a> {
62 Read {
64 schema: &'a Schema,
65 types: &'a [TypeName],
66 state: StateData,
67 },
68 Write {
70 schema: &'a Schema,
71 ops: &'a [Op],
72 state: StateData,
73 },
74 EnsureSchema { schema: &'a Schema },
76 PreviewSchema { schema: &'a Schema },
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ExternalObject {
83 pub type_name: TypeName,
85 pub key: Key,
87 pub attrs: JsonMap,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub backend_id: Option<BackendId>,
92}
93
94#[derive(Debug, Serialize, Deserialize)]
96pub struct ExternalResponse<T> {
97 pub ok: bool,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub result: Option<T>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub error: Option<String>,
105}
106
107impl<T> ExternalResponse<T> {
108 pub fn ok(result: T) -> Self {
110 Self {
111 ok: true,
112 result: Some(result),
113 error: None,
114 }
115 }
116
117 pub fn error(message: impl Into<String>) -> Self {
119 Self {
120 ok: false,
121 result: None,
122 error: Some(message.into()),
123 }
124 }
125
126 pub fn from_result(result: Result<T>) -> Self {
128 match result {
129 Ok(value) => Self::ok(value),
130 Err(err) => Self::error(format!("{err:#}")),
131 }
132 }
133}
134
135pub trait ExternalAdapter {
137 fn setup(&mut self, configuration: &serde_yaml::Value) -> Result<()>;
139
140 fn read(
142 &mut self,
143 schema: &Schema,
144 types: &[TypeName],
145 state: &StateData,
146 ) -> Result<Vec<ExternalObject>>;
147
148 fn write(&mut self, schema: &Schema, ops: &[Op], state: &StateData) -> Result<ApplyReport>;
150
151 fn ensure_schema(&mut self, schema: &Schema) -> Result<ProvisionReport> {
153 let _ = schema;
154 Ok(ProvisionReport::default())
155 }
156
157 fn preview_schema(&mut self, schema: &Schema) -> Result<Option<ProvisionReport>> {
161 let _ = schema;
162 Ok(None)
163 }
164}
165
166pub fn run_external_adapter<A: ExternalAdapter>(
168 mut adapter: A,
169 (reader, mut writer): (impl Read, impl Write),
170) -> io::Result<()> {
171 let mut input = String::new();
172 BufReader::new(reader).read_to_string(&mut input)?;
173
174 let envelope: ExternalEnvelope = match serde_json::from_str(&input) {
175 Ok(envelope) => envelope,
176 Err(err) => return write_error(&mut writer, format!("invalid request: {err}")),
177 };
178
179 if envelope.version != EXTERNAL_PROTOCOL_VERSION {
180 return write_error(
181 &mut writer,
182 format!(
183 "unsupported protocol version {} (expected {})",
184 envelope.version, EXTERNAL_PROTOCOL_VERSION
185 ),
186 );
187 }
188
189 if let Err(e) = adapter.setup(&envelope.setup) {
190 return write_error(&mut writer, format!("invalid setup: {e}"));
191 }
192
193 match envelope.request {
194 ExternalRequest::Read {
195 schema,
196 types,
197 state,
198 } => {
199 let response = ExternalResponse::from_result(adapter.read(&schema, &types, &state));
200 write_response(&mut writer, response)
201 }
202 ExternalRequest::Write { schema, ops, state } => {
203 let response = ExternalResponse::from_result(adapter.write(&schema, &ops, &state));
204 write_response(&mut writer, response)
205 }
206 ExternalRequest::EnsureSchema { schema } => {
207 let response = ExternalResponse::from_result(adapter.ensure_schema(&schema));
208 write_response(&mut writer, response)
209 }
210 ExternalRequest::PreviewSchema { schema } => {
211 let response = ExternalResponse::from_result(adapter.preview_schema(&schema));
212 write_response(&mut writer, response)
213 }
214 }
215}
216
217fn write_error(out: &mut impl Write, message: String) -> io::Result<()> {
218 let response = ExternalResponse::<serde_json::Value>::error(message);
219 write_response(out, response)
220}
221
222fn write_response<T: Serialize>(
223 out: &mut impl Write,
224 response: ExternalResponse<T>,
225) -> io::Result<()> {
226 serde_json::to_writer(&mut *out, &response).map_err(io::Error::other)?;
227 out.write_all(b"\n")?;
228 out.flush()
229}
230
231#[macro_export]
233macro_rules! alembic_external_main {
234 ($adapter:expr) => {
235 fn main() -> std::io::Result<()> {
236 let stdin = std::io::stdin();
237 let mut stdout = std::io::BufWriter::new(std::io::stdout());
238 $crate::external::run_external_adapter($adapter, (stdin, stdout))
239 }
240 };
241}
242
243#[cfg(test)]
244mod tests {
245 use super::ExternalResponse;
246 use crate::{
247 run_external_adapter, ApplyReport, ExternalAdapter, ExternalEnvelope, ExternalEnvelopeRef,
248 ExternalObject, ExternalRequest, ExternalRequestRef, Op, ProvisionReport, StateData,
249 EXTERNAL_PROTOCOL_VERSION,
250 };
251 use alembic_core::{Key, Object, Schema, TypeName, TypeSchema, Uid};
252 use serde_json::json;
253 use serde_yaml::Value;
254 use std::io::BufReader;
255 use std::io::{BufRead, Write};
256
257 #[test]
258 fn external_response_ok_serializes() {
259 let response = ExternalResponse::ok(vec!["one".to_string()]);
260 let value = serde_json::to_value(&response).unwrap();
261 assert_eq!(value, json!({"ok": true, "result": ["one"]}));
262 }
263
264 #[test]
265 fn external_response_error_serializes() {
266 let response: ExternalResponse<Vec<String>> = ExternalResponse::error("boom");
267 let value = serde_json::to_value(&response).unwrap();
268 assert_eq!(value, json!({"ok": false, "error": "boom"}));
269 }
270
271 #[test]
272 fn external_response_from_result_renders_error_chain() {
273 let err = anyhow::anyhow!("connection refused")
274 .context("connecting to backend")
275 .context("reading inventory");
276 let response: ExternalResponse<()> = ExternalResponse::from_result(Err(err));
277 let error = response.error.unwrap();
278 assert!(error.contains("reading inventory"));
279 assert!(error.contains("connecting to backend"));
280 assert!(error.contains("connection refused"));
281 }
282
283 #[test]
284 fn ref_and_owned_request_types_serialize_identically() {
285 let schema = Schema {
286 types: [(
287 "dcim.device".to_string(),
288 TypeSchema {
289 key: [].into(),
290 fields: [].into(),
291 },
292 )]
293 .into(),
294 };
295 let types = vec![TypeName::new("dcim.device")];
296 let ops = vec![Op::Create {
297 uid: Uid::from_u128(1),
298 type_name: TypeName::new("dcim.device"),
299 desired: Object {
300 uid: Uid::from_u128(1),
301 type_name: TypeName::new("dcim.device"),
302 key: Key::default(),
303 attrs: Default::default(),
304 source: None,
305 },
306 }];
307 let state = StateData::default();
308
309 let owned_read = serde_json::to_value(ExternalRequest::Read {
310 schema: schema.clone(),
311 types: types.clone(),
312 state: state.clone(),
313 })
314 .unwrap();
315 let ref_read = serde_json::to_value(ExternalRequestRef::Read {
316 schema: &schema,
317 types: &types,
318 state: state.clone(),
319 })
320 .unwrap();
321 assert_eq!(owned_read, ref_read);
322
323 let owned_write = serde_json::to_value(ExternalRequest::Write {
324 schema: schema.clone(),
325 ops: ops.clone(),
326 state: state.clone(),
327 })
328 .unwrap();
329 let ref_write = serde_json::to_value(ExternalRequestRef::Write {
330 schema: &schema,
331 ops: &ops,
332 state: state.clone(),
333 })
334 .unwrap();
335 assert_eq!(owned_write, ref_write);
336
337 let owned_ensure = serde_json::to_value(ExternalRequest::EnsureSchema {
338 schema: schema.clone(),
339 })
340 .unwrap();
341 let ref_ensure =
342 serde_json::to_value(ExternalRequestRef::EnsureSchema { schema: &schema }).unwrap();
343 assert_eq!(owned_ensure, ref_ensure);
344
345 let owned_preview = serde_json::to_value(ExternalRequest::PreviewSchema {
346 schema: schema.clone(),
347 })
348 .unwrap();
349 let ref_preview =
350 serde_json::to_value(ExternalRequestRef::PreviewSchema { schema: &schema }).unwrap();
351 assert_eq!(owned_preview, ref_preview);
352
353 let owned_envelope = serde_json::to_value(ExternalEnvelope {
354 version: EXTERNAL_PROTOCOL_VERSION,
355 setup: Default::default(),
356 request: ExternalRequest::Read {
357 schema: schema.clone(),
358 types: types.clone(),
359 state: state.clone(),
360 },
361 })
362 .unwrap();
363 let ref_envelope = serde_json::to_value(ExternalEnvelopeRef {
364 version: EXTERNAL_PROTOCOL_VERSION,
365 setup: Default::default(),
366 request: ExternalRequestRef::Read {
367 schema: &schema,
368 types: &types,
369 state,
370 },
371 })
372 .unwrap();
373 assert_eq!(owned_envelope, ref_envelope);
374 }
375
376 #[derive(Debug, Default)]
377 struct TestExternalAdapter {
378 pub x: i64,
379 }
380
381 impl ExternalAdapter for TestExternalAdapter {
382 fn setup(&mut self, configuration: &Value) -> anyhow::Result<()> {
383 if let Some(x) = configuration.get("x").and_then(serde_yaml::Value::as_i64) {
384 self.x = x;
385 }
386 Ok(())
387 }
388
389 fn read(
390 &mut self,
391 _schema: &Schema,
392 _types: &[TypeName],
393 _state: &StateData,
394 ) -> anyhow::Result<Vec<ExternalObject>> {
395 let mut result = vec![];
396 for _ in 0..self.x {
397 result.push(ExternalObject {
398 type_name: TypeName::new(""),
399 key: Default::default(),
400 attrs: Default::default(),
401 backend_id: None,
402 })
403 }
404 Ok(result)
405 }
406
407 fn write(
408 &mut self,
409 _schema: &Schema,
410 _ops: &[Op],
411 _state: &StateData,
412 ) -> anyhow::Result<ApplyReport> {
413 Err(anyhow::anyhow!("unsupported operation"))
414 }
415
416 fn ensure_schema(&mut self, schema: &Schema) -> anyhow::Result<ProvisionReport> {
417 let mut created_fields = vec![];
418 for ty_name in schema.types.keys() {
419 created_fields.push(ty_name.clone());
420 }
421 Ok(ProvisionReport {
422 created_fields,
423 ..Default::default()
424 })
425 }
426
427 fn preview_schema(&mut self, schema: &Schema) -> anyhow::Result<Option<ProvisionReport>> {
428 Ok(Some(ProvisionReport {
430 created_fields: schema.types.keys().cloned().collect(),
431 ..Default::default()
432 }))
433 }
434 }
435
436 #[test]
437 fn external_adapter_communication_over_stdio() {
438 let adapter = TestExternalAdapter::default();
439
440 let (in_reader, mut in_writer) = std::io::pipe().unwrap();
441 let (out_reader, out_writer) = std::io::pipe().unwrap();
442
443 let t = std::thread::spawn(move || {
444 assert!(run_external_adapter(adapter, (in_reader, out_writer)).is_ok());
445 });
446
447 let dummy_type_schema = TypeSchema {
448 key: [].into(),
449 fields: [].into(),
450 };
451
452 let request = ExternalRequest::EnsureSchema {
453 schema: Schema {
454 types: [
455 ("a".to_string(), dummy_type_schema.clone()),
456 ("b".to_string(), dummy_type_schema.clone()),
457 ]
458 .into(),
459 },
460 };
461 let envelope = ExternalEnvelope {
462 version: EXTERNAL_PROTOCOL_VERSION,
463 setup: Default::default(),
464 request,
465 };
466
467 writeln!(in_writer, "{}", serde_json::to_string(&envelope).unwrap()).unwrap();
468 drop(in_writer);
469
470 let mut response = String::new();
471 BufReader::new(out_reader).read_line(&mut response).unwrap();
472
473 let response: ExternalResponse<ProvisionReport> = serde_json::from_str(&response).unwrap();
474 assert!(response.ok);
475 assert_eq!(
476 response.result.unwrap().created_fields,
477 vec!["a".to_string(), "b".to_string()]
478 );
479
480 t.join().unwrap();
481 }
482
483 #[test]
484 fn external_adapter_preview_schema_roundtrip() {
485 let adapter = TestExternalAdapter::default();
486
487 let (in_reader, mut in_writer) = std::io::pipe().unwrap();
488 let (out_reader, out_writer) = std::io::pipe().unwrap();
489
490 let t = std::thread::spawn(move || {
491 assert!(run_external_adapter(adapter, (in_reader, out_writer)).is_ok());
492 });
493
494 let dummy_type_schema = TypeSchema {
495 key: [].into(),
496 fields: [].into(),
497 };
498 let request = ExternalRequest::PreviewSchema {
499 schema: Schema {
500 types: [
501 ("a".to_string(), dummy_type_schema.clone()),
502 ("b".to_string(), dummy_type_schema.clone()),
503 ]
504 .into(),
505 },
506 };
507 let envelope = ExternalEnvelope {
508 version: EXTERNAL_PROTOCOL_VERSION,
509 setup: Default::default(),
510 request,
511 };
512
513 writeln!(in_writer, "{}", serde_json::to_string(&envelope).unwrap()).unwrap();
514 drop(in_writer);
515
516 let mut response = String::new();
517 BufReader::new(out_reader).read_line(&mut response).unwrap();
518
519 let response: ExternalResponse<Option<ProvisionReport>> =
520 serde_json::from_str(&response).unwrap();
521 assert!(response.ok);
522 assert_eq!(
524 response.result.flatten().unwrap().created_fields,
525 vec!["a".to_string(), "b".to_string()]
526 );
527
528 t.join().unwrap();
529 }
530
531 #[test]
532 fn preview_schema_none_roundtrips_as_null_result() {
533 let response: ExternalResponse<Option<ProvisionReport>> =
537 ExternalResponse::from_result(Ok(None));
538 let wire = serde_json::to_value(&response).unwrap();
539 assert_eq!(wire, json!({"ok": true, "result": null}));
540 let back: ExternalResponse<Option<ProvisionReport>> = serde_json::from_value(wire).unwrap();
541 assert!(back.ok);
542 assert!(back.result.flatten().is_none());
543 }
544
545 #[test]
546 fn external_adapter_communication_error() {
547 let adapter = TestExternalAdapter::default();
548
549 let (in_reader, mut in_writer) = std::io::pipe().unwrap();
550 let (out_reader, out_writer) = std::io::pipe().unwrap();
551
552 let t = std::thread::spawn(move || {
553 assert!(run_external_adapter(adapter, (in_reader, out_writer)).is_ok());
554 });
555
556 let request = ExternalRequest::Write {
558 schema: Default::default(),
559 ops: vec![],
560 state: Default::default(),
561 };
562 let envelope = ExternalEnvelope {
563 version: EXTERNAL_PROTOCOL_VERSION,
564 setup: Default::default(),
565 request,
566 };
567
568 writeln!(in_writer, "{}", serde_json::to_string(&envelope).unwrap()).unwrap();
569 drop(in_writer);
570
571 let mut response = String::new();
572 BufReader::new(out_reader).read_line(&mut response).unwrap();
573
574 let response: ExternalResponse<ProvisionReport> = serde_json::from_str(&response).unwrap();
575 assert!(response.error.is_some());
576 assert!(!response.ok);
577
578 t.join().unwrap();
579 }
580
581 #[test]
582 fn external_adapter_outdated() {
583 let adapter = TestExternalAdapter::default();
584
585 let (in_reader, mut in_writer) = std::io::pipe().unwrap();
586 let (out_reader, out_writer) = std::io::pipe().unwrap();
587
588 let t = std::thread::spawn(move || {
589 assert!(run_external_adapter(adapter, (in_reader, out_writer)).is_ok());
590 });
591
592 let request = ExternalRequest::EnsureSchema {
593 schema: Default::default(),
594 };
595 let envelope = ExternalEnvelope {
596 version: EXTERNAL_PROTOCOL_VERSION + 1,
597 setup: Default::default(),
598 request,
599 };
600
601 writeln!(in_writer, "{}", serde_json::to_string(&envelope).unwrap()).unwrap();
602 drop(in_writer);
603
604 let mut response = String::new();
605 BufReader::new(out_reader).read_line(&mut response).unwrap();
606
607 let response: ExternalResponse<ProvisionReport> = serde_json::from_str(&response).unwrap();
608 if let Some(error) = response.error {
609 assert_eq!(
610 error,
611 format!(
612 "unsupported protocol version {} (expected {})",
613 EXTERNAL_PROTOCOL_VERSION + 1,
614 EXTERNAL_PROTOCOL_VERSION
615 )
616 );
617 }
618 assert!(!response.ok);
619
620 t.join().unwrap();
621 }
622
623 #[test]
624 fn external_adapter_configuration() {
625 let adapter = TestExternalAdapter::default();
626
627 let (in_reader, mut in_writer) = std::io::pipe().unwrap();
628 let (out_reader, out_writer) = std::io::pipe().unwrap();
629
630 let t = std::thread::spawn(move || {
631 assert!(run_external_adapter(adapter, (in_reader, out_writer)).is_ok());
632 });
633
634 let request = ExternalRequest::Read {
635 schema: Default::default(),
636 types: vec![],
637 state: Default::default(),
638 };
639 const MAGIC_NUMBER: usize = 13;
640
641 let envelope = ExternalEnvelope {
642 version: EXTERNAL_PROTOCOL_VERSION,
643 setup: serde_yaml::from_str(&format!("x: {MAGIC_NUMBER}")).unwrap(),
644 request,
645 };
646
647 writeln!(in_writer, "{}", serde_json::to_string(&envelope).unwrap()).unwrap();
648 drop(in_writer);
649
650 let mut response = String::new();
651 BufReader::new(out_reader).read_line(&mut response).unwrap();
652
653 let response: ExternalResponse<Vec<ExternalObject>> =
654 serde_json::from_str(&response).unwrap();
655 assert!(response.ok);
656 assert_eq!(response.result.unwrap().len(), MAGIC_NUMBER,);
657
658 t.join().unwrap();
659 }
660}