stormchaser_cli/commands/
storage.rs1use crate::utils::{handle_response, require_token};
2use anyhow::Result;
3use clap::Subcommand;
4use serde_json::json;
5use serde_json::Value;
6use std::fs;
7use std::path::PathBuf;
8
9#[derive(Subcommand)]
10pub enum StorageCommands {
11 List,
13 Create {
15 name: String,
16 #[arg(long)]
18 backend_type: String,
19 #[arg(long)]
21 config: PathBuf,
22 #[arg(long)]
23 default_sfs: bool,
24 #[arg(long)]
25 description: Option<String>,
26 #[arg(long)]
28 aws_assume_role_arn: Option<String>,
29 },
30 Get { id: stormchaser_model::BackendId },
32 Update {
34 id: stormchaser_model::BackendId,
35 #[arg(long)]
36 name: Option<String>,
37 #[arg(long)]
39 config: Option<PathBuf>,
40 #[arg(long)]
41 default_sfs: Option<bool>,
42 #[arg(long)]
43 description: Option<String>,
44 #[arg(long)]
46 aws_assume_role_arn: Option<String>,
47 },
48 Delete { id: stormchaser_model::BackendId },
50}
51
52pub async fn handle(
53 url: &str,
54 token: Option<&str>,
55 http_client: &reqwest_middleware::ClientWithMiddleware,
56 command: StorageCommands,
57) -> Result<()> {
58 match command {
59 StorageCommands::List => {
60 let token = require_token(token)?;
61 let res = http_client
62 .get(format!("{}/api/v1/storage-backends", url))
63 .header("Authorization", format!("Bearer {}", token))
64 .send()
65 .await?;
66 handle_response(res).await?;
67 }
68 StorageCommands::Create {
69 name,
70 backend_type,
71 config,
72 default_sfs,
73 description,
74 aws_assume_role_arn,
75 } => {
76 let config_json: Value = serde_json::from_str(&fs::read_to_string(config)?)?;
77 let token = require_token(token)?;
78 let res = http_client
79 .post(format!("{}/api/v1/storage-backends", url))
80 .header("Authorization", format!("Bearer {}", token))
81 .json(&json!({
82 "name": name,
83 "backend_type": backend_type,
84 "config": config_json,
85 "is_default_sfs": default_sfs,
86 "description": description,
87 "aws_assume_role_arn": aws_assume_role_arn,
88 }))
89 .send()
90 .await?;
91 handle_response(res).await?;
92 }
93 StorageCommands::Get { id } => {
94 let token = require_token(token)?;
95 let res = http_client
96 .get(format!("{}/api/v1/storage-backends/{}", url, id))
97 .header("Authorization", format!("Bearer {}", token))
98 .send()
99 .await?;
100 handle_response(res).await?;
101 }
102 StorageCommands::Update {
103 id,
104 name,
105 config,
106 default_sfs,
107 description,
108 aws_assume_role_arn,
109 } => {
110 let mut body = json!({});
111 if let Some(n) = name {
112 body["name"] = json!(n);
113 }
114 if let Some(c) = config {
115 body["config"] = serde_json::from_str(&fs::read_to_string(c)?)?;
116 }
117 if let Some(d) = default_sfs {
118 body["is_default_sfs"] = json!(d);
119 }
120 if let Some(desc) = description {
121 body["description"] = json!(desc);
122 }
123 if let Some(arn) = aws_assume_role_arn {
124 body["aws_assume_role_arn"] = json!(arn);
125 }
126
127 let token = require_token(token)?;
128 let res = http_client
129 .patch(format!("{}/api/v1/storage-backends/{}", url, id))
130 .header("Authorization", format!("Bearer {}", token))
131 .json(&body)
132 .send()
133 .await?;
134 handle_response(res).await?;
135 }
136 StorageCommands::Delete { id } => {
137 let token = require_token(token)?;
138 let res = http_client
139 .delete(format!("{}/api/v1/storage-backends/{}", url, id))
140 .header("Authorization", format!("Bearer {}", token))
141 .send()
142 .await?;
143 handle_response(res).await?;
144 }
145 }
146 Ok(())
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use reqwest_middleware::ClientBuilder;
153 use wiremock::matchers::{header, method, path};
154 use wiremock::{Mock, MockServer, ResponseTemplate};
155
156 #[tokio::test]
157 async fn test_storage_list() {
158 let server = MockServer::start().await;
159 Mock::given(method("GET"))
160 .and(path("/api/v1/storage-backends"))
161 .and(header("Authorization", "Bearer test-token"))
162 .respond_with(ResponseTemplate::new(200).set_body_json(json!([])))
163 .mount(&server)
164 .await;
165
166 let client = ClientBuilder::new(reqwest::Client::new()).build();
167 let cmd = StorageCommands::List;
168
169 let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
170 assert!(result.is_ok());
171 }
172
173 #[tokio::test]
174 async fn test_storage_create() {
175 let server = MockServer::start().await;
176 Mock::given(method("POST"))
177 .and(path("/api/v1/storage-backends"))
178 .and(header("Authorization", "Bearer test-token"))
179 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"status": "created"})))
180 .mount(&server)
181 .await;
182
183 use std::io::Write;
184 use tempfile::NamedTempFile;
185 let mut temp_file = NamedTempFile::new().unwrap();
186 writeln!(temp_file, "{{\"bucket\":\"my-bucket\"}}").unwrap();
187
188 let client = ClientBuilder::new(reqwest::Client::new()).build();
189 let cmd = StorageCommands::Create {
190 name: "test-storage".to_string(),
191 backend_type: "s3".to_string(),
192 config: temp_file.path().to_path_buf(),
193 default_sfs: true,
194 description: None,
195 aws_assume_role_arn: None,
196 };
197
198 let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
199 assert!(result.is_ok());
200 }
201
202 #[tokio::test]
203 async fn test_storage_delete() {
204 let server = MockServer::start().await;
205 let id = stormchaser_model::BackendId::new_v4();
206 Mock::given(method("DELETE"))
207 .and(path(format!("/api/v1/storage-backends/{}", id)))
208 .and(header("Authorization", "Bearer test-token"))
209 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"status": "deleted"})))
210 .mount(&server)
211 .await;
212
213 let client = ClientBuilder::new(reqwest::Client::new()).build();
214 let cmd = StorageCommands::Delete { id };
215
216 let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
217 assert!(result.is_ok());
218 }
219}