From 5e841befeea03acbbc6d2751e68ea24049632c9c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 Aug 2013 12:20:35 -0700 Subject: [PATCH] provisioner/chef-solo: massive refactor and style nitpick --- provisioner/chef-solo/provisioner.go | 336 ++++++---------------- provisioner/chef-solo/provisioner_test.go | 39 +-- 2 files changed, 106 insertions(+), 269 deletions(-) diff --git a/provisioner/chef-solo/provisioner.go b/provisioner/chef-solo/provisioner.go index 62e197d3a..700bbd33a 100644 --- a/provisioner/chef-solo/provisioner.go +++ b/provisioner/chef-solo/provisioner.go @@ -4,20 +4,14 @@ package chefsolo import ( - "bufio" "bytes" "encoding/json" "fmt" - "github.com/mitchellh/iochan" "github.com/mitchellh/packer/common" "github.com/mitchellh/packer/packer" - "io" - "io/ioutil" - "log" "os" "path/filepath" "strings" - "text/template" ) const ( @@ -32,12 +26,15 @@ var Ui packer.Ui type Config struct { common.PackerConfig `mapstructure:",squash"` - CookbooksPaths []string `mapstructure:"cookbooks_paths"` - InstallCommand string `mapstructure:"install_command"` - Recipes []string - Json map[string]interface{} - PreventSudo bool `mapstructure:"prevent_sudo"` - SkipInstall bool `mapstructure:"skip_install"` + CookbookPaths []string `mapstructure:"cookbook_paths"` + ExecuteCommand string `mapstructure:"execute_command"` + InstallCommand string `mapstructure:"install_command"` + RemoteCookbookPaths []string `mapstructure:"remote_cookbook_paths"` + Json map[string]interface{} + PreventSudo bool `mapstructure:"prevent_sudo"` + RunList []string `mapstructure:"run_list"` + SkipInstall bool `mapstructure:"skip_install"` + StagingDir string `mapstructure:"staging_directory"` tpl *packer.ConfigTemplate } @@ -46,8 +43,12 @@ type Provisioner struct { config Config } -type ExecuteRecipeTemplate struct { - SoloRbPath string +type ConfigTemplate struct { + CookbookPaths string +} + +type ExecuteTemplate struct { + ConfigPath string JsonPath string Sudo bool } @@ -68,31 +69,26 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } p.config.tpl.UserVars = p.config.PackerUserVars - // Accumulate any errors - errs := common.CheckUnusedConfig(md) - - if p.config.CookbooksPaths == nil { - p.config.CookbooksPaths = []string{DefaultCookbooksPath} + if p.config.ExecuteCommand == "" { + p.config.ExecuteCommand = "{{if .Sudo}}sudo {{end}}chef-solo --no-color -c {{.ConfigPath}} -j {{.JsonPath}}" } if p.config.InstallCommand == "" { p.config.InstallCommand = "curl -L https://www.opscode.com/chef/install.sh | {{if .Sudo}}sudo {{end}}bash" } - if p.config.Recipes == nil { - p.config.Recipes = make([]string, 0) + if p.config.RunList == nil { + p.config.RunList = make([]string, 0) } - if p.config.Json != nil { - if _, err := json.Marshal(p.config.Json); err != nil { - errs = packer.MultiErrorAppend( - errs, fmt.Errorf("Bad JSON: %s", err)) - } - } else { - p.config.Json = make(map[string]interface{}) + if p.config.StagingDir == "" { + p.config.StagingDir = "/tmp/packer-chef-solo" } - for _, path := range p.config.CookbooksPaths { + // Accumulate any errors + errs := common.CheckUnusedConfig(md) + + for _, path := range p.config.CookbookPaths { pFileInfo, err := os.Stat(path) if err != nil || !pFileInfo.IsDir() { @@ -109,218 +105,129 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { - var err error - Ui = ui - if !p.config.SkipInstall { if err := p.installChef(ui, comm); err != nil { return fmt.Errorf("Error installing Chef: %s", err) } } - err = CreateRemoteDirectory(RemoteCookbookPath, comm) - if err != nil { - return fmt.Errorf("Error creating remote staging directory: %s", err) + if err := p.createStagingDir(ui, comm); err != nil { + return fmt.Errorf("Error creating staging directory: %s", err) } - soloRbPath, err := CreateSoloRb(p.config.CookbooksPaths, comm) - if err != nil { - return fmt.Errorf("Error creating Chef Solo configuration file: %s", err) - } + // TODO(mitchellh): Upload cookbooks - jsonPath, err := CreateAttributesJson(p.config.Json, p.config.Recipes, comm) + configPath, err := p.createConfig(ui, comm) if err != nil { - return fmt.Errorf("Error uploading JSON attributes file: %s", err) + return fmt.Errorf("Error creating Chef config file: %s", err) } - // Upload all cookbooks - for _, path := range p.config.CookbooksPaths { - ui.Say(fmt.Sprintf("Copying cookbook path: %s", path)) - err = UploadLocalDirectory(path, comm) - if err != nil { - return fmt.Errorf("Error uploading cookbooks: %s", err) - } + jsonPath, err := p.createJson(ui, comm) + if err != nil { + return fmt.Errorf("Error creating JSON attributes: %s", err) } - // Execute requested recipes - ui.Say("Beginning Chef Solo run") - - // Compile the command - var command bytes.Buffer - t := template.Must(template.New("chef-run").Parse("{{if .Sudo}}sudo {{end}}chef-solo --no-color -c {{.SoloRbPath}} -j {{.JsonPath}}")) - t.Execute(&command, &ExecuteRecipeTemplate{soloRbPath, jsonPath, !p.config.PreventSudo}) - - err = executeCommand(command.String(), comm) - if err != nil { - return fmt.Errorf("Error running Chef Solo: %s", err) + if err := p.executeChef(ui, comm, configPath, jsonPath); err != nil { + return fmt.Errorf("Error executing Chef: %s", err) } return nil } -func UploadLocalDirectory(localDir string, comm packer.Communicator) (err error) { - visitPath := func(path string, f os.FileInfo, err error) (err2 error) { - var remotePath = RemoteCookbookPath + "/" + path - if f.IsDir() { - // Make remote directory - err = CreateRemoteDirectory(remotePath, comm) - if err != nil { - return err - } - } else { - // Upload file to existing directory - file, err := os.Open(path) - if err != nil { - return fmt.Errorf("Error opening file: %s", err) - } - - err = comm.Upload(remotePath, file) - if err != nil { - return fmt.Errorf("Error uploading file: %s", err) - } - } - return +func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator) (string, error) { + ui.Message("Creating configuration file 'solo.rb'") + + cookbook_paths := make([]string, len(p.config.RemoteCookbookPaths)) + for i, path := range p.config.RemoteCookbookPaths { + cookbook_paths[i] = fmt.Sprintf(`"%s"`, path) } - log.Printf("Uploading directory %s", localDir) - err = filepath.Walk(localDir, visitPath) + configString, err := p.config.tpl.Process(DefaultConfigTemplate, &ConfigTemplate{ + CookbookPaths: strings.Join(cookbook_paths, ","), + }) if err != nil { - return fmt.Errorf("Error uploading cookbook %s: %s", localDir, err) + return "", err } - return nil -} - -func CreateRemoteDirectory(path string, comm packer.Communicator) (err error) { - log.Printf("Creating remote directory: %s ", path) - - var copyCommand = []string{"mkdir -p", path} - - var cmd packer.RemoteCmd - cmd.Command = strings.Join(copyCommand, " ") - - var stdout bytes.Buffer - cmd.Stdout = &stdout - - // Start the command - if err := comm.Start(&cmd); err != nil { - return fmt.Errorf("Unable to create remote directory %s: %d", path, err) + remotePath := filepath.Join(p.config.StagingDir, "solo.rb") + if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString))); err != nil { + return "", err } - // Wait for it to complete - cmd.Wait() - - return + return remotePath, nil } -func CreateSoloRb(cookbooksPaths []string, comm packer.Communicator) (str string, err error) { - Ui.Say("Creating Chef configuration file...") - - remotePath := RemoteStagingPath + "/solo.rb" - - tf, err := ioutil.TempFile("", "packer-chef-solo-rb") - if err != nil { - return "", fmt.Errorf("Error preparing Chef solo.rb: %s", err) - } +func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) { + ui.Message("Creating JSON attribute file") - // Write our contents to it - writer := bufio.NewWriter(tf) + jsonData := make(map[string]interface{}) - var cookbooksPathsFull = make([]string, len(cookbooksPaths)) - for i, path := range cookbooksPaths { - cookbooksPathsFull[i] = "\"" + RemoteCookbookPath + "/" + path + "\"" + // Copy the configured JSON + for k, v := range p.config.Json { + jsonData[k] = v } - var contents bytes.Buffer - var soloRbText = ` - file_cache_path "{{.FileCachePath}}" - cookbook_path [{{.CookbookPath}}] -` - - t := template.Must(template.New("soloRb").Parse(soloRbText)) - t.Execute(&contents, map[string]string{ - "FileCachePath": RemoteFileCachePath, - "CookbookPath": strings.Join(cookbooksPathsFull, ","), - }) - - if _, err := writer.WriteString(contents.String()); err != nil { - return "", fmt.Errorf("Error preparing solo.rb: %s", err) + // Set the run list if it was specified + if len(p.config.RunList) > 0 { + jsonData["run_list"] = p.config.RunList } - if err := writer.Flush(); err != nil { - return "", fmt.Errorf("Error preparing solo.rb: %s", err) + jsonBytes, err := json.MarshalIndent(jsonData, "", " ") + if err != nil { + return "", err } - name := tf.Name() - tf.Close() - f, err := os.Open(name) - defer os.Remove(name) - - log.Printf("Chef configuration file contents: %s", contents) - - // Upload the Chef Solo configuration file to the cookbook directory. - log.Printf("Uploading chef configuration file to %s", remotePath) - err = comm.Upload(remotePath, f) - if err != nil { - return "", fmt.Errorf("Error uploading Chef Solo configuration file: %s", err) + // Upload the bytes + remotePath := filepath.Join(p.config.StagingDir, "node.json") + if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes)); err != nil { + return "", err } return remotePath, nil } -func CreateAttributesJson(jsonAttrs map[string]interface{}, recipes []string, comm packer.Communicator) (str string, err error) { - Ui.Say("Creating and uploading Chef attributes file") - remotePath := RemoteStagingPath + "/node.json" - - var formattedRecipes []string - for _, value := range recipes { - formattedRecipes = append(formattedRecipes, "recipe["+value+"]") +func (p *Provisioner) createStagingDir(ui packer.Ui, comm packer.Communicator) error { + ui.Message(fmt.Sprintf("Creating staging directory: %s", p.config.StagingDir)) + cmd := &packer.RemoteCmd{ + Command: fmt.Sprintf("mkdir -p '%s'", p.config.StagingDir), } - // Add Recipes to JSON - if len(formattedRecipes) > 0 { - log.Printf("Overriding node run list: %s", strings.Join(formattedRecipes, ", ")) - jsonAttrs["run_list"] = formattedRecipes + if err := cmd.StartWithUi(comm, ui); err != nil { + return err } - // Convert to JSON string - jsonString, err := json.MarshalIndent(jsonAttrs, "", " ") - if err != nil { - return "", fmt.Errorf("Error parsing JSON attributes: %s", err) + if cmd.ExitStatus != 0 { + return fmt.Errorf("Non-zero exit status.") } - tf, err := ioutil.TempFile("", "packer-chef-solo-json") + return nil +} + +func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error { + command, err := p.config.tpl.Process(p.config.InstallCommand, &ExecuteTemplate{ + ConfigPath: config, + JsonPath: json, + Sudo: !p.config.PreventSudo, + }) if err != nil { - return "", fmt.Errorf("Error preparing Chef attributes file: %s", err) + return err } - defer os.Remove(tf.Name()) - // Write our contents to it - writer := bufio.NewWriter(tf) - if _, err := writer.WriteString(string(jsonString)); err != nil { - return "", fmt.Errorf("Error preparing Chef attributes file: %s", err) - } + ui.Message(fmt.Sprintf("Executing Chef: %s", command)) - if err := writer.Flush(); err != nil { - return "", fmt.Errorf("Error preparing Chef attributes file: %s", err) + cmd := &packer.RemoteCmd{ + Command: command, } - jsonFile := tf.Name() - tf.Close() - - log.Printf("Opening %s for reading", jsonFile) - f, err := os.Open(jsonFile) - if err != nil { - return "", fmt.Errorf("Error opening JSON attributes file: %s", err) + if err := cmd.StartWithUi(comm, ui); err != nil { + return err } - log.Printf("Uploading %s => %s", jsonFile, remotePath) - err = comm.Upload(remotePath, f) - if err != nil { - return "", fmt.Errorf("Error uploading JSON attributes file: %s", err) + if cmd.ExitStatus != 0 { + return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) } - return remotePath, nil + return nil } func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error { @@ -346,61 +253,6 @@ func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error return nil } -func executeCommand(command string, comm packer.Communicator) (err error) { - // Setup the remote command - stdout_r, stdout_w := io.Pipe() - stderr_r, stderr_w := io.Pipe() - - var cmd packer.RemoteCmd - cmd.Command = command - cmd.Stdout = stdout_w - cmd.Stderr = stderr_w - - log.Printf("Executing command: %s", cmd.Command) - err = comm.Start(&cmd) - if err != nil { - return fmt.Errorf("Failed executing command: %s", err) - } - - exitChan := make(chan int, 1) - stdoutChan := iochan.DelimReader(stdout_r, '\n') - stderrChan := iochan.DelimReader(stderr_r, '\n') - - go func() { - defer stdout_w.Close() - defer stderr_w.Close() - - cmd.Wait() - exitChan <- cmd.ExitStatus - }() - -OutputLoop: - for { - select { - case output := <-stderrChan: - Ui.Message(strings.TrimSpace(output)) - case output := <-stdoutChan: - Ui.Message(strings.TrimSpace(output)) - case exitStatus := <-exitChan: - log.Printf("Chef Solo provisioner exited with status %d", exitStatus) - - if exitStatus != 0 { - return fmt.Errorf("Command exited with non-zero exit status: %d", exitStatus) - } - - break OutputLoop - } - } - - // Make sure we finish off stdout/stderr because we may have gotten - // a message from the exit channel first. - for output := range stdoutChan { - Ui.Message(output) - } - - for output := range stderrChan { - Ui.Message(output) - } - - return nil -} +var DefaultConfigTemplate = ` +cookbook_path [{{.CookbookPaths}}] +` diff --git a/provisioner/chef-solo/provisioner_test.go b/provisioner/chef-solo/provisioner_test.go index 4b96b5486..50a727446 100644 --- a/provisioner/chef-solo/provisioner_test.go +++ b/provisioner/chef-solo/provisioner_test.go @@ -8,9 +8,7 @@ import ( ) func testConfig() map[string]interface{} { - return map[string]interface{}{ - // "inline": []interface{}{"foo", "bar"}, - } + return map[string]interface{}{} } func TestProvisioner_Impl(t *testing.T) { @@ -21,48 +19,35 @@ func TestProvisioner_Impl(t *testing.T) { } } -// Cookbook paths -////////////////// - -func TestProvisionerPrepare_DefaultCookbookPathIsUsed(t *testing.T) { - var p Provisioner - config := testConfig() - - err := p.Prepare(config) - if err == nil { - t.Errorf("expected error to be thrown for unavailable cookbook path") - } - - if len(p.config.CookbooksPaths) != 1 || p.config.CookbooksPaths[0] != DefaultCookbooksPath { - t.Errorf("unexpected default cookbook path: %s", p.config.CookbooksPaths) - } -} - -func TestProvisionerPrepare_GivenCookbookPathsAreAddedToConfig(t *testing.T) { +func TestProvisionerPrepare_cookbookPaths(t *testing.T) { var p Provisioner path1, err := ioutil.TempDir("", "cookbooks_one") if err != nil { - t.Errorf("err: %s", err) + t.Fatalf("err: %s", err) } path2, err := ioutil.TempDir("", "cookbooks_two") if err != nil { - t.Errorf("err: %s", err) + t.Fatalf("err: %s", err) } defer os.Remove(path1) defer os.Remove(path2) config := testConfig() - config["cookbooks_paths"] = []string{path1, path2} + config["cookbook_paths"] = []string{path1, path2} err = p.Prepare(config) if err != nil { - t.Errorf("err: %s", err) + t.Fatalf("err: %s", err) + } + + if len(p.config.CookbookPaths) != 2 { + t.Fatalf("unexpected: %#v", p.config.CookbookPaths) } - if len(p.config.CookbooksPaths) != 2 || p.config.CookbooksPaths[0] != path1 || p.config.CookbooksPaths[1] != path2 { - t.Errorf("unexpected default cookbook path: %s", p.config.CookbooksPaths) + if p.config.CookbookPaths[0] != path1 || p.config.CookbookPaths[1] != path2 { + t.Fatalf("unexpected: %#v", p.config.CookbookPaths) } }