Skip to content

Commit 1a21963

Browse files
APT-491 added Artifactory tool source target support (#80)
Added Hosts section support in toml Added Artifactory tool source target support. Note: Artifactory can be targeted, but installing tools from Artifactory is still not supported.
1 parent 3b14a12 commit 1a21963

File tree

5 files changed

+283
-6
lines changed

5 files changed

+283
-6
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,23 @@ As you may already have noticed, the tool name is located at the left side of `=
6464

6565
Previously, foreman was only able to download tools from GitHub and the format used to be `source = "rojo-rbx/rojo"`. For backward compatibility, foreman still supports this format.
6666

67+
### Hosts (Under Construction)
68+
foreman supports Github and Gitlab as hosts by default, but you can define your own custom hosts as well using a single `hosts` entry and an enumeration of the hosts you want to download tools from, which looks like this.
69+
70+
```toml
71+
[hosts]
72+
# default hosts
73+
# source = {source = "https://github.com", protocol = "github"}
74+
# github = {source = "https://github.com", protocol = "github"}
75+
# gitlab = {source = "https://gitlab.com", protocol = "gitlab"}
76+
artifactory = {souce = "https://artifactory.com", protocol = "artifactory"}
77+
78+
[tools]
79+
rotrieve = {artifactory = "tools/rotriever", version = "0.5.12"}
80+
```
81+
82+
foreman currently only supports github, gitlab, and artifactory as protocols.
83+
6784
### System Tools
6885
To start using Foreman to manage your system's default tools, create the file `~/.foreman/foreman.toml`.
6986

src/config.rs

Lines changed: 208 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub struct ToolSpec {
2626
pub enum Protocol {
2727
Github,
2828
Gitlab,
29+
Artifactory,
2930
}
3031

3132
impl ToolSpec {
@@ -96,13 +97,15 @@ impl ToolSpec {
9697
match self.protocol {
9798
Protocol::Github => CiString(format!("{}", self.path)),
9899
Protocol::Gitlab => CiString(format!("gitlab@{}", self.path)),
100+
Protocol::Artifactory => CiString(format!("{}@{}", self.host, self.path)),
99101
}
100102
}
101103

102104
pub fn source(&self) -> String {
103105
let provider = match self.protocol {
104106
Protocol::Github => "github.com",
105107
Protocol::Gitlab => "gitlab.com",
108+
Protocol::Artifactory => "artifactory.com",
106109
};
107110

108111
format!("{}/{}", provider, self.path)
@@ -120,6 +123,7 @@ impl ToolSpec {
120123
match self.protocol {
121124
Protocol::Github => Provider::Github,
122125
Protocol::Gitlab => Provider::Gitlab,
126+
Protocol::Artifactory => Provider::Artifactory,
123127
}
124128
}
125129
}
@@ -130,7 +134,7 @@ impl fmt::Display for ToolSpec {
130134
}
131135
}
132136

133-
#[derive(Debug)]
137+
#[derive(Debug, PartialEq)]
134138
pub struct ConfigFile {
135139
pub tools: BTreeMap<String, ToolSpec>,
136140
pub hosts: HashMap<String, Host>,
@@ -149,6 +153,57 @@ impl Host {
149153
protocol,
150154
}
151155
}
156+
157+
pub fn from_value(value: &Value) -> ConfigFileParseResult<Self> {
158+
if let Value::Table(mut map) = value.clone() {
159+
let source = map
160+
.remove("source")
161+
.ok_or_else(|| ConfigFileParseError::Host {
162+
host: value.to_string(),
163+
})?
164+
.as_str()
165+
.ok_or_else(|| ConfigFileParseError::Host {
166+
host: value.to_string(),
167+
})?
168+
.to_string();
169+
170+
let protocol_value =
171+
map.remove("protocol")
172+
.ok_or_else(|| ConfigFileParseError::Host {
173+
host: value.to_string(),
174+
})?;
175+
176+
if !map.is_empty() {
177+
return Err(ConfigFileParseError::Host {
178+
host: value.to_string(),
179+
});
180+
}
181+
182+
let protocol_str =
183+
protocol_value
184+
.as_str()
185+
.ok_or_else(|| ConfigFileParseError::Host {
186+
host: value.to_string(),
187+
})?;
188+
189+
let protocol = match protocol_str {
190+
"github" => Protocol::Github,
191+
"gitlab" => Protocol::Gitlab,
192+
"artifactory" => Protocol::Artifactory,
193+
_ => {
194+
return Err(ConfigFileParseError::InvalidProtocol {
195+
protocol: protocol_str.to_string(),
196+
})
197+
}
198+
};
199+
200+
Ok(Self { source, protocol })
201+
} else {
202+
Err(ConfigFileParseError::Host {
203+
host: value.to_string(),
204+
})
205+
}
206+
}
152207
}
153208

154209
impl ConfigFile {
@@ -167,6 +222,18 @@ impl ConfigFile {
167222
let mut config = ConfigFile::new_with_defaults();
168223

169224
if let Value::Table(top_level) = &value {
225+
if let Some(hosts) = &top_level.get("hosts") {
226+
if let Value::Table(hosts) = hosts {
227+
for (host, toml) in hosts {
228+
let host_source =
229+
Host::from_value(&toml).map_err(|_| ConfigFileParseError::Tool {
230+
tool: value.to_string(),
231+
})?;
232+
config.hosts.insert(host.to_owned(), host_source);
233+
}
234+
}
235+
}
236+
170237
if let Some(tools) = &top_level.get("tools") {
171238
if let Value::Table(tools) = tools {
172239
for (tool, toml) in tools {
@@ -260,6 +327,7 @@ impl fmt::Display for ConfigFile {
260327

261328
#[cfg(test)]
262329
mod test {
330+
const ARTIFACTORY: &'static str = "https://artifactory.com";
263331
use super::*;
264332

265333
fn new_github<S: Into<String>>(github: S, version: VersionReq) -> ToolSpec {
@@ -280,10 +348,32 @@ mod test {
280348
}
281349
}
282350

351+
fn new_artifactory<S: Into<String>>(host: S, path: S, version: VersionReq) -> ToolSpec {
352+
ToolSpec {
353+
host: host.into(),
354+
path: path.into(),
355+
version: version,
356+
protocol: Protocol::Artifactory,
357+
}
358+
}
359+
360+
fn new_config(tools: BTreeMap<String, ToolSpec>, hosts: HashMap<String, Host>) -> ConfigFile {
361+
let mut config = ConfigFile::new_with_defaults();
362+
config.fill_from(ConfigFile { tools, hosts });
363+
config
364+
}
365+
283366
fn version(string: &str) -> VersionReq {
284367
VersionReq::parse(string).unwrap()
285368
}
286369

370+
fn new_host<S: Into<String>>(source: S, protocol: Protocol) -> Host {
371+
Host {
372+
source: source.into(),
373+
protocol,
374+
}
375+
}
376+
287377
fn default_hosts() -> HashMap<String, Host> {
288378
HashMap::from([
289379
(
@@ -301,7 +391,17 @@ mod test {
301391
])
302392
}
303393

394+
fn artifactory_host() -> HashMap<String, Host> {
395+
let mut hosts = default_hosts();
396+
hosts.insert(
397+
"artifactory".to_string(),
398+
Host::new(ARTIFACTORY.to_string(), Protocol::Artifactory),
399+
);
400+
hosts
401+
}
402+
304403
mod deserialization {
404+
305405
use super::*;
306406

307407
#[test]
@@ -333,25 +433,65 @@ mod test {
333433
assert_eq!(gitlab, new_gitlab("user/repo", version("0.1.0")));
334434
}
335435

436+
#[test]
437+
fn artifactory_from_artifactory_field() {
438+
let value: Value = toml::from_str(
439+
&[
440+
r#"artifactory = "generic-rbx-local-tools/rotriever/""#,
441+
r#"version = "0.5.4""#,
442+
]
443+
.join("\n"),
444+
)
445+
.unwrap();
446+
447+
let artifactory = ToolSpec::from_value(&value, &artifactory_host()).unwrap();
448+
assert_eq!(
449+
artifactory,
450+
new_artifactory(
451+
"https://artifactory.com",
452+
"generic-rbx-local-tools/rotriever/",
453+
version("0.5.4")
454+
)
455+
);
456+
}
457+
458+
#[test]
459+
fn host_artifactory() {
460+
let value: Value = toml::from_str(
461+
&[
462+
r#"source = "https://artifactory.com""#,
463+
r#"protocol = "artifactory""#,
464+
]
465+
.join("\n"),
466+
)
467+
.unwrap();
468+
469+
let host = Host::from_value(&value).unwrap();
470+
assert_eq!(
471+
host,
472+
new_host("https://artifactory.com", Protocol::Artifactory)
473+
)
474+
}
475+
336476
#[test]
337477
fn extraneous_fields_tools() {
338478
let value: Value = toml::from_str(
339479
&[
340-
r#"github = "Roblox/rotriever""#,
341-
r#"path = "Roblox/rotriever""#,
480+
r#"rbx_artifactory = "generic-rbx-local-tools/rotriever/""#,
481+
r#"path = "generic-rbx-local-tools/rotriever/""#,
342482
r#"version = "0.5.4""#,
343483
]
344484
.join("\n"),
345485
)
346486
.unwrap();
347487

348-
let artifactory = ToolSpec::from_value(&value, &default_hosts()).unwrap_err();
488+
let artifactory = ToolSpec::from_value(&value, &artifactory_host()).unwrap_err();
349489
assert_eq!(
350490
artifactory,
351491
ConfigFileParseError::Tool {
352492
tool: [
353-
r#"github = "Roblox/rotriever""#,
354-
r#"path = "Roblox/rotriever""#,
493+
r#"path = "generic-rbx-local-tools/rotriever/""#,
494+
r#"rbx_artifactory = "generic-rbx-local-tools/rotriever/""#,
355495
r#"version = "0.5.4""#,
356496
r#""#,
357497
]
@@ -360,6 +500,68 @@ mod test {
360500
}
361501
)
362502
}
503+
504+
#[test]
505+
fn extraneous_fields_host() {
506+
let value: Value = toml::from_str(
507+
&[
508+
r#"source = "https://artifactory.com""#,
509+
r#"protocol = "artifactory""#,
510+
r#"extra = "field""#,
511+
]
512+
.join("\n"),
513+
)
514+
.unwrap();
515+
516+
let err = Host::from_value(&value).unwrap_err();
517+
assert_eq!(
518+
err,
519+
ConfigFileParseError::Host {
520+
host: [
521+
r#"extra = "field""#,
522+
r#"protocol = "artifactory""#,
523+
r#"source = "https://artifactory.com""#,
524+
r#""#,
525+
]
526+
.join("\n")
527+
.to_string()
528+
}
529+
)
530+
}
531+
#[test]
532+
fn config_file_with_hosts() {
533+
let value: Value = toml::from_str(&[
534+
r#"[hosts]"#,
535+
r#"artifactory = {source = "https://artifactory.com", protocol = "artifactory"}"#,
536+
r#""#,
537+
r#"[tools]"#,
538+
r#"tool = {artifactory = "path/to/tool", version = "1.0.0"}"#,
539+
].join("\n"))
540+
.unwrap();
541+
542+
let config = ConfigFile::from_value(value).unwrap();
543+
assert_eq!(
544+
config,
545+
new_config(
546+
BTreeMap::from([(
547+
"tool".to_string(),
548+
ToolSpec {
549+
host: "https://artifactory.com".to_string(),
550+
path: "path/to/tool".to_string(),
551+
version: VersionReq::parse("1.0.0").unwrap(),
552+
protocol: Protocol::Artifactory
553+
}
554+
)]),
555+
HashMap::from([(
556+
"artifactory".to_string(),
557+
Host {
558+
source: "https://artifactory.com".to_string(),
559+
protocol: Protocol::Artifactory
560+
}
561+
)])
562+
)
563+
)
564+
}
363565
}
364566

365567
#[test]

src/error.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,17 @@ pub enum ForemanError {
7171
ToolsNotDownloaded {
7272
tools: Vec<String>,
7373
},
74+
Other {
75+
message: String,
76+
},
7477
}
7578

7679
#[derive(Debug, PartialEq)]
7780
pub enum ConfigFileParseError {
7881
MissingField { field: String },
7982
Tool { tool: String },
83+
Host { host: String },
84+
InvalidProtocol { protocol: String },
8085
}
8186

8287
impl ForemanError {
@@ -314,6 +319,9 @@ impl fmt::Display for ForemanError {
314319
Self::ToolsNotDownloaded { tools } => {
315320
write!(f, "The following tools were not installed:\n{:#?}", tools)
316321
}
322+
Self::Other { message } => {
323+
write!(f, "{}", message)
324+
}
317325
}
318326
}
319327
}
@@ -325,6 +333,12 @@ impl fmt::Display for ConfigFileParseError {
325333
Self::Tool { tool } => {
326334
write!(f, "data is not properly formatted for tool:\n\n{}", tool)
327335
}
336+
Self::Host { host } => {
337+
write!(f, "data is not properly formatted for host:\n\n{}", host)
338+
}
339+
Self::InvalidProtocol { protocol } => {
340+
write!(f, "protocol `{}` is not valid. Foreman only supports `github`, `gitlab`, and `artifactory`\n\n", protocol)
341+
}
328342
}
329343
}
330344
}

0 commit comments

Comments
 (0)