diff --git a/docs/config/README.md b/docs/config/README.md index e65a28df..168c76c0 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -2424,7 +2424,7 @@ format = "via [🔹 $version](147 bold) " ## Pulumi -The `pulumi` module shows the currently selected [Pulumi Stack](https://www.pulumi.com/docs/intro/concepts/stack/) and version. +The `pulumi` module shows the current username, selected [Pulumi Stack](https://www.pulumi.com/docs/intro/concepts/stack/), and version. ::: tip @@ -2440,13 +2440,13 @@ By default the module will be shown if any of the following conditions are met: ### Options -| Option | Default | Description | -| ---------------- | -------------------------------- | ------------------------------------------------------------------------- | -| `format` | `"via [$symbol$stack]($style) "` | The format string for the module. | -| `version_format` | `"v${raw}"` | The version format. Available vars are `raw`, `major`, `minor`, & `patch` | -| `symbol` | `" "` | A format string shown before the Pulumi stack. | -| `style` | `"bold 5"` | The style for the module. | -| `disabled` | `false` | Disables the `pulumi` module. | +| Option | Default | Description | +| ---------------- | -------------------------------------------- | ------------------------------------------------------------------------- | +| `format` | `"via [$symbol($username@)$stack]($style) "` | The format string for the module. | +| `version_format` | `"v${raw}"` | The version format. Available vars are `raw`, `major`, `minor`, & `patch` | +| `symbol` | `" "` | A format string shown before the Pulumi stack. | +| `style` | `"bold 5"` | The style for the module. | +| `disabled` | `false` | Disables the `pulumi` module. | ### Variables @@ -2454,6 +2454,7 @@ By default the module will be shown if any of the following conditions are met: | -------- | ---------- | ------------------------------------ | | version | `v0.12.24` | The version of `pulumi` | | stack | `dev` | The current Pulumi stack | +| username | `alice` | The current Pulumi username | | symbol | | Mirrors the value of option `symbol` | | style\* | | Mirrors the value of option `style` | diff --git a/src/configs/pulumi.rs b/src/configs/pulumi.rs index 5a796dd7..2941bc1c 100644 --- a/src/configs/pulumi.rs +++ b/src/configs/pulumi.rs @@ -15,7 +15,7 @@ pub struct PulumiConfig<'a> { impl<'a> Default for PulumiConfig<'a> { fn default() -> Self { PulumiConfig { - format: "via [$symbol$stack]($style) ", + format: "via [$symbol($username@)$stack]($style) ", version_format: "v${raw}", symbol: " ", style: "bold 5", diff --git a/src/modules/mod.rs b/src/modules/mod.rs index fa79f449..91333ccb 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -222,7 +222,7 @@ pub fn description(module: &str) -> &'static str { "package" => "The package version of the current directory's project", "perl" => "The currently installed version of Perl", "php" => "The currently installed version of PHP", - "pulumi" => "The current stack and installed version of Pulumi", + "pulumi" => "The current username, stack, and installed version of Pulumi", "purescript" => "The currently installed version of PureScript", "python" => "The currently installed version of Python", "red" => "The currently installed version of Red", diff --git a/src/modules/pulumi.rs b/src/modules/pulumi.rs index 7d07e87b..fdd6dd5f 100644 --- a/src/modules/pulumi.rs +++ b/src/modules/pulumi.rs @@ -1,8 +1,10 @@ #![warn(missing_docs)] +use serde::Deserialize; use sha1::{Digest, Sha1}; +use std::collections::HashMap; use std::ffi::OsStr; use std::fs::File; -use std::io::Read; +use std::io::{BufReader, Read}; use std::path::{Path, PathBuf}; use std::str::FromStr; use yaml_rust::{Yaml, YamlLoader}; @@ -13,6 +15,17 @@ use crate::formatter::{StringFormatter, VersionFormatter}; static PULUMI_HOME: &str = "PULUMI_HOME"; +#[derive(Deserialize)] +struct Credentials { + current: Option, + accounts: Option>, +} + +#[derive(Deserialize)] +struct Account { + username: Option, +} + /// Creates a module with the current Pulumi version and stack name. pub fn module<'a>(context: &'a Context) -> Option> { let mut module = context.new_module("pulumi"); @@ -40,6 +53,7 @@ pub fn module<'a>(context: &'a Context) -> Option> { ) } .map(Ok), + "username" => get_pulumi_username(context).map(Ok), "stack" => stack_name(&project_file, context).map(Ok), _ => None, }) @@ -163,6 +177,21 @@ fn pulumi_home_dir(context: &Context) -> Option { } } +fn get_pulumi_username(context: &Context) -> Option { + let home_dir = pulumi_home_dir(context)?; + let creds_path = home_dir.join("credentials.json"); + + let file = File::open(creds_path).ok()?; + let reader = BufReader::new(file); + + // Read the JSON contents of the file as an instance of `User`. + let creds: Credentials = serde_json::from_reader(reader).ok()?; + + let current_api_provider = creds.current?; + + creds.accounts?.remove(¤t_api_provider)?.username +} + #[cfg(test)] mod tests { use std::io; @@ -259,16 +288,103 @@ mod tests { ), )?; workspace.sync_all()?; + + let credential_path = root.join(".pulumi"); + let _ = std::fs::create_dir_all(&credential_path)?; + let credential_path = &credential_path.join("credentials.json"); + let mut credential = File::create(&credential_path)?; + serde_json::to_writer_pretty( + &mut credential, + &serde_json::json!( + { + "current": "https://api.example.com", + "accessTokens": { + "https://api.example.com": "redacted", + "https://api.pulumi.com": "redacted" + }, + "accounts": { + "https://api.example.com": { + "accessToken": "redacted", + "username": "test-user", + "lastValidatedAt": "2022-01-12T00:00:00.000000000-08:00" + } + } + } + ), + )?; + credential.sync_all()?; let rendered = ModuleRenderer::new("pulumi") .path(root.clone()) .logical_path(root.clone()) .config(toml::toml! { [pulumi] - format = "in [$symbol($stack)]($style) " + format = "via [$symbol($username@)$stack]($style) " }) .env("HOME", root.to_str().unwrap()) .collect(); - let expected = format!("in {} ", Color::Fixed(5).bold().paint(" launch")); + let expected = format!( + "via {} ", + Color::Fixed(5).bold().paint(" test-user@launch") + ); + assert_eq!(expected, rendered.expect("a result")); + dir.close()?; + Ok(()) + } + + #[test] + /// This test confirms a render when the account information incomplete, i.e.: no username for + /// the current API. + fn partial_login() -> io::Result<()> { + use io::Write; + let dir = tempfile::tempdir()?; + let root = std::fs::canonicalize(dir.path())?; + let mut yaml = File::create(root.join("Pulumi.yml"))?; + yaml.write_all("name: starship\nruntime: nodejs\ndescription: A thing\n".as_bytes())?; + yaml.sync_all()?; + + let workspace_path = root.join(".pulumi").join("workspaces"); + let _ = std::fs::create_dir_all(&workspace_path)?; + let workspace_path = &workspace_path.join("starship-test-workspace.json"); + let mut workspace = File::create(&workspace_path)?; + serde_json::to_writer_pretty( + &mut workspace, + &serde_json::json!( + { + "stack": "launch" + } + ), + )?; + workspace.sync_all()?; + + let credential_path = root.join(".pulumi"); + let _ = std::fs::create_dir_all(&credential_path)?; + let credential_path = &credential_path.join("starship-test-credential.json"); + let mut credential = File::create(&credential_path)?; + serde_json::to_writer_pretty( + &mut credential, + &serde_json::json!( + { + "current": "https://api.example.com", + "accessTokens": { + "https://api.example.com": "redacted", + "https://api.pulumi.com": "redacted" + }, + "accounts": { + } + } + ), + )?; + credential.sync_all()?; + let rendered = ModuleRenderer::new("pulumi") + .path(root.clone()) + .logical_path(root.clone()) + .config(toml::toml! { + [pulumi] + format = "via [$symbol($username@)$stack]($style) " + }) + .env("HOME", root.to_str().unwrap()) + .collect(); + let expected = format!("via {} ", Color::Fixed(5).bold().paint(" launch")); assert_eq!(expected, rendered.expect("a result")); dir.close()?; Ok(())