Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(runner): remove useless snapshots #24

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 143 additions & 38 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ tokio-tar = "0.3.1"
md5 = "0.7.0"
base64 = "0.21.0"
async-compression = { version = "0.4.5", features = ["tokio", "gzip"] }
simplelog = { version = "0.12.1", default-features = false }
simplelog = { version = "0.12.1", default-features = false, features = [
"termcolor",
] }
tempfile = "3.10.0"
git2 = "0.18.3"
nestify = "0.3.3"
gql_client = { git = "https://github.com/adriencaccia/gql-client-rs" }
serde_yaml = "0.9.34"

[dev-dependencies]
temp-env = { version = "0.3.6", features = ["async_closure"] }
Expand Down
5 changes: 4 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{prelude::*, run};
use crate::{auth, prelude::*, run};
use clap::{Parser, Subcommand};

#[derive(Parser, Debug)]
Expand All @@ -11,13 +11,16 @@ struct Cli {
enum Commands {
/// Run the bench command and upload the results to CodSpeed
Run(run::RunArgs),
/// Commands related to authentication with CodSpeed
Auth(auth::AuthArgs),
}

pub async fn run() -> Result<()> {
let cli = Cli::parse();

match cli.command {
Commands::Run(args) => run::run(args).await?,
Commands::Auth(args) => auth::run(args).await?,
}
Ok(())
}
166 changes: 166 additions & 0 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use std::time::Duration;

use crate::{config::Config, logger::get_local_logger, prelude::*};
use clap::{Args, Subcommand};
use gql_client::{Client as GQLClient, ClientConfig};
use nestify::nest;
use serde::{Deserialize, Serialize};
use simplelog::CombinedLogger;
use tokio::time::{sleep, Instant};

#[derive(Debug, Args)]
pub struct AuthArgs {
/// The URL of the CodSpeed GraphQL API
#[arg(long, env = "CODSPEED_API_URL", global = true, hide = true)]
api_url: Option<String>,

#[command(subcommand)]
command: AuthCommands,
}

#[derive(Debug, Subcommand)]
enum AuthCommands {
/// Login to CodSpeed
Login,
}

// TODO: tweak the logger to make it more user-friendly
fn init_logger() -> Result<()> {
let logger = get_local_logger();
CombinedLogger::init(vec![logger])?;
Ok(())
}

pub async fn run(args: AuthArgs) -> Result<()> {
init_logger()?;
let api_client = CodSpeedAPIClient::from(&args);

match args.command {
AuthCommands::Login => login(api_client).await?,
}
Ok(())
}

nest! {
#[derive(Debug, Deserialize, Serialize)]*
#[serde(rename_all = "camelCase")]*
struct CreateLoginSessionData {
create_login_session: struct CreateLoginSessionPayload {
callback_url: String,
session_id: String,
}
}
}

nest! {
#[derive(Debug, Deserialize, Serialize)]*
#[serde(rename_all = "camelCase")]*
struct ConsumeLoginSessionData {
consume_login_session: struct ConsumeLoginSessionPayload {
token: Option<String>
}
}
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ConsumeLoginSessionVars {
session_id: String,
}

struct CodSpeedAPIClient {
gql_client: GQLClient,
}

impl From<&AuthArgs> for CodSpeedAPIClient {
fn from(args: &AuthArgs) -> Self {
Self {
gql_client: build_gql_api_client(args.api_url.clone()),
}
}
}

const CODSPEED_GRAPHQL_ENDPOINT: &str = "https://gql.codspeed.io/";

fn build_gql_api_client(api_url: Option<String>) -> GQLClient {
let endpoint = api_url.unwrap_or_else(|| CODSPEED_GRAPHQL_ENDPOINT.to_string());

GQLClient::new_with_config(ClientConfig {
endpoint,
timeout: Some(10),
headers: Default::default(),
proxy: None,
})
}

impl CodSpeedAPIClient {
async fn create_login_session(&self) -> Result<CreateLoginSessionPayload> {
let response = self
.gql_client
.query_unwrap::<CreateLoginSessionData>(include_str!("queries/CreateLoginSession.gql"))
.await;
match response {
Ok(response) => Ok(response.create_login_session),
Err(err) => bail!("Failed to create login session: {}", err),
}
}

async fn consume_login_session(&self, session_id: &str) -> Result<ConsumeLoginSessionPayload> {
let response = self
.gql_client
.query_with_vars_unwrap::<ConsumeLoginSessionData, ConsumeLoginSessionVars>(
include_str!("queries/ConsumeLoginSession.gql"),
ConsumeLoginSessionVars {
session_id: session_id.to_string(),
},
)
.await;
match response {
Ok(response) => Ok(response.consume_login_session),
Err(err) => bail!("Failed to use login session: {}", err),
}
}
}

const LOGIN_SESSION_MAX_DURATION: Duration = Duration::from_secs(60 * 5); // 5 minutes

async fn login(api_client: CodSpeedAPIClient) -> Result<()> {
debug!("Login to CodSpeed");
debug!("Creating login session...");
let login_session_payload = api_client.create_login_session().await?;
info!(
"Login session created, open the following URL in your browser: {}",
login_session_payload.callback_url
);

info!("Waiting for the login to be completed...");
let token;
let start = Instant::now();
loop {
if LOGIN_SESSION_MAX_DURATION < start.elapsed() {
bail!("Login session expired, please try again");
}

match api_client
.consume_login_session(&login_session_payload.session_id)
.await?
.token
{
Some(token_from_api) => {
token = token_from_api;
break;
}
None => sleep(Duration::from_secs(5)).await,
}
}
debug!("Login completed");

let mut config = Config::load().await?;
config.auth.token = token;
config.persist().await?;
debug!("Token saved to configuration file");

info!("Login successful, your are now authenticated on CodSpeed");

Ok(())
}
73 changes: 73 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use std::{env, path::PathBuf};

use crate::prelude::*;
use nestify::nest;
use serde::{Deserialize, Serialize};

nest! {
#[derive(Debug, Deserialize, Serialize)]*
#[serde(rename_all = "kebab-case")]*
pub struct Config {
pub auth: pub struct AuthConfig {
pub token: String,
}
}
}

/// Get the path to the configuration file, following the XDG Base Directory Specification
/// at https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
fn get_configuration_file_path() -> PathBuf {
let config_dir = env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = env::var("HOME").expect("HOME env variable not set");
PathBuf::from(home).join(".config")
});
let config_dir = config_dir.join("codspeed");
config_dir.join("config.yaml")
}

