kcode_k1_ktool_set_launch_node/
lib.rs1#![doc = include_str!("../Documentation.md")]
2
3use std::sync::Arc;
4
5use kcode_k1_access_launch_nodes::{
6 AccessContext, AccessId, AccessPolicy, Authority, K1AccessLaunchNodes, ProfileId, TargetId,
7 TargetName, TxId,
8};
9use serde::Deserialize;
10
11const INVALID: &str = "invalid SetLaunchNode arguments";
12
13#[derive(Clone)]
14pub struct SetLaunchNodeKtool {
15 launch_nodes: Arc<K1AccessLaunchNodes>,
16}
17
18impl SetLaunchNodeKtool {
19 pub fn new(launch_nodes: Arc<K1AccessLaunchNodes>) -> Self {
20 Self { launch_nodes }
21 }
22
23 pub fn launch(
24 &self,
25 context: &AccessContext,
26 profile_id: ProfileId,
27 policy: AccessPolicy,
28 arguments: &str,
29 ) -> Result<String, String> {
30 let parsed = parse_arguments(arguments)?;
31 let target_name = TargetName::new(parsed.target).map_err(|_| invalid())?;
32 let node = parsed
33 .node_id
34 .parse::<TxId>()
35 .map(AccessId::new)
36 .map_err(|_| invalid())?;
37 let target = TargetId::new(Authority::User(context.user()), target_name);
38 self.launch_nodes
39 .set(context, profile_id, policy, target, node)?;
40 Ok("success".to_owned())
41 }
42}
43
44#[derive(Deserialize)]
45#[serde(deny_unknown_fields)]
46struct Arguments {
47 target: String,
48 node_id: String,
49}
50
51fn parse_arguments(arguments: &str) -> Result<Arguments, String> {
52 serde_json::from_str(arguments).map_err(|_| invalid())
53}
54
55fn invalid() -> String {
56 INVALID.to_owned()
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use kcode_k1_access::{
63 FilteredAuthorities, K1Access, ModelId, SubsystemId, Target, UserId, ViewerSubject,
64 };
65 use kcode_k1_groups::K1Groups;
66 use kcode_k1_launch_nodes::LaunchNodes;
67 use kcode_k1_peering::K1Peering;
68 use kcode_k1_txn_ordering::K1TxnOrdering;
69
70 fn tx(value: u8) -> TxId {
71 TxId::from_bytes([value; 12])
72 }
73
74 #[test]
75 fn strict_arguments_reject_every_malformed_shape() {
76 let valid = r#"{"target":"Harness","node_id":"000000000000000000000001"}"#;
77 assert!(parse_arguments(valid).is_ok());
78 for arguments in [
79 "",
80 "{}",
81 "[]",
82 r#"{"target":"Harness"}"#,
83 r#"{"node_id":"000000000000000000000001"}"#,
84 r#"{"target":"Harness","node_id":"000000000000000000000001","extra":true}"#,
85 ] {
86 assert_eq!(parse_arguments(arguments).err(), Some(INVALID.to_owned()));
87 }
88 }
89
90 #[test]
91 fn real_stack_create_and_update_use_the_context_user() {
92 let root = tempfile::tempdir().unwrap();
93 let ordering = Arc::new(K1TxnOrdering::open(&root.path().join("ordering")).unwrap());
94 let peering =
95 Arc::new(K1Peering::open(&root.path().join("peering"), ordering.clone()).unwrap());
96 let groups = Arc::new(
97 K1Groups::open(
98 &root.path().join("groups"),
99 ordering.clone(),
100 peering.clone(),
101 )
102 .unwrap(),
103 );
104 let access = Arc::new(
105 K1Access::open(
106 &root.path().join("access"),
107 ordering.clone(),
108 peering.clone(),
109 groups.clone(),
110 )
111 .unwrap(),
112 );
113 let bindings = Arc::new(
114 LaunchNodes::open(&root.path().join("launch-nodes"), ordering, peering).unwrap(),
115 );
116 let facade = Arc::new(K1AccessLaunchNodes::open(access.clone(), groups, bindings).unwrap());
117 let tool = SetLaunchNodeKtool::new(facade.clone());
118
119 let user = UserId::from_tx_id(tx(1));
120 let model = ModelId::from_bytes([2; 32]);
121 let context =
122 AccessContext::new(user, model, FilteredAuthorities::new(Vec::new()).unwrap()).unwrap();
123 let profile = ProfileId::new(tx(3));
124 let policy = AccessPolicy::new(
125 Authority::User(user),
126 Vec::new(),
127 vec![ViewerSubject::Model(model)],
128 )
129 .unwrap();
130 let subsystem = SubsystemId::from_str("k1-kmap").unwrap();
131 let first = access
132 .create(
133 &context,
134 Target::new(subsystem, vec![4; 12]),
135 profile,
136 policy.clone(),
137 )
138 .unwrap()
139 .access_id();
140 let second = access
141 .create(
142 &context,
143 Target::new(subsystem, vec![5; 12]),
144 profile,
145 policy.clone(),
146 )
147 .unwrap()
148 .access_id();
149 let target = TargetId::new(
150 Authority::User(user),
151 TargetName::new("Harness".to_owned()).unwrap(),
152 );
153
154 let first_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, first.txid());
155 assert_eq!(
156 tool.launch(&context, profile, policy.clone(), &first_arguments),
157 Ok("success".to_owned())
158 );
159 assert_eq!(facade.lookup(&context, &target), Ok(first));
160
161 let second_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, second.txid());
162 assert_eq!(
163 tool.launch(&context, profile, policy, &second_arguments),
164 Ok("success".to_owned())
165 );
166 assert_eq!(facade.lookup(&context, &target), Ok(second));
167 }
168
169 #[test]
170 fn launch_rejects_invalid_target_and_node_before_mutation() {
171 let invalid_target = Arguments {
172 target: " ".to_owned(),
173 node_id: "000000000000000000000001".to_owned(),
174 };
175 assert!(TargetName::new(invalid_target.target).is_err());
176 assert!("00000000000000000000000A".parse::<TxId>().is_err());
177 }
178
179 #[test]
180 fn complete_package_stays_below_the_managed_limit() {
181 let files = [
182 include_str!("../Cargo.toml"),
183 include_str!("../Documentation.md"),
184 include_str!("lib.rs"),
185 ];
186 let count = files
187 .iter()
188 .flat_map(|file| file.lines())
189 .filter(|line| !line.trim().is_empty())
190 .count();
191 assert!(count < 500, "complete package has {count} nonblank lines");
192 }
193}