github.phpd.cn/hashicorp/packer@v1.3.2/provisioner/converge/provisioner.go (about) 1 // This package implements a provisioner for Packer that executes 2 // Converge to provision a remote machine 3 4 package converge 5 6 import ( 7 "bytes" 8 "errors" 9 "fmt" 10 11 "strings" 12 13 "encoding/json" 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/template/interpolate" 19 ) 20 21 // Config for Converge provisioner 22 type Config struct { 23 common.PackerConfig `mapstructure:",squash"` 24 25 // Bootstrapping 26 Bootstrap bool `mapstructure:"bootstrap"` 27 Version string `mapstructure:"version"` 28 BootstrapCommand string `mapstructure:"bootstrap_command"` 29 PreventBootstrapSudo bool `mapstructure:"prevent_bootstrap_sudo"` 30 31 // Modules 32 ModuleDirs []ModuleDir `mapstructure:"module_dirs"` 33 34 // Execution 35 Module string `mapstructure:"module"` 36 WorkingDirectory string `mapstructure:"working_directory"` 37 Params map[string]string `mapstructure:"params"` 38 ExecuteCommand string `mapstructure:"execute_command"` 39 PreventSudo bool `mapstructure:"prevent_sudo"` 40 41 ctx interpolate.Context 42 } 43 44 // ModuleDir is a directory to transfer to the remote system 45 type ModuleDir struct { 46 Source string `mapstructure:"source"` 47 Destination string `mapstructure:"destination"` 48 Exclude []string `mapstructure:"exclude"` 49 } 50 51 // Provisioner for Converge 52 type Provisioner struct { 53 config Config 54 } 55 56 // Prepare provisioner somehow. TODO: actual docs 57 func (p *Provisioner) Prepare(raws ...interface{}) error { 58 err := config.Decode( 59 &p.config, 60 &config.DecodeOpts{ 61 Interpolate: true, 62 InterpolateContext: &p.config.ctx, 63 InterpolateFilter: &interpolate.RenderFilter{ 64 Exclude: []string{ 65 "execute_command", 66 "bootstrap_command", 67 }, 68 }, 69 }, 70 raws..., 71 ) 72 if err != nil { 73 return err 74 } 75 76 // require a single module 77 if p.config.Module == "" { 78 return errors.New("Converge requires a module to provision the system") 79 } 80 81 // set defaults 82 if p.config.WorkingDirectory == "" { 83 p.config.WorkingDirectory = "/tmp" 84 } 85 86 if p.config.ExecuteCommand == "" { 87 p.config.ExecuteCommand = "cd {{.WorkingDirectory}} && {{if .Sudo}}sudo {{end}}converge apply --local --log-level=WARNING --paramsJSON '{{.ParamsJSON}}' {{.Module}}" 88 } 89 90 if p.config.BootstrapCommand == "" { 91 p.config.BootstrapCommand = "curl -s https://get.converge.sh | {{if .Sudo}}sudo {{end}}sh {{if ne .Version \"\"}}-s -- -v {{.Version}}{{end}}" 92 } 93 94 // validate sources and destinations 95 for i, dir := range p.config.ModuleDirs { 96 if dir.Source == "" { 97 return fmt.Errorf("Source (\"source\" key) is required in Converge module dir #%d", i) 98 } 99 if dir.Destination == "" { 100 return fmt.Errorf("Destination (\"destination\" key) is required in Converge module dir #%d", i) 101 } 102 } 103 104 return err 105 } 106 107 // Provision node somehow. TODO: actual docs 108 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 109 ui.Say("Provisioning with Converge") 110 111 // bootstrapping 112 if err := p.maybeBootstrap(ui, comm); err != nil { 113 return err // error messages are already user-friendly 114 } 115 116 // send module directories to the remote host 117 if err := p.sendModuleDirectories(ui, comm); err != nil { 118 return err // error messages are already user-friendly 119 } 120 121 // apply all the modules 122 if err := p.applyModules(ui, comm); err != nil { 123 return err // error messages are already user-friendly 124 } 125 126 return nil 127 } 128 129 func (p *Provisioner) maybeBootstrap(ui packer.Ui, comm packer.Communicator) error { 130 if !p.config.Bootstrap { 131 return nil 132 } 133 ui.Message("bootstrapping converge") 134 135 p.config.ctx.Data = struct { 136 Version string 137 Sudo bool 138 }{ 139 Version: p.config.Version, 140 Sudo: !p.config.PreventBootstrapSudo, 141 } 142 command, err := interpolate.Render(p.config.BootstrapCommand, &p.config.ctx) 143 if err != nil { 144 return fmt.Errorf("Could not interpolate bootstrap command: %s", err) 145 } 146 147 var out, outErr bytes.Buffer 148 cmd := &packer.RemoteCmd{ 149 Command: command, 150 Stdin: nil, 151 Stdout: &out, 152 Stderr: &outErr, 153 } 154 155 if err = comm.Start(cmd); err != nil { 156 return fmt.Errorf("Error bootstrapping converge: %s", err) 157 } 158 159 cmd.Wait() 160 if cmd.ExitStatus != 0 { 161 ui.Error(out.String()) 162 ui.Error(outErr.String()) 163 return errors.New("Error bootstrapping converge") 164 } 165 166 ui.Message(strings.TrimSpace(out.String())) 167 return nil 168 } 169 170 func (p *Provisioner) sendModuleDirectories(ui packer.Ui, comm packer.Communicator) error { 171 for _, dir := range p.config.ModuleDirs { 172 if err := comm.UploadDir(dir.Destination, dir.Source, dir.Exclude); err != nil { 173 return fmt.Errorf("Could not upload %q: %s", dir.Source, err) 174 } 175 ui.Message(fmt.Sprintf("transferred %q to %q", dir.Source, dir.Destination)) 176 } 177 178 return nil 179 } 180 181 func (p *Provisioner) applyModules(ui packer.Ui, comm packer.Communicator) error { 182 // create params JSON file 183 params, err := json.Marshal(p.config.Params) 184 if err != nil { 185 return fmt.Errorf("Could not marshal parameters as JSON: %s", err) 186 } 187 188 p.config.ctx.Data = struct { 189 ParamsJSON, WorkingDirectory, Module string 190 Sudo bool 191 }{ 192 ParamsJSON: string(params), 193 WorkingDirectory: p.config.WorkingDirectory, 194 Module: p.config.Module, 195 Sudo: !p.config.PreventSudo, 196 } 197 command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 198 if err != nil { 199 return fmt.Errorf("Could not interpolate execute command: %s", err) 200 } 201 202 // run Converge in the specified directory 203 var runOut, runErr bytes.Buffer 204 cmd := &packer.RemoteCmd{ 205 Command: command, 206 Stdin: nil, 207 Stdout: &runOut, 208 Stderr: &runErr, 209 } 210 if err := comm.Start(cmd); err != nil { 211 return fmt.Errorf("Error applying %q: %s", p.config.Module, err) 212 } 213 214 cmd.Wait() 215 if cmd.ExitStatus == 127 { 216 ui.Error("Could not find Converge. Is it installed and in PATH?") 217 if !p.config.Bootstrap { 218 ui.Error("Bootstrapping was disabled for this run. That might be why Converge isn't present.") 219 } 220 221 return errors.New("Could not find Converge") 222 223 } else if cmd.ExitStatus != 0 { 224 ui.Error(strings.TrimSpace(runOut.String())) 225 ui.Error(strings.TrimSpace(runErr.String())) 226 ui.Error(fmt.Sprintf("Exited with error code %d.", cmd.ExitStatus)) 227 return fmt.Errorf("Error applying %q", p.config.Module) 228 } 229 230 ui.Message(strings.TrimSpace(runOut.String())) 231 232 return nil 233 } 234 235 // Cancel the provisioning process 236 func (p *Provisioner) Cancel() { 237 // there's not an awful lot we can do to cancel Converge at the moment. 238 // The default semantics are fine. 239 }