mirror of https://github.com/hashicorp/packer
Merge pull request #1289 from mitchellh/vagrant-cloud-post-processor
Vagrant Cloud Post-Processorpull/1329/head
commit
3bdd9ccb86
@ -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()
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
package main
|
||||
@ -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
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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`}}"
|
||||
}]
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
Loading…
Reference in new issue