diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-06 02:27:06 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-06 02:27:06 +0300 |
| commit | 9e9bc8a144667a6b7debfd4f0c7a31ff0e11493a (patch) | |
| tree | 03f1259f91fd9edeacfd4b904f2e84d4acf25232 /src/client/endpoint.rs | |
| parent | 35b0dff96579f099e0436290085024424ffb289c (diff) | |
feat: manage multiple ssh machines from one client (#3670)
Diffstat (limited to 'src/client/endpoint.rs')
| -rw-r--r-- | src/client/endpoint.rs | 120 |
1 files changed, 120 insertions, 0 deletions
diff --git a/src/client/endpoint.rs b/src/client/endpoint.rs new file mode 100644 index 00000000..77aaae3b --- /dev/null +++ b/src/client/endpoint.rs @@ -0,0 +1,120 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; +use sha2::{Digest as _, Sha256}; + +mod activation; +mod catalog; +mod control; +mod health; +mod message_policy; +mod registry; +mod supervisor; +mod writer; + +pub(crate) use activation::*; +pub(crate) use catalog::*; +pub(crate) use control::*; +pub(crate) use message_policy::*; +pub(crate) use registry::*; +pub(crate) use supervisor::*; +pub(crate) use writer::NativeEndpointTransport; + +const PROFILE_ID_BYTES: usize = 16; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub(crate) struct ProfileId(String); + +impl ProfileId { + pub(crate) fn parse(value: impl Into<String>) -> Result<Self, String> { + let value = value.into(); + if value.len() != PROFILE_ID_BYTES * 2 + || !value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err("endpoint profile id must be 32 lowercase hexadecimal characters".into()); + } + Ok(Self(value)) + } + + pub(crate) fn generate() -> Self { + use std::sync::atomic::{AtomicU64, Ordering}; + static NEXT_ID: AtomicU64 = AtomicU64::new(1); + + let sequence = NEXT_ID.fetch_add(1, Ordering::Relaxed); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let digest = Sha256::digest(format!("{}:{now}:{sequence}", std::process::id()).as_bytes()); + Self( + digest[..PROFILE_ID_BYTES] + .iter() + .map(|byte| format!("{byte:02x}")) + .collect(), + ) + } + + pub(crate) fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for ProfileId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.0) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum ClientEndpointId { + Local, + Ssh(ProfileId), +} + +impl ClientEndpointId { + pub(crate) fn is_local(&self) -> bool { + matches!(self, Self::Local) + } + + pub(crate) fn storage_key(&self) -> String { + match self { + Self::Local => "local".into(), + Self::Ssh(profile_id) => format!("ssh:{profile_id}"), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ClientEndpointStatus { + Connecting, + Online, + Reconnecting, + Attention, + Disabled, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn profile_ids_are_opaque_and_stable_when_parsed() { + let first = ProfileId::generate(); + let second = ProfileId::generate(); + assert_ne!(first, second); + assert_eq!(ProfileId::parse(first.to_string()).unwrap(), first); + assert_eq!(first.as_str().len(), 32); + } + + #[test] + fn endpoint_storage_keys_do_not_contain_ssh_targets() { + let profile = ProfileId::parse("0123456789abcdef0123456789abcdef").unwrap(); + assert_eq!( + ClientEndpointId::Ssh(profile).storage_key(), + "ssh:0123456789abcdef0123456789abcdef" + ); + } +} |
