use crate::{
names::{AnyNodeName, BackendName, ControllerName, DroneName},
util::{random_prefixed_string, random_token},
PlaneClient,
};
pub use backend_state::{BackendState, BackendStatus, TerminationKind, TerminationReason};
use bollard::auth::DockerCredentials;
use chrono::Duration;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::{collections::HashMap, fmt::Display, ops::Deref, path::PathBuf, str::FromStr};
pub mod backend_state;
#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Hash, Eq)]
pub struct NodeId(i32);
impl From<i32> for NodeId {
fn from(i: i32) -> Self {
Self(i)
}
}
impl NodeId {
pub fn as_i32(&self) -> i32 {
self.0
}
}
impl Display for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_i32())
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, valuable::Valuable)]
pub struct ClusterName(String);
impl ClusterName {
pub fn is_https(&self) -> bool {
let port = self.0.split_once(':').map(|x| x.1);
port.is_none() || port == Some("443")
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Display for ClusterName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl FromStr for ClusterName {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.splitn(2, ':');
let host = parts.next().ok_or("missing hostname or ip")?;
let port = parts.next();
url::Host::parse(host).map_err(|_| "invalid hostname or ip")?;
if let Some(port) = port {
port.parse::<u16>().map_err(|_| "invalid port")?;
}
Ok(Self(s.to_string()))
}
}
#[derive(
Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, valuable::Valuable,
)]
pub struct DronePoolName(String);
impl DronePoolName {
pub fn is_default(&self) -> bool {
self == &Self::default()
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<String> for DronePoolName {
fn from(s: String) -> Self {
DronePoolName(s)
}
}
impl From<&str> for DronePoolName {
fn from(s: &str) -> Self {
DronePoolName(s.to_string())
}
}
impl Display for DronePoolName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Deref for DronePoolName {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Default, valuable::Valuable, PartialEq)]
pub enum PullPolicy {
#[default]
IfNotPresent,
Always,
Never,
}
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(transparent)]
pub struct DockerCpuPeriod(
#[serde_as(as = "serde_with::DurationMicroSeconds<u64>")] std::time::Duration,
);
impl valuable::Valuable for DockerCpuPeriod {
fn as_value(&self) -> valuable::Value {
valuable::Value::U128(self.0.as_micros())
}
fn visit(&self, visit: &mut dyn valuable::Visit) {
visit.visit_value(self.as_value())
}
}
impl Default for DockerCpuPeriod {
fn default() -> Self {
Self(std::time::Duration::from_millis(100))
}
}
impl From<&DockerCpuPeriod> for std::time::Duration {
fn from(value: &DockerCpuPeriod) -> Self {
value.0
}
}
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(transparent)]
pub struct DockerCpuTimeLimit(
#[serde_as(as = "serde_with::DurationSeconds<u64>")] pub std::time::Duration,
);
impl valuable::Valuable for DockerCpuTimeLimit {
fn as_value(&self) -> valuable::Value {
valuable::Value::U64(self.0.as_secs())
}
fn visit(&self, visit: &mut dyn valuable::Visit) {
visit.visit_value(self.as_value())
}
}
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, valuable::Valuable)]
pub struct ResourceLimits {
pub cpu_period: Option<DockerCpuPeriod>,
pub cpu_period_percent: Option<u8>,
pub cpu_time_limit: Option<DockerCpuTimeLimit>,
pub memory_limit_bytes: Option<i64>,
pub disk_limit_bytes: Option<i64>,
}
impl ResourceLimits {
pub fn cpu_quota(&self) -> Option<std::time::Duration> {
let pc = self.cpu_period_percent?;
let cpu_period = self.cpu_period.clone().unwrap_or_default();
let quota = cpu_period.0.mul_f64((pc as f64) / 100.0);
Some(quota)
}
}
#[derive(Clone, Serialize, Deserialize, Debug, valuable::Valuable, PartialEq)]
#[serde(untagged)]
pub enum DockerRegistryAuth {
UsernamePassword { username: String, password: String },
}
impl From<DockerRegistryAuth> for DockerCredentials {
fn from(
DockerRegistryAuth::UsernamePassword { username, password }: DockerRegistryAuth,
) -> Self {
DockerCredentials {
username: Some(username),
password: Some(password),
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, valuable::Valuable, PartialEq)]
#[serde(untagged)]
pub enum Mount {
Bool(bool),
Path(PathBuf),
}
#[derive(Clone, Serialize, Deserialize, Debug, valuable::Valuable, PartialEq)]
pub struct DockerExecutorConfig {
pub image: String,
pub pull_policy: Option<PullPolicy>,
pub credentials: Option<DockerRegistryAuth>,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default)]
pub resource_limits: ResourceLimits,
pub mount: Option<Mount>,
pub network_name: Option<String>,
}
impl DockerExecutorConfig {
pub fn from_image_with_defaults<T: Into<String>>(image: T) -> Self {
Self {
image: image.into(),
pull_policy: None,
env: HashMap::default(),
resource_limits: ResourceLimits::default(),
credentials: None,
mount: None,
network_name: None,
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct SpawnConfig {
pub id: Option<BackendName>,
pub cluster: Option<ClusterName>,
#[serde(default)]
pub pool: DronePoolName,
pub executable: Value,
pub lifetime_limit_seconds: Option<i32>,
pub max_idle_seconds: Option<i32>,
#[serde(default)]
pub use_static_token: bool,
pub subdomain: Option<Subdomain>,
}
#[derive(
Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq, Hash, valuable::Valuable,
)]
pub struct KeyConfig {
pub name: String,
#[serde(default)]
pub namespace: String,
#[serde(default)]
pub tag: String,
}
impl KeyConfig {
pub fn new_random() -> Self {
Self {
name: random_prefixed_string("lk"),
..Default::default()
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct ConnectRequest {
#[serde(default)]
pub key: Option<KeyConfig>,
pub spawn_config: Option<SpawnConfig>,
pub user: Option<String>,
#[serde(default)]
pub auth: Map<String, Value>,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, valuable::Valuable)]
pub struct BearerToken(String);
const STATIC_TOKEN_PREFIX: &str = "s.";
impl BearerToken {
pub fn new_random_static() -> Self {
Self(format!("{}{}", STATIC_TOKEN_PREFIX, random_token()))
}
pub fn is_static(&self) -> bool {
self.0.starts_with(STATIC_TOKEN_PREFIX)
}
}
impl From<String> for BearerToken {
fn from(s: String) -> Self {
Self(s)
}
}
impl Display for BearerToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, valuable::Valuable)]
pub struct SecretToken(String);
impl From<String> for SecretToken {
fn from(s: String) -> Self {
Self(s)
}
}
impl Display for SecretToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
impl SecretToken {
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ConnectResponse {
pub backend_id: BackendName,
pub spawned: bool,
pub status: BackendStatus,
pub token: BearerToken,
pub url: String,
pub subdomain: Option<Subdomain>,
pub secret_token: Option<SecretToken>,
pub status_url: String,
pub drone: Option<DroneName>,
}
impl ConnectResponse {
#[allow(clippy::too_many_arguments)]
pub fn new(
backend_id: BackendName,
cluster: &ClusterName,
spawned: bool,
status: BackendStatus,
token: BearerToken,
secret_token: Option<SecretToken>,
subdomain: Option<Subdomain>,
client: &PlaneClient,
drone: Option<DroneName>,
) -> Self {
let protocol = if cluster.is_https() { "https" } else { "http" };
let url = if let Some(subdomain) = &subdomain {
format!("{}://{}.{}/{}/", protocol, subdomain, cluster, token)
} else {
format!("{}://{}/{}/", protocol, cluster, token)
};
let status_url = client.backend_status_url(&backend_id).to_string();
Self {
backend_id,
spawned,
status,
token,
url,
subdomain,
secret_token,
status_url,
drone,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum NodeKind {
Proxy,
Drone,
AcmeDnsServer,
}
impl Display for NodeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let result = serde_json::to_value(self);
match result {
Ok(Value::String(v)) => write!(f, "{}", v),
_ => unreachable!(),
}
}
}
impl TryFrom<String> for NodeKind {
type Error = serde_json::Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
serde_json::from_value(Value::String(s))
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct RevokeRequest {
pub backend_id: BackendName,
pub user: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DrainResult {
pub updated: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct DroneState {
pub ready: bool,
pub draining: bool,
#[serde(with = "crate::serialization::serialize_duration_as_seconds")]
pub last_heartbeat_age: Duration,
pub backend_count: u32,
pub node: NodeState,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct NodeState {
pub name: AnyNodeName,
pub plane_version: String,
pub plane_hash: String,
pub controller: ControllerName,
#[serde(with = "crate::serialization::serialize_duration_as_seconds")]
pub controller_heartbeat_age: Duration,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct ClusterState {
pub drones: Vec<DroneState>,
pub proxies: Vec<NodeState>,
}
#[derive(thiserror::Error, Debug)]
#[error("Invalid subdomain: {0}")]
pub struct InvalidSubdomain(String);
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash, valuable::Valuable)]
pub struct Subdomain(String);
impl std::str::FromStr for Subdomain {
type Err = InvalidSubdomain;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let valid_subdomain = s.chars().all(|c| c.is_alphanumeric() || c == '-')
&& !s.starts_with('-')
&& !s.ends_with('-');
if valid_subdomain {
Ok(Subdomain(s.to_string()))
} else {
Err(InvalidSubdomain(s.to_string()))
}
}
}
impl Display for Subdomain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
impl TryFrom<String> for Subdomain {
type Error = InvalidSubdomain;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse::<Subdomain>()
}
}
impl Deref for Subdomain {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}