use std::net::IpAddr;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::info;
use crate::shared::command::{CommandError, CommandExecutor};
use super::json_parser::{OpenTofuJsonParser, ParseError};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InstanceInfo {
pub image: String,
pub ip_address: IpAddr,
pub name: String,
pub status: String,
}
#[derive(Error, Debug)]
pub enum OpenTofuError {
#[error("Command execution failed: {0}")]
CommandError(#[from] CommandError),
#[error("Parse error: {0}")]
ParseError(#[from] ParseError),
}
impl crate::shared::Traceable for OpenTofuError {
fn trace_format(&self) -> String {
match self {
Self::CommandError(e) => format!("OpenTofuError: Command execution failed - {e}"),
Self::ParseError(e) => format!("OpenTofuError: JSON parsing failed - {e}"),
}
}
fn trace_source(&self) -> Option<&dyn crate::shared::Traceable> {
match self {
Self::CommandError(e) => Some(e),
Self::ParseError(_) => None, }
}
fn error_kind(&self) -> crate::shared::ErrorKind {
crate::shared::ErrorKind::InfrastructureOperation
}
}
pub struct OpenTofuClient {
working_dir: PathBuf,
command_executor: CommandExecutor,
}
impl OpenTofuClient {
#[must_use]
pub fn new<P: Into<PathBuf>>(working_dir: P) -> Self {
Self {
working_dir: working_dir.into(),
command_executor: CommandExecutor::new(),
}
}
pub fn init(&self) -> Result<String, CommandError> {
info!(
"Initializing OpenTofu in directory: {}",
self.working_dir.display()
);
self.command_executor
.run_command("tofu", &["init"], Some(&self.working_dir))
.map(|result| result.stdout)
}
pub fn validate(&self) -> Result<String, CommandError> {
info!(
"Validating OpenTofu configuration in directory: {}",
self.working_dir.display()
);
self.command_executor
.run_command("tofu", &["validate"], Some(&self.working_dir))
.map(|result| result.stdout)
}
pub fn plan(&self, extra_args: &[&str]) -> Result<String, CommandError> {
info!(
"Planning infrastructure changes in directory: {}",
self.working_dir.display()
);
let mut args = vec!["plan"];
args.extend_from_slice(extra_args);
self.command_executor
.run_command("tofu", &args, Some(&self.working_dir))
.map(|result| result.stdout)
}
pub fn apply(&self, auto_approve: bool, extra_args: &[&str]) -> Result<String, CommandError> {
info!(
"Applying infrastructure changes in directory: {}",
self.working_dir.display()
);
let mut args = vec!["apply"];
args.extend_from_slice(extra_args);
if auto_approve {
args.push("-auto-approve");
}
self.command_executor
.run_command("tofu", &args, Some(&self.working_dir))
.map(|result| result.stdout)
}
pub fn destroy(&self, auto_approve: bool, extra_args: &[&str]) -> Result<String, CommandError> {
info!(
"Destroying infrastructure in directory: {}",
self.working_dir.display()
);
let mut args = vec!["destroy"];
args.extend_from_slice(extra_args);
if auto_approve {
args.push("-auto-approve");
}
self.command_executor
.run_command("tofu", &args, Some(&self.working_dir))
.map(|result| result.stdout)
}
pub fn get_instance_info(&self) -> Result<InstanceInfo, OpenTofuError> {
info!(
"Getting OpenTofu outputs from directory: {}",
self.working_dir.display()
);
let output = self.command_executor.run_command(
"tofu",
&["output", "-json"],
Some(&self.working_dir),
)?;
let instance_info = OpenTofuJsonParser::parse_instance_info(&output.stdout)?;
Ok(instance_info)
}
#[must_use]
pub fn working_dir(&self) -> &Path {
&self.working_dir
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_create_opentofu_client_with_valid_parameters() {
let client = OpenTofuClient::new("/path/to/config");
assert_eq!(client.working_dir.to_string_lossy(), "/path/to/config");
}
#[test]
fn it_should_create_opentofu_client_with_working_directory() {
let client = OpenTofuClient::new("/path/to/config");
assert_eq!(client.working_dir.to_string_lossy(), "/path/to/config");
}
#[test]
fn it_should_return_working_directory_path() {
let client = OpenTofuClient::new("/test/path");
assert_eq!(client.working_dir(), Path::new("/test/path"));
}
#[test]
fn it_should_construct_pathbuf_from_string() {
let path_str = "/some/test/path";
let client = OpenTofuClient::new(path_str);
assert_eq!(client.working_dir(), Path::new(path_str));
}
#[test]
fn it_should_construct_pathbuf_from_path() {
let path = Path::new("/another/test/path");
let client = OpenTofuClient::new(path);
assert_eq!(client.working_dir(), path);
}
#[test]
fn it_should_wrap_parse_error_in_opentofu_error() {
use crate::adapters::tofu::json_parser::OpenTofuJsonParser;
let invalid_json = "not valid json";
let parse_error = OpenTofuJsonParser::parse_instance_info(invalid_json).unwrap_err();
let opentofu_error = OpenTofuError::ParseError(parse_error);
assert!(matches!(opentofu_error, OpenTofuError::ParseError(_)));
assert!(opentofu_error.to_string().contains("Parse error"));
}
#[test]
fn it_should_wrap_command_error_in_opentofu_error() {
let command_error = CommandError::StartupFailed {
command: "tofu".to_string(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "Command not found"),
};
let opentofu_error = OpenTofuError::CommandError(command_error);
assert!(matches!(opentofu_error, OpenTofuError::CommandError(_)));
assert!(opentofu_error
.to_string()
.contains("Command execution failed"));
}
}