diff --git a/Cargo.lock b/Cargo.lock index bc5ae50..f6f2120 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2149,6 +2149,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe" + [[package]] name = "httparse" version = "1.8.0" @@ -4219,8 +4225,15 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", + "http-range-header", + "httpdate", "iri-string", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", diff --git a/frontend/.gitignore b/frontend/.gitignore index 11f5d71..4e5a078 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -20,3 +20,7 @@ pnpm-debug.log* *.njsproj *.sln *.sw? + +# auto-generated via `yarn dev` +components.d.ts +typed-router.d.ts \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index aa6f0fd..43289d4 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -19,7 +19,7 @@ ] } }, - "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "./typed-router.d.ts"], "references": [{ "path": "./tsconfig.node.json" }], "exclude": ["node_modules"] } diff --git a/server/Cargo.toml b/server/Cargo.toml index 1f0b0e0..13a5877 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -32,5 +32,5 @@ deadpool-lapin = "0.11.0" axum = "0.7.4" tracing-subscriber = "0.3.18" tracing = "0.1.40" -tower-http = { version = "0.5.2", features = ["trace"] } +tower-http = { version = "0.5.2", features = ["trace", "fs"] } diesel = { version = "2.1.4", features = ["postgres", "chrono", "r2d2"] } diff --git a/server/src/api.rs b/server/src/api.rs new file mode 100644 index 0000000..7e17c46 --- /dev/null +++ b/server/src/api.rs @@ -0,0 +1,78 @@ +use crate::{ + models::{NewJob, NewPipeline, Pipeline}, + ALL_ARCH, ARGS, +}; +use anyhow::Context; +use buildit_utils::github::update_abbs; +use diesel::{ + r2d2::{ConnectionManager, Pool}, + PgConnection, RunQueryDsl, SelectableHelper, +}; + +pub async fn pipeline_new( + pool: Pool>, + git_branch: &str, + packages: &str, + archs: &str, +) -> anyhow::Result { + // resolve branch name to commit hash + update_abbs(git_branch, &ARGS.abbs_path) + .await + .context("Failed to update ABBS tree")?; + + let output = tokio::process::Command::new("git") + .arg("rev-parse") + .arg("HEAD") + .current_dir(&ARGS.abbs_path) + .output() + .await + .context("Failed to resolve branch to git commit")?; + let git_sha = String::from_utf8_lossy(&output.stdout).trim().to_string(); + + // sanitize archs arg + let mut archs: Vec<&str> = archs.split(",").collect(); + if archs.contains(&"mainline") { + // archs + archs.extend(ALL_ARCH.iter()); + archs.retain(|arch| *arch != "mainline"); + } + archs.sort(); + archs.dedup(); + + // create a new pipeline + let mut conn = pool + .get() + .context("Failed to get db connection from pool")?; + + use crate::schema::pipelines; + let new_pipeline = NewPipeline { + packages: packages.to_string(), + archs: archs.join(","), + git_branch: git_branch.to_string(), + git_sha: git_sha.clone(), + creation_time: chrono::Utc::now(), + }; + let pipeline = diesel::insert_into(pipelines::table) + .values(&new_pipeline) + .returning(Pipeline::as_returning()) + .get_result(&mut conn) + .context("Failed to create pipeline")?; + + // for each arch, create a new job + for arch in &archs { + use crate::schema::jobs; + let new_job = NewJob { + pipeline_id: pipeline.id, + packages: packages.to_string(), + arch: arch.to_string(), + creation_time: chrono::Utc::now(), + status: "created".to_string(), + }; + diesel::insert_into(jobs::table) + .values(&new_job) + .execute(&mut conn) + .context("Failed to create job")?; + } + + Ok(pipeline.id) +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 58b23f6..587c424 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -16,6 +16,8 @@ pub mod heartbeat; pub mod job; pub mod models; pub mod schema; +pub mod routes; +pub mod api; pub struct WorkerStatus { pub last_heartbeat: DateTime, diff --git a/server/src/main.rs b/server/src/main.rs index dcb17d8..47e2f58 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,13 +1,12 @@ +use axum::routing::post; use axum::{routing::get, Router}; use diesel::pg::PgConnection; use diesel::r2d2::ConnectionManager; use diesel::r2d2::Pool; +use server::routes::ping; +use server::routes::pipeline_new; use server::ARGS; - -// basic handler that responds with a static string -async fn root() -> &'static str { - "Hello, World!" -} +use tower_http::services::{ServeDir, ServeFile}; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -16,22 +15,23 @@ async fn main() -> anyhow::Result<()> { tracing::info!("Connecting to database"); let manager = ConnectionManager::::new(&ARGS.database_url); - let pool = Pool::builder() - .test_on_check_out(true) - .build(manager)?; + let pool = Pool::builder().test_on_check_out(true).build(manager)?; tracing::info!("Starting http server"); // build our application with a route + let serve_dir = ServeDir::new("frontend/dist") + .not_found_service(ServeFile::new("frontend/dist/index.html")); let app = Router::new() - // `GET /` goes to `root` - .route("/", get(root)) + .route("/api/ping", get(ping)) + .route("/api/pipeline/new", post(pipeline_new)) + .fallback_service(serve_dir) + .with_state(pool) .layer(tower_http::trace::TraceLayer::new_for_http()); - // run our app with hyper, listening globally on port 3000 - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); - tracing::debug!("listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app).await.unwrap(); + tracing::debug!("listening on 127.0.0.1:3000"); + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?; + axum::serve(listener, app).await?; /* dotenv::dotenv().ok(); diff --git a/server/src/models.rs b/server/src/models.rs index 19f32e6..8d92226 100644 --- a/server/src/models.rs +++ b/server/src/models.rs @@ -12,6 +12,17 @@ pub struct Pipeline { pub creation_time: chrono::DateTime, } +#[derive(Insertable)] +#[diesel(table_name = crate::schema::pipelines)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct NewPipeline { + pub packages: String, + pub archs: String, + pub git_branch: String, + pub git_sha: String, + pub creation_time: chrono::DateTime, +} + #[derive(Queryable, Selectable, Associations)] #[diesel(belongs_to(Pipeline))] #[diesel(table_name = crate::schema::jobs)] @@ -25,6 +36,17 @@ pub struct Job { pub status: String, } +#[derive(Insertable)] +#[diesel(table_name = crate::schema::jobs)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct NewJob { + pub pipeline_id: i32, + pub packages: String, + pub arch: String, + pub creation_time: chrono::DateTime, + pub status: String, +} + #[derive(Queryable, Selectable)] #[diesel(table_name = crate::schema::workers)] #[diesel(check_for_backend(diesel::pg::Pg))] diff --git a/server/src/routes.rs b/server/src/routes.rs new file mode 100644 index 0000000..dcdfe4b --- /dev/null +++ b/server/src/routes.rs @@ -0,0 +1,55 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use diesel::{ + r2d2::{ConnectionManager, Pool}, + PgConnection, +}; +use serde::{Deserialize, Serialize}; + +use crate::api; + +pub async fn ping() -> &'static str { + "PONG" +} + +// learned from https://github.com/tokio-rs/axum/blob/main/examples/anyhow-error-response/src/main.rs +pub struct AnyhowError(anyhow::Error); + +impl IntoResponse for AnyhowError { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", self.0)).into_response() + } +} + +impl From for AnyhowError +where + E: Into, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} + +#[derive(Deserialize)] +pub struct PipelineNewRequest { + git_branch: String, + packages: String, + archs: String, +} + +#[derive(Serialize)] +pub struct PipelineNewResponse { + id: i32, +} + +pub async fn pipeline_new( + State(pool): State>>, + Json(payload): Json, +) -> Result, AnyhowError> { + let pipeline_id = + api::pipeline_new(pool, &payload.git_branch, &payload.packages, &payload.archs).await?; + Ok(Json(PipelineNewResponse { id: pipeline_id })) +}