Skip to content

Commit 850fb0e

Browse files
authored
Merge pull request #6 from bittrance/dedicated-config
Config parsing is gathered in its own file
2 parents 51b674b + 58741ae commit 850fb0e

File tree

16 files changed

+477
-436
lines changed

16 files changed

+477
-436
lines changed

src/actions.rs

Lines changed: 23 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use std::{
2-
collections::HashMap,
32
io::Read,
43
path::Path,
54
process::{Command, Stdio},
@@ -8,11 +7,9 @@ use std::{
87
time::Instant,
98
};
109

11-
use serde::Deserialize;
12-
1310
use crate::{
11+
config::ActionConfig,
1412
errors::GitOpsError,
15-
opts::CliOptions,
1613
receiver::{SourceType, WorkloadEvent},
1714
utils::POLL_INTERVAL,
1815
};
@@ -23,60 +20,35 @@ pub enum ActionResult {
2320
Failure,
2421
}
2522

26-
#[derive(Clone, Debug, Deserialize)]
23+
#[derive(Clone)]
2724
pub struct Action {
28-
name: String,
29-
entrypoint: String,
30-
#[serde(default)]
31-
args: Vec<String>,
32-
#[serde(default)]
33-
environment: HashMap<String, String>,
34-
#[serde(default)]
35-
inherit_environment: bool,
25+
config: ActionConfig,
3626
}
3727

3828
impl Action {
39-
pub fn id(&self) -> String {
40-
self.name.clone()
29+
pub fn new(config: ActionConfig) -> Self {
30+
Action { config }
4131
}
4232

43-
pub fn set_env(&mut self, key: String, val: String) {
44-
self.environment.insert(key, val);
33+
pub fn id(&self) -> String {
34+
self.config.name.clone()
4535
}
46-
}
47-
48-
impl TryFrom<&CliOptions> for Action {
49-
type Error = GitOpsError;
5036

51-
fn try_from(opts: &CliOptions) -> Result<Self, Self::Error> {
52-
let mut environment = HashMap::new();
53-
for env in &opts.environment {
54-
let (key, val) = env
55-
.split_once('=')
56-
.ok_or_else(|| GitOpsError::InvalidEnvVar(env.clone()))?;
57-
environment.insert(key.to_owned(), val.to_owned());
58-
}
59-
Ok(Self {
60-
name: opts.action.clone().unwrap(),
61-
// TODO --action won't work on Windows
62-
entrypoint: "/bin/sh".to_string(),
63-
args: vec!["-c".to_string(), opts.action.clone().unwrap()],
64-
environment,
65-
inherit_environment: false,
66-
})
37+
pub fn set_env(&mut self, key: String, val: String) {
38+
self.config.environment.insert(key, val);
6739
}
6840
}
6941

70-
fn build_command(action: &Action, cwd: &Path) -> Command {
71-
let mut command = Command::new(action.entrypoint.clone());
72-
command.args(action.args.clone());
73-
if !action.inherit_environment {
42+
fn build_command(config: &ActionConfig, cwd: &Path) -> Command {
43+
let mut command = Command::new(config.entrypoint.clone());
44+
command.args(config.args.clone());
45+
if !config.inherit_environment {
7446
command.env_clear();
7547
if let Ok(path) = std::env::var("PATH") {
7648
command.env("PATH", path);
7749
}
7850
}
79-
command.envs(action.environment.iter());
51+
command.envs(config.environment.iter());
8052
command.current_dir(cwd);
8153
command.stdout(Stdio::piped());
8254
command.stderr(Stdio::piped());
@@ -121,7 +93,7 @@ pub fn run_action<F>(
12193
where
12294
F: Fn(WorkloadEvent) -> Result<(), GitOpsError> + Send + 'static,
12395
{
124-
let mut command = build_command(action, cwd);
96+
let mut command = build_command(&action.config, cwd);
12597
let mut child = command.spawn().map_err(GitOpsError::ActionError)?;
12698
let stdout = child.stdout.take().unwrap();
12799
let stderr = child.stderr.take().unwrap();
@@ -152,6 +124,7 @@ where
152124
#[cfg(test)]
153125
mod tests {
154126
use std::{
127+
collections::HashMap,
155128
process::ExitStatus,
156129
sync::{Arc, Mutex},
157130
time::Duration,
@@ -162,11 +135,13 @@ mod tests {
162135

163136
fn shell_action(cmd: &str) -> Action {
164137
Action {
165-
name: "test".to_owned(),
166-
entrypoint: "/bin/sh".to_owned(),
167-
args: vec!["-c".to_owned(), cmd.to_owned()],
168-
environment: HashMap::new(),
169-
inherit_environment: false,
138+
config: ActionConfig {
139+
name: "test".to_owned(),
140+
entrypoint: "/bin/sh".to_owned(),
141+
args: vec!["-c".to_owned(), cmd.to_owned()],
142+
environment: HashMap::new(),
143+
inherit_environment: false,
144+
},
170145
}
171146
}
172147

src/config.rs

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
use std::{collections::HashMap, io::Read, path::PathBuf, time::Duration};
2+
3+
use gix::Url;
4+
use serde::{Deserialize, Deserializer};
5+
6+
use crate::{errors::GitOpsError, opts::CliOptions};
7+
8+
#[derive(Deserialize)]
9+
#[serde(deny_unknown_fields)]
10+
pub struct ConfigFile {
11+
pub tasks: Vec<GitTaskConfig>,
12+
}
13+
14+
#[derive(Clone, Deserialize)]
15+
#[serde(deny_unknown_fields)]
16+
pub struct GitTaskConfig {
17+
pub name: String,
18+
pub github: Option<GithubConfig>,
19+
pub git: GitConfig,
20+
pub actions: Vec<ActionConfig>,
21+
#[serde(
22+
default = "GitTaskConfig::default_interval",
23+
deserialize_with = "human_readable_duration"
24+
)]
25+
pub interval: Duration,
26+
#[serde(
27+
default = "GitTaskConfig::default_timeout",
28+
deserialize_with = "human_readable_duration"
29+
)]
30+
pub timeout: Duration,
31+
}
32+
33+
impl GitTaskConfig {
34+
pub fn default_interval() -> Duration {
35+
Duration::from_secs(60)
36+
}
37+
38+
pub fn default_timeout() -> Duration {
39+
Duration::from_secs(3600)
40+
}
41+
}
42+
43+
impl TryFrom<&CliOptions> for GitTaskConfig {
44+
type Error = GitOpsError;
45+
46+
fn try_from(opts: &CliOptions) -> Result<Self, Self::Error> {
47+
let url = Url::try_from(opts.url.clone().unwrap()).map_err(GitOpsError::InvalidUrl)?;
48+
let action: ActionConfig = TryFrom::try_from(opts)?;
49+
Ok(Self {
50+
name: url.path.to_string(),
51+
github: TryFrom::try_from(opts)?,
52+
git: TryFrom::try_from(opts)?,
53+
actions: vec![action],
54+
interval: opts.interval.unwrap_or(Self::default_interval()),
55+
timeout: opts.timeout.unwrap_or(Self::default_timeout()),
56+
})
57+
}
58+
}
59+
60+
#[derive(Clone, Deserialize)]
61+
#[serde(deny_unknown_fields)]
62+
pub struct GithubConfig {
63+
pub app_id: String,
64+
pub private_key_file: PathBuf,
65+
#[serde(default = "GithubConfig::default_context")]
66+
pub status_context: Option<String>,
67+
}
68+
69+
impl GithubConfig {
70+
pub fn default_context() -> Option<String> {
71+
Some("kitops".to_owned())
72+
}
73+
}
74+
75+
impl TryFrom<&CliOptions> for Option<GithubConfig> {
76+
type Error = GitOpsError;
77+
78+
fn try_from(opts: &CliOptions) -> Result<Self, Self::Error> {
79+
match (&opts.github_app_id, &opts.github_private_key_file) {
80+
(None, None) => Ok(None),
81+
(Some(app_id), Some(private_key_file)) => Ok(Some(GithubConfig {
82+
app_id: app_id.clone(),
83+
private_key_file: private_key_file.clone(),
84+
status_context: opts.github_status_context.clone(),
85+
})),
86+
_ => Err(GitOpsError::InvalidNotifyConfig),
87+
}
88+
}
89+
}
90+
91+
#[derive(Clone, Deserialize)]
92+
#[serde(deny_unknown_fields)]
93+
pub struct GitConfig {
94+
#[serde(deserialize_with = "url_from_string")]
95+
pub url: Url,
96+
#[serde(default = "GitConfig::default_branch")]
97+
pub branch: String,
98+
}
99+
100+
impl GitConfig {
101+
pub fn default_branch() -> String {
102+
"main".to_owned()
103+
}
104+
}
105+
106+
impl TryFrom<&CliOptions> for GitConfig {
107+
type Error = GitOpsError;
108+
109+
fn try_from(opts: &CliOptions) -> Result<Self, Self::Error> {
110+
let url = Url::try_from(opts.url.clone().unwrap()).map_err(GitOpsError::InvalidUrl)?;
111+
Ok(GitConfig {
112+
url,
113+
branch: opts.branch.clone(),
114+
})
115+
}
116+
}
117+
118+
#[derive(Clone, Debug, Deserialize)]
119+
#[serde(deny_unknown_fields)]
120+
pub struct ActionConfig {
121+
pub name: String,
122+
pub entrypoint: String,
123+
#[serde(default)]
124+
pub args: Vec<String>,
125+
#[serde(default)]
126+
pub environment: HashMap<String, String>,
127+
#[serde(default)]
128+
pub inherit_environment: bool,
129+
}
130+
131+
impl TryFrom<&CliOptions> for ActionConfig {
132+
type Error = GitOpsError;
133+
134+
fn try_from(opts: &CliOptions) -> Result<Self, Self::Error> {
135+
let mut environment = HashMap::new();
136+
for env in &opts.environment {
137+
let (key, val) = env
138+
.split_once('=')
139+
.ok_or_else(|| GitOpsError::InvalidEnvVar(env.clone()))?;
140+
environment.insert(key.to_owned(), val.to_owned());
141+
}
142+
Ok(ActionConfig {
143+
name: opts.action.clone().unwrap(),
144+
// TODO --action won't work on Windows
145+
entrypoint: "/bin/sh".to_string(),
146+
args: vec!["-c".to_string(), opts.action.clone().unwrap()],
147+
environment,
148+
inherit_environment: false,
149+
})
150+
}
151+
}
152+
153+
fn human_readable_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
154+
where
155+
D: Deserializer<'de>,
156+
{
157+
let s: String = Deserialize::deserialize(deserializer)?;
158+
humantime::parse_duration(&s).map_err(serde::de::Error::custom)
159+
}
160+
161+
fn url_from_string<'de, D>(deserializer: D) -> Result<Url, D::Error>
162+
where
163+
D: Deserializer<'de>,
164+
{
165+
let s: String = Deserialize::deserialize(deserializer)?;
166+
Url::try_from(s).map_err(serde::de::Error::custom)
167+
}
168+
169+
pub fn read_config(reader: impl Read) -> Result<ConfigFile, GitOpsError> {
170+
serde_yaml::from_reader(reader).map_err(GitOpsError::MalformedConfig)
171+
}
172+
173+
#[cfg(test)]
174+
mod tests {
175+
use std::time::Duration;
176+
177+
use crate::{config::GitTaskConfig, errors::GitOpsError};
178+
179+
use super::read_config;
180+
181+
#[test]
182+
fn minimum_config() {
183+
let config = r#"tasks:
184+
- name: testo
185+
git:
186+
url: https://github.com/bittrance/kitops
187+
actions:
188+
- name: list files
189+
entrypoint: /bin/ls
190+
"#;
191+
read_config(config.as_bytes()).unwrap();
192+
}
193+
194+
#[test]
195+
fn fail_on_unknown_git_config() {
196+
let config = r#"tasks:
197+
- name: testo
198+
git:
199+
url: https://github.com/bittrance/kitops
200+
non: sense
201+
actions:
202+
- name: list files
203+
entrypoint: /bin/ls
204+
"#;
205+
assert!(matches!(
206+
read_config(config.as_bytes()),
207+
Err(GitOpsError::MalformedConfig(_))
208+
));
209+
}
210+
211+
#[test]
212+
fn fail_on_unknown_action_config() {
213+
let config = r#"tasks:
214+
- name: testo
215+
git:
216+
url: https://github.com/bittrance/kitops
217+
actions:
218+
- name: list files
219+
non: sense
220+
entrypoint: /bin/ls
221+
"#;
222+
assert!(matches!(
223+
read_config(config.as_bytes()),
224+
Err(GitOpsError::MalformedConfig(_))
225+
));
226+
}
227+
228+
#[test]
229+
fn action_environment_config() {
230+
let config = r#"tasks:
231+
- name: testo
232+
git:
233+
url: https://github.com/bittrance/kitops
234+
actions:
235+
- name: list files
236+
entrypoint: /bin/ls
237+
environment:
238+
FOO: bar
239+
"#;
240+
read_config(config.as_bytes()).unwrap();
241+
}
242+
243+
#[test]
244+
fn parse_gittaskconfig() {
245+
let raw_config = r#"name: testo
246+
git:
247+
url: https://github.com/bittrance/kitops
248+
timeout: 3s
249+
interval: 1m 2s
250+
actions: []
251+
"#;
252+
let config = serde_yaml::from_str::<GitTaskConfig>(raw_config).unwrap();
253+
assert_eq!(config.timeout, Duration::from_secs(3));
254+
assert_eq!(config.interval, Duration::from_secs(62));
255+
}
256+
}

src/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ pub enum GitOpsError {
4444
NotifyError(String),
4545
#[error("Failed to launch action: {0}")]
4646
ActionError(std::io::Error),
47+
#[error("Auth only on HTTPS URLs: {0}")]
48+
GitHubAuthNonHttpsUrl(String),
4749
#[error("Missing private key file: {0}")]
4850
GitHubMissingPrivateKeyFile(std::io::Error),
4951
#[error("Malformed private RS256 key: {0}")]

0 commit comments

Comments
 (0)