impl Default for Config {
fn default() -> Self {
Self {
auth: AuthConfig { token: "".into() },
}
}
}

impl Config {
/// Load the configuration. If it does not exist, store and return a default configuration
pub async fn load() -> Result<Self> {
let config_path = get_configuration_file_path();

match tokio::fs::read(&config_path).await {
Ok(config_str) => {
let config = serde_yaml::from_slice(&config_str).context(format!(
"Failed to parse CodSpeed config at {}",
config_path.display()
))?;
debug!("Config loaded from {}", config_path.display());
Ok(config)
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
debug!("Config file not found at {}", config_path.display());
let config = Config::default();
config.persist().await?;
Ok(config)
}
Err(e) => bail!("Failed to load config: {}", e),
}
}

/// Persist changes to the configuration
pub async fn persist(&self) -> Result<()> {
let config_path = get_configuration_file_path();
tokio::fs::create_dir_all(config_path.parent().unwrap()).await?;

let config_str = serde_yaml::to_string(self)?;
tokio::fs::write(&config_path, config_str).await?;
debug!("Config written to {}", config_path.display());

Ok(())
}
}
21 changes: 21 additions & 0 deletions src/logger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use std::env;

use simplelog::{ConfigBuilder, SharedLogger};

pub fn get_local_logger() -> Box<dyn SharedLogger> {
let log_level = env::var("CODSPEED_LOG")
.ok()
.and_then(|log_level| log_level.parse::<log::LevelFilter>().ok())
.unwrap_or(log::LevelFilter::Info);

let config = ConfigBuilder::new()
.set_time_level(log::LevelFilter::Debug)
.build();

simplelog::TermLogger::new(
log_level,
config,
simplelog::TerminalMode::Mixed,
simplelog::ColorChoice::Auto,
)
}
13 changes: 9 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
mod app;
mod auth;
mod config;
mod logger;
mod prelude;
mod request_client;
mod run;
Expand All @@ -15,10 +18,12 @@ pub const VALGRIND_CODSPEED_VERSION: &str = "3.21.0-0codspeed1";
async fn main() {
let res = crate::app::run().await;
if let Err(err) = res {
if log_enabled!(log::Level::Error) {
error!("Error {}", err);
} else {
eprintln!("Error {}", err);
for cause in err.chain() {
if log_enabled!(log::Level::Error) {
error!("Error {}", cause);
} else {
eprintln!("Error {}", cause);
}
}
if log_enabled!(log::Level::Debug) {
for e in err.chain().skip(1) {
Expand Down
5 changes: 5 additions & 0 deletions src/queries/ConsumeLoginSession.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation ConsumeLoginSession($sessionId: String!) {
consumeLoginSession(sessionId: $sessionId) {
token
}
}
6 changes: 6 additions & 0 deletions src/queries/CreateLoginSession.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mutation CreateLoginSession {
createLoginSession {
callbackUrl
sessionId
}
}
Loading
Loading