feat: Add pulumi module (#3055)

This commit is contained in:
Ian Wahbe 2021-10-05 16:27:25 -07:00 committed by GitHub
parent 190743e4e0
commit dfb1208787
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 489 additions and 7 deletions

69
Cargo.lock generated
View File

@ -160,7 +160,16 @@ dependencies = [
"block-padding",
"byte-tools",
"byteorder",
"generic-array",
"generic-array 0.12.4",
]
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array 0.14.4",
]
[[package]]
@ -301,6 +310,15 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
[[package]]
name = "cpufeatures"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.1"
@ -368,7 +386,16 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
dependencies = [
"generic-array",
"generic-array 0.12.4",
]
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array 0.14.4",
]
[[package]]
@ -630,6 +657,16 @@ dependencies = [
"typenum",
]
[[package]]
name = "generic-array"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "gethostname"
version = "0.2.1"
@ -1079,6 +1116,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "open"
version = "2.0.1"
@ -1217,7 +1260,7 @@ checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
dependencies = [
"maplit",
"pest",
"sha-1",
"sha-1 0.8.2",
]
[[package]]
@ -1627,10 +1670,23 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
dependencies = [
"block-buffer",
"digest",
"block-buffer 0.7.3",
"digest 0.8.1",
"fake-simd",
"opaque-debug",
"opaque-debug 0.2.3",
]
[[package]]
name = "sha-1"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6"
dependencies = [
"block-buffer 0.9.0",
"cfg-if 1.0.0",
"cpufeatures",
"digest 0.9.0",
"opaque-debug 0.3.0",
]
[[package]]
@ -1699,6 +1755,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"sha-1 0.9.8",
"shadow-rs",
"shell-words",
"starship_module_config_derive",

View File

@ -69,6 +69,7 @@ which = "4.2.2"
shadow-rs = "0.7.1"
versions = "3.0.3"
strsim = "0.10.0"
sha-1 = "0.9.8"
process_control = { version = "3.1.0", features = ["crossbeam-channel"] }

View File

