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/artifact.go b/post-processor/vagrant-cloud/artifact.go new file mode 100644 index 000000000..775bc2998 --- /dev/null +++ b/post-processor/vagrant-cloud/artifact.go @@ -0,0 +1,39 @@ +package vagrantcloud + +import ( + "fmt" +) + +const BuilderId = "pearkes.post-processor.vagrant-cloud" + +type Artifact struct { + Tag string + Provider string +} + +func NewArtifact(provider, tag string) *Artifact { + return &Artifact{ + Tag: tag, + Provider: provider, + } +} + +func (*Artifact) BuilderId() string { + return BuilderId +} + +func (a *Artifact) Files() []string { + return nil +} + +func (a *Artifact) Id() string { + return "" +} + +func (a *Artifact) String() string { + return fmt.Sprintf("'%s': %s", a.Provider, a.Tag) +} + +func (a *Artifact) Destroy() error { + return nil +} 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/client.go b/post-processor/vagrant-cloud/client.go new file mode 100644 index 000000000..783745c67 --- /dev/null +++ b/post-processor/vagrant-cloud/client.go @@ -0,0 +1,185 @@ +package vagrantcloud + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "mime/multipart" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" +) + +type VagrantCloudClient struct { + // The http client for communicating + client *http.Client + + // The base URL of the API + BaseURL string + + // Access token + AccessToken string +} + +type VagrantCloudErrors struct { + Errors map[string][]string `json:"errors"` +} + +func (v VagrantCloudErrors) FormatErrors() string { + errs := make([]string, 0) + for e := range v.Errors { + msg := fmt.Sprintf("%s %s", e, strings.Join(v.Errors[e], ",")) + errs = append(errs, msg) + } + return strings.Join(errs, ". ") +} + +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) 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) + req.Header.Add("Content-Type", "application/json") + resp, err := v.client.Do(req) + + log.Printf("Post-Processor Vagrant Cloud API Response: \n\n%s", resp) + + return resp, err +} + +func (v VagrantCloudClient) Delete(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 DELETE: %s", scrubbedUrl) + + req, err := http.NewRequest("DELETE", reqUrl, nil) + req.Header.Add("Content-Type", "application/json") + resp, err := v.client.Do(req) + + log.Printf("Post-Processor Vagrant Cloud API Response: \n\n%s", resp) + + return resp, err +} + +func (v VagrantCloudClient) Upload(path string, url string) (*http.Response, error) { + file, err := os.Open(path) + + if err != nil { + return nil, fmt.Errorf("Error opening file for upload: %s", err) + } + + defer file.Close() + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", filepath.Base(path)) + if err != nil { + return nil, err + } + + _, err = io.Copy(part, file) + + if err != nil { + return nil, fmt.Errorf("Error uploading file: %s", err) + } + + request, err := http.NewRequest("PUT", url, body) + + if err != nil { + return nil, fmt.Errorf("Error preparing upload request: %s", err) + } + + log.Printf("Post-Processor Vagrant Cloud API Upload: %s %s", path, url) + + resp, err := v.client.Do(request) + + log.Printf("Post-Processor Vagrant Cloud Upload Response: \n\n%s", resp) + + return resp, err +} + +func (v VagrantCloudClient) Post(path string, body interface{}) (*http.Response, error) { + params := url.Values{} + params.Set("access_token", v.AccessToken) + reqUrl := fmt.Sprintf("%s/%s?%s", v.BaseURL, path, params.Encode()) + + encBody, err := encodeBody(body) + + if err != nil { + return nil, fmt.Errorf("Error encoding body for request: %s", err) + } + + // Scrub API key for logs + scrubbedUrl := strings.Replace(reqUrl, v.AccessToken, "ACCESS_TOKEN", -1) + log.Printf("Post-Processor Vagrant Cloud API POST: %s. \n\n Body: %s", scrubbedUrl, encBody) + + req, err := http.NewRequest("POST", reqUrl, encBody) + req.Header.Add("Content-Type", "application/json") + + resp, err := v.client.Do(req) + + log.Printf("Post-Processor Vagrant Cloud API Response: \n\n%s", resp) + + return resp, err +} + +func (v VagrantCloudClient) Put(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 PUT: %s", scrubbedUrl) + + req, err := http.NewRequest("PUT", reqUrl, nil) + req.Header.Add("Content-Type", "application/json") + + resp, err := v.client.Do(req) + + log.Printf("Post-Processor Vagrant Cloud API Response: \n\n%s", resp) + + return resp, err +} diff --git a/post-processor/vagrant-cloud/post-processor.go b/post-processor/vagrant-cloud/post-processor.go new file mode 100644 index 000000000..8a7f53ebc --- /dev/null +++ b/post-processor/vagrant-cloud/post-processor.go @@ -0,0 +1,171 @@ +// 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/multistep" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "log" + "strings" +) + +const VAGRANT_CLOUD_URL = "https://vagrantcloud.com/api/v1" + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + Tag string `mapstructure:"box_tag"` + Version string `mapstructure:"version"` + VersionDescription string `mapstructure:"version_description"` + NoRelease bool `mapstructure:"no_release"` + + AccessToken string `mapstructure:"access_token"` + VagrantCloudUrl string `mapstructure:"vagrant_cloud_url"` + + tpl *packer.ConfigTemplate +} + +type PostProcessor struct { + config Config + client *VagrantCloudClient + runner multistep.Runner +} + +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) { + // 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()) + } + + // We assume that there is only one .box file to upload + if !strings.HasSuffix(artifact.Files()[0], ".box") { + return nil, false, fmt.Errorf( + "Unknown files in artifact from vagrant post-processor: %s", artifact.Files()) + } + + // create the HTTP client + p.client = VagrantCloudClient{}.New(p.config.VagrantCloudUrl, p.config.AccessToken) + + // The name of the provider for vagrant cloud, and vagrant + providerName := providerFromBuilderName(artifact.Id()) + + // Set up the state + state := new(multistep.BasicStateBag) + state.Put("config", p.config) + state.Put("client", p.client) + state.Put("artifact", artifact) + state.Put("artifactFilePath", artifact.Files()[0]) + state.Put("ui", ui) + state.Put("providerName", providerName) + + // Build the steps + steps := []multistep.Step{ + new(stepVerifyBox), + new(stepCreateVersion), + new(stepCreateProvider), + new(stepPrepareUpload), + new(stepUpload), + new(stepVerifyUpload), + new(stepReleaseVersion), + } + + // Run the steps + if p.config.PackerDebug { + p.runner = &multistep.DebugRunner{ + Steps: steps, + PauseFn: common.MultistepDebugFn(ui), + } + } else { + p.runner = &multistep.BasicRunner{Steps: steps} + } + + p.runner.Run(state) + + // If there was an error, return that + if rawErr, ok := state.GetOk("error"); ok { + return nil, false, rawErr.(error) + } + + return NewArtifact(providerName, p.config.Tag), true, nil +} + +// Runs a cleanup if the post processor fails to upload +func (p *PostProcessor) Cancel() { + if p.runner != nil { + log.Println("Cancelling the step runner...") + p.runner.Cancel() + } +} + +// 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..ff99314c4 --- /dev/null +++ b/post-processor/vagrant-cloud/post-processor_test.go @@ -0,0 +1,59 @@ +package vagrantcloud + +import ( + "bytes" + "github.com/mitchellh/packer/packer" + "testing" +) + +func testGoodConfig() map[string]interface{} { + return map[string]interface{}{ + "access_token": "foo", + "version_description": "bar", + "box_tag": "hashicorp/precise64", + "version": "0.5", + } +} + +func testBadConfig() map[string]interface{} { + return map[string]interface{}{ + "access_token": "foo", + "box_tag": "baz", + "version_description": "bar", + } +} + +func TestPostProcessor_Configure_Good(t *testing.T) { + var p PostProcessor + if err := p.Configure(testGoodConfig()); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestPostProcessor_Configure_Bad(t *testing.T) { + var p PostProcessor + if err := p.Configure(testBadConfig()); err == nil { + t.Fatalf("should have err") + } +} + +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-cloud/step_create_provider.go b/post-processor/vagrant-cloud/step_create_provider.go new file mode 100644 index 000000000..887932c4f --- /dev/null +++ b/post-processor/vagrant-cloud/step_create_provider.go @@ -0,0 +1,91 @@ +package vagrantcloud + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type Provider struct { + Name string `json:"name"` + HostedToken string `json:"hosted_token,omitempty"` + UploadUrl string `json:"upload_url,omitempty"` +} + +type stepCreateProvider struct { + name string // the name of the provider +} + +func (s *stepCreateProvider) Run(state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*VagrantCloudClient) + ui := state.Get("ui").(packer.Ui) + box := state.Get("box").(*Box) + version := state.Get("version").(*Version) + providerName := state.Get("providerName").(string) + + path := fmt.Sprintf("box/%s/version/%v/providers", box.Tag, version.Number) + + provider := &Provider{Name: providerName} + + // Wrap the provider in a provider object for the API + wrapper := make(map[string]interface{}) + wrapper["provider"] = provider + + ui.Say(fmt.Sprintf("Creating provider: %s", providerName)) + + resp, err := client.Post(path, wrapper) + + if err != nil || (resp.StatusCode != 200) { + cloudErrors := &VagrantCloudErrors{} + err = decodeBody(resp, cloudErrors) + state.Put("error", fmt.Errorf("Error creating provider: %s", cloudErrors.FormatErrors())) + return multistep.ActionHalt + } + + if err = decodeBody(resp, provider); err != nil { + state.Put("error", fmt.Errorf("Error parsing provider response: %s", err)) + return multistep.ActionHalt + } + + // Save the name for cleanup + s.name = provider.Name + + state.Put("provider", provider) + + return multistep.ActionContinue +} + +func (s *stepCreateProvider) Cleanup(state multistep.StateBag) { + client := state.Get("client").(*VagrantCloudClient) + ui := state.Get("ui").(packer.Ui) + box := state.Get("box").(*Box) + version := state.Get("version").(*Version) + + // If we didn't save the provider name, it likely doesn't exist + if s.name == "" { + ui.Say("Cleaning up provider") + ui.Message("Provider was not created, not deleting") + return + } + + _, cancelled := state.GetOk(multistep.StateCancelled) + _, halted := state.GetOk(multistep.StateHalted) + + // Return if we didn't cancel or halt, and thus need + // no cleanup + if !cancelled && !halted { + return + } + + ui.Say("Cleaning up provider") + ui.Message(fmt.Sprintf("Deleting provider: %s", s.name)) + + path := fmt.Sprintf("box/%s/version/%v/provider/%s", box.Tag, version.Number, s.name) + + // No need for resp from the cleanup DELETE + _, err := client.Delete(path) + + if err != nil { + ui.Error(fmt.Sprintf("Error destroying provider: %s", err)) + } +} diff --git a/post-processor/vagrant-cloud/step_create_version.go b/post-processor/vagrant-cloud/step_create_version.go new file mode 100644 index 000000000..8108577cc --- /dev/null +++ b/post-processor/vagrant-cloud/step_create_version.go @@ -0,0 +1,97 @@ +package vagrantcloud + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type Version struct { + Version string `json:"version"` + Description string `json:"description,omitempty"` + Number uint `json:"number,omitempty"` +} + +type stepCreateVersion struct { + number uint // number of the version, if needed in cleanup +} + +func (s *stepCreateVersion) Run(state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*VagrantCloudClient) + ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(Config) + box := state.Get("box").(*Box) + + ui.Say(fmt.Sprintf("Creating version: %s", config.Version)) + + if hasVersion, v := box.HasVersion(config.Version); hasVersion { + ui.Message(fmt.Sprintf("Version exists, skipping creation")) + state.Put("version", v) + return multistep.ActionContinue + } + + path := fmt.Sprintf("box/%s/versions", box.Tag) + + version := &Version{Version: config.Version, Description: config.VersionDescription} + + // Wrap the version in a version object for the API + wrapper := make(map[string]interface{}) + wrapper["version"] = version + + resp, err := client.Post(path, wrapper) + + if err != nil || (resp.StatusCode != 200) { + cloudErrors := &VagrantCloudErrors{} + err = decodeBody(resp, cloudErrors) + state.Put("error", fmt.Errorf("Error creating version: %s", cloudErrors.FormatErrors())) + return multistep.ActionHalt + } + + if err = decodeBody(resp, version); err != nil { + state.Put("error", fmt.Errorf("Error parsing version response: %s", err)) + return multistep.ActionHalt + } + + // Save the number for cleanup + s.number = version.Number + + state.Put("version", version) + + return multistep.ActionContinue +} + +func (s *stepCreateVersion) Cleanup(state multistep.StateBag) { + client := state.Get("client").(*VagrantCloudClient) + ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(Config) + box := state.Get("box").(*Box) + + // If we didn't save the version number, it likely doesn't exist or + // already existed + if s.number == 0 { + ui.Message("Version was not created or previously existed, not deleting") + return + } + + _, cancelled := state.GetOk(multistep.StateCancelled) + _, halted := state.GetOk(multistep.StateHalted) + + // Return if we didn't cancel or halt, and thus need + // no cleanup + if !cancelled && !halted { + return + } + + path := fmt.Sprintf("box/%s/version/%v", box.Tag, s.number) + + ui.Say("Cleaning up version") + ui.Message(fmt.Sprintf("Deleting version: %s", config.Version)) + + // No need for resp from the cleanup DELETE + _, err := client.Delete(path) + + if err != nil { + ui.Error(fmt.Sprintf("Error destroying version: %s", err)) + } + +} diff --git a/post-processor/vagrant-cloud/step_prepare_upload.go b/post-processor/vagrant-cloud/step_prepare_upload.go new file mode 100644 index 000000000..5c82d02c3 --- /dev/null +++ b/post-processor/vagrant-cloud/step_prepare_upload.go @@ -0,0 +1,54 @@ +package vagrantcloud + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type Upload struct { + Token string `json:"token"` + UploadPath string `json:"upload_path"` +} + +type stepPrepareUpload struct { +} + +func (s *stepPrepareUpload) Run(state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*VagrantCloudClient) + ui := state.Get("ui").(packer.Ui) + box := state.Get("box").(*Box) + version := state.Get("version").(*Version) + provider := state.Get("provider").(*Provider) + artifactFilePath := state.Get("artifactFilePath").(string) + + path := fmt.Sprintf("box/%s/version/%v/provider/%s/upload", box.Tag, version.Number, provider.Name) + upload := &Upload{} + + ui.Say(fmt.Sprintf("Preparing upload of box: %s", artifactFilePath)) + + resp, err := client.Get(path) + + if err != nil || (resp.StatusCode != 200) { + cloudErrors := &VagrantCloudErrors{} + err = decodeBody(resp, cloudErrors) + state.Put("error", fmt.Errorf("Error preparing upload: %s", cloudErrors.FormatErrors())) + return multistep.ActionHalt + } + + if err = decodeBody(resp, upload); err != nil { + state.Put("error", fmt.Errorf("Error parsing upload response: %s", err)) + return multistep.ActionHalt + } + + ui.Message(fmt.Sprintf("Box upload prepared with token %s", upload.Token)) + + // Save the upload details to the state + state.Put("upload", upload) + + return multistep.ActionContinue +} + +func (s *stepPrepareUpload) Cleanup(state multistep.StateBag) { + // No cleanup +} diff --git a/post-processor/vagrant-cloud/step_release_version.go b/post-processor/vagrant-cloud/step_release_version.go new file mode 100644 index 000000000..43512bdca --- /dev/null +++ b/post-processor/vagrant-cloud/step_release_version.go @@ -0,0 +1,44 @@ +package vagrantcloud + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type stepReleaseVersion struct { +} + +func (s *stepReleaseVersion) Run(state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*VagrantCloudClient) + ui := state.Get("ui").(packer.Ui) + box := state.Get("box").(*Box) + version := state.Get("version").(*Version) + config := state.Get("config").(Config) + + ui.Say(fmt.Sprintf("Releasing version: %s", version.Version)) + + if config.NoRelease { + ui.Message("Not releasing version due to configuration") + return multistep.ActionContinue + } + + path := fmt.Sprintf("box/%s/version/%v/release", box.Tag, version.Number) + + resp, err := client.Put(path) + + if err != nil || (resp.StatusCode != 200) { + cloudErrors := &VagrantCloudErrors{} + err = decodeBody(resp, cloudErrors) + state.Put("error", fmt.Errorf("Error releasing version: %s", cloudErrors.FormatErrors())) + return multistep.ActionHalt + } + + ui.Message(fmt.Sprintf("Version successfully released and available")) + + return multistep.ActionContinue +} + +func (s *stepReleaseVersion) Cleanup(state multistep.StateBag) { + // No cleanup +} diff --git a/post-processor/vagrant-cloud/step_upload.go b/post-processor/vagrant-cloud/step_upload.go new file mode 100644 index 000000000..f82f125f8 --- /dev/null +++ b/post-processor/vagrant-cloud/step_upload.go @@ -0,0 +1,37 @@ +package vagrantcloud + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type stepUpload struct { +} + +func (s *stepUpload) Run(state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*VagrantCloudClient) + ui := state.Get("ui").(packer.Ui) + upload := state.Get("upload").(*Upload) + artifactFilePath := state.Get("artifactFilePath").(string) + url := upload.UploadPath + + ui.Say(fmt.Sprintf("Uploading box: %s", artifactFilePath)) + + ui.Message("Depending on your internet connection and the size of the box, this may take some time") + + resp, err := client.Upload(artifactFilePath, url) + + if err != nil || (resp.StatusCode != 200) { + state.Put("error", fmt.Errorf("Error uploading Box: %s", err)) + return multistep.ActionHalt + } + + ui.Message("Box succesfully uploaded") + + return multistep.ActionContinue +} + +func (s *stepUpload) Cleanup(state multistep.StateBag) { + // No cleanup +} diff --git a/post-processor/vagrant-cloud/step_verify_box.go b/post-processor/vagrant-cloud/step_verify_box.go new file mode 100644 index 000000000..bbbc3b4b7 --- /dev/null +++ b/post-processor/vagrant-cloud/step_verify_box.go @@ -0,0 +1,66 @@ +package vagrantcloud + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type Box struct { + Tag string `json:"tag"` + Versions []*Version `json:"versions"` +} + +func (b *Box) HasVersion(version string) (bool, *Version) { + for _, v := range b.Versions { + if v.Version == version { + return true, v + } + } + return false, nil +} + +type stepVerifyBox struct { +} + +func (s *stepVerifyBox) Run(state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*VagrantCloudClient) + ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(Config) + + ui.Say(fmt.Sprintf("Verifying box is accessible: %s", config.Tag)) + + path := fmt.Sprintf("box/%s", config.Tag) + resp, err := client.Get(path) + + if err != nil || (resp.StatusCode != 200) { + cloudErrors := &VagrantCloudErrors{} + err = decodeBody(resp, cloudErrors) + state.Put("error", fmt.Errorf("Error retrieving box: %s", cloudErrors.FormatErrors())) + return multistep.ActionHalt + } + + box := &Box{} + + if err = decodeBody(resp, box); err != nil { + state.Put("error", fmt.Errorf("Error parsing box response: %s", err)) + return multistep.ActionHalt + } + + if box.Tag != config.Tag { + state.Put("error", fmt.Errorf("Could not verify box: %s", config.Tag)) + return multistep.ActionHalt + } + + ui.Message("Box accessible and matches tag") + + // Keep the box in state for later + state.Put("box", box) + + // Box exists and is accessible + return multistep.ActionContinue +} + +func (s *stepVerifyBox) Cleanup(state multistep.StateBag) { + // no cleanup needed +} diff --git a/post-processor/vagrant-cloud/step_verify_upload.go b/post-processor/vagrant-cloud/step_verify_upload.go new file mode 100644 index 000000000..120bd647d --- /dev/null +++ b/post-processor/vagrant-cloud/step_verify_upload.go @@ -0,0 +1,103 @@ +package vagrantcloud + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "time" +) + +type stepVerifyUpload struct { +} + +func (s *stepVerifyUpload) Run(state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*VagrantCloudClient) + ui := state.Get("ui").(packer.Ui) + box := state.Get("box").(*Box) + version := state.Get("version").(*Version) + upload := state.Get("upload").(*Upload) + provider := state.Get("provider").(*Provider) + + path := fmt.Sprintf("box/%s/version/%v/provider/%s", box.Tag, version.Number, provider.Name) + + providerCheck := &Provider{} + + ui.Say(fmt.Sprintf("Verifying provider upload: %s", provider.Name)) + + done := make(chan struct{}) + defer close(done) + + result := make(chan error, 1) + + go func() { + attempts := 0 + for { + attempts += 1 + + log.Printf("Checking token match for provider.. (attempt: %d)", attempts) + + resp, err := client.Get(path) + + if err != nil || (resp.StatusCode != 200) { + cloudErrors := &VagrantCloudErrors{} + err = decodeBody(resp, cloudErrors) + err = fmt.Errorf("Error retrieving provider: %s", cloudErrors.FormatErrors()) + result <- err + return + } + + if err = decodeBody(resp, providerCheck); err != nil { + err = fmt.Errorf("Error parsing provider response: %s", err) + result <- err + return + } + + if err != nil { + result <- err + return + } + + if upload.Token == providerCheck.HostedToken { + // success! + result <- nil + return + } + + // Wait 3 seconds in between + time.Sleep(3 * time.Second) + + // Verify we shouldn't exit + select { + case <-done: + // We finished, so just exit the goroutine + return + default: + // Keep going + } + } + }() + + ui.Message("Waiting for upload token match") + log.Printf("Waiting for up to 600 seconds for provider hosted token to match %s", upload.Token) + + select { + case err := <-result: + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + ui.Message(fmt.Sprintf("Upload succesfully verified with token %s", providerCheck.HostedToken)) + log.Printf("Box succesfully verified %s == %s", upload.Token, providerCheck.HostedToken) + + return multistep.ActionContinue + case <-time.After(600 * time.Second): + state.Put("error", fmt.Errorf("Timeout while waiting to for upload to verify token '%s'", upload.Token)) + return multistep.ActionHalt + } +} + +func (s *stepVerifyUpload) Cleanup(state multistep.StateBag) { + // No cleanup +} 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") + } +} diff --git a/website/source/docs/post-processors/vagrant-cloud.html.markdown b/website/source/docs/post-processors/vagrant-cloud.html.markdown new file mode 100644 index 000000000..499234262 --- /dev/null +++ b/website/source/docs/post-processors/vagrant-cloud.html.markdown @@ -0,0 +1,104 @@ +--- +layout: "docs" +page_title: "Vagrant Cloud Post-Processor" +--- + +# Vagrant Cloud Post-Processor + +Type: `vagrant-cloud` + +The Vagrant Cloud post-processor recieves a Vagrant box from the `vagrant` +post-processor and pushes it to Vagrant Cloud. [Vagrant Cloud](https://vagrantcloud.com) +hosts and serves boxes to Vagrant, allowing you to version and distribute +boxes to an organization in a simple way. + +You'll need to be familiar with Vagrant Cloud, have an upgraded account +to enable box hosting, and be distributing your box via the [shorthand name](http://docs.vagrantup.com/v2/cli/box.html) +configuration. + +## Workflow + +It's important to understand the workflow that using this post-processor +enforces in order to take full advantage of Vagrant and Vagrant Cloud. + +The use of this processor assume that you currently distribute, or plan +to distrubute, boxes via Vagrant Cloud. It also assumes you create Vagrant +Boxes and deliver them to your team in some fashion. + +Here is an example workflow: + +1. You use Packer to build a Vagrant Box for the `virtualbox` provider +2. The `vagrant-cloud` post-processor is configured to point to the box `hashicorp/foobar` on Vagrant Cloud +via the `box_tag` configuration +2. The post-processor receives the box from the `vagrant` post-processor +3. It then creates the configured version, or verifies the existence of it, on Vagrant Cloud +4. A provider matching the name of the Vagrant provider is then created +5. The box is uploaded to Vagrant Cloud +6. The upload is verified +7. The version is released and available to users of the box + + +## Configuration + +The configuration allows you to specify the target box that you have +access to on Vagrant Cloud, as well as authentication and version information. + +### Required: + +* `access_token` (string) - Your access token for the Vagrant Cloud API. + This can be generated on your [tokens page](https://vagrantcloud.com/account/tokens). + +* `box_tag` (string) - The shorthand tag for your box that maps to + Vagrant Cloud, i.e `hashicorp/precise64` for `vagrantcloud.com/hashicorp/precise64` + +* `version` (string) - The version number, typically incrementing a previous version. + The version string is validated based on [Semantic Versioning](http://semver.org/). The string must match + a pattern that could be semver, and doesn't validate that the version comes after + your previous versions. + + +### Optional: + +* `version_description` (string) - Optionally markdown text used as a full-length + and in-depth description of the version, typically for denoting changes introduced + +* `no_release` (string) - If set to true, does not release the version +on Vagrant Cloud, making it active. You can manually release the version +via the API or Web UI. Defaults to false. + +* `vagrant_cloud_url` (string) - Override the base URL for Vagrant Cloud. This +is useful if you're using Vagrant Private Cloud in your own network. Defaults +to `https://vagrantcloud.com/api/v1` + +## Use with Vagrant Post-Processor + +You'll need to use the Vagrant post-processor before using this post-processor. +An example configuration is below. Note the use of the array specifying +the execution order. + +```json +{ + "variables": { + "version": "", + "cloud_token": "" + }, + "builders": [{ + ... + }], + "post-processors": [ + [{ + "type": "vagrant", + "include": ["image.iso"], + "vagrantfile_template": "vagrantfile.tpl", + "output": "proxycore_{{.Provider}}.box" + }, + { + "type": "vagrant-cloud", + "box_tag": "hashicorp/precise64", + "access_token": "{{user `cloud_token`}}", + "version": "{{user `version`}}" + }] + ] +} + +``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 83897136e..ea395d2f9 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -61,6 +61,7 @@
  • docker-import
  • docker-push
  • Vagrant
  • +
  • Vagrant Cloud
  • vSphere