provisioner/powershell: Update remote clean up logic

* Add retry logic so that the provisioner will retry if it fails to upload/execute because of some restart provisioner step
* Add a testConfigWithSkipClean for testing that the provisioner executes the correct commands
* Add a test case for toggling the "skip_clean" config option
This commit is contained in:
Wilken Rivera 2020-03-19 19:50:58 -04:00
parent f6a61e2511
commit 11db6014fa
2 changed files with 103 additions and 43 deletions

View File

@ -73,6 +73,8 @@ type Config struct {
ExecutionPolicy ExecutionPolicy `mapstructure:"execution_policy"`
remoteCleanUpScriptPath string
ctx interpolate.Context
}
@ -155,6 +157,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,
@ -249,6 +253,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))
@ -295,50 +301,57 @@ 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 {
files := []string{p.config.RemotePath, p.config.RemoteEnvVarPath}
command, err := p.cleanUpRemoteFilesCommand(files...)
if err != nil {
log.Printf("failed to prepare packer cleanup script")
}
if p.config.SkipClean {
return nil
}
cmd = &packer.RemoteCmd{Command: command}
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
log.Printf("failed to clean up temporary files")
}
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
}
func (p *Provisioner) cleanUpRemoteFilesCommand(files ...string) (string, error) {
if len(files) == 0 {
return "", fmt.Errorf("no files provided for cleanup")
// 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
baseDir := filepath.Dir(p.config.RemotePath)
uploadPath := filepath.Join(baseDir, fmt.Sprintf("packer-cleanup-%s.ps1", uuid.TimeOrderedUUID()))
// This script should self destruct.
files = append(files, uploadPath)
for _, filename := range files {
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(uploadPath, strings.NewReader(b.String()), nil); err != nil {
log.Printf("packer clean up script %q failed to upload: %s", uploadPath, err)
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": uploadPath,
"Path": remotePath,
"Vars": p.config.RemoteEnvVarPath,
}
p.config.ctx.Data = data

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