From 3dd4c08f2d95b2afdaa998b267eee5780e0bc15e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Dec 2013 13:22:46 -0800 Subject: [PATCH] post-processor/vagrant: start new format --- post-processor/vagrant/post-processor.go | 211 +++++++++++------- post-processor/vagrant/post-processor_test.go | 20 +- post-processor/vagrant/provider.go | 18 ++ post-processor/vagrant/util.go | 8 - post-processor/vagrant/virtualbox.go | 180 ++------------- post-processor/vagrant/virtualbox_test.go | 9 +- 6 files changed, 182 insertions(+), 264 deletions(-) create mode 100644 post-processor/vagrant/provider.go diff --git a/post-processor/vagrant/post-processor.go b/post-processor/vagrant/post-processor.go index 75d99215a..57ef84b92 100644 --- a/post-processor/vagrant/post-processor.go +++ b/post-processor/vagrant/post-processor.go @@ -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,74 +26,76 @@ var builtins = map[string]string{ type Config struct { common.PackerConfig `mapstructure:",squash"` - OutputPath string `mapstructure:"output"` + Include []string `mapstructure:"include"` + OutputPath string `mapstructure:"output"` + VagrantfileTemplate string `mapstructure:"vagrantfile_template"` + CompressionLevel int `mapstructure:"compression_level"` + + tpl *packer.ConfigTemplate +} + +// OutputPathTemplate is the structure that is availalable within the +// OutputPath variables. +type OutputPathTemplate struct { + ArtifactId string + BuildName string + Provider string } type PostProcessor struct { - config Config - premade map[string]packer.PostProcessor - extraConfig map[string]interface{} + config Config +} + +type VagrantfileTemplate struct { + ProviderVagrantfile string + CustomVagrantfile string } func (p *PostProcessor) Configure(raws ...interface{}) error { - _, err := common.DecodeConfig(&p.config, raws...) + md, err := common.DecodeConfig(&p.config, raws...) if err != nil { return err } - tpl, err := packer.NewConfigTemplate() + p.config.tpl, err = packer.NewConfigTemplate() if err != nil { return err } - tpl.UserVars = p.config.PackerUserVars + p.config.tpl.UserVars = p.config.PackerUserVars // Defaults if p.config.OutputPath == "" { p.config.OutputPath = "packer_{{ .BuildName }}_{{.Provider}}.box" } - // 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 + found := false + for _, k := range md.Keys { + if k == "compression_level" { + found = true + break + } } - p.premade = make(map[string]packer.PostProcessor) - for k, raw := range mapConfig { - pp, err := p.subPostProcessor(k, raw, p.extraConfig) - if err != nil { - errs = packer.MultiErrorAppend(errs, err) - continue - } + if !found { + p.config.CompressionLevel = flate.DefaultCompression + } - if pp == nil { - continue - } + // Accumulate any errors + errs := common.CheckUnusedConfig(md) + + validates := map[string]*string{ + "output": &p.config.OutputPath, + "vagrantfile_template": &p.config.VagrantfileTemplate, + } - p.premade[k] = pp + 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 len(errs.Errors) > 0 { + if errs != nil && len(errs.Errors) > 0 { return errs } @@ -97,58 +103,107 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { } func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { - ppName, ok := builtins[artifact.BuilderId()] + name, ok := builtins[artifact.BuilderId()] if !ok { - return nil, false, fmt.Errorf("Unknown artifact type, can't build box: %s", artifact.BuilderId()) + return nil, false, fmt.Errorf( + "Unknown artifact type, can't build box: %s", artifact.BuilderId()) } - // 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) + 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)) + } - var err error - pp, err = p.subPostProcessor(ppName, nil, p.extraConfig) - if err != nil { + ui.Say(fmt.Sprintf("Creating Vagrant box for '%s' provider", name)) + + outputPath, err := p.config.tpl.Process(p.config.OutputPath, &OutputPathTemplate{ + ArtifactId: artifact.Id(), + BuildName: p.config.PackerBuildName, + Provider: name, + }) + 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 } + } + + // 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 + } - if pp == nil { - return nil, false, fmt.Errorf("Vagrant box post-processor not found: %s", ppName) + // Write our Vagrantfile + var customVagrantfile string + if p.config.VagrantfileTemplate != "" { + ui.Message(fmt.Sprintf( + "Using custom Vagrantfile: %s", p.config.VagrantfileTemplate)) + customBytes, err := ioutil.ReadFile(p.config.VagrantfileTemplate) + if err != nil { + return nil, false, err } + + customVagrantfile = string(customBytes) } - ui.Say(fmt.Sprintf("Creating Vagrant box for '%s' provider", ppName)) - return pp.PostProcess(ui, artifact) -} + f, err := os.Create(filepath.Join(dir, "Vagrantfile")) + if err != nil { + return nil, false, err + } -func (p *PostProcessor) subPostProcessor(key string, specific interface{}, extra map[string]interface{}) (packer.PostProcessor, error) { - pp := keyToPostProcessor(key) - if pp == nil { - return nil, nil + 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 } - if err := pp.Configure(extra, specific); err != nil { - return nil, err + // Create the box + if err := DirToBox(outputPath, dir, ui, p.config.CompressionLevel); err != nil { + return nil, false, err } - return pp, nil + return nil, false, 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 } } + +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 }} +` diff --git a/post-processor/vagrant/post-processor_test.go b/post-processor/vagrant/post-processor_test.go index 9a0ac876b..9046b0cf0 100644 --- a/post-processor/vagrant/post-processor_test.go +++ b/post-processor/vagrant/post-processor_test.go @@ -10,11 +10,7 @@ func testConfig() map[string]interface{} { } 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 TestBuilderPrepare_OutputPath(t *testing.T) { @@ -36,14 +32,12 @@ func TestBuilderPrepare_OutputPath(t *testing.T) { } } -func TestBuilderPrepare_PPConfig(t *testing.T) { - var p PostProcessor +func TestProviderForName(t *testing.T) { + if v, ok := providerForName("virtualbox").(*VBoxProvider); !ok { + t.Fatalf("bad: %#v", v) + } - // Default - c := testConfig() - c["aws"] = map[string]interface{}{} - err := p.Configure(c) - if err != nil { - t.Fatalf("err: %s", err) + if providerForName("nope") != nil { + t.Fatal("should be nil if bad provider") } } diff --git a/post-processor/vagrant/provider.go b/post-processor/vagrant/provider.go new file mode 100644 index 000000000..00201cb9c --- /dev/null +++ b/post-processor/vagrant/provider.go @@ -0,0 +1,18 @@ +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) +} diff --git a/post-processor/vagrant/util.go b/post-processor/vagrant/util.go index e8be84e43..d6a1a750a 100644 --- a/post-processor/vagrant/util.go +++ b/post-processor/vagrant/util.go @@ -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) diff --git a/post-processor/vagrant/virtualbox.go b/post-processor/vagrant/virtualbox.go index 2711c9159..eeaf401c6 100644 --- a/post-processor/vagrant/virtualbox.go +++ b/post-processor/vagrant/virtualbox.go @@ -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 ` diff --git a/post-processor/vagrant/virtualbox_test.go b/post-processor/vagrant/virtualbox_test.go index eb525c29b..c3815ce77 100644 --- a/post-processor/vagrant/virtualbox_test.go +++ b/post-processor/vagrant/virtualbox_test.go @@ -1,14 +1,9 @@ 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) }