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