Skip to content

Commit

Permalink
feat: add jenkin support
Browse files Browse the repository at this point in the history
closes duckhq#21
  • Loading branch information
RichiCoder1 committed Feb 7, 2020
1 parent 81712cb commit 4816dd7
Show file tree
Hide file tree
Showing 8 changed files with 471 additions and 1 deletion.
139 changes: 139 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ regex = "1.3.3"
schemars = "0.7.0-alpha-1"
derive_builder = "0.9.0"
base64 = "0.11.0"
jenkins_api = "0.7.0"

[dev-dependencies]
test-case = "0.3.3"
Expand Down
1 change: 1 addition & 0 deletions src/builds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ pub enum BuildProvider {
AzureDevOps,
GitHub,
OctopusDeploy,
Jenkins
}

#[derive(Clone, Debug, PartialEq, Eq)]
Expand Down
34 changes: 34 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ impl Configuration {
CollectorConfiguration::Azure(c) => c.id.clone(),
CollectorConfiguration::OctopusDeploy(c) => c.id.clone(),
CollectorConfiguration::GitHub(c) => c.id.clone(),
CollectorConfiguration::Jenkins(c) => c.id.clone(),
})
.collect();
// Get all observer id:s
Expand Down Expand Up @@ -138,6 +139,10 @@ pub enum CollectorConfiguration {
/// Gets deployments from Octopus Deploy
#[serde(rename = "octopus")]
OctopusDeploy(OctopusDeployConfiguration),
/// # Jenkins collector
/// Gets pipeline status from Jenkins
#[serde(rename = "jenkins")]
Jenkins(JenkinsConfiguration),
}

#[derive(Serialize, Deserialize, JsonSchema, Clone)]
Expand Down Expand Up @@ -234,6 +239,35 @@ pub enum GitHubCredentials {
},
}

#[derive(Serialize, Deserialize, JsonSchema, Clone)]
pub struct JenkinsConfiguration {
/// # The Jenkins collector ID
pub id: String,
/// # Determines whether or not this collector is enabled
#[serde(default)]
pub enabled: Option<bool>,
/// # The Jenkins Server URL
#[serde(rename = "serverUrl")]
pub server_url: String,
/// # The Jenkins jobs to include
pub jobs: Vec<String>,
/// # The Jenkins User Credentials
pub credentials: JenkinsCredentials,
}

#[derive(Serialize, Deserialize, JsonSchema, Clone)]
pub enum JenkinsCredentials {
/// # Basic authentication
/// Authenticate using basic authentication
#[serde(rename = "basic")]
Basic {
/// # The username to use
username: String,
/// # The password to use
password: String,
},
}

#[derive(Serialize, Deserialize, JsonSchema, Clone)]
pub struct OctopusDeployConfiguration {
/// # The Octopus Deploy collector ID
Expand Down
9 changes: 9 additions & 0 deletions src/config/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ fn validate_collector_references(configuration: &Configuration) -> DuckResult<()
},
);
}
CollectorConfiguration::Jenkins(c) => {
collectors.insert(
c.id.clone(),
match c.enabled {
None => true,
Some(enabled) => enabled,
},
);
}
}
}

Expand Down
119 changes: 119 additions & 0 deletions src/providers/collectors/jenkins/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use std::sync::Arc;

use log::warn;
use waithandle::{EventWaitHandle, WaitHandle};

use jenkins_api::{JenkinsBuilder, Jenkins};
use jenkins_api::build::{BuildStatus as JobBuildStatus, CommonBuild};

use crate::builds::{Build, BuildBuilder, BuildProvider, BuildStatus};
use crate::config::{JenkinsConfiguration, JenkinsCredentials};
use crate::providers::collectors::{Collector, CollectorInfo};
use crate::utils::{DuckResult};

mod validation;

pub struct JenkinsCollector {
client: Jenkins,
jobs: Vec<String>,
info: CollectorInfo
}

impl JenkinsCollector {
pub fn new(config: &JenkinsConfiguration) -> Self {
let mut builder = JenkinsBuilder::new(&config.server_url);
let JenkinsCredentials::Basic { username, password } = &config.credentials;
builder = builder.with_user(&username, Some(&password));

let jenkins = builder.build().expect("Failed to create a jenkins client");

return Self {
client: jenkins,
jobs: config.jobs.clone(),
info: CollectorInfo {
id: config.id.clone(),
enabled: match config.enabled {
Option::None => true,
Option::Some(enabled) => enabled,
},
provider: BuildProvider::Jenkins
}
}
}
}

fn get_build_status(build: &CommonBuild) -> BuildStatus {
if build.building {
return BuildStatus::Running;
}

match build.result {
Some(status) => {
match status {
JobBuildStatus::Success => BuildStatus::Success,
JobBuildStatus::Aborted => BuildStatus::Canceled,
JobBuildStatus::Failure => BuildStatus::Failed,
JobBuildStatus::NotBuilt => BuildStatus::Queued,
_ => BuildStatus::Unknown,
}
},
None => BuildStatus::Unknown
}
}

