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