Skip to content

Commit f07c0f5

Browse files
committed
feat: ✨ support etcd backend
support etcd backend Signed-off-by: Asura <[email protected]> Tongsuo-Project#26
1 parent b823ef1 commit f07c0f5

File tree

8 files changed

+349
-4
lines changed

8 files changed

+349
-4
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,10 @@ Cargo.lock
2222

2323
# VIM temp file
2424
*.swp
25+
26+
# IDE files
27+
.idea
28+
.vscode
29+
30+
# OS files
31+
.DS_Store

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ ureq = "2.9"
5858
glob = "0.3"
5959
serde_asn1_der = "0.8"
6060
base64 = "0.22"
61+
etcd-client = { version = "0.12.4", features = ["tls"] }
62+
tokio = "1.37.0"
6163

6264
[features]
6365
storage_mysql = ["diesel", "r2d2", "r2d2-diesel"]

examples/example.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"listener": {
3+
"tcp": {
4+
"ltype": "tcp",
5+
"address": "0.0.0.0:8080",
6+
"tls_disable": true
7+
}
8+
},
9+
"storage": {
10+
"etcd": {
11+
"stype": "etcd",
12+
"address": "http://127.0.0.1:2379",
13+
"path": "rustyvault"
14+
}
15+
},
16+
"api_addr": "http://localhost:8080",
17+
"log_format": "json",
18+
"log_level": "info",
19+
"pid_file": "/tmp/rustyvault/vault.pid",
20+
"work_dir": "/tmp/rustyvault",
21+
"daemon": true,
22+
"daemon_user": "vault",
23+
"daemon_group": "vault"
24+
}

src/cli/config.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ where
163163
let storage: HashMap<String, Storage> = Deserialize::deserialize(deserializer)?;
164164