impl Collector for JenkinsCollector {
fn info(&self) -> &CollectorInfo {
&self.info
}

fn collect(&self, handle: Arc<EventWaitHandle>, callback: &mut dyn FnMut(Build)) -> DuckResult<()> {

for job_name in self.jobs.iter() {
let job = match self.client.get_job(job_name) {
Ok(j) => j,
Err(_) => {
warn!("Job '{}' couldn't be requested.", job_name);
continue;
}
};

let build = match job.last_build {
Some(b) => b,
None => {
warn!("The Job '{}' hasn't been built yet.", job_name);
continue;
}
};

let build = match build.get_full_build(&self.client) {
Ok(b) => b,
Err(_) => {
warn!("Failed to be full build info for build {} of '{}'", build.number, job_name);
continue;
}
};

callback(
BuildBuilder::new()
.build_id(&build.id)
.provider(BuildProvider::Jenkins)
.collector(&self.info.id)
.definition_id(job_name)
.definition_name(&job.display_name)
.build_number(format!("{}", &build.number))
.status(get_build_status(&build))
.url(&build.url)
.build()
.unwrap(),
);

// Wait for a little time between calls.
if handle.wait(std::time::Duration::from_millis(300)).unwrap() {
return Ok(());
}

}

Ok(())
}
}
151 changes: 151 additions & 0 deletions src/providers/collectors/jenkins/validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use crate::config::{JenkinsConfiguration, JenkinsCredentials, Validate};
use crate::utils::DuckResult;

impl Validate for JenkinsConfiguration {
fn validate(&self) -> DuckResult<()> {
if self.server_url.is_empty() {
return Err(format_err!("Server URL is empty."));
}

match &self.credentials {
JenkinsCredentials::Basic { username, password } => {
if username.is_empty() {
return Err(format_err!("Jenkins username is empty."));
}
if password.is_empty() {
return Err(format_err!("Jenkins password is empty."));
}
}
};

Ok(())
}
}

#[cfg(test)]
mod tests {
use crate::config::*;
use crate::providers::DuckProviderCollection;
use crate::utils::text::TestVariableProvider;

#[test]
#[should_panic(expected = "The id \\'\\' is invalid.")]
fn should_return_error_if_jenkins_id_is_empty() {
let config = Configuration::from_json(
&TestVariableProvider::new(),
r#"
{
"collectors": [
{
"jenkins": {
"id": "",
"serverUrl": "http://jenkins:8080",
"credentials": {
"basic": {
"username": "patrik",
"password": "hunter1!"
}
}
}
}
]
}
"#,
)
.unwrap();

let collection = DuckProviderCollection::new();
collection.get_collectors(&config).unwrap();
}
#[test]
#[should_panic(expected = "Jenkins server url is empty.")]
fn should_return_error_if_jenkins_repository_is_empty() {
let config = Configuration::from_json(
&TestVariableProvider::new(),
r#"
{
"collectors": [
{
"jenkins": {
"id": "duck_pipelines",
"serverUrl": "",
"credentials": {
"basic": {
"username": "patrik",
"password": "hunter1!"
}
}
}
}
]
}
"#,
)
.unwrap();

let collection = DuckProviderCollection::new();
collection.get_collectors(&config).unwrap();
}

#[test]
#[should_panic(expected = "Jenkins username is empty.")]
fn should_return_error_if_jenkins_username_is_empty() {
let config = Configuration::from_json(
&TestVariableProvider::new(),
r#"
{
"collectors": [
{
"jenkins": {
"id": "duck_pipelines",
"serverUrl": "http://jenkins:8080",
"credentials":
{
"basic": {
"username": "",
"password": "hunter1!"
}
}
}
}
]
}
"#,
)
.unwrap();

let collection = DuckProviderCollection::new();
collection.get_collectors(&config).unwrap();
}

#[test]
#[should_panic(expected = "Jenkins password is empty.")]
fn should_return_error_if_jenkins_password_is_empty() {
let config = Configuration::from_json(
&TestVariableProvider::new(),
r#"
{
"collectors": [
{
"jenkins": {
"id": "duck_pipelines",
"serverUrl": "http://jenkins:8080",
"credentials":
{
"basic": {
"username": "patriksvensson",
"password": ""
}
}
}
}
]
}
"#,
)
.unwrap();

let collection = DuckProviderCollection::new();
collection.get_collectors(&config).unwrap();
}
}
18 changes: 17 additions & 1 deletion src/providers/collectors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ use crate::utils::DuckResult;

use self::azure::AzureDevOpsCollector;
use self::github::GitHubCollector;
use self::octopus::OctopusDeployCollector;
use self::teamcity::TeamCityCollector;
use self::octopus::OctopusDeployCollector;
use self::jenkins::JenkinsCollector;

use super::DuckProvider;

mod azure;
mod github;
mod jenkins;
mod octopus;
mod teamcity;

Expand Down Expand Up @@ -76,6 +78,20 @@ impl<'a> DuckProvider<'a> for GitHubProvider {
}
}

pub struct JenkinsProvider {}
impl<'a> DuckProvider<'a> for JenkinsProvider {
fn get_collectors(&self, config: &Configuration) -> DuckResult<Vec<Box<dyn Collector>>> {
let mut result = Vec::<Box<dyn Collector>>::new();
for item in config.collectors.iter() {
if let CollectorConfiguration::Jenkins(c) = item {
c.validate()?;
result.push(Box::new(JenkinsCollector::new(&c)));
}
}
return Ok(result);
}
}

pub struct OctopusDeployProvider {}
impl<'a> DuckProvider<'a> for OctopusDeployProvider {
fn get_collectors(&self, config: &Configuration) -> DuckResult<Vec<Box<dyn Collector>>> {
Expand Down

0 comments on commit 4816dd7

Please sign in to comment.