diff --git a/config.go b/config.go index 9f1485aa8..44d362d26 100644 --- a/config.go +++ b/config.go @@ -47,7 +47,8 @@ const defaultConfig = ` "vagrant": "packer-post-processor-vagrant", "vsphere": "packer-post-processor-vsphere", "docker-push": "packer-post-processor-docker-push", - "docker-import": "packer-post-processor-docker-import" + "docker-import": "packer-post-processor-docker-import", + "vagrant-cloud": "packer-post-processor-vagrant-cloud" }, "provisioners": { diff --git a/plugin/post-processor-vagrant-cloud/main.go b/plugin/post-processor-vagrant-cloud/main.go new file mode 100644 index 000000000..b5e3e7044 --- /dev/null +++ b/plugin/post-processor-vagrant-cloud/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/post-processor/vagrant-cloud" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterPostProcessor(new(vagrantcloud.PostProcessor)) + server.Serve() +} diff --git a/plugin/post-processor-vagrant-cloud/main_test.go b/plugin/post-processor-vagrant-cloud/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/post-processor-vagrant-cloud/main_test.go @@ -0,0 +1 @@ +package main diff --git a/post-processor/vagrant-cloud/api.go b/post-processor/vagrant-cloud/api.go new file mode 100644 index 000000000..ba809c358 --- /dev/null +++ b/post-processor/vagrant-cloud/api.go @@ -0,0 +1,97 @@ +package vagrantcloud + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strings" +) + +type Box struct { + Tag string `json:"tag"` +} + +type VagrantCloudClient struct { + // The http client for communicating + client *http.Client + + // The base URL of the API + BaseURL string + + // Access token + AccessToken string +} + +func (v VagrantCloudClient) New(baseUrl string, token string) *VagrantCloudClient { + c := &VagrantCloudClient{ + client: &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + }, + }, + BaseURL: baseUrl, + AccessToken: token, + } + return c +} + +func decodeBody(resp *http.Response, out interface{}) error { + defer resp.Body.Close() + dec := json.NewDecoder(resp.Body) + return dec.Decode(out) +} + +// encodeBody is used to encode a request body +func encodeBody(obj interface{}) (io.Reader, error) { + buf := bytes.NewBuffer(nil) + enc := json.NewEncoder(buf) + if err := enc.Encode(obj); err != nil { + return nil, err + } + return buf, nil +} + +func (v VagrantCloudClient) Box(tag string) (*Box, error) { + resp, err := v.Get(tag) + + if err != nil { + return nil, fmt.Errorf("Error retrieving box: %s", err) + } + + box := &Box{} + + if err = decodeBody(resp, box); err != nil { + return nil, fmt.Errorf("Error parsing box response: %s", err) + } + + return box, nil +} + +func (v VagrantCloudClient) Get(path string) (*http.Response, error) { + params := url.Values{} + params.Set("access_token", v.AccessToken) + reqUrl := fmt.Sprintf("%s/%s%s", v.BaseURL, path, params.Encode()) + + // Scrub API key for logs + scrubbedUrl := strings.Replace(reqUrl, v.AccessToken, "ACCESS_TOKEN", -1) + log.Printf("Post-Processor Vagrant Cloud API GET: %s", scrubbedUrl) + + req, err := http.NewRequest("GET", reqUrl, nil) + resp, err := v.client.Do(req) + + log.Printf("Post-Processor Vagrant Cloud API Response: \n\n%s", resp) + + return resp, err +} + +func (v VagrantCloudClient) Post(path string, body map[string]interface{}) (map[string]interface{}, error) { + + // Scrub API key for logs + scrubbedUrl := strings.Replace(path, v.AccessToken, "ACCESS_TOKEN", -1) + log.Printf("Post-Processor Vagrant Cloud API POST: %s. \n\n Body: %s", scrubbedUrl, body) + return nil, nil +} diff --git a/post-processor/vagrant-cloud/artifact.go b/post-processor/vagrant-cloud/artifact.go new file mode 100644 index 000000000..cebef8696 --- /dev/null +++ b/post-processor/vagrant-cloud/artifact.go @@ -0,0 +1,40 @@ +package vagrantcloud + +import ( + "fmt" + "os" +) + +const BuilderId = "pearkes.post-processor.vagrant-cloud" + +type Artifact struct { + Path string + Provider string +} + +func NewArtifact(provider, path string) *Artifact { + return &Artifact{ + Path: path, + Provider: provider, + } +} + +func (*Artifact) BuilderId() string { + return BuilderId +} + +func (a *Artifact) Files() []string { + return []string{a.Path} +} + +func (a *Artifact) Id() string { + return "" +} + +func (a *Artifact) String() string { + return fmt.Sprintf("'%s' provider box: %s", a.Provider, a.Path) +} + +func (a *Artifact) Destroy() error { + return os.Remove(a.Path) +} diff --git a/post-processor/vagrant-cloud/artifact_test.go b/post-processor/vagrant-cloud/artifact_test.go new file mode 100644 index 000000000..b95e04511 --- /dev/null +++ b/post-processor/vagrant-cloud/artifact_test.go @@ -0,0 +1,14 @@ +package vagrantcloud + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestArtifact_ImplementsArtifact(t *testing.T) { + var raw interface{} + raw = &Artifact{} + if _, ok := raw.(packer.Artifact); !ok { + t.Fatalf("Artifact should be a Artifact") + } +} diff --git a/post-processor/vagrant-cloud/post-processor.go b/post-processor/vagrant-cloud/post-processor.go new file mode 100644 index 000000000..b62542c6d --- /dev/null +++ b/post-processor/vagrant-cloud/post-processor.go @@ -0,0 +1,142 @@ +// vagrant_cloud implements the packer.PostProcessor interface and adds a +// post-processor that uploads artifacts from the vagrant post-processor +// to Vagrant Cloud (vagrantcloud.com) +package vagrantcloud + +import ( + "fmt" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" +) + +const VAGRANT_CLOUD_URL = "https://vagrantcloud.com" + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + Tag string `mapstructure:"box_tag"` + Version string `mapstructure:"version"` + + AccessToken string `mapstructure:"access_token"` + VagrantCloudUrl string `mapstructure:"vagrant_cloud_url"` + + tpl *packer.ConfigTemplate +} + +type PostProcessor struct { + config Config + client *VagrantCloudClient +} + +func (p *PostProcessor) Configure(raws ...interface{}) error { + _, 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 + + // Default configuration + if p.config.VagrantCloudUrl == "" { + p.config.VagrantCloudUrl = VAGRANT_CLOUD_URL + } + + // Accumulate any errors + errs := new(packer.MultiError) + + // required configuration + templates := map[string]*string{ + "box_tag": &p.config.Tag, + "version": &p.config.Version, + "access_token": &p.config.AccessToken, + } + + for key, ptr := range templates { + if *ptr == "" { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("%s must be set", key)) + } + } + + // Template process + for key, ptr := range templates { + *ptr, err = p.config.tpl.Process(*ptr, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing %s: %s", key, err)) + } + } + + if len(errs.Errors) > 0 { + return errs + } + + return nil +} + +func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { + config := p.config + + // Only accepts input from the vagrant post-processor + if artifact.BuilderId() != "mitchellh.post-processor.vagrant" { + return nil, false, fmt.Errorf( + "Unknown artifact type, requires box from vagrant post-processor: %s", artifact.BuilderId()) + } + + // The name of the provider for vagrant cloud, and vagrant + provider := providerFromBuilderName(artifact.Id()) + version := p.config.Version + tag := p.config.Tag + + // create the HTTP client + p.client = VagrantCloudClient{}.New(p.config.VagrantCloudUrl, p.config.AccessToken) + + ui.Say(fmt.Sprintf("Verifying box is accessible: %s", tag)) + + box, err := p.client.Box(tag) + + if err != nil { + return nil, false, err + } + + if box.Tag != tag { + ui.Say(fmt.Sprintf("Could not verify box is correct: %s", tag)) + return nil, false, err + } + + ui.Say(fmt.Sprintf("Creating Version %s", version)) + ui.Say(fmt.Sprintf("Creating Provider %s", version)) + ui.Say(fmt.Sprintf("Uploading Box %s", version)) + ui.Say(fmt.Sprintf("Verifying upload %s", version)) + ui.Say(fmt.Sprintf("Releasing version %s", version)) + + return NewArtifact(provider, config.Tag), true, nil +} + +// Runs a cleanup if the post processor fails to upload +func (p *PostProcessor) Cleanup() { + // Delete the version +} + +// converts a packer builder name to the corresponding vagrant +// provider +func providerFromBuilderName(name string) string { + switch name { + case "aws": + return "aws" + case "digitalocean": + return "digitalocean" + case "virtualbox": + return "virtualbox" + case "vmware": + return "vmware_desktop" + case "parallels": + return "parallels" + default: + return name + } +} diff --git a/post-processor/vagrant-cloud/post-processor_test.go b/post-processor/vagrant-cloud/post-processor_test.go new file mode 100644 index 000000000..1a302fc4c --- /dev/null +++ b/post-processor/vagrant-cloud/post-processor_test.go @@ -0,0 +1,41 @@ +package vagrantcloud + +import ( + "bytes" + "github.com/mitchellh/packer/packer" + "testing" +) + +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 _ packer.PostProcessor = new(PostProcessor) +} + +func TestproviderFromBuilderName(t *testing.T) { + if providerFromBuilderName("foobar") != "foobar" { + t.Fatal("should copy unknown provider") + } + + if providerFromBuilderName("vmware") != "vmware_desktop" { + t.Fatal("should convert provider") + } +} diff --git a/post-processor/vagrant/artifact.go b/post-processor/vagrant/artifact.go index 1b4885b52..c4cfe394b 100644 --- a/post-processor/vagrant/artifact.go +++ b/post-processor/vagrant/artifact.go @@ -28,7 +28,7 @@ func (a *Artifact) Files() []string { } func (a *Artifact) Id() string { - return "" + return a.Provider } func (a *Artifact) String() string { diff --git a/post-processor/vagrant/artifact_test.go b/post-processor/vagrant/artifact_test.go index 5c711dad2..6e16285a2 100644 --- a/post-processor/vagrant/artifact_test.go +++ b/post-processor/vagrant/artifact_test.go @@ -12,3 +12,10 @@ func TestArtifact_ImplementsArtifact(t *testing.T) { t.Fatalf("Artifact should be a Artifact") } } + +func TestArtifact_Id(t *testing.T) { + artifact := NewArtifact("vmware", "./") + if artifact.Id() != "vmware" { + t.Fatalf("should return name as Id") + } +}