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 pub cpu_period: Option<DockerCpuPeriod>,
173
174 pub cpu_period_percent: Option<u8>,
176
177 pub cpu_time_limit: Option<DockerCpuTimeLimit>,
179
180 pub memory_limit_bytes: Option<i64>,
182
183 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#[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 pub id: Option<BackendName>,
257
258 pub cluster: Option<ClusterName>,
260
261 #[serde(default)]
263 pub pool: DronePoolName,
264
265 pub executable: Value,
267
268 pub lifetime_limit_seconds: Option<i32>,
271
272 pub max_idle_seconds: Option<i32>,
275
276 #[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 pub name: String,
292
293 #[serde(default)]
296 pub namespace: String,
297
298 #[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 #[serde(default)]
318 pub key: Option<KeyConfig>,
319
320 pub spawn_config: Option<SpawnConfig>,
322
323 pub user: Option<String>,
326
327 #[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 pub spawned: bool,
387
388 pub status: BackendStatus,
389
390 pub token: BearerToken,
391
392 pub url: String,
393
394 pub subdomain: Option<Subdomain>,
396
397 pub secret_token: Option<SecretToken>,
398
399 pub status_url: String,
400
401 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 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}