@ -221,6 +221,7 @@ $nodejs\
$ocaml\
$perl\
$php\
$pulumi\
$purescript\
$python\
$rlang\
@ -2349,6 +2350,64 @@ By default the module will be shown if any of the following conditions are met:
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.
::: tip
By default the Pulumi version is not shown, since it takes an order of magnitude longer to load then most plugins (~70ms).
If you still want to enable it, [follow the example shown below](#with-pulumi-version).
:::
By default the module will be shown if any of the following conditions are met:
- The current directory contains either `Pulumi.yaml` or `Pulumi.yml`
- A parent directory contains either `Pulumi.yaml` or `Pulumi.yml`
### 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. |
### Variables
| Variable | Example | Description |
| -------- | ---------- | ------------------------------------ |
| version | `v0.12.24` | The version of `pulumi` |
| stack | `dev` | The current Pulumi stack |
| 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
### Example
#### With Pulumi Version
```toml
# ~/.config/starship.toml
[pulumi]
format = "[🛥 ($version )$stack]($style) "
```
#### Without Pulumi version
```toml
# ~/.config/starship.toml
[pulumi]
symbol = "🛥 "
format = "[$symbol$stack]($style) "
```
## PureScript
The `purescript` module shows the currently installed version of [PureScript](https://www.purescript.org/) version.

View File

@ -205,6 +205,9 @@ format = '\[[$symbol($version)]($style)\]'
[php]
format = '\[[$symbol($version)]($style)\]'
[pulumi]
format = '\[[$symbol$stack]($style)\]'
[purescript]
format = '\[[$symbol($version)]($style)\]'
@ -354,6 +357,9 @@ symbol = "pl "
[php]
symbol = "php "
[pulumi]
symbol = "pulumi "
[purescript]
symbol = "purs "
@ -439,6 +445,9 @@ format = 'via [$symbol]($style)'
[php]
format = 'via [$symbol]($style)'
[pulumi]
format = 'via [$symbol$stack]($style)'
[purescript]
format = 'via [$symbol]($style)'

View File

@ -48,6 +48,7 @@ pub mod openstack;
pub mod package;
pub mod perl;
pub mod php;
pub mod pulumi;
pub mod purescript;
pub mod python;
pub mod red;
@ -125,6 +126,7 @@ pub struct FullConfig<'a> {
package: package::PackageConfig<'a>,
perl: perl::PerlConfig<'a>,
php: php::PhpConfig<'a>,
pulumi: pulumi::PulumiConfig<'a>,
purescript: purescript::PureScriptConfig<'a>,
python: python::PythonConfig<'a>,
red: red::RedConfig<'a>,
@ -200,6 +202,7 @@ impl<'a> Default for FullConfig<'a> {
package: Default::default(),
perl: Default::default(),
php: Default::default(),
pulumi: Default::default(),
purescript: Default::default(),
python: Default::default(),
red: Default::default(),

25
src/configs/pulumi.rs Normal file
View File

@ -0,0 +1,25 @@
use crate::config::ModuleConfig;
use serde::Serialize;
use starship_module_config_derive::ModuleConfig;
#[derive(Clone, ModuleConfig, Serialize)]
pub struct PulumiConfig<'a> {
pub format: &'a str,
pub version_format: &'a str,
pub symbol: &'a str,
pub style: &'a str,
pub disabled: bool,
}
impl<'a> Default for PulumiConfig<'a> {
fn default() -> Self {
PulumiConfig {
format: "via [$symbol$stack]($style) ",
version_format: "v${raw}",
symbol: "",
style: "bold 5",
disabled: false,
}
}
}

View File

@ -53,6 +53,7 @@ pub const PROMPT_ORDER: &[&str] = &[
"ocaml",
"perl",
"php",
"pulumi",
"purescript",
"python",
"rlang",

View File

@ -53,6 +53,7 @@ pub const ALL_MODULES: &[&str] = &[
"package",
"perl",
"php",
"pulumi",
"purescript",
"python",
"red",

View File

@ -43,6 +43,7 @@ mod openstack;
mod package;
mod perl;
mod php;
mod pulumi;
mod purescript;
mod python;
mod red;
@ -125,6 +126,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option<Module<'a>> {
"package" => package::module(context),
"perl" => perl::module(context),
"php" => php::module(context),
"pulumi" => pulumi::module(context),
"purescript" => purescript::module(context),
"python" => python::module(context),
"rlang" => rlang::module(context),
@ -212,6 +214,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",
"purescript" => "The currently installed version of PureScript",
"python" => "The currently installed version of Python",
"red" => "The currently installed version of Red",

296
src/modules/pulumi.rs Normal file
View File

@ -0,0 +1,296 @@
#![warn(missing_docs)]
use sha1::{Digest, Sha1};
use std::ffi::OsStr;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use yaml_rust::{Yaml, YamlLoader};
use super::{Context, Module, RootModuleConfig};
use crate::configs::pulumi::PulumiConfig;
use crate::formatter::{StringFormatter, VersionFormatter};
static PULUMI_HOME: &str = "PULUMI_HOME";
/// Creates a module with the current Pulumi version and stack name.
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let mut module = context.new_module("pulumi");
let config = PulumiConfig::try_load(module.config);
let project_file = find_package_file(&context.logical_dir)?;
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 {
"version" => {
let stdout = context.exec_cmd("pulumi", &["version"])?.stdout;
VersionFormatter::format_module_version(
module.get_name(),
parse_version(&stdout),
config.version_format,
)
}
.map(Ok),
"stack" => stack_name(&project_file, context).map(Ok),
_ => None,
})
.parse(None)
});
match parsed {
Ok(x) => {
module.set_segments(x);
Some(module)
}
Err(e) => {
log::warn!("Error in module `pulumi`:\n{}", e);
None
}
}
}
/// Parse the output of `pulumi version` into just the version string.
///
/// Normally, this just means returning it. When Pulumi is being developed, it
/// can return results like `3.12.0-alpha.1630554544+f89e9a29.dirty`, which we
/// don't want to see. Instead we display that as `3.12.0-alpha`.
fn parse_version(version: &str) -> &str {
let mut periods = 0;
for (i, c) in version.as_bytes().iter().enumerate() {
if *c == b'.' {
if periods == 2 {
return &version[0..i];
} else {
periods += 1;
}
}
}
// We didn't hit 3 periods, so we just return the whole string.
version
}
/// Find a file describing a Pulumi package in the current directory (or any parrent directory).
fn find_package_file(path: &Path) -> Option<PathBuf> {
for path in path.ancestors() {
log::trace!("Looking for package file in {:?}", path);
let dir = std::fs::read_dir(path).ok()?;
let goal = dir.filter_map(Result::ok).find(|path| {
path.file_name() == OsStr::new("Pulumi.yaml")
|| path.file_name() == OsStr::new("Pulumi.yml")
});
if let Some(goal) = goal {
return Some(goal.path());
}
}
log::trace!("Did not find a Pulumi package file");
None
}
/// We get the name of the current stack.
///
/// Pulumi has no CLI option that is fast enough to get this for us, but finding
/// the location is simple. We get it ourselves.
fn stack_name(project_file: &Path, context: &Context) -> Option<String> {
let mut file = File::open(&project_file).ok()?;
let mut contents = String::new();
file.read_to_string(&mut contents).ok()?;
let name = YamlLoader::load_from_str(&contents).ok().and_then(
|yaml| -> Option<Option<String>> {
log::trace!("Parsed {:?} into yaml", project_file);
let yaml = yaml.into_iter().next()?;
yaml.into_hash().map(|mut hash| -> Option<String> {
hash.remove(&Yaml::String("name".to_string()))?
.into_string()
})
},
)??;
log::trace!("Found project name: {:?}", name);
let workspace_file = get_pulumi_workspace(context, &name, project_file)
.map(File::open)?
.ok()?;
log::trace!("Trying to read workspace_file: {:?}", workspace_file);
let workspace: serde_json::Value = match serde_json::from_reader(workspace_file) {
Ok(k) => k,
Err(e) => {
log::debug!("Failed to parse workspace file: {}", e);
return None;
}
};
log::trace!("Read workspace_file: {:?}", workspace);
workspace
.as_object()?
.get("stack")?
.as_str()
.map(ToString::to_string)
}
/// Calculates the path of the workspace settings file for a given pulumi stack.
fn get_pulumi_workspace(context: &Context, name: &str, project_file: &Path) -> Option<PathBuf> {
let project_file = if cfg!(test) {
// Because this depends on the absolute path of the file, it changes in
// each test run. We thus mock it.
"test".to_string()
} else {
let mut hasher = Sha1::new();
hasher.update(project_file.to_str()?.as_bytes());
crate::utils::encode_to_hex(&hasher.finalize().to_vec())
};
let unique_file_name = format!("{}-{}-workspace.json", name, project_file);
let mut path = pulumi_home_dir(context)?;
path.push("workspaces");
path.push(unique_file_name);
Some(path)
}
/// Get the Pulumi home directory. We first check `PULUMI_HOME`. If that isn't
/// set, we return `$HOME/.pulumi`.
fn pulumi_home_dir(context: &Context) -> Option<PathBuf> {
if let Some(k) = context.get_env(PULUMI_HOME) {
std::path::PathBuf::from_str(&k).ok()
} else {
context.get_home().map(|p| p.join(".pulumi"))
}
}
#[cfg(test)]
mod tests {
use std::io;
use super::*;
use crate::test::ModuleRenderer;
use ansi_term::Color;
use clap::ArgMatches;
#[test]
fn pulumi_version_release() {
let input = "3.12.0";
assert_eq!(parse_version(input), input);
}
#[test]
fn pulumi_version_prerelease() {
let input = "3.12.0-alpha";
assert_eq!(parse_version(input), input);
}
#[test]
fn pulumi_version_dirty() {
let input = "3.12.0-alpha.1630554544+f89e9a29.dirty";
assert_eq!(parse_version(input), "3.12.0-alpha");
}
#[test]
fn get_home_dir() {
let mut context = Context::new(ArgMatches::default());
context.env.insert("HOME", "/home/sweet/home".to_string());
assert_eq!(
pulumi_home_dir(&context),
Some(PathBuf::from("/home/sweet/home/.pulumi"))
);
context.env.insert("PULUMI_HOME", "/a/dir".to_string());
assert_eq!(pulumi_home_dir(&context), Some(PathBuf::from("/a/dir")))
}
#[test]
fn test_get_pulumi_workspace() {
let mut context = Context::new(ArgMatches::default());
context.env.insert("HOME", "/home/sweet/home".to_string());
let name = "foobar";
let project_file = PathBuf::from("/hello/Pulumi.yaml");
assert_eq!(
get_pulumi_workspace(&context, name, &project_file),
Some("/home/sweet/home/.pulumi/workspaces/foobar-test-workspace.json")
.map(PathBuf::from)
);
}
#[test]
fn version_render() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let pulumi_file = File::create(dir.path().join("Pulumi.yaml"))?;
pulumi_file.sync_all()?;
let rendered = ModuleRenderer::new("pulumi")
.path(dir.path())
.config(toml::toml! {
[pulumi]
format = "with [$version]($style) "
})
.collect();
dir.close()?;
let expected = format!("with {} ", Color::Fixed(5).bold().paint("v1.2.3-ver"));
assert_eq!(expected, rendered.expect("a result"));
Ok(())
}
#[test]
/// This test confirms a full render. This means finding a Pulumi.yml file,
/// tracing back to the backing workspace settings file, and printing the
/// stack name.
fn render_valid_paths() -> 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 rendered = ModuleRenderer::new("pulumi")
.path(root.clone())
.logical_path(root.clone())
.config(toml::toml! {
[pulumi]
format = "in [$symbol($stack)]($style) "
})
.env("HOME", root.to_str().unwrap())
.collect();
let expected = format!("in {} ", Color::Fixed(5).bold().paint(" launch"));
assert_eq!(expected, rendered.expect("a result"));
dir.close()?;
Ok(())
}
#[test]
fn empty_config_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let yaml = File::create(dir.path().join("Pulumi.yaml"))?;
yaml.sync_all()?;
let rendered = ModuleRenderer::new("pulumi")
.path(dir.path())
.logical_path(dir.path())
.config(toml::toml! {
[pulumi]
format = "in [$symbol($stack)]($style) "
})
.collect();
let expected = format!("in {} ", Color::Fixed(5).bold().paint(""));
assert_eq!(expected, rendered.expect("a result"));
dir.close()?;
Ok(())
}
}

