From 45334427ce5d448b0fd8a17bf77e2761128af353 Mon Sep 17 00:00:00 2001 From: martezr Date: Fri, 18 Sep 2020 14:15:41 -0500 Subject: [PATCH] Add support for Bolt project path and installing modules --- README.md | 5 +- bolt/provisioner.go | 97 +++++++++++++++++++++++++++++++++--- bolt/provisioner.hcl2spec.go | 4 ++ bolt/provisioner_test.go | 41 +++++++++++++++ 4 files changed, 139 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 82a2317..7a0c133 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,12 @@ Optional Parameters: |------|------|-------------| |bolt_module_path| string |The path that Bolt should look for modules | |bolt_params | json | The parameters to pass the Bolt Task or Plan. | +|install_modules|boolean|Whether to install modules from a Bolt project using the Puppetfile| |inventory_file | string | The inventory file to use during provisioning. When unspecified, Packer will create a temporary inventory file and will use the host_alias.| |local_port|uint|The port on which to attempt to listen for SSH connections. This value is a starting point. The provisioner will attempt listen for SSH connections on the first available of ten ports, starting at local_port. A system-chosen port is used when local_port is missing or empty.| -|user|string|The bolt_user to use. Defaults to the user running packer.| |log_level|string|The level of logging (debug, error, info, notice, warn, fatal, any)| - +|project_path|string|The path that Bolt should look for a Bolt project directory| +|user|string|The bolt_user to use. Defaults to the user running packer.| SSH Settings ------------ diff --git a/bolt/provisioner.go b/bolt/provisioner.go index 6daffbf..96e66ee 100644 --- a/bolt/provisioner.go +++ b/bolt/provisioner.go @@ -71,6 +71,9 @@ type Config struct { // The bolt module path BoltModulePath string `mapstructure:"bolt_module_path"` + // The bolt project path + ProjectPath string `mapstructure:"project_path"` + // The bolt inventory file InventoryFile string `mapstructure:"inventory_file"` @@ -95,6 +98,7 @@ type Config struct { NoWinRMSSLVerify bool `mapstructure:"no_winrm_ssl_verify"` NoWinRMSSL bool `mapstructure:"no_winrm_ssl"` LogLevel string `mapstructure:"log_level"` + InstallModules bool `mapstructure:"install_modules"` } // Provisioner data passed to the provision operation @@ -165,13 +169,22 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { // Validate that the specified module path exists if len(p.config.BoltModulePath) > 0 { - err = validateDirectoryConfig(p.config.BoltModulePath) + err = validateDirectoryConfig(p.config.BoltModulePath, "bolt_module_path") if err != nil { log.Println(p.config.BoltModulePath, "does not exist") errs = packer.MultiErrorAppend(errs, err) } } + // Validate that the specified project path exists + if len(p.config.ProjectPath) > 0 { + err = validateDirectoryConfig(p.config.ProjectPath, "project_path") + if err != nil { + log.Println(p.config.ProjectPath, "does not exist") + errs = packer.MultiErrorAppend(errs, err) + } + } + // Validate that the specified log level is valid validLogLevels := []string{"debug", "info", "notice", "warn", "error", "fatal", "any"} if p.config.LogLevel != "" { @@ -209,7 +222,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } if len(p.config.InventoryDirectory) > 0 { - err = validateDirectoryConfig(p.config.InventoryDirectory) + err = validateDirectoryConfig(p.config.InventoryDirectory, "inventory_directory") if err != nil { log.Println(p.config.InventoryDirectory, "does not exist") errs = packer.MultiErrorAppend(errs, err) @@ -222,6 +235,66 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { return nil } +func (p *Provisioner) installModules(ui packer.Ui) error { + args := []string{"puppetfile", "install"} + boltprojectpath := p.config.ProjectPath + if p.config.ProjectPath != "" { + args = append(args, "--project", boltprojectpath) + } + + cmd := exec.Command(p.config.Command, args...) + var envvars []string + cmd.Env = os.Environ() + if len(envvars) > 0 { + cmd.Env = append(cmd.Env, envvars...) + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + + wg := sync.WaitGroup{} + repeat := func(r io.ReadCloser) { + reader := bufio.NewReader(r) + for { + line, err := reader.ReadString('\n') + if line != "" { + line = strings.TrimRightFunc(line, unicode.IsSpace) + ui.Message(line) + } + if err != nil { + if err == io.EOF { + break + } else { + ui.Error(err.Error()) + break + } + } + } + wg.Done() + } + wg.Add(2) + go repeat(stdout) + go repeat(stderr) + + ui.Say(fmt.Sprintf("Installing Puppet modules: %s", strings.Join(cmd.Args, " "))) + if err := cmd.Start(); err != nil { + return err + } + wg.Wait() + err = cmd.Wait() + if err != nil { + return fmt.Errorf("Non-zero exit status: %s", err) + } + + return nil +} + func (p *Provisioner) getVersion() error { out, err := exec.Command(p.config.Command, "--version").Output() if err != nil { @@ -254,6 +327,13 @@ func (p *Provisioner) Provision(ctx context.Context, ui packer.Ui, comm packer.C ui.Say("Provisioning with Puppet Bolt...") p.config.ctx.Data = generatedData + if p.config.InstallModules { + err := p.installModules(ui) + if err != nil { + return fmt.Errorf("error installing modules: %s", err) + } + } + if p.config.Backend == "" { p.config.Backend = generatedData["ConnType"].(string) } @@ -383,6 +463,7 @@ func (p *Provisioner) executeBolt(ui packer.Ui, comm packer.Communicator, privKe bolttask := p.config.BoltTask boltplan := p.config.BoltPlan boltmodulepath := p.config.BoltModulePath + boltprojectpath := p.config.ProjectPath boltparams := p.config.BoltParams var envvars []string @@ -413,6 +494,10 @@ func (p *Provisioner) executeBolt(ui packer.Ui, comm packer.Communicator, privKe args = append(args, "--modulepath", boltmodulepath) } + if p.config.ProjectPath != "" { + args = append(args, "--project", boltprojectpath) + } + args = append(args, "--targets", target) args = append(args, "--user", p.config.User) @@ -575,7 +660,7 @@ func convertParams(m map[interface{}]interface{}) map[string]interface{} { func validateFileConfig(name string, config string, req bool) error { if req { if name == "" { - return fmt.Errorf("%s must be specified.", config) + return fmt.Errorf("%s must be specified", config) } } info, err := os.Stat(name) @@ -587,12 +672,12 @@ func validateFileConfig(name string, config string, req bool) error { return nil } -func validateDirectoryConfig(name string) error { +func validateDirectoryConfig(name string, setting string) error { info, err := os.Stat(name) if err != nil { - return fmt.Errorf("Directory: %s is invalid: %s", name, err) + return fmt.Errorf("Directory: %s specified for %s is invalid: %s", name, setting, err) } else if !info.IsDir() { - return fmt.Errorf("Directory: %s must point to a directory", name) + return fmt.Errorf("Directory: %s specified for %s must point to a directory", name, setting) } return nil } diff --git a/bolt/provisioner.hcl2spec.go b/bolt/provisioner.hcl2spec.go index e7341a0..75041ca 100644 --- a/bolt/provisioner.hcl2spec.go +++ b/bolt/provisioner.hcl2spec.go @@ -38,6 +38,8 @@ type FlatConfig struct { SSHAuthorizedKeyFile *string `mapstructure:"ssh_authorized_key_file" cty:"ssh_authorized_key_file" hcl:"ssh_authorized_key_file"` NoWinRMSSLVerify *bool `mapstructure:"no_winrm_ssl_verify" cty:"no_winrm_ssl_verify" hcl:"no_winrm_ssl_verify"` NoWinRMSSL *bool `mapstructure:"no_winrm_ssl" cty:"no_winrm_ssl" hcl:"no_winrm_ssl"` + InstallModules *bool `mapstructure:"install_modules" cty:"install_modules" hcl:"install_modules"` + ProjectPath *string `mapstructure:"project_path" cty:"project_path" hcl:"project_path"` } // FlatMapstructure returns a new FlatConfig. @@ -81,6 +83,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "no_winrm_ssl_verify": &hcldec.AttrSpec{Name: "no_winrm_ssl_verify", Type: cty.Bool, Required: false}, "no_winrm_ssl": &hcldec.AttrSpec{Name: "no_winrm_ssl", Type: cty.Bool, Required: false}, "log_level": &hcldec.AttrSpec{Name: "log_level", Type: cty.String, Required: false}, + "install_modules": &hcldec.AttrSpec{Name: "install_modules", Type: cty.String, Required: false}, + "project_path": &hcldec.AttrSpec{Name: "project_path", Type: cty.String, Required: false}, } return s } diff --git a/bolt/provisioner_test.go b/bolt/provisioner_test.go index 1910d61..f9e4908 100644 --- a/bolt/provisioner_test.go +++ b/bolt/provisioner_test.go @@ -238,6 +238,47 @@ func TestProvisionerPrepare_InventoryDirectory(t *testing.T) { } } +func TestProvisionerPrepare_ProjectDirectory(t *testing.T) { + var p Provisioner + config := testConfig(t) + defer os.Remove(config["command"].(string)) + + hostkeyFile, err := ioutil.TempFile("", "hostkey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(hostkeyFile.Name()) + + publickeyFile, err := ioutil.TempFile("", "publickey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(publickeyFile.Name()) + + config["ssh_host_key_file"] = hostkeyFile.Name() + config["ssh_authorized_key_file"] = publickeyFile.Name() + config["user"] = "root" + config["bolt_task"] = "facts" + + config["project_path"] = "doesnotexist" + err = p.Prepare(config) + if err == nil { + t.Errorf("should error if project_path does not exist") + } + + projectDirectory, err := ioutil.TempDir("", "some_project_dir") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(projectDirectory) + + config["project_path"] = projectDirectory + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} + func TestProvisionerPrepare_BoltTask(t *testing.T) { var p Provisioner config := testConfig(t)