242 lines
6.6 KiB
Go
242 lines
6.6 KiB
Go
//go:generate mapstructure-to-hcl2 -type Config,ModuleDir
|
|
|
|
// This package implements a provisioner for Packer that executes
|
|
// Converge to provision a remote machine
|
|
|
|
package converge
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"encoding/json"
|
|
|
|
"github.com/hashicorp/hcl/v2/hcldec"
|
|
"github.com/hashicorp/packer-plugin-sdk/common"
|
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
|
"github.com/hashicorp/packer-plugin-sdk/template/config"
|
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
|
)
|
|
|
|
// Config for Converge provisioner
|
|
type Config struct {
|
|
common.PackerConfig `mapstructure:",squash"`
|
|
|
|
// Bootstrapping
|
|
Bootstrap bool `mapstructure:"bootstrap"`
|
|
Version string `mapstructure:"version"`
|
|
BootstrapCommand string `mapstructure:"bootstrap_command"`
|
|
PreventBootstrapSudo bool `mapstructure:"prevent_bootstrap_sudo"`
|
|
|
|
// Modules
|
|
ModuleDirs []ModuleDir `mapstructure:"module_dirs"`
|
|
|
|
// Execution
|
|
Module string `mapstructure:"module"`
|
|
WorkingDirectory string `mapstructure:"working_directory"`
|
|
Params map[string]string `mapstructure:"params"`
|
|
ExecuteCommand string `mapstructure:"execute_command"`
|
|
PreventSudo bool `mapstructure:"prevent_sudo"`
|
|
|
|
ctx interpolate.Context
|
|
}
|
|
|
|
// ModuleDir is a directory to transfer to the remote system
|
|
type ModuleDir struct {
|
|
Source string `mapstructure:"source"`
|
|
Destination string `mapstructure:"destination"`
|
|
Exclude []string `mapstructure:"exclude"`
|
|
}
|
|
|
|
// Provisioner for Converge
|
|
type Provisioner struct {
|
|
config Config
|
|
}
|
|
|
|
func (p *Provisioner) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
|
|
|
|
func (p *Provisioner) Prepare(raws ...interface{}) error {
|
|
err := config.Decode(
|
|
&p.config,
|
|
&config.DecodeOpts{
|
|
PluginType: "converge",
|
|
Interpolate: true,
|
|
InterpolateContext: &p.config.ctx,
|
|
InterpolateFilter: &interpolate.RenderFilter{
|
|
Exclude: []string{
|
|
"execute_command",
|
|
"bootstrap_command",
|
|
},
|
|
},
|
|
},
|
|
raws...,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// require a single module
|
|
if p.config.Module == "" {
|
|
return errors.New("Converge requires a module to provision the system")
|
|
}
|
|
|
|
// set defaults
|
|
if p.config.WorkingDirectory == "" {
|
|
p.config.WorkingDirectory = "/tmp"
|
|
}
|
|
|
|
if p.config.ExecuteCommand == "" {
|
|
p.config.ExecuteCommand = "cd {{.WorkingDirectory}} && {{if .Sudo}}sudo {{end}}converge apply --local --log-level=WARNING --paramsJSON '{{.ParamsJSON}}' {{.Module}}"
|
|
}
|
|
|
|
if p.config.BootstrapCommand == "" {
|
|
p.config.BootstrapCommand = "curl -s https://get.converge.sh | {{if .Sudo}}sudo {{end}}sh {{if ne .Version \"\"}}-s -- -v {{.Version}}{{end}}"
|
|
}
|
|
|
|
// validate sources and destinations
|
|
for i, dir := range p.config.ModuleDirs {
|
|
if dir.Source == "" {
|
|
return fmt.Errorf("Source (\"source\" key) is required in Converge module dir #%d", i)
|
|
}
|
|
if dir.Destination == "" {
|
|
return fmt.Errorf("Destination (\"destination\" key) is required in Converge module dir #%d", i)
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Provision node somehow. TODO: actual docs
|
|
func (p *Provisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, _ map[string]interface{}) error {
|
|
ui.Say("Provisioning with Converge")
|
|
|
|
// bootstrapping
|
|
if err := p.maybeBootstrap(ui, comm); err != nil {
|
|
return err // error messages are already user-friendly
|
|
}
|
|
|
|
// send module directories to the remote host
|
|
if err := p.sendModuleDirectories(ui, comm); err != nil {
|
|
return err // error messages are already user-friendly
|
|
}
|
|
|
|
// apply all the modules
|
|
if err := p.applyModules(ui, comm); err != nil {
|
|
return err // error messages are already user-friendly
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Provisioner) maybeBootstrap(ui packersdk.Ui, comm packersdk.Communicator) error {
|
|
ctx := context.TODO()
|
|
if !p.config.Bootstrap {
|
|
return nil
|
|
}
|
|
ui.Message("bootstrapping converge")
|
|
|
|
p.config.ctx.Data = struct {
|
|
Version string
|
|
Sudo bool
|
|
}{
|
|
Version: p.config.Version,
|
|
Sudo: !p.config.PreventBootstrapSudo,
|
|
}
|
|
command, err := interpolate.Render(p.config.BootstrapCommand, &p.config.ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not interpolate bootstrap command: %s", err)
|
|
}
|
|
|
|
var out, outErr bytes.Buffer
|
|
cmd := &packersdk.RemoteCmd{
|
|
Command: command,
|
|
Stdin: nil,
|
|
Stdout: &out,
|
|
Stderr: &outErr,
|
|
}
|
|
|
|
if err = comm.Start(ctx, cmd); err != nil {
|
|
return fmt.Errorf("Error bootstrapping converge: %s", err)
|
|
}
|
|
|
|
cmd.Wait()
|
|
if cmd.ExitStatus() != 0 {
|
|
ui.Error(out.String())
|
|
ui.Error(outErr.String())
|
|
return errors.New("Error bootstrapping converge")
|
|
}
|
|
|
|
ui.Message(strings.TrimSpace(out.String()))
|
|
return nil
|
|
}
|
|
|
|
func (p *Provisioner) sendModuleDirectories(ui packersdk.Ui, comm packersdk.Communicator) error {
|
|
for _, dir := range p.config.ModuleDirs {
|
|
if err := comm.UploadDir(dir.Destination, dir.Source, dir.Exclude); err != nil {
|
|
return fmt.Errorf("Could not upload %q: %s", dir.Source, err)
|
|
}
|
|
ui.Message(fmt.Sprintf("transferred %q to %q", dir.Source, dir.Destination))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Provisioner) applyModules(ui packersdk.Ui, comm packersdk.Communicator) error {
|
|
ctx := context.TODO()
|
|
// create params JSON file
|
|
params, err := json.Marshal(p.config.Params)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not marshal parameters as JSON: %s", err)
|
|
}
|
|
|
|
p.config.ctx.Data = struct {
|
|
ParamsJSON, WorkingDirectory, Module string
|
|
Sudo bool
|
|
}{
|
|
ParamsJSON: string(params),
|
|
WorkingDirectory: p.config.WorkingDirectory,
|
|
Module: p.config.Module,
|
|
Sudo: !p.config.PreventSudo,
|
|
}
|
|
command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("Could not interpolate execute command: %s", err)
|
|
}
|
|
|
|
// run Converge in the specified directory
|
|
var runOut, runErr bytes.Buffer
|
|
cmd := &packersdk.RemoteCmd{
|
|
Command: command,
|
|
Stdin: nil,
|
|
Stdout: &runOut,
|
|
Stderr: &runErr,
|
|
}
|
|
if err := comm.Start(ctx, cmd); err != nil {
|
|
return fmt.Errorf("Error applying %q: %s", p.config.Module, err)
|
|
}
|
|
|
|
cmd.Wait()
|
|
if cmd.ExitStatus() == 127 {
|
|
ui.Error("Could not find Converge. Is it installed and in PATH?")
|
|
if !p.config.Bootstrap {
|
|
ui.Error("Bootstrapping was disabled for this run. That might be why Converge isn't present.")
|
|
}
|
|
|
|
return errors.New("Could not find Converge")
|
|
|
|
} else if cmd.ExitStatus() != 0 {
|
|
ui.Error(strings.TrimSpace(runOut.String()))
|
|
ui.Error(strings.TrimSpace(runErr.String()))
|
|
ui.Error(fmt.Sprintf("Exited with error code %d.", cmd.ExitStatus()))
|
|
return fmt.Errorf("Error applying %q", p.config.Module)
|
|
}
|
|
|
|
ui.Message(strings.TrimSpace(runOut.String()))
|
|
|
|
return nil
|
|
}
|