Selaa lähdekoodia

Merge pull request #6 from rust-lang/dockerize

Dockerize triagebot and add CI
Mark Rousskov 6 vuotta sitten
vanhempi
commit
545eda57aa
15 muutettua tiedostoa jossa 152 lisäystä ja 31 poistoa
  1. 1 0
      .dockerignore
  2. 15 0
      .travis.yml
  3. 55 0
      Dockerfile
  4. 18 0
      ci/check-nightly-version.sh
  5. 9 0
      ci/publish-docker.sh
  6. 12 0
      ci/run.sh
  7. 3 2
      parser/src/command.rs
  8. 1 1
      parser/src/token.rs
  9. 1 0
      rust-toolchain
  10. 15 17
      src/github.rs
  11. 3 1
      src/handlers.rs
  12. 3 1
      src/handlers/label.rs
  13. 1 1
      src/interactions.rs
  14. 7 5
      src/main.rs
  15. 8 3
      src/payload.rs

+ 1 - 0
.dockerignore

@@ -0,0 +1 @@
+/target

+ 15 - 0
.travis.yml

@@ -0,0 +1,15 @@
+language: rust
+rust: nightly-2019-03-15
+cache:
+  - cargo
+
+script: ci/run.sh
+
+# Publish the Docker image to the infra AWS registry
+before_deploy:
+  - pip install --user awscli
+deploy:
+  provider: script
+  script: sh ci/publish-docker.sh
+  on:
+    branch: master

+ 55 - 0
Dockerfile

@@ -0,0 +1,55 @@
+# This Dockerfile is composed of two steps: the first one builds the release
+# binary, and then the binary is copied inside another, empty image.
+
+#################
+#  Build image  #
+#################
+
+FROM ubuntu:bionic AS build
+
+RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
+    ca-certificates \
+    curl \
+    build-essential \
+    pkg-config \
+    libssl-dev
+
+# Install the currently pinned toolchain with rustup
+COPY rust-toolchain /tmp/rust-toolchain
+RUN curl https://static.rust-lang.org/rustup/dist/x86_64-unknown-linux-gnu/rustup-init >/tmp/rustup-init && \
+    chmod +x /tmp/rustup-init && \
+    /tmp/rustup-init -y --no-modify-path --default-toolchain $(cat /tmp/rust-toolchain)
+ENV PATH=/root/.cargo/bin:$PATH
+
+# Build the dependencies in a separate step to avoid rebuilding all of them
+# every time the source code changes. This takes advantage of Docker's layer
+# caching, and it works by copying the Cargo.{toml,lock} with dummy source code
+# and doing a full build with it.
+WORKDIR /tmp/source
+COPY Cargo.lock Cargo.toml /tmp/source/
+COPY parser/Cargo.toml /tmp/source/parser/Cargo.toml
+RUN mkdir -p /tmp/source/src /tmp/source/parser/src && \
+    echo "fn main() {}" > /tmp/source/src/main.rs && \
+    touch /tmp/source/parser/src/lib.rs
+RUN cargo fetch
+RUN cargo build --release
+
+# Dependencies are now cached, copy the actual source code and do another full
+# build. The touch on all the .rs files is needed, otherwise cargo assumes the
+# source code didn't change thanks to mtime weirdness.
+RUN rm -rf /tmp/source/src /tmp/source/parser/src
+COPY src /tmp/source/src
+COPY parser/src /tmp/source/parser/src
+RUN find -name "*.rs" -exec touch {} \; && cargo build --release
+
+##################
+#  Output image  #
+##################
+
+FROM ubuntu:bionic AS binary
+
+RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
+    ca-certificates
+
+COPY --from=build /tmp/source/target/release/triagebot /usr/local/bin/
+CMD ["triagebot"]

+ 18 - 0
ci/check-nightly-version.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+set -euo pipefail
+IFS=$'\n\t'
+
+# Extract the nightly version from all the places we declared it in
+rust_toolchain="$(cat rust-toolchain)"
+travis_yml="$(cat .travis.yml | grep '^rust: ' | head -n 1 | sed -E 's/^rust: (nightly-[0-9]{4}-[0-9]{2}-[0-9]{2})$/\1/')"
+
+# Ensure all the configuration files points to the same nightly
+check() {
+    if [[ "$2" != "${rust_toolchain}" ]]; then
+        echo "error: the nightly version in $1 is different than the one in rust-toolchain!" 1>&2
+        exit 1
+    fi
+}
+check .travis.yml "${travis_yml}"
+
+echo "all the configuration files points to the same nightly!"

+ 9 - 0
ci/publish-docker.sh

@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+
+ECR_IMAGE="890664054962.dkr.ecr.us-west-1.amazonaws.com/rust-triagebot:latest"
+
+$(aws ecr get-login --no-include-email --region us-west-1)
+
+docker tag rust-triagebot:latest "${ECR_IMAGE}"
+docker push "${ECR_IMAGE}"

