Commit 7ff549ec authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

Merge pull request #733 from mitchellh/f-vagrant-pp-revamp

Vagrant Post-Processor Refactor + "Include" feature
parents 930b844b 99cbe1fc
......@@ -49,15 +49,8 @@ func (c Command) Run(env packer.Environment, args []string) int {
// Close the file since we're done with that
tplF.Close()
// Run the template through the various fixers
fixers := []string{
"iso-md5",
"createtime",
"virtualbox-gaattach",
}
input := templateData
for _, name := range fixers {
for _, name := range FixerOrder {
var err error
fixer, ok := Fixers[name]
if !ok {
......
......@@ -15,10 +15,21 @@ type Fixer interface {
// Fixers is the map of all available fixers, by name.
var Fixers map[string]Fixer
// FixerOrder is the default order the fixers should be run.
var FixerOrder []string
func init() {
Fixers = map[string]Fixer{
"iso-md5": new(FixerISOMD5),
"createtime": new(FixerCreateTime),
"pp-vagrant-override": new(FixerVagrantPPOverride),
"virtualbox-gaattach": new(FixerVirtualBoxGAAttach),
}
FixerOrder = []string{
"iso-md5",
"createtime",
"virtualbox-gaattach",
"pp-vagrant-override",
}
}
package fix
import (
"github.com/mitchellh/mapstructure"
)
// FixerVagrantPPOvveride is a Fixer that replaces the provider-specific
// overrides for the Vagrant post-processor with the new style introduced
// as part of Packer 0.5.0.
type FixerVagrantPPOverride struct{}
func (FixerVagrantPPOverride) Fix(input map[string]interface{}) (map[string]interface{}, error) {
// Our template type we'll use for this fixer only
type template struct {
PostProcessors []interface{} `mapstructure:"post-processors"`
}
// Decode the input into our structure, if we can
var tpl template
if err := mapstructure.Decode(input, &tpl); err != nil {
return nil, err
}
// Go through each post-processor and get out all the complex configs
pps := make([]map[string]interface{}, 0, len(tpl.PostProcessors))
for _, rawPP := range tpl.PostProcessors {
switch pp := rawPP.(type) {
case string:
case map[string]interface{}:
pps = append(pps, pp)
case []interface{}:
for _, innerRawPP := range pp {
if innerPP, ok := innerRawPP.(map[string]interface{}); ok {
pps = append(pps, innerPP)
}
}
}
}
// Go through each post-processor and make the fix if necessary
possible := []string{"aws", "digitalocean", "virtualbox", "vmware"}
for _, pp := range pps {
typeRaw, ok := pp["type"]
if !ok {
continue
}
if typeName, ok := typeRaw.(string); !ok {
continue
} else if typeName != "vagrant" {
continue
}
overrides := make(map[string]interface{})
for _, name := range possible {
if _, ok := pp[name]; !ok {
continue
}
overrides[name] = pp[name]
delete(pp, name)
}
if len(overrides) > 0 {
pp["override"] = overrides
}
}
input["post-processors"] = tpl.PostProcessors
return input, nil
}
func (FixerVagrantPPOverride) Synopsis() string {
return `Fixes provider-specific overrides for Vagrant post-processor`
}
package fix
import (
"reflect"
"testing"
)
func TestFixerVagrantPPOverride_Impl(t *testing.T) {
var _ Fixer = new(FixerVagrantPPOverride)
}
func TestFixerVagrantPPOverride_Fix(t *testing.T) {
var f FixerVagrantPPOverride
input := map[string]interface{}{
"post-processors": []interface{}{
"foo",
map[string]interface{}{
"type": "vagrant",
"aws": map[string]interface{}{
"foo": "bar",
},
},
map[string]interface{}{
"type": "vsphere",
},
[]interface{}{
map[string]interface{}{
"type": "vagrant",
"vmware": map[string]interface{}{
"foo": "bar",
},
},
},
},
}
expected := map[string]interface{}{
"post-processors": []interface{}{
"foo",
map[string]interface{}{
"type": "vagrant",
"override": map[string]interface{}{
"aws": map[string]interface{}{
"foo": "bar",
},
},
},
map[string]interface{}{
"type": "vsphere",
},
[]interface{}{
map[string]interface{}{
"type": "vagrant",
"override": map[string]interface{}{
"vmware": map[string]interface{}{
"foo": "bar",
},
},
},
},
},
}
output, err := f.Fix(input)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(output, expected) {
t.Fatalf("unexpected: %#v\nexpected: %#v\n", output, expected)
}
}
......@@ -11,6 +11,11 @@ Usage: packer fix [options] TEMPLATE
Fixes that are run:
iso-md5 Replaces "iso_md5" in builders with newer "iso_checksum"
iso-md5 Replaces "iso_md5" in builders with newer "iso_checksum"
createtime Replaces ".CreateTime" in builder configs with "{{timestamp}}"
virtualbox-gaattach Updates VirtualBox builders using "guest_additions_attach"
to use "guest_additions_mode"
pp-vagrant-override Replaces old-style provider overrides for the Vagrant
post-processor to new-style as of Packer 0.5.0.
`
......@@ -2,16 +2,26 @@ package packer
// MockArtifact is an implementation of Artifact that can be used for tests.
type MockArtifact struct {
IdValue string
DestroyCalled bool
BuilderIdValue string
FilesValue []string
IdValue string
DestroyCalled bool
}
func (*MockArtifact) BuilderId() string {
return "bid"
func (a *MockArtifact) BuilderId() string {
if a.BuilderIdValue == "" {
return "bid"
}
return a.BuilderIdValue
}
func (*MockArtifact) Files() []string {
return []string{"a", "b"}
func (a *MockArtifact) Files() []string {
if a.FilesValue == nil {
return []string{"a", "b"}
}
return a.FilesValue
}
func (a *MockArtifact) Id() string {
......
package vagrant
import (
"compress/flate"
"bytes"
"fmt"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
"strings"
)
type AWSBoxConfig struct {
common.PackerConfig `mapstructure:",squash"`
OutputPath string `mapstructure:"output"`
VagrantfileTemplate string `mapstructure:"vagrantfile_template"`
CompressionLevel string `mapstructure:"compression_level"`
tpl *packer.ConfigTemplate
}
type AWSVagrantfileTemplate struct {
Images map[string]string
}
type AWSBoxPostProcessor struct {
config AWSBoxConfig
}
func (p *AWSBoxPostProcessor) Configure(raws ...interface{}) error {
md, err := common.DecodeConfig(&p.config, raws...)
if err != nil {
return err
}
p.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
p.config.tpl.UserVars = p.config.PackerUserVars
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
validates := map[string]*string{
"output": &p.config.OutputPath,
"vagrantfile_template": &p.config.VagrantfileTemplate,
"compression_level": &p.config.CompressionLevel,
}
"text/template"
for n, ptr := range validates {
if err := p.config.tpl.Validate(*ptr); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error parsing %s: %s", n, err))
}
}
"github.com/mitchellh/packer/packer"
)
if errs != nil && len(errs.Errors) > 0 {
return errs
}
type AWSProvider struct{}
return nil
}
func (p *AWSProvider) Process(ui packer.Ui, artifact packer.Artifact, dir string) (vagrantfile string, metadata map[string]interface{}, err error) {
// Create the metadata
metadata = map[string]interface{}{"provider": "aws"}
func (p *AWSBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
// Determine the regions...
tplData := &AWSVagrantfileTemplate{
// Build up the template data to build our Vagrantfile
tplData := &awsVagrantfileTemplate{
Images: make(map[string]string),
}
for _, regions := range strings.Split(artifact.Id(), ",") {
parts := strings.Split(regions, ":")
if len(parts) != 2 {
return nil, false, fmt.Errorf("Poorly formatted artifact ID: %s", artifact.Id())
err = fmt.Errorf("Poorly formatted artifact ID: %s", artifact.Id())
return
}
tplData.Images[parts[0]] = parts[1]
}
// Compile the output path
outputPath, err := p.config.tpl.Process(p.config.OutputPath, &OutputPathTemplate{
ArtifactId: artifact.Id(),
BuildName: p.config.PackerBuildName,
Provider: "aws",
})
if err != nil {
return nil, false, err
}
// Create a temporary directory for us to build the contents of the box in
dir, err := ioutil.TempDir("", "packer")
if err != nil {
return nil, false, err
}
defer os.RemoveAll(dir)
// Create the Vagrantfile from the template
vf, err := os.Create(filepath.Join(dir, "Vagrantfile"))
if err != nil {
return nil, false, err
}
defer vf.Close()
vagrantfileContents := defaultAWSVagrantfile
if p.config.VagrantfileTemplate != "" {
log.Printf("Using vagrantfile template: %s", p.config.VagrantfileTemplate)
f, err := os.Open(p.config.VagrantfileTemplate)
if err != nil {
err = fmt.Errorf("error opening vagrantfile template: %s", err)
return nil, false, err
}
defer f.Close()
contents, err := ioutil.ReadAll(f)
if err != nil {
err = fmt.Errorf("error reading vagrantfile template: %s", err)
return nil, false, err
}
vagrantfileContents = string(contents)
}
vagrantfileContents, err = p.config.tpl.Process(vagrantfileContents, tplData)
if err != nil {
return nil, false, fmt.Errorf("Error writing Vagrantfile: %s", err)
}
vf.Write([]byte(vagrantfileContents))
vf.Close()
var level int = flate.DefaultCompression
if p.config.CompressionLevel != "" {
level, err = strconv.Atoi(p.config.CompressionLevel)
if err != nil {
return nil, false, err
}
}
// Create the metadata
metadata := map[string]string{"provider": "aws"}
if err := WriteMetadata(dir, metadata); err != nil {
return nil, false, err
}
// Compress the directory to the given output path
if err := DirToBox(outputPath, dir, ui, level); err != nil {
err = fmt.Errorf("error creating box: %s", err)
return nil, false, err
}
// Build up the contents
var contents bytes.Buffer
t := template.Must(template.New("vf").Parse(defaultAWSVagrantfile))
err = t.Execute(&contents, tplData)
vagrantfile = contents.String()
return
}
return NewArtifact("aws", outputPath), true, nil
type awsVagrantfileTemplate struct {
Images map[string]string
}
var defaultAWSVagrantfile = `
......
package vagrant
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestAWSBoxPostProcessor_ImplementsPostProcessor(t *testing.T) {
var raw interface{}
raw = &AWSBoxPostProcessor{}
if _, ok := raw.(packer.PostProcessor); !ok {
t.Fatalf("AWS PostProcessor should be a PostProcessor")
}
func TestAWSProvider_impl(t *testing.T) {
var _ Provider = new(AWSProvider)
}
package vagrant
import (
"compress/flate"
"bytes"
"fmt"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
)
type DigitalOceanBoxConfig struct {
common.PackerConfig `mapstructure:",squash"`
OutputPath string `mapstructure:"output"`
VagrantfileTemplate string `mapstructure:"vagrantfile_template"`
CompressionLevel string `mapstructure:"compression_level"`
tpl *packer.ConfigTemplate
}
type DigitalOceanVagrantfileTemplate struct {
type digitalOceanVagrantfileTemplate struct {
Image string ""
Region string ""
}
type DigitalOceanBoxPostProcessor struct {
config DigitalOceanBoxConfig
}
func (p *DigitalOceanBoxPostProcessor) Configure(rDigitalOcean ...interface{}) error {
md, err := common.DecodeConfig(&p.config, rDigitalOcean...)
if err != nil {
return err
}
p.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
p.config.tpl.UserVars = p.config.PackerUserVars
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
type DigitalOceanProvider struct{}
validates := map[string]*string{
"output": &p.config.OutputPath,
"vagrantfile_template": &p.config.VagrantfileTemplate,
"compression_level": &p.config.CompressionLevel,
}
for n, ptr := range validates {
if err := p.config.tpl.Validate(*ptr); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error parsing %s: %s", n, err))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
}
func (p *DigitalOceanProvider) Process(ui packer.Ui, artifact packer.Artifact, dir string) (vagrantfile string, metadata map[string]interface{}, err error) {
// Create the metadata
metadata = map[string]interface{}{"provider": "digital_ocean"}
func (p *DigitalOceanBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
// Determine the image and region...
tplData := &DigitalOceanVagrantfileTemplate{}
tplData := &digitalOceanVagrantfileTemplate{}
parts := strings.Split(artifact.Id(), ":")
if len(parts) != 2 {
return nil, false, fmt.Errorf("Poorly formatted artifact ID: %s", artifact.Id())
err = fmt.Errorf("Poorly formatted artifact ID: %s", artifact.Id())
return
}
tplData.Region = parts[0]
tplData.Image = parts[1]
// Compile the output path
outputPath, err := p.config.tpl.Process(p.config.OutputPath, &OutputPathTemplate{
ArtifactId: artifact.Id(),
BuildName: p.config.PackerBuildName,
Provider: "digitalocean",
})
if err != nil {
return nil, false, err
}
// Create a temporary directory for us to build the contents of the box in
dir, err := ioutil.TempDir("", "packer")
if err != nil {
return nil, false, err
}
defer os.RemoveAll(dir)
// Create the Vagrantfile from the template
vf, err := os.Create(filepath.Join(dir, "Vagrantfile"))
if err != nil {
return nil, false, err
}
defer vf.Close()
vagrantfileContents := defaultDigitalOceanVagrantfile
if p.config.VagrantfileTemplate != "" {
log.Printf("Using vagrantfile template: %s", p.config.VagrantfileTemplate)
f, err := os.Open(p.config.VagrantfileTemplate)
if err != nil {
err = fmt.Errorf("error opening vagrantfile template: %s", err)
return nil, false, err
}
defer f.Close()
contents, err := ioutil.ReadAll(f)
if err != nil {
err = fmt.Errorf("error reading vagrantfile template: %s", err)
return nil, false, err
}
vagrantfileContents = string(contents)
}
vagrantfileContents, err = p.config.tpl.Process(vagrantfileContents, tplData)
if err != nil {
return nil, false, fmt.Errorf("Error writing Vagrantfile: %s", err)
}
vf.Write([]byte(vagrantfileContents))
vf.Close()
// Create the metadata
metadata := map[string]string{"provider": "digital_ocean"}
if err := WriteMetadata(dir, metadata); err != nil {
return nil, false, err
}
// Compress the directory to the given output path
var level int = flate.DefaultCompression
if p.config.CompressionLevel != "" {
level, err = strconv.Atoi(p.config.CompressionLevel)
if err != nil {
return nil, false, err
}
}
if err := DirToBox(outputPath, dir, ui, level); err != nil {
err = fmt.Errorf("error creating box: %s", err)
return nil, false, err
}
return NewArtifact("DigitalOcean", outputPath), true, nil
// Build up the Vagrantfile
var contents bytes.Buffer
t := template.Must(template.New("vf").Parse(defaultDigitalOceanVagrantfile))
err = t.Execute(&contents, tplData)
vagrantfile = contents.String()
return
}
var defaultDigitalOceanVagrantfile = `
......@@ -159,5 +45,4 @@ Vagrant.configure("2") do |config|
digital_ocean.region = "{{ .Region }}"
end
end
`
package vagrant
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestDigitalOceanBoxPostProcessor_ImplementsPostProcessor(t *testing.T) {
var raw interface{}
raw = &DigitalOceanBoxPostProcessor{}
if _, ok := raw.(packer.PostProcessor); !ok {
t.Fatalf("Digitalocean PostProcessor should be a PostProcessor")
}
func TestDigitalOceanProvider_impl(t *testing.T) {
var _ Provider = new(DigitalOceanProvider)
}
......@@ -4,11 +4,15 @@
package vagrant
import (
"compress/flate"
"fmt"
"github.com/mitchellh/mapstructure"
"io/ioutil"
"os"
"path/filepath"
"text/template"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
)
var builtins = map[string]string{
......@@ -22,133 +26,213 @@ var builtins = map[string]string{
type Config struct {
common.PackerConfig `mapstructure:",squash"`
OutputPath string `mapstructure:"output"`
CompressionLevel int `mapstructure:"compression_level"`
Include []string `mapstructure:"include"`
OutputPath string `mapstructure:"output"`
Override map[string]interface{}
VagrantfileTemplate string `mapstructure:"vagrantfile_template"`
tpl *packer.ConfigTemplate
}
type PostProcessor struct {
config Config
premade map[string]packer.PostProcessor
extraConfig map[string]interface{}
configs map[string]*Config
}
func (p *PostProcessor) Configure(raws ...interface{}) error {
_, err := common.DecodeConfig(&p.config, raws...)
if err != nil {
p.configs = make(map[string]*Config)
p.configs[""] = new(Config)
if err := p.configureSingle(p.configs[""], raws...); err != nil {
return err
}
tpl, err := packer.NewConfigTemplate()
// Go over any of the provider-specific overrides and load those up.
for name, override := range p.configs[""].Override {
subRaws := make([]interface{}, len(raws)+1)
copy(subRaws, raws)
subRaws[len(raws)] = override
config := new(Config)
p.configs[name] = config
if err := p.configureSingle(config, subRaws...); err != nil {
return fmt.Errorf("Error configuring %s: %s", name, err)
}
}
return nil
}
func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
name, ok := builtins[artifact.BuilderId()]
if !ok {
return nil, false, fmt.Errorf(
"Unknown artifact type, can't build box: %s", artifact.BuilderId())
}
provider := providerForName(name)
if provider == nil {
// This shouldn't happen since we hard code all of these ourselves
panic(fmt.Sprintf("bad provider name: %s", name))
}
config := p.configs[""]
if specificConfig, ok := p.configs[name]; ok {
config = specificConfig
}
ui.Say(fmt.Sprintf("Creating Vagrant box for '%s' provider", name))
outputPath, err := config.tpl.Process(config.OutputPath, &outputPathTemplate{
ArtifactId: artifact.Id(),
BuildName: config.PackerBuildName,
Provider: name,
})
if err != nil {
return err
return nil, false, err
}
tpl.UserVars = p.config.PackerUserVars
// Defaults
if p.config.OutputPath == "" {
p.config.OutputPath = "packer_{{ .BuildName }}_{{.Provider}}.box"
// Create a temporary directory for us to build the contents of the box in
dir, err := ioutil.TempDir("", "packer")
if err != nil {
return nil, false, err
}
defer os.RemoveAll(dir)
// Copy all of the includes files into the temporary directory
for _, src := range config.Include {
ui.Message(fmt.Sprintf("Copying from include: %s", src))
dst := filepath.Join(dir, filepath.Base(src))
if err := CopyContents(dst, src); err != nil {
err = fmt.Errorf("Error copying include file: %s\n\n%s", src, err)
return nil, false, err
}
}
// Accumulate any errors
errs := new(packer.MultiError)
if err := tpl.Validate(p.config.OutputPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error parsing output template: %s", err))
}
// Store extra configuration we'll send to each post-processor type
p.extraConfig = make(map[string]interface{})
p.extraConfig["output"] = p.config.OutputPath
p.extraConfig["packer_build_name"] = p.config.PackerBuildName
p.extraConfig["packer_builder_type"] = p.config.PackerBuilderType
p.extraConfig["packer_debug"] = p.config.PackerDebug
p.extraConfig["packer_force"] = p.config.PackerForce
p.extraConfig["packer_user_variables"] = p.config.PackerUserVars
// TODO(mitchellh): Properly handle multiple raw configs. This isn't
// very pressing at the moment because at the time of this comment
// only the first member of raws can contain the actual type-overrides.
var mapConfig map[string]interface{}
if err := mapstructure.Decode(raws[0], &mapConfig); err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Failed to decode config: %s", err))
return errs
// Run the provider processing step
vagrantfile, metadata, err := provider.Process(ui, artifact, dir)
if err != nil {
return nil, false, err
}
// Write the metadata we got
if err := WriteMetadata(dir, metadata); err != nil {
return nil, false, err
}
p.premade = make(map[string]packer.PostProcessor)
for k, raw := range mapConfig {
pp, err := p.subPostProcessor(k, raw, p.extraConfig)
// Write our Vagrantfile
var customVagrantfile string
if config.VagrantfileTemplate != "" {
ui.Message(fmt.Sprintf(
"Using custom Vagrantfile: %s", config.VagrantfileTemplate))
customBytes, err := ioutil.ReadFile(config.VagrantfileTemplate)
if err != nil {
errs = packer.MultiErrorAppend(errs, err)
continue
return nil, false, err
}
if pp == nil {
continue
}
customVagrantfile = string(customBytes)
}
p.premade[k] = pp
f, err := os.Create(filepath.Join(dir, "Vagrantfile"))
if err != nil {
return nil, false, err
}
if len(errs.Errors) > 0 {
return errs
t := template.Must(template.New("root").Parse(boxVagrantfileContents))
err = t.Execute(f, &vagrantfileTemplate{
ProviderVagrantfile: vagrantfile,
CustomVagrantfile: customVagrantfile,
})
f.Close()
if err != nil {
return nil, false, err
}
return nil
// Create the box
if err := DirToBox(outputPath, dir, ui, config.CompressionLevel); err != nil {
return nil, false, err
}
return nil, false, nil
}
func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
ppName, ok := builtins[artifact.BuilderId()]
if !ok {
return nil, false, fmt.Errorf("Unknown artifact type, can't build box: %s", artifact.BuilderId())
func (p *PostProcessor) configureSingle(config *Config, raws ...interface{}) error {
md, err := common.DecodeConfig(config, raws...)
if err != nil {
return err
}
// Use the premade PostProcessor if we have one. Otherwise, we
// create it and configure it here.
pp, ok := p.premade[ppName]
if !ok {
log.Printf("Premade post-processor for '%s' not found. Creating.", ppName)
config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
config.tpl.UserVars = config.PackerUserVars
var err error
pp, err = p.subPostProcessor(ppName, nil, p.extraConfig)
if err != nil {
return nil, false, err
}
// Defaults
if config.OutputPath == "" {
config.OutputPath = "packer_{{ .BuildName }}_{{.Provider}}.box"
}
if pp == nil {
return nil, false, fmt.Errorf("Vagrant box post-processor not found: %s", ppName)
found := false
for _, k := range md.Keys {
if k == "compression_level" {
found = true
break
}
}
ui.Say(fmt.Sprintf("Creating Vagrant box for '%s' provider", ppName))
return pp.PostProcess(ui, artifact)
}
if !found {
config.CompressionLevel = flate.DefaultCompression
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
validates := map[string]*string{
"output": &config.OutputPath,
"vagrantfile_template": &config.VagrantfileTemplate,
}
func (p *PostProcessor) subPostProcessor(key string, specific interface{}, extra map[string]interface{}) (packer.PostProcessor, error) {
pp := keyToPostProcessor(key)
if pp == nil {
return nil, nil
for n, ptr := range validates {
if err := config.tpl.Validate(*ptr); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error parsing %s: %s", n, err))
}
}
if err := pp.Configure(extra, specific); err != nil {
return nil, err
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return pp, nil
return nil
}
// keyToPostProcessor maps a configuration key to the actual post-processor
// it will be configuring. This returns a new instance of that post-processor.
func keyToPostProcessor(key string) packer.PostProcessor {
switch key {
case "aws":
return new(AWSBoxPostProcessor)
case "digitalocean":
return new(DigitalOceanBoxPostProcessor)
func providerForName(name string) Provider {
switch name {
case "virtualbox":
return new(VBoxBoxPostProcessor)
case "vmware":
return new(VMwareBoxPostProcessor)
return new(VBoxProvider)
default:
return nil
}
}
// OutputPathTemplate is the structure that is availalable within the
// OutputPath variables.
type outputPathTemplate struct {
ArtifactId string
BuildName string
Provider string
}
type vagrantfileTemplate struct {
ProviderVagrantfile string
CustomVagrantfile string
}
const boxVagrantfileContents string = `
# The contents below were provided by the Packer Vagrant post-processor
{{ .ProviderVagrantfile }}
# The contents below (if any) are custom contents provided by the
# Packer template during image build.
{{ .CustomVagrantfile }}
`
package vagrant
import (
"bytes"
"compress/flate"
"github.com/mitchellh/packer/packer"
"strings"
"testing"
)
......@@ -9,15 +12,55 @@ func testConfig() map[string]interface{} {
return map[string]interface{}{}
}
func testPP(t *testing.T) *PostProcessor {
var p PostProcessor
if err := p.Configure(testConfig()); err != nil {
t.Fatalf("err: %s", err)
}
return &p
}
func testUi() *packer.BasicUi {
return &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
}
func TestPostProcessor_ImplementsPostProcessor(t *testing.T) {
var raw interface{}
raw = &PostProcessor{}
if _, ok := raw.(packer.PostProcessor); !ok {
t.Fatalf("AWS PostProcessor should be a PostProcessor")
var _ packer.PostProcessor = new(PostProcessor)
}
func TestPostProcessorPrepare_compressionLevel(t *testing.T) {
var p PostProcessor
// Default
c := testConfig()
delete(c, "compression_level")
if err := p.Configure(c); err != nil {
t.Fatalf("err: %s", err)
}
config := p.configs[""]
if config.CompressionLevel != flate.DefaultCompression {
t.Fatalf("bad: %#v", config.CompressionLevel)
}
// Set
c = testConfig()
c["compression_level"] = 7
if err := p.Configure(c); err != nil {
t.Fatalf("err: %s", err)
}
config = p.configs[""]
if config.CompressionLevel != 7 {
t.Fatalf("bad: %#v", config.CompressionLevel)
}
}
func TestBuilderPrepare_OutputPath(t *testing.T) {
func TestPostProcessorPrepare_outputPath(t *testing.T) {
var p PostProcessor
// Default
......@@ -36,14 +79,57 @@ func TestBuilderPrepare_OutputPath(t *testing.T) {
}
}
func TestBuilderPrepare_PPConfig(t *testing.T) {
func TestPostProcessorPrepare_subConfigs(t *testing.T) {
var p PostProcessor
// Default
c := testConfig()
c["aws"] = map[string]interface{}{}
c["compression_level"] = 42
c["vagrantfile_template"] = "foo"
c["override"] = map[string]interface{}{
"aws": map[string]interface{}{
"compression_level": 7,
},
}
err := p.Configure(c)
if err != nil {
t.Fatalf("err: %s", err)
}
if p.configs[""].CompressionLevel != 42 {
t.Fatalf("bad: %#v", p.configs[""].CompressionLevel)
}
if p.configs[""].VagrantfileTemplate != "foo" {
t.Fatalf("bad: %#v", p.configs[""].VagrantfileTemplate)
}
if p.configs["aws"].CompressionLevel != 7 {
t.Fatalf("bad: %#v", p.configs["aws"].CompressionLevel)
}
if p.configs["aws"].VagrantfileTemplate != "foo" {
t.Fatalf("bad: %#v", p.configs["aws"].VagrantfileTemplate)
}
}
func TestPostProcessorPostProcess_badId(t *testing.T) {
artifact := &packer.MockArtifact{
BuilderIdValue: "invalid.packer",
}
_, _, err := testPP(t).PostProcess(testUi(), artifact)
if !strings.Contains(err.Error(), "artifact type") {
t.Fatalf("err: %s", err)
}
}
func TestProviderForName(t *testing.T) {
if v, ok := providerForName("virtualbox").(*VBoxProvider); !ok {
t.Fatalf("bad: %#v", v)
}
if providerForName("nope") != nil {
t.Fatal("should be nil if bad provider")
}
}
package vagrant
import (
"github.com/mitchellh/packer/packer"
)
// Provider is the interface that each provider must implement in order
// to package the artifacts into a Vagrant-compatible box.
type Provider interface {
// Process is called to process an artifact into a Vagrant box. The
// artifact is given as well as the temporary directory path to
// put things.
//
// The Provider should return the contents for the Vagrantfile,
// any metadata (including the provider type in that), and an error
// if any.
Process(packer.Ui, packer.Artifact, string) (vagrantfile string, metadata map[string]interface{}, err error)
}
......@@ -13,14 +13,6 @@ import (
"path/filepath"
)
// OutputPathTemplate is the structure that is availalable within the
// OutputPath variables.
type OutputPathTemplate struct {
ArtifactId string
BuildName string
Provider string
}
// Copies a file by copying the contents of the file to another place.
func CopyContents(dst, src string) error {
srcF, err := os.Open(src)
......
......@@ -2,10 +2,8 @@ package vagrant
import (
"archive/tar"
"compress/flate"
"errors"
"fmt"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
......@@ -15,183 +13,49 @@ import (
"regexp"
)
type VBoxBoxConfig struct {
common.PackerConfig `mapstructure:",squash"`
type VBoxProvider struct{}
Include []string `mapstructure:"include"`
OutputPath string `mapstructure:"output"`
VagrantfileTemplate string `mapstructure:"vagrantfile_template"`
CompressionLevel int `mapstructure:"compression_level"`
tpl *packer.ConfigTemplate
}
type VBoxVagrantfileTemplate struct {
BaseMacAddress string
}
type VBoxBoxPostProcessor struct {
config VBoxBoxConfig
}
func (p *VBoxBoxPostProcessor) Configure(raws ...interface{}) error {
md, err := common.DecodeConfig(&p.config, raws...)
if err != nil {
return err
}
p.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
p.config.tpl.UserVars = p.config.PackerUserVars
// Defaults
found := false
for _, k := range md.Keys {
println(k)
if k == "compression_level" {
found = true
break
}
}
if !found {
p.config.CompressionLevel = flate.DefaultCompression
}
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
validates := map[string]*string{
"output": &p.config.OutputPath,
"vagrantfile_template": &p.config.VagrantfileTemplate,
}
for n, ptr := range validates {
if err := p.config.tpl.Validate(*ptr); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error parsing %s: %s", n, err))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
}
func (p *VBoxBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
var err error
// Compile the output path
outputPath, err := p.config.tpl.Process(p.config.OutputPath, &OutputPathTemplate{
ArtifactId: artifact.Id(),
BuildName: p.config.PackerBuildName,
Provider: "virtualbox",
})
if err != nil {
return nil, false, err
}
// Create a temporary directory for us to build the contents of the box in
dir, err := ioutil.TempDir("", "packer")
if err != nil {
return nil, false, err
}
defer os.RemoveAll(dir)
// Copy all of the includes files into the temporary directory
for _, src := range p.config.Include {
ui.Message(fmt.Sprintf("Copying from include: %s", src))
dst := filepath.Join(dir, filepath.Base(src))
if err := CopyContents(dst, src); err != nil {
err = fmt.Errorf("Error copying include file: %s\n\n%s", src, err)
return nil, false, err
}
}
func (p *VBoxProvider) Process(ui packer.Ui, artifact packer.Artifact, dir string) (vagrantfile string, metadata map[string]interface{}, err error) {
// Create the metadata
metadata = map[string]interface{}{"provider": "virtualbox"}
// Copy all of the original contents into the temporary directory
for _, path := range artifact.Files() {
// We treat OVA files specially, we unpack those into the temporary
// directory so we can get the resulting disk and OVF.
if extension := filepath.Ext(path); extension == ".ova" {
ui.Message(fmt.Sprintf("Unpacking OVA: %s", path))
if err := DecompressOva(dir, path); err != nil {
return nil, false, err
if err = DecompressOva(dir, path); err != nil {
return
}
} else {
ui.Message(fmt.Sprintf("Copying from artifact: %s", path))
dstPath := filepath.Join(dir, filepath.Base(path))
if err := CopyContents(dstPath, path); err != nil {
return nil, false, err
if err = CopyContents(dstPath, path); err != nil {
return
}
}
}
// Create the Vagrantfile from the template
tplData := &VBoxVagrantfileTemplate{}
tplData.BaseMacAddress, err = p.findBaseMacAddress(dir)
if err != nil {
return nil, false, err
}
vf, err := os.Create(filepath.Join(dir, "Vagrantfile"))
if err != nil {
return nil, false, err
}
defer vf.Close()
vagrantfileContents := defaultVBoxVagrantfile
if p.config.VagrantfileTemplate != "" {
ui.Message(fmt.Sprintf(
"Using Vagrantfile template: %s", p.config.VagrantfileTemplate))
f, err := os.Open(p.config.VagrantfileTemplate)
if err != nil {
return nil, false, err
}
defer f.Close()
contents, err := ioutil.ReadAll(f)
if err != nil {
return nil, false, err
}
vagrantfileContents = string(contents)
}
vagrantfileContents, err = p.config.tpl.Process(vagrantfileContents, tplData)
if err != nil {
return nil, false, fmt.Errorf("Error writing Vagrantfile: %s", err)
}
vf.Write([]byte(vagrantfileContents))
vf.Close()
// Create the metadata
metadata := map[string]string{"provider": "virtualbox"}
if err := WriteMetadata(dir, metadata); err != nil {
return nil, false, err
}
// Rename the OVF file to box.ovf, as required by Vagrant
ui.Message("Renaming the OVF to box.ovf...")
if err := p.renameOVF(dir); err != nil {
return nil, false, err
if err = p.renameOVF(dir); err != nil {
return
}
// Compress the directory to the given output path
ui.Message(fmt.Sprintf("Compressing box..."))
if err := DirToBox(outputPath, dir, ui, p.config.CompressionLevel); err != nil {
return nil, false, err
// Create the Vagrantfile from the template
var baseMacAddress string
baseMacAddress, err = p.findBaseMacAddress(dir)
if err != nil {
return
}
return NewArtifact("virtualbox", outputPath), false, nil
vagrantfile = fmt.Sprintf(vboxVagrantfile, baseMacAddress)
return
}
func (p *VBoxBoxPostProcessor) findOvf(dir string) (string, error) {
func (p *VBoxProvider) findOvf(dir string) (string, error) {
log.Println("Looking for OVF in artifact...")
file_matches, err := filepath.Glob(filepath.Join(dir, "*.ovf"))
if err != nil {
......@@ -209,7 +73,7 @@ func (p *VBoxBoxPostProcessor) findOvf(dir string) (string, error) {
return file_matches[0], err
}
func (p *VBoxBoxPostProcessor) renameOVF(dir string) error {
func (p *VBoxProvider) renameOVF(dir string) error {
log.Println("Looking for OVF to rename...")
ovf, err := p.findOvf(dir)
if err != nil {
......@@ -220,7 +84,7 @@ func (p *VBoxBoxPostProcessor) renameOVF(dir string) error {
return os.Rename(ovf, filepath.Join(dir, "box.ovf"))
}
func (p *VBoxBoxPostProcessor) findBaseMacAddress(dir string) (string, error) {
func (p *VBoxProvider) findBaseMacAddress(dir string) (string, error) {
log.Println("Looking for OVF for base mac address...")
ovf, err := p.findOvf(dir)
if err != nil {
......@@ -295,8 +159,8 @@ func DecompressOva(dir, src string) error {
return nil
}
var defaultVBoxVagrantfile = `
var vboxVagrantfile = `
Vagrant.configure("2") do |config|
config.vm.base_mac = "{{ .BaseMacAddress }}"
config.vm.base_mac = "%s"
end
`
package vagrant
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestVBoxBoxPostProcessor_ImplementsPostProcessor(t *testing.T) {
var raw interface{}
raw = &VBoxBoxPostProcessor{}
if _, ok := raw.(packer.PostProcessor); !ok {
t.Fatalf("VBox PostProcessor should be a PostProcessor")
}
func TestVBoxProvider_impl(t *testing.T) {
var _ Provider = new(VBoxProvider)
}
package vagrant
import (
"compress/flate"
"fmt"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"os"
"path/filepath"
"strconv"
)
type VMwareBoxConfig struct {
common.PackerConfig `mapstructure:",squash"`
type VMwareProvider struct{}
OutputPath string `mapstructure:"output"`
VagrantfileTemplate string `mapstructure:"vagrantfile_template"`
CompressionLevel string `mapstructure:"compression_level"`
tpl *packer.ConfigTemplate
}
type VMwareBoxPostProcessor struct {
config VMwareBoxConfig
}
func (p *VMwareBoxPostProcessor) Configure(raws ...interface{}) error {
md, err := common.DecodeConfig(&p.config, raws...)
if err != nil {
return err
}
p.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return err
}
p.config.tpl.UserVars = p.config.PackerUserVars
// Accumulate any errors
errs := common.CheckUnusedConfig(md)
validates := map[string]*string{
"output": &p.config.OutputPath,
"vagrantfile_template": &p.config.VagrantfileTemplate,
"compression_level": &p.config.CompressionLevel,
}
for n, ptr := range validates {
if err := p.config.tpl.Validate(*ptr); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Error parsing %s: %s", n, err))
}
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
}
func (p *VMwareBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
// Compile the output path
outputPath, err := p.config.tpl.Process(p.config.OutputPath, &OutputPathTemplate{
ArtifactId: artifact.Id(),
BuildName: p.config.PackerBuildName,
Provider: "vmware",
})
if err != nil {
return nil, false, err
}
// Create a temporary directory for us to build the contents of the box in
dir, err := ioutil.TempDir("", "packer")
if err != nil {
return nil, false, err
}
defer os.RemoveAll(dir)
func (p *VMwareProvider) Process(ui packer.Ui, artifact packer.Artifact, dir string) (vagrantfile string, metadata map[string]interface{}, err error) {
// Create the metadata
metadata = map[string]interface{}{"provider": "vmware_desktop"}
// Copy all of the original contents into the temporary directory
for _, path := range artifact.Files() {
ui.Message(fmt.Sprintf("Copying: %s", path))
dstPath := filepath.Join(dir, filepath.Base(path))
if err := CopyContents(dstPath, path); err != nil {
return nil, false, err
}
}
if p.config.VagrantfileTemplate != "" {
f, err := os.Open(p.config.VagrantfileTemplate)
if err != nil {
return nil, false, err
}
defer f.Close()
contents, err := ioutil.ReadAll(f)
if err != nil {
return nil, false, err
}
// Create the Vagrantfile from the template
vf, err := os.Create(filepath.Join(dir, "Vagrantfile"))
if err != nil {
return nil, false, err
if err = CopyContents(dstPath, path); err != nil {
return
}
defer vf.Close()
vagrantfileContents, err := p.config.tpl.Process(string(contents), nil)
if err != nil {
return nil, false, fmt.Errorf("Error writing Vagrantfile: %s", err)
}
vf.Write([]byte(vagrantfileContents))
vf.Close()
}
var level int = flate.DefaultCompression
if p.config.CompressionLevel != "" {
level, err = strconv.Atoi(p.config.CompressionLevel)
if err != nil {
return nil, false, err
}
}
// Create the metadata
metadata := map[string]string{"provider": "vmware_desktop"}
if err := WriteMetadata(dir, metadata); err != nil {
return nil, false, err
}
// Compress the directory to the given output path
ui.Message(fmt.Sprintf("Compressing box..."))
if err := DirToBox(outputPath, dir, ui, level); err != nil {
return nil, false, err
}
return NewArtifact("vmware", outputPath), false, nil
return
}
package vagrant
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func TestVMwareBoxPostProcessor_ImplementsPostProcessor(t *testing.T) {
var raw interface{}
raw = &VMwareBoxPostProcessor{}
if _, ok := raw.(packer.PostProcessor); !ok {
t.Fatalf("VMware PostProcessor should be a PostProcessor")
}
func TestVMwareProvider_impl(t *testing.T) {
var _ Provider = new(VMwareProvider)
}
......@@ -18,7 +18,7 @@ documentation on [using post-processors](/docs/templates/post-processors.html)
in templates. This knowledge will be expected for the remainder of
this document.
Because Vagrant boxes are [provider-specific](#),
Because Vagrant boxes are [provider-specific](http://docs.vagrantup.com/v2/boxes/format.html),
the Vagrant post-processor is hardcoded to understand how to convert
the artifacts of certain builders into proper boxes for their
respective providers.
......@@ -27,6 +27,7 @@ Currently, the Vagrant post-processor can create boxes for the following
providers.
* AWS
* DigitalOcean
* VirtualBox
* VMware
......@@ -47,82 +48,52 @@ However, if you want to configure things a bit more, the post-processor
does expose some configuration options. The available options are listed
below, with more details about certain options in following sections.
* `compression_level` (integer) - An integer repesenting the
compression level to use when creating the Vagrant box. Valid
values range from 0 to 9, with 0 being no compression and 9 being
the best compression. By default, compression is enabled at level 1.
* `include` (array of strings) - Paths to files to include in the
Vagrant box. These files will each be copied into the top level directory
of the Vagrant box (regardless of their paths). They can then be used
from the Vagrantfile.
* `output` (string) - The full path to the box file that will be created
by this post-processor. This is a
[configuration template](/docs/templates/configuration-templates.html).
The variable `Provider` is replaced by the Vagrant provider the box is for.
The variable `ArtifactId` is replaced by the ID of the input artifact.
The variable `BuildName` is replaced with the name of the build.
By default, the value of this config is `packer_{{.BuildName}}_{{.Provider}}.box`.
* `aws`, `virtualbox`, or `vmware` (objects) - These are used to configure
the specific options for certain providers. A reference of available
configuration parameters for each is in the section below.
### AWS Provider
The AWS provider itself can be configured with specific options:
* `vagrantfile_template` (string) - Path to a template to use for the
Vagrantfile that is packaged with the box. The contents of the file must be a valid Go
[text template](http://golang.org/pkg/text/template). By default
this is a template that simply sets the AMIs for the various regions
of the AWS build.
* `compression_level` (integer) - An integer repesenting the
compression level to use when creating the Vagrant box. Valid
values range from 0 to 9, with 0 being no compression and 9 being
the best compression.
The `vagrantfile_template` has the `Images` variable which is a map
of region (string) to AMI ID (string). An example Vagrantfile template for
AWS is shown below. The example simply sets the AMI for each region.
```
Vagrant.configure("2") do |config|
config.vm.provider "aws" do |aws|
{{ range $region, $ami := .Images }}
aws.region_config "{{ $region }}", ami: "{{ $ami }}"
{{ end }}
end
end
```
Vagrantfile that is packaged with the box.
### VirtualBox Provider
## Provider-Specific Overrides
The VirtualBox provider itself can be configured with specific options:
* `vagrantfile_template` (string) - Path to a template to use for the
Vagrantfile that is packaged with the box. The contents of the file must be a valid Go
[text template](http://golang.org/pkg/text/template). By default this is
a template that just sets the base MAC address so that networking works.
If you have a Packer template with multiple builder types within it,
you may want to configure the box creation for each type a little differently.
For example, the contents of the Vagrantfile for a Vagrant box for AWS might
be different from the contents of the Vagrantfile you want for VMware.
The post-processor lets you do this.
* `compression_level` (integer) - An integer repesenting the
compression level to use when creating the Vagrant box. Valid
values range from 0 to 9, with 0 being no compression and 9 being
the best compression.
Specify overrides within the `override` configuration by provider name:
The `vagrantfile_template` has the `BaseMACAddress` variable which is a string
containing the MAC address of the first network interface. This must be set
in the Vagrantfile for networking to work properly with Vagrant. An example
Vagrantfile template is shown below:
```json
{
"type": "vagrant",
"compression_level": 1,
"override": {
"vmware": {
"compression_level": 0
}
}
}
```
Vagrant.configure("2") do |config|
config.vm.base_mac = "{{ .BaseMacAddress }}"
end
```
### VMware Provider
The VMware provider itself can be configured with specific options:
* `vagrantfile_template` (string) - Path to a template to use for the
Vagrantfile that is packaged with the box. The contents of the file must be a valid Go
[text template](http://golang.org/pkg/text/template). By default no
Vagrantfile is packaged with the box. Note that currently no variables
are available in the template, but this may change in the future.
In the example above, the compression level will be set to 1 except for
VMware, where it will be set to 0.
* `compression_level` (integer) - An integer repesenting the
compression level to use when creating the Vagrant box. Valid
values range from 0 to 9, with 0 being no compression and 9 being
the best compression.
The available provider names are: `aws`, `digitalocean`, `virtualbox`,
and `vmware`.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment