Skip to content

Commit 40913ce

Browse files
feat: headscale commands
1 parent 0d83e0f commit 40913ce

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

src/commands/headscale_cmd.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use futures::{StreamExt, TryStreamExt};
2+
use k8s_openapi::api::core::v1::Pod;
3+
use kube::api::{AttachParams, AttachedProcess};
4+
use kube::{Api, Client};
5+
use sea_orm::ColIdx;
6+
#[derive(Debug, Clone)]
7+
struct HeadscalePod {
8+
name: String,
9+
namespace: String,
10+
}
11+
12+
impl HeadscalePod {
13+
fn new() -> Self {
14+
let name = std::env::var("HEADSCALE_POD_NAME").expect("HEADSCALE_POD_NAME not set");
15+
let namespace =
16+
std::env::var("HEADSCALE_POD_NAMESPACE").expect("HEADSCALE_POD_NAMESPACE not set");
17+
HeadscalePod { name, namespace }
18+
}
19+
}
20+
21+
async fn get_output(mut attached: AttachedProcess) -> String {
22+
let stdout = tokio_util::io::ReaderStream::new(attached.stdout().unwrap());
23+
let out = stdout
24+
.filter_map(|r| async { r.ok().and_then(|v| String::from_utf8(v.to_vec()).ok()) })
25+
.collect::<Vec<_>>()
26+
.await
27+
.join("");
28+
attached.join().await.unwrap();
29+
out
30+
}
31+
32+
async fn headscale_cmd(cmd: Vec<&str>) -> Result<String, Box<dyn std::error::Error>> {
33+
let headscale_pod = HeadscalePod::new();
34+
35+
let client = Client::try_default().await?;
36+
let api: Api<Pod> = Api::namespaced(client, headscale_pod.namespace.as_str());
37+
38+
let attached = api
39+
.exec(
40+
headscale_pod.name.as_str(),
41+
cmd,
42+
&AttachParams::default().stderr(false),
43+
)
44+
.await?;
45+
let output = get_output(attached).await;
46+
Ok(output)
47+
}
48+
49+
pub async fn create_api_key(expiration: &str) -> Result<String, Box<dyn std::error::Error>> {
50+
let cmd = vec!["headscale", "apikeys", "create", "--expiration", expiration];
51+
let api_key = headscale_cmd(cmd.into()).await?;
52+
Ok(api_key)
53+
}
54+
55+
pub async fn validate_api_key(prefix: &str) -> Result<bool, Box<dyn std::error::Error>> {
56+
let cmd = vec!["headscale", "apikeys", "list", "-o", "json"];
57+
let output = headscale_cmd(cmd).await?;
58+
let api_keys = serde_json::from_str::<Vec<serde_json::Value>>(&output)?;
59+
60+
let cmd = vec!["date", "+%s%N"];
61+
let timestamp = headscale_cmd(cmd).await?;
62+
63+
for apikey in api_keys {
64+
if let Some(other_prefix) = apikey.get("prefix") {
65+
if other_prefix.as_str() == Some(prefix) {
66+
println!("API key with prefix {} found", prefix);
67+
68+
return if let Some(expiration) = apikey.get("expiration") {
69+
let seconds = expiration
70+
.get("seconds")
71+
.expect("API key has no expiration seconds");
72+
let nanos = expiration
73+
.get("nanos")
74+
.expect("API key has no expiration nanos");
75+
let expiration_time =
76+
seconds.as_i64().unwrap() * 1_000_000_000 + nanos.as_i64().unwrap();
77+
78+
if expiration_time < timestamp.parse::<i64>().unwrap() {
79+
println!("API key has expired");
80+
Ok(false)
81+
} else {
82+
println!("API key is valid");
83+
Ok(true)
84+
}
85+
} else {
86+
Err(format!("API key with prefix {} has no expiration time", prefix).into())
87+
};
88+
}
89+
}
90+
}
91+
println!("API key with prefix {} not found", prefix);
92+
Ok(false)
93+
}
94+
95+
pub async fn revoke_api_key(prefix: &str) -> Result<(), Box<dyn std::error::Error>> {
96+
let cmd = vec!["headscale", "apikeys", "expire", "--prefix", prefix];
97+
headscale_cmd(cmd).await?;
98+
Ok(())
99+
}

src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod daemon_cmd;
55
pub mod delete_cmd;
66
pub mod exec_cmd;
77
pub mod get_cmd;
8+
mod headscale_cmd;
89
pub mod log_cmd;
910
pub mod login_cmd;
1011
pub mod proxy_cmd;

0 commit comments

Comments
 (0)