Skip to content

Commit

Permalink
Add experimental generate subcommand to CLI (#175)
Browse files Browse the repository at this point in the history
Closes #123

Signed-off-by: Sergio Castaño Arteaga <[email protected]>
  • Loading branch information
tegioz authored Mar 18, 2024
1 parent 7cc5756 commit 2e47eeb
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 48 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions clowarden-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
110 changes: 77 additions & 33 deletions clowarden-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -17,15 +18,15 @@ use clowarden_core::{
Change,
},
};
use std::{env, sync::Arc};
use std::{env, fs::File, path::PathBuf, sync::Arc};

#[derive(Parser)]
#[command(
version,
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 {
Expand All @@ -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)]
Expand All @@ -66,6 +70,17 @@ struct BaseArgs {
people_file: Option<String>,
}

#[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";

Expand All @@ -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(())
Expand All @@ -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
Expand All @@ -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<directory::legacy::sheriff::Team>,
repositories: Vec<github::state::Repository>,
}

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<GHApi>, Arc<SvcApi>) {
let gh = GHApi::new_with_token(github_token.clone());
Expand All @@ -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(),
}
}

Expand Down
21 changes: 19 additions & 2 deletions clowarden-core/src/directory/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ impl Cfg {
}
}

pub(crate) mod sheriff {
pub mod sheriff {
use crate::{
directory::{TeamName, UserName},
github::{DynGH, Source},
Expand Down Expand Up @@ -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<Vec<UserName>>,

#[serde(skip_serializing_if = "Option::is_none")]
pub members: Option<Vec<UserName>>,

#[serde(skip_serializing_if = "Option::is_none")]
pub formation: Option<Vec<TeamName>>,
}

impl From<crate::directory::Team> 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 {
Expand Down
25 changes: 13 additions & 12 deletions clowarden-core/src/directory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::{
fmt::Write,
};

mod legacy;
pub mod legacy;

lazy_static! {
static ref GITHUB_URL: Regex =
Expand Down Expand Up @@ -192,17 +192,7 @@ impl From<legacy::Cfg> 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 {
Expand Down Expand Up @@ -266,6 +256,17 @@ pub struct Team {
pub annotations: HashMap<String, String>,
}

impl From<legacy::sheriff::Team> 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 {
Expand Down
2 changes: 1 addition & 1 deletion clowarden-core/src/services/github/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 2e47eeb

Please sign in to comment.