Skip to content

Commit

Permalink
feat(contrib): add badge uservice (#4335)
Browse files Browse the repository at this point in the history
close #546
Signed-off-by: Benjamin Coenen <[email protected]>
  • Loading branch information
bnjjj authored Jun 6, 2019
1 parent 22abbb9 commit 22ac845
Show file tree
Hide file tree
Showing 24 changed files with 1,172 additions and 0 deletions.
13 changes: 13 additions & 0 deletions contrib/uservices/badge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/target
**/*.rs.bk
Cargo.lock


## IntelliJ
*.iml
.idea

test.db

.env
config.toml
37 changes: 37 additions & 0 deletions contrib/uservices/badge/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "badge-cds-service"
version = "0.1.0"
authors = ["Benjamin Coenen <[email protected]>"]

[dependencies]
# Error
failure = "0.1.3"
failure_derive = "0.1.2"
# Json
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0.0"
# Actor
actix = "0.7"
actix-web = { version = "0.7", features = ["alpn"] }
tokio = "0.1.11"
# Log
log = "0.4"
env_logger = "0.5.13"
# Database code
uuid = { version = "0.5", features = ["serde", "v4"] }
diesel = { version = "1.4.2", features = ["postgres", "r2d2"] }
diesel_migrations= "1.1.0"
r2d2 = "0.8"
badge = "0.2.0"
futures = "0.1.26"
bytes = "0.4.12"
dotenv = "0.13.0"
config = "0.9"
chrono = { version = "0.4.6", features = ["serde"] }
sdk-cds = "0.3.0"
rdkafka = { version = "0.21.0", features = ["ssl", "sasl"] }
rdkafka-sys = "1.0.0"
clap = "2.33.0"
url = "1.7.2"
url_serde = "0.2.0"
35 changes: 35 additions & 0 deletions contrib/uservices/badge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Badge CDS Service [Under active development]

µService which generate badge for CDS workflow.

## Prerequisites

+ PostgreSQL
+ Kafka (using the SASL authentication. Or nothing for sandboxing)
+ [Diesel cli](https://github.com/diesel-rs/diesel/tree/master/diesel_cli)

## Usage

There are 2 different modes.

+ `kafka` : the µService will simply listen to the kafka topic of CDS events.
+ `web` : the µService act like a CDS service and register to the API. It needs authentified HTTP API calls to save states of workflows.

```bash
$ export DATABASE_URL=postgres://username:password@hostname/table_name # Only useful for diesel cli
$ diesel migration run # Initialize database and make migrations
$ ./badge-cds-service config new # You have to edit the config.toml file generated to correspond with your configuration before next command
$ ./badge-cds-service start # You can indicate a -f path_to_my_conf_file.toml
```

## TODO

- [ ] add some tests

## Development

Use cargo to compile.

```bash
$ cargo build # or cargo run
```
5 changes: 5 additions & 0 deletions contrib/uservices/badge/diesel.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli

[print_schema]
file = "src/schema.rs"
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.

DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.




-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE "run";
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Your SQL goes here
CREATE TABLE IF NOT EXISTS "run" (
id BIGSERIAL PRIMARY KEY,
run_id BIGINT NOT NULL,
num BIGINT NOT NULL,
project_key TEXT NOT NULL,
workflow_name TEXT NOT NULL,
branch TEXT DEFAULT '',
status TEXT NOT NULL,
updated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp
);
71 changes: 71 additions & 0 deletions contrib/uservices/badge/src/badge/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use actix_web::error;
use actix_web::http::HeaderMap;
use actix_web::{AsyncResponder, FutureResponse, HttpRequest, HttpResponse};
use badge_gen::{Badge, BadgeOptions};
use futures::Future;

use crate::models::StatusEnum;
use crate::run::QueryRun;
use crate::web::WebState;

const GREEN: &str = "#21BA45";
const RED: &str = "#FF4F60";
const BLUE: &str = "#4fa3e3";

pub fn badge_handler(req: &HttpRequest<WebState>) -> FutureResponse<HttpResponse> {
let project_key = req.match_info().get("project").unwrap_or_default();
let workflow_name = req.match_info().get("workflow").unwrap_or_default();
let query_params = req.query();
let branch = query_params
.get("branch")
.map(std::string::ToString::to_string)
.or_else(|| get_branch_from_referer(req.headers())); // fetch branch from referer

req.state()
.db
.send(QueryRun {
project_key: project_key.to_string(),
workflow_name: workflow_name.to_string(),
branch,
})
.from_err()
.and_then(|res| {
let run = res?;
let color = match StatusEnum::from(run.status.clone()) {
StatusEnum::Success => GREEN.to_string(),
StatusEnum::Building | StatusEnum::Waiting | StatusEnum::Checking => {
String::from(BLUE)
}
StatusEnum::Fail | StatusEnum::Stopped => RED.to_string(),
_ => "grey".to_string(),
};

let opts = BadgeOptions {
subject: String::from("CDS"),
status: run.status,
color,
};
let badge = Badge::new(opts).map_err(error::ErrorBadRequest)?.to_svg();

Ok(HttpResponse::Ok().content_type("image/svg+xml").body(badge))
})
.responder()
}

fn get_branch_from_referer(headers: &HeaderMap) -> Option<String> {
let mut branch = None;
if let Some(ref referer_value) = headers.get("Referer") {
let referer_value_str = referer_value.to_str().unwrap();
if let Some(tree_index) = referer_value_str.find("/tree/") {
branch = Some(referer_value_str[tree_index + 6..].to_string());
} else if let Some(src_index) = referer_value_str.find("/src/") {
branch = Some(
referer_value_str[src_index + 5..]
.trim_end_matches('/')
.to_string(),
);
}
}

branch
}
1 change: 1 addition & 0 deletions contrib/uservices/badge/src/badge/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod handlers;
141 changes: 141 additions & 0 deletions contrib/uservices/badge/src/configuration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@

use config::{Config, ConfigError, Environment, File};
use sdk_cds::service::APIConfiguration;
use std::str::FromStr;
#[derive(Default, Debug, Deserialize, Clone)]
#[serde(default)]
pub struct Configuration {
pub badge: BadgeConfiguration,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(default)]
pub struct BadgeConfiguration {
#[serde(with = "url_serde")]
pub url: url::Url,
pub name: String,
pub mode: String,
pub api: APIConfiguration,
pub database: DatabaseConfiguration,
pub kafka: KafkaConfiguration,
pub http: HTTPConfiguration,
}

impl std::default::Default for BadgeConfiguration {
fn default() -> Self {
BadgeConfiguration {
url: url::Url::from_str("http://localhost:8086").unwrap(),
name: String::default(),
mode: String::from("kafka"),
api: APIConfiguration::default(),
database: DatabaseConfiguration::default(),
kafka: KafkaConfiguration::default(),
http: HTTPConfiguration::default(),
}
}
}

#[derive(Default, Debug, Deserialize, Serialize, Clone)]
#[serde(default)]
pub struct DatabaseConfiguration {
pub user: String,
pub password: String,
pub name: String,
pub host: String,
pub port: i32,
pub sslmode: String,
pub maxconn: i32,
pub timeout: i32,
}

#[derive(Default, Debug, Deserialize, Serialize, Clone)]
#[serde(default)]
pub struct KafkaConfiguration {
pub group: String,
pub user: String,
pub password: String,
pub broker: String,
pub topic: String,
}

#[derive(Default, Debug, Deserialize, Serialize, Clone)]
pub struct HTTPConfiguration {
#[serde(default = "default_addr")]
pub addr: String,
#[serde(default)]
pub port: i32,
}

fn default_addr() -> String {
"0.0.0.0".to_string()
}

pub fn get_configuration(filename: &str) -> Result<BadgeConfiguration, ConfigError> {
let mut settings = Config::default();
settings
.merge(File::with_name(filename))?
.merge(Environment::with_prefix("CDS"))?;

let conf: Configuration = settings.try_into()?;
Ok(conf.badge)
}

pub fn get_example_config_file() -> &'static str {
r#"#############################
# CDS Badge Service Settings
#############################
[badge]
url = "http://localhost:8086"
# Name of this CDS badge Service
name = "cds-badge"
mode = "kafka"
######################
# CDS API Settings
#######################
[badge.api]
maxHeartbeatFailures = 10
requestTimeout = 10
token = "USECDSAPITOKEN"
[badge.api.grpc]
insecure = true
url = "http://localhost:8082"
[badge.api.http]
insecure = true
url = "http://localhost:8081"
######################
# CDS Badge Database Settings (postgresql)
#######################
[badge.database]
user = ""
password = ""
name = ""
host = "localhost"
port = 5432
maxconn = 20
timeout = 3000
######################
# CDS Badge Kafka Settings (kafka mode only)
#######################
[badge.kafka]
broker = "localhost:9092"
password = ""
topic = "cds"
user = ""
group = "" # optional
######################
# CDS Badge HTTP Configuration
#######################
[badge.http]
# Listen address without port, example: 127.0.0.1
# addr = ""
port = 8088"#
}
9 changes: 9 additions & 0 deletions contrib/uservices/badge/src/database.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use actix::{Actor, SyncContext};
use diesel::prelude::PgConnection;
use diesel::r2d2::{ConnectionManager, Pool};

pub struct DbExecutor(pub Pool<ConnectionManager<PgConnection>>);

impl Actor for DbExecutor {
type Context = SyncContext<Self>;
}
Loading

0 comments on commit 22ac845

Please sign in to comment.