From 26abac69993178ae51926b12dd46fdfa2c377e4d Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Tue, 24 Jun 2014 15:58:45 -0400 Subject: [PATCH] post-processor/vagrant-cloud: steps for create, upload and release --- post-processor/vagrant-cloud/client.go | 63 +++++++++++++- .../vagrant-cloud/post-processor.go | 18 +++- post-processor/vagrant-cloud/provider.go | 44 ---------- .../vagrant-cloud/step_create_provider.go | 71 +++++++++++++++ .../vagrant-cloud/step_create_version.go | 76 +++++++++------- .../vagrant-cloud/step_prepare_upload.go | 37 ++++++++ .../vagrant-cloud/step_release_version.go | 36 ++++++++ post-processor/vagrant-cloud/step_upload.go | 18 ++++ .../vagrant-cloud/step_verify_box.go | 12 ++- .../vagrant-cloud/step_verify_upload.go | 87 ++++++++++++++++++- 10 files changed, 379 insertions(+), 83 deletions(-) delete mode 100644 post-processor/vagrant-cloud/provider.go create mode 100644 post-processor/vagrant-cloud/step_release_version.go diff --git a/post-processor/vagrant-cloud/client.go b/post-processor/vagrant-cloud/client.go index fe47d8471..19b029576 100644 --- a/post-processor/vagrant-cloud/client.go +++ b/post-processor/vagrant-cloud/client.go @@ -6,8 +6,11 @@ import ( "fmt" "io" "log" + "mime/multipart" "net/http" "net/url" + "os" + "path/filepath" "strings" ) @@ -74,6 +77,7 @@ func (v VagrantCloudClient) Get(path string) (*http.Response, error) { 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) @@ -91,6 +95,7 @@ func (v VagrantCloudClient) Delete(path string) (*http.Response, error) { 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) @@ -98,13 +103,48 @@ func (v VagrantCloudClient) Delete(path string) (*http.Response, error) { 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()) - log.Println(reqUrl) - encBody, err := encodeBody(body) log.Println(encBody) @@ -126,3 +166,22 @@ func (v VagrantCloudClient) Post(path string, body interface{}) (*http.Response, 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 index 9e1e261ca..a655819f0 100644 --- a/post-processor/vagrant-cloud/post-processor.go +++ b/post-processor/vagrant-cloud/post-processor.go @@ -9,6 +9,7 @@ import ( "github.com/mitchellh/packer/common" "github.com/mitchellh/packer/packer" "log" + "strings" ) const VAGRANT_CLOUD_URL = "https://vagrantcloud.com/api/v1" @@ -88,15 +89,26 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac "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{ @@ -106,6 +118,7 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac new(stepPrepareUpload), new(stepUpload), new(stepVerifyUpload), + new(stepReleaseVersion), } // Run the steps @@ -125,10 +138,7 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac return nil, false, rawErr.(error) } - // // The name of the provider for vagrant cloud, and vagrant - provider := providerFromBuilderName(artifact.Id()) - - return NewArtifact(provider, p.config.Tag), true, nil + return NewArtifact(providerName, p.config.Tag), true, nil } // Runs a cleanup if the post processor fails to upload diff --git a/post-processor/vagrant-cloud/provider.go b/post-processor/vagrant-cloud/provider.go deleted file mode 100644 index d8c3d201e..000000000 --- a/post-processor/vagrant-cloud/provider.go +++ /dev/null @@ -1,44 +0,0 @@ -package vagrantcloud - -import ( - "fmt" -) - -type Provider struct { - client *VagrantCloudClient - Name string `json:"name"` -} - -// https://vagrantcloud.com/docs/providers -func (v VagrantCloudClient) Provider(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 -} - -// Save persist the box over HTTP to Vagrant Cloud -func (p Provider) Save(name string) (bool, error) { - resp, err := p.client.Get(name) - - if err != nil { - return false, fmt.Errorf("Error retrieving box: %s", err) - } - - provider := &Provider{} - - if err = decodeBody(resp, provider); err != nil { - return false, fmt.Errorf("Error parsing box response: %s", err) - } - - return true, nil -} diff --git a/post-processor/vagrant-cloud/step_create_provider.go b/post-processor/vagrant-cloud/step_create_provider.go index 7816f5016..e149ddba1 100644 --- a/post-processor/vagrant-cloud/step_create_provider.go +++ b/post-processor/vagrant-cloud/step_create_provider.go @@ -1,15 +1,86 @@ 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) { + // If we didn't save the provider name, it likely doesn't exist + if s.name == "" { + 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 + } + + client := state.Get("client").(*VagrantCloudClient) + ui := state.Get("ui").(packer.Ui) + box := state.Get("box").(*Box) + version := state.Get("version").(*Version) + + 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 index 054baacd0..211604875 100644 --- a/post-processor/vagrant-cloud/step_create_version.go +++ b/post-processor/vagrant-cloud/step_create_version.go @@ -2,17 +2,13 @@ package vagrantcloud import ( "fmt" - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" ) type Version struct { Version string `json:"version"` - Number uint `json:"number"` -} - -type NewVersion struct { - Version string `json:"version"` + Number uint `json:"number,omitempty"` } type stepCreateVersion struct { @@ -25,53 +21,71 @@ func (s *stepCreateVersion) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(Config) box := state.Get("box").(*Box) - path := fmt.Sprintf("box/%s/versions", box.Tag) + if hasVersion, v := box.HasVersion(config.Version); hasVersion { + ui.Say(fmt.Sprintf("Version exists: %s", config.Version)) + state.Put("version", v) + return multistep.ActionContinue + } + + path := fmt.Sprintf("box/%s/versions", box.Tag) + + version := &Version{Version: config.Version} // Wrap the version in a version object for the API wrapper := make(map[string]interface{}) - wrapper["version"] = NewVersion{Version: config.Version} + wrapper["version"] = version - ui.Say(fmt.Sprintf("Creating version: %s", config.Version)) + ui.Say(fmt.Sprintf("Creating version: %s", config.Version)) resp, err := client.Post(path, wrapper) - version := &Version{} if err != nil || (resp.StatusCode != 200) { - cloudErrors := &VagrantCloudErrors{}; - err = decodeBody(resp, cloudErrors); - state.Put("error", fmt.Errorf("Error creating version: %s", cloudErrors.FormatErrors())) + 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 + return multistep.ActionHalt } - // Save the number for cleanup - s.number = version.Number + // Save the number for cleanup + s.number = version.Number - state.Put("version", version) + state.Put("version", version) return multistep.ActionContinue } func (s *stepCreateVersion) Cleanup(state multistep.StateBag) { - // If we didn't save the version number, it likely doesn't exist - if (s.number == 0) { - return - } + // If we didn't save the version number, it likely doesn't exist or + // already existed + if s.number == 0 { + return + } - client := state.Get("client").(*VagrantCloudClient) - ui := state.Get("ui").(packer.Ui) - box := state.Get("box").(*Box) + _, cancelled := state.GetOk(multistep.StateCancelled) + _, halted := state.GetOk(multistep.StateHalted) - path := fmt.Sprintf("box/%s/version/%s", box.Tag, s.number) + // Return if we didn't cancel or halt, and thus need + // no cleanup + if !cancelled && !halted { + return + } - // No need for resp from the cleanup DELETE - _, err := client.Delete(path) + client := state.Get("client").(*VagrantCloudClient) + ui := state.Get("ui").(packer.Ui) + box := state.Get("box").(*Box) + + path := fmt.Sprintf("box/%s/version/%v", box.Tag, s.number) + + // No need for resp from the cleanup DELETE + _, err := client.Delete(path) + + if err != nil { + ui.Error(fmt.Sprintf("Error destroying version: %s", err)) + } - 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 index 1b3d61ac3..5c7fee2b9 100644 --- a/post-processor/vagrant-cloud/step_prepare_upload.go +++ b/post-processor/vagrant-cloud/step_prepare_upload.go @@ -1,15 +1,52 @@ 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 + } + + // 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..0065c6100 --- /dev/null +++ b/post-processor/vagrant-cloud/step_release_version.go @@ -0,0 +1,36 @@ +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) + + path := fmt.Sprintf("box/%s/version/%v/release", box.Tag, version.Number) + + ui.Say(fmt.Sprintf("Releasing version: %s", version.Version)) + + 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 + } + + 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 index 5f3b27a55..4fb43028c 100644 --- a/post-processor/vagrant-cloud/step_upload.go +++ b/post-processor/vagrant-cloud/step_upload.go @@ -1,15 +1,33 @@ 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)) + + resp, err := client.Upload(artifactFilePath, url) + + if err != nil || (resp.StatusCode != 200) { + state.Put("error", fmt.Errorf("Error uploading Box: %s", resp.Body)) + return multistep.ActionHalt + } + 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 index 784f7f7e7..95aaa0c0f 100644 --- a/post-processor/vagrant-cloud/step_verify_box.go +++ b/post-processor/vagrant-cloud/step_verify_box.go @@ -7,7 +7,17 @@ import ( ) type Box struct { - Tag string `json:"tag"` + 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 { diff --git a/post-processor/vagrant-cloud/step_verify_upload.go b/post-processor/vagrant-cloud/step_verify_upload.go index d665589ce..eb787886f 100644 --- a/post-processor/vagrant-cloud/step_verify_upload.go +++ b/post-processor/vagrant-cloud/step_verify_upload.go @@ -1,15 +1,100 @@ 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 { - return multistep.ActionContinue + 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 + } + } + }() + + 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 + } + + 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 }