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,
108 peering,
109 groups.clone(),
110 )
111 .unwrap(),
112 );
113 let bindings = Arc::new(LaunchNodes::open(&root.path().join("launch-nodes")).unwrap());
114 let facade = Arc::new(K1AccessLaunchNodes::open(access.clone(), groups, bindings).unwrap());
115 let tool = SetLaunchNodeKtool::new(facade.clone());
116
117 let user = UserId::from_tx_id(tx(1));
118 let model = ModelId::from_bytes([2; 32]);
119 let context =
120 AccessContext::new(user, model, FilteredAuthorities::new(Vec::new()).unwrap()).unwrap();
121 let profile = ProfileId::new(tx(3));
122 let policy = AccessPolicy::new(
123 Authority::User(user),
124 Vec::new(),
125 vec![ViewerSubject::Model(model)],
126 )
127 .unwrap();
128 let subsystem = SubsystemId::from_str("k1-kmap").unwrap();
129 let first = access
130 .create(
131 &context,
132 Target::new(subsystem, vec![4; 12]),
133 profile,
134 policy.clone(),
135 )
136 .unwrap()
137 .access_id();
138 let second = access
139 .create(
140 &context,
141 Target::new(subsystem, vec![5; 12]),
142 profile,
143 policy.clone(),
144 )
145 .unwrap()
146 .access_id();
147 let target = TargetId::new(
148 Authority::User(user),
149 TargetName::new("Harness".to_owned()).unwrap(),
150 );
151
152 let first_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, first.txid());
153 assert_eq!(
154 tool.launch(&context, profile, policy.clone(), &first_arguments),
155 Ok("success".to_owned())
156 );
157 assert_eq!(facade.lookup(&context, &target), Ok(first));
158
159 let second_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, second.txid());
160 assert_eq!(
161 tool.launch(&context, profile, policy, &second_arguments),
162 Ok("success".to_owned())
163 );
164 assert_eq!(facade.lookup(&context, &target), Ok(second));
165 }
166
167 #[test]
168 fn launch_rejects_invalid_target_and_node_before_mutation() {
169 let invalid_target = Arguments {
170 target: " ".to_owned(),
171 node_id: "000000000000000000000001".to_owned(),
172 };
173 assert!(TargetName::new(invalid_target.target).is_err());
174 assert!("00000000000000000000000A".parse::<TxId>().is_err());
175 }
176
177 #[test]
178 fn complete_package_stays_below_the_managed_limit() {
179 let files = [
180 include_str!("../Cargo.toml"),
181 include_str!("../Documentation.md"),
182 include_str!("lib.rs"),
183 ];
184 let count = files
185 .iter()
186 .flat_map(|file| file.lines())
187 .filter(|line| !line.trim().is_empty())
188 .count();
189 assert!(count < 500, "complete package has {count} nonblank lines");
190 }
191}