Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Test

on:
pull_request:

permissions:
contents: read

concurrency:
group: test-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
linux:
name: Linux tests
runs-on: ubuntu-24.04

steps:
- name: Checkout sources
uses: actions/checkout@v7

- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: 1.97.0
target: x86_64-unknown-linux-gnu
components: rustfmt, clippy
cache-key: x86_64-unknown-linux-gnu-test

- name: Check formatting
run: cargo fmt --all -- --check

- name: Run Clippy
run: cargo clippy -p wings-rs --bin wings-rs -- -D warnings

- name: Install cross
run: cargo install cross --git https://github.com/cross-rs/cross --rev 8c1a8aa4b661711f4b7b6ac07c2e8929ce2f7d27 --locked

- name: Run Linux tests
run: cross test --release --target x86_64-unknown-linux-gnu
env:
JEMALLOC_SYS_WITH_LG_PAGE: 12
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions application/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ utoipa = { version = "5.4.0", features = ["axum_extras", "preserve_order", "chro
utoipa-axum = "0.2.0"
reqwest = { workspace = true }
url = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["io-util"] }
chrono = { version = "0.4.40", features = ["serde"] }
colored = "3.0.0"
jsonwebtoken = { version = "11.0.0", features = ["aws_lc_rs"] }
Expand Down Expand Up @@ -59,7 +59,7 @@ dialoguer = { version = "0.12.0", default-features = false }
sysinfo = { version = "0.39.0", default-features = false, features = ["disk", "network", "system", "user"] }
crc32fast = { workspace = true }
cap-std = "4.0.0"
rustix = { version = "1.0.7", features = ["fs", "process", "net", "thread"] }
rustix = { version = "1.0.7", features = ["fs", "process", "net", "thread", "pty"] }
cidr = { version = "0.3.1", features = ["serde"] }
hickory-resolver = { workspace = true }
constant_time_eq = "0.6.0"
Expand All @@ -73,6 +73,7 @@ zstd = { workspace = true }
base64 = "0.23.0"
chunked_vec = "=0.5.1"
async-channel = "2.5.0"
shell-words = "1.1.0"
lzzzz = "2.0.0"
xmltree = { version = "0.12.0", features = ["attribute-order"] }
xml = "1.3.0"
Expand Down
2 changes: 2 additions & 0 deletions application/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ fn handle_git_info() {
{
git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
}
} else {
println!("cargo:rustc-env=CARGO_GIT_BRANCH=unknown");
}
println!("cargo:rustc-env=CARGO_GIT_COMMIT={git_hash}");
}
Expand Down
227 changes: 226 additions & 1 deletion application/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::{
io::{BufRead, BufWriter, IsTerminal},
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
sync::{Arc, OnceLock},
};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{
Expand All @@ -25,6 +25,9 @@ use utoipa::ToSchema;
fn app_name() -> String {
"Calagopus".to_string()
}
fn default_true() -> bool {
true
}
fn api_host() -> String {
"0.0.0.0".to_string()
}
Expand Down Expand Up @@ -726,6 +729,93 @@ impl From<String> for SystemPath {
}
}

#[derive(ToSchema, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Debug, Default)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeBackend {
#[default]
Auto,
Docker,
PveLxc,
}

#[derive(ToSchema, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Debug, Default)]
#[serde(rename_all = "snake_case")]
pub enum PveLxcFirewallBackend {
#[default]
Auto,
Proxmox,
Nftables,
Iptables,
Disabled,
}

fn pve_lxc_pct_path() -> String {
"/usr/sbin/pct".to_string()
}

fn pve_lxc_pvesh_path() -> String {
"/usr/bin/pvesh".to_string()
}

fn pve_lxc_pveversion_path() -> String {
"/usr/bin/pveversion".to_string()
}
fn pve_lxc_pvesm_path() -> String {
"/usr/sbin/pvesm".to_string()
}

fn pve_lxc_lxc_attach_path() -> String {
"/usr/bin/lxc-attach".to_string()
}

fn pve_lxc_lxc_stop_path() -> String {
"/usr/bin/lxc-stop".to_string()
}

fn pve_lxc_perl_path() -> String {
"/usr/bin/perl".to_string()
}