View File

@ -224,7 +224,11 @@ active boot switches: -d:release\n",
stdout: String::from("7.3.8"),
stderr: String::default(),
})
}
},
"pulumi version" => Some(CommandOutput{
stdout: String::from("1.2.3-ver.1631311768+e696fb6c"),
stderr: String::default(),
}),
"purs --version" => Some(CommandOutput {
stdout: String::from("0.13.5\n"),
stderr: String::default(),
@ -499,6 +503,21 @@ pub fn home_dir() -> Option<PathBuf> {
directories_next::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_owned())
}
const HEXTABLE: &[char] = &[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
];
/// Encode a u8 slice into a hexadecimal string.
pub fn encode_to_hex(slice: &[u8]) -> String {
// let mut j = 0;
let mut dst = Vec::with_capacity(slice.len() * 2);
for &v in slice {
dst.push(HEXTABLE[(v >> 4) as usize] as u8);
dst.push(HEXTABLE[(v & 0x0f) as usize] as u8);
}
String::from_utf8(dst).unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
@ -709,4 +728,12 @@ mod tests {
};
assert_eq!(get_command_string_output(case2), "stderr");
}
#[test]
fn sha1_hex() {
assert_eq!(
encode_to_hex(&[8, 13, 9, 189, 129, 94]),
"080d09bd815e".to_string()
);
}
}