mirror of
https://github.com/moghtech/komodo.git
synced 2025-12-05 19:17:36 -06:00
* setup network page * add Network, Image, Container * Docker ListItems and Inspects * frontend build * dev0 * network info working * fix cargo lock * dev1 * pages for the things * implement Active in dashboard * RunBuild update trigger list refresh * rename deployment executions to StartDeployment etc * add server level container control * dev2 * add Config field to Image * can get image labels from Config.Labels * mount container page * server show resource count * add GetContainerLog api * add _AllContainers api * dev3 * move ResourceTarget to entities mod * GetResourceMatchingContainer api * connect container to resource * dev4 add volume names to container list items * ts types * volume / image / network unused management * add image history to image page * fix PruneContainers incorret Operation * update cache for server for server after server actions * dev5 * add singapore to Hetzner * implement delete single network / image / volume api * dev6 * include "in use" on Docker Lists * add docker resource delete buttons * is nice * fix volume all in use * remove google font dependency * use host networking in test compose * implement Secret Variables (hidden in logs) * remove unneeded borrow * interpolate variables / secrets into extra args / onclone / onpull / command etc * validate empty strings before SelectItem * rename everything to Komodo * rename workspace to komodo * rc1
281 lines
7.8 KiB
Rust
281 lines
7.8 KiB
Rust
use std::{
|
|
sync::{Arc, Mutex, OnceLock},
|
|
time::Duration,
|
|
};
|
|
|
|
use anyhow::{anyhow, Context};
|
|
use futures::future::join_all;
|
|
use komodo_client::entities::server_template::hetzner::{
|
|
HetznerDatacenter, HetznerServerTemplateConfig, HetznerServerType,
|
|
HetznerVolumeFormat,
|
|
};
|
|
|
|
use crate::{
|
|
cloud::hetzner::{
|
|
common::HetznerServerStatus, create_server::CreateServerBody,
|
|
create_volume::CreateVolumeBody,
|
|
},
|
|
config::core_config,
|
|
};
|
|
|
|
use self::{client::HetznerClient, common::HetznerVolumeStatus};
|
|
|
|
mod client;
|
|
mod common;
|
|
mod create_server;
|
|
mod create_volume;
|
|
|
|
fn hetzner() -> Option<&'static HetznerClient> {
|
|
static HETZNER_CLIENT: OnceLock<Option<HetznerClient>> =
|
|
OnceLock::new();
|
|
HETZNER_CLIENT
|
|
.get_or_init(|| {
|
|
let token = &core_config().hetzner.token;
|
|
(!token.is_empty()).then(|| HetznerClient::new(token))
|
|
})
|
|
.as_ref()
|
|
}
|
|
|
|
pub struct HetznerServerMinimal {
|
|
pub id: i64,
|
|
pub ip: String,
|
|
}
|
|
|
|
const POLL_RATE_SECS: u64 = 3;
|
|
const MAX_POLL_TRIES: usize = 100;
|
|
|
|
#[instrument]
|
|
pub async fn launch_hetzner_server(
|
|
name: &str,
|
|
config: HetznerServerTemplateConfig,
|
|
) -> anyhow::Result<HetznerServerMinimal> {
|
|
let hetzner =
|
|
*hetzner().as_ref().context("Hetzner token not configured")?;
|
|
let HetznerServerTemplateConfig {
|
|
image,
|
|
datacenter,
|
|
private_network_ids,
|
|
placement_group,
|
|
enable_public_ipv4,
|
|
enable_public_ipv6,
|
|
firewall_ids,
|
|
server_type,
|
|
ssh_keys,
|
|
user_data,
|
|
use_public_ip,
|
|
labels,
|
|
volumes,
|
|
port: _,
|
|
} = config;
|
|
let datacenter = hetzner_datacenter(datacenter);
|
|
|
|
// Create volumes and get their ids
|
|
let mut volume_ids = Vec::new();
|
|
for volume in volumes {
|
|
let body = CreateVolumeBody {
|
|
name: volume.name,
|
|
format: Some(hetzner_format(volume.format)),
|
|
location: Some(datacenter.into()),
|
|
labels: volume.labels,
|
|
size: volume.size_gb,
|
|
automount: None,
|
|
server: None,
|
|
};
|
|
let id = hetzner
|
|
.create_volume(&body)
|
|
.await
|
|
.context("failed to create hetzner volume")?
|
|
.volume
|
|
.id;
|
|
volume_ids.push(id);
|
|
}
|
|
|
|
// Make sure volumes are available before continue
|
|
let vol_ids_poll = Arc::new(Mutex::new(volume_ids.clone()));
|
|
for _ in 0..MAX_POLL_TRIES {
|
|
if vol_ids_poll.lock().unwrap().is_empty() {
|
|
break;
|
|
}
|
|
tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;
|
|
let ids = vol_ids_poll.lock().unwrap().clone();
|
|
let futures = ids.into_iter().map(|id| {
|
|
let vol_ids = vol_ids_poll.clone();
|
|
async move {
|
|
let Ok(res) = hetzner.get_volume(id).await else {
|
|
return;
|
|
};
|
|
if matches!(res.volume.status, HetznerVolumeStatus::Available)
|
|
{
|
|
vol_ids.lock().unwrap().retain(|_id| *_id != id);
|
|
}
|
|
}
|
|
});
|
|
join_all(futures).await;
|
|
}
|
|
if !vol_ids_poll.lock().unwrap().is_empty() {
|
|
return Err(anyhow!("Volumes not ready after poll"));
|
|
}
|
|
|
|
let body = CreateServerBody {
|
|
name: name.to_string(),
|
|
automount: None,
|
|
datacenter: Some(datacenter),
|
|
location: None,
|
|
firewalls: firewall_ids
|
|
.into_iter()
|
|
.map(|firewall| create_server::Firewall { firewall })
|
|
.collect(),
|
|
image,
|
|
labels,
|
|
networks: private_network_ids,
|
|
placement_group: (placement_group > 0).then_some(placement_group),
|
|
public_net: create_server::PublicNet {
|
|
enable_ipv4: enable_public_ipv4,
|
|
enable_ipv6: enable_public_ipv6,
|
|
ipv4: None,
|
|
ipv6: None,
|
|
},
|
|
server_type: hetzner_server_type(server_type),
|
|
ssh_keys,
|
|
start_after_create: true,
|
|
user_data: (!user_data.is_empty()).then_some(user_data),
|
|
volumes: volume_ids,
|
|
};
|
|
|
|
let server_id = hetzner
|
|
.create_server(&body)
|
|
.await
|
|
.context("failed to create hetnzer server")?
|
|
.server
|
|
.id;
|
|
|
|
for _ in 0..MAX_POLL_TRIES {
|
|
tokio::time::sleep(Duration::from_secs(POLL_RATE_SECS)).await;
|
|
let Ok(res) = hetzner.get_server(server_id).await else {
|
|
continue;
|
|
};
|
|
if matches!(res.server.status, HetznerServerStatus::Running) {
|
|
let ip = if use_public_ip {
|
|
res
|
|
.server
|
|
.public_net
|
|
.ipv4
|
|
.context("instance does not have public ipv4 attached")?
|
|
.ip
|
|
} else {
|
|
res
|
|
.server
|
|
.private_net
|
|
.first()
|
|
.context("no private networks attached")?
|
|
.ip
|
|
.to_string()
|
|
};
|
|
let server = HetznerServerMinimal { id: server_id, ip };
|
|
return Ok(server);
|
|
}
|
|
}
|
|
|
|
Err(anyhow!(
|
|
"failed to verify server running after polling status"
|
|
))
|
|
}
|
|
|
|
fn hetzner_format(
|
|
format: HetznerVolumeFormat,
|
|
) -> common::HetznerVolumeFormat {
|
|
match format {
|
|
HetznerVolumeFormat::Xfs => common::HetznerVolumeFormat::Xfs,
|
|
HetznerVolumeFormat::Ext4 => common::HetznerVolumeFormat::Ext4,
|
|
}
|
|
}
|
|
|
|
fn hetzner_datacenter(
|
|
datacenter: HetznerDatacenter,
|
|
) -> common::HetznerDatacenter {
|
|
match datacenter {
|
|
HetznerDatacenter::Nuremberg1Dc3 => {
|
|
common::HetznerDatacenter::Nuremberg1Dc3
|
|
}
|
|
HetznerDatacenter::Helsinki1Dc2 => {
|
|
common::HetznerDatacenter::Helsinki1Dc2
|
|
}
|
|
HetznerDatacenter::Falkenstein1Dc14 => {
|
|
common::HetznerDatacenter::Falkenstein1Dc14
|
|
}
|
|
HetznerDatacenter::AshburnDc1 => {
|
|
common::HetznerDatacenter::AshburnDc1
|
|
}
|
|
HetznerDatacenter::HillsboroDc1 => {
|
|
common::HetznerDatacenter::HillsboroDc1
|
|
}
|
|
HetznerDatacenter::SingaporeDc1 => {
|
|
common::HetznerDatacenter::SingaporeDc1
|
|
}
|
|
}
|
|
}
|
|
|
|
fn hetzner_server_type(
|
|
server_type: HetznerServerType,
|
|
) -> common::HetznerServerType {
|
|
match server_type {
|
|
HetznerServerType::SharedAmd2Core2Ram40Disk => {
|
|
common::HetznerServerType::SharedAmd2Core2Ram40Disk
|
|
}
|
|
HetznerServerType::SharedArm2Core4Ram40Disk => {
|
|
common::HetznerServerType::SharedArm2Core4Ram40Disk
|
|
}
|
|
HetznerServerType::SharedIntel2Core4Ram40Disk => {
|
|
common::HetznerServerType::SharedIntel2Core4Ram40Disk
|
|
}
|
|
HetznerServerType::SharedAmd3Core4Ram80Disk => {
|
|
common::HetznerServerType::SharedAmd3Core4Ram80Disk
|
|
}
|
|
HetznerServerType::SharedArm4Core8Ram80Disk => {
|
|
common::HetznerServerType::SharedArm4Core8Ram80Disk
|
|
}
|
|
HetznerServerType::SharedIntel4Core8Ram80Disk => {
|
|
common::HetznerServerType::SharedIntel4Core8Ram80Disk
|
|
}
|
|
HetznerServerType::SharedAmd4Core8Ram160Disk => {
|
|
common::HetznerServerType::SharedAmd4Core8Ram160Disk
|
|
}
|
|
HetznerServerType::SharedArm8Core16Ram160Disk => {
|
|
common::HetznerServerType::SharedArm8Core16Ram160Disk
|
|
}
|
|
HetznerServerType::SharedIntel8Core16Ram160Disk => {
|
|
common::HetznerServerType::SharedIntel8Core16Ram160Disk
|
|
}
|
|
HetznerServerType::SharedAmd8Core16Ram240Disk => {
|
|
common::HetznerServerType::SharedAmd8Core16Ram240Disk
|
|
}
|
|
HetznerServerType::SharedArm16Core32Ram320Disk => {
|
|
common::HetznerServerType::SharedArm16Core32Ram320Disk
|
|
}
|
|
HetznerServerType::SharedIntel16Core32Ram320Disk => {
|
|
common::HetznerServerType::SharedIntel16Core32Ram320Disk
|
|
}
|
|
HetznerServerType::SharedAmd16Core32Ram360Disk => {
|
|
common::HetznerServerType::SharedAmd16Core32Ram360Disk
|
|
}
|
|
HetznerServerType::DedicatedAmd2Core8Ram80Disk => {
|
|
common::HetznerServerType::DedicatedAmd2Core8Ram80Disk
|
|
}
|
|
HetznerServerType::DedicatedAmd4Core16Ram160Disk => {
|
|
common::HetznerServerType::DedicatedAmd4Core16Ram160Disk
|
|
}
|
|
HetznerServerType::DedicatedAmd8Core32Ram240Disk => {
|
|
common::HetznerServerType::DedicatedAmd8Core32Ram240Disk
|
|
}
|
|
HetznerServerType::DedicatedAmd16Core64Ram360Disk => {
|
|
common::HetznerServerType::DedicatedAmd16Core64Ram360Disk
|
|
}
|
|
HetznerServerType::DedicatedAmd32Core128Ram600Disk => {
|
|
common::HetznerServerType::DedicatedAmd32Core128Ram600Disk
|
|
}
|
|
HetznerServerType::DedicatedAmd48Core192Ram960Disk => {
|
|
common::HetznerServerType::DedicatedAmd48Core192Ram960Disk
|
|
}
|
|
}
|
|
}
|