fn pve_lxc_template_storage() -> String {
"local".to_string()
}

fn pve_lxc_rootfs_storage() -> String {
"auto".to_string()
}

fn pve_lxc_bridge() -> String {
"vmbr0".to_string()
}

fn pve_lxc_edge_wireguard_interface() -> String {
"wg-calagopus".to_string()
}

fn pve_lxc_edge_ssh_user() -> String {
"root".to_string()
}

fn pve_lxc_rootfs_size_gib() -> u64 {
8
}

fn pve_lxc_min_vmid() -> u32 {
200
}

fn pve_lxc_console_log_max_bytes() -> u64 {
5 * 1024 * 1024
}

fn pve_lxc_tag_prefix() -> String {
"calagopus".to_string()
}

fn pve_lxc_managed_file_directory() -> SystemPath {
SystemPath::new("/var/lib/lxc/calagopus-wings-managed")
}

nestify::nest! {
#[derive(ToSchema, Deserialize, Serialize, DefaultFromSerde)]
pub struct InnerConfig {
Expand All @@ -741,6 +831,99 @@ nestify::nest! {
#[serde(default)]
pub token: String,

#[serde(default)]
#[schema(inline)]
pub runtime: #[derive(ToSchema, Deserialize, Serialize, DefaultFromSerde)] #[serde(default)] pub struct Runtime {
#[serde(default)]
pub backend: RuntimeBackend,

#[serde(default)]
#[schema(inline)]
pub pve_lxc: #[derive(Clone, ToSchema, Deserialize, Serialize, DefaultFromSerde)] #[serde(default)] pub struct PveLxcRuntime {
#[serde(default = "pve_lxc_pct_path")]
pub pct_path: String,
#[serde(default = "pve_lxc_pvesh_path")]
pub pvesh_path: String,
#[serde(default = "pve_lxc_pveversion_path")]
pub pveversion_path: String,
#[serde(default = "pve_lxc_pvesm_path")]
pub pvesm_path: String,
#[serde(default = "pve_lxc_lxc_attach_path")]
pub lxc_attach_path: String,
#[serde(default = "pve_lxc_lxc_stop_path")]
pub lxc_stop_path: String,
#[serde(default = "pve_lxc_perl_path")]
pub perl_path: String,

/// Empty means discover the local Proxmox node name at runtime.
#[serde(default)]
pub node: String,
#[serde(default = "pve_lxc_template_storage")]
pub template_storage: String,
#[serde(default = "pve_lxc_rootfs_storage")]
pub rootfs_storage: String,
#[serde(default = "pve_lxc_bridge")]
pub bridge: String,
/// Optional VLAN tag applied to each LXC veth on `bridge`.
#[serde(default)]
pub vlan_tag: Option<u16>,
/// IPv4 CIDR prefix used by static panel allocations (for example, 24).
#[serde(default)]
pub network_prefix: Option<u8>,
/// IPv4 gateway used by static panel allocations on the LXC network.
#[serde(default)]
pub gateway: Option<String>,
/// WireGuard interface whose peers publish private LXC allocations.
/// The panel allocation IP is matched against each peer endpoint.
#[serde(default = "pve_lxc_edge_wireguard_interface")]
pub edge_wireguard_interface: String,
/// SSH account used by the restricted edge forwarding key.
#[serde(default = "pve_lxc_edge_ssh_user")]
pub edge_ssh_user: String,
/// Restricted SSH key used only to synchronize edge port forwards.
#[serde(default)]
pub edge_ssh_identity_path: String,
/// Pinned SSH known-hosts file for the edge host.
#[serde(default)]
pub edge_known_hosts_path: String,
#[serde(default = "pve_lxc_rootfs_size_gib")]
pub rootfs_size_gib: u64,
/// Maximum retained console bytes for each LXC server. When the
/// limit is reached Wings starts a fresh bounded log.
#[serde(default = "pve_lxc_console_log_max_bytes")]
pub console_log_max_bytes: u64,
#[serde(default = "pve_lxc_tag_prefix")]
pub tag_prefix: String,
/// Host-visible staging directory used for read-only managed file binds.
/// Its parent path must be traversable by unprivileged LXC mount setup.
#[serde(default = "pve_lxc_managed_file_directory")]
pub managed_file_directory: SystemPath,
#[serde(default = "default_true")]
pub unprivileged: bool,
/// Maximum process count for LXC workloads. When omitted, the
/// legacy Docker limit is used for configuration compatibility.
#[serde(default)]
pub pids_limit: Option<u64>,
/// Minimum VMID used when Wings auto-allocates Proxmox
/// containers. PVE itself starts at 100; raising the floor
/// (for example to 200) keeps Wings-owned guests clear of
/// manually created infrastructure containers.
#[serde(default = "pve_lxc_min_vmid")]
pub min_vmid: u32,

#[serde(default)]
#[schema(inline)]
pub firewall: #[derive(Clone, ToSchema, Deserialize, Serialize, DefaultFromSerde)] #[serde(default)] pub struct PveLxcFirewall {
#[serde(default)]
pub backend: PveLxcFirewallBackend,
#[serde(default = "docker_firewall_source_file_max_entries")]
pub source_file_max_entries: u64,
#[serde(default = "docker_firewall_source_file_max_bytes")]
pub source_file_max_bytes: u64,
},
},
},

