Skip to content
Clone
eba4fd1
175 lines 6.2 KB Rust
use figment::Figment;
use figment::providers::{Env, Format, Serialized, Toml};
use serde::Deserialize;
use crate::error::ConfigError;
pub const RUNNER_ENV_PREFIX: &str = "APP_RUNNER__";
pub const DEFAULT_CONFIG_PATH: &str = "config/default.toml";
pub const LOCAL_CONFIG_PATH: &str = "config/local.toml";
#[derive(Clone, Debug, Deserialize, serde::Serialize)]
#[serde(default)]
pub struct RunnerConfig {
pub server_url: String,
pub registration_token: String,
pub name: String,
pub labels: Vec<String>,
pub capacity: i32,
pub workspace_root: String,
pub credential_path: String,
pub docker_enabled: bool,
pub default_shell: String,
pub pat: String,
pub git_binary: String,
pub docker_binary: String,
pub clone_timeout_secs: u64,
pub default_job_timeout_secs: u64,
pub poll_timeout_secs: u64,
pub heartbeat_interval_secs: u64,
pub log_flush_interval_ms: u64,
pub log_flush_bytes: usize,
pub keep_workspace: bool,
pub once: bool,
pub actions_engine_binary: String,
pub actions_platforms: std::collections::BTreeMap<String, String>,
pub actions_node_runtimes: std::collections::BTreeMap<String, String>,
pub actions_cache_dir: String,
pub actions_action_server: String,
pub actions_action_token: String,
pub actions_groups: Vec<String>,
}
impl Default for RunnerConfig {
fn default() -> Self {
Self {
server_url: "http://127.0.0.1:3005".to_string(),
registration_token: String::new(),
name: String::new(),
labels: vec!["self-hosted".to_string()],
capacity: 1,
workspace_root: "/var/lib/gitdata-runner".to_string(),
credential_path: "/var/lib/gitdata-runner/credential".to_string(),
docker_enabled: false,
default_shell: "sh".to_string(),
pat: String::new(),
git_binary: "git".to_string(),
docker_binary: "docker".to_string(),
clone_timeout_secs: 1800,
default_job_timeout_secs: 3600,
poll_timeout_secs: 30,
heartbeat_interval_secs: 15,
log_flush_interval_ms: 1000,
log_flush_bytes: 65536,
keep_workspace: false,
once: false,
actions_engine_binary: "gitdata-actions-engine".into(),
actions_platforms: [
("self-hosted".into(), "-self-hosted".into()),
(
"ubuntu-latest".into(),
"ghcr.io/catthehacker/ubuntu:act-24.04".into(),
),
(
"ubuntu-24.04".into(),
"ghcr.io/catthehacker/ubuntu:act-24.04".into(),
),
(
"ubuntu-22.04".into(),
"ghcr.io/catthehacker/ubuntu:act-22.04".into(),
),
]
.into(),
actions_node_runtimes: ["node12", "node16", "node20", "node24"]
.into_iter()
.map(|version| {
(
version.into(),
format!("/opt/gitdata/node/{version}/bin/node"),
)
})
.collect(),
actions_cache_dir: "/var/lib/gitdata-runner/actions".into(),
actions_action_server: "https://github.com".into(),
actions_action_token: String::new(),
actions_groups: Vec::new(),
}
}
}
impl RunnerConfig {
pub fn load() -> Result<Self, ConfigError> {
let _ = dotenvy::dotenv();
let mut figment = Figment::new();
if std::path::Path::new(DEFAULT_CONFIG_PATH).exists() {
figment = figment.merge(Toml::file(DEFAULT_CONFIG_PATH));
}
if std::path::Path::new(LOCAL_CONFIG_PATH).exists() {
figment = figment.merge(Toml::file(LOCAL_CONFIG_PATH));
}
// The shared config files keep runner settings under `[runner]`, while
// `APP_RUNNER__*` maps onto the flat keys of this struct; layer both so
// a file-only config is not silently ignored.
let from_file: Self = figment.extract_inner("runner").unwrap_or_default();
let figment =
Figment::from(Serialized::defaults(from_file)).merge(Env::prefixed(RUNNER_ENV_PREFIX));
let cfg: Self = figment.extract().map_err(ConfigError::from)?;
cfg.validate()?;
Ok(cfg)
}
pub fn validate(&self) -> Result<(), ConfigError> {
if self.server_url.trim().is_empty() {
return Err(ConfigError::Validation(
"runner.server_url must not be empty".to_string(),
));
}
if self.labels.is_empty() || self.labels.iter().any(|l| l.trim().is_empty()) {
return Err(ConfigError::Validation(
"runner.labels must contain at least one non-empty label".to_string(),
));
}
if self.capacity <= 0 {
return Err(ConfigError::Validation(
"runner.capacity must be greater than zero".to_string(),
));
}
if self.workspace_root.trim().is_empty() || self.credential_path.trim().is_empty() {
return Err(ConfigError::Validation(
"runner workspace_root and credential_path must not be empty".to_string(),
));
}
if self.default_shell.trim().is_empty() {
return Err(ConfigError::Validation(
"runner.default_shell must not be empty".to_string(),
));
}
if self.clone_timeout_secs == 0
|| self.default_job_timeout_secs == 0
|| self.poll_timeout_secs == 0
|| self.heartbeat_interval_secs == 0
{
return Err(ConfigError::Validation(
"runner timeouts must be greater than zero".to_string(),
));
}
let _ = Env::prefixed(RUNNER_ENV_PREFIX);
Ok(())
}
#[must_use]
pub fn resolved_name(&self) -> String {
if self.name.trim().is_empty() {
hostname()
} else {
self.name.clone()
}
}
}
fn hostname() -> String {
std::env::var("HOSTNAME")
.ok()
.filter(|h| !h.trim().is_empty())
.unwrap_or_else(|| "gitdata-runner".to_string())
}