+ 12 - 0
ci/run.sh

@@ -0,0 +1,12 @@
+#!/bin/bash
+set -euo pipefail
+IFS=$'\n\t'
+
+rustup component add rustfmt clippy
+
+ci/check-nightly-version.sh
+cargo test
+cargo fmt -- --check
+cargo clippy -- -Dwarnings
+
+docker build -t rust-triagebot .

+ 3 - 2
parser/src/command.rs

@@ -28,7 +28,7 @@ impl<'a> Input<'a> {
             all: input,
             parsed: 0,
             code: ColorCodeBlocks::new(input),
-            bot: bot,
+            bot,
         }
     }
 
@@ -70,9 +70,10 @@ impl<'a> Input<'a> {
             );
         }
 
-        if let Some(_) = self
+        if self
             .code
             .overlaps_code((self.parsed)..(self.parsed + tok.position()))
+            .is_some()
         {
             return Command::None;
         }

+ 1 - 1
parser/src/token.rs

@@ -72,7 +72,7 @@ impl<'a> Error<'a> {
 impl<'a> Tokenizer<'a> {
     pub fn new(input: &'a str) -> Tokenizer<'a> {
         Tokenizer {
-            input: input,
+            input,
             chars: input.char_indices().peekable(),
         }
     }

+ 1 - 0
rust-toolchain

@@ -0,0 +1 @@
+nightly-2019-03-15

+ 15 - 17
src/github.rs

@@ -1,7 +1,6 @@
 use failure::{Error, ResultExt};
-use reqwest::header::USER_AGENT;
-use reqwest::Error as HttpError;
-use reqwest::{Client, RequestBuilder, Response};
+use reqwest::header::{AUTHORIZATION, USER_AGENT};
+use reqwest::{Client, Error as HttpError, RequestBuilder, Response};
 
 #[derive(Debug, serde::Deserialize)]
 pub struct User {
@@ -9,13 +8,20 @@ pub struct User {
 }
 
 impl User {
+    pub fn current(client: &GithubClient) -> Result<Self, Error> {
+        Ok(client
+            .get("https://api.github.com/user")
+            .send_req()?
+            .json()?)
+    }
+
     pub fn is_team_member(&self, client: &GithubClient) -> Result<bool, Error> {
         let client = client.raw();
         let url = format!("{}/teams.json", rust_team_data::v1::BASE_URL);
         let permission: rust_team_data::v1::Teams = client
             .get(&url)
             .send()
-            .and_then(|r| r.error_for_status())
+            .and_then(Response::error_for_status)
             .and_then(|mut r| r.json())
             .context("could not get team data")?;
         let map = permission.teams;
@@ -30,6 +36,7 @@ pub struct Label {
 
 impl Label {
     fn exists(&self, repo_api_prefix: &str, client: &GithubClient) -> bool {
+        #[allow(clippy::redundant_pattern_matching)]
         match client
             .get(&format!("{}/labels/{}", repo_api_prefix, self.name))
             .send_req()
@@ -68,7 +75,7 @@ impl Issue {
         }
         client
             .post(&self.comments_url)
-            .json(&PostComment { body: body })
+            .json(&PostComment { body })
             .send_req()
             .context("failed to post comment")?;
         Ok(())
@@ -150,7 +157,7 @@ trait RequestSend: Sized {
 impl RequestSend for RequestBuilder {
     fn configure(self, g: &GithubClient) -> RequestBuilder {
         self.header(USER_AGENT, "rust-lang-triagebot")
-            .basic_auth(&g.username, Some(&g.token))
+            .header(AUTHORIZATION, format!("token {}", g.token))
     }
 
     fn send_req(self) -> Result<Response, HttpError> {
@@ -163,28 +170,19 @@ impl RequestSend for RequestBuilder {
 
 #[derive(Clone)]
 pub struct GithubClient {
-    username: String,
     token: String,
     client: Client,
 }
 
 impl GithubClient {
-    pub fn new(c: Client, token: String, username: String) -> Self {
-        GithubClient {
-            client: c,
-            token,
-            username,
-        }
+    pub fn new(client: Client, token: String) -> Self {
+        GithubClient { client, token }
     }
 
     pub fn raw(&self) -> &Client {
         &self.client
     }
 
-    pub fn username(&self) -> &str {
-        self.username.as_str()
-    }
-
     fn get(&self, url: &str) -> RequestBuilder {
         log::trace!("get {:?}", url);
         self.client.get(url).configure(self)

+ 3 - 1
src/handlers.rs

@@ -1,13 +1,15 @@
 use crate::github::GithubClient;
 use crate::registry::HandleRegistry;
+use std::sync::Arc;
 
 //mod assign;
 mod label;
 //mod tracking_issue;
 
-pub fn register_all(registry: &mut HandleRegistry, client: GithubClient) {
+pub fn register_all(registry: &mut HandleRegistry, client: GithubClient, username: Arc<String>) {
     registry.register(label::LabelHandler {
         client: client.clone(),
+        username: username.clone(),
     });
     //registry.register(assign::AssignmentHandler {
     //    client: client.clone(),

+ 3 - 1
src/handlers/label.rs

@@ -16,9 +16,11 @@ use crate::{
 use failure::Error;
 use parser::command::label::{LabelCommand, LabelDelta};
 use parser::command::{Command, Input};
+use std::sync::Arc;
 
 pub struct LabelHandler {
     pub client: GithubClient,
+    pub username: Arc<String>,
 }
 
 impl Handler for LabelHandler {
@@ -33,7 +35,7 @@ impl Handler for LabelHandler {
 
         let mut issue_labels = event.issue.labels().to_owned();
 
-        let mut input = Input::new(&event.comment.body, self.client.username());
+        let mut input = Input::new(&event.comment.body, &self.username);
         let deltas = match input.parse_command() {
             Command::Label(Ok(LabelCommand(deltas))) => deltas,
             Command::Label(Err(err)) => {

+ 1 - 1
src/interactions.rs

@@ -21,7 +21,7 @@ impl<'a> ErrorComment<'a> {
     pub fn post(&self, client: &GithubClient) -> Result<(), Error> {
         let mut body = String::new();
         writeln!(body, "**Error**: {}", self.message)?;
-        writeln!(body, "")?;
+        writeln!(body)?;
         writeln!(
             body,
             "Please let **`@rust-lang/release`** know if you're having trouble with this bot."

+ 7 - 5
src/main.rs

@@ -1,4 +1,5 @@
 #![feature(proc_macro_hygiene, decl_macro)]
+#![allow(clippy::new_without_default)]
 
 #[macro_use]
 extern crate rocket;
@@ -9,6 +10,7 @@ use rocket::request;
 use rocket::State;
 use rocket::{http::Status, Outcome, Request};
 use std::env;
+use std::sync::Arc;
 
 mod handlers;
 mod registry;
@@ -18,7 +20,7 @@ mod interactions;
 mod payload;
 mod team;
 
-use github::{Comment, GithubClient, Issue};
+use github::{Comment, GithubClient, Issue, User};
 use payload::SignedPayload;
 use registry::HandleRegistry;
 
@@ -48,7 +50,7 @@ impl<'a, 'r> request::FromRequest<'a, 'r> for Event {
         let ev = if let Some(ev) = req.headers().get_one("X-GitHub-Event") {
             ev
         } else {
-            return Outcome::Failure((Status::BadRequest, format!("Needs a X-GitHub-Event")));
+            return Outcome::Failure((Status::BadRequest, "Needs a X-GitHub-Event".into()));
         };
         let ev = match ev {
             "issue_comment" => Event::IssueComment,
@@ -110,11 +112,11 @@ fn main() {
     let client = Client::new();
     let gh = GithubClient::new(
         client.clone(),
-        env::var("GITHUB_API_TOKEN").unwrap(),
-        env::var("GITHUB_USERNAME").unwrap(),
+        env::var("GITHUB_API_TOKEN").expect("Missing GITHUB_API_TOKEN"),
     );
+    let username = Arc::new(User::current(&gh).unwrap().login);
     let mut registry = HandleRegistry::new();
-    handlers::register_all(&mut registry, gh.clone());
+    handlers::register_all(&mut registry, gh.clone(), username);
     rocket::ignite()
         .manage(gh)
         .manage(registry)

+ 8 - 3
src/payload.rs

@@ -19,7 +19,7 @@ impl FromDataSimple for SignedPayload {
             None => {
                 return Outcome::Failure((
                     Status::Unauthorized,
-                    format!("Unauthorized, no signature"),
+                    "Unauthorized, no signature".into(),
                 ));
             }
         };
@@ -46,13 +46,18 @@ impl FromDataSimple for SignedPayload {
             ));
         }
 
-        let key = PKey::hmac(env::var("GITHUB_WEBHOOK_SECRET").unwrap().as_bytes()).unwrap();
+        let key = PKey::hmac(
+            env::var("GITHUB_WEBHOOK_SECRET")
+                .expect("Missing GITHUB_WEBHOOK_SECRET")
+                .as_bytes(),
+        )
+        .unwrap();
         let mut signer = Signer::new(MessageDigest::sha1(), &key).unwrap();
         signer.update(&buf).unwrap();
         let hmac = signer.sign_to_vec().unwrap();
 
         if !memcmp::eq(&hmac, &signature) {
-            return Outcome::Failure((Status::Unauthorized, format!("HMAC not correct")));
+            return Outcome::Failure((Status::Unauthorized, "HMAC not correct".into()));
         }
 
         Outcome::Success(SignedPayload(buf))