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