plane_common/types/
mod.rs

1use crate::{
2    names::{AnyNodeName, BackendName, ControllerName, DroneName},
3    util::{random_prefixed_string, random_token},
4    PlaneClient,
5};
6pub use backend_state::{BackendState, BackendStatus, TerminationKind, TerminationReason};
7use bollard::auth::DockerCredentials;
8use chrono::Duration;
9use serde::{Deserialize, Serialize};
10use serde_json::{Map, Value};
11use std::{collections::HashMap, fmt::Display, ops::Deref, path::PathBuf, str::FromStr};
12
13pub mod backend_state;
14
15#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Hash, Eq)]
16pub struct NodeId(i32);
17
18impl From<i32> for NodeId {
19    fn from(i: i32) -> Self {
20        Self(i)
21    }
22}
23
24impl NodeId {
25    pub fn as_i32(&self) -> i32 {
26        self.0
27    }
28}
29
30impl Display for NodeId {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        write!(f, "{}", self.as_i32())
33    }
34}
35
36#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, valuable::Valuable)]
37pub struct ClusterName(String);
38
39impl ClusterName {
40    pub fn is_https(&self) -> bool {
41        let port = self.0.split_once(':').map(|x| x.1);
42        port.is_none() || port == Some("443")
43    }
44
45    pub fn as_str(&self) -> &str {
46        &self.0
47    }
48}
49
50impl Display for ClusterName {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "{}", self.as_str())
53    }
54}
55
56impl FromStr for ClusterName {
57    type Err = &'static str;
58
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        let mut parts = s.splitn(2, ':');
61        let host = parts.next().ok_or("missing hostname or ip")?;
62        let port = parts.next();
63
64        url::Host::parse(host).map_err(|_| "invalid hostname or ip")?;
65        if let Some(port) = port {
66            port.parse::<u16>().map_err(|_| "invalid port")?;
67        }
68
69        Ok(Self(s.to_string()))
70    }
71}
72
73#[derive(
74    Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, valuable::Valuable,
75)]
76pub struct DronePoolName(String);
77
78impl DronePoolName {
79    pub fn is_default(&self) -> bool {
80        self == &Self::default()
81    }
82
83    pub fn as_str(&self) -> &str {
84        &self.0
85    }
86}
87
88impl From<String> for DronePoolName {
89    fn from(s: String) -> Self {
90        DronePoolName(s)
91    }
92}
93
94impl From<&str> for DronePoolName {
95    fn from(s: &str) -> Self {
96        DronePoolName(s.to_string())
97    }
98}
99
100impl Display for DronePoolName {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        write!(f, "{}", self.0)
103    }
104}
105
106impl Deref for DronePoolName {
107    type Target = str;
108
109    fn deref(&self) -> &Self::Target {
110        &self.0
111    }
112}
113
114#[derive(Clone, Copy, Serialize, Deserialize, Debug, Default, valuable::Valuable, PartialEq)]
115pub enum PullPolicy {
116    #[default]
117    IfNotPresent,
118    Always,
119    Never,
120}
121
122#[serde_with::serde_as]
123#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
124#[serde(transparent)]
125pub struct DockerCpuPeriod(
126    #[serde_as(as = "serde_with::DurationMicroSeconds<u64>")] std::time::Duration,
127);
128
129impl valuable::Valuable for DockerCpuPeriod {
130    fn as_value(&self) -> valuable::Value {
131        valuable::Value::U128(self.0.as_micros())
132    }
133
134    fn visit(&self, visit: &mut dyn valuable::Visit) {
135        visit.visit_value(self.as_value())
136    }
137}
138
139impl Default for DockerCpuPeriod {
140    fn default() -> Self {
141        Self(std::time::Duration::from_millis(100))
142    }
143}
144
145impl From<&DockerCpuPeriod> for std::time::Duration {
146    fn from(value: &DockerCpuPeriod) -> Self {
147        value.0
148    }
149}
150
151#[serde_with::serde_as]
152#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
153#[serde(transparent)]
154pub struct DockerCpuTimeLimit(
155    #[serde_as(as = "serde_with::DurationSeconds<u64>")] pub std::time::Duration,
156);
157
158impl valuable::Valuable for DockerCpuTimeLimit {
159    fn as_value(&self) -> valuable::Value {
160        valuable::Value::U64(self.0.as_secs())
161    }
162
163    fn visit(&self, visit: &mut dyn valuable::Visit) {
164        visit.visit_value(self.as_value())
165    }
166}
167
168#[serde_with::serde_as]
169#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, valuable::Valuable)]
170pub struct ResourceLimits {
171    /// Period of cpu time (de/serializes as microseconds)
172    pub cpu_period: Option<DockerCpuPeriod>,
173
174    /// Proportion of period used by container
175    pub cpu_period_percent: Option<u8>,
176
177    /// Total cpu time allocated to container
178    pub cpu_time_limit: Option<DockerCpuTimeLimit>,
179
180    /// Maximum amount of memory container can use (in bytes)
181    pub memory_limit_bytes: Option<i64>,
182
183    /// Maximum disk space container can use (in bytes)
184    pub disk_limit_bytes: Option<i64>,
185}
186
187impl ResourceLimits {
188    pub fn cpu_quota(&self) -> Option<std::time::Duration> {
189        let pc = self.cpu_period_percent?;
190        let cpu_period = self.cpu_period.clone().unwrap_or_default();
191
192        let quota = cpu_period.0.mul_f64((pc as f64) / 100.0);
193        Some(quota)
194    }
195}
196
197#[derive(Clone, Serialize, Deserialize, Debug, valuable::Valuable, PartialEq)]
198#[serde(untagged)]
199pub enum DockerRegistryAuth {
200    UsernamePassword { username: String, password: String },
201}
202
203impl From<DockerRegistryAuth> for DockerCredentials {
204    fn from(
205        DockerRegistryAuth::UsernamePassword { username, password }: DockerRegistryAuth,
206    ) -> Self {
207        DockerCredentials {
208            username: Some(username),
209            password: Some(password),
210            ..Default::default()
211        }
212    }
213}
214
215// A spawn requestor can provide a mount parameter, which can be a string or a boolean.
216#[derive(Debug, Clone, Serialize, Deserialize, valuable::Valuable, PartialEq)]
217#[serde(untagged)]
218pub enum Mount {
219    Bool(bool),
220    Path(PathBuf),
221}
222
223#[derive(Clone, Serialize, Deserialize, Debug, valuable::Valuable, PartialEq)]
224pub struct DockerExecutorConfig {
225    pub image: String,
226    pub pull_policy: Option<PullPolicy>,
227    pub credentials: Option<DockerRegistryAuth>,
228    #[serde(default)]
229    pub env: HashMap<String, String>,
230    #[serde(default)]
231    pub resource_limits: ResourceLimits,
232    pub mount: Option<Mount>,
233    pub network_name: Option<String>,
234}
235
236impl DockerExecutorConfig {
237    pub fn from_image_with_defaults<T: Into<String>>(image: T) -> Self {
238        Self {
239            image: image.into(),
240            pull_policy: None,
241            env: HashMap::default(),
242            resource_limits: ResourceLimits::default(),
243            credentials: None,
244            mount: None,
245            network_name: None,
246        }
247    }
248}
249
250#[derive(Clone, Serialize, Deserialize, Debug)]
251pub struct SpawnConfig {
252    /// ID to assign to the new backend. Must be unique.
253    /// This should only be used if you really need it, otherwise you can leave it blank
254    /// and let Plane assign a unique ID automatically. This may be removed from
255    /// future versions of Plane.
256    pub id: Option<BackendName>,
257
258    /// Cluster to spawn to. Uses the controller default if not provided.
259    pub cluster: Option<ClusterName>,
260
261    /// The drone pool to use for the connect request.
262    #[serde(default)]
263    pub pool: DronePoolName,
264
265    /// Config to use to spawn the backend process.
266    pub executable: Value,
267
268    /// If provided, the maximum amount of time the backend will be allowed to
269    /// stay alive. Time counts from when the backend is scheduled.
270    pub lifetime_limit_seconds: Option<i32>,
271
272    /// If provided, the maximum amount of time the backend will be allowed to
273    /// stay alive with no inbound connections to it.
274    pub max_idle_seconds: Option<i32>,
275
276    /// If true, the backend will have a single connection token associated with it at spawn
277    /// time instead of dynamic tokens for each user.
278    #[serde(default)]
279    pub use_static_token: bool,
280
281    pub subdomain: Option<Subdomain>,
282}
283
284#[derive(
285    Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq, Hash, valuable::Valuable,
286)]
287pub struct KeyConfig {
288    /// If provided, and a running backend was created with the same key,
289    /// namespace, and tag, we will connect to that backend instead
290    /// of creating a new one.
291    pub name: String,
292
293    /// Namespace of the key. If not specified, the default namespace (empty string)
294    /// is used.
295    #[serde(default)]
296    pub namespace: String,
297
298    /// If we request a connection to a key and the backend for that key
299    /// is running, we will only connect to it if the tag matches the tag
300    /// of the connection request that created it.
301    #[serde(default)]
302    pub tag: String,
303}
304
305impl KeyConfig {
306    pub fn new_random() -> Self {
307        Self {
308            name: random_prefixed_string("lk"),
309            ..Default::default()
310        }
311    }
312}
313
314#[derive(Clone, Serialize, Deserialize, Debug, Default)]
315pub struct ConnectRequest {
316    /// Configuration for the key to use.
317    #[serde(default)]
318    pub key: Option<KeyConfig>,
319
320    /// Config to use if we need to create a new backend to connect to.
321    pub spawn_config: Option<SpawnConfig>,
322
323    /// Username or other identifier to associate with the generated connection URL.
324    /// Passed to the backend through the X-Plane-User header.
325    pub user: Option<String>,
326
327    /// Arbitrary JSON object to pass along with each request to the backend.
328    /// Passed to the backend through the X-Plane-Auth header.
329    #[serde(default)]
330    pub auth: Map<String, Value>,
331}
332
333#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, valuable::Valuable)]
334pub struct BearerToken(String);
335
336const STATIC_TOKEN_PREFIX: &str = "s.";
337
338impl BearerToken {
339    pub fn new_random_static() -> Self {
340        Self(format!("{}{}", STATIC_TOKEN_PREFIX, random_token()))
341    }
342
343    pub fn is_static(&self) -> bool {
344        self.0.starts_with(STATIC_TOKEN_PREFIX)
345    }
346}
347
348impl From<String> for BearerToken {
349    fn from(s: String) -> Self {
350        Self(s)
351    }
352}
353
354impl Display for BearerToken {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        write!(f, "{}", &self.0)
357    }
358}
359
360#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, valuable::Valuable)]
361pub struct SecretToken(String);
362
363impl From<String> for SecretToken {
364    fn from(s: String) -> Self {
365        Self(s)
366    }
367}
368
369impl Display for SecretToken {
370    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371        write!(f, "{}", &self.0)
372    }
373}
374
375impl SecretToken {
376    pub fn as_str(&self) -> &str {
377        &self.0
378    }
379}
380
381#[derive(Clone, Serialize, Deserialize, Debug)]
382pub struct ConnectResponse {
383    pub backend_id: BackendName,
384
385    /// Whether the backend is a new one spawned due to the request.
386    pub spawned: bool,
387
388    pub status: BackendStatus,
389
390    pub token: BearerToken,
391
392    pub url: String,
393
394    /// Subdomain associated with the backend, if any.
395    pub subdomain: Option<Subdomain>,
396
397    pub secret_token: Option<SecretToken>,
398
399    pub status_url: String,
400
401    /// The drone that spawned this backend, if the request resulted in a spawn.
402    pub drone: Option<DroneName>,
403}
404
405impl ConnectResponse {
406    #[allow(clippy::too_many_arguments)]
407    pub fn new(
408        backend_id: BackendName,
409        cluster: &ClusterName,
410        spawned: bool,
411        status: BackendStatus,
412        token: BearerToken,
413        secret_token: Option<SecretToken>,
414        subdomain: Option<Subdomain>,
415        client: &PlaneClient,
416        drone: Option<DroneName>,
417    ) -> Self {
418        let protocol = if cluster.is_https() { "https" } else { "http" };
419        let url = if let Some(subdomain) = &subdomain {
420            format!("{}://{}.{}/{}/", protocol, subdomain, cluster, token)
421        } else {
422            format!("{}://{}/{}/", protocol, cluster, token)
423        };
424
425        let status_url = client.backend_status_url(&backend_id).to_string();
426
427        Self {
428            backend_id,
429            spawned,
430            status,
431            token,
432            url,
433            subdomain,
434            secret_token,
435            status_url,
436            drone,
437        }
438    }
439}
440
441#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug)]
442pub enum NodeKind {
443    Proxy,
444    Drone,
445    AcmeDnsServer,
446}
447
448impl Display for NodeKind {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450        let result = serde_json::to_value(self);
451        match result {
452            Ok(Value::String(v)) => write!(f, "{}", v),
453            _ => unreachable!(),
454        }
455    }
456}
457
458impl TryFrom<String> for NodeKind {
459    type Error = serde_json::Error;
460
461    fn try_from(s: String) -> Result<Self, Self::Error> {
462        serde_json::from_value(Value::String(s))
463    }
464}
465
466#[derive(Clone, Serialize, Deserialize, Debug)]
467pub struct RevokeRequest {
468    pub backend_id: BackendName,
469    pub user: String,
470}
471
472#[derive(Serialize, Deserialize, Debug, Clone)]
473pub struct DrainResult {
474    pub updated: bool,
475}
476
477#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
478pub struct DroneState {
479    pub ready: bool,
480    pub draining: bool,
481    #[serde(with = "crate::serialization::serialize_duration_as_seconds")]
482    pub last_heartbeat_age: Duration,
483    pub backend_count: u32,
484    pub node: NodeState,
485}
486
487#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
488pub struct NodeState {
489    pub name: AnyNodeName,
490    pub plane_version: String,
491    pub plane_hash: String,
492    pub controller: ControllerName,
493    #[serde(with = "crate::serialization::serialize_duration_as_seconds")]
494    pub controller_heartbeat_age: Duration,
495}
496
497#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
498pub struct ClusterState {
499    pub drones: Vec<DroneState>,
500    pub proxies: Vec<NodeState>,
501}
502
503#[derive(thiserror::Error, Debug)]
504#[error("Invalid subdomain: {0}")]
505pub struct InvalidSubdomain(String);
506
507#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, valuable::Valuable)]
508pub struct Subdomain(String);
509
510impl std::str::FromStr for Subdomain {
511    type Err = InvalidSubdomain;
512
513    fn from_str(s: &str) -> Result<Self, Self::Err> {
514        // Subdomains can consist of a-z, 0-9, and '-' but not as the first or last char
515        let valid_subdomain = s.chars().all(|c| c.is_alphanumeric() || c == '-')
516            && !s.starts_with('-')
517            && !s.ends_with('-');
518        if valid_subdomain {
519            Ok(Subdomain(s.to_string()))
520        } else {
521            Err(InvalidSubdomain(s.to_string()))
522        }
523    }
524}
525
526impl Display for Subdomain {
527    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528        write!(f, "{}", &self.0)
529    }
530}
531
532impl TryFrom<String> for Subdomain {
533    type Error = InvalidSubdomain;
534
535    fn try_from(s: String) -> Result<Self, Self::Error> {
536        s.parse::<Subdomain>()
537    }
538}
539
540impl Deref for Subdomain {
541    type Target = str;
542
543    fn deref(&self) -> &Self::Target {
544        &self.0
545    }
546}