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
pull/13333/merge
Karthik P 1 year ago committed by GitHub
parent fe6eba27f2
commit 089df02532
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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

@ -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)
}

@ -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
}

@ -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"]
}

@ -0,0 +1,40 @@
<powershell>
# 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
</powershell>

@ -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"
}
Loading…
Cancel
Save