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