github.com/bendemaree/terraform@v0.5.4-0.20150613200311-f50d97d6eee6/builtin/provisioners/chef/resource_provisioner.go (about) 1 package chef 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "log" 10 "os" 11 "path" 12 "regexp" 13 "strings" 14 "text/template" 15 "time" 16 17 "github.com/hashicorp/terraform/communicator" 18 "github.com/hashicorp/terraform/communicator/remote" 19 "github.com/hashicorp/terraform/terraform" 20 "github.com/mitchellh/go-homedir" 21 "github.com/mitchellh/go-linereader" 22 "github.com/mitchellh/mapstructure" 23 ) 24 25 const ( 26 clienrb = "client.rb" 27 defaultEnv = "_default" 28 firstBoot = "first-boot.json" 29 logfileDir = "logfiles" 30 linuxConfDir = "/etc/chef" 31 validationKey = "validation.pem" 32 windowsConfDir = "C:/chef" 33 ) 34 35 const clientConf = ` 36 log_location STDOUT 37 chef_server_url "{{ .ServerURL }}" 38 validation_client_name "{{ .ValidationClientName }}" 39 node_name "{{ .NodeName }}" 40 41 {{ if .HTTPProxy }} 42 http_proxy "{{ .HTTPProxy }}" 43 ENV['http_proxy'] = "{{ .HTTPProxy }}" 44 ENV['HTTP_PROXY'] = "{{ .HTTPProxy }}" 45 {{ end }} 46 47 {{ if .HTTPSProxy }} 48 https_proxy "{{ .HTTPSProxy }}" 49 ENV['https_proxy'] = "{{ .HTTPSProxy }}" 50 ENV['HTTPS_PROXY'] = "{{ .HTTPSProxy }}" 51 {{ end }} 52 53 {{ if .NOProxy }}no_proxy "{{ join .NOProxy "," }}"{{ end }} 54 {{ if .SSLVerifyMode }}ssl_verify_mode {{ .SSLVerifyMode }}{{ end }} 55 ` 56 57 // Provisioner represents a specificly configured chef provisioner 58 type Provisioner struct { 59 Attributes interface{} `mapstructure:"attributes"` 60 Environment string `mapstructure:"environment"` 61 LogToFile bool `mapstructure:"log_to_file"` 62 HTTPProxy string `mapstructure:"http_proxy"` 63 HTTPSProxy string `mapstructure:"https_proxy"` 64 NOProxy []string `mapstructure:"no_proxy"` 65 NodeName string `mapstructure:"node_name"` 66 PreventSudo bool `mapstructure:"prevent_sudo"` 67 RunList []string `mapstructure:"run_list"` 68 ServerURL string `mapstructure:"server_url"` 69 SkipInstall bool `mapstructure:"skip_install"` 70 SSLVerifyMode string `mapstructure:"ssl_verify_mode"` 71 ValidationClientName string `mapstructure:"validation_client_name"` 72 ValidationKeyPath string `mapstructure:"validation_key_path"` 73 Version string `mapstructure:"version"` 74 75 installChefClient func(terraform.UIOutput, communicator.Communicator) error 76 createConfigFiles func(terraform.UIOutput, communicator.Communicator) error 77 runChefClient func(terraform.UIOutput, communicator.Communicator) error 78 useSudo bool 79 } 80 81 // ResourceProvisioner represents a generic chef provisioner 82 type ResourceProvisioner struct{} 83 84 // Apply executes the file provisioner 85 func (r *ResourceProvisioner) Apply( 86 o terraform.UIOutput, 87 s *terraform.InstanceState, 88 c *terraform.ResourceConfig) error { 89 // Decode the raw config for this provisioner 90 p, err := r.decodeConfig(c) 91 if err != nil { 92 return err 93 } 94 95 // Set some values based on the targeted OS 96 switch s.Ephemeral.ConnInfo["type"] { 97 case "ssh", "": // The default connection type is ssh, so if the type is empty use ssh 98 p.installChefClient = p.sshInstallChefClient 99 p.createConfigFiles = p.sshCreateConfigFiles 100 p.runChefClient = p.runChefClientFunc(linuxConfDir) 101 p.useSudo = !p.PreventSudo && s.Ephemeral.ConnInfo["user"] != "root" 102 case "winrm": 103 p.installChefClient = p.winrmInstallChefClient 104 p.createConfigFiles = p.winrmCreateConfigFiles 105 p.runChefClient = p.runChefClientFunc(windowsConfDir) 106 p.useSudo = false 107 default: 108 return fmt.Errorf("Unsupported connection type: %s", s.Ephemeral.ConnInfo["type"]) 109 } 110 111 // Get a new communicator 112 comm, err := communicator.New(s) 113 if err != nil { 114 return err 115 } 116 117 // Wait and retry until we establish the connection 118 err = retryFunc(comm.Timeout(), func() error { 119 err := comm.Connect(o) 120 return err 121 }) 122 if err != nil { 123 return err 124 } 125 defer comm.Disconnect() 126 127 if !p.SkipInstall { 128 if err := p.installChefClient(o, comm); err != nil { 129 return err 130 } 131 } 132 133 o.Output("Creating configuration files...") 134 if err := p.createConfigFiles(o, comm); err != nil { 135 return err 136 } 137 138 o.Output("Starting initial Chef-Client run...") 139 if err := p.runChefClient(o, comm); err != nil { 140 return err 141 } 142 143 return nil 144 } 145 146 // Validate checks if the required arguments are configured 147 func (r *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) { 148 p, err := r.decodeConfig(c) 149 if err != nil { 150 es = append(es, err) 151 return ws, es 152 } 153 154 if p.NodeName == "" { 155 es = append(es, fmt.Errorf("Key not found: node_name")) 156 } 157 if p.RunList == nil { 158 es = append(es, fmt.Errorf("Key not found: run_list")) 159 } 160 if p.ServerURL == "" { 161 es = append(es, fmt.Errorf("Key not found: server_url")) 162 } 163 if p.ValidationClientName == "" { 164 es = append(es, fmt.Errorf("Key not found: validation_client_name")) 165 } 166 if p.ValidationKeyPath == "" { 167 es = append(es, fmt.Errorf("Key not found: validation_key_path")) 168 } 169 170 return ws, es 171 } 172 173 func (r *ResourceProvisioner) decodeConfig(c *terraform.ResourceConfig) (*Provisioner, error) { 174 p := new(Provisioner) 175 176 decConf := &mapstructure.DecoderConfig{ 177 ErrorUnused: true, 178 WeaklyTypedInput: true, 179 Result: p, 180 } 181 dec, err := mapstructure.NewDecoder(decConf) 182 if err != nil { 183 return nil, err 184 } 185 186 // We need to decode this twice. Once for the Raw config and once 187 // for the parsed Config. This makes sure that all values are there 188 // even if some still need to be interpolated later on. 189 // Without this the validation will fail when using a variable for 190 // a required parameter (the node_name for example). 191 if err := dec.Decode(c.Raw); err != nil { 192 return nil, err 193 } 194 195 if err := dec.Decode(c.Config); err != nil { 196 return nil, err 197 } 198 199 if p.Environment == "" { 200 p.Environment = defaultEnv 201 } 202 203 if p.ValidationKeyPath != "" { 204 keyPath, err := homedir.Expand(p.ValidationKeyPath) 205 if err != nil { 206 return nil, fmt.Errorf("Error expanding the validation key path: %v", err) 207 } 208 p.ValidationKeyPath = keyPath 209 } 210 211 if attrs, ok := c.Config["attributes"]; ok { 212 p.Attributes, err = rawToJSON(attrs) 213 if err != nil { 214 return nil, fmt.Errorf("Error parsing the attributes: %v", err) 215 } 216 } 217 218 return p, nil 219 } 220 221 func rawToJSON(raw interface{}) (interface{}, error) { 222 switch s := raw.(type) { 223 case []map[string]interface{}: 224 if len(s) != 1 { 225 return nil, errors.New("unexpected input while parsing raw config to JSON") 226 } 227 228 var err error 229 for k, v := range s[0] { 230 s[0][k], err = rawToJSON(v) 231 if err != nil { 232 return nil, err 233 } 234 } 235 236 return s[0], nil 237 default: 238 return raw, nil 239 } 240 } 241 242 // retryFunc is used to retry a function for a given duration 243 func retryFunc(timeout time.Duration, f func() error) error { 244 finish := time.After(timeout) 245 for { 246 err := f() 247 if err == nil { 248 return nil 249 } 250 log.Printf("Retryable error: %v", err) 251 252 select { 253 case <-finish: 254 return err 255 case <-time.After(3 * time.Second): 256 } 257 } 258 } 259 260 func (p *Provisioner) runChefClientFunc( 261 confDir string) func(terraform.UIOutput, communicator.Communicator) error { 262 return func(o terraform.UIOutput, comm communicator.Communicator) error { 263 fb := path.Join(confDir, firstBoot) 264 cmd := fmt.Sprintf("chef-client -j %q -E %q", fb, p.Environment) 265 266 if p.LogToFile { 267 if err := os.MkdirAll(logfileDir, 0755); err != nil { 268 return fmt.Errorf("Error creating logfile directory %s: %v", logfileDir, err) 269 } 270 271 logFile := path.Join(logfileDir, p.NodeName) 272 f, err := os.Create(path.Join(logFile)) 273 if err != nil { 274 return fmt.Errorf("Error creating logfile %s: %v", logFile, err) 275 } 276 f.Close() 277 278 o.Output("Writing Chef Client output to " + logFile) 279 o = p 280 } 281 282 return p.runCommand(o, comm, cmd) 283 } 284 } 285 286 // Output implementation of terraform.UIOutput interface 287 func (p *Provisioner) Output(output string) { 288 logFile := path.Join(logfileDir, p.NodeName) 289 f, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY, 0666) 290 if err != nil { 291 log.Printf("Error creating logfile %s: %v", logFile, err) 292 return 293 } 294 defer f.Close() 295 296 // These steps are needed to remove any ANSI escape codes used to colorize 297 // the output and to make sure we have proper line endings before writing 298 // the string to the logfile. 299 re := regexp.MustCompile(`\x1b\[[0-9;]+m`) 300 output = re.ReplaceAllString(output, "") 301 output = strings.Replace(output, "\r", "\n", -1) 302 303 if _, err := f.WriteString(output); err != nil { 304 log.Printf("Error writing output to logfile %s: %v", logFile, err) 305 } 306 307 if err := f.Sync(); err != nil { 308 log.Printf("Error saving logfile %s to disk: %v", logFile, err) 309 } 310 } 311 312 func (p *Provisioner) deployConfigFiles( 313 o terraform.UIOutput, 314 comm communicator.Communicator, 315 confDir string) error { 316 // Open the validation key file 317 f, err := os.Open(p.ValidationKeyPath) 318 if err != nil { 319 return err 320 } 321 defer f.Close() 322 323 // Copy the validation key to the new instance 324 if err := comm.Upload(path.Join(confDir, validationKey), f); err != nil { 325 return fmt.Errorf("Uploading %s failed: %v", validationKey, err) 326 } 327 328 // Make strings.Join available for use within the template 329 funcMap := template.FuncMap{ 330 "join": strings.Join, 331 } 332 333 // Create a new template and parse the client config into it 334 t := template.Must(template.New(clienrb).Funcs(funcMap).Parse(clientConf)) 335 336 var buf bytes.Buffer 337 err = t.Execute(&buf, p) 338 if err != nil { 339 return fmt.Errorf("Error executing %s template: %s", clienrb, err) 340 } 341 342 // Copy the client config to the new instance 343 if err := comm.Upload(path.Join(confDir, clienrb), &buf); err != nil { 344 return fmt.Errorf("Uploading %s failed: %v", clienrb, err) 345 } 346 347 // Create a map with first boot settings 348 fb := make(map[string]interface{}) 349 if p.Attributes != nil { 350 fb = p.Attributes.(map[string]interface{}) 351 } 352 353 // Check if the run_list was also in the attributes and if so log a warning 354 // that it will be overwritten with the value of the run_list argument. 355 if _, found := fb["run_list"]; found { 356 log.Printf("[WARNING] Found a 'run_list' specified in the configured attributes! " + 357 "This value will be overwritten by the value of the `run_list` argument!") 358 } 359 360 // Add the initial runlist to the first boot settings 361 fb["run_list"] = p.RunList 362 363 // Marshal the first boot settings to JSON 364 d, err := json.Marshal(fb) 365 if err != nil { 366 return fmt.Errorf("Failed to create %s data: %s", firstBoot, err) 367 } 368 369 // Copy the first-boot.json to the new instance 370 if err := comm.Upload(path.Join(confDir, firstBoot), bytes.NewReader(d)); err != nil { 371 return fmt.Errorf("Uploading %s failed: %v", firstBoot, err) 372 } 373 374 return nil 375 } 376 377 // runCommand is used to run already prepared commands 378 func (p *Provisioner) runCommand( 379 o terraform.UIOutput, 380 comm communicator.Communicator, 381 command string) error { 382 var err error 383 384 // Unless prevented, prefix the command with sudo 385 if p.useSudo { 386 command = "sudo " + command 387 } 388 389 outR, outW := io.Pipe() 390 errR, errW := io.Pipe() 391 outDoneCh := make(chan struct{}) 392 errDoneCh := make(chan struct{}) 393 go p.copyOutput(o, outR, outDoneCh) 394 go p.copyOutput(o, errR, errDoneCh) 395 396 cmd := &remote.Cmd{ 397 Command: command, 398 Stdout: outW, 399 Stderr: errW, 400 } 401 402 if err := comm.Start(cmd); err != nil { 403 return fmt.Errorf("Error executing command %q: %v", cmd.Command, err) 404 } 405 406 cmd.Wait() 407 if cmd.ExitStatus != 0 { 408 err = fmt.Errorf( 409 "Command %q exited with non-zero exit status: %d", cmd.Command, cmd.ExitStatus) 410 } 411 412 // Wait for output to clean up 413 outW.Close() 414 errW.Close() 415 <-outDoneCh 416 <-errDoneCh 417 418 // If we have an error, return it out now that we've cleaned up 419 if err != nil { 420 return err 421 } 422 423 return nil 424 } 425 426 func (p *Provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { 427 defer close(doneCh) 428 lr := linereader.New(r) 429 for line := range lr.Ch { 430 o.Output(line) 431 } 432 }