diff --git a/.github/workflows/dynamic-analysis.yml b/.github/workflows/dynamic-analysis.yml index 423ea925..15f6b8ef 100644 --- a/.github/workflows/dynamic-analysis.yml +++ b/.github/workflows/dynamic-analysis.yml @@ -32,6 +32,8 @@ jobs: - run: cargo nextest run env: RUN_INTEGRATION_TESTS: "1" + # This key is used for integration tests + FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} # nextest doesn't run doctests, but does test everything else: https://github.com/nextest-rs/nextest/issues/16 # run doctests after; this won't result in any extra rebuilds and is very quick. # doctest overview: https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html diff --git a/src/cmd/run.rs b/src/cmd/run.rs index eb8248fd..95c1ce1d 100644 --- a/src/cmd/run.rs +++ b/src/cmd/run.rs @@ -2,7 +2,7 @@ use std::time::Duration; -use error_stack::{Result, ResultExt}; +use error_stack::{report, Result, ResultExt}; use futures::TryStreamExt; use futures::{future::try_join_all, try_join, StreamExt}; use governor::{Quota, RateLimiter}; @@ -18,7 +18,11 @@ use tracing::{debug, info}; use uuid::Uuid; use crate::api::fossa::{self, CliMetadata, ProjectMetadata}; -use crate::api::remote::{git, BranchImportStrategy, Reference, TagImportStrategy}; +use crate::api::remote::git::repository; +use crate::api::remote::{ + git, BranchImportStrategy, Integrations, Protocol, Reference, TagImportStrategy, +}; +use crate::ext::result::WrapErr; use crate::ext::tracing::span_record; use crate::fossa_cli::{self, DesiredVersion, Location, SourceUnits}; use crate::queue::Queue; @@ -84,6 +88,18 @@ pub enum Error { /// If we fail to delete tasks' state in the sqlite DB, this error is raised #[error("delete tasks' state")] TaskDeleteState, + + /// Preflight checks failed + #[error("preflight checks")] + PreflightChecks, + + /// Failed to connect to at least one integration + #[error("integration connections")] + IntegrationConnection, + + /// Failed to connect to FOSSA + #[error("FOSSA connection")] + FossaConnection, } /// Similar to [`AppContext`], but scoped for this subcommand. @@ -114,9 +130,52 @@ pub async fn main(ctx: &AppContext, config: Config, db: D) -> Resul } } + let preflight_checks = preflight_checks(&ctx); let healthcheck_worker = healthcheck(&ctx.db); let integration_worker = integrations(&ctx); - try_join!(healthcheck_worker, integration_worker).discard_ok() + try_join!(preflight_checks, healthcheck_worker, integration_worker).discard_ok() +} + +/// Checks and catches network misconfigurations before Broker attempts its operations +async fn preflight_checks(ctx: &CmdContext) -> Result<(), Error> { + let check_integration_connections = check_integration_connections(ctx.config.integrations()); + let check_fossa_connection = check_fossa_connection(&ctx.config); + try_join!(check_integration_connections, check_fossa_connection) + .discard_ok() + .change_context(Error::PreflightChecks) +} + +#[tracing::instrument(skip_all)] +/// Check that Broker can connect to at least one integration +async fn check_integration_connections(integrations: &Integrations) -> Result<(), Error> { + if integrations.as_ref().is_empty() { + return Ok(()); + } + + for integration in integrations.iter() { + let Protocol::Git(transport) = integration.protocol(); + if repository::ls_remote(transport).await.is_ok() { + return Ok(()); + } + } + + report!(Error::IntegrationConnection) + .wrap_err() + .help("run broker fix for detailed explanation on failing integration connections") + .describe("integration connections") +} + +#[tracing::instrument(skip_all)] +/// Check that Broker can connect to FOSSA +async fn check_fossa_connection(config: &Config) -> Result<(), Error> { + match fossa::OrgConfig::lookup(config.fossa_api()).await { + Ok(_) => Ok(()), + Err(err) => err + .change_context(Error::FossaConnection) + .wrap_err() + .help("run broker fix for detailed explanation on failing fossa connection") + .describe("fossa connection"), + } } #[tracing::instrument(skip_all)] diff --git a/tests/it/binary.rs b/tests/it/binary.rs index a73a8ac6..96622ff9 100644 --- a/tests/it/binary.rs +++ b/tests/it/binary.rs @@ -7,6 +7,7 @@ use std::path::Path; use tempfile::TempDir; +use crate::guard_integration_test; use crate::temp_config; macro_rules! run { @@ -47,6 +48,8 @@ fn interrupt(pid: u32) { #[track_caller] #[cfg(target_family = "unix")] fn run_and_interrupt_broker(tmp: &TempDir, config_path: &Path) { + guard_integration_test!(); + let config_path = config_path.to_string_lossy().to_string(); let data_root = tmp.path().to_string_lossy().to_string(); let child = run!(broker => "run -c {config_path} -r {data_root}"); diff --git a/tests/it/helper.rs b/tests/it/helper.rs index 4348fb92..befc2b90 100644 --- a/tests/it/helper.rs +++ b/tests/it/helper.rs @@ -40,16 +40,18 @@ macro_rules! temp_config { () => {{ let tmp = tempfile::tempdir().expect("must create tempdir"); let dir = tmp.path().join("debug"); + let fossa_key = std::env::var("FOSSA_API_KEY").expect("test"); + let content = indoc::formatdoc! {r#" fossa_endpoint: https://app.fossa.com - fossa_integration_key: abcd1234 + fossa_integration_key: {} version: 1 debugging: location: {dir:?} retention: days: 1 integrations: - "#}; + "#, fossa_key }; let path = tmp.path().join("config.yml"); std::fs::write(&path, content).expect("must write config file"); @@ -291,6 +293,10 @@ macro_rules! guard_integration_test { if std::env::var("RUN_INTEGRATION_TESTS").is_err() { return; } + + if std::env::var("FOSSA_API_KEY").is_err() { + return; + } }; }