Skip to main content

kcode_k1_ktool_set_launch_node/
lib.rs

1#![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}