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

Add experimental generate subcommand to CLI #175

Merged
merged 1 commit into from
Mar 18, 2024
Merged
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
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