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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use crate as cindy;
/// The entire fleet as the user's `#[cindy::inventory]` function declares
/// it.
///
/// `Inventory<V>` is what the user constructs and returns; the CLI then
/// pulls a JSON dump of it out of the orchestrator binary at startup
/// (via the `CINDY_DUMP_INVENTORY=1` env-var mode) and consults it to
/// figure out which hosts to deploy to and — after applying `--limit`
/// — what `CINDY_HOST_CONTEXT` to inject into each per-host orchestrator
/// invocation.
///
/// The CLI is *type-erased*: it parses the dump into
/// `Inventory<serde_json::Value>` so it can introspect `name` / `tags`
/// without knowing your `V`. Your `V` round-trips intact and is
/// re-deserialised inside the orchestrator process, where the
/// `#[cindy::main]`-generated entry knows the type. Target architecture
/// for cross-compilation is SSH-discovered (`uname -m`) at preflight,
/// not declared in the inventory.
///
/// Construct it however you want — read TOML, hit Consul, hardcode it,
/// shell out. The function is just async Rust:
///
/// ```ignore
/// #[derive(serde::Serialize, serde::Deserialize)]
/// struct VyosVars { eth0_mac: String, vrrp_priority: u8 }
///
/// #[cindy::inventory]
/// async fn inventory() -> cindy::Result<cindy::Inventory<VyosVars>> {
/// Ok(cindy::Inventory {
/// hosts: vec![
/// cindy::Host {
/// name: "vyos-01".into(),
/// tags: vec!["router".into(), "eu".into()],
/// vars: VyosVars { eth0_mac: "10:66:6a:1a:30:ec".into(), vrrp_priority: 200 },
/// },
/// ],
/// })
/// }
/// ```
/// Conversion glue so `#[cindy::inventory]` can accept *either*
/// `Inventory<V>` or `Result<Inventory<V>, E>` and always emit the same
/// JSON shape on the wire.
///
/// The infallible impl just serialises straight through. The fallible
/// impl unwraps to stdout-as-JSON on `Ok`, prints the debug rendering
/// of the error to stderr and exits 1 on `Err`. Either way the CLI
/// reads a clean `{"hosts": [...]}` from FD 1 — no `{"Ok": {...}}`
/// wrapper for it to peel.
/// Inventory-crate slot that `#[cindy::inventory]` registers into.
///
/// The macro stores a thunk that builds the user's inventory and
/// serialises the result to a `serde_json::Value`. The
/// `#[cindy::main]`-generated entry walks `inventory::iter::<RegisteredInventory>`
/// when `CINDY_DUMP_INVENTORY=1` is set, finds the (unique) registered
/// inventory, executes it on the current tokio runtime, and writes the
/// JSON to the original FD 1 for the CLI to consume.
///
/// Multiple registrations are a runtime error — the macro doesn't try
/// to merge inventories.
collect!;
/// A single deployment target as the orchestrator sees it at run-time.
///
/// `Host<V>` is the runtime view that the `#[cindy::main]`-generated entry
/// hands to your `main`. The generic `V` is *your* per-host vars struct
/// — anything `Serialize + DeserializeOwned`. The CLI itself never
/// names `V`; it only deals with the structural fields (`name`,
/// `tags`) and treats `vars` as an opaque JSON blob that round-trips
/// through `serde_json::Value`. That keeps the CLI ignorant of your
/// project's types while still letting your code reach typed vars.