Merge pull request #8908 from hashicorp/fix_4795

provisioner/powershell: Add cleanup step to remove any temporarily created scripts
This commit is contained in:
Megan Marsh 2020-04-09 08:56:19 -07:00 committed by GitHub
commit 73c349d09c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 180 additions and 34 deletions

View File

@ -25,6 +25,7 @@ type elevatedOptions struct {
TaskDescription string
LogFile string
XMLEscapedCommand string
ScriptFile string
}
var psEscape = strings.NewReplacer(
@ -117,6 +118,14 @@ $result = $t.LastTaskResult
if (Test-Path $log) {
Remove-Item $log -Force -ErrorAction SilentlyContinue | Out-Null
}
$script = [System.Environment]::ExpandEnvironmentVariables("{{.ScriptFile}}")
if (Test-Path $script) {
Remove-Item $script -Force -ErrorAction SilentlyContinue | Out-Null
}
$f = $s.GetFolder("\")
$f.DeleteTask("\$name", "")
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($s) | Out-Null
exit $result`))
@ -166,12 +175,16 @@ func GenerateElevatedRunner(command string, p ElevatedProvisioner) (uploadedPath
elevatedPassword, escapedElevatedPassword)
}
uuid := uuid.TimeOrderedUUID()
path := fmt.Sprintf(`C:/Windows/Temp/packer-elevated-shell-%s.ps1`, uuid)
// Generate command
err = elevatedTemplate.Execute(&buffer, elevatedOptions{
User: escapedElevatedUser,
Password: escapedElevatedPassword,
TaskName: taskName,
TaskDescription: "Packer elevated task",
ScriptFile: path,
LogFile: logFile,
XMLEscapedCommand: escapedCommand,
})
@ -180,8 +193,6 @@ func GenerateElevatedRunner(command string, p ElevatedProvisioner) (uploadedPath
fmt.Printf("Error creating elevated template: %s", err)
return "", err
}
uuid := uuid.TimeOrderedUUID()
path := fmt.Sprintf(`C:/Windows/Temp/packer-elevated-shell-%s.ps1`, uuid)
log.Printf("Uploading elevated shell wrapper for command [%s] to [%s]", command, path)
err = p.Communicator().Upload(path, &buffer, nil)
if err != nil {

View File

@ -52,6 +52,13 @@ type Config struct {
// can be used to inject the environment_vars into the environment.
ElevatedExecuteCommand string `mapstructure:"elevated_execute_command"`
// Whether to clean scripts up after executing the provisioner.
// Defaults to false. When true any script created by a non-elevated Powershell
// provisioner will be removed from the remote machine. Elevated scripts,
// along with the scheduled tasks, will always be removed regardless of the
// value set for `skip_clean`.
SkipClean bool `mapstructure:"skip_clean"`
// The timeout for retrying to start the process. Until this timeout is
// reached, if the provisioner can't start a process, it retries. This
// can be set high to allow for reboots.
@ -69,6 +76,8 @@ type Config struct {
ExecutionPolicy ExecutionPolicy `mapstructure:"execution_policy"`
remoteCleanUpScriptPath string
ctx interpolate.Context
}
@ -82,12 +91,14 @@ func (p *Provisioner) defaultExecuteCommand() string {
baseCmd := `& { if (Test-Path variable:global:ProgressPreference)` +
`{set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};` +
`. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }`
if p.config.ExecutionPolicy == ExecutionPolicyNone {
return baseCmd
} else {
return fmt.Sprintf(`powershell -executionpolicy %s "%s"`, p.config.ExecutionPolicy, baseCmd)
}
return fmt.Sprintf(`powershell -executionpolicy %s "%s"`, p.config.ExecutionPolicy, baseCmd)
}
func (p *Provisioner) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
func (p *Provisioner) Prepare(raws ...interface{}) error {
@ -149,6 +160,8 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
p.config.Vars = make([]string, 0)
}
p.config.remoteCleanUpScriptPath = fmt.Sprintf(`c:/Windows/Temp/packer-cleanup-%s.ps1`, uuid.TimeOrderedUUID())
var errs error
if p.config.Script != "" && len(p.config.Scripts) > 0 {
errs = packer.MultiErrorAppend(errs,
@ -243,6 +256,8 @@ func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.C
defer os.Remove(temp)
}
// every provisioner run will only have one env var script file so lets add it first
uploadedScripts := []string{p.config.RemoteEnvVarPath}
for _, path := range scripts {
ui.Say(fmt.Sprintf("Provisioning with powershell script: %s", path))
@ -289,16 +304,63 @@ func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.C
// Close the original file since we copied it
f.Close()
log.Printf("%s returned with exit code %d", p.config.RemotePath, cmd.ExitStatus())
// Record every other uploaded script file so we can clean it up later
uploadedScripts = append(uploadedScripts, p.config.RemotePath)
log.Printf("%s returned with exit code %d", p.config.RemotePath, cmd.ExitStatus())
if err := p.config.ValidExitCode(cmd.ExitStatus()); err != nil {
return err
}
}
if p.config.SkipClean {
return nil
}
err := retry.Config{StartTimeout: p.config.StartRetryTimeout}.Run(ctx, func(ctx context.Context) error {
command, err := p.createRemoteCleanUpCommand(uploadedScripts)
if err != nil {
log.Printf("failed to create a remote cleanup script: %s", err)
return err
}
cmd := &packer.RemoteCmd{Command: command}
return cmd.RunWithUi(ctx, comm, ui)
})
if err != nil {
log.Printf("failed to clean up temporary files: %s", strings.Join(uploadedScripts, ","))
}
return nil
}
// createRemoteCleanUpCommand will generated a powershell script that will remove remote files;
// returning a command that can be executed remotely to do the cleanup.
func (p *Provisioner) createRemoteCleanUpCommand(remoteFiles []string) (string, error) {
if len(remoteFiles) == 0 {
return "", fmt.Errorf("no remoteFiles provided for cleanup")
}
var b strings.Builder
// This script should self destruct.
remotePath := p.config.remoteCleanUpScriptPath
remoteFiles = append(remoteFiles, remotePath)
for _, filename := range remoteFiles {
fmt.Fprintf(&b, "Remove-Item %s\n", filename)
}
if err := p.communicator.Upload(remotePath, strings.NewReader(b.String()), nil); err != nil {
return "", fmt.Errorf("clean up script %q failed to upload: %s", remotePath, err)
}
data := map[string]string{
"Path": remotePath,
"Vars": p.config.RemoteEnvVarPath,
}
p.config.ctx.Data = data
return interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
}
// Environment variables required within the remote environment are uploaded
// within a PS script and then enabled by 'dot sourcing' the script
// immediately prior to execution of the main command
@ -388,9 +450,6 @@ func (p *Provisioner) uploadEnvVars(flattenedEnvVars string) (err error) {
}
return err
})
if err != nil {
return err
}
return
}

View File

@ -27,6 +27,7 @@ type FlatConfig struct {
ExecuteCommand *string `mapstructure:"execute_command" cty:"execute_command"`
RemoteEnvVarPath *string `mapstructure:"remote_env_var_path" cty:"remote_env_var_path"`
ElevatedExecuteCommand *string `mapstructure:"elevated_execute_command" cty:"elevated_execute_command"`
SkipClean *bool `mapstructure:"skip_clean" cty:"skip_clean"`
StartRetryTimeout *string `mapstructure:"start_retry_timeout" cty:"start_retry_timeout"`
ElevatedEnvVarFormat *string `mapstructure:"elevated_env_var_format" cty:"elevated_env_var_format"`
ElevatedUser *string `mapstructure:"elevated_user" cty:"elevated_user"`
@ -64,6 +65,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"execute_command": &hcldec.AttrSpec{Name: "execute_command", Type: cty.String, Required: false},
"remote_env_var_path": &hcldec.AttrSpec{Name: "remote_env_var_path", Type: cty.String, Required: false},
"elevated_execute_command": &hcldec.AttrSpec{Name: "elevated_execute_command", Type: cty.String, Required: false},
"skip_clean": &hcldec.AttrSpec{Name: "skip_clean", Type: cty.Bool, Required: false},
"start_retry_timeout": &hcldec.AttrSpec{Name: "start_retry_timeout", Type: cty.String, Required: false},
"elevated_env_var_format": &hcldec.AttrSpec{Name: "elevated_env_var_format", Type: cty.String, Required: false},
"elevated_user": &hcldec.AttrSpec{Name: "elevated_user", Type: cty.String, Required: false},

View File

@ -8,24 +8,31 @@ import (
"path/filepath"
"testing"
"github.com/hashicorp/packer/provisioner/powershell"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/packer/command"
"github.com/hashicorp/packer/helper/tests/acc"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/provisioner/powershell"
windowsshellprovisioner "github.com/hashicorp/packer/provisioner/windows-shell"
)
const TestProvisionerName = "powershell"
func TestPowershellProvisioner_Inline(t *testing.T) {
func TestAccPowershellProvisioner_basic(t *testing.T) {
acc.TestProvisionersPreCheck(TestProvisionerName, t)
testProvisioner := PowershellProvisionerAccTest{"powershell-provisioner-cleanup.txt"}
acc.TestProvisionersAgainstBuilders(&testProvisioner, t)
}
func TestAccPowershellProvisioner_Inline(t *testing.T) {
acc.TestProvisionersPreCheck(TestProvisionerName, t)
testProvisioner := PowershellProvisionerAccTest{"powershell-inline-provisioner.txt"}
acc.TestProvisionersAgainstBuilders(&testProvisioner, t)
}
func TestPowershellProvisioner_Script(t *testing.T) {
func TestAccPowershellProvisioner_Script(t *testing.T) {
acc.TestProvisionersPreCheck(TestProvisionerName, t)
testProvisioner := PowershellProvisionerAccTest{"powershell-script-provisioner.txt"}
@ -58,6 +65,7 @@ func (s *PowershellProvisionerAccTest) GetConfig() (string, error) {
func (s *PowershellProvisionerAccTest) GetProvisionerStore() packer.MapOfProvisioner {
return packer.MapOfProvisioner{
TestProvisionerName: func() (packer.Provisioner, error) { return &powershell.Provisioner{}, nil },
"windows-shell": func() (packer.Provisioner, error) { return &windowsshellprovisioner.Provisioner{}, nil },
}
}

View File

@ -15,16 +15,6 @@ import (
"github.com/stretchr/testify/assert"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"inline": []interface{}{"foo", "bar"},
}
}
func init() {
//log.SetOutput(ioutil.Discard)
}
func TestProvisionerPrepare_extractScript(t *testing.T) {
config := testConfig()
p := new(Provisioner)
@ -335,11 +325,6 @@ func testUi() *packer.BasicUi {
}
}
func testObjects() (packer.Ui, packer.Communicator) {
ui := testUi()
return ui, new(packer.MockCommunicator)
}
func TestProvisionerProvision_ValidExitCodes(t *testing.T) {
config := testConfig()
delete(config, "inline")
@ -387,7 +372,8 @@ func TestProvisionerProvision_InvalidExitCodes(t *testing.T) {
}
func TestProvisionerProvision_Inline(t *testing.T) {
config := testConfig()
// skip_clean is set to true otherwise the last command executed by the provisioner is the cleanup.
config := testConfigWithSkipClean()
delete(config, "inline")
// Defaults provided by Packer
@ -400,7 +386,7 @@ func TestProvisionerProvision_Inline(t *testing.T) {
p.config.PackerBuildName = "vmware"
p.config.PackerBuilderType = "iso"
comm := new(packer.MockCommunicator)
p.Prepare(config)
_ = p.Prepare(config)
err := p.Provision(context.Background(), ui, comm, make(map[string]interface{}))
if err != nil {
t.Fatal("should not have error")
@ -439,7 +425,8 @@ func TestProvisionerProvision_Scripts(t *testing.T) {
defer os.Remove(tempFile.Name())
defer tempFile.Close()
config := testConfig()
// skip_clean is set to true otherwise the last command executed by the provisioner is the cleanup.
config := testConfigWithSkipClean()
delete(config, "inline")
config["scripts"] = []string{tempFile.Name()}
config["packer_build_name"] = "foobuild"
@ -465,11 +452,12 @@ func TestProvisionerProvision_Scripts(t *testing.T) {
func TestProvisionerProvision_ScriptsWithEnvVars(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "packer")
config := testConfig()
ui := testUi()
defer os.Remove(tempFile.Name())
defer tempFile.Close()
// skip_clean is set to true otherwise the last command executed by the provisioner is the cleanup.
config := testConfigWithSkipClean()
delete(config, "inline")
config["scripts"] = []string{tempFile.Name()}
@ -499,10 +487,56 @@ func TestProvisionerProvision_ScriptsWithEnvVars(t *testing.T) {
}
}
func TestProvisionerProvision_UISlurp(t *testing.T) {
// UI should be called n times
func TestProvisionerProvision_SkipClean(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "packer")
defer func() {
tempFile.Close()
os.Remove(tempFile.Name())
}()
// UI should receive following messages / output
config := map[string]interface{}{
"scripts": []string{tempFile.Name()},
"remote_path": "c:/Windows/Temp/script.ps1",
}
tt := []struct {
SkipClean bool
LastExecutedCommandRegex string
}{
{
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 }"`,
},
{
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 }"`,
},
}
for _, tc := range tt {
tc := tc
p := new(Provisioner)
ui := testUi()
comm := new(packer.MockCommunicator)
config["skip_clean"] = tc.SkipClean
if err := p.Prepare(config); err != nil {
t.Fatalf("failed to prepare config when SkipClean is %t: %s", tc.SkipClean, err)
}
err := p.Provision(context.Background(), ui, comm, make(map[string]interface{}))
if err != nil {
t.Fatal("should not have error")
}
// When SkipClean is false the last executed command should be the clean up command;
// otherwise it will be the execution command for the provisioning script.
cmd := comm.StartCmd.Command
re := regexp.MustCompile(tc.LastExecutedCommandRegex)
matched := re.MatchString(cmd)
if !matched {
t.Fatalf(`Got unexpected command when SkipClean is %t: %s`, tc.SkipClean, cmd)
}
}
}
func TestProvisionerProvision_UploadFails(t *testing.T) {
@ -771,3 +805,16 @@ func TestCancel(t *testing.T) {
// Don't actually call Cancel() as it performs an os.Exit(0)
// which kills the 'go test' tool
}
func testConfig() map[string]interface{} {
return map[string]interface{}{
"inline": []interface{}{"foo", "bar"},
}
}
func testConfigWithSkipClean() map[string]interface{} {
return map[string]interface{}{
"inline": []interface{}{"foo", "bar"},
"skip_clean": true,
}
}

View File

@ -0,0 +1,13 @@
{
"type": "powershell",
"remote_path": "c:/Windows/Temp/packer-acc-test-script-test.ps1",
"remote_env_var_path": "C:/Windows/Temp/packer-acc-test-vars.ps1",
"inline": [
"Write-Host \"Total files found in Temp directory\" ( dir C:/Windows/Temp/packer-*.ps1 | measure).Count;"
]
},
{
"type": "windows-shell",
"inline": ["dir C:\\Windows\\Temp\\packer-*.ps1"],
"valid_exit_codes": ["1"]
}

View File

@ -142,6 +142,12 @@ The example below is fully functional.
script is uploaded to. The value must be a writable location and any parent
directories must already exist.
- `skip_clean` (bool) - Whether to clean scripts up after executing the provisioner.
Defaults to false. When true any script created by a non-elevated Powershell
provisioner will be removed from the remote machine. Elevated scripts,
along with the scheduled tasks, will always be removed regardless of the
value set for `skip_clean`.
- `start_retry_timeout` (string) - The amount of time to attempt to _start_
the remote process. By default this is "5m" or 5 minutes. This setting
exists in order to deal with times when SSH may restart, such as a system