diff --git a/Cargo.lock b/Cargo.lock index f500a31..102c387 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,8 @@ dependencies = [ "clap", "clowarden-core", "config", + "serde", + "serde_yaml", "tokio", "tracing", "tracing-subscriber", diff --git a/clowarden-cli/Cargo.toml b/clowarden-cli/Cargo.toml index 493a969..090c358 100644 --- a/clowarden-cli/Cargo.toml +++ b/clowarden-cli/Cargo.toml @@ -11,6 +11,8 @@ anyhow = { workspace = true } clap = { workspace = true } clowarden-core = { path = "../clowarden-core" } config = { workspace = true } +serde = { workspace = true } +serde_yaml = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/clowarden-cli/src/main.rs b/clowarden-cli/src/main.rs index 0b04563..c9a1069 100644 --- a/clowarden-cli/src/main.rs +++ b/clowarden-cli/src/main.rs @@ -5,6 +5,7 @@ use anyhow::{format_err, Result}; use clap::{Args, Parser, Subcommand}; use clowarden_core::{ cfg::Legacy, + directory, github::{GHApi, Source}, multierror, services::{ @@ -17,7 +18,7 @@ use clowarden_core::{ Change, }, }; -use std::{env, sync::Arc}; +use std::{env, fs::File, path::PathBuf, sync::Arc}; #[derive(Parser)] #[command( @@ -25,7 +26,7 @@ use std::{env, sync::Arc}; about = "CLOWarden CLI tool This tool uses the Github API, which requires authentication. Please make sure -you provide a Github token (with repo and read:org scopes) by setting the +you provide a GitHub token (with repo and read:org scopes) by setting the GITHUB_TOKEN environment variable." )] struct Cli { @@ -35,12 +36,15 @@ struct Cli { #[derive(Subcommand)] enum Command { - /// Validate the configuration in the repository provided. - Validate(BaseArgs), - /// Display changes between the actual state (as defined in the services) /// and the desired state (as defined in the configuration). Diff(BaseArgs), + + /// Generate configuration file from the actual state (experimental). + Generate(GenerateArgs), + + /// Validate the configuration in the repository provided. + Validate(BaseArgs), } #[derive(Args)] @@ -66,6 +70,17 @@ struct BaseArgs { people_file: Option, } +#[derive(Args)] +struct GenerateArgs { + /// GitHub organization. + #[arg(long)] + org: String, + + /// Output file. + #[arg(long)] + output_file: PathBuf, +} + /// Environment variable containing Github token. const GITHUB_TOKEN: &str = "GITHUB_TOKEN"; @@ -87,31 +102,9 @@ async fn main() -> Result<()> { // Run command match cli.command { - Command::Validate(args) => validate(args, github_token).await?, Command::Diff(args) => diff(args, github_token).await?, - } - - Ok(()) -} - -/// Validate configuration. -async fn validate(args: BaseArgs, github_token: String) -> Result<()> { - // GitHub - - // Setup services - let (gh, svc) = setup_services(github_token); - let legacy = setup_legacy(&args); - let ctx = setup_context(&args); - let src = setup_source(&args); - - // Validate configuration and display results - println!("Validating configuration..."); - match github::State::new_from_config(gh, svc, &legacy, &ctx, &src).await { - Ok(_) => println!("Configuration is valid!"), - Err(err) => { - println!("{}\n", multierror::format_error(&err)?); - return Err(format_err!("Invalid configuration")); - } + Command::Validate(args) => validate(args, github_token).await?, + Command::Generate(args) => generate(args, github_token).await?, } Ok(()) @@ -124,7 +117,7 @@ async fn diff(args: BaseArgs, github_token: String) -> Result<()> { // Setup services let (gh, svc) = setup_services(github_token); let legacy = setup_legacy(&args); - let ctx = setup_context(&args); + let ctx = setup_context(&args.org); let src = setup_source(&args); // Get changes from the actual state to the desired state @@ -148,6 +141,57 @@ async fn diff(args: BaseArgs, github_token: String) -> Result<()> { Ok(()) } +/// Generate a configuration file from the actual state of the services. +/// +/// NOTE: at the moment the configuration generated uses the legacy format for +/// backwards compatibility reasons. +async fn generate(args: GenerateArgs, github_token: String) -> Result<()> { + #[derive(serde::Serialize)] + struct LegacyCfg { + teams: Vec, + repositories: Vec, + } + + println!("Getting actual state from GitHub..."); + let (_, svc) = setup_services(github_token); + let ctx = setup_context(&args.org); + let actual_state = github::State::new_from_service(svc.clone(), &ctx).await?; + + println!("Generating configuration file and writing it to the output file provided..."); + let cfg = LegacyCfg { + teams: actual_state.directory.teams.into_iter().map(Into::into).collect(), + repositories: actual_state.repositories, + }; + let file = File::create(&args.output_file)?; + serde_yaml::to_writer(file, &cfg)?; + + println!("done!"); + Ok(()) +} + +/// Validate configuration. +async fn validate(args: BaseArgs, github_token: String) -> Result<()> { + // GitHub + + // Setup services + let (gh, svc) = setup_services(github_token); + let legacy = setup_legacy(&args); + let ctx = setup_context(&args.org); + let src = setup_source(&args); + + // Validate configuration and display results + println!("Validating configuration..."); + match github::State::new_from_config(gh, svc, &legacy, &ctx, &src).await { + Ok(_) => println!("Configuration is valid!"), + Err(err) => { + println!("{}\n", multierror::format_error(&err)?); + return Err(format_err!("Invalid configuration")); + } + } + + Ok(()) +} + /// Helper function to setup some services from the arguments provided. fn setup_services(github_token: String) -> (Arc, Arc) { let gh = GHApi::new_with_token(github_token.clone()); @@ -165,11 +209,11 @@ fn setup_legacy(args: &BaseArgs) -> Legacy { } } -/// Helper function to create a context instance from the arguments. -fn setup_context(args: &BaseArgs) -> Ctx { +/// Helper function to create a context instance for the organization provided. +fn setup_context(org: &str) -> Ctx { Ctx { inst_id: None, - org: args.org.clone(), + org: org.to_string(), } } diff --git a/clowarden-core/src/directory/legacy.rs b/clowarden-core/src/directory/legacy.rs index 660cc5a..45c666c 100644 --- a/clowarden-core/src/directory/legacy.rs +++ b/clowarden-core/src/directory/legacy.rs @@ -50,7 +50,7 @@ impl Cfg { } } -pub(crate) mod sheriff { +pub mod sheriff { use crate::{ directory::{TeamName, UserName}, github::{DynGH, Source}, @@ -185,12 +185,29 @@ pub(crate) mod sheriff { /// Team configuration. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - pub(crate) struct Team { + pub struct Team { pub name: String, + + #[serde(skip_serializing_if = "Option::is_none")] pub maintainers: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] pub members: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] pub formation: Option>, } + + impl From for Team { + fn from(team: crate::directory::Team) -> Self { + Team { + name: team.name, + maintainers: Some(team.maintainers), + members: Some(team.members), + ..Default::default() + } + } + } } pub(crate) mod cncf { diff --git a/clowarden-core/src/directory/mod.rs b/clowarden-core/src/directory/mod.rs index 5000fc1..43a247b 100644 --- a/clowarden-core/src/directory/mod.rs +++ b/clowarden-core/src/directory/mod.rs @@ -16,7 +16,7 @@ use std::{ fmt::Write, }; -mod legacy; +pub mod legacy; lazy_static! { static ref GITHUB_URL: Regex = @@ -192,17 +192,7 @@ impl From for Directory { /// Create a new directory instance from the legacy configuration. fn from(cfg: legacy::Cfg) -> Self { // Teams - let teams = cfg - .sheriff - .teams - .into_iter() - .map(|t| Team { - name: t.name, - maintainers: t.maintainers.unwrap_or_default(), - members: t.members.unwrap_or_default(), - ..Default::default() - }) - .collect(); + let teams = cfg.sheriff.teams.into_iter().map(Into::into).collect(); // Users let users = if let Some(cncf) = cfg.cncf { @@ -266,6 +256,17 @@ pub struct Team { pub annotations: HashMap, } +impl From for Team { + fn from(team: legacy::sheriff::Team) -> Self { + Team { + name: team.name.clone(), + maintainers: team.maintainers.clone().unwrap_or_default(), + members: team.members.clone().unwrap_or_default(), + ..Default::default() + } + } +} + /// User profile. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct User { diff --git a/clowarden-core/src/services/github/mod.rs b/clowarden-core/src/services/github/mod.rs index cb41f73..d11d328 100644 --- a/clowarden-core/src/services/github/mod.rs +++ b/clowarden-core/src/services/github/mod.rs @@ -18,7 +18,7 @@ use tracing::debug; mod legacy; pub mod service; -mod state; +pub mod state; pub use state::State; /// GitHub's service name.