Merge pull request #4326 from asteris-llc/feature/converge-provisioner
New Provisioner: Converge
This commit is contained in:
commit
5fc6a2ee70
|
@ -0,0 +1,237 @@
|
|||
// This package implements a provisioner for Packer that executes
|
||||
// Converge to provision a remote machine
|
||||
|
||||
package converge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/helper/config"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/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 `mapstucture:"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
|
||||
}
|
||||
|
||||
// Prepare provisioner somehow. TODO: actual docs
|
||||
func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
err := config.Decode(
|
||||
&p.config,
|
||||
&config.DecodeOpts{
|
||||
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(ui packer.Ui, comm packer.Communicator) 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 packer.Ui, comm packer.Communicator) error {
|
||||
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 bytes.Buffer
|
||||
cmd := &packer.RemoteCmd{
|
||||
Command: command,
|
||||
Stdin: nil,
|
||||
Stdout: &out,
|
||||
Stderr: &out,
|
||||
}
|
||||
|
||||
if err = comm.Start(cmd); err != nil {
|
||||
return fmt.Errorf("Error bootstrapping converge: %s", err)
|
||||
}
|
||||
|
||||
cmd.Wait()
|
||||
if cmd.ExitStatus != 0 {
|
||||
ui.Error(out.String())
|
||||
return errors.New("Error bootstrapping converge")
|
||||
}
|
||||
|
||||
ui.Message(strings.TrimSpace(out.String()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) sendModuleDirectories(ui packer.Ui, comm packer.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 packer.Ui, comm packer.Communicator) error {
|
||||
// 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 bytes.Buffer
|
||||
cmd := &packer.RemoteCmd{
|
||||
Command: command,
|
||||
Stdin: nil,
|
||||
Stdout: &runOut,
|
||||
Stderr: &runOut,
|
||||
}
|
||||
if err := comm.Start(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(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
|
||||
}
|
||||
|
||||
// Cancel the provisioning process
|
||||
func (p *Provisioner) Cancel() {
|
||||
// there's not an awful lot we can do to cancel Converge at the moment.
|
||||
// The default semantics are fine.
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package converge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"module_dirs": []map[string]interface{}{
|
||||
{
|
||||
"source": "from",
|
||||
"destination": "/opt/converge",
|
||||
},
|
||||
},
|
||||
"module": "/opt/converge/test.hcl",
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisioner_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Provisioner{}
|
||||
if _, ok := raw.(packer.Provisioner); !ok {
|
||||
t.Fatal("must be a Provisioner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare(t *testing.T) {
|
||||
t.Run("defaults", func(t *testing.T) {
|
||||
t.Run("working_directory", func(t *testing.T) {
|
||||
var p Provisioner
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "working_directory")
|
||||
|
||||
if err := p.Prepare(config); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if p.config.WorkingDirectory != "/tmp" {
|
||||
t.Fatalf("unexpected module directory: %s", p.config.WorkingDirectory)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("execute_command", func(t *testing.T) {
|
||||
var p Provisioner
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "execute_command")
|
||||
|
||||
if err := p.Prepare(config); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if p.config.ExecuteCommand == "" {
|
||||
t.Fatal("execute command unexpectedly blank")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bootstrap_command", func(t *testing.T) {
|
||||
var p Provisioner
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "bootstrap_command")
|
||||
|
||||
if err := p.Prepare(config); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if p.config.BootstrapCommand == "" {
|
||||
t.Fatal("bootstrap command unexpectedly blank")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("validate", func(t *testing.T) {
|
||||
t.Run("module dir", func(t *testing.T) {
|
||||
t.Run("missing source", func(t *testing.T) {
|
||||
var p Provisioner
|
||||
config := testConfig()
|
||||
delete(config["module_dirs"].([]map[string]interface{})[0], "source")
|
||||
|
||||
err := p.Prepare(config)
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
} else if err.Error() != "Source (\"source\" key) is required in Converge module dir #0" {
|
||||
t.Errorf("bad error message: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing destination", func(t *testing.T) {
|
||||
var p Provisioner
|
||||
config := testConfig()
|
||||
delete(config["module_dirs"].([]map[string]interface{})[0], "destination")
|
||||
|
||||
err := p.Prepare(config)
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
} else if err.Error() != "Destination (\"destination\" key) is required in Converge module dir #0" {
|
||||
t.Errorf("bad error message: %s", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("no module specified", func(t *testing.T) {
|
||||
var p Provisioner
|
||||
config := testConfig()
|
||||
delete(config, "module")
|
||||
|
||||
err := p.Prepare(config)
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
} else if err.Error() != "Converge requires a module to provision the system" {
|
||||
t.Errorf("bad error message: %s", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
description: |-
|
||||
The Converge Packer provisioner uses Converge modules to provision the machine.
|
||||
layout: docs
|
||||
page_title: Converge Provisioner
|
||||
...
|
||||
|
||||
# Converge Provisioner
|
||||
|
||||
Type: `converge`
|
||||
|
||||
The [Converge](http://converge.aster.is) Packer provisioner uses Converge
|
||||
modules to provision the machine. It uploads module directories to use as
|
||||
source, or you can use remote modules.
|
||||
|
||||
The provisioner can optionally bootstrap the Converge client/server binary onto
|
||||
new images.
|
||||
|
||||
## Basic Example
|
||||
|
||||
The example below is fully functional.
|
||||
|
||||
``` {.javascript}
|
||||
{
|
||||
"type": "converge",
|
||||
"module": "https://raw.githubusercontent.com/asteris-llc/converge/master/samples/fileContent.hcl",
|
||||
"params": {
|
||||
"message": "Hello, Packer!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
The reference of available configuration options is listed below. The only
|
||||
required element is "module". Every other option is optional.
|
||||
|
||||
- `module` (string) - Path (or URL) to the root module that Converge will apply.
|
||||
|
||||
Optional parameters:
|
||||
|
||||
- `bootstrap` (boolean, defaults to false) - Set to allow the provisioner to
|
||||
download the latest Converge bootstrap script and the specified `version` of
|
||||
Converge from the internet.
|
||||
|
||||
- `version` (string) - Set to a [released Converge version](https://github.com/asteris-llc/converge/releases) for bootstrap.
|
||||
|
||||
- `module_dirs` (array of directory specifications) - Module directories to
|
||||
transfer to the remote host for execution. See below for the specification.
|
||||
|
||||
- `working_directory` (string) - The directory that Converge will change to
|
||||
before execution.
|
||||
|
||||
- `params` (maps of string to string) - parameters to pass into the root module.
|
||||
|
||||
- `execute_command` (string) - the command used to execute Converge. This has
|
||||
various
|
||||
[configuration template variables](/docs/templates/configuration-templates.html) available.
|
||||
|
||||
- `prevent_sudo` (bool) - stop Converge from running with adminstrator
|
||||
privileges via sudo
|
||||
|
||||
- `bootstrap_command` (string) - the command used to bootstrap Converge. This
|
||||
has various
|
||||
[configuration template variables](/docs/templates/configuration-templates.html) available.
|
||||
|
||||
- `prevent_bootstrap_sudo` (bool) - stop Converge from bootstrapping with
|
||||
administrator privileges via sudo
|
||||
|
||||
### Module Directories
|
||||
|
||||
The provisioner can transfer module directories to the remote host for
|
||||
provisioning. Of these fields, `source` and `destination` are required in every
|
||||
directory.
|
||||
|
||||
- `source` (string) - the path to the folder on the local machine.
|
||||
|
||||
- `destination` (string) - the path to the folder on the remote machine. Parent
|
||||
directories will not be created; use the shell module to do this.
|
||||
|
||||
- `exclude` (array of string) - files and directories to exclude from transfer.
|
||||
|
||||
### Execute Command
|
||||
|
||||
By default, Packer uses the following command (broken across multiple lines for readability) to execute Converge:
|
||||
|
||||
``` {.liquid}
|
||||
cd {{.WorkingDirectory}} && \
|
||||
{{if .Sudo}}sudo {{end}}converge apply \
|
||||
--local \
|
||||
--log-level=WARNING \
|
||||
--paramsJSON '{{.ParamsJSON}}' \
|
||||
{{.Module}}
|
||||
```
|
||||
|
||||
This command can be customized using the `execute_command` configuration. As you
|
||||
can see from the default value above, the value of this configuration can
|
||||
contain various template variables:
|
||||
|
||||
- `WorkingDirectory` - `directory` from the configuration.
|
||||
- `Sudo` - the opposite of `prevent_sudo` from the configuration.
|
||||
- `ParamsJSON` - The unquoted JSONified form of `params` from the configuration.
|
||||
- `Module` - `module` from the configuration.
|
||||
|
||||
### Bootstrap Command
|
||||
|
||||
By default, Packer uses the following command to bootstrap Converge:
|
||||
|
||||
``` {.liquid}
|
||||
curl -s https://get.converge.sh | {{if .Sudo}}sudo {{end}}sh {{if ne .Version ""}}-s -- -v {{.Version}}{{end}}
|
||||
```
|
||||
|
||||
This command can be customized using the `bootstrap_command` configuration. As you
|
||||
can see from the default values above, the value of this configuration can
|
||||
contain various template variables:
|
||||
|
||||
- `Sudo` - the opposite of `prevent_bootstrap_sudo` from the configuration.
|
||||
- `Version` - `version` from the configuration.
|
Loading…
Reference in New Issue