From da06110cc3302bafa00f5ab2788969324e5f7121 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Tue, 5 Nov 2024 14:19:51 -0500 Subject: [PATCH 1/4] hcl2template: add filebase64 function The filebase64 function aims to read and encode a file's contents into base64. This is mostly to support reading the content of a file that is not valid UTF-8, as is the case with the `file` function. --- hcl2template/function/filebase64.go | 40 +++++++++++++++ hcl2template/function/filebase64_test.go | 62 ++++++++++++++++++++++++ hcl2template/functions.go | 1 + 3 files changed, 103 insertions(+) create mode 100644 hcl2template/function/filebase64.go create mode 100644 hcl2template/function/filebase64_test.go diff --git a/hcl2template/function/filebase64.go b/hcl2template/function/filebase64.go new file mode 100644 index 000000000..1f92e704d --- /dev/null +++ b/hcl2template/function/filebase64.go @@ -0,0 +1,40 @@ +package function + +import ( + "encoding/base64" + "fmt" + "os" + "strings" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +var Filebase64 = function.New(&function.Spec{ + Params: []function.Parameter{ + function.Parameter{ + Name: "path", + Description: "Read a file and encode it as a base64 string", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + path := args[0].AsString() + content, err := os.ReadFile(path) + if err != nil { + return cty.NullVal(cty.String), fmt.Errorf("failed to read file %q: %s", path, err) + } + + out := &strings.Builder{} + enc := base64.NewEncoder(base64.StdEncoding, out) + _, err = enc.Write(content) + if err != nil { + return cty.NullVal(cty.String), fmt.Errorf("failed to write file %q as base64: %s", path, err) + } + _ = enc.Close() + + return cty.StringVal(out.String()), nil + }, +}) diff --git a/hcl2template/function/filebase64_test.go b/hcl2template/function/filebase64_test.go new file mode 100644 index 000000000..a865060c5 --- /dev/null +++ b/hcl2template/function/filebase64_test.go @@ -0,0 +1,62 @@ +package function + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" +) + +func TestFilebase64(t *testing.T) { + tests := []struct { + name string + file string + expectedOutput string + expectError bool + }{ + { + "file exists, return base64'd contents, no error", + "./testdata/list.tmpl", + "JXsgZm9yIHggaW4gbGlzdCB+fQotICR7eH0KJXsgZW5kZm9yIH59Cg==", + false, + }, + { + "file doesn't exist, return nilval and an error", + "./testdata/no_file", + "", + true, + }, + { + "directory passed as arg, should error", + "./testdata", + "", + true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := Filebase64.Call([]cty.Value{ + cty.StringVal(tt.file), + }) + + if tt.expectError && err == nil { + t.Fatal("succeeded; want error") + } + + if !tt.expectError && err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if err != nil { + return + } + + retVal := res.AsString() + diff := cmp.Diff(retVal, tt.expectedOutput) + if diff != "" { + t.Errorf("expected output and returned are different: %s", diff) + } + }) + } +} diff --git a/hcl2template/functions.go b/hcl2template/functions.go index 3736d12f5..8d50ecf49 100644 --- a/hcl2template/functions.go +++ b/hcl2template/functions.go @@ -60,6 +60,7 @@ func Functions(basedir string) map[string]function.Function { "distinct": stdlib.DistinctFunc, "element": stdlib.ElementFunc, "file": filesystem.MakeFileFunc(basedir, false), + "filebase64": pkrfunction.Filebase64, "fileexists": filesystem.MakeFileExistsFunc(basedir), "fileset": filesystem.MakeFileSetFunc(basedir), "flatten": stdlib.FlattenFunc, From 5bfd778aa732cf0855ef1d01c498f073a6dfb4b0 Mon Sep 17 00:00:00 2001 From: karthik P Date: Thu, 24 Apr 2025 23:05:04 +0530 Subject: [PATCH 2/4] adding docs for filebase64 function --- .../functions/file/filebase64.mdx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 website/content/docs/templates/hcl_templates/functions/file/filebase64.mdx diff --git a/website/content/docs/templates/hcl_templates/functions/file/filebase64.mdx b/website/content/docs/templates/hcl_templates/functions/file/filebase64.mdx new file mode 100644 index 000000000..1b60fcc7f --- /dev/null +++ b/website/content/docs/templates/hcl_templates/functions/file/filebase64.mdx @@ -0,0 +1,46 @@ +--- +page_title: filebase64 - Functions - Configuration Language +description: |- + The filebase64 function reads the contents of the file at the given path and + returns them as a base64-encoded string. +--- + +# `filebase64` Function + +`filebase64` reads the contents of a file at the given path and returns them as +a base64-encoded string. + +```hcl +filebase64(path) +``` + +The result is a Base64 representation of the raw bytes in the given file. +Strings in the Packer language are sequences of Unicode characters, so +Base64 is the standard way to represent raw binary data that cannot be +interpreted as Unicode characters. Resource types that operate on binary +data will accept this data encoded in Base64, thus avoiding the need to +decode the result of this function. + +Packer uses the "standard" Base64 alphabet as defined in +[RFC 4648 section 4](https://tools.ietf.org/html/rfc4648#section-4). + +This function can be used only with functions that already exist as static +files on disk at the beginning of a Packer run. Language functions do not +participate in the dependency graph, so this function cannot be used with +files that are generated dynamically during a Packer operation. + +## Examples + +``` +> filebase64("${path.module}/hello.txt") +SGVsbG8gV29ybGQ= +``` + +## Related Functions + +* [`file`](/packer/docs/templates/hcl_templates/functions/file/file) also reads the contents of a given file, + but interprets the data as UTF-8 text and returns the result directly + as a string, without any further encoding. +* [`base64decode`](/packer/docs/templates/hcl_templates/functions/encoding/base64decode) can decode a Base64 string representing + bytes in UTF-8, but in practice `base64decode(filebase64(...))` is equivalent + to the shorter expression `file(...)`. From 089df02532c8d7b73579b9e64601ba81220c4a46 Mon Sep 17 00:00:00 2001 From: Karthik P Date: Fri, 25 Apr 2025 11:23:32 +0530 Subject: [PATCH 3/4] Powershell Provisioner Error Handling (#13334) * WIP Testing Approach * WIP Testing Approach * WIP Testing Approach Work * WIP Testing Approach * adding acceptance test for windows Amazon EBS. * modify wrapper string to use Set-Variable * fixing unit tests * cleanup * updated approach to use -file instead of inline powershell execution. * adding more scenarios for acceptance test. * using writeString for file directly. * changing variable name * updating test case. cleanup. * updating test case * updating test case * updating test case - nested try catch * adding unit test for None Execution Policy * adding unit test for None Execution Policy * fix test case --- provisioner/powershell/provisioner.go | 71 ++++++++++++----- .../powershell/provisioner_acc_test.go | 23 ++++++ provisioner/powershell/provisioner_test.go | 71 +++++++++++++---- .../powershell-exit_codes-provisioner.txt | 76 +++++++++++++++++++ .../test-fixtures/scripts/bootstrap_win.txt | 40 ++++++++++ .../scripts/set_version_latest.ps1 | 13 ++++ 6 files changed, 261 insertions(+), 33 deletions(-) create mode 100644 provisioner/powershell/test-fixtures/powershell-exit_codes-provisioner.txt create mode 100644 provisioner/powershell/test-fixtures/scripts/bootstrap_win.txt create mode 100644 provisioner/powershell/test-fixtures/scripts/set_version_latest.ps1 diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go index aae71f9fb..0f2719275 100644 --- a/provisioner/powershell/provisioner.go +++ b/provisioner/powershell/provisioner.go @@ -8,7 +8,6 @@ package powershell import ( - "bufio" "context" "errors" "fmt" @@ -38,6 +37,33 @@ var psEscape = strings.NewReplacer( "'", "`'", ) +// wraps the content in try catch block and exits with a status. +const wrapPowershellString string = ` + if (Test-Path variable:global:ProgressPreference) { + set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue' + } + {{if .DebugMode}} + Set-PsDebug -Trace {{.DebugMode}} + {{- end}} + $exitCode = 0 + try { + {{.Vars}} + {{.Payload}} + $exitCode = 0 + } catch { + Write-Error "An error occurred: $_" + $exitCode = 1 + } + + if ($LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0) { + $exitCode = $LASTEXITCODE + } + + Write-Host $result + exit $exitCode + +` + type Config struct { shell.Provisioner `mapstructure:",squash"` @@ -105,23 +131,15 @@ type Provisioner struct { } func (p *Provisioner) defaultExecuteCommand() string { - baseCmd := `& { if (Test-Path variable:global:ProgressPreference)` + - `{set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};` - - if p.config.DebugMode != 0 { - baseCmd += fmt.Sprintf(`Set-PsDebug -Trace %d;`, p.config.DebugMode) - } - - baseCmd += `. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }` if p.config.ExecutionPolicy == ExecutionPolicyNone { - return baseCmd + return `-file {{.Path}}` } if p.config.UsePwsh { - return fmt.Sprintf(`pwsh -executionpolicy %s -command "%s"`, p.config.ExecutionPolicy, baseCmd) + return fmt.Sprintf(`pwsh -executionpolicy %s -file {{.Path}}`, p.config.ExecutionPolicy) } else { - return fmt.Sprintf(`powershell -executionpolicy %s "%s"`, p.config.ExecutionPolicy, baseCmd) + return fmt.Sprintf(`powershell -executionpolicy %s -file {{.Path}}`, p.config.ExecutionPolicy) } } @@ -247,24 +265,41 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { return nil } -// Takes the inline scripts, concatenates them into a temporary file and +// Takes the inline scripts, adds a wrapper around the inline scripts, concatenates them into a temporary file and // returns a string containing the location of said file. func extractScript(p *Provisioner) (string, error) { temp, err := tmp.File("powershell-provisioner") if err != nil { return "", err } + defer temp.Close() - writer := bufio.NewWriter(temp) + + var commandBuilder strings.Builder + + // we concatenate all the inline commands for _, command := range p.config.Inline { log.Printf("Found command: %s", command) - if _, err := writer.WriteString(command + "\n"); err != nil { - return "", fmt.Errorf("Error preparing powershell script: %s", err) + if _, err := commandBuilder.WriteString(command); err != nil { + return "", fmt.Errorf("failed to wrap script contents: %w", err) } } - if err := writer.Flush(); err != nil { - return "", fmt.Errorf("Error preparing powershell script: %s", err) + // injecting all the variables in the string + ctxData := p.generatedData + ctxData["Vars"] = p.createFlattenedEnvVars(p.config.ElevatedUser != "") + ctxData["Payload"] = commandBuilder.String() + ctxData["DebugMode"] = p.config.DebugMode + p.config.ctx.Data = ctxData + + data, err := interpolate.Render(wrapPowershellString, &p.config.ctx) + if err != nil { + return "", fmt.Errorf("Error building powershell wrapper: %w", err) + } + + log.Printf("Writing PowerShell script to file: %s", temp.Name()) + if _, err := temp.WriteString(data); err != nil { + return "", fmt.Errorf("Error writing PowerShell script: %w", err) } return temp.Name(), nil diff --git a/provisioner/powershell/provisioner_acc_test.go b/provisioner/powershell/provisioner_acc_test.go index 8073d0113..90c16ddef 100644 --- a/provisioner/powershell/provisioner_acc_test.go +++ b/provisioner/powershell/provisioner_acc_test.go @@ -111,3 +111,26 @@ func TestAccPowershellProvisioner_Script(t *testing.T) { provisioneracc.TestProvisionersAgainstBuilders(testCase, t) } + +func TestAccPowershellProvisioner_ExitCodes(t *testing.T) { + templateString, err := LoadProvisionerFragment("powershell-exit_codes-provisioner.txt") + if err != nil { + t.Fatalf("Couldn't load test fixture; %s", err.Error()) + } + testCase := &provisioneracc.ProvisionerTestCase{ + IsCompatible: powershellIsCompatible, + Name: "powershell-provisioner-script", + Template: templateString, + Type: TestProvisionerType, + Check: func(buildcommand *exec.Cmd, logfile string) error { + if buildcommand.ProcessState != nil { + if buildcommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + return nil + }, + } + provisioneracc.TestProvisionersAgainstBuilders(testCase, t) + +} diff --git a/provisioner/powershell/provisioner_test.go b/provisioner/powershell/provisioner_test.go index 024b0585f..b858700b9 100644 --- a/provisioner/powershell/provisioner_test.go +++ b/provisioner/powershell/provisioner_test.go @@ -23,6 +23,7 @@ func TestProvisionerPrepare_extractScript(t *testing.T) { config := testConfig() p := new(Provisioner) _ = p.Prepare(config) + p.generatedData = generatedData() file, err := extractScript(p) defer os.Remove(file) if err != nil { @@ -35,13 +36,15 @@ func TestProvisionerPrepare_extractScript(t *testing.T) { // File contents should contain 2 lines concatenated by newlines: foo\nbar readFile, err := os.ReadFile(file) - expectedContents := "foo\nbar\n" + expectedContents := "if (Test-Path variable:global:ProgressPreference) {\n set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'\n }\n \n $exitCode = 0\n try {\n $env:PACKER_BUILDER_TYPE=\"\"; $env:PACKER_BUILD_NAME=\"\"; \n foobar\n $exitCode = 0\n } catch {\n Write-Error \"An error occurred: $_\"\n $exitCode = 1\n }\n \n if ($LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0) {\n $exitCode = $LASTEXITCODE\n }\n \n Write-Host $result\n exit $exitCode" + normalizedExpectedContent := normalizeWhiteSpace(expectedContents) if err != nil { t.Fatalf("Should not be error: %s", err) } s := string(readFile[:]) - if s != expectedContents { - t.Fatalf("Expected generated inlineScript to equal '%s', got '%s'", expectedContents, s) + normalizedString := normalizeWhiteSpace(s) + if normalizedString != normalizedExpectedContent { + t.Fatalf("Expected generated inlineScript to equal '%s', got '%s'", normalizedExpectedContent, normalizedString) } } @@ -74,12 +77,12 @@ func TestProvisionerPrepare_Defaults(t *testing.T) { t.Error("expected elevated_password to be empty") } - if p.config.ExecuteCommand != `powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"` { - t.Fatalf(`Default command should be 'powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"', but got '%s'`, p.config.ExecuteCommand) + if p.config.ExecuteCommand != `powershell -executionpolicy bypass -file {{.Path}}` { + t.Fatalf(`Default command should be 'powershell -executionpolicy bypass -file {{.Path}}', but got '%s'`, p.config.ExecuteCommand) } - if p.config.ElevatedExecuteCommand != `powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"` { - t.Fatalf(`Default command should be 'powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"', but got '%s'`, p.config.ElevatedExecuteCommand) + if p.config.ElevatedExecuteCommand != `powershell -executionpolicy bypass -file {{.Path}}` { + t.Fatalf(`Default command should be 'powershell -executionpolicy bypass -file {{.Path}}', but got '%s'`, p.config.ElevatedExecuteCommand) } if p.config.ElevatedEnvVarFormat != `$env:%s="%s"; ` { @@ -120,7 +123,7 @@ func TestProvisionerPrepare_DebugMode(t *testing.T) { t.Fatalf("err: %s", err) } - command := `powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};Set-PsDebug -Trace 1;. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"` + command := `powershell -executionpolicy bypass -file {{.Path}}` if p.config.ExecuteCommand != command { t.Fatalf(fmt.Sprintf(`Expected command should be '%s' but got '%s'`, command, p.config.ExecuteCommand)) } @@ -483,7 +486,8 @@ func TestProvisionerProvision_Inline(t *testing.T) { } cmd := comm.StartCmd.Command - re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/inlineScript.ps1'; exit \$LastExitCode }"`) + re := regexp.MustCompile(`powershell -executionpolicy bypass -file c:/Windows/Temp/inlineScript.ps1`) + matched := re.MatchString(cmd) if !matched { t.Fatalf("Got unexpected command: %s", cmd) @@ -503,7 +507,7 @@ func TestProvisionerProvision_Inline(t *testing.T) { } cmd = comm.StartCmd.Command - re = regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/inlineScript.ps1'; exit \$LastExitCode }"`) + re = regexp.MustCompile(`powershell -executionpolicy bypass -file c:/Windows/Temp/inlineScript.ps1`) matched = re.MatchString(cmd) if !matched { t.Fatalf("Got unexpected command: %s", cmd) @@ -533,7 +537,7 @@ func TestProvisionerProvision_Scripts(t *testing.T) { } cmd := comm.StartCmd.Command - re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1'; exit \$LastExitCode }"`) + re := regexp.MustCompile(`powershell -executionpolicy bypass -file c:/Windows/Temp/script.ps1`) matched := re.MatchString(cmd) if !matched { t.Fatalf("Got unexpected command: %s", cmd) @@ -570,7 +574,7 @@ func TestProvisionerProvision_ScriptsWithEnvVars(t *testing.T) { } cmd := comm.StartCmd.Command - re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1'; exit \$LastExitCode }"`) + re := regexp.MustCompile(`powershell -executionpolicy bypass -file c:/Windows/Temp/script.ps1`) matched := re.MatchString(cmd) if !matched { t.Fatalf("Got unexpected command: %s", cmd) @@ -595,11 +599,11 @@ func TestProvisionerProvision_SkipClean(t *testing.T) { }{ { SkipClean: true, - LastExecutedCommandRegex: `powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1'; exit \$LastExitCode }"`, + LastExecutedCommandRegex: `powershell -executionpolicy bypass -file c:/Windows/Temp/script.ps1`, }, { SkipClean: false, - LastExecutedCommandRegex: `powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/packer-cleanup-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1'; exit \$LastExitCode }"`, + LastExecutedCommandRegex: `powershell -executionpolicy bypass -file c:/Windows/Temp/packer-cleanup-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1`, }, } @@ -917,7 +921,7 @@ func TestProvision_createCommandText(t *testing.T) { p.generatedData = make(map[string]interface{}) cmd, _ := p.createCommandText() - re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1'; exit \$LastExitCode }"`) + re := regexp.MustCompile(`powershell -executionpolicy bypass -file c:/Windows/Temp/script.ps1`) matched := re.MatchString(cmd) if !matched { t.Fatalf("Got unexpected command: %s", cmd) @@ -934,6 +938,28 @@ func TestProvision_createCommandText(t *testing.T) { } } +func TestProvision_createCommandTextNoneExecutionPolicy(t *testing.T) { + config := testConfig() + config["remote_path"] = "c:/Windows/Temp/script.ps1" + p := new(Provisioner) + + comm := new(packersdk.MockCommunicator) + p.communicator = comm + config["execution_policy"] = ExecutionPolicyNone + _ = p.Prepare(config) + + // Non-elevated + p.generatedData = make(map[string]interface{}) + + cmd, _ := p.createCommandText() + re := regexp.MustCompile(`-file c:/Windows/Temp/script.ps1`) + matched := re.MatchString(cmd) + if !matched { + t.Fatalf("Got unexpected command: %s", cmd) + } + +} + func TestProvision_uploadEnvVars(t *testing.T) { p := new(Provisioner) comm := new(packersdk.MockCommunicator) @@ -976,3 +1002,18 @@ func generatedData() map[string]interface{} { "PackerHTTPPort": commonsteps.HttpPortNotImplemented, } } + +func normalizeWhiteSpace(s string) string { + // Replace multiple spaces/tabs with a single space + re := regexp.MustCompile(`[\t ]+`) + s = re.ReplaceAllString(s, " ") + + // Trim leading/trailing spaces and newlines + s = strings.TrimSpace(s) + + // Normalize line breaks (remove excessive empty lines) + s = strings.ReplaceAll(s, "\r\n", "\n") // Convert Windows line endings to Unix + s = strings.ReplaceAll(s, "\r", "\n") // Convert old Mac line endings to Unix + + return s +} diff --git a/provisioner/powershell/test-fixtures/powershell-exit_codes-provisioner.txt b/provisioner/powershell/test-fixtures/powershell-exit_codes-provisioner.txt new file mode 100644 index 000000000..f0445951d --- /dev/null +++ b/provisioner/powershell/test-fixtures/powershell-exit_codes-provisioner.txt @@ -0,0 +1,76 @@ +{ + "type": "powershell", + "inline": ["invalid-cmdlet"], + "valid_exit_codes": ["1"] +}, +{ + "type": "powershell", + "inline": ["#Requires -Version 10.0"], + "valid_exit_codes": ["1"] +}, +{ + "type": "powershell", + "inline": ["exit 1"], + "valid_exit_codes": ["1"] +}, +{ + "type": "powershell", + "inline": ["}}"], + "valid_exit_codes": ["1"] +}, +{ + "type": "powershell", + "inline": ["$LASTEXITCODE=1"], + "valid_exit_codes": ["1"] +}, +{ + "type": "powershell", + "inline": ["throw 'XXX'"], + "valid_exit_codes": ["1"] +}, +{ + "type": "powershell", + "script": "../../provisioner/powershell/test-fixtures/scripts/set_version_latest.ps1", + "valid_exit_codes": ["0"] +}, +{ + "type": "powershell", + "elevated_user": "Administrator", + "elevated_password": "{{.WinRMPassword}}", + "inline": "Get-ItemProperty -Path HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion", + "valid_exit_codes": ["0"] +}, +{ + "type": "powershell", + "inline": "ping invalidhost", + "valid_exit_codes": ["1"] +}, +{ + "type": "powershell", + "inline": "sc.exe start command", + "valid_exit_codes": ["1060"] +}, +{ + "type": "powershell", + "inline": "echo 'Hi testing echo'; invalid command!; echo 'Another valid command';", + "valid_exit_codes": ["1"] +}, +{ + "type": "powershell", + "inline": ["$ErrorActionPreference='Stop'", "Get-Item 'C:\\nonexistent.txt'"], + "valid_exit_codes": ["1"] +}, +{ + "type": "powershell", + "inline": [ + "try {", + " invalid command", + "} catch {", + " exit 1", + "}" + ], + "valid_exit_codes": ["1"] +} + + + diff --git a/provisioner/powershell/test-fixtures/scripts/bootstrap_win.txt b/provisioner/powershell/test-fixtures/scripts/bootstrap_win.txt new file mode 100644 index 000000000..ea3aedf70 --- /dev/null +++ b/provisioner/powershell/test-fixtures/scripts/bootstrap_win.txt @@ -0,0 +1,40 @@ + +# Set administrator password +net user Administrator SuperS3cr3t!!!! +wmic useraccount where "name='Administrator'" set PasswordExpires=FALSE + +# First, make sure WinRM can't be connected to +netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes action=block + +# Delete any existing WinRM listeners +winrm delete winrm/config/listener?Address=*+Transport=HTTP 2>$Null +winrm delete winrm/config/listener?Address=*+Transport=HTTPS 2>$Null + +# Disable group policies which block basic authentication and unencrypted login + +Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Client -Name AllowBasic -Value 1 +Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Client -Name AllowUnencryptedTraffic -Value 1 +Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Service -Name AllowBasic -Value 1 +Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Service -Name AllowUnencryptedTraffic -Value 1 + + +# Create a new WinRM listener and configure +winrm create winrm/config/listener?Address=*+Transport=HTTP +winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="0"}' +winrm set winrm/config '@{MaxTimeoutms="7200000"}' +winrm set winrm/config/service '@{AllowUnencrypted="true"}' +winrm set winrm/config/service '@{MaxConcurrentOperationsPerUser="12000"}' +winrm set winrm/config/service/auth '@{Basic="true"}' +winrm set winrm/config/client/auth '@{Basic="true"}' + +# Configure UAC to allow privilege elevation in remote shells +$Key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' +$Setting = 'LocalAccountTokenFilterPolicy' +Set-ItemProperty -Path $Key -Name $Setting -Value 1 -Force + +# Configure and restart the WinRM Service; Enable the required firewall exception +Stop-Service -Name WinRM +Set-Service -Name WinRM -StartupType Automatic +netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new action=allow localip=any remoteip=any +Start-Service -Name WinRM + \ No newline at end of file diff --git a/provisioner/powershell/test-fixtures/scripts/set_version_latest.ps1 b/provisioner/powershell/test-fixtures/scripts/set_version_latest.ps1 new file mode 100644 index 000000000..f548cea7a --- /dev/null +++ b/provisioner/powershell/test-fixtures/scripts/set_version_latest.ps1 @@ -0,0 +1,13 @@ +# Test fixture is a modified version of the example found at +# https://www.powershellmagazine.com/2012/10/23/pstip-set-strictmode-why-should-you-care/ + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$myNumbersCollection = 1..5 +if($myNumbersCollection -contains 3) { + "collection contains 3" +} +else { + "collection doesn't contain 3" +} \ No newline at end of file From 6ef741d5f5c70110af1e668b1f7f882fb0a50ddb Mon Sep 17 00:00:00 2001 From: karthik P Date: Mon, 28 Apr 2025 10:51:47 +0530 Subject: [PATCH 4/4] adding fn to nav data json --- website/data/docs-nav-data.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index b80e72fef..d878ec5e3 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -508,6 +508,10 @@ "title": "file", "path": "templates/hcl_templates/functions/file/file" }, + { + "title": "filebase64", + "path": "templates/hcl_templates/functions/file/filebase64" + }, { "title": "fileexists", "path": "templates/hcl_templates/functions/file/fileexists"