Skip to content

Commit

Permalink
Config file structs are now config and will not be state.
Browse files Browse the repository at this point in the history
This commit also renames some modules so it is clean what parts are gitoxide-specific.
  • Loading branch information
bittrance committed Dec 27, 2023
1 parent cce01a3 commit b86302a
Show file tree
Hide file tree
Showing 12 changed files with 345 additions and 366 deletions.
71 changes: 23 additions & 48 deletions src/actions.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::{
collections::HashMap,
io::Read,
path::Path,
process::{Command, Stdio},
Expand All @@ -8,11 +7,9 @@ use std::{
time::Instant,
};

use serde::Deserialize;

use crate::{
config::ActionConfig,
errors::GitOpsError,
opts::CliOptions,
receiver::{SourceType, WorkloadEvent},
utils::POLL_INTERVAL,
};
Expand All @@ -23,60 +20,35 @@ pub enum ActionResult {
Failure,
}

#[derive(Clone, Debug, Deserialize)]
#[derive(Clone)]
pub struct Action {
name: String,
entrypoint: String,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
environment: HashMap<String, String>,
#[serde(default)]
inherit_environment: bool,
config: ActionConfig,
}

impl Action {
pub fn id(&self) -> String {
self.name.clone()
pub fn new(config: ActionConfig) -> Self {
Action { config }
}

pub fn set_env(&mut self, key: String, val: String) {
self.environment.insert(key, val);
pub fn id(&self) -> String {
self.config.name.clone()
}
}

impl TryFrom<&CliOptions> for Action {
type Error = GitOpsError;

fn try_from(opts: &CliOptions) -> Result<Self, Self::Error> {
let mut environment = HashMap::new();
for env in &opts.environment {
let (key, val) = env
.split_once('=')
.ok_or_else(|| GitOpsError::InvalidEnvVar(env.clone()))?;
environment.insert(key.to_owned(), val.to_owned());
}
Ok(Self {
name: opts.action.clone().unwrap(),
// TODO --action won't work on Windows
entrypoint: "/bin/sh".to_string(),
args: vec!["-c".to_string(), opts.action.clone().unwrap()],
environment,
inherit_environment: false,
})
pub fn set_env(&mut self, key: String, val: String) {
self.config.environment.insert(key, val);
}
}

fn build_command(action: &Action, cwd: &Path) -> Command {
let mut command = Command::new(action.entrypoint.clone());
command.args(action.args.clone());
if !action.inherit_environment {
fn build_command(config: &ActionConfig, cwd: &Path) -> Command {
let mut command = Command::new(config.entrypoint.clone());
command.args(config.args.clone());
if !config.inherit_environment {
command.env_clear();
if let Ok(path) = std::env::var("PATH") {
command.env("PATH", path);
}
}
command.envs(action.environment.iter());
command.envs(config.environment.iter());
command.current_dir(cwd);
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
Expand Down Expand Up @@ -121,7 +93,7 @@ pub fn run_action<F>(
where
F: Fn(WorkloadEvent) -> Result<(), GitOpsError> + Send + 'static,
{
let mut command = build_command(action, cwd);
let mut command = build_command(&action.config, cwd);
let mut child = command.spawn().map_err(GitOpsError::ActionError)?;
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
Expand Down Expand Up @@ -152,6 +124,7 @@ where
#[cfg(test)]
mod tests {
use std::{
collections::HashMap,
process::ExitStatus,
sync::{Arc, Mutex},
time::Duration,
Expand All @@ -162,11 +135,13 @@ mod tests {

fn shell_action(cmd: &str) -> Action {
Action {
name: "test".to_owned(),
entrypoint: "/bin/sh".to_owned(),
args: vec!["-c".to_owned(), cmd.to_owned()],
environment: HashMap::new(),
inherit_environment: false,
config: ActionConfig {
name: "test".to_owned(),
entrypoint: "/bin/sh".to_owned(),
args: vec!["-c".to_owned(), cmd.to_owned()],
environment: HashMap::new(),
inherit_environment: false,
},
}
}

Expand Down
202 changes: 202 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
use std::{collections::HashMap, io::Read, path::PathBuf, time::Duration};

use gix::Url;
use serde::{Deserialize, Deserializer};

use crate::{errors::GitOpsError, opts::CliOptions};

#[derive(Deserialize)]
pub struct ConfigFile {
pub tasks: Vec<GitTaskConfig>,
}

#[derive(Clone, Deserialize)]
pub struct GitTaskConfig {
pub name: String,
pub github: Option<GithubConfig>,
pub git: GitConfig,
pub actions: Vec<ActionConfig>,
#[serde(
default = "GitTaskConfig::default_interval",
deserialize_with = "human_readable_duration"
)]
pub interval: Duration,
#[serde(
default = "GitTaskConfig::default_timeout",
deserialize_with = "human_readable_duration"
)]
pub timeout: Duration,
}

impl GitTaskConfig {
pub fn default_interval() -> Duration {
Duration::from_secs(60)
}

pub fn default_timeout() -> Duration {
Duration::from_secs(3600)
}
}

impl TryFrom<&CliOptions> for GitTaskConfig {
type Error = GitOpsError;

fn try_from(opts: &CliOptions) -> Result<Self, Self::Error> {
let url = Url::try_from(opts.url.clone().unwrap()).map_err(GitOpsError::InvalidUrl)?;
let action: ActionConfig = TryFrom::try_from(opts)?;
Ok(Self {
name: url.path.to_string(),
github: TryFrom::try_from(opts)?,
git: TryFrom::try_from(opts)?,
actions: vec![action],
interval: opts.interval.unwrap_or(Self::default_interval()),
timeout: opts.timeout.unwrap_or(Self::default_timeout()),
})
}
}

#[derive(Clone, Deserialize)]
pub struct GithubConfig {
pub app_id: String,
pub private_key_file: PathBuf,
#[serde(default = "GithubConfig::default_context")]
pub status_context: Option<String>,
}

impl GithubConfig {
pub fn default_context() -> Option<String> {
Some("kitops".to_owned())
}
}

impl TryFrom<&CliOptions> for Option<GithubConfig> {
type Error = GitOpsError;

fn try_from(opts: &CliOptions) -> Result<Self, Self::Error> {
match (&opts.github_app_id, &opts.github_private_key_file) {
(None, None) => Ok(None),
(Some(app_id), Some(private_key_file)) => Ok(Some(GithubConfig {
app_id: app_id.clone(),
private_key_file: private_key_file.clone(),
status_context: opts.github_status_context.clone(),
})),
_ => Err(GitOpsError::InvalidNotifyConfig),
}
}
}

#[derive(Clone, Deserialize)]
pub struct GitConfig {
#[serde(deserialize_with = "url_from_string")]
pub url: Url,
#[serde(default = "GitConfig::default_branch")]
pub branch: String,
}

impl GitConfig {
pub fn default_branch() -> String {
"main".to_owned()
}
}

impl TryFrom<&CliOptions> for GitConfig {
type Error = GitOpsError;

fn try_from(opts: &CliOptions) -> Result<Self, Self::Error> {
let url = Url::try_from(opts.url.clone().unwrap()).map_err(GitOpsError::InvalidUrl)?;
Ok(GitConfig {
url,
branch: opts.branch.clone(),
})
}
}

#[derive(Clone, Debug, Deserialize)]
pub struct ActionConfig {
pub name: String,
pub entrypoint: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub environment: HashMap<String, String>,
#[serde(default)]
pub inherit_environment: bool,
}

impl TryFrom<&CliOptions> for ActionConfig {
type Error = GitOpsError;

fn try_from(opts: &CliOptions) -> Result<Self, Self::Error> {
let mut environment = HashMap::new();
for env in &opts.environment {
let (key, val) = env
.split_once('=')
.ok_or_else(|| GitOpsError::InvalidEnvVar(env.clone()))?;
environment.insert(key.to_owned(), val.to_owned());
}
Ok(ActionConfig {
name: opts.action.clone().unwrap(),
// TODO --action won't work on Windows
entrypoint: "/bin/sh".to_string(),
args: vec!["-c".to_string(), opts.action.clone().unwrap()],
environment,
inherit_environment: false,
})
}
}

fn human_readable_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
humantime::parse_duration(&s).map_err(serde::de::Error::custom)
}

fn url_from_string<'de, D>(deserializer: D) -> Result<Url, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
Url::try_from(s).map_err(serde::de::Error::custom)
}

pub fn read_config(reader: impl Read) -> Result<ConfigFile, GitOpsError> {
serde_yaml::from_reader(reader).map_err(GitOpsError::MalformedConfig)
}

#[cfg(test)]
mod tests {
use std::time::Duration;

use crate::config::GitTaskConfig;

use super::read_config;

#[test]
fn minimum_config() {
let config = r#"tasks:
- name: testo
git:
url: https://github.com/bittrance/kitops
actions:
- name: list files
entrypoint: /bin/ls
"#;
read_config(config.as_bytes()).unwrap();
}

#[test]
fn parse_gittaskconfig() {
let raw_config = r#"name: testo
git:
url: https://github.com/bittrance/kitops
timeout: 3s
interval: 1m 2s
actions: []
"#;
let config = serde_yaml::from_str::<GitTaskConfig>(raw_config).unwrap();
assert_eq!(config.timeout, Duration::from_secs(3));
assert_eq!(config.interval, Duration::from_secs(62));
}
}
Loading

0 comments on commit b86302a

Please sign in to comment.