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