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