diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 7d3f2153..a3837de6 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -139,6 +139,11 @@ jobs: - name: Install Mercurial (windows) if: matrix.os == 'windows-latest' run: choco install hg + + # Install Terraform at a fixed version + - uses: volcano-coffee-company/setup-terraform@v1 + with: + version: "0.12.14" # Run the ignored tests that expect the above setup - name: Run all tests diff --git a/docs/config/README.md b/docs/config/README.md index f9687e97..176e3c51 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -103,6 +103,7 @@ prompt_order = [ "python", "ruby", "rust", + "terraform", "nix_shell", "conda", "memory_usage", @@ -989,6 +990,33 @@ The module will be shown if any of the following conditions are met: symbol = "⚙️ " ``` +## Terraform + +The `terraform` module shows the currently selected terraform workspace and version. +By default the terraform version is not shown, since this is slow on current versions of terraform when a lot of plugins are in use. +The module will be shown if any of the following conditions are met: + +- The current directory contains a `.terraform` folder +- Current directory contains a file with the `.tf` extension + +### Options + +| Variable | Default | Description | +| -------------- | ------------- | ----------------------------------------------------------- | +| `symbol` | `"💠 "` | The symbol used before displaying the terraform workspace. | +| `style` | `"bold 105"` | The style for the module. | +| `disabled` | `false` | Disables the `terraform` module. | +| `show_version` | `false` | Shows the terraform version. Very slow on large workspaces. | + +### Example + +```toml +# ~/.config/starship.toml + +[terraform] +symbol = "🏎💨 " +``` + ## Time The `time` module shows the current **local** time. diff --git a/src/configs/mod.rs b/src/configs/mod.rs index c840f3bb..dcf0337f 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -25,6 +25,7 @@ pub mod python; pub mod ruby; pub mod rust; mod starship_root; +pub mod terraform; pub mod time; pub mod username; diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index 293dcb17..00a64735 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -36,6 +36,7 @@ impl<'a> RootModuleConfig<'a> for StarshipRootConfig<'a> { "python", "ruby", "rust", + "terraform", // ↑ Toolchain version modules ↑ "nix_shell", "conda", diff --git a/src/configs/terraform.rs b/src/configs/terraform.rs new file mode 100644 index 00000000..3d427e5c --- /dev/null +++ b/src/configs/terraform.rs @@ -0,0 +1,27 @@ +use crate::config::{ModuleConfig, RootModuleConfig, SegmentConfig}; + +use ansi_term::{Color, Style}; +use starship_module_config_derive::ModuleConfig; + +#[derive(Clone, ModuleConfig)] +pub struct TerraformConfig<'a> { + pub symbol: SegmentConfig<'a>, + pub workspace: SegmentConfig<'a>, + pub version: SegmentConfig<'a>, + pub style: Style, + pub disabled: bool, + pub show_version: bool, +} + +impl<'a> RootModuleConfig<'a> for TerraformConfig<'a> { + fn new() -> Self { + TerraformConfig { + symbol: SegmentConfig::new("💠 "), + workspace: SegmentConfig::default(), + version: SegmentConfig::default(), + style: Color::Fixed(105).bold(), + disabled: false, + show_version: false, + } + } +} diff --git a/src/module.rs b/src/module.rs index 71b25627..873c2e1a 100644 --- a/src/module.rs +++ b/src/module.rs @@ -36,6 +36,7 @@ pub const ALL_MODULES: &[&str] = &[ "ruby", "rust", "php", + "terraform", "time", "username", ]; diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 42951324..a95ea4fd 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -25,6 +25,7 @@ mod php; mod python; mod ruby; mod rust; +mod terraform; mod time; mod username; mod utils; @@ -68,6 +69,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "python" => python::module(context), "ruby" => ruby::module(context), "rust" => rust::module(context), + "terraform" => terraform::module(context), "time" => time::module(context), "username" => username::module(context), _ => { diff --git a/src/modules/terraform.rs b/src/modules/terraform.rs new file mode 100644 index 00000000..60d0b46c --- /dev/null +++ b/src/modules/terraform.rs @@ -0,0 +1,125 @@ +use super::{Context, Module, RootModuleConfig}; + +use crate::configs::terraform::TerraformConfig; +use crate::utils; +use std::env; +use std::io; +use std::path::PathBuf; + +/// Creates a module with the current Terraform version and workspace +/// +/// Will display the Terraform version and workspace if any of the following criteria are met: +/// - Current directory contains a `.terraform` directory +/// - Current directory contains a file with the `.tf` extension +pub fn module<'a>(context: &'a Context) -> Option> { + let is_terraform_project = context + .try_begin_scan()? + .set_folders(&[".terraform"]) + .set_extensions(&["tf"]) + .is_match(); + + if !is_terraform_project { + return None; + } + + let mut module = context.new_module("terraform"); + let config: TerraformConfig = TerraformConfig::try_load(module.config); + + module.set_style(config.style); + module.create_segment("symbol", &config.symbol); + + if config.show_version { + let terraform_version = + format_terraform_version(&utils::exec_cmd("terraform", &["version"])?.stdout.as_str())?; + module.create_segment("version", &config.version.with_value(&terraform_version)); + } + + let terraform_workspace = &get_terraform_workspace(&context.current_dir)?; + module.create_segment( + "workspace", + &config.workspace.with_value(&terraform_workspace), + ); + + Some(module) +} + +// Determines the currently selected workspace (see https://github.com/hashicorp/terraform/blob/master/command/meta.go for the original implementation) +fn get_terraform_workspace(cwd: &PathBuf) -> Option { + // Workspace can be explicitly overwritten by an env var + let workspace_override = env::var("TF_WORKSPACE"); + if workspace_override.is_ok() { + return workspace_override.ok(); + } + + // Data directory containing current workspace can be overwritten by an env var + let datadir = match env::var("TF_DATA_DIR") { + Ok(s) => PathBuf::from(s), + Err(_) => cwd.join(".terraform"), + }; + match utils::read_file(datadir.join("environment")) { + Err(ref e) if e.kind() == io::ErrorKind::NotFound => Some("default".to_string()), + Ok(s) => Some(s), + _ => None, + } +} + +fn format_terraform_version(version: &str) -> Option { + // `terraform version` output looks like this + // Terraform v0.12.14 + // With potential extra output if it detects you are not running the latest version + Some( + version + .lines() + .next()? + .trim_start_matches("Terraform ") + .trim() + .to_owned() + + " ", + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_terraform_version_release() { + let input = "Terraform v0.12.14"; + assert_eq!( + format_terraform_version(input), + Some("v0.12.14 ".to_string()) + ); + } + + #[test] + fn test_format_terraform_version_prerelease() { + let input = "Terraform v0.12.14-rc1"; + assert_eq!( + format_terraform_version(input), + Some("v0.12.14-rc1 ".to_string()) + ); + } + + #[test] + fn test_format_terraform_version_development() { + let input = "Terraform v0.12.14-dev (cca89f74)"; + assert_eq!( + format_terraform_version(input), + Some("v0.12.14-dev (cca89f74) ".to_string()) + ); + } + + #[test] + fn test_format_terraform_version_multiline() { + let input = "Terraform v0.12.13 + +Your version of Terraform is out of date! The latest version +is 0.12.14. You can update by downloading from www.terraform.io/downloads.html + +"; + assert_eq!( + format_terraform_version(input), + Some("v0.12.13 ".to_string()) + ); + } +} diff --git a/tests/Dockerfile b/tests/Dockerfile index 12372e24..8043d9dc 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -77,6 +77,18 @@ RUN php --version RUN HGPYTHON3=1 pip install mercurial # Check that Mercurial was correctly installed RUN hg --version +# Install Terraform +ENV TERRAFORM_HOME /home/nonroot/terraform +ENV TERRAFORM_VERSION 0.12.14 +ENV PATH ${TERRAFORM_HOME}:${PATH} + +RUN mkdir -p ${TERRAFORM_HOME} \ + && terraform_download="${TERRAFORM_HOME}/terraform.zip" \ + && curl -SL --output "${terraform_download}" "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" \ + && unzip "${terraform_download}" -d "${TERRAFORM_HOME}" \ + && rm "${terraform_download}" +# Check that terraform was correctly installed +RUN terraform version # Create blank project RUN USER=nonroot cargo new --bin /src/starship diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 214f8b01..f8192f41 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -21,5 +21,6 @@ mod nix_shell; mod nodejs; mod python; mod ruby; +mod terraform; mod time; mod username; diff --git a/tests/testsuite/terraform.rs b/tests/testsuite/terraform.rs new file mode 100644 index 00000000..ef6d16b5 --- /dev/null +++ b/tests/testsuite/terraform.rs @@ -0,0 +1,174 @@ +use ansi_term::Color; +use std::fs::{self, File}; +use std::io::{self, Write}; +use tempfile; + +use crate::common; +use crate::common::TestCommand; + +#[test] +fn folder_without_dotterraform() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let output = common::render_module("terraform") + .arg("--path") + .arg(dir.path()) + .output()?; + let actual = String::from_utf8(output.stdout).unwrap(); + + let expected = ""; + assert_eq!(expected, actual); + Ok(()) +} + +#[test] +#[ignore] +fn folder_with_tf_file() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("main.tf"))?; + + let output = common::render_module("terraform") + .arg("--path") + .arg(dir.path()) + .output()?; + let actual = String::from_utf8(output.stdout).unwrap(); + + let expected = format!("via {} ", Color::Fixed(105).bold().paint("💠 default")); + assert_eq!(expected, actual); + Ok(()) +} + +#[test] +#[ignore] +fn folder_with_workspace_override() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("main.tf"))?; + + let output = common::render_module("terraform") + .arg("--path") + .arg(dir.path()) + .env_clear() + .env("TF_WORKSPACE", "development") + .output()?; + let actual = String::from_utf8(output.stdout).unwrap(); + + let expected = format!("via {} ", Color::Fixed(105).bold().paint("💠 development")); + assert_eq!(expected, actual); + Ok(()) +} + +#[test] +#[ignore] +fn folder_with_datadir_override() -> io::Result<()> { + let dir = tempfile::tempdir()?; + File::create(dir.path().join("main.tf"))?; + + let datadir = tempfile::tempdir()?; + let mut file = File::create(datadir.path().join("environment"))?; + file.write_all(b"development")?; + file.sync_all()?; + + let output = common::render_module("terraform") + .arg("--path") + .arg(dir.path()) + .env_clear() + .env("TF_DATA_DIR", datadir.path()) + .output()?; + let actual = String::from_utf8(output.stdout).unwrap(); + + let expected = format!("via {} ", Color::Fixed(105).bold().paint("💠 development")); + assert_eq!(expected, actual); + Ok(()) +} + +#[test] +#[ignore] +fn folder_with_dotterraform_no_environment() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let tf_dir = dir.path().join(".terraform"); + fs::create_dir(&tf_dir)?; + + let output = common::render_module("terraform") + .arg("--path") + .arg(dir.path()) + .output()?; + let actual = String::from_utf8(output.stdout).unwrap(); + + let expected = format!("via {} ", Color::Fixed(105).bold().paint("💠 default")); + assert_eq!(expected, actual); + Ok(()) +} + +#[test] +#[ignore] +fn folder_with_dotterraform_with_environment() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let tf_dir = dir.path().join(".terraform"); + fs::create_dir(&tf_dir)?; + let mut file = File::create(tf_dir.join("environment"))?; + file.write_all(b"development")?; + file.sync_all()?; + + let output = common::render_module("terraform") + .arg("--path") + .arg(dir.path()) + .output()?; + let actual = String::from_utf8(output.stdout).unwrap(); + + let expected = format!("via {} ", Color::Fixed(105).bold().paint("💠 development")); + assert_eq!(expected, actual); + Ok(()) +} + +#[test] +#[ignore] +fn folder_with_dotterraform_with_version_no_environment() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let tf_dir = dir.path().join(".terraform"); + fs::create_dir(&tf_dir)?; + + let output = common::render_module("terraform") + .arg("--path") + .arg(dir.path()) + .use_config(toml::toml! { + [terraform] + show_version = true + }) + .output()?; + let actual = String::from_utf8(output.stdout).unwrap(); + + let expected = format!( + "via {} ", + Color::Fixed(105).bold().paint("💠 v0.12.14 default") + ); + assert_eq!(expected, actual); + Ok(()) +} + +#[test] +#[ignore] +fn folder_with_dotterraform_with_version_with_environment() -> io::Result<()> { + let dir = tempfile::tempdir()?; + let tf_dir = dir.path().join(".terraform"); + fs::create_dir(&tf_dir)?; + let mut file = File::create(tf_dir.join("environment"))?; + file.write_all(b"development")?; + file.sync_all()?; + + let output = common::render_module("terraform") + .arg("--path") + .arg(dir.path()) + .use_config(toml::toml! { + [terraform] + show_version = true + }) + .output()?; + let actual = String::from_utf8(output.stdout).unwrap(); + + let expected = format!( + "via {} ", + Color::Fixed(105).bold().paint("💠 v0.12.14 development") + ); + assert_eq!(expected, actual); + Ok(()) +}