Skip to content
This repository has been archived by the owner on Nov 13, 2022. It is now read-only.

Commit

Permalink
Add retry command.
Browse files Browse the repository at this point in the history
  • Loading branch information
brndnmtthws committed Mar 7, 2019
1 parent 29da598 commit 9e759f6
Show file tree
Hide file tree
Showing 26 changed files with 3,703 additions and 106 deletions.
712 changes: 712 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ git2 = "0.8"
hex = "0.3"
lazy_static = "1.3"
log = "0.4"
regex = "1"
reqwest = "0.9"
ring = "0.13"
rocket = "0.4"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
tempfile = "3.0"
toml = "0.4"
url = "1.7"
yansi = "0.5"

[dependencies.rocket_contrib]
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ RUN cd src \
&& rm -rf $HOME/.cargo/git

ENV ROCKET_ENV=production
ENV RUST_LOG=labhub=info

ENTRYPOINT [ "labhub" ]
21 changes: 16 additions & 5 deletions LabHub.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
# List of enabled features. At the moment there's only one, so this parameter
# does nothing. :)
# List of enabled features
features = [
"external-pr",
"external_pr",
"commands"
]

# Command settings
[commands]
# List of commands to enable
enabled_commands = [
"retry",
]

# Settings for GitHub
[github]
webhook_secret = "secret"
username = "ci-user"
ssh_key = "/path/to/labhub-key.ecdsa"
ssh_key = "/etc/ssh-keys/labhub-key.ecdsa"
api_token = "token"
hostname = "github.com"

# Settings for GitLab
[gitlab]
webhook_secret = "secret"
username = "ci-user"
ssh_key = "/path/to/labhub-key.ecdsa"
ssh_key = "/etc/ssh-keys/labhub-key.ecdsa"
api_token = "token"
hostname = "gitlab.com"

# List of mappings to/from GitHub & GitLab
[[mappings]]
Expand Down
42 changes: 33 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@

A GitHub bot written in Rust for using GitLab CI in OSS projects.

## ✔️ Features
## Features

- Listens for webhooks from GitHub
- Pushes new branches to GitLab for external (forked) PRs
- Pushes branches to GitLab from external (forked) PRs
- Accepts commands by way of PR comments
- Possibly more coming soon 👻

Not implemented:
### Commands

- No periodic reconciling of GitLab branches with open PRs: if a webhook is missed for any reason, the GitLab pipeline may not correctly reflect the PR state
Commands can be executed by commenting on a PR with your CI user's login.

- **`@labhub retry`**: retry a pipeline that has failed

## ⁉️ The Problem
## The Problem

GitLab has a great CI system, however it's not suitable for open source projects 😧 (at the time of writing) because it won't build external PRs by default. There are security concerns about the risk of exposing secrets in external builds, and GitLab errs on the side of caution by not building external PRs by default.

Expand All @@ -32,7 +35,7 @@ LabHub is currently being used by the following projects:

