diff --git a/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs b/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs index 5822ec1792..f97b2dd043 100644 --- a/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs +++ b/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs @@ -148,7 +148,12 @@ public static (string bootstrapFile, string[] temporaryFiles) PrepareBootstrapFi writer.NewLine = LinuxNewLine; writer.WriteLine("#!/bin/bash"); writer.WriteLine("source \"$(pwd)/" + Path.GetFileName(configurationFile) + "\""); - writer.WriteLine("shift"); // Shift the variable decryption key out of scope of the user script (see: https://github.com/OctopusDeploy/Calamari/pull/773) + writer.WriteLine("shift"); // Shift the variable decryption key out of scope of the user script and script modules (see: https://github.com/OctopusDeploy/Calamari/pull/773) + + var preloadModules = variables.Get(BashScriptVariables.PreloadScriptModules); + if (!string.IsNullOrWhiteSpace(preloadModules)) + PreloadScriptModules(writer, preloadModules, scriptModulePaths); + writer.WriteLine("source \"$(pwd)/" + Path.GetFileName(script.File) + "\" " + script.Parameters); writer.Flush(); } @@ -158,6 +163,23 @@ public static (string bootstrapFile, string[] temporaryFiles) PrepareBootstrapFi return (bootstrapFile, scriptModulePaths); } + + static void PreloadScriptModules(StreamWriter writer, string preloadModules, string[] scriptModulePaths) + { + var modules = preloadModules.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var module in modules) + { + var sanitizedName = ScriptVariables.FormatScriptName(module.Trim()); + var fileName = $"{sanitizedName}.sh"; + var scriptModule = scriptModulePaths.FirstOrDefault(p => string.Equals(Path.GetFileName(p), fileName, StringComparison.OrdinalIgnoreCase)); + if (scriptModule != null) + { + Log.VerboseFormat("Preloading script module '{0}'.", module.Trim()); + writer.WriteLine("source \"$(pwd)/" + fileName + "\""); + } + } + } + static IEnumerable PrepareScriptModules(IVariables variables, string workingDirectory) { foreach (var variableName in variables.GetNames().Where(ScriptVariables.IsLibraryScriptModule)) diff --git a/source/Calamari.Common/Features/Scripting/Bash/BashScriptVariables.cs b/source/Calamari.Common/Features/Scripting/Bash/BashScriptVariables.cs new file mode 100644 index 0000000000..fead467ec7 --- /dev/null +++ b/source/Calamari.Common/Features/Scripting/Bash/BashScriptVariables.cs @@ -0,0 +1,7 @@ +namespace Calamari.Common.Features.Scripting.Bash +{ + public static class BashScriptVariables + { + public static readonly string PreloadScriptModules = "Octopus.Action.Script.PreloadScriptModules"; + } +} diff --git a/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs b/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs index 52bbb0a127..070828a62c 100644 --- a/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs +++ b/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs @@ -352,6 +352,126 @@ public void ShouldBeAbleToEnumerateVariableValues(FeatureToggle? featureToggle) output.AssertOutput("Key: VariableName`prop`anotherprop` 13, Value: Value`prop`13"); output.AssertOutput($"Key: {specialCharacters}, Value: {specialCharacters}"); } + + [Test] + [RequiresBashDotExeIfOnWindows] + public void ShouldPreloadScriptModules() + { + var (output, _) = RunScript("preload-modules.sh", + new Dictionary() + { + ["Octopus.Script.Module[test_module]"] = @"function test_function { + echo ""Function from preloaded module"" +} + +export PRELOADED_VAR=""module_value""", + ["Octopus.Script.Module.Language[test_module]"] = "Bash", + ["Octopus.Action.Script.PreloadScriptModules"] = "test_module" + }); + + Assert.Multiple(() => + { + output.AssertSuccess(); + output.AssertOutput("Calling test_function from preloaded module..."); + output.AssertOutput("Function from preloaded module"); + output.AssertOutput("Checking preloaded variable..."); + output.AssertOutput("PRELOADED_VAR=module_value"); + }); + } + + [Test] + [RequiresBashDotExeIfOnWindows] + public void ShouldPreloadMultipleScriptModulesInOrder() + { + var (output, _) = RunScript("preload-modules.sh", + new Dictionary() + { + ["Octopus.Script.Module[module1]"] = @"function test_function { + echo ""Module 1"" +} + +export PRELOADED_VAR=""value1""", + ["Octopus.Script.Module.Language[module1]"] = "Bash", + ["Octopus.Script.Module[module2]"] = @"function test_function { + echo ""Module 2"" +} + +export PRELOADED_VAR=""value2""", + ["Octopus.Script.Module.Language[module2]"] = "Bash", + // module2 sourced last, so it wins + ["Octopus.Action.Script.PreloadScriptModules"] = "module1,module2" + }); + + Assert.Multiple(() => + { + output.AssertSuccess(); + // Should use module2's function (last wins) + output.AssertOutput("Module 2"); + // Should use module2's variable (last wins) + output.AssertOutput("PRELOADED_VAR=value2"); + }); + } + + [Test] + [RequiresBashDotExeIfOnWindows] + [TestCase("module1,module2", "module1,module2")] + [TestCase("module1, module2", "module1,module2")] + [TestCase("module1 , module2", "module1,module2")] + [TestCase("module1;module2", "module1,module2")] + [TestCase("module1; module2", "module1,module2")] + [TestCase("module1 ; module2", "module1,module2")] + [TestCase("module1,module2;module3", "module1,module2,module3")] + [TestCase("module1, module2; module3", "module1,module2,module3")] + [TestCase("module1,,module2", "module1,module2")] + [TestCase(" module1 , module2 ", "module1,module2")] + [TestCase("module1, ,module2", "module1,module2")] + [TestCase("module1,missing_module", "module1")] + public void PreloadModulesShouldHandleVariousDelimiterCombinations(string moduleList, string expectedModules) + { + var (output, _) = RunScript("preload-modules.sh", + new Dictionary() + { + ["Octopus.Script.Module[module1]"] = @"function test_function { + echo ""Module 1"" +} + +if [ -z ""$PRELOADED_VAR"" ]; then + export PRELOADED_VAR=""module1"" +else + export PRELOADED_VAR=""$PRELOADED_VAR,module1"" +fi", + ["Octopus.Script.Module.Language[module1]"] = "Bash", + ["Octopus.Script.Module[module2]"] = @"function test_function { + echo ""Module 2"" +} + +if [ -z ""$PRELOADED_VAR"" ]; then + export PRELOADED_VAR=""module2"" +else + export PRELOADED_VAR=""$PRELOADED_VAR,module2"" +fi", + ["Octopus.Script.Module.Language[module2]"] = "Bash", + ["Octopus.Script.Module[module3]"] = @"function test_function { + echo ""Module 3"" +} + +if [ -z ""$PRELOADED_VAR"" ]; then + export PRELOADED_VAR=""module3"" +else + export PRELOADED_VAR=""$PRELOADED_VAR,module3"" +fi", + ["Octopus.Script.Module.Language[module3]"] = "Bash", + ["Octopus.Action.Script.PreloadScriptModules"] = moduleList + }); + + Assert.Multiple(() => + { + output.AssertSuccess(); + + // Verify expected modules were loaded in the correct order + output.AssertOutput($"PRELOADED_VAR={expectedModules}"); + }); + } } public static class AdditionalVariablesExtensions diff --git a/source/Calamari.Tests/Fixtures/Bash/Scripts/preload-modules.sh b/source/Calamari.Tests/Fixtures/Bash/Scripts/preload-modules.sh new file mode 100644 index 0000000000..bfe95cae1b --- /dev/null +++ b/source/Calamari.Tests/Fixtures/Bash/Scripts/preload-modules.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Test that preloaded modules are available +echo "Calling test_function from preloaded module..." +test_function + +echo "Checking preloaded variable..." +echo "PRELOADED_VAR=$PRELOADED_VAR"