From c5f2eedf0780010d29d0e9ebb6a8c542284cfa56 Mon Sep 17 00:00:00 2001 From: Takahiro Tsuruda Date: Tue, 4 Aug 2020 06:30:20 +0900 Subject: [PATCH] feat: Add gcloud module (#1493) * feat(gcloud): Add document of gcloud module Signed-off-by: dulltz * feat(gcloud): Add gcloud module Signed-off-by: dulltz * feat(gcloud): Add test for gcloud module Signed-off-by: dulltz * Apply the comment https://github.com/starship/starship/pull/1493\#discussion_r456965413 Signed-off-by: dulltz --- docs/config/README.md | 62 ++++++++++++++ src/configs/gcloud.rs | 24 ++++++ src/configs/mod.rs | 1 + src/configs/starship_root.rs | 1 + src/module.rs | 1 + src/modules/gcloud.rs | 151 +++++++++++++++++++++++++++++++++ src/modules/mod.rs | 3 + tests/testsuite/gcloud.rs | 159 +++++++++++++++++++++++++++++++++++ tests/testsuite/main.rs | 1 + 9 files changed, 403 insertions(+) create mode 100644 src/configs/gcloud.rs create mode 100644 src/modules/gcloud.rs create mode 100644 tests/testsuite/gcloud.rs diff --git a/docs/config/README.md b/docs/config/README.md index 9514d8f2..21f59a6e 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -212,6 +212,7 @@ $nix_shell\ $conda\ $memory_usage\ $aws\ +$gcloud\ $env_var\ $crystal\ $cmd_duration\ @@ -930,6 +931,67 @@ The module will be shown if any of the following conditions are met: format = "via [e $version](bold red) " ``` +## Gcloud + +The `gcloud` module shows the current configuration for [`gcloud`](https://cloud.google.com/sdk/gcloud) CLI. +This is based on the `~/.config/gcloud/active_config` file and the `~/.config/gcloud/configurations/config_{CONFIG NAME}` file and the `CLOUDSDK_CONFIG` env var. + +### Options + +| Variable | Default | Description | +| ----------------- | ------------------------------------------------- | --------------------------------------------------------------------------- | +| `format` | `"on [$symbol$account(\\($region\\))]($style) "` | The format for the module. | +| `symbol` | `"☁️ "` | The symbol used before displaying the current GCP profile. | +| `region_aliases` | | Table of region aliases to display in addition to the GCP name. | +| `style` | `"bold blue"` | The style for the module. | +| `disabled` | `false` | Disables the `gcloud` module. | + +### Variables + +| Variable | Example | Description | +| -------- | ----------------- | ------------------------------------------------------------------ | +| region | `us-central1` | The current GCP region | +| account | `foo@example.com` | The current GCP profile | +| project | | The current GCP project | +| active | `default` | The active config name written in `~/.config/gcloud/active_config` | +| symbol | | Mirrors the value of option `symbol` | +| style\* | | Mirrors the value of option `style` | + +\*: This variable can only be used as a part of a style string + +### Examples + +#### Display account and project + +```toml +# ~/.config/starship.toml + +[gcloud] +format = "on [$symbol$account(\\($project\\))]($style) " +``` + +#### Display active config name only + +```toml +# ~/.config/starship.toml + +[gcloud] +format = "[$symbol$active]($style) " +style = "bold yellow" +``` + +#### Display account and aliased region + +```toml +# ~/.config/starship.toml + +[gcloud] +symbol = "️🇬️ " +[gcloud.region_aliases] +us-central1 = "uc1" +asia-northeast1 = "an1" +``` + ## Git Branch The `git_branch` module shows the active branch of the repo in your current directory. diff --git a/src/configs/gcloud.rs b/src/configs/gcloud.rs new file mode 100644 index 00000000..b392a98e --- /dev/null +++ b/src/configs/gcloud.rs @@ -0,0 +1,24 @@ +use crate::config::{ModuleConfig, RootModuleConfig}; +use starship_module_config_derive::ModuleConfig; +use std::collections::HashMap; + +#[derive(Clone, ModuleConfig)] +pub struct GcloudConfig<'a> { + pub format: &'a str, + pub symbol: &'a str, + pub style: &'a str, + pub disabled: bool, + pub region_aliases: HashMap, +} + +impl<'a> RootModuleConfig<'a> for GcloudConfig<'a> { + fn new() -> Self { + GcloudConfig { + format: "on [$symbol$account(\\($region\\))]($style) ", + symbol: "☁️ ", + style: "bold blue", + disabled: false, + region_aliases: HashMap::new(), + } + } +} diff --git a/src/configs/mod.rs b/src/configs/mod.rs index 3ba51bd8..6946f79b 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -14,6 +14,7 @@ pub mod elixir; pub mod elm; pub mod env_var; pub mod erlang; +pub mod gcloud; pub mod git_branch; pub mod git_commit; pub mod git_state; diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index e832f309..20c7034e 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -52,6 +52,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "conda", "memory_usage", "aws", + "gcloud", "env_var", "crystal", "cmd_duration", diff --git a/src/module.rs b/src/module.rs index 5e833830..7e592214 100644 --- a/src/module.rs +++ b/src/module.rs @@ -23,6 +23,7 @@ pub const ALL_MODULES: &[&str] = &[ "elm", "erlang", "env_var", + "gcloud", "git_branch", "git_commit", "git_state", diff --git a/src/modules/gcloud.rs b/src/modules/gcloud.rs new file mode 100644 index 00000000..c5101e60 --- /dev/null +++ b/src/modules/gcloud.rs @@ -0,0 +1,151 @@ +use std::collections::HashMap; +use std::env; +use std::fs::File; +use std::io::{BufRead, BufReader, Error, ErrorKind}; +use std::path::PathBuf; +use std::str::FromStr; + +use super::{Context, Module, RootModuleConfig}; + +use crate::configs::gcloud::GcloudConfig; +use crate::formatter::StringFormatter; + +type Account = String; +type Project = String; +type Region = String; +type Active = String; + +fn get_gcloud_account_from_config(current_config: &PathBuf) -> Option { + let file = File::open(¤t_config).ok()?; + let reader = BufReader::new(file); + let lines = reader.lines().filter_map(Result::ok); + let account_line = lines + .skip_while(|line| line != "[core]") + .skip(1) + .take_while(|line| !line.starts_with('[')) + .find(|line| line.starts_with("account"))?; + let account = account_line.split('=').nth(1)?.trim(); + Some(account.to_string()) +} + +fn get_gcloud_project_from_config(current_config: &PathBuf) -> Option { + let file = File::open(¤t_config).ok()?; + let reader = BufReader::new(file); + let lines = reader.lines().filter_map(Result::ok); + let project_line = lines + .skip_while(|line| line != "[core]") + .skip(1) + .take_while(|line| !line.starts_with('[')) + .find(|line| line.starts_with("project"))?; + let project = project_line.split('=').nth(1)?.trim(); + Some(project.to_string()) +} + +fn get_gcloud_region_from_config(current_config: &PathBuf) -> Option { + let file = File::open(¤t_config).ok()?; + let reader = BufReader::new(file); + let lines = reader.lines().filter_map(Result::ok); + let region_line = lines + .skip_while(|line| line != "[compute]") + .skip(1) + .take_while(|line| !line.starts_with('[')) + .find(|line| line.starts_with("region"))?; + let region = region_line.split('=').nth(1)?.trim(); + Some(region.to_string()) +} + +fn get_active_config(config_root: &PathBuf) -> Option { + let path = config_root.join("active_config"); + let file = File::open(&path).ok()?; + let reader = BufReader::new(file); + let first_line = match reader.lines().next() { + Some(res) => res, + None => Err(Error::new(ErrorKind::NotFound, "empty")), + }; + match first_line { + Ok(c) => Some(c), + Err(_) => None, + } +} + +fn get_current_config_path() -> Option { + let config_dir = get_config_dir()?; + let active_config = get_active_config(&config_dir)?; + let current_config = config_dir.join(format!("configurations/config_{}", active_config)); + Some(current_config) +} + +fn get_config_dir() -> Option { + let config_dir = env::var("CLOUDSDK_CONFIG") + .ok() + .and_then(|path| PathBuf::from_str(&path).ok()) + .or_else(|| { + let mut home = dirs_next::home_dir()?; + home.push(".config/gcloud"); + Some(home) + })?; + Some(config_dir) +} + +fn alias_region(region: String, aliases: &HashMap) -> String { + match aliases.get(®ion) { + None => region.to_string(), + Some(alias) => (*alias).to_string(), + } +} + +pub fn module<'a>(context: &'a Context) -> Option> { + let mut module = context.new_module("gcloud"); + let config: GcloudConfig = GcloudConfig::try_load(module.config); + + let config_path = get_current_config_path()?; + let gcloud_account = get_gcloud_account_from_config(&config_path); + let gcloud_project = get_gcloud_project_from_config(&config_path); + let gcloud_region = get_gcloud_region_from_config(&config_path); + let config_dir = get_config_dir()?; + let gcloud_active: Option = get_active_config(&config_dir); + + if gcloud_account.is_none() + && gcloud_project.is_none() + && gcloud_region.is_none() + && gcloud_active.is_none() + { + return None; + } + + let mapped_region = if let Some(gcloud_region) = gcloud_region { + Some(alias_region(gcloud_region, &config.region_aliases)) + } else { + None + }; + + let parsed = StringFormatter::new(config.format).and_then(|formatter| { + formatter + .map_meta(|variable, _| match variable { + "symbol" => Some(config.symbol), + _ => None, + }) + .map_style(|variable| match variable { + "style" => Some(Ok(config.style)), + _ => None, + }) + .map(|variable| match variable { + "account" => gcloud_account.as_ref().map(Ok), + "project" => gcloud_project.as_ref().map(Ok), + "region" => mapped_region.as_ref().map(Ok), + "active" => gcloud_active.as_ref().map(Ok), + _ => None, + }) + .parse(None) + }); + + module.set_segments(match parsed { + Ok(segments) => segments, + Err(error) => { + log::error!("Error in module `gcloud`: \n{}", error); + return None; + } + }); + + Some(module) +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index f2984f9e..b2bda793 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -14,6 +14,7 @@ mod elixir; mod elm; mod env_var; mod erlang; +mod gcloud; mod git_branch; mod git_commit; mod git_state; @@ -72,6 +73,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "elm" => elm::module(context), "erlang" => erlang::module(context), "env_var" => env_var::module(context), + "gcloud" => gcloud::module(context), "git_branch" => git_branch::module(context), "git_commit" => git_commit::module(context), "git_state" => git_state::module(context), @@ -127,6 +129,7 @@ pub fn description(module: &str) -> &'static str { "dotnet" => "The relevant version of the .NET Core SDK for the current directory", "env_var" => "Displays the current value of a selected environment variable", "erlang" => "Current OTP version", + "gcloud" => "The current GCP client configuration", "git_branch" => "The active branch of the repo in your current directory", "git_commit" => "The active commit of the repo in your current directory", "git_state" => "The current git operation, and it's progress", diff --git a/tests/testsuite/gcloud.rs b/tests/testsuite/gcloud.rs new file mode 100644 index 00000000..2405e06d --- /dev/null +++ b/tests/testsuite/gcloud.rs @@ -0,0 +1,159 @@ +use std::fs::{create_dir, File}; +use std::io::{self, Write}; + +use ansi_term::Color; + +use crate::common::{self, TestCommand}; + +#[test] +fn account_set() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let active_config_path = dir.path().join("active_config"); + let mut active_config_file = File::create(&active_config_path)?; + active_config_file.write_all(b"default")?; + + create_dir(dir.path().join("configurations"))?; + let config_default_path = dir.path().join("configurations/config_default"); + let mut config_default_file = File::create(&config_default_path)?; + config_default_file.write_all( + b"[core] +account = foo@example.com +", + )?; + + let output = common::render_module("gcloud") + .env("CLOUDSDK_CONFIG", dir.path().to_string_lossy().as_ref()) + .output()?; + let expected = format!("on {} ", Color::Blue.bold().paint("☁️ foo@example.com")); + let actual = String::from_utf8(output.stdout).unwrap(); + assert_eq!(actual, expected); + dir.close() +} + +#[test] +fn account_and_region_set() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let active_config_path = dir.path().join("active_config"); + let mut active_config_file = File::create(&active_config_path)?; + active_config_file.write_all(b"default")?; + + create_dir(dir.path().join("configurations"))?; + let config_default_path = dir.path().join("configurations/config_default"); + let mut config_default_file = File::create(&config_default_path)?; + config_default_file.write_all( + b"[core] +account = foo@example.com + +[compute] +region = us-central1 +", + )?; + + let output = common::render_module("gcloud") + .env("CLOUDSDK_CONFIG", dir.path().to_string_lossy().as_ref()) + .output()?; + let expected = format!( + "on {} ", + Color::Blue.bold().paint("☁️ foo@example.com(us-central1)") + ); + let actual = String::from_utf8(output.stdout).unwrap(); + assert_eq!(actual, expected); + dir.close() +} + +#[test] +fn account_and_region_set_with_alias() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let active_config_path = dir.path().join("active_config"); + let mut active_config_file = File::create(&active_config_path)?; + active_config_file.write_all(b"default")?; + + create_dir(dir.path().join("configurations"))?; + let config_default_path = dir.path().join("configurations/config_default"); + let mut config_default_file = File::create(&config_default_path)?; + config_default_file.write_all( + b"[core] +account = foo@example.com + +[compute] +region = us-central1 +", + )?; + + let output = common::render_module("gcloud") + .env("CLOUDSDK_CONFIG", dir.path().to_string_lossy().as_ref()) + .use_config(toml::toml! { + [gcloud.region_aliases] + us-central1 = "uc1" + }) + .output()?; + let expected = format!("on {} ", Color::Blue.bold().paint("☁️ foo@example.com(uc1)")); + let actual = String::from_utf8(output.stdout).unwrap(); + assert_eq!(actual, expected); + dir.close() +} + +#[test] +fn active_set() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let active_config_path = dir.path().join("active_config"); + let mut active_config_file = File::create(&active_config_path)?; + active_config_file.write_all(b"default1")?; + + let output = common::render_module("gcloud") + .env("CLOUDSDK_CONFIG", dir.path().to_string_lossy().as_ref()) + .use_config(toml::toml! { + [gcloud] + format = "on [$symbol$active]($style) " + }) + .output()?; + let expected = format!("on {} ", Color::Blue.bold().paint("☁️ default1")); + let actual = String::from_utf8(output.stdout).unwrap(); + assert_eq!(actual, expected); + dir.close() +} + +#[test] +fn project_set() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let active_config_path = dir.path().join("active_config"); + let mut active_config_file = File::create(&active_config_path)?; + active_config_file.write_all(b"default")?; + + create_dir(dir.path().join("configurations"))?; + let config_default_path = dir.path().join("configurations/config_default"); + let mut config_default_file = File::create(&config_default_path)?; + config_default_file.write_all( + b"[core] +project = abc +", + )?; + + let output = common::render_module("gcloud") + .env("CLOUDSDK_CONFIG", dir.path().to_string_lossy().as_ref()) + .use_config(toml::toml! { + [gcloud] + format = "on [$symbol$project]($style) " + }) + .output()?; + let expected = format!("on {} ", Color::Blue.bold().paint("☁️ abc")); + let actual = String::from_utf8(output.stdout).unwrap(); + assert_eq!(actual, expected); + dir.close() +} + +#[test] +fn region_not_set_with_display_region() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let output = common::render_module("gcloud") + .env("CLOUDSDK_CONFIG", dir.path().to_string_lossy().as_ref()) + .use_config(toml::toml! { + [gcloud] + format = "on [$symbol$region]($style) " + }) + .output()?; + let expected = ""; + let actual = String::from_utf8(output.stdout).unwrap(); + assert_eq!(expected, actual); + dir.close() +} diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index f6c73f9f..6c59fa68 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -7,6 +7,7 @@ mod configuration; mod directory; mod dotnet; mod env_var; +mod gcloud; mod git_branch; mod git_commit; mod git_state;