165165
for key in storage.keys() {
166-
if key != "file" {
166+
if key != "file" && key != "etcd" {
167167
return Err(serde::de::Error::custom("Invalid storage key"));
168168
}
169169
}

src/errors.rs

+6
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@ pub enum RvError {
229229
#[error("RwLock was poisoned (writing)")]
230230
ErrRwLockWritePoison,
231231

232+
#[error("Some backend error happened, {:?}", .source)]
233+
BackendError {
234+
#[from]
235+
source: crate::storage::physical::error::BackendError,
236+
},
237+
232238
/// Database Errors Begin
233239
///
234240
#[error("Database type is not support now. Please try postgressql or mysql again.")]

src/storage/physical/error.rs

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use thiserror::Error;
2+
3+
#[derive(Error, Debug)]
4+
pub enum BackendError {
5+
#[error("Backend etcd error: {0}!")]
6+
EtcdError(String),
7+
}

src/storage/physical/etcd.rs

+294
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
use super::{Backend, BackendEntry};
2+
use crate::errors::RvError;
3+
use crate::storage::physical::error::BackendError::EtcdError;
4+
use anyhow::Ok;
5+
use etcd_client::*;
6+
use serde_json::Value;
7+
use std::{collections::HashMap, env, time::Duration};
8+
9+
pub const ETCD_BACKEND_PATH: &str = "/rusty_vault";
10+
11+
pub struct EtcdBackend {
12+
path: Vec<String>,
13+
endpoints: Vec<String>,
14+
options: ConnectOptions,
15+
}
16+
17+
/// Implementation of the `Backend` trait for the Etcd backend.
18+
impl Backend for EtcdBackend {
19+
/// Retrieves a list of keys with the specified prefix from the Etcd backend.
20+
///
21+
/// # Arguments
22+
///
23+
/// * `prefix` - The prefix used to filter the keys.
24+
///
25+
/// # Examples
26+
///
27+
/// ```
28+
/// use rusty_vault::storage::Backend;
29+
/// use rusty_vault::error::RvError;
30+
///
31+
/// let etcd_backend = EtcdBackend::new();
32+
/// let keys = etcd_backend.list("prefix");
33+
/// match keys {
34+
/// Ok(keys) => {
35+
/// for key in keys {
36+
/// println!("{}", key);
37+
/// }
38+
/// },
39+
/// Err(error) => {
40+
/// eprintln!("Error: {}", error);
41+
/// }
42+
/// }
43+
/// ```
44+
fn list(&self, prefix: &str) -> Result<Vec<String>, RvError> {
45+
// Implementation details...
46+
let mut path = self.path.clone();
47+
let rt = tokio::runtime::Handle::current();
48+
if !prefix.is_empty() {
49+
path.push(prefix.to_string());
50+
}
51+
52+
let _ = rt
53+
.block_on(async {
54+
let mut client = Client::connect(self.endpoints.clone(), Some(self.options.clone())).await?;
55+
client.get(path.join("/"), Some(GetOptions::new().with_prefix().with_keys_only())).await
56+
})
57+
.map_err(|_error| RvError::BackendError { source: EtcdError("request error".to_string()) })
58+
.map(|resp| {
59+
let mut ks = vec![];
60+
for kv in resp.kvs() {
61+
ks.push(kv.key_str().unwrap().to_string());
62+
}
63+
Ok(ks)
64+
})?;
65+
Err(RvError::BackendError { source: EtcdError(format!("list key {} error", path.join("/"))) })
66+
}
67+
68+
/// Retrieves the value associated with the specified key from the Etcd backend.
69+
///
70+
/// # Arguments
71+
///
72+
/// * `key` - The key used to retrieve the value.
73+
///
74+
/// # Examples
75+
///
76+
/// ```
77+
/// use rusty_vault::storage::Backend;
78+
/// use rusty_vault::error::RvError;
79+
///
80+
/// let etcd_backend = EtcdBackend::new();
81+
/// let entry = etcd_backend.get("key");
82+
/// match entry {
83+
/// Ok(Some(backend_entry)) => {
84+
/// println!("Value: {:?}", backend_entry.value);
85+
/// },
86+
/// Ok(None) => {
87+
/// println!("Key not found");
88+
/// },
89+
/// Err(error) => {
90+
/// eprintln!("Error: {}", error);
91+
/// }
92+
/// }
93+
/// ```
94+
fn get(&self, key: &str) -> Result<Option<BackendEntry>, RvError> {
95+
// Implementation details...
96+
let mut path = self.path.clone();
97+
let rt = tokio::runtime::Handle::current();
98+
99+
path.push(key.to_string());
100+
println!("{:?}", path.join("/"));
101+
102+
let _ = rt
103+
.block_on(async {
104+
let mut client = Client::connect(self.endpoints.clone(), Some(self.options.clone())).await?;
105+
client.get(path.join("/"), None).await
106+
})
107+
.map_err(|_error| RvError::BackendError { source: EtcdError("request error".to_string()) })
108+
.map(|resp| {
109+
if let Some(kv) = resp.kvs().first() {
110+
Ok(Some(BackendEntry { key: kv.key_str().unwrap().to_string(), value: kv.value().to_vec() }))
111+
} else {
112+
Ok(None)
113+
}
114+
})?;
115+
Err(RvError::BackendError { source: EtcdError(format!("get key {} error", path.join("/"))) })
116+
}
117+
118+
/// Stores the specified key-value pair in the Etcd backend.
119+
///
120+
/// # Arguments
121+
///
122+
/// * `entry` - The key-value pair to store.
123+
///
124+
/// # Examples
125+
///
126+
/// ```
127+
/// use rusty_vault::storage::Backend;
128+
/// use rusty_vault::error::RvError;
129+
///
130+
/// let etcd_backend = EtcdBackend::new();
131+
/// let backend_entry = BackendEntry::new("key", vec![1, 2, 3]);
132+
/// let result = etcd_backend.put(&backend_entry);
133+
/// match result {
134+
/// Ok(()) => {
135+
/// println!("Key-value pair stored successfully");
136+
/// },
137+
/// Err(error) => {
138+
/// eprintln!("Error: {}", error);
139+
/// }
140+
/// }
141+
/// ```
142+
fn put(&self, entry: &BackendEntry) -> Result<(), RvError> {
143+
// Implementation details...
144+
let mut path = self.path.clone();
145+
let rt = tokio::runtime::Handle::current();
146+
147+
path.push(entry.key.to_string());
148+
rt.block_on(async {
149+
let mut client = Client::connect(self.endpoints.clone(), Some(self.options.clone())).await?;
150+
client.put(path.join("/"), entry.value.clone(), None).await
151+
})
152+
.map_err(|_error| RvError::BackendError { source: EtcdError("request error".to_string()) })
153+
.map(|_resp| ())
154+
}
155+
156+
/// Deletes the specified key from the Etcd backend.
157+
///
158+
/// # Arguments
159+
///
160+
/// * `key` - The key to delete.
161+
///
162+
/// # Examples
163+
///
164+
/// ```
165+
/// use rusty_vault::storage::Backend;
166+
/// use rusty_vault::error::RvError;
167+
///
168+
/// let etcd_backend = EtcdBackend::new();
169+
/// let result = etcd_backend.delete("key");
170+
/// match result {
171+
/// Ok(()) => {
172+
/// println!("Key deleted successfully");
173+
/// },
174+
/// Err(error) => {
175+
/// eprintln!("Error: {}", error);
176+
/// }
177+
/// }
178+
/// ```
179+
fn delete(&self, key: &str) -> Result<(), RvError> {
180+
// Implementation details...
181+
let mut path = self.path.clone();
182+
let rt = tokio::runtime::Handle::current();
183+
path.push(key.to_string());
184+
185+
rt.block_on(async {
186+
let mut client = Client::connect(self.endpoints.clone(), Some(self.options.clone())).await?;
187+
client.delete(path.join("/"), None).await
188+
})
189+
.map_err(|_error| RvError::BackendError { source: EtcdError("request error".to_string()) })
190+
.map(|_resp| ())
191+
}
192+
}
193+
194+
/// Implementation of the EtcdBackend struct.
195+
impl EtcdBackend {
196+
/// Creates a new instance of EtcdBackend.
197+
///
198+
/// # Arguments
199+
///
200+
/// * `conf` - A reference to a HashMap containing configuration values.
201+
///
202+
/// # Returns
203+
///
204+
/// * `Result<Self, RvError>` - A Result containing the initialized EtcdBackend instance or an error.
205+
pub fn new(conf: &HashMap<String, Value>) -> Result<Self, RvError> {
206+
// Extract the 'path' configuration value from the HashMap, or use the default value if not present.
207+
let path = conf
208+
.get("path")
209+
.map(|p| {
210+
let mut p = p.as_str().unwrap().to_string();
211+
if !p.starts_with('/') {
212+
p.insert(0, '/');
213+
}
214+
p
215+
})
216+
.unwrap_or_else(|| ETCD_BACKEND_PATH.to_string());
217+
218+
// Extract the 'address' configuration value from the environment variable or the HashMap, or use the default value if not present.
219+
let address = env::var("ETCD_ADDR").unwrap_or_else(|_| {
220+
conf.get("address")
221+
.map(|a| a.as_str().unwrap().to_string())
222+
.unwrap_or_else(|| "http://127.0.0.1:2379".to_string())
223+
});
224+
225+
// Split the address into individual endpoints and collect them into a Vec<String>.
226+
let endpoints: Vec<String> = address.split(',').map(|s| s.to_string()).collect();
227+
228+
// Create a new ConnectOptions instance.
229+
let mut options = ConnectOptions::new();
230+
231+
// Extract the 'username' configuration value from the environment variable or the HashMap, or use an empty string if not present.
232+
let username = env::var("ETCD_USERNAME").unwrap_or_else(|_| {
233+
conf.get("username").map(|u| u.as_str().unwrap().to_string()).unwrap_or("".to_string())
234+
});
235+
236+
// Extract the 'password' configuration value from the environment variable or the HashMap, or use an empty string if not present.
237+
let password = env::var("ETCD_PASSWORD").unwrap_or_else(|_| {
238+
conf.get("password").map(|p| p.as_str().unwrap().to_string()).unwrap_or("".to_string())
239+
});
240+
241+
// Set the username and password in the ConnectOptions if they are not empty.
242+
if !username.is_empty() || !password.is_empty() {
243+
options = options.with_user(&username, &password);
244+
}
245+
246+
// Extract the 'request_timeout' configuration value from the HashMap, or use the default value if not present.
247+
let request_timeout = conf.get("request_timeout").map(|t| t.as_i64().unwrap() as i32).unwrap_or(1);
248+
249+
// Set the request timeout in the ConnectOptions.
250+
options = options.with_timeout(Duration::from_secs(request_timeout as u64));
251+
252+
// Extract the 'tls' configuration value from the HashMap, if present.
253+
if let Some(tls) = conf.get("tls") {
254+
// Extract the 'tls_ca_file' configuration value from the 'tls' HashMap, if present.
255+
let ca_file = tls.get("tls_ca_file").map(|c| c.as_str().unwrap().to_string());
256+
257+
// If 'tls_ca_file' is present, read the certificate from the file and set it in the ConnectOptions.
258+
if let Some(ca_file) = ca_file {
259+
let cert = Certificate::from_pem(&std::fs::read(ca_file.as_str()).unwrap());
260+
let tls_options = TlsOptions::new().ca_certificate(cert);
261+
options = options.with_tls(tls_options);
262+
}
263+
}
264+
265+
// Create a new instance of the Etcd client.
266+
Ok(EtcdBackend { path: path.split('/').map(String::from).collect(), endpoints, options })
267+
.map_err(|error| RvError::BackendError { source: EtcdError(format!("connect etcd error: {}", error)) })
268+
}
269+
}
270+
271+
#[cfg(test)]
272+
mod test {
273+
use std::collections::HashMap;
274+
275+
use super::{
276+
super::test::{test_backend, test_backend_list_prefix},
277+
*,
278+
};
279+
280+
#[test]
281+
fn test_etcd_backend() {
282+
let mut conf: HashMap<String, Value> = HashMap::new();
283+
conf.insert("path".to_string(), Value::String("/rusty_vault".to_string()));
284+
conf.insert("address".to_string(), Value::String("http://127.0.0.1:2379".to_string()));
285+
let backend = EtcdBackend::new(&conf);
286+
287+
assert!(backend.is_ok());
288+
289+
let backend = backend.unwrap();
290+
291+
test_backend(&backend);
292+
test_backend_list_prefix(&backend);
293+
}
294+
}

0 commit comments

Comments
 (0)