- [Conky](https://github.com/brndnmtthws/conky)

## 🖥 Compiling
## Compiling

LabHub requires Rust nightly. To compile using [`rustup`](https://rustup.rs/):

Expand All @@ -50,7 +53,7 @@ LabHub is configured using [`LabHub.toml`](LabHub.toml). For details, see [src/c

## 🚀 Deployment

### Setup Webhooks
### Setup Webhooks

You'll need to set up webhooks for any repo you wish to enable LabHub for. Currently, only GitHub webhooks are required. To get started, go to `github.com/<org>/<repo>/settings/hooks` and add a new webhook.

Expand All @@ -61,7 +64,7 @@ Configure the webhook to send PR and push events.
- Make sure the payload type is `application/json`.
- [Here's how your webhook should look](docs/github-webhook-config.png)

### 🔑 Create SSH keys
### Create SSH keys

You'll need a CI user with SSH keys for both GitHub and GitLab. Create an account on both sites (if you don't already have a CI user), and create an SSH key for LabHub:

Expand All @@ -71,7 +74,24 @@ $ ssh-keygen -f labhub-key.ecdsa -t ecdsa -b 521

Keep `labhub-key.ecdsa` safe, and upload `labhub-key.ecdsa.pub` to both GitHub and GitLab for the CI user.

### ☸️ Deploy to Kubernetes with Helm
### Create Personal Access Tokens

Create personal access tokens for your CI user on both GitHub, and GitLab. Supply these tokens by setting the `api_token` parameter in `LabHub.toml` for both GitHub and GitLab.

#### Personal Access Token for GitHub

- Go to https://github.com/settings/tokens
- Click "Generate new token"
- Give the token a name, and [enable the `repo` scope, like this](docs/github-personal-access-token.png).
- Save that token to your `LabHub.toml`

#### Personal Access Token for GitLab

- Go to https://gitlab.com/profile/personal_access_tokens
- Give the token a name, and [enable the `api` scope, like this](docs/gitlab-personal-access-token.png).
- Save that token to your `LabHub.toml`

### Deploy to Kubernetes with Helm

There's a Helm chart included in this repo, which is the preferred method of deployment. To use you, you must first create the SSH key secrets with kubectl. Assuming your SSH private key is `labhub-key.ecdsa`:

Expand All @@ -89,3 +109,7 @@ $ cp values.yaml myvalues.yaml
### Edit myvalues.yaml to your liking ###
$ helm upgrade --install labhub . -f myvalues.yaml
```

### Not implemented:

- No periodic reconciling of GitLab branches with open PRs: if a webhook is missed for any reason, the GitLab pipeline may not correctly reflect the PR state
2 changes: 1 addition & 1 deletion Rocket.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ limits = { forms = 32768 }
address = "0.0.0.0"
port = 8000
keep_alive = 5
log = "normal"
log = "critical"
limits = { forms = 32768 }
Binary file added docs/github-personal-access-token.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/gitlab-personal-access-token.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 16 additions & 3 deletions helm/labhub/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,35 @@ labhub_toml: |
# List of enabled features. At the moment there's only one, so this parameter
# does nothing. :)
features = [
"external-pr",
"external_pr",
"commands"
]
[commands]
enabled_commands = [
"retry",
]
# Settings for GitHub
[github]
webhook_secret = "secret"
username = "ci-user"
ssh_key = "/etc/ssh-keys/github"
ssh_key = "/etc/ssh-keys/labhub-key.ecdsa"
api_token = "token"
hostname = "github.com"
# Settings for GitLab
[gitlab]
webhook_secret = "secret"
username = "ci-user"
ssh_key = "/etc/ssh-keys/gitlab"
ssh_key = "/etc/ssh-keys/labhub-key.ecdsa"
api_token = "token"
hostname = "gitlab.com"
# List of mappings to/from GitHub & GitLab
[[mappings]]
github_repo = "brndnmtthws/labhub"
gitlab_repo = "brndnmtthws-oss/labhub"
[[mappings]]
github_repo = "brndnmtthws/conky"
gitlab_repo = "brndnmtthws-oss/conky"
72 changes: 72 additions & 0 deletions src/api/github_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use crate::api::models::github;
use crate::config;
use crate::errors::GitError;

use log::error;
use reqwest;

fn headers(token: &str) -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("token {}", token)).unwrap(),
);
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/vnd.github.v3+json"),
);
headers.insert(
reqwest::header::ACCEPT_ENCODING,
reqwest::header::HeaderValue::from_static("Accept-Encoding: deflate, gzip"),
);
headers
}

fn make_repo_url(org: &str, repo: &str) -> String {
let hostname = match config::CONFIG.github.hostname.as_ref() {
Some(hostname) => hostname.clone(),
_ => "github.com".to_string(),
};
format!("https://api.{}/repos/{}/{}", hostname, org, repo)
}

pub fn get_pull(
client: &reqwest::Client,
org: &str,
repo: &str,
number: i64,
) -> Result<github::RepoPr, GitError> {
let res: github::RepoPr = client
.get(&format!("{}/pulls/{}", make_repo_url(org, repo), number))
.headers(headers(&config::CONFIG.github.api_token))
.send()?
.json()?;
Ok(res)
}

pub fn create_issue_comment(
client: &reqwest::Client,
org: &str,
repo: &str,
number: i64,
body: &str,
) -> Result<(), GitError> {
let mut res = client
.post(&format!(
"{}/issues/{}/comments",
make_repo_url(org, repo),
number
))
.headers(headers(&config::CONFIG.github.api_token))
.body(serde_json::json!({"body":body.to_string()}).to_string())
.send()?;
let body = res.text()?;
match res.status() {
reqwest::StatusCode::CREATED => Ok(()),
_ => {
let msg = format!("Error creating issue comment: res={:#?} body={}", res, body);
error!("{}", msg);
Err(GitError { message: msg })
}
}
}
106 changes: 106 additions & 0 deletions src/api/gitlab_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use crate::api::models::gitlab;
use crate::config;
use crate::errors::GitError;

use log::error;
use reqwest;
use url::percent_encoding::{utf8_percent_encode, PATH_SEGMENT_ENCODE_SET};

fn headers(token: &str) -> reqwest::header::HeaderMap {
let token_header = reqwest::header::HeaderName::from_static("private-token");
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
token_header,
reqwest::header::HeaderValue::from_str(token).unwrap(),
);
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
);
headers.insert(
reqwest::header::ACCEPT_ENCODING,
reqwest::header::HeaderValue::from_static("Accept-Encoding: deflate, gzip"),
);
headers
}

fn make_api_url(project: &str) -> String {
let hostname = match config::CONFIG.gitlab.hostname.as_ref() {
Some(hostname) => hostname.clone(),
_ => "gitlab.com".to_string(),
};
let project = utf8_percent_encode(project, PATH_SEGMENT_ENCODE_SET).to_string();
format!("https://{}/api/v4/projects/{}", hostname, project)
}

pub fn make_ext_url(project: &str) -> String {
let hostname = match config::CONFIG.gitlab.hostname.as_ref() {
Some(hostname) => hostname.clone(),
_ => "gitlab.com".to_string(),
};
format!("https://{}/{}", hostname, project)
}

pub fn get_pipelines(
client: &reqwest::Client,
project: &str,
page: i64,
per_page: i64,
) -> Result<Vec<gitlab::Pipeline>, GitError> {
let res: Vec<gitlab::Pipeline> = client
.get(&format!(
"{}/pipelines?page={}&per_page={}",
make_api_url(project),
page,
per_page
))
.headers(headers(&config::CONFIG.gitlab.api_token))
.send()?
.json()?;
Ok(res)
}

pub fn retry_pipeline(
client: &reqwest::Client,
project: &str,
pipeline_id: i64,
) -> Result<(), GitError> {
let res = client
.post(&format!(
"{}/pipelines/{}/retry",
make_api_url(project),
pipeline_id
))
.headers(headers(&config::CONFIG.gitlab.api_token))
.send()?;

match res.status() {
reqwest::StatusCode::CREATED => Ok(()),
_ => {
let msg = format!("Error retrying pipeline: {:#?}", res);
error!("{}", msg);
Err(GitError { message: msg })
}
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_make_ext_url() {
assert_eq!(
make_ext_url("brndnmtthws-oss/conky"),
"https://gitlab.com/brndnmtthws-oss/conky"
);
}

#[test]
fn test_make_api_url() {
assert_eq!(
make_api_url("brndnmtthws-oss/conky"),
"https://gitlab.com/api/v4/projects/brndnmtthws-oss%2Fconky"
);
}
}
2 changes: 2 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod github_client;
pub mod github_proto;
pub mod github_signature;
pub mod gitlab_client;
pub mod models;
Loading

0 comments on commit 9e759f6

Please sign in to comment.