mirror of
https://codeberg.org/spire/dispenser.git
synced 2026-06-03 18:14:06 +02:00
wip
This commit is contained in:
parent
83c7136354
commit
04b08706ef
9 changed files with 2069 additions and 4 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
config.toml
|
||||||
1446
Cargo.lock
generated
Normal file
1446
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
|
@ -4,6 +4,16 @@ version = "0.1.0"
|
||||||
authors = ["Robin Appelman <robin@icewind.nl>"]
|
authors = ["Robin Appelman <robin@icewind.nl>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
async-trait = "0.1"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
thiserror = "1"
|
||||||
|
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
toml = "0.5"
|
||||||
|
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
camino = "1"
|
||||||
|
petname = "1"
|
||||||
|
thrussh = "0.32"
|
||||||
|
thrussh-keys = "0.20"
|
||||||
|
futures-util = "0.3"
|
||||||
14
config.example.toml
Normal file
14
config.example.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[server]
|
||||||
|
rcon = "xxxx"
|
||||||
|
password = "xxxx"
|
||||||
|
demostf_key = "xxxx" # optional
|
||||||
|
logstf_key = "xxxx" # optional
|
||||||
|
config_league = "etf2l" # optional, defaults to etf2l
|
||||||
|
config_mode = "6v6" # optional, defaults to 6v6
|
||||||
|
name = "MyCoolServer" # optional, defaults to Spire
|
||||||
|
tv_name = "MyCoolSTV" # optional, defaults to SpireTV
|
||||||
|
|
||||||
|
[vultr]
|
||||||
|
api_key = "xxx"
|
||||||
|
region = "ams" # see https://api.vultr.com/v2/regions for a list of regions
|
||||||
|
plan = "vc2-1c-2gb" # optional, defaults to vc2-1c-2gb (2GB, $10/month) see https://api.vultr.com/v2/plans for a lis of plan
|
||||||
90
src/cloud/mod.rs
Normal file
90
src/cloud/mod.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
mod ssh;
|
||||||
|
pub mod vultr;
|
||||||
|
|
||||||
|
use crate::cloud::ssh::SshError;
|
||||||
|
use crate::config::ServerConfig;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CloudError {
|
||||||
|
#[error("Invalid credentials")]
|
||||||
|
Unauthorized,
|
||||||
|
#[error("Specified server not found")]
|
||||||
|
ServerNotFound,
|
||||||
|
#[error("Network error: {0}")]
|
||||||
|
Network(#[from] NetworkError),
|
||||||
|
#[error("Network response from server: {0}")]
|
||||||
|
InvalidResponse(#[from] ResponseError),
|
||||||
|
#[error("Server boot timed out")]
|
||||||
|
StartTimeout,
|
||||||
|
#[error("Error while trying to connect trough ssh: {0}")]
|
||||||
|
Ssh(#[from] SshError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Intentionally opaque error
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("{0}")]
|
||||||
|
pub struct NetworkError(reqwest::Error);
|
||||||
|
|
||||||
|
impl CloudError {
|
||||||
|
fn from_status_code(status: StatusCode) -> Result<()> {
|
||||||
|
if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN {
|
||||||
|
return Err(CloudError::Unauthorized);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Intentionally opaque error
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ResponseError {
|
||||||
|
#[error("{0}")]
|
||||||
|
JSON(reqwest::Error),
|
||||||
|
#[error("Unexpected response {0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for NetworkError {
|
||||||
|
fn from(e: reqwest::Error) -> Self {
|
||||||
|
NetworkError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for ResponseError {
|
||||||
|
fn from(e: reqwest::Error) -> Self {
|
||||||
|
ResponseError::JSON(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T, E = CloudError> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Cloud {
|
||||||
|
/// List all running servers on this cloud
|
||||||
|
async fn list(&self) -> Result<Vec<Server>>;
|
||||||
|
/// Create a new server with the given parameter
|
||||||
|
async fn spawn(&self) -> Result<Created>;
|
||||||
|
/// Destroy a given server
|
||||||
|
async fn kill(&self, id: &str) -> Result<()>;
|
||||||
|
/// Wait until the server has an ip
|
||||||
|
async fn wait_for_ip(&self, id: &str) -> Result<Server>;
|
||||||
|
/// Setup the tf2 server on the instance
|
||||||
|
async fn setup(&self, id: &str, password: &str, config: &ServerConfig) -> Result<Server>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Server {
|
||||||
|
pub id: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub ip: IpAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Created {
|
||||||
|
pub id: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
102
src/cloud/ssh.rs
Normal file
102
src/cloud/ssh.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
use futures_util::future::{self};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use thiserror::Error;
|
||||||
|
use thrussh::client::Handle;
|
||||||
|
use thrussh::*;
|
||||||
|
use thrussh_keys::key::PublicKey;
|
||||||
|
|
||||||
|
struct Client {}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SshError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Other(#[from] SshErrorImpl),
|
||||||
|
#[error("Invalid credentials")]
|
||||||
|
Unauthorized,
|
||||||
|
#[error("Connection timeout")]
|
||||||
|
ConnectionTimeout,
|
||||||
|
#[error("Disconnected by server")]
|
||||||
|
Disconnected,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error(transparent)]
|
||||||
|
pub struct SshErrorImpl(thrussh::Error);
|
||||||
|
|
||||||
|
impl From<thrussh::Error> for SshError {
|
||||||
|
fn from(e: thrussh::Error) -> Self {
|
||||||
|
match e {
|
||||||
|
thrussh::Error::Disconnect => SshError::Disconnected,
|
||||||
|
thrussh::Error::HUP => SshError::Disconnected,
|
||||||
|
thrussh::Error::ConnectionTimeout => SshError::ConnectionTimeout,
|
||||||
|
thrussh::Error::IO(io) if io.raw_os_error() == Some(110) => SshError::ConnectionTimeout,
|
||||||
|
e => SshError::Other(SshErrorImpl(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl client::Handler for Client {
|
||||||
|
type Error = SshError;
|
||||||
|
type FutureBool = future::Ready<Result<(Self, bool), SshError>>;
|
||||||
|
type FutureUnit = future::Ready<Result<(Self, client::Session), SshError>>;
|
||||||
|
|
||||||
|
fn finished_bool(self, b: bool) -> Self::FutureBool {
|
||||||
|
future::ready(Ok((self, b)))
|
||||||
|
}
|
||||||
|
fn finished(self, session: client::Session) -> Self::FutureUnit {
|
||||||
|
future::ready(Ok((self, session)))
|
||||||
|
}
|
||||||
|
fn check_server_key(self, server_public_key: &PublicKey) -> Self::FutureBool {
|
||||||
|
println!("check_server_key: {:?}", server_public_key);
|
||||||
|
self.finished_bool(true)
|
||||||
|
}
|
||||||
|
fn channel_open_confirmation(
|
||||||
|
self,
|
||||||
|
channel: ChannelId,
|
||||||
|
_max_packet_size: u32,
|
||||||
|
_window_size: u32,
|
||||||
|
session: client::Session,
|
||||||
|
) -> Self::FutureUnit {
|
||||||
|
println!("channel_open_confirmation: {:?}", channel);
|
||||||
|
self.finished(session)
|
||||||
|
}
|
||||||
|
fn data(self, channel: ChannelId, data: &[u8], session: client::Session) -> Self::FutureUnit {
|
||||||
|
println!(
|
||||||
|
"data on channel {:?}: {:?}",
|
||||||
|
channel,
|
||||||
|
std::str::from_utf8(data)
|
||||||
|
);
|
||||||
|
self.finished(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SshSession {
|
||||||
|
handle: Handle<Client>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SshSession {
|
||||||
|
pub async fn open(ip: IpAddr, password: &str) -> Result<Self, SshError> {
|
||||||
|
let config = thrussh::client::Config::default();
|
||||||
|
let config = Arc::new(config);
|
||||||
|
let sh = Client {};
|
||||||
|
|
||||||
|
let mut handle = thrussh::client::connect(config, (ip, 22), sh).await?;
|
||||||
|
if handle.authenticate_password("root", password).await? {
|
||||||
|
Ok(SshSession { handle })
|
||||||
|
} else {
|
||||||
|
Err(SshError::Unauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn exec<S: Into<String>>(&mut self, cmd: S) -> Result<(), SshError> {
|
||||||
|
let mut channel = self.handle.channel_open_session().await?;
|
||||||
|
println!("exec");
|
||||||
|
channel.exec(true, cmd).await?;
|
||||||
|
println!("exec'd");
|
||||||
|
if let Some(msg) = channel.wait().await {
|
||||||
|
println!("{:?}", msg)
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
259
src/cloud/vultr.rs
Normal file
259
src/cloud/vultr.rs
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
use crate::cloud::ssh::{SshError, SshSession};
|
||||||
|
use crate::cloud::{Cloud, CloudError, Created, NetworkError, ResponseError, Result, Server};
|
||||||
|
use crate::config::ServerConfig;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use petname::petname;
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::{sleep, timeout};
|
||||||
|
|
||||||
|
pub struct Vultr {
|
||||||
|
region: String,
|
||||||
|
plan: String,
|
||||||
|
token: String,
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vultr {
|
||||||
|
pub fn new(token: String, region: String, plan: String) -> Self {
|
||||||
|
Vultr {
|
||||||
|
token,
|
||||||
|
region,
|
||||||
|
plan,
|
||||||
|
client: Client::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Cloud for Vultr {
|
||||||
|
async fn list(&self) -> Result<Vec<Server>> {
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.get("https://api.vultr.com/v2/instances")
|
||||||
|
.bearer_auth(&self.token)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(NetworkError::from)?;
|
||||||
|
CloudError::from_status_code(response.status())?;
|
||||||
|
|
||||||
|
let response: VultrListResponse = response.json().await.map_err(ResponseError::from)?;
|
||||||
|
|
||||||
|
Ok(response
|
||||||
|
.instances
|
||||||
|
.into_iter()
|
||||||
|
.filter(|instance| instance.tag == "spire")
|
||||||
|
.map(Server::from)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn(&self) -> Result<Created> {
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post("https://api.vultr.com/v2/instances")
|
||||||
|
.bearer_auth(&self.token)
|
||||||
|
.json(&VultrCreateParams {
|
||||||
|
region: self.region.as_str(),
|
||||||
|
plan: self.plan.as_str(),
|
||||||
|
tag: "spire",
|
||||||
|
label: petname(2, "-"),
|
||||||
|
app_id: self.get_app_id("docker").await?,
|
||||||
|
})
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(NetworkError::from)?;
|
||||||
|
CloudError::from_status_code(response.status())?;
|
||||||
|
|
||||||
|
if response.status().is_success() {
|
||||||
|
let response: VultrCreateResponse =
|
||||||
|
response.json().await.map_err(ResponseError::from)?;
|
||||||
|
Ok(response.instance.into())
|
||||||
|
} else {
|
||||||
|
Err(ResponseError::Other(response.text().await.map_err(NetworkError::from)?).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn kill(&self, _id: &str) -> Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_ip(&self, id: &str) -> Result<Server> {
|
||||||
|
let instance = loop {
|
||||||
|
let instance = self.get_instance(id).await?;
|
||||||
|
if !instance.main_ip.is_unspecified() {
|
||||||
|
break instance;
|
||||||
|
} else {
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(instance.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
password: &str,
|
||||||
|
config: &ServerConfig,
|
||||||
|
) -> Result<Server, CloudError> {
|
||||||
|
let server = self.wait_for_ip(id).await?;
|
||||||
|
let ip = server.ip;
|
||||||
|
let mut ssh = timeout(Duration::from_secs(5 * 60), async move {
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
match SshSession::open(ip, password).await {
|
||||||
|
Ok(ssh) => return Ok(ssh),
|
||||||
|
Err(SshError::ConnectionTimeout) => {}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| CloudError::StartTimeout)??;
|
||||||
|
println!("connected");
|
||||||
|
|
||||||
|
ssh.exec("docker pull spiretf/docker-spire-server").await?;
|
||||||
|
println!("pulled");
|
||||||
|
ssh.exec(format!(
|
||||||
|
"docker run --name spire -d \
|
||||||
|
-e NAME={name} -e TV_NAME={tv_name} -e PASSWORD={password} -e RCON_PASSWORD={rcon} \
|
||||||
|
-e DEMOSTF_APIKEY={demostf} -e LOGSTF_APIKEY={logstf} \
|
||||||
|
-e CONFIG_LEAGUE={league} -e CONFIG_MODE={mode} \
|
||||||
|
-p 27015:27015 -p 27021:27021 -p 27015:27015/udp -p 27020:27020/udp -p 27025:27025 \
|
||||||
|
-p 28015:27015 -p 28015:27015/udp -p 27115:27015 -p 27115:27015/udp -p 27215:27015 \
|
||||||
|
-p 27215:27015/udp -p 27315:27015 -p 27315:27015/udp -p 27415:27015 -p 27415:27015/udp \
|
||||||
|
-p 27515:27015 -p 27515:27015/udp -p 27615:27015 -p 27615:27015/udp -p 27715:27015 \
|
||||||
|
-p 27715:27015/udp -p 27815:27015 -p 27815:27015/udp -p 27915:27015 -p 27915:27015/udp \
|
||||||
|
{image}
|
||||||
|
",
|
||||||
|
name = config.name,
|
||||||
|
tv_name = config.tv_name,
|
||||||
|
password = config.password,
|
||||||
|
rcon = config.rcon,
|
||||||
|
demostf = config
|
||||||
|
.demostf_key
|
||||||
|
.as_ref()
|
||||||
|
.map(String::as_str)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
logstf = config
|
||||||
|
.logstf_key
|
||||||
|
.as_ref()
|
||||||
|
.map(String::as_str)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
league = config.config_league,
|
||||||
|
mode = config.config_mode,
|
||||||
|
image = config.image
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vultr {
|
||||||
|
async fn get_app_id(&self, short_name: &str) -> Result<u16> {
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.get("https://api.vultr.com/v2/applications")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(NetworkError::from)?;
|
||||||
|
let response: VultrApplicationsResponse =
|
||||||
|
response.json().await.map_err(ResponseError::from)?;
|
||||||
|
Ok(response
|
||||||
|
.applications
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|application| (application.short_name == short_name).then(|| application.id))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ResponseError::Other(format!("Application \"{}\" not found", short_name))
|
||||||
|
})?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_instance(&self, id: &str) -> Result<VultrInstanceResponse> {
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.get(format!("https://api.vultr.com/v2/instances/{}", id))
|
||||||
|
.bearer_auth(&self.token)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(NetworkError::from)?;
|
||||||
|
CloudError::from_status_code(response.status())?;
|
||||||
|
|
||||||
|
let response: VultrGetResponse = response.json().await.map_err(ResponseError::from)?;
|
||||||
|
Ok(response.instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct VultrCreateParams<'a> {
|
||||||
|
region: &'a str,
|
||||||
|
plan: &'a str,
|
||||||
|
tag: &'a str,
|
||||||
|
label: String,
|
||||||
|
app_id: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VultrListResponse {
|
||||||
|
instances: Vec<VultrInstanceResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VultrGetResponse {
|
||||||
|
instance: VultrInstanceResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VultrCreateResponse {
|
||||||
|
instance: VultrCreatedInstanceResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VultrInstanceResponse {
|
||||||
|
id: String,
|
||||||
|
os: String,
|
||||||
|
ram: u64,
|
||||||
|
main_ip: IpAddr,
|
||||||
|
region: String,
|
||||||
|
vcpu_count: u16,
|
||||||
|
date_created: DateTime<Utc>,
|
||||||
|
tag: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VultrCreatedInstanceResponse {
|
||||||
|
id: String,
|
||||||
|
default_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VultrInstanceResponse> for Server {
|
||||||
|
fn from(instance: VultrInstanceResponse) -> Self {
|
||||||
|
Server {
|
||||||
|
id: instance.id,
|
||||||
|
created: instance.date_created,
|
||||||
|
ip: instance.main_ip,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VultrCreatedInstanceResponse> for Created {
|
||||||
|
fn from(instance: VultrCreatedInstanceResponse) -> Self {
|
||||||
|
Created {
|
||||||
|
id: instance.id,
|
||||||
|
password: instance.default_password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VultrApplicationsResponse {
|
||||||
|
applications: Vec<VultrApplicationResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VultrApplicationResponse {
|
||||||
|
id: u16,
|
||||||
|
short_name: String,
|
||||||
|
}
|
||||||
105
src/config.rs
Normal file
105
src/config.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
use crate::cloud::vultr::Vultr;
|
||||||
|
use crate::cloud::Cloud;
|
||||||
|
use camino::Utf8PathBuf;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
use std::path::Path;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error("Failed to open \"{0}\"")]
|
||||||
|
Open(Utf8PathBuf),
|
||||||
|
#[error("Malformed toml: {0}")]
|
||||||
|
Toml(#[from] TomlError),
|
||||||
|
#[error("No cloud provider configured")]
|
||||||
|
NoProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Intentionally opaque error
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("{0}")]
|
||||||
|
pub struct TomlError(toml::de::Error);
|
||||||
|
|
||||||
|
impl From<toml::de::Error> for TomlError {
|
||||||
|
fn from(e: toml::de::Error) -> Self {
|
||||||
|
TomlError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
pub vultr: Option<VultrConfig>,
|
||||||
|
pub server: ServerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn from_file<P: AsRef<Path> + Into<Utf8PathBuf>>(path: P) -> Result<Self, ConfigError> {
|
||||||
|
let content = read_to_string(path.as_ref()).map_err(|_| ConfigError::Open(path.into()))?;
|
||||||
|
Ok(toml::from_str(&content).map_err(TomlError::from)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cloud(&self) -> Result<Box<dyn Cloud>, ConfigError> {
|
||||||
|
if let Some(vultr) = &self.vultr {
|
||||||
|
Ok(Box::new(Vultr::new(
|
||||||
|
vultr.api_key.clone(),
|
||||||
|
vultr.region.clone(),
|
||||||
|
vultr.plan.clone(),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Err(ConfigError::NoProvider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
pub rcon: String,
|
||||||
|
pub password: String,
|
||||||
|
#[serde(default = "server_default_image")]
|
||||||
|
pub image: String,
|
||||||
|
pub demostf_key: Option<String>,
|
||||||
|
pub logstf_key: Option<String>,
|
||||||
|
#[serde(default = "server_default_league")]
|
||||||
|
pub config_league: String,
|
||||||
|
#[serde(default = "server_default_mode")]
|
||||||
|
pub config_mode: String,
|
||||||
|
#[serde(default = "server_default_name")]
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default = "server_default_tv_name")]
|
||||||
|
pub tv_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_default_image() -> String {
|
||||||
|
String::from("spiretf/docker-spire-server")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_default_name() -> String {
|
||||||
|
String::from("Spire")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_default_tv_name() -> String {
|
||||||
|
String::from("SpireTV")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_default_league() -> String {
|
||||||
|
String::from("etf2l")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_default_mode() -> String {
|
||||||
|
String::from("6v6")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct VultrConfig {
|
||||||
|
pub api_key: String,
|
||||||
|
/// See https://api.vultr.com/v2/regions for a list of plans
|
||||||
|
pub region: String,
|
||||||
|
/// See https://api.vultr.com/v2/plans for a list of plans
|
||||||
|
#[serde(default = "vultr_default_plan")]
|
||||||
|
pub plan: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vultr_default_plan() -> String {
|
||||||
|
String::from("vc2-1c-2gb")
|
||||||
|
}
|
||||||
42
src/main.rs
42
src/main.rs
|
|
@ -1,3 +1,41 @@
|
||||||
fn main() {
|
pub mod cloud;
|
||||||
println!("Hello, world!");
|
pub mod config;
|
||||||
|
|
||||||
|
use crate::cloud::CloudError;
|
||||||
|
use crate::config::{Config, ConfigError};
|
||||||
|
use std::env::args;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Error while interacting with cloud provider: {0}")]
|
||||||
|
Cloud(#[from] CloudError),
|
||||||
|
#[error("Error while loading configuration: {0}")]
|
||||||
|
Config(#[from] ConfigError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Error> {
|
||||||
|
let mut args = args();
|
||||||
|
let bin = args.next().unwrap();
|
||||||
|
|
||||||
|
let config = match args.next() {
|
||||||
|
Some(file) => Config::from_file(file)?,
|
||||||
|
None => {
|
||||||
|
eprintln!("Usage {} <config.toml>", bin);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let cloud = config.cloud()?;
|
||||||
|
|
||||||
|
let created = dbg!(cloud.spawn().await?);
|
||||||
|
let server = cloud.wait_for_ip(&created.id).await?;
|
||||||
|
println!("IP: {}", server.ip);
|
||||||
|
println!("Password: {}", created.password);
|
||||||
|
dbg!(
|
||||||
|
cloud
|
||||||
|
.setup(&created.id, &created.password, &config.server)
|
||||||
|
.await?
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue