github.com/hashicorp/packer@v1.14.3/provisioner/windows-shell/provisioner.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  //go:generate packer-sdc mapstructure-to-hcl2 -type Config
     5  
     6  // This package implements a provisioner for Packer that executes
     7  // shell scripts within the remote machine.
     8  package shell
     9  
    10  import (
    11  	"bufio"
    12  	"context"
    13  	"errors"
    14  	"fmt"
    15  	"log"
    16  	"os"
    17  	"sort"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/hashicorp/hcl/v2/hcldec"
    22  	"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
    23  	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
    24  	"github.com/hashicorp/packer-plugin-sdk/retry"
    25  	"github.com/hashicorp/packer-plugin-sdk/shell"
    26  	"github.com/hashicorp/packer-plugin-sdk/template/config"
    27  	"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
    28  	"github.com/hashicorp/packer-plugin-sdk/tmp"
    29  )
    30  
    31  // FIXME query remote host or use %SYSTEMROOT%, %TEMP% and more creative filename
    32  const DefaultRemotePath = "c:/Windows/Temp/script.bat"
    33  
    34  type Config struct {
    35  	shell.Provisioner `mapstructure:",squash"`
    36  
    37  	shell.ProvisionerRemoteSpecific `mapstructure:",squash"`
    38  
    39  	// The timeout for retrying to start the process. Until this timeout
    40  	// is reached, if the provisioner can't start a process, it retries.
    41  	// This can be set high to allow for reboots.
    42  	StartRetryTimeout time.Duration `mapstructure:"start_retry_timeout"`
    43  
    44  	ctx interpolate.Context
    45  }
    46  
    47  type Provisioner struct {
    48  	config        Config
    49  	generatedData map[string]interface{}
    50  }
    51  
    52  type ExecuteCommandTemplate struct {
    53  	Vars string
    54  	Path string
    55  }
    56  
    57  func (p *Provisioner) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
    58  
    59  func (p *Provisioner) Prepare(raws ...interface{}) error {
    60  	err := config.Decode(&p.config, &config.DecodeOpts{
    61  		PluginType:         "windows-shell",
    62  		Interpolate:        true,
    63  		InterpolateContext: &p.config.ctx,
    64  		InterpolateFilter: &interpolate.RenderFilter{
    65  			Exclude: []string{
    66  				"execute_command",
    67  			},
    68  		},
    69  	}, raws...)
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	if p.config.EnvVarFormat == "" {
    75  		p.config.EnvVarFormat = `set "%s=%s" && `
    76  	}
    77  
    78  	if p.config.ExecuteCommand == "" {
    79  		p.config.ExecuteCommand = `{{.Vars}}"{{.Path}}"`
    80  	}
    81  
    82  	if p.config.Inline != nil && len(p.config.Inline) == 0 {
    83  		p.config.Inline = nil
    84  	}
    85  
    86  	if p.config.StartRetryTimeout == 0 {
    87  		p.config.StartRetryTimeout = 5 * time.Minute
    88  	}
    89  
    90  	if p.config.RemotePath == "" {
    91  		p.config.RemotePath = DefaultRemotePath
    92  	}
    93  
    94  	if p.config.Scripts == nil {
    95  		p.config.Scripts = make([]string, 0)
    96  	}
    97  
    98  	if p.config.Vars == nil {
    99  		p.config.Vars = make([]string, 0)
   100  	}
   101  
   102  	var errs error
   103  	if p.config.Script != "" && len(p.config.Scripts) > 0 {
   104  		errs = packersdk.MultiErrorAppend(errs,
   105  			errors.New("Only one of script or scripts can be specified."))
   106  	}
   107  
   108  	if p.config.Script != "" {
   109  		p.config.Scripts = []string{p.config.Script}
   110  	}
   111  
   112  	if len(p.config.Scripts) == 0 && p.config.Inline == nil {
   113  		errs = packersdk.MultiErrorAppend(errs,
   114  			errors.New("Either a script file or inline script must be specified."))
   115  	} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
   116  		errs = packersdk.MultiErrorAppend(errs,
   117  			errors.New("Only a script file or an inline script can be specified, not both."))
   118  	}
   119  
   120  	for _, path := range p.config.Scripts {
   121  		if _, err := os.Stat(path); err != nil {
   122  			errs = packersdk.MultiErrorAppend(errs,
   123  				fmt.Errorf("Bad script '%s': %s", path, err))
   124  		}
   125  	}
   126  
   127  	// Do a check for bad environment variables, such as '=foo', 'foobar'
   128  	for _, kv := range p.config.Vars {
   129  		vs := strings.SplitN(kv, "=", 2)
   130  		if len(vs) != 2 || vs[0] == "" {
   131  			errs = packersdk.MultiErrorAppend(errs,
   132  				fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
   133  		}
   134  	}
   135  
   136  	return errs
   137  }
   138  
   139  // This function takes the inline scripts, concatenates them
   140  // into a temporary file and returns a string containing the location
   141  // of said file.
   142  func extractScript(p *Provisioner) (string, error) {
   143  	temp, err := tmp.File("windows-shell-provisioner")
   144  	if err != nil {
   145  		log.Printf("Unable to create temporary file for inline scripts: %s", err)
   146  		return "", err
   147  	}
   148  	writer := bufio.NewWriter(temp)
   149  	for _, command := range p.config.Inline {
   150  		log.Printf("Found command: %s", command)
   151  		if _, err := writer.WriteString(command + "\n"); err != nil {
   152  			return "", fmt.Errorf("Error preparing shell script: %s", err)
   153  		}
   154  	}
   155  
   156  	if err := writer.Flush(); err != nil {
   157  		return "", fmt.Errorf("Error preparing shell script: %s", err)
   158  	}
   159  
   160  	temp.Close()
   161  
   162  	return temp.Name(), nil
   163  }
   164  
   165  func (p *Provisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error {
   166  	ui.Say("Provisioning with windows-shell...")
   167  	scripts := make([]string, len(p.config.Scripts))
   168  	copy(scripts, p.config.Scripts)
   169  	p.generatedData = generatedData
   170  
   171  	if p.config.Inline != nil {
   172  		temp, err := extractScript(p)
   173  		if err != nil {
   174  			ui.Error(fmt.Sprintf("Unable to extract inline scripts into a file: %s", err))
   175  		}
   176  		scripts = append(scripts, temp)
   177  		// Remove temp script containing the inline commands when done
   178  		defer os.Remove(temp)
   179  	}
   180  
   181  	for _, path := range scripts {
   182  		ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path))
   183  
   184  		log.Printf("Opening %s for reading", path)
   185  		f, err := os.Open(path)
   186  		if err != nil {
   187  			return fmt.Errorf("Error opening shell script: %s", err)
   188  		}
   189  		defer f.Close()
   190  
   191  		// Create environment variables to set before executing the command
   192  		flattenedVars := p.createFlattenedEnvVars()
   193  
   194  		// Compile the command
   195  		p.config.ctx.Data = &ExecuteCommandTemplate{
   196  			Vars: flattenedVars,
   197  			Path: p.config.RemotePath,
   198  		}
   199  		command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
   200  		if err != nil {
   201  			return fmt.Errorf("Error processing command: %s", err)
   202  		}
   203  
   204  		// Upload the file and run the command. Do this in the context of
   205  		// a single retryable function so that we don't end up with
   206  		// the case that the upload succeeded, a restart is initiated,
   207  		// and then the command is executed but the file doesn't exist
   208  		// any longer.
   209  		var cmd *packersdk.RemoteCmd
   210  		err = retry.Config{StartTimeout: p.config.StartRetryTimeout}.Run(ctx, func(ctx context.Context) error {
   211  			if _, err := f.Seek(0, 0); err != nil {
   212  				return err
   213  			}
   214  
   215  			if err := comm.Upload(p.config.RemotePath, f, nil); err != nil {
   216  				return fmt.Errorf("Error uploading script: %s", err)
   217  			}
   218  
   219  			cmd = &packersdk.RemoteCmd{Command: command}
   220  			return cmd.RunWithUi(ctx, comm, ui)
   221  		})
   222  		if err != nil {
   223  			return err
   224  		}
   225  
   226  		// Close the original file since we copied it
   227  		f.Close()
   228  
   229  		if err := p.config.ValidExitCode(cmd.ExitStatus()); err != nil {
   230  			return err
   231  		}
   232  	}
   233  
   234  	return nil
   235  }
   236  
   237  func (p *Provisioner) createFlattenedEnvVars() (flattened string) {
   238  	flattened = ""
   239  	envVars := make(map[string]string)
   240  
   241  	// Always available Packer provided env vars
   242  	envVars["PACKER_BUILD_NAME"] = p.config.PackerBuildName
   243  	envVars["PACKER_BUILDER_TYPE"] = p.config.PackerBuilderType
   244  
   245  	// expose ip address variables
   246  	httpAddr := p.generatedData["PackerHTTPAddr"]
   247  	if httpAddr != nil && httpAddr != commonsteps.HttpAddrNotImplemented {
   248  		envVars["PACKER_HTTP_ADDR"] = httpAddr.(string)
   249  	}
   250  	httpIP := p.generatedData["PackerHTTPIP"]
   251  	if httpIP != nil && httpIP != commonsteps.HttpIPNotImplemented {
   252  		envVars["PACKER_HTTP_IP"] = httpIP.(string)
   253  	}
   254  	httpPort := p.generatedData["PackerHTTPPort"]
   255  	if httpPort != nil && httpPort != commonsteps.HttpPortNotImplemented {
   256  		envVars["PACKER_HTTP_PORT"] = httpPort.(string)
   257  	}
   258  
   259  	// Split vars into key/value components
   260  	for _, envVar := range p.config.Vars {
   261  		keyValue := strings.SplitN(envVar, "=", 2)
   262  		envVars[keyValue[0]] = keyValue[1]
   263  	}
   264  
   265  	for k, v := range p.config.Env {
   266  		envVars[k] = v
   267  	}
   268  
   269  	// Create a list of env var keys in sorted order
   270  	var keys []string
   271  	for k := range envVars {
   272  		keys = append(keys, k)
   273  	}
   274  	sort.Strings(keys)
   275  	// Re-assemble vars using OS specific format pattern and flatten
   276  	for _, key := range keys {
   277  		flattened += fmt.Sprintf(p.config.EnvVarFormat, key, envVars[key])
   278  	}
   279  	return
   280  }