mirror of
https://github.com/hashicorp/packer.git
synced 2026-09-21 15:31:41 -04:00
Vagrant Cloud provides support for uploading directly to the backend storage instead of streaming through Vagrant Cloud. This adds support for direct to storage uploads and sets it as the default upload method. A new option has been added to disable this behavior and revert back to streaming upload via Vagrant Cloud (`no_direct_upload`). This default for uploading directly to the backend storage also matches up with changes being added to Vagrant proper for box upload behavior: hashicorp/vagrant#11916
298 lines
8.9 KiB
Go
298 lines
8.9 KiB
Go
//go:generate mapstructure-to-hcl2 -type Config
|
|
|
|
// vagrant_cloud implements the packer.PostProcessor interface and adds a
|
|
// post-processor that uploads artifacts from the vagrant post-processor
|
|
// and vagrant builder to Vagrant Cloud (vagrantcloud.com) or manages
|
|
// self hosted boxes on the Vagrant Cloud
|
|
package vagrantcloud
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2/hcldec"
|
|
"github.com/hashicorp/packer/common"
|
|
"github.com/hashicorp/packer/helper/config"
|
|
"github.com/hashicorp/packer/helper/multistep"
|
|
"github.com/hashicorp/packer/packer"
|
|
"github.com/hashicorp/packer/template/interpolate"
|
|
)
|
|
|
|
var builtins = map[string]string{
|
|
"mitchellh.post-processor.vagrant": "vagrant",
|
|
"packer.post-processor.artifice": "artifice",
|
|
"vagrant": "vagrant",
|
|
}
|
|
|
|
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"`
|
|
InsecureSkipTLSVerify bool `mapstructure:"insecure_skip_tls_verify"`
|
|
BoxDownloadUrl string `mapstructure:"box_download_url"`
|
|
NoDirectUpload bool `mapstructure:"no_direct_upload"`
|
|
|
|
ctx interpolate.Context
|
|
}
|
|
|
|
type PostProcessor struct {
|
|
config Config
|
|
client *VagrantCloudClient
|
|
runner multistep.Runner
|
|
warnAtlasToken bool
|
|
insecureSkipTLSVerify bool
|
|
}
|
|
|
|
func (p *PostProcessor) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
|
|
|
|
func (p *PostProcessor) Configure(raws ...interface{}) error {
|
|
err := config.Decode(&p.config, &config.DecodeOpts{
|
|
PluginType: BuilderId,
|
|
Interpolate: true,
|
|
InterpolateContext: &p.config.ctx,
|
|
InterpolateFilter: &interpolate.RenderFilter{
|
|
Exclude: []string{
|
|
"box_download_url",
|
|
},
|
|
},
|
|
}, raws...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Default configuration
|
|
if p.config.VagrantCloudUrl == "" {
|
|
p.config.VagrantCloudUrl = VAGRANT_CLOUD_URL
|
|
}
|
|
|
|
p.insecureSkipTLSVerify = p.config.InsecureSkipTLSVerify == true && p.config.VagrantCloudUrl != VAGRANT_CLOUD_URL
|
|
|
|
if p.config.AccessToken == "" {
|
|
envToken := os.Getenv("VAGRANT_CLOUD_TOKEN")
|
|
if envToken == "" {
|
|
envToken = os.Getenv("ATLAS_TOKEN")
|
|
if envToken != "" {
|
|
p.warnAtlasToken = true
|
|
}
|
|
}
|
|
p.config.AccessToken = envToken
|
|
}
|
|
|
|
// Accumulate any errors
|
|
errs := new(packer.MultiError)
|
|
|
|
// Required configuration
|
|
templates := map[string]*string{
|
|
"box_tag": &p.config.Tag,
|
|
"version": &p.config.Version,
|
|
}
|
|
|
|
for key, ptr := range templates {
|
|
if *ptr == "" {
|
|
errs = packer.MultiErrorAppend(
|
|
errs, fmt.Errorf("%s must be set", key))
|
|
}
|
|
}
|
|
|
|
if p.config.VagrantCloudUrl == VAGRANT_CLOUD_URL && p.config.AccessToken == "" {
|
|
errs = packer.MultiErrorAppend(errs, fmt.Errorf("access_token must be set if vagrant_cloud_url has not been overriden"))
|
|
}
|
|
|
|
// Create the HTTP client
|
|
p.client, err = VagrantCloudClient{}.New(p.config.VagrantCloudUrl, p.config.AccessToken, p.insecureSkipTLSVerify)
|
|
if err != nil {
|
|
errs = packer.MultiErrorAppend(
|
|
errs, fmt.Errorf("Failed to verify authentication token: %v", err))
|
|
}
|
|
|
|
if len(errs.Errors) > 0 {
|
|
return errs
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, bool, error) {
|
|
if _, ok := builtins[artifact.BuilderId()]; !ok {
|
|
return nil, false, false, fmt.Errorf(
|
|
"Unknown artifact type: this post-processor requires an input artifact from the artifice post-processor, vagrant post-processor, or vagrant builder: %s", artifact.BuilderId())
|
|
}
|
|
|
|
// We assume that there is only one .box file to upload
|
|
if !strings.HasSuffix(artifact.Files()[0], ".box") {
|
|
return nil, false, false, fmt.Errorf(
|
|
"Unknown files in artifact, Vagrant box with .box suffix is required as first artifact file: %s", artifact.Files())
|
|
}
|
|
|
|
if p.warnAtlasToken {
|
|
ui.Message("Warning: Using Vagrant Cloud token found in ATLAS_TOKEN. Please make sure it is correct, or set VAGRANT_CLOUD_TOKEN")
|
|
}
|
|
|
|
// Determine the name of the provider for Vagrant Cloud, and Vagrant
|
|
providerName, err := getProvider(artifact.Id(), artifact.Files()[0], builtins[artifact.BuilderId()])
|
|
if err != nil {
|
|
return nil, false, false, fmt.Errorf("error getting provider name: %s", err)
|
|
}
|
|
|
|
var generatedData map[interface{}]interface{}
|
|
stateData := artifact.State("generated_data")
|
|
if stateData != nil {
|
|
// Make sure it's not a nil map so we can assign to it later.
|
|
generatedData = stateData.(map[interface{}]interface{})
|
|
}
|
|
// If stateData has a nil map generatedData will be nil
|
|
// and we need to make sure it's not
|
|
if generatedData == nil {
|
|
generatedData = make(map[interface{}]interface{})
|
|
}
|
|
generatedData["ArtifactId"] = artifact.Id()
|
|
generatedData["Provider"] = providerName
|
|
p.config.ctx.Data = generatedData
|
|
|
|
boxDownloadUrl, err := interpolate.Render(p.config.BoxDownloadUrl, &p.config.ctx)
|
|
if err != nil {
|
|
return nil, false, false, fmt.Errorf("Error processing box_download_url: %s", err)
|
|
}
|
|
|
|
// 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)
|
|
state.Put("boxDownloadUrl", boxDownloadUrl)
|
|
|
|
// Build the steps
|
|
steps := []multistep.Step{
|
|
new(stepVerifyBox),
|
|
new(stepCreateVersion),
|
|
new(stepCreateProvider),
|
|
}
|
|
if p.config.BoxDownloadUrl == "" {
|
|
steps = append(steps, new(stepPrepareUpload), new(stepUpload))
|
|
if !p.config.NoDirectUpload {
|
|
steps = append(steps, new(stepConfirmUpload))
|
|
}
|
|
}
|
|
steps = append(steps, new(stepReleaseVersion))
|
|
|
|
// Run the steps
|
|
p.runner = common.NewRunner(steps, p.config.PackerConfig, ui)
|
|
p.runner.Run(ctx, state)
|
|
|
|
// If there was an error, return that
|
|
if rawErr, ok := state.GetOk("error"); ok {
|
|
return nil, false, false, rawErr.(error)
|
|
}
|
|
|
|
return NewArtifact(providerName, p.config.Tag), true, false, nil
|
|
}
|
|
|
|
func getProvider(builderName, boxfile, builderId string) (providerName string, err error) {
|
|
if builderId == "artifice" {
|
|
// The artifice post processor cannot embed any data in the
|
|
// supplied artifact so the provider information must be extracted
|
|
// from the box file directly
|
|
providerName, err = providerFromVagrantBox(boxfile)
|
|
} else {
|
|
// For the Vagrant builder and Vagrant post processor the provider can
|
|
// be determined from information embedded in the artifact
|
|
providerName = providerFromBuilderName(builderName)
|
|
}
|
|
return providerName, err
|
|
}
|
|
|
|
// Converts a packer builder name to the corresponding vagrant provider
|
|
func providerFromBuilderName(name string) string {
|
|
switch name {
|
|
case "aws":
|
|
return "aws"
|
|
case "scaleway":
|
|
return "scaleway"
|
|
case "digitalocean":
|
|
return "digitalocean"
|
|
case "virtualbox":
|
|
return "virtualbox"
|
|
case "vmware":
|
|
return "vmware_desktop"
|
|
case "parallels":
|
|
return "parallels"
|
|
default:
|
|
return name
|
|
}
|
|
}
|
|
|
|
// Returns the Vagrant provider the box is intended for use with by
|
|
// reading the metadata file packaged inside the box
|
|
func providerFromVagrantBox(boxfile string) (providerName string, err error) {
|
|
log.Println("Attempting to determine provider from metadata in box file. This may take some time...")
|
|
|
|
f, err := os.Open(boxfile)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error attempting to open box file: %s", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
// Vagrant boxes are gzipped tar archives
|
|
ar, err := gzip.NewReader(f)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error unzipping box archive: %s", err)
|
|
}
|
|
tr := tar.NewReader(ar)
|
|
|
|
// The metadata.json file in the tar archive contains a 'provider' key
|
|
type metadata struct {
|
|
ProviderName string `json:"provider"`
|
|
}
|
|
md := metadata{}
|
|
|
|
// Loop through the files in the archive and read the provider
|
|
// information from the boxes metadata.json file
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
if md.ProviderName == "" {
|
|
return "", fmt.Errorf("Error: Provider info was not found in box: %s", boxfile)
|
|
}
|
|
break
|
|
}
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error reading header info from box tar archive: %s", err)
|
|
}
|
|
|
|
if hdr.Name == "metadata.json" {
|
|
contents, err := ioutil.ReadAll(tr)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error reading contents of metadata.json file from box file: %s", err)
|
|
}
|
|
err = json.Unmarshal(contents, &md)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error parsing metadata.json file: %s", err)
|
|
}
|
|
if md.ProviderName == "" {
|
|
return "", fmt.Errorf("Error: Could not determine Vagrant provider from box metadata.json file")
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return md.ProviderName, nil
|
|
}
|