#[serde(default)]
#[schema(inline)]
pub api: #[derive(ToSchema, Deserialize, Serialize, DefaultFromSerde)] #[serde(default)] pub struct Api {
Expand Down Expand Up @@ -1578,6 +1761,7 @@ fn log_filter(debug: bool) -> Targets {

pub struct Config {
inner: ArcSwap<InnerConfig>,
active_runtime_backend: OnceLock<RuntimeBackend>,
log_reload_handle: ReloadHandle,

pub path: String,
Expand Down Expand Up @@ -1715,6 +1899,7 @@ impl Config {

let config = Arc::new(Self {
inner: ArcSwap::new(Arc::new(inner)),
active_runtime_backend: OnceLock::new(),
log_reload_handle,

path: path.to_string(),
Expand All @@ -1735,6 +1920,7 @@ impl Config {

Self {
inner: ArcSwap::new(Arc::new(inner)),
active_runtime_backend: OnceLock::new(),
log_reload_handle: tracing_subscriber::reload::Layer::new(log_filter(false)).1,
path: String::new(),
ignore_certificate_errors: false,
Expand All @@ -1749,6 +1935,26 @@ impl Config {
self.inner.load()
}

/// Runtime selected for this Wings process. Automatic detection is kept in
/// memory so starting Wings never rewrites the operator's configuration.
pub fn active_runtime_backend(&self) -> RuntimeBackend {
self.active_runtime_backend
.get()
.copied()
.unwrap_or_else(|| self.load().runtime.backend)
}

pub fn set_active_runtime_backend(
&self,
backend: RuntimeBackend,
) -> Result<(), RuntimeBackend> {
match self.active_runtime_backend.set(backend) {
Ok(()) => Ok(()),
Err(backend) if self.active_runtime_backend.get() == Some(&backend) => Ok(()),
Err(backend) => Err(backend),
}
}

pub fn replace(&self, mut new: InnerConfig) -> Result<(), anyhow::Error> {
if Self::migrate_legacy_limits(&mut new) {
tracing::warn!("{LEGACY_LIMITS_WARNING}");
Expand Down Expand Up @@ -2643,4 +2849,23 @@ mod tests {
assert!(Config::migrate_legacy_limits(&mut cfg));
assert_eq!(cfg.limits.server_concurrent_pulls, 5);
}

#[tokio::test]
async fn active_runtime_backend_is_process_local_and_idempotent() {
let config = Config::mock();
assert_eq!(config.active_runtime_backend(), RuntimeBackend::Auto);
assert_eq!(
config.set_active_runtime_backend(RuntimeBackend::PveLxc),
Ok(())
);
assert_eq!(config.active_runtime_backend(), RuntimeBackend::PveLxc);
assert_eq!(
config.set_active_runtime_backend(RuntimeBackend::PveLxc),
Ok(())
);
assert_eq!(
config.set_active_runtime_backend(RuntimeBackend::Docker),
Err(RuntimeBackend::Docker)
);
}
}
Loading