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 }