packer-cn/post-processor/vagrant-cloud/post-processor.go

299 lines
9.1 KiB
Go
Raw Normal View History

//go:generate mapstructure-to-hcl2 -type Config
// vagrant_cloud implements the packersdk.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"
2019-03-22 09:56:02 -04:00
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
2017-06-07 13:40:31 -04:00
"os"
2015-05-27 17:56:22 -04:00
"strings"
build using HCL2 (#8423) This follows #8232 which added the code to generate the code required to parse HCL files for each packer component. All old config files of packer will keep on working the same. Packer takes one argument. When a directory is passed, all files in the folder with a name ending with “.pkr.hcl” or “.pkr.json” will be parsed using the HCL2 format. When a file ending with “.pkr.hcl” or “.pkr.json” is passed it will be parsed using the HCL2 format. For every other case; the old packer style will be used. ## 1. the hcl2template pkg can create a packer.Build from a set of HCL (v2) files I had to make the packer.coreBuild (which is our one and only packer.Build ) a public struct with public fields ## 2. Components interfaces get a new ConfigSpec Method to read a file from an HCL file. This is a breaking change for packer plugins. a packer component can be a: builder/provisioner/post-processor each component interface now gets a `ConfigSpec() hcldec.ObjectSpec` which allows packer to tell what is the layout of the hcl2 config meant to configure that specific component. This ObjectSpec is sent through the wire (RPC) and a cty.Value is now sent through the already existing configuration entrypoints: Provisioner.Prepare(raws ...interface{}) error Builder.Prepare(raws ...interface{}) ([]string, error) PostProcessor.Configure(raws ...interface{}) error close #1768 Example hcl files: ```hcl // file amazon-ebs-kms-key/run.pkr.hcl build { sources = [ "source.amazon-ebs.first", ] provisioner "shell" { inline = [ "sleep 5" ] } post-processor "shell-local" { inline = [ "sleep 5" ] } } // amazon-ebs-kms-key/source.pkr.hcl source "amazon-ebs" "first" { ami_name = "hcl2-test" region = "us-east-1" instance_type = "t2.micro" kms_key_id = "c729958f-c6ba-44cd-ab39-35ab68ce0a6c" encrypt_boot = true source_ami_filter { filters { virtualization-type = "hvm" name = "amzn-ami-hvm-????.??.?.????????-x86_64-gp2" root-device-type = "ebs" } most_recent = true owners = ["amazon"] } launch_block_device_mappings { device_name = "/dev/xvda" volume_size = 20 volume_type = "gp2" delete_on_termination = "true" } launch_block_device_mappings { device_name = "/dev/xvdf" volume_size = 500 volume_type = "gp2" delete_on_termination = true encrypted = true } ami_regions = ["eu-central-1"] run_tags { Name = "packer-solr-something" stack-name = "DevOps Tools" } communicator = "ssh" ssh_pty = true ssh_username = "ec2-user" associate_public_ip_address = true } ```
2019-12-17 05:25:56 -05:00
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer/packer-plugin-sdk/common"
2020-11-17 19:31:03 -05:00
"github.com/hashicorp/packer/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer/packer-plugin-sdk/multistep/commonsteps"
packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/packer-plugin-sdk/template/config"
"github.com/hashicorp/packer/packer-plugin-sdk/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"`
2015-05-27 17:56:22 -04:00
ctx interpolate.Context
}
type PostProcessor struct {
config Config
client *VagrantCloudClient
runner multistep.Runner
warnAtlasToken bool
insecureSkipTLSVerify bool
}
build using HCL2 (#8423) This follows #8232 which added the code to generate the code required to parse HCL files for each packer component. All old config files of packer will keep on working the same. Packer takes one argument. When a directory is passed, all files in the folder with a name ending with “.pkr.hcl” or “.pkr.json” will be parsed using the HCL2 format. When a file ending with “.pkr.hcl” or “.pkr.json” is passed it will be parsed using the HCL2 format. For every other case; the old packer style will be used. ## 1. the hcl2template pkg can create a packer.Build from a set of HCL (v2) files I had to make the packer.coreBuild (which is our one and only packer.Build ) a public struct with public fields ## 2. Components interfaces get a new ConfigSpec Method to read a file from an HCL file. This is a breaking change for packer plugins. a packer component can be a: builder/provisioner/post-processor each component interface now gets a `ConfigSpec() hcldec.ObjectSpec` which allows packer to tell what is the layout of the hcl2 config meant to configure that specific component. This ObjectSpec is sent through the wire (RPC) and a cty.Value is now sent through the already existing configuration entrypoints: Provisioner.Prepare(raws ...interface{}) error Builder.Prepare(raws ...interface{}) ([]string, error) PostProcessor.Configure(raws ...interface{}) error close #1768 Example hcl files: ```hcl // file amazon-ebs-kms-key/run.pkr.hcl build { sources = [ "source.amazon-ebs.first", ] provisioner "shell" { inline = [ "sleep 5" ] } post-processor "shell-local" { inline = [ "sleep 5" ] } } // amazon-ebs-kms-key/source.pkr.hcl source "amazon-ebs" "first" { ami_name = "hcl2-test" region = "us-east-1" instance_type = "t2.micro" kms_key_id = "c729958f-c6ba-44cd-ab39-35ab68ce0a6c" encrypt_boot = true source_ami_filter { filters { virtualization-type = "hvm" name = "amzn-ami-hvm-????.??.?.????????-x86_64-gp2" root-device-type = "ebs" } most_recent = true owners = ["amazon"] } launch_block_device_mappings { device_name = "/dev/xvda" volume_size = 20 volume_type = "gp2" delete_on_termination = "true" } launch_block_device_mappings { device_name = "/dev/xvdf" volume_size = 500 volume_type = "gp2" delete_on_termination = true encrypted = true } ami_regions = ["eu-central-1"] run_tags { Name = "packer-solr-something" stack-name = "DevOps Tools" } communicator = "ssh" ssh_pty = true ssh_username = "ec2-user" associate_public_ip_address = true } ```
2019-12-17 05:25:56 -05:00
func (p *PostProcessor) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
func (p *PostProcessor) Configure(raws ...interface{}) error {
2015-05-27 17:56:22 -04:00
err := config.Decode(&p.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &p.config.ctx,
2015-05-27 17:56:22 -04:00
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
2017-06-07 13:40:31 -04:00
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(packersdk.MultiError)
// Required configuration
templates := map[string]*string{
"box_tag": &p.config.Tag,
"version": &p.config.Version,
}
for key, ptr := range templates {
if *ptr == "" {
errs = packersdk.MultiErrorAppend(
errs, fmt.Errorf("%s must be set", key))
}
}
if p.config.VagrantCloudUrl == VAGRANT_CLOUD_URL && p.config.AccessToken == "" {
errs = packersdk.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 = packersdk.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 packersdk.Ui, artifact packersdk.Artifact) (packersdk.Artifact, bool, bool, error) {
if _, ok := builtins[artifact.BuilderId()]; !ok {
2019-04-02 19:51:58 -04:00
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") {
2019-04-02 19:51:58 -04:00
return nil, false, false, fmt.Errorf(
"Unknown files in artifact, Vagrant box with .box suffix is required as first artifact file: %s", artifact.Files())
}
2017-06-07 13:40:31 -04:00
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{})
2015-05-27 17:56:22 -04:00
}
generatedData["ArtifactId"] = artifact.Id()
generatedData["Provider"] = providerName
p.config.ctx.Data = generatedData
2015-05-27 17:56:22 -04:00
boxDownloadUrl, err := interpolate.Render(p.config.BoxDownloadUrl, &p.config.ctx)
if err != nil {
2019-04-02 19:51:58 -04:00
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 = commonsteps.NewRunner(steps, p.config.PackerConfig, ui)
2019-03-22 09:56:02 -04:00
p.runner.Run(ctx, state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
2019-04-02 19:51:58 -04:00
return nil, false, false, rawErr.(error)
}
2019-04-02 19:51:58 -04:00
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"
2017-04-06 05:19:17 -04:00
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
}