Skip to content

Commit

Permalink
APT-491 added Artifactory tool source target support (#80)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
afujiwara-roblox committed Sep 12, 2023
1 parent 3b14a12 commit 1a21963
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 6 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ As you may already have noticed, the tool name is located at the left side of `=

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.

### Hosts (Under Construction)
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.

```toml
[hosts]
# default hosts
# source = {source = "https://github.com", protocol = "github"}
# github = {source = "https://github.com", protocol = "github"}
# gitlab = {source = "https://gitlab.com", protocol = "gitlab"}
artifactory = {souce = "https://artifactory.com", protocol = "artifactory"}

[tools]
rotrieve = {artifactory = "tools/rotriever", version = "0.5.12"}
```

foreman currently only supports github, gitlab, and artifactory as protocols.

### System Tools
To start using Foreman to manage your system's default tools, create the file `~/.foreman/foreman.toml`.

Expand Down
214 changes: 208 additions & 6 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub struct ToolSpec {
pub enum Protocol {
Github,
Gitlab,
Artifactory,
}

impl ToolSpec {
Expand Down Expand Up @@ -96,13 +97,15 @@ impl ToolSpec {
match self.protocol {
Protocol::Github => CiString(format!("{}", self.path)),
Protocol::Gitlab => CiString(format!("gitlab@{}", self.path)),
Protocol::Artifactory => CiString(format!("{}@{}", self.host, self.path)),
}
}

pub fn source(&self) -> String {
let provider = match self.protocol {
Protocol::Github => "github.com",
Protocol::Gitlab => "gitlab.com",
Protocol::Artifactory => "artifactory.com",
};

format!("{}/{}", provider, self.path)
Expand All @@ -120,6 +123,7 @@ impl ToolSpec {
match self.protocol {
Protocol::Github => Provider::Github,
Protocol::Gitlab => Provider::Gitlab,
Protocol::Artifactory => Provider::Artifactory,
}
}
}
Expand All @@ -130,7 +134,7 @@ impl fmt::Display for ToolSpec {
}
}

#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub struct ConfigFile {
pub tools: BTreeMap<String, ToolSpec>,
pub hosts: HashMap<String, Host>,
Expand All @@ -149,6 +153,57 @@ impl Host {
protocol,
}
}

pub fn from_value(value: &Value) -> ConfigFileParseResult<Self> {
if let Value::Table(mut map) = value.clone() {
let source = map
.remove("source")
.ok_or_else(|| ConfigFileParseError::Host {
host: value.to_string(),
})?
.as_str()
.ok_or_else(|| ConfigFileParseError::Host {
host: value.to_string(),
})?
.to_string();

let protocol_value =
map.remove("protocol")
.ok_or_else(|| ConfigFileParseError::Host {
host: value.to_string(),
})?;

if !map.is_empty() {
return Err(ConfigFileParseError::Host {
host: value.to_string(),
});
}

let protocol_str =
protocol_value
.as_str()
.ok_or_else(|| ConfigFileParseError::Host {
host: value.to_string(),
})?;

let protocol = match protocol_str {
"github" => Protocol::Github,
"gitlab" => Protocol::Gitlab,
"artifactory" => Protocol::Artifactory,
_ => {
return Err(ConfigFileParseError::InvalidProtocol {
protocol: protocol_str.to_string(),
})
}
};

Ok(Self { source, protocol })
} else {
Err(ConfigFileParseError::Host {
host: value.to_string(),
})
}
}
}

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

if let Value::Table(top_level) = &value {
if let Some(hosts) = &top_level.get("hosts") {
if let Value::Table(hosts) = hosts {
for (host, toml) in hosts {
let host_source =
Host::from_value(&toml).map_err(|_| ConfigFileParseError::Tool {
tool: value.to_string(),
})?;
config.hosts.insert(host.to_owned(), host_source);
}
}
}

if let Some(tools) = &top_level.get("tools") {
if let Value::Table(tools) = tools {
for (tool, toml) in tools {
Expand Down Expand Up @@ -260,6 +327,7 @@ impl fmt::Display for ConfigFile {

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

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

fn new_artifactory<S: Into<String>>(host: S, path: S, version: VersionReq) -> ToolSpec {
ToolSpec {
host: host.into(),
path: path.into(),
version: version,
protocol: Protocol::Artifactory,
}
}

fn new_config(tools: BTreeMap<String, ToolSpec>, hosts: HashMap<String, Host>) -> ConfigFile {
let mut config = ConfigFile::new_with_defaults();
config.fill_from(ConfigFile { tools, hosts });
config
}

fn version(string: &str) -> VersionReq {
VersionReq::parse(string).unwrap()
}

fn new_host<S: Into<String>>(source: S, protocol: Protocol) -> Host {
Host {
source: source.into(),
protocol,
}
}

fn default_hosts() -> HashMap<String, Host> {
HashMap::from([
(
Expand All @@ -301,7 +391,17 @@ mod test {
])
}

fn artifactory_host() -> HashMap<String, Host> {
let mut hosts = default_hosts();
hosts.insert(
"artifactory".to_string(),
Host::new(ARTIFACTORY.to_string(), Protocol::Artifactory),
);
hosts
}

mod deserialization {

use super::*;

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

#[test]
fn artifactory_from_artifactory_field() {
let value: Value = toml::from_str(
&[
r#"artifactory = "generic-rbx-local-tools/rotriever/""#,
r#"version = "0.5.4""#,
]
.join("\n"),
)
.unwrap();

let artifactory = ToolSpec::from_value(&value, &artifactory_host()).unwrap();
assert_eq!(
artifactory,
new_artifactory(
"https://artifactory.com",
"generic-rbx-local-tools/rotriever/",
version("0.5.4")
)
);
}

#[test]
fn host_artifactory() {
let value: Value = toml::from_str(
&[
r#"source = "https://artifactory.com""#,
r#"protocol = "artifactory""#,
]
.join("\n"),
)
.unwrap();

let host = Host::from_value(&value).unwrap();
assert_eq!(
host,
new_host("https://artifactory.com", Protocol::Artifactory)
)
}

#[test]
fn extraneous_fields_tools() {
let value: Value = toml::from_str(
&[
r#"github = "Roblox/rotriever""#,
r#"path = "Roblox/rotriever""#,
r#"rbx_artifactory = "generic-rbx-local-tools/rotriever/""#,
r#"path = "generic-rbx-local-tools/rotriever/""#,
r#"version = "0.5.4""#,
]
.join("\n"),
)
.unwrap();

let artifactory = ToolSpec::from_value(&value, &default_hosts()).unwrap_err();
let artifactory = ToolSpec::from_value(&value, &artifactory_host()).unwrap_err();
assert_eq!(
artifactory,
ConfigFileParseError::Tool {
tool: [
r#"github = "Roblox/rotriever""#,
r#"path = "Roblox/rotriever""#,
r#"path = "generic-rbx-local-tools/rotriever/""#,
r#"rbx_artifactory = "generic-rbx-local-tools/rotriever/""#,
r#"version = "0.5.4""#,
r#""#,
]
Expand All @@ -360,6 +500,68 @@ mod test {
}
)
}

#[test]
fn extraneous_fields_host() {
let value: Value = toml::from_str(
&[
r#"source = "https://artifactory.com""#,
r#"protocol = "artifactory""#,
r#"extra = "field""#,
]
.join("\n"),
)
.unwrap();

let err = Host::from_value(&value).unwrap_err();
assert_eq!(
err,
ConfigFileParseError::Host {
host: [
r#"extra = "field""#,
r#"protocol = "artifactory""#,
r#"source = "https://artifactory.com""#,
r#""#,
]
.join("\n")
.to_string()
}
)
}
#[test]
fn config_file_with_hosts() {
let value: Value = toml::from_str(&[
r#"[hosts]"#,
r#"artifactory = {source = "https://artifactory.com", protocol = "artifactory"}"#,
r#""#,
r#"[tools]"#,
r#"tool = {artifactory = "path/to/tool", version = "1.0.0"}"#,
].join("\n"))
.unwrap();

let config = ConfigFile::from_value(value).unwrap();
assert_eq!(
config,
new_config(
BTreeMap::from([(
"tool".to_string(),
ToolSpec {
host: "https://artifactory.com".to_string(),
path: "path/to/tool".to_string(),
version: VersionReq::parse("1.0.0").unwrap(),
protocol: Protocol::Artifactory
}
)]),
HashMap::from([(
"artifactory".to_string(),
Host {
source: "https://artifactory.com".to_string(),
protocol: Protocol::Artifactory
}
)])
)
)
}
}

#[test]
Expand Down
14 changes: 14 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,17 @@ pub enum ForemanError {
ToolsNotDownloaded {
tools: Vec<String>,
},
Other {
message: String,
},
}

#[derive(Debug, PartialEq)]
pub enum ConfigFileParseError {
MissingField { field: String },
Tool { tool: String },
Host { host: String },
InvalidProtocol { protocol: String },
}

impl ForemanError {
Expand Down Expand Up @@ -314,6 +319,9 @@ impl fmt::Display for ForemanError {
Self::ToolsNotDownloaded { tools } => {
write!(f, "The following tools were not installed:\n{:#?}", tools)
}
Self::Other { message } => {
write!(f, "{}", message)
}
}
}
}
Expand All @@ -325,6 +333,12 @@ impl fmt::Display for ConfigFileParseError {
Self::Tool { tool } => {
write!(f, "data is not properly formatted for tool:\n\n{}", tool)
}
Self::Host { host } => {
write!(f, "data is not properly formatted for host:\n\n{}", host)
}
Self::InvalidProtocol { protocol } => {
write!(f, "protocol `{}` is not valid. Foreman only supports `github`, `gitlab`, and `artifactory`\n\n", protocol)
}
}
}
}
Expand Down
Loading

0 comments on commit 1a21963

Please sign in to comment.