diff --git a/.deny.toml b/.deny.toml index ea5a4af6..8930c2aa 100644 --- a/.deny.toml +++ b/.deny.toml @@ -76,6 +76,7 @@ ignore = [ #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, { id = "RUSTSEC-2024-0320", reason = "Nothing we can do about it now." }, { id = "RUSTSEC-2024-0370", reason = "Nothing we can do about it now." }, + { id = "RUSTSEC-2023-0071", reason = "Nothing we can do about it now." }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. diff --git a/.env b/.env new file mode 100644 index 00000000..8a373d3e --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=mysql://root:password@localhost:3306/lrzcc diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca0f96a6..3f27c793 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,23 +19,18 @@ on: env: CARGO_TERM_COLOR: always - SQLX_VERSION: 0.7.3 - SQLX_FEATURES: "rustls,postgres" + SQLX_FEATURES: "rustls,mysql" + DATABASE_URL: "mysql://root:password@127.0.0.1:3306/lrzcc" jobs: test: name: test runs-on: ubuntu-latest - services: - postgres: - image: postgres:14 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - POSTGRES_DB: postgres - ports: - - 5432:5432 steps: + - uses: getong/mariadb-action@v1.11 + with: + mysql database: 'lrzcc' + mysql root password: 'password' - name: Check out repository code uses: actions/checkout@v4 - name: Rust Cache Action @@ -49,8 +44,8 @@ jobs: --features ${{ env.SQLX_FEATURES }} --no-default-features --locked - - name: Install postgresql-client and mold - run: sudo apt update && sudo apt install postgresql-client mold -y + - name: Install mariadb-client and mold + run: sudo apt update && sudo apt install mariadb-client mold -y - name: Migrate database run: SKIP_DOCKER=true ./scripts/init_db.sh - name: Check sqlx offline data is up to date diff --git a/.mariadb.cnf b/.mariadb.cnf new file mode 100644 index 00000000..0504ef8a --- /dev/null +++ b/.mariadb.cnf @@ -0,0 +1,7 @@ +[client] +protocol = tcp +host = 127.0.0.1 +port = 3306 +user = root +password = password +database = lrzcc diff --git a/Cargo.lock b/Cargo.lock index 1e853daa..f02b11a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1542,6 +1542,7 @@ dependencies = [ "cargo-husky", "config", "jzon", + "lrzcc-wire", "once_cell", "reqwest", "secrecy", diff --git a/api/Cargo.toml b/api/Cargo.toml index b5164578..6b6e526a 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -25,13 +25,13 @@ path = "src/main.rs" [features] default = ["all"] all = ["accounting", "budgeting", "hello", "pricing", "quota", "resources", "user"] -accounting = [] -budgeting = [] -hello = [] -pricing = [] -quota = [] -resources = [] -user = [] +accounting = ["lrzcc-wire/accounting"] +budgeting = ["lrzcc-wire/budgeting"] +hello = ["lrzcc-wire/hello"] +pricing = ["lrzcc-wire/pricing"] +quota = ["lrzcc-wire/quota"] +resources = ["lrzcc-wire/resources"] +user = ["lrzcc-wire/user"] [dependencies] actix-web = "4" @@ -49,6 +49,7 @@ config = "0.14" uuid = { version = "1.10", features = ["v4", "serde"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } jzon = "0.12" +lrzcc-wire = { version = "1.0", path = "../wire" } [dependencies.sqlx] version = "0.8" @@ -57,7 +58,7 @@ features = [ "runtime-tokio", "tls-rustls", "macros", - "postgres", + "mysql", "uuid", "chrono", "migrate", diff --git a/api/configuration/base.yaml b/api/configuration/base.yaml index adf411d8..64e4d84d 100644 --- a/api/configuration/base.yaml +++ b/api/configuration/base.yaml @@ -2,8 +2,8 @@ application: port: 8000 database: host: "127.0.0.1" - port: 5432 - username: "postgres" + port: 3306 + username: "root" password: "password" database_name: "lrzcc" openstack: diff --git a/api/migrations/20240912151726_create_project_table.sql b/api/migrations/20240912151726_create_project_table.sql new file mode 100644 index 00000000..256e0df4 --- /dev/null +++ b/api/migrations/20240912151726_create_project_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE project ( + id INT PRIMARY KEY AUTO_INCREMENT, + name TEXT(255) UNIQUE NOT NULL, + openstack_id TEXT(255) UNIQUE NOT NULL, + user_class SMALLINT UNSIGNED NOT NULL +); diff --git a/api/src/configuration.rs b/api/src/configuration.rs index 2cef6dad..e8f801b1 100644 --- a/api/src/configuration.rs +++ b/api/src/configuration.rs @@ -1,6 +1,6 @@ use secrecy::{ExposeSecret, Secret}; use serde_aux::field_attributes::deserialize_number_from_string; -use sqlx::postgres::{PgConnectOptions, PgSslMode}; +use sqlx::mysql::{MySqlConnectOptions, MySqlSslMode}; #[derive(Clone, serde::Deserialize)] pub struct Settings { @@ -41,13 +41,13 @@ pub struct OpenStackSettings { } impl DatabaseSettings { - pub fn without_db(&self) -> PgConnectOptions { + pub fn without_db(&self) -> MySqlConnectOptions { let ssl_mode = if self.require_ssl { - PgSslMode::Require + MySqlSslMode::Required } else { - PgSslMode::Prefer + MySqlSslMode::Preferred }; - PgConnectOptions::new() + MySqlConnectOptions::new() .host(&self.host) .port(self.port) .ssl_mode(ssl_mode) @@ -55,7 +55,7 @@ impl DatabaseSettings { .password(self.password.expose_secret()) } - pub fn with_db(&self) -> PgConnectOptions { + pub fn with_db(&self) -> MySqlConnectOptions { self.without_db().database(&self.database_name) } } diff --git a/api/src/openstack.rs b/api/src/openstack.rs index bb19a77e..7032b66d 100644 --- a/api/src/openstack.rs +++ b/api/src/openstack.rs @@ -60,7 +60,7 @@ pub struct OpenStack { token: TokenHandler, } -#[derive(Debug, serde::Deserialize)] +#[derive(Clone, Debug, serde::Deserialize)] pub struct ProjectMinimal { pub id: String, pub name: String, diff --git a/api/src/routes/hello.rs b/api/src/routes/hello.rs index 8afd4d60..3ac7c669 100644 --- a/api/src/routes/hello.rs +++ b/api/src/routes/hello.rs @@ -1,18 +1,15 @@ +use crate::openstack::ProjectMinimal; +use actix_web::web::ReqData; use actix_web::HttpResponse; +use lrzcc_wire::hello::Hello; #[tracing::instrument(name = "hello")] -pub async fn hello() -> Result { - // TODO implement - Ok(HttpResponse::Ok().finish()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn hello_works() { - let response = hello().await.unwrap(); - assert!(response.status().is_success()) - } +pub async fn hello( + project: ReqData, +) -> Result { + Ok(HttpResponse::Ok() + .content_type("application/json") + .json(Hello { + message: format!("Hello, user {}!", project.name), + })) } diff --git a/api/src/startup.rs b/api/src/startup.rs index 3c41de44..7a8f00f8 100644 --- a/api/src/startup.rs +++ b/api/src/startup.rs @@ -5,8 +5,8 @@ use crate::routes::{health_check, hello}; use actix_web::{ dev::Server, middleware::from_fn, web, web::Data, App, HttpServer, }; -use sqlx::postgres::PgPoolOptions; -use sqlx::PgPool; +use sqlx::mysql::MySqlPoolOptions; +use sqlx::MySqlPool; use std::net::TcpListener; use tracing_actix_web::TracingLogger; @@ -50,7 +50,7 @@ pub struct ApplicationBaseUrl(pub String); async fn run( listener: TcpListener, - db_pool: PgPool, + db_pool: MySqlPool, base_url: String, openstack: OpenStack, ) -> Result { @@ -78,6 +78,6 @@ async fn run( Ok(server) } -pub fn get_connection_pool(configuration: &DatabaseSettings) -> PgPool { - PgPoolOptions::new().connect_lazy_with(configuration.with_db()) +pub fn get_connection_pool(configuration: &DatabaseSettings) -> MySqlPool { + MySqlPoolOptions::new().connect_lazy_with(configuration.with_db()) } diff --git a/api/tests/api/hello.rs b/api/tests/api/hello.rs index 8e195128..a6b18a06 100644 --- a/api/tests/api/hello.rs +++ b/api/tests/api/hello.rs @@ -1,3 +1,4 @@ +use lrzcc_wire::hello::Hello; use uuid::Uuid; use crate::helpers::spawn_app; @@ -52,7 +53,8 @@ async fn hello_works_with_valid_token() { let app = spawn_app().await; let client = reqwest::Client::new(); let token = Uuid::new_v4().to_string(); - app.mock_keystone_auth(&token, "project_id", "project_name") + let project_name = "project_name"; + app.mock_keystone_auth(&token, "project_id", project_name) .mount(&app.keystone_server) .await; @@ -66,4 +68,28 @@ async fn hello_works_with_valid_token() { // assert assert_eq!(response.status().as_u16(), 200); + assert_eq!( + response.headers().get("Content-Type").unwrap(), + "application/json" + ); + let hello = + serde_json::from_str::(&response.text().await.unwrap()).unwrap(); + assert_eq!(hello.message, format!("Hello, user {}!", project_name)); +} + +#[tokio::test] +async fn database_insert_works() { + // arrange + let app = spawn_app().await; + + // act and assert + sqlx::query!( + "INSERT INTO project (name, openstack_id, user_class) VALUES (?, ?, ?)", + "test", + "some-uuid", + 4, + ) + .execute(&app.db_pool) + .await + .expect("Failed to insert user."); } diff --git a/api/tests/api/helpers.rs b/api/tests/api/helpers.rs index b768e494..5406f0ef 100644 --- a/api/tests/api/helpers.rs +++ b/api/tests/api/helpers.rs @@ -3,7 +3,7 @@ use lrzcc_api::startup::{get_connection_pool, Application}; use lrzcc_api::telemetry::{get_subscriber, init_subscriber}; use once_cell::sync::Lazy; use serde_json::json; -use sqlx::{Connection, Executor, PgConnection, PgPool}; +use sqlx::{Connection, Executor, MySqlConnection, MySqlPool}; use uuid::Uuid; use wiremock::matchers::{header, method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -31,7 +31,7 @@ static TRACING: Lazy<()> = Lazy::new(|| { pub struct TestApp { pub address: String, pub _port: u16, - pub _db_pool: sqlx::PgPool, + pub db_pool: sqlx::MySqlPool, pub _api_client: reqwest::Client, pub keystone_server: MockServer, pub keystone_token: String, @@ -70,7 +70,7 @@ pub async fn spawn_app() -> TestApp { let configuration = { let mut c = get_configuration().expect("Failed to read configuration."); - c.database.database_name = Uuid::new_v4().to_string(); + c.database.database_name = Uuid::new_v4().simple().to_string(); c.application.port = 0; c.openstack.keystone_endpoint = keystone_server.uri(); c @@ -102,7 +102,7 @@ pub async fn spawn_app() -> TestApp { let test_app = TestApp { address: format!("http://127.0.0.1:{}", application_port), _port: application_port, - _db_pool: get_connection_pool(&configuration.database), + db_pool: get_connection_pool(&configuration.database), _api_client: client, keystone_server, keystone_token, @@ -110,22 +110,26 @@ pub async fn spawn_app() -> TestApp { test_app } -async fn configure_database(config: &DatabaseSettings) -> PgPool { +async fn configure_database(config: &DatabaseSettings) -> MySqlPool { // Create database - let mut connection = PgConnection::connect_with(&config.without_db()) + let mut connection = MySqlConnection::connect_with(&config.without_db()) .await - .expect("Failed to connect to Postgres."); + .expect("Failed to connect to MariaDB."); connection .execute( - format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str(), + format!( + "CREATE DATABASE IF NOT EXISTS `{}`;", + config.database_name + ) + .as_str(), ) .await .expect("Failed to create database."); // Migrate database - let connection_pool = PgPool::connect_with(config.with_db()) + let connection_pool = MySqlPool::connect_with(config.with_db()) .await - .expect("Failed to connect to Postgres."); + .expect("Failed to connect to MariaDB."); sqlx::migrate!("./migrations") .run(&connection_pool) .await diff --git a/scripts/init_db.sh b/scripts/init_db.sh index e65c2098..f8a23f5e 100755 --- a/scripts/init_db.sh +++ b/scripts/init_db.sh @@ -2,8 +2,8 @@ set -x set -eo pipefail -if ! [ -x "$(command -v psql)" ]; then - echo >&2 "Error: psql is not installed." +if ! [ -x "$(command -v mariadb)" ]; then + echo >&2 "Error: mariadb-client is not installed." exit 1 fi @@ -15,32 +15,30 @@ if ! [ -x "$(command -v sqlx)" ]; then exit 1 fi -DB_USER=${POSTGRES_USER:=postgres} -DB_PASSWORD="${POSTGRES_PASSWORD:=password}" -DB_NAME="${POSTGRES_DB:=lrzcc}" -DB_PORT="${POSTGRES_PORT:=5432}" +DB_HOST="${MARIADB_HOST:=127.0.0.1}" +DB_USER=${MARIADB_USER:=root} +DB_PASSWORD="${MARIADB_PASSWORD:=password}" +DB_NAME="${MARIADB_DB:=lrzcc}" +DB_PORT="${MARIADB_PORT:=3306}" if [[ -z "${SKIP_DOCKER}" ]] then docker run \ - -e POSTGRES_USER="${DB_USER}" \ - -e POSTGRES_PASSWORD="${DB_PASSWORD}" \ - -e POSTGRES_DB="${DB_NAME}" \ - -p "${DB_PORT}":5432 \ - -d postgres \ - postgres -N 1000 + -e MARIADB_ROOT_PASSWORD="${DB_PASSWORD}" \ + -e MARIADB_DB="${DB_NAME}" \ + -p "${DB_PORT}":3306 \ + -d mariadb:latest fi -export PGPASSWORD="${DB_PASSWORD}" -until psql -h "localhost" -U "${DB_USER}" -p "${DB_PORT}" -c '\q'; do - >&2 echo "Postgres is still unavailable - sleeping" +until mariadb -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" -D "" -e "QUIT"; do + >&2 echo "MariaDB is still unavailable - sleeping" sleep 1 done ->&2 echo "Postgres is up and running on port ${DB_PORT}!" +>&2 echo "MariaDB is up and running on ${DB_HOST} on port ${DB_PORT}!" -export DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_NAME} +export DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME} sqlx database create sqlx migrate run ->&2 echo "Postgres has been migrated, ready to go!" +>&2 echo "MariaDB has been migrated, ready to go!" diff --git a/scripts/mariadb.sh b/scripts/mariadb.sh new file mode 100755 index 00000000..5bc442b4 --- /dev/null +++ b/scripts/mariadb.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +mariadb --defaults-file=.mariadb.cnf