github.com/aclaygray/packer@v1.3.2/provisioner/chef-solo/provisioner.go (about) 1 // This package implements a provisioner for Packer that uses 2 // Chef to provision the remote machine, specifically with chef-solo (that is, 3 // without a Chef server). 4 package chefsolo 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 "strings" 14 15 "github.com/hashicorp/packer/common" 16 "github.com/hashicorp/packer/helper/config" 17 "github.com/hashicorp/packer/packer" 18 "github.com/hashicorp/packer/provisioner" 19 "github.com/hashicorp/packer/template/interpolate" 20 ) 21 22 type guestOSTypeConfig struct { 23 executeCommand string 24 installCommand string 25 stagingDir string 26 } 27 28 var guestOSTypeConfigs = map[string]guestOSTypeConfig{ 29 provisioner.UnixOSType: { 30 executeCommand: "{{if .Sudo}}sudo {{end}}chef-solo --no-color -c {{.ConfigPath}} -j {{.JsonPath}}", 31 installCommand: "curl -L https://omnitruck.chef.io/install.sh | {{if .Sudo}}sudo {{end}}bash -s --{{if .Version}} -v {{.Version}}{{end}}", 32 stagingDir: "/tmp/packer-chef-solo", 33 }, 34 provisioner.WindowsOSType: { 35 executeCommand: "c:/opscode/chef/bin/chef-solo.bat --no-color -c {{.ConfigPath}} -j {{.JsonPath}}", 36 installCommand: "powershell.exe -Command \". { iwr -useb https://omnitruck.chef.io/install.ps1 } | iex; Install-Project{{if .Version}} -version {{.Version}}{{end}}\"", 37 stagingDir: "C:/Windows/Temp/packer-chef-solo", 38 }, 39 } 40 41 type Config struct { 42 common.PackerConfig `mapstructure:",squash"` 43 44 ChefEnvironment string `mapstructure:"chef_environment"` 45 ConfigTemplate string `mapstructure:"config_template"` 46 CookbookPaths []string `mapstructure:"cookbook_paths"` 47 RolesPath string `mapstructure:"roles_path"` 48 DataBagsPath string `mapstructure:"data_bags_path"` 49 EncryptedDataBagSecretPath string `mapstructure:"encrypted_data_bag_secret_path"` 50 EnvironmentsPath string `mapstructure:"environments_path"` 51 ExecuteCommand string `mapstructure:"execute_command"` 52 InstallCommand string `mapstructure:"install_command"` 53 RemoteCookbookPaths []string `mapstructure:"remote_cookbook_paths"` 54 Json map[string]interface{} 55 PreventSudo bool `mapstructure:"prevent_sudo"` 56 RunList []string `mapstructure:"run_list"` 57 SkipInstall bool `mapstructure:"skip_install"` 58 StagingDir string `mapstructure:"staging_directory"` 59 GuestOSType string `mapstructure:"guest_os_type"` 60 Version string `mapstructure:"version"` 61 62 ctx interpolate.Context 63 } 64 65 type Provisioner struct { 66 config Config 67 guestOSTypeConfig guestOSTypeConfig 68 guestCommands *provisioner.GuestCommands 69 } 70 71 type ConfigTemplate struct { 72 CookbookPaths string 73 DataBagsPath string 74 EncryptedDataBagSecretPath string 75 RolesPath string 76 EnvironmentsPath string 77 ChefEnvironment string 78 79 // Templates don't support boolean statements until Go 1.2. In the 80 // mean time, we do this. 81 // TODO(mitchellh): Remove when Go 1.2 is released 82 HasDataBagsPath bool 83 HasEncryptedDataBagSecretPath bool 84 HasRolesPath bool 85 HasEnvironmentsPath bool 86 } 87 88 type ExecuteTemplate struct { 89 ConfigPath string 90 JsonPath string 91 Sudo bool 92 } 93 94 type InstallChefTemplate struct { 95 Sudo bool 96 Version string 97 } 98 99 func (p *Provisioner) Prepare(raws ...interface{}) error { 100 err := config.Decode(&p.config, &config.DecodeOpts{ 101 Interpolate: true, 102 InterpolateContext: &p.config.ctx, 103 InterpolateFilter: &interpolate.RenderFilter{ 104 Exclude: []string{ 105 "execute_command", 106 "install_command", 107 }, 108 }, 109 }, raws...) 110 if err != nil { 111 return err 112 } 113 114 if p.config.GuestOSType == "" { 115 p.config.GuestOSType = provisioner.DefaultOSType 116 } 117 p.config.GuestOSType = strings.ToLower(p.config.GuestOSType) 118 119 var ok bool 120 p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType] 121 if !ok { 122 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 123 } 124 125 p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo) 126 if err != nil { 127 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 128 } 129 130 if p.config.ExecuteCommand == "" { 131 p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand 132 } 133 134 if p.config.InstallCommand == "" { 135 p.config.InstallCommand = p.guestOSTypeConfig.installCommand 136 } 137 138 if p.config.RunList == nil { 139 p.config.RunList = make([]string, 0) 140 } 141 142 if p.config.StagingDir == "" { 143 p.config.StagingDir = p.guestOSTypeConfig.stagingDir 144 } 145 146 var errs *packer.MultiError 147 if p.config.ConfigTemplate != "" { 148 fi, err := os.Stat(p.config.ConfigTemplate) 149 if err != nil { 150 errs = packer.MultiErrorAppend( 151 errs, fmt.Errorf("Bad config template path: %s", err)) 152 } else if fi.IsDir() { 153 errs = packer.MultiErrorAppend( 154 errs, fmt.Errorf("Config template path must be a file: %s", err)) 155 } 156 } 157 158 for _, path := range p.config.CookbookPaths { 159 pFileInfo, err := os.Stat(path) 160 161 if err != nil || !pFileInfo.IsDir() { 162 errs = packer.MultiErrorAppend( 163 errs, fmt.Errorf("Bad cookbook path '%s': %s", path, err)) 164 } 165 } 166 167 if p.config.RolesPath != "" { 168 pFileInfo, err := os.Stat(p.config.RolesPath) 169 170 if err != nil || !pFileInfo.IsDir() { 171 errs = packer.MultiErrorAppend( 172 errs, fmt.Errorf("Bad roles path '%s': %s", p.config.RolesPath, err)) 173 } 174 } 175 176 if p.config.DataBagsPath != "" { 177 pFileInfo, err := os.Stat(p.config.DataBagsPath) 178 179 if err != nil || !pFileInfo.IsDir() { 180 errs = packer.MultiErrorAppend( 181 errs, fmt.Errorf("Bad data bags path '%s': %s", p.config.DataBagsPath, err)) 182 } 183 } 184 185 if p.config.EncryptedDataBagSecretPath != "" { 186 pFileInfo, err := os.Stat(p.config.EncryptedDataBagSecretPath) 187 188 if err != nil || pFileInfo.IsDir() { 189 errs = packer.MultiErrorAppend( 190 errs, fmt.Errorf("Bad encrypted data bag secret '%s': %s", p.config.EncryptedDataBagSecretPath, err)) 191 } 192 } 193 194 if p.config.EnvironmentsPath != "" { 195 pFileInfo, err := os.Stat(p.config.EnvironmentsPath) 196 197 if err != nil || !pFileInfo.IsDir() { 198 errs = packer.MultiErrorAppend( 199 errs, fmt.Errorf("Bad environments path '%s': %s", p.config.EnvironmentsPath, err)) 200 } 201 } 202 203 jsonValid := true 204 for k, v := range p.config.Json { 205 p.config.Json[k], err = p.deepJsonFix(k, v) 206 if err != nil { 207 errs = packer.MultiErrorAppend( 208 errs, fmt.Errorf("Error processing JSON: %s", err)) 209 jsonValid = false 210 } 211 } 212 213 if jsonValid { 214 // Process the user variables within the JSON and set the JSON. 215 // Do this early so that we can validate and show errors. 216 p.config.Json, err = p.processJsonUserVars() 217 if err != nil { 218 errs = packer.MultiErrorAppend( 219 errs, fmt.Errorf("Error processing user variables in JSON: %s", err)) 220 } 221 } 222 223 if errs != nil && len(errs.Errors) > 0 { 224 return errs 225 } 226 227 return nil 228 } 229 230 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 231 ui.Say("Provisioning with chef-solo") 232 233 if !p.config.SkipInstall { 234 if err := p.installChef(ui, comm, p.config.Version); err != nil { 235 return fmt.Errorf("Error installing Chef: %s", err) 236 } 237 } 238 239 if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { 240 return fmt.Errorf("Error creating staging directory: %s", err) 241 } 242 243 cookbookPaths := make([]string, 0, len(p.config.CookbookPaths)) 244 for i, path := range p.config.CookbookPaths { 245 targetPath := fmt.Sprintf("%s/cookbooks-%d", p.config.StagingDir, i) 246 if err := p.uploadDirectory(ui, comm, targetPath, path); err != nil { 247 return fmt.Errorf("Error uploading cookbooks: %s", err) 248 } 249 250 cookbookPaths = append(cookbookPaths, targetPath) 251 } 252 253 rolesPath := "" 254 if p.config.RolesPath != "" { 255 rolesPath = fmt.Sprintf("%s/roles", p.config.StagingDir) 256 if err := p.uploadDirectory(ui, comm, rolesPath, p.config.RolesPath); err != nil { 257 return fmt.Errorf("Error uploading roles: %s", err) 258 } 259 } 260 261 dataBagsPath := "" 262 if p.config.DataBagsPath != "" { 263 dataBagsPath = fmt.Sprintf("%s/data_bags", p.config.StagingDir) 264 if err := p.uploadDirectory(ui, comm, dataBagsPath, p.config.DataBagsPath); err != nil { 265 return fmt.Errorf("Error uploading data bags: %s", err) 266 } 267 } 268 269 encryptedDataBagSecretPath := "" 270 if p.config.EncryptedDataBagSecretPath != "" { 271 encryptedDataBagSecretPath = fmt.Sprintf("%s/encrypted_data_bag_secret", p.config.StagingDir) 272 if err := p.uploadFile(ui, comm, encryptedDataBagSecretPath, p.config.EncryptedDataBagSecretPath); err != nil { 273 return fmt.Errorf("Error uploading encrypted data bag secret: %s", err) 274 } 275 } 276 277 environmentsPath := "" 278 if p.config.EnvironmentsPath != "" { 279 environmentsPath = fmt.Sprintf("%s/environments", p.config.StagingDir) 280 if err := p.uploadDirectory(ui, comm, environmentsPath, p.config.EnvironmentsPath); err != nil { 281 return fmt.Errorf("Error uploading environments: %s", err) 282 } 283 } 284 285 configPath, err := p.createConfig(ui, comm, cookbookPaths, rolesPath, dataBagsPath, encryptedDataBagSecretPath, environmentsPath, p.config.ChefEnvironment) 286 if err != nil { 287 return fmt.Errorf("Error creating Chef config file: %s", err) 288 } 289 290 jsonPath, err := p.createJson(ui, comm) 291 if err != nil { 292 return fmt.Errorf("Error creating JSON attributes: %s", err) 293 } 294 295 if err := p.executeChef(ui, comm, configPath, jsonPath); err != nil { 296 return fmt.Errorf("Error executing Chef: %s", err) 297 } 298 299 return nil 300 } 301 302 func (p *Provisioner) Cancel() { 303 // Just hard quit. It isn't a big deal if what we're doing keeps 304 // running on the other side. 305 os.Exit(0) 306 } 307 308 func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 309 if err := p.createDir(ui, comm, dst); err != nil { 310 return err 311 } 312 313 // Make sure there is a trailing "/" so that the directory isn't 314 // created on the other side. 315 if src[len(src)-1] != '/' { 316 src = src + "/" 317 } 318 319 return comm.UploadDir(dst, src, nil) 320 } 321 322 func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 323 f, err := os.Open(src) 324 if err != nil { 325 return err 326 } 327 defer f.Close() 328 329 return comm.Upload(dst, f, nil) 330 } 331 332 func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, localCookbooks []string, rolesPath string, dataBagsPath string, encryptedDataBagSecretPath string, environmentsPath string, chefEnvironment string) (string, error) { 333 ui.Message("Creating configuration file 'solo.rb'") 334 335 cookbook_paths := make([]string, len(p.config.RemoteCookbookPaths)+len(localCookbooks)) 336 for i, path := range p.config.RemoteCookbookPaths { 337 cookbook_paths[i] = fmt.Sprintf(`"%s"`, path) 338 } 339 340 for i, path := range localCookbooks { 341 i = len(p.config.RemoteCookbookPaths) + i 342 cookbook_paths[i] = fmt.Sprintf(`"%s"`, path) 343 } 344 345 // Read the template 346 tpl := DefaultConfigTemplate 347 if p.config.ConfigTemplate != "" { 348 f, err := os.Open(p.config.ConfigTemplate) 349 if err != nil { 350 return "", err 351 } 352 defer f.Close() 353 354 tplBytes, err := ioutil.ReadAll(f) 355 if err != nil { 356 return "", err 357 } 358 359 tpl = string(tplBytes) 360 } 361 362 p.config.ctx.Data = &ConfigTemplate{ 363 CookbookPaths: strings.Join(cookbook_paths, ","), 364 RolesPath: rolesPath, 365 DataBagsPath: dataBagsPath, 366 EncryptedDataBagSecretPath: encryptedDataBagSecretPath, 367 EnvironmentsPath: environmentsPath, 368 HasRolesPath: rolesPath != "", 369 HasDataBagsPath: dataBagsPath != "", 370 HasEncryptedDataBagSecretPath: encryptedDataBagSecretPath != "", 371 HasEnvironmentsPath: environmentsPath != "", 372 ChefEnvironment: chefEnvironment, 373 } 374 configString, err := interpolate.Render(tpl, &p.config.ctx) 375 if err != nil { 376 return "", err 377 } 378 379 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "solo.rb")) 380 if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil { 381 return "", err 382 } 383 384 return remotePath, nil 385 } 386 387 func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) { 388 ui.Message("Creating JSON attribute file") 389 390 jsonData := make(map[string]interface{}) 391 392 // Copy the configured JSON 393 for k, v := range p.config.Json { 394 jsonData[k] = v 395 } 396 397 // Set the run list if it was specified 398 if len(p.config.RunList) > 0 { 399 jsonData["run_list"] = p.config.RunList 400 } 401 402 jsonBytes, err := json.MarshalIndent(jsonData, "", " ") 403 if err != nil { 404 return "", err 405 } 406 407 // Upload the bytes 408 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "node.json")) 409 if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes), nil); err != nil { 410 return "", err 411 } 412 413 return remotePath, nil 414 } 415 416 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 417 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 418 419 cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)} 420 if err := cmd.StartWithUi(comm, ui); err != nil { 421 return err 422 } 423 if cmd.ExitStatus != 0 { 424 return fmt.Errorf("Non-zero exit status. See output above for more info.") 425 } 426 427 // Chmod the directory to 0777 just so that we can access it as our user 428 cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")} 429 if err := cmd.StartWithUi(comm, ui); err != nil { 430 return err 431 } 432 if cmd.ExitStatus != 0 { 433 return fmt.Errorf("Non-zero exit status. See output above for more info.") 434 } 435 436 return nil 437 } 438 439 func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error { 440 p.config.ctx.Data = &ExecuteTemplate{ 441 ConfigPath: config, 442 JsonPath: json, 443 Sudo: !p.config.PreventSudo, 444 } 445 command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 446 if err != nil { 447 return err 448 } 449 450 ui.Message(fmt.Sprintf("Executing Chef: %s", command)) 451 452 cmd := &packer.RemoteCmd{ 453 Command: command, 454 } 455 456 if err := cmd.StartWithUi(comm, ui); err != nil { 457 return err 458 } 459 460 if cmd.ExitStatus != 0 { 461 return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) 462 } 463 464 return nil 465 } 466 467 func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator, version string) error { 468 ui.Message("Installing Chef...") 469 470 p.config.ctx.Data = &InstallChefTemplate{ 471 Sudo: !p.config.PreventSudo, 472 Version: version, 473 } 474 command, err := interpolate.Render(p.config.InstallCommand, &p.config.ctx) 475 if err != nil { 476 return err 477 } 478 479 cmd := &packer.RemoteCmd{Command: command} 480 if err := cmd.StartWithUi(comm, ui); err != nil { 481 return err 482 } 483 484 if cmd.ExitStatus != 0 { 485 return fmt.Errorf( 486 "Install script exited with non-zero exit status %d", cmd.ExitStatus) 487 } 488 489 return nil 490 } 491 492 func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) { 493 if current == nil { 494 return nil, nil 495 } 496 497 switch c := current.(type) { 498 case []interface{}: 499 val := make([]interface{}, len(c)) 500 for i, v := range c { 501 var err error 502 val[i], err = p.deepJsonFix(fmt.Sprintf("%s[%d]", key, i), v) 503 if err != nil { 504 return nil, err 505 } 506 } 507 508 return val, nil 509 case []uint8: 510 return string(c), nil 511 case map[interface{}]interface{}: 512 val := make(map[string]interface{}) 513 for k, v := range c { 514 ks, ok := k.(string) 515 if !ok { 516 return nil, fmt.Errorf("%s: key is not string", key) 517 } 518 519 var err error 520 val[ks], err = p.deepJsonFix( 521 fmt.Sprintf("%s.%s", key, ks), v) 522 if err != nil { 523 return nil, err 524 } 525 } 526 527 return val, nil 528 default: 529 return current, nil 530 } 531 } 532 533 func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) { 534 jsonBytes, err := json.Marshal(p.config.Json) 535 if err != nil { 536 // This really shouldn't happen since we literally just unmarshalled 537 panic(err) 538 } 539 540 // Copy the user variables so that we can restore them later, and 541 // make sure we make the quotes JSON-friendly in the user variables. 542 originalUserVars := make(map[string]string) 543 for k, v := range p.config.ctx.UserVariables { 544 originalUserVars[k] = v 545 } 546 547 // Make sure we reset them no matter what 548 defer func() { 549 p.config.ctx.UserVariables = originalUserVars 550 }() 551 552 // Make the current user variables JSON string safe. 553 for k, v := range p.config.ctx.UserVariables { 554 v = strings.Replace(v, `\`, `\\`, -1) 555 v = strings.Replace(v, `"`, `\"`, -1) 556 p.config.ctx.UserVariables[k] = v 557 } 558 559 // Process the bytes with the template processor 560 p.config.ctx.Data = nil 561 jsonBytesProcessed, err := interpolate.Render(string(jsonBytes), &p.config.ctx) 562 if err != nil { 563 return nil, err 564 } 565 566 var result map[string]interface{} 567 if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil { 568 return nil, err 569 } 570 571 return result, nil 572 } 573 574 var DefaultConfigTemplate = ` 575 cookbook_path [{{.CookbookPaths}}] 576 {{if .HasRolesPath}} 577 role_path "{{.RolesPath}}" 578 {{end}} 579 {{if .HasDataBagsPath}} 580 data_bag_path "{{.DataBagsPath}}" 581 {{end}} 582 {{if .HasEncryptedDataBagSecretPath}} 583 encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}" 584 {{end}} 585 {{if .HasEnvironmentsPath}} 586 environment_path "{{.EnvironmentsPath}}" 587 environment "{{.ChefEnvironment}}" 588 {{end}} 589 `