use std::fs::File;
use std::io::Read;
use tokio::time::{Duration, sleep};
use tracing::{error, info, span, warn};
use tracing_subscriber::{Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt};
use regent_sdk::hosts::managed_host::{ManagedHost, ManagedHostBuilder};
use regent_sdk::hosts::handlers::{ConnectionMethod, TargetUser};
use regent_sdk::ExpectedState;
mod config;
mod git;
use crate::config::{LogFormat, RegOpsConfig, RunningMode, SystemIntegrationConfig};
use crate::git::{clone_fresh, git_pull, local_repo_matches_expected};
#[tokio::main]
async fn main() {
loop {
if let Err(details) = run().await {
eprintln!("[FATAL] unrecoverable error, retrying shortly: {}", details);
}
sleep(Duration::from_secs(10)).await;
}
}
async fn run() -> Result<(), String> {
let config = match load_config("/etc/regops/config.toml") {
Ok(config) => config,
Err(details) => return Err(format!("Failed to load configuration: {}", details)),
};
init_tracing(&config.system_integration);
let hostname = match hostname::get() {
Ok(value) => value.to_string_lossy().to_string(),
Err(details) => {
warn!(%details, "Failed to get hostname");
"localhost".to_string()
}
};
let global_span = span!(tracing::Level::INFO, "RegOps", ?hostname);
let _guard = global_span.enter();
let repo = match &config.git.repo {
Some(repo) => repo.clone(),
None => {
warn!("No repository url set yet");
return Err("No git repository configured (git.repo is unset)".to_string());
}
};
info!("Git repository : {}", repo);
info!("Running mode : {:?}", config.behavior.mode);
let auth = config.authentication_mode();
if !local_repo_matches_expected(&config.git.local_path, &repo, &config.git.branch) {
match clone_fresh(&config.git.local_path, &repo, &config.git.branch, &auth) {
Ok(()) => {}
Err(details) => return Err(format!("Failed initial cloning: {}", details)),
}
}
let managed_host_builder = ManagedHostBuilder::new(
&hostname,
"localhost",
Some(ConnectionMethod::Localhost(TargetUser::current_user())),
);
let mut managed_localhost: ManagedHost = match managed_host_builder.build(None).await {
Ok(managed_host) => managed_host,
Err(details) => return Err(format!("Failed to build managed host: {}", details)),
};
match managed_localhost.connect().await {
Ok(()) => {}
Err(details) => return Err(format!("Failed to connect to managed host: {}", details)),
}
loop {
match git_pull(&config.git.local_path, &auth) {
Ok(()) => {}
Err(details) => {
warn!(details, "Failed to pull git repository, wiping local copy and re-cloning");
match clone_fresh(&config.git.local_path, &repo, &config.git.branch, &auth) {
Ok(()) => {}
Err(recovery_details) => {
error!(recovery_details, "Failed to recover local git repository, will retry next cycle");
}
}
sleep(Duration::from_secs(config.behavior.interval_sec)).await;
continue;
}
}
let expected_state_description = match std::fs::read_to_string(format!(
"{}/{}",
config.git.local_path, config.git.expected_state_path
)) {
Ok(content) => content,
Err(details) => {
error!(?details, "Failed to get file content");
sleep(Duration::from_secs(config.behavior.interval_sec)).await;
continue;
}
};
let expected_state = match ExpectedState::from_raw_yaml(&expected_state_description) {
Ok(state) => state,
Err(error_detail) => {
error!("Wrong yaml content : {:?}", error_detail);
sleep(Duration::from_secs(config.behavior.interval_sec)).await;
continue;
}
};
match &config.behavior.mode {
RunningMode::Assess => {
match managed_localhost.assess_compliance(&expected_state, true).await {
Ok(_assessment) => {}
Err(details) => warn!(%details, "Failed to assess compliance"),
}
}
RunningMode::Enforce => {
match managed_localhost.reach_compliance(&expected_state).await {
Ok(_outcome) => {}
Err(details) => warn!(%details, "Failed to enforce compliance"),
}
}
}
sleep(Duration::from_secs(config.behavior.interval_sec)).await;
}
}
fn load_config(path: &str) -> Result<RegOpsConfig, String> {
let mut configuration_file = match File::open(path) {
Ok(file) => file,
Err(details) => return Err(format!("Failed to open '{}': {}", path, details)),
};
let mut file_content: Vec<u8> = Vec::new();
match configuration_file.read_to_end(&mut file_content) {
Ok(_size) => {}
Err(details) => return Err(format!("Failed to read '{}': {}", path, details)),
}
match toml::from_slice(&file_content) {
Ok(config) => Ok(config),
Err(details) => Err(format!("Failed to parse '{}': {}", path, details)),
}
}
fn init_tracing(system_integration: &SystemIntegrationConfig) {
let fmt_layer = match system_integration.log_format {
LogFormat::Raw => fmt::layer().boxed(),
LogFormat::Json => fmt::layer().json().boxed(),
};
match tracing_subscriber::registry()
.with(system_integration.log_level.to_tracing_level())
.with(fmt_layer)
.try_init()
{
Ok(()) => {}
Err(details) => {
warn!(%details, "Tracing global subscriber init failed");
}
}
}