From 073fbd398a787936ca1ef0fb3cfc2ef7b7f32db5 Mon Sep 17 00:00:00 2001 From: David Obee <35892395+david-obee@users.noreply.github.com> Date: Thu, 23 May 2024 19:12:48 +0100 Subject: [PATCH] Add support for the output command (#54) added Output wrapper --------- Co-authored-by: David Obee --- .../TerraformEnvListTests.cs | 1 - .../TerraformOutputTests.cs | 205 ++++++++++++++++++ src/Cake.Terraform.sln | 56 ++--- .../Output/TerraformOutputRunner.cs | 93 ++++++++ .../Output/TerraformOutputSettings.cs | 11 + src/Cake.Terraform/TerraformAliases.cs | 8 + 6 files changed, 345 insertions(+), 29 deletions(-) create mode 100644 src/Cake.Terraform.Tests/TerraformOutputTests.cs create mode 100644 src/Cake.Terraform/Output/TerraformOutputRunner.cs create mode 100644 src/Cake.Terraform/Output/TerraformOutputSettings.cs diff --git a/src/Cake.Terraform.Tests/TerraformEnvListTests.cs b/src/Cake.Terraform.Tests/TerraformEnvListTests.cs index abf19c7..972f910 100644 --- a/src/Cake.Terraform.Tests/TerraformEnvListTests.cs +++ b/src/Cake.Terraform.Tests/TerraformEnvListTests.cs @@ -1,7 +1,6 @@ using Cake.Core; using Cake.Terraform.EnvList; using Cake.Testing; -using Cake.Testing.Fixtures; using Xunit; namespace Cake.Terraform.Tests diff --git a/src/Cake.Terraform.Tests/TerraformOutputTests.cs b/src/Cake.Terraform.Tests/TerraformOutputTests.cs new file mode 100644 index 0000000..f7bf081 --- /dev/null +++ b/src/Cake.Terraform.Tests/TerraformOutputTests.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using Cake.Core; +using Cake.Terraform.Output; +using Cake.Testing; +using Xunit; + +namespace Cake.Terraform.Tests +{ + public class TerraformOutputTests + { + class Fixture : TerraformFixture + { + public Fixture(PlatformFamily platformFamily = PlatformFamily.Windows) : base(platformFamily) { } + + public List ToolOutput { get; set; } = new List { "foo = 123", "bar = abc" }; + public string Outputs { get; private set; } = null; + + protected override void RunTool() + { + ProcessRunner.Process.SetStandardOutput(ToolOutput); + + var tool = new TerraformOutputRunner(FileSystem, Environment, ProcessRunner, Tools); + + Outputs = tool.Run(Settings); + } + } + + public class TheExecutable + { + [Fact] + public void Should_throw_if_terraform_runner_was_not_found() + { + var fixture = new Fixture(); + fixture.GivenDefaultToolDoNotExist(); + + var result = Record.Exception(() => fixture.Run()); + + Assert.IsType(result); + Assert.Equal("Terraform: Could not locate executable.", result.Message); + } + + [Theory] + [InlineData("/bin/tools/terraform/terraform.exe", "/bin/tools/terraform/terraform.exe")] + [InlineData("/bin/tools/terraform/terraform", "/bin/tools/terraform/terraform")] + public void Should_use_terraform_from_tool_path_if_provided(string toolPath, string expected) + { + var fixture = new Fixture() {Settings = {ToolPath = toolPath}}; + fixture.GivenSettingsToolPathExist(); + + var result = fixture.Run(); + + Assert.Equal(expected, result.Path.FullPath); + } + + [Fact] + public void Should_find_terraform_if_tool_path_not_provided() + { + var fixture = new Fixture(); + + var result = fixture.Run(); + + Assert.Equal("/Working/tools/terraform.exe", result.Path.FullPath); + } + + [Fact] + public void Should_throw_if_process_has_a_non_zero_exit_code() + { + var fixture = new Fixture(); + fixture.GivenProcessExitsWithCode(1); + + var result = Record.Exception(() => fixture.Run()); + + Assert.IsType(result); + Assert.Equal("Terraform: Process returned an error (exit code 1).", result.Message); + } + + [Fact] + public void Should_find_linux_executable() + { + var fixture = new Fixture(PlatformFamily.Linux); + fixture.Environment.Platform.Family = PlatformFamily.Linux; + + var result = fixture.Run(); + + Assert.Equal("/Working/tools/terraform", result.Path.FullPath); + } + + [Fact] + public void Should_call_only_command_if_no_settings_specified() + { + var fixture = new Fixture(); + + var result = fixture.Run(); + + Assert.Equal("output -no-color", result.Args); + } + + [Fact] + public void Should_request_json_if_specified() + { + var fixture = new Fixture { Settings = new TerraformOutputSettings { Json = true } }; + + var result = fixture.Run(); + + Assert.Equal("output -no-color -json", result.Args); + } + + [Fact] + public void Should_request_raw_if_specified() + { + var fixture = new Fixture { Settings = new TerraformOutputSettings { Raw = true } }; + + var result = fixture.Run(); + + Assert.Equal("output -no-color -raw", result.Args); + } + + [Fact] + public void Should_request_raw_over_json_if_both_are_specified() + { + var fixture = new Fixture { Settings = new TerraformOutputSettings { Raw = true, Json = true} }; + + var result = fixture.Run(); + + Assert.Equal("output -no-color -raw", result.Args); + } + + [Fact] + public void Should_request_specific_state_path_if_specified() + { + var fixture = new Fixture { Settings = new TerraformOutputSettings { StatePath = "some_path"} }; + + var result = fixture.Run(); + + Assert.Equal("output -no-color -state=some_path", result.Args); + } + + [Fact] + public void Should_request_particular_output_if_specified() + { + var fixture = new Fixture { Settings = new TerraformOutputSettings { OutputName = "some_output"} }; + + var result = fixture.Run(); + + Assert.Equal("output -no-color some_output", result.Args); + } + + [Fact] + public void Should_combine_output_lines_into_string() + { + var fixture = new Fixture(); + + fixture.Run(); + + Assert.Equal("foo = 123\nbar = abc", fixture.Outputs); + } + + [Fact] + public void Should_raise_exception_if_output_not_found() + { + var fixture = new Fixture + { + ToolOutput = new List + { + "The output variable requested could not be found in the state", + "file. If you recently added this to your configuration, be", + "sure to run `terraform apply`, since the state won't be updated", + "with new output variables until that command is run." + } + }; + + var exception = Assert.Throws(() => fixture.Run()); + Assert.Equal( + "The output variable requested could not be found in the state\nfile. If you recently added this to your configuration, be\nsure to run `terraform apply`, since the state won't be updated\nwith new output variables until that command is run.", + exception.Message); + } + + [Fact] + public void Should_not_raise_exception_if_output_not_found_and_validation_disabled() + { + var fixture = new Fixture + { + ToolOutput = new List + { + "The output variable requested could not be found in the state", + "file. If you recently added this to your configuration, be", + "sure to run `terraform apply`, since the state won't be updated", + "with new output variables until that command is run." + }, + Settings = new TerraformOutputSettings + { + ValidateToolOutput = false + } + }; + + fixture.Run(); + + Assert.Equal( + "The output variable requested could not be found in the state\nfile. If you recently added this to your configuration, be\nsure to run `terraform apply`, since the state won't be updated\nwith new output variables until that command is run.", + fixture.Outputs); + } + } + } +} \ No newline at end of file diff --git a/src/Cake.Terraform.sln b/src/Cake.Terraform.sln index f38f01f..dc1bbf6 100644 --- a/src/Cake.Terraform.sln +++ b/src/Cake.Terraform.sln @@ -1,28 +1,28 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26228.12 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cake.Terraform", "Cake.Terraform\Cake.Terraform.csproj", "{C6E9A60F-9055-4E54-9E29-7F277875ABC1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cake.Terraform.Tests", "Cake.Terraform.Tests\Cake.Terraform.Tests.csproj", "{AB9EDCDE-48B6-4B6E-B0B6-28CF549A6591}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C6E9A60F-9055-4E54-9E29-7F277875ABC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C6E9A60F-9055-4E54-9E29-7F277875ABC1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C6E9A60F-9055-4E54-9E29-7F277875ABC1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C6E9A60F-9055-4E54-9E29-7F277875ABC1}.Release|Any CPU.Build.0 = Release|Any CPU - {AB9EDCDE-48B6-4B6E-B0B6-28CF549A6591}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AB9EDCDE-48B6-4B6E-B0B6-28CF549A6591}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AB9EDCDE-48B6-4B6E-B0B6-28CF549A6591}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AB9EDCDE-48B6-4B6E-B0B6-28CF549A6591}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26228.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cake.Terraform", "Cake.Terraform\Cake.Terraform.csproj", "{C6E9A60F-9055-4E54-9E29-7F277875ABC1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cake.Terraform.Tests", "Cake.Terraform.Tests\Cake.Terraform.Tests.csproj", "{AB9EDCDE-48B6-4B6E-B0B6-28CF549A6591}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C6E9A60F-9055-4E54-9E29-7F277875ABC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6E9A60F-9055-4E54-9E29-7F277875ABC1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6E9A60F-9055-4E54-9E29-7F277875ABC1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6E9A60F-9055-4E54-9E29-7F277875ABC1}.Release|Any CPU.Build.0 = Release|Any CPU + {AB9EDCDE-48B6-4B6E-B0B6-28CF549A6591}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB9EDCDE-48B6-4B6E-B0B6-28CF549A6591}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB9EDCDE-48B6-4B6E-B0B6-28CF549A6591}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB9EDCDE-48B6-4B6E-B0B6-28CF549A6591}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/Cake.Terraform/Output/TerraformOutputRunner.cs b/src/Cake.Terraform/Output/TerraformOutputRunner.cs new file mode 100644 index 0000000..2f0d7f2 --- /dev/null +++ b/src/Cake.Terraform/Output/TerraformOutputRunner.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using System.Text; +using Cake.Core; +using Cake.Core.IO; +using Cake.Core.Tooling; + +namespace Cake.Terraform.Output +{ + public class TerraformOutputRunner : TerraformRunner + { + public TerraformOutputRunner(IFileSystem fileSystem, ICakeEnvironment environment, IProcessRunner processRunner, IToolLocator tools) + : base(fileSystem, environment, processRunner, tools) + { + } + + private static string RemoveWhitespace(string x) + { + return x + .Replace("\n", "") + .Replace("\r", "") + .Replace(" ", ""); + } + + private void ConfirmSuccess(string output) + { + string outputNotFoundErrorString = RemoveWhitespace("The output variable requested could not be found in the state file. If you recently added this to your configuration, be sure to run `terraform apply`, since the state won't be updated with new output variables until that command is run."); + + if (RemoveWhitespace(output) == outputNotFoundErrorString) + { + throw new ArgumentException(output); + } + } + + public string Run(TerraformOutputSettings settings) + { + var arguments = new ProcessArgumentBuilder() + .Append("output") + .Append("-no-color"); + + if (settings.StatePath != null) + { + arguments.Append($"-state={settings.StatePath}"); + } + + if (settings.Raw) + { + arguments.Append("-raw"); + } + else if (settings.Json) + { + arguments.Append("-json"); + } + + if (settings.OutputName != null) + { + arguments.Append(settings.OutputName); + } + + var processSettings = new ProcessSettings + { + RedirectStandardOutput = true + }; + + string output = null; + Run(settings, arguments, processSettings, x => + { + var outputLines = x.GetStandardOutput().ToList(); + int lineCount = outputLines.Count(); + + var builder = new StringBuilder(); + for (int i = 0; i < lineCount; i++) + { + builder.Append(outputLines[i]); + + if (i < lineCount - 1) + { + builder.Append("\n"); // OS consistent + } + } + + output = builder.ToString(); + }); + + if (settings.ValidateToolOutput) + { + ConfirmSuccess(output); + } + + return output; + } + } +} \ No newline at end of file diff --git a/src/Cake.Terraform/Output/TerraformOutputSettings.cs b/src/Cake.Terraform/Output/TerraformOutputSettings.cs new file mode 100644 index 0000000..7bc3234 --- /dev/null +++ b/src/Cake.Terraform/Output/TerraformOutputSettings.cs @@ -0,0 +1,11 @@ +namespace Cake.Terraform.Output +{ + public class TerraformOutputSettings : TerraformSettings + { + public string OutputName { get; set; } + public bool Json { get; set; } + public bool Raw { get; set; } + public string StatePath { get; set; } + public bool ValidateToolOutput { get; set; } = true; + } +} \ No newline at end of file diff --git a/src/Cake.Terraform/TerraformAliases.cs b/src/Cake.Terraform/TerraformAliases.cs index de4fcbc..5fa31aa 100644 --- a/src/Cake.Terraform/TerraformAliases.cs +++ b/src/Cake.Terraform/TerraformAliases.cs @@ -8,6 +8,7 @@ using Cake.Terraform.EnvNew; using Cake.Terraform.EnvSelect; using Cake.Terraform.Init; +using Cake.Terraform.Output; using Cake.Terraform.Plan; using Cake.Terraform.Refresh; using Cake.Terraform.Show; @@ -147,5 +148,12 @@ public static void TerraformRefresh(this ICakeContext context, TerraformRefreshS var runner = new TerraformRefreshRunner(context.FileSystem, context.Environment, context.ProcessRunner, context.Tools); runner.Run(settings); } + + [CakeMethodAlias] + public static string TerraformOutput(this ICakeContext context, TerraformOutputSettings settings) + { + var runner = new TerraformOutputRunner(context.FileSystem, context.Environment, context.ProcessRunner, context.Tools); + return runner.Run(settings); + } } }