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

[SM-1097] Add support for manpage generation #175

Merged
merged 7 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
32 changes: 32 additions & 0 deletions .github/workflows/build-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -435,3 +435,35 @@ jobs:
name: THIRDPARTY.html
path: ./crates/bws/THIRDPARTY.html
if-no-files-found: error

manpages:
name: Generate manpages
runs-on: ubuntu-22.04
needs:
- setup
steps:
- name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: Install rust
uses: dtolnay/rust-toolchain@be73d7920c329f220ce78e0234b8f96b7ae60248 # stable
with:
toolchain: stable

- name: Cache cargo registry
uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
with:
key: cargo-cli-manpage

- name: Generate manpages
run: |
cargo check -p bws --message-format json > build.json
OUT_DIR=$(jq -r --slurp '.[] | select (.reason == "build-script-executed") | select(.package_id|contains("crates/bws")) .out_dir' build.json)
mv $OUT_DIR/manpages .

- name: Upload artifact
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: manpages
path: ./manpages/*
if-no-files-found: error
17 changes: 17 additions & 0 deletions Cargo.lock

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

6 changes: 6 additions & 0 deletions crates/bws/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ uuid = { version = "^1.7.0", features = ["serde"] }

bitwarden = { workspace = true, features = ["secrets"] }

[build-dependencies]
clap = { version = "4.5.1", features = ["derive", "string"] }
clap_complete = "4.5.0"
clap_mangen = "0.2.20"
uuid = { version = "^1.7.0" }

[dev-dependencies]
tempfile = "3.10.0"

Expand Down
14 changes: 14 additions & 0 deletions crates/bws/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
include!("src/cli.rs");

fn main() -> Result<(), std::io::Error> {
use std::{env, fs, path::Path};

let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR exists");
let path = Path::new(&out_dir).join("manpages");
fs::create_dir_all(&path).expect("OUT_DIR is writable");

let cmd = <Cli as clap::CommandFactory>::command();
clap_mangen::generate_to(cmd, &path)?;

Ok(())
}
234 changes: 234 additions & 0 deletions crates/bws/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
use std::path::PathBuf;

use clap::{ArgGroup, Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
use uuid::Uuid;

pub(crate) const ACCESS_TOKEN_KEY_VAR_NAME: &str = "BWS_ACCESS_TOKEN";
pub(crate) const CONFIG_FILE_KEY_VAR_NAME: &str = "BWS_CONFIG_FILE";
pub(crate) const PROFILE_KEY_VAR_NAME: &str = "BWS_PROFILE";
pub(crate) const SERVER_URL_KEY_VAR_NAME: &str = "BWS_SERVER_URL";

pub(crate) const DEFAULT_CONFIG_FILENAME: &str = "config";
pub(crate) const DEFAULT_CONFIG_DIRECTORY: &str = ".bws";

#[allow(non_camel_case_types)]
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub(crate) enum ProfileKey {
server_base,
server_api,
server_identity,
state_file_dir,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
#[allow(clippy::upper_case_acronyms)]
pub(crate) enum Output {
JSON,
YAML,
Env,
Table,
TSV,
None,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub(crate) enum Color {
No,
Yes,
Auto,
}

#[derive(Parser, Debug)]
#[command(name = "bws", version, about = "Bitwarden Secrets CLI", long_about = None)]
pub(crate) struct Cli {
// Optional as a workaround for https://github.com/clap-rs/clap/issues/3572
#[command(subcommand)]
pub(crate) command: Option<Commands>,

#[arg(short = 'o', long, global = true, value_enum, default_value_t = Output::JSON, help="Output format")]
pub(crate) output: Output,

Check warning on line 50 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L49-L50

Added lines #L49 - L50 were not covered by tests

#[arg(short = 'c', long, global = true, value_enum, default_value_t = Color::Auto, help="Use colors in the output")]
pub(crate) color: Color,

Check warning on line 53 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L52-L53

Added lines #L52 - L53 were not covered by tests

#[arg(short = 't', long, global = true, env = ACCESS_TOKEN_KEY_VAR_NAME, hide_env_values = true, help="Specify access token for the service account")]
pub(crate) access_token: Option<String>,

#[arg(
short = 'f',
long,
global = true,
env = CONFIG_FILE_KEY_VAR_NAME,
help = format!("[default: ~/{}/{}] Config file to use", DEFAULT_CONFIG_DIRECTORY, DEFAULT_CONFIG_FILENAME)
)]
pub(crate) config_file: Option<PathBuf>,

#[arg(short = 'p', long, global = true, env = PROFILE_KEY_VAR_NAME, help="Profile to use from the config file")]
pub(crate) profile: Option<String>,

#[arg(short = 'u', long, global = true, env = SERVER_URL_KEY_VAR_NAME, help="Override the server URL from the config file")]
pub(crate) server_url: Option<String>,
}

#[derive(Subcommand, Debug)]
pub(crate) enum Commands {
#[command(long_about = "Configure the CLI", arg_required_else_help(true))]
Config {
name: Option<ProfileKey>,
value: Option<String>,

#[arg(short = 'd', long)]
delete: bool,

Check warning on line 82 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L82

Added line #L82 was not covered by tests
},

#[command(long_about = "Generate shell completion files")]
Completions { shell: Option<Shell> },

#[command(long_about = "Commands available on Projects")]
Project {
#[command(subcommand)]
cmd: ProjectCommand,
},
#[command(long_about = "Commands available on Secrets")]
Secret {
#[command(subcommand)]
cmd: SecretCommand,
},
#[command(long_about = "Create a single item (deprecated)", hide(true))]
Create {
#[command(subcommand)]
cmd: CreateCommand,
},
#[command(long_about = "Delete one or more items (deprecated)", hide(true))]
Delete {
#[command(subcommand)]
cmd: DeleteCommand,
},
#[command(long_about = "Edit a single item (deprecated)", hide(true))]
Edit {
#[command(subcommand)]
cmd: EditCommand,
},
#[command(long_about = "Retrieve a single item (deprecated)", hide(true))]
Get {
#[command(subcommand)]
cmd: GetCommand,
},
#[command(long_about = "List items (deprecated)", hide(true))]
List {
#[command(subcommand)]
cmd: ListCommand,
},
}

#[derive(Subcommand, Debug)]
pub(crate) enum SecretCommand {
Create {
key: String,
value: String,

Check warning on line 129 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L128-L129

Added lines #L128 - L129 were not covered by tests

#[arg(help = "The ID of the project this secret will be added to")]
project_id: Uuid,

Check warning on line 132 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L132

Added line #L132 was not covered by tests

#[arg(long, help = "An optional note to add to the secret")]
note: Option<String>,
},
Delete {
secret_ids: Vec<Uuid>,

Check warning on line 138 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L138

Added line #L138 was not covered by tests
},
#[clap(group = ArgGroup::new("edit_field").required(true).multiple(true))]
Edit {
secret_id: Uuid,

Check warning on line 142 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L142

Added line #L142 was not covered by tests
#[arg(long, group = "edit_field")]
key: Option<String>,
#[arg(long, group = "edit_field")]
value: Option<String>,
#[arg(long, group = "edit_field")]
note: Option<String>,
#[arg(long, group = "edit_field")]
project_id: Option<Uuid>,
},
Get {
secret_id: Uuid,

Check warning on line 153 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L153

Added line #L153 was not covered by tests
},
List {
project_id: Option<Uuid>,
},
}

#[derive(Subcommand, Debug)]
pub(crate) enum ProjectCommand {
Create {
name: String,

Check warning on line 163 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L163

Added line #L163 was not covered by tests
},
Delete {
project_ids: Vec<Uuid>,

Check warning on line 166 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L166

Added line #L166 was not covered by tests
},
Edit {
project_id: Uuid,

Check warning on line 169 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L169

Added line #L169 was not covered by tests
#[arg(long, group = "edit_field")]
name: String,

Check warning on line 171 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L171

Added line #L171 was not covered by tests
},
Get {
project_id: Uuid,

Check warning on line 174 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L174

Added line #L174 was not covered by tests
},
List,
}

#[derive(Subcommand, Debug)]
pub(crate) enum ListCommand {
Projects,
Secrets { project_id: Option<Uuid> },
}

#[derive(Subcommand, Debug)]
pub(crate) enum GetCommand {
Project { project_id: Uuid },
Secret { secret_id: Uuid },

Check warning on line 188 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L187-L188

Added lines #L187 - L188 were not covered by tests
}

#[derive(Subcommand, Debug)]
pub(crate) enum CreateCommand {
Project {
name: String,

Check warning on line 194 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L194

Added line #L194 was not covered by tests
},
Secret {
key: String,
value: String,

Check warning on line 198 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L197-L198

Added lines #L197 - L198 were not covered by tests

#[arg(long, help = "An optional note to add to the secret")]
note: Option<String>,

#[arg(long, help = "The ID of the project this secret will be added to")]
project_id: Uuid,

Check warning on line 204 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L204

Added line #L204 was not covered by tests
},
}

#[derive(Subcommand, Debug)]
pub(crate) enum EditCommand {
#[clap(group = ArgGroup::new("edit_field").required(true).multiple(true))]
Project {
project_id: Uuid,

Check warning on line 212 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L212

Added line #L212 was not covered by tests
#[arg(long, group = "edit_field")]
name: String,

Check warning on line 214 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L214

Added line #L214 was not covered by tests
},
#[clap(group = ArgGroup::new("edit_field").required(true).multiple(true))]
Secret {
secret_id: Uuid,

Check warning on line 218 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L218

Added line #L218 was not covered by tests
#[arg(long, group = "edit_field")]
key: Option<String>,
#[arg(long, group = "edit_field")]
value: Option<String>,
#[arg(long, group = "edit_field")]
note: Option<String>,
#[arg(long, group = "edit_field")]
project_id: Option<Uuid>,
},
}

#[derive(Subcommand, Debug)]
pub(crate) enum DeleteCommand {
Project { project_ids: Vec<Uuid> },
Secret { secret_ids: Vec<Uuid> },

Check warning on line 233 in crates/bws/src/cli.rs

View check run for this annotation

Codecov / codecov/patch

crates/bws/src/cli.rs#L232-L233

Added lines #L232 - L233 were not covered by tests
}
21 changes: 6 additions & 15 deletions crates/bws/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use std::{
path::{Path, PathBuf},
};

use clap::ValueEnum;
use color_eyre::eyre::{bail, Result};
use directories::BaseDirs;
use serde::{Deserialize, Serialize};

use crate::cli::{ProfileKey, DEFAULT_CONFIG_DIRECTORY, DEFAULT_CONFIG_FILENAME};

#[derive(Debug, Serialize, Deserialize, Default)]
pub(crate) struct Config {
pub profiles: HashMap<String, Profile>,
Expand All @@ -22,16 +23,6 @@ pub(crate) struct Profile {
pub state_file_dir: Option<String>,
}

// TODO: This could probably be derived with a macro if we start adding more fields
#[allow(non_camel_case_types)]
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub(crate) enum ProfileKey {
server_base,
server_api,
server_identity,
state_file_dir,
}

impl ProfileKey {
fn update_profile_value(&self, p: &mut Profile, value: String) {
match self {
Expand All @@ -43,17 +34,17 @@ impl ProfileKey {
}
}

pub(crate) const FILENAME: &str = "config";
pub(crate) const DIRECTORY: &str = ".bws";

fn get_config_path(config_file: Option<&Path>, ensure_folder_exists: bool) -> Result<PathBuf> {
let config_file = match config_file {
Some(path) => path.to_owned(),
None => {
let Some(base_dirs) = BaseDirs::new() else {
bail!("A valid home directory doesn't exist");
};
base_dirs.home_dir().join(DIRECTORY).join(FILENAME)
base_dirs
.home_dir()
.join(DEFAULT_CONFIG_DIRECTORY)
.join(DEFAULT_CONFIG_FILENAME)
}
};

Expand Down
Loading
Loading