packer-cn/post-processor/vagrant-cloud/post-processor_test.go
Chris Roberts d8222b1656
Add support for uploading directly to storage on Vagrant Cloud (#10193)
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
2020-11-05 20:01:55 -05:00

700 lines
20 KiB
Go

package vagrantcloud
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/hashicorp/packer/packer"
"github.com/stretchr/testify/assert"
)
type stubResponse struct {
Path string
Method string
Response string
StatusCode int
}
type tarFiles []struct {
Name, Body string
}
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 testNoAccessTokenProvidedConfig() map[string]interface{} {
return map[string]interface{}{
"box_tag": "baz",
"version_description": "bar",
"version": "0.5",
}
}
func newStackServer(stack []stubResponse) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if len(stack) < 1 {
rw.Header().Add("Error", fmt.Sprintf("Request stack is empty - Method: %s Path: %s", req.Method, req.URL.Path))
http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
match := stack[0]
stack = stack[1:]
if match.Method != "" && req.Method != match.Method {
rw.Header().Add("Error", fmt.Sprintf("Request %s != %s", match.Method, req.Method))
http.Error(rw, fmt.Sprintf("Request %s != %s", match.Method, req.Method), http.StatusInternalServerError)
return
}
if match.Path != "" && match.Path != req.URL.Path {
rw.Header().Add("Error", fmt.Sprintf("Request %s != %s", match.Path, req.URL.Path))
http.Error(rw, fmt.Sprintf("Request %s != %s", match.Path, req.URL.Path), http.StatusInternalServerError)
return
}
rw.Header().Add("Complete", fmt.Sprintf("Method: %s Path: %s", match.Method, match.Path))
rw.WriteHeader(match.StatusCode)
if match.Response != "" {
_, err := rw.Write([]byte(match.Response))
if err != nil {
panic("failed to write response: " + err.Error())
}
}
}))
}
func newSecureServer(token string, handler http.HandlerFunc) *httptest.Server {
token = fmt.Sprintf("Bearer %s", token)
return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Header.Get("authorization") != token {
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
if handler != nil {
handler(rw, req)
}
}))
}
func newSelfSignedSslServer(token string, handler http.HandlerFunc) *httptest.Server {
token = fmt.Sprintf("Bearer %s", token)
return httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Header.Get("authorization") != token {
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
if handler != nil {
handler(rw, req)
}
}))
}
func newNoAuthServer(handler http.HandlerFunc) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Header.Get("authorization") != "" {
http.Error(rw, "Authorization header was provider", http.StatusBadRequest)
return
}
if handler != nil {
handler(rw, req)
}
}))
}
func TestPostProcessor_Insecure_Ssl(t *testing.T) {
var p PostProcessor
server := newSelfSignedSslServer("foo", nil)
defer server.Close()
config := testGoodConfig()
config["vagrant_cloud_url"] = server.URL
config["insecure_skip_tls_verify"] = true
if err := p.Configure(config); err != nil {
t.Fatalf("Expected TLS to skip certificate validation: %s", err)
}
}
func TestPostProcessor_Configure_fromVagrantEnv(t *testing.T) {
var p PostProcessor
config := testGoodConfig()
server := newSecureServer("bar", nil)
defer server.Close()
config["vagrant_cloud_url"] = server.URL
config["access_token"] = ""
os.Setenv("VAGRANT_CLOUD_TOKEN", "bar")
defer func() {
os.Setenv("VAGRANT_CLOUD_TOKEN", "")
}()
if err := p.Configure(config); err != nil {
t.Fatalf("err: %s", err)
}
if p.config.AccessToken != "bar" {
t.Fatalf("Expected to get token from VAGRANT_CLOUD_TOKEN env var. Got '%s' instead",
p.config.AccessToken)
}
}
func TestPostProcessor_Configure_fromAtlasEnv(t *testing.T) {
var p PostProcessor
config := testGoodConfig()
config["access_token"] = ""
server := newSecureServer("foo", nil)
defer server.Close()
config["vagrant_cloud_url"] = server.URL
os.Setenv("ATLAS_TOKEN", "foo")
defer func() {
os.Setenv("ATLAS_TOKEN", "")
}()
if err := p.Configure(config); err != nil {
t.Fatalf("err: %s", err)
}
if p.config.AccessToken != "foo" {
t.Fatalf("Expected to get token from ATLAS_TOKEN env var. Got '%s' instead",
p.config.AccessToken)
}
if !p.warnAtlasToken {
t.Fatal("Expected warn flag to be set when getting token from atlas env var.")
}
}
func TestPostProcessor_Configure_Good(t *testing.T) {
config := testGoodConfig()
server := newSecureServer("foo", nil)
defer server.Close()
config["vagrant_cloud_url"] = server.URL
var p PostProcessor
if err := p.Configure(config); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestPostProcessor_Configure_Bad(t *testing.T) {
config := testBadConfig()
server := newSecureServer("foo", nil)
defer server.Close()
config["vagrant_cloud_url"] = server.URL
var p PostProcessor
if err := p.Configure(config); err == nil {
t.Fatalf("should have err")
}
}
func TestPostProcessor_Configure_checkAccessTokenIsRequiredByDefault(t *testing.T) {
var p PostProcessor
server := newSecureServer("foo", nil)
defer server.Close()
config := testNoAccessTokenProvidedConfig()
config["vagrant_cloud_url"] = server.URL
if err := p.Configure(config); err == nil {
t.Fatalf("Expected access token to be required.")
}
}
func TestPostProcessor_Configure_checkAccessTokenIsNotRequiredForOverridenVagrantCloud(t *testing.T) {
var p PostProcessor
server := newNoAuthServer(nil)
defer server.Close()
config := testNoAccessTokenProvidedConfig()
config["vagrant_cloud_url"] = server.URL
if err := p.Configure(config); err != nil {
t.Fatalf("Expected blank access token to be allowed and authenticate to pass: %s", err)
}
}
func TestPostProcessor_PostProcess_checkArtifactType(t *testing.T) {
artifact := &packer.MockArtifact{
BuilderIdValue: "invalid.builder",
}
config := testGoodConfig()
server := newSecureServer("foo", nil)
defer server.Close()
config["vagrant_cloud_url"] = server.URL
var p PostProcessor
p.Configure(config)
_, _, _, err := p.PostProcess(context.Background(), testUi(), artifact)
if !strings.Contains(err.Error(), "Unknown artifact type") {
t.Fatalf("Should error with message 'Unknown artifact type...' with BuilderId: %s", artifact.BuilderIdValue)
}
}
func TestPostProcessor_PostProcess_checkArtifactFileIsBox(t *testing.T) {
artifact := &packer.MockArtifact{
BuilderIdValue: "mitchellh.post-processor.vagrant", // good
FilesValue: []string{"invalid.boxfile"}, // should have .box extension
}
config := testGoodConfig()
server := newSecureServer("foo", nil)
defer server.Close()
config["vagrant_cloud_url"] = server.URL
var p PostProcessor
p.Configure(config)
_, _, _, err := p.PostProcess(context.Background(), testUi(), artifact)
if !strings.Contains(err.Error(), "Unknown files in artifact") {
t.Fatalf("Should error with message 'Unknown files in artifact...' with artifact file: %s",
artifact.FilesValue[0])
}
}
func TestPostProcessor_PostProcess_uploadsAndReleases(t *testing.T) {
files := tarFiles{
{"foo.txt", "This is a foo file"},
{"bar.txt", "This is a bar file"},
{"metadata.json", `{"provider": "virtualbox"}`},
}
boxfile, err := createBox(files)
if err != nil {
t.Fatalf("%s", err)
}
defer os.Remove(boxfile.Name())
artifact := &packer.MockArtifact{
BuilderIdValue: "mitchellh.post-processor.vagrant",
FilesValue: []string{boxfile.Name()},
}
s := newStackServer([]stubResponse{stubResponse{StatusCode: 200, Method: "PUT", Path: "/box-upload-path"}})
defer s.Close()
stack := []stubResponse{
stubResponse{StatusCode: 200, Method: "GET", Path: "/authenticate"},
stubResponse{StatusCode: 200, Method: "GET", Path: "/box/hashicorp/precise64", Response: `{"tag": "hashicorp/precise64"}`},
stubResponse{StatusCode: 200, Method: "POST", Path: "/box/hashicorp/precise64/versions", Response: `{}`},
stubResponse{StatusCode: 200, Method: "POST", Path: "/box/hashicorp/precise64/version/0.5/providers", Response: `{}`},
stubResponse{StatusCode: 200, Method: "GET", Path: "/box/hashicorp/precise64/version/0.5/provider/id/upload", Response: `{"upload_path": "` + s.URL + `/box-upload-path"}`},
stubResponse{StatusCode: 200, Method: "PUT", Path: "/box/hashicorp/precise64/version/0.5/release"},
}
server := newStackServer(stack)
defer server.Close()
config := testGoodConfig()
config["vagrant_cloud_url"] = server.URL
config["no_direct_upload"] = true
var p PostProcessor
err = p.Configure(config)
if err != nil {
t.Fatalf("err: %s", err)
}
_, _, _, err = p.PostProcess(context.Background(), testUi(), artifact)
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestPostProcessor_PostProcess_uploadsAndNoRelease(t *testing.T) {
files := tarFiles{
{"foo.txt", "This is a foo file"},
{"bar.txt", "This is a bar file"},
{"metadata.json", `{"provider": "virtualbox"}`},
}
boxfile, err := createBox(files)
if err != nil {
t.Fatalf("%s", err)
}
defer os.Remove(boxfile.Name())
artifact := &packer.MockArtifact{
BuilderIdValue: "mitchellh.post-processor.vagrant",
FilesValue: []string{boxfile.Name()},
}
s := newStackServer([]stubResponse{stubResponse{StatusCode: 200, Method: "PUT", Path: "/box-upload-path"}})
defer s.Close()
stack := []stubResponse{
stubResponse{StatusCode: 200, Method: "GET", Path: "/authenticate"},
stubResponse{StatusCode: 200, Method: "GET", Path: "/box/hashicorp/precise64", Response: `{"tag": "hashicorp/precise64"}`},
stubResponse{StatusCode: 200, Method: "POST", Path: "/box/hashicorp/precise64/versions", Response: `{}`},
stubResponse{StatusCode: 200, Method: "POST", Path: "/box/hashicorp/precise64/version/0.5/providers", Response: `{}`},
stubResponse{StatusCode: 200, Method: "GET", Path: "/box/hashicorp/precise64/version/0.5/provider/id/upload", Response: `{"upload_path": "` + s.URL + `/box-upload-path"}`},
}
server := newStackServer(stack)
defer server.Close()
config := testGoodConfig()
config["vagrant_cloud_url"] = server.URL
config["no_direct_upload"] = true
config["no_release"] = true
var p PostProcessor
err = p.Configure(config)
if err != nil {
t.Fatalf("err: %s", err)
}
_, _, _, err = p.PostProcess(context.Background(), testUi(), artifact)
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestPostProcessor_PostProcess_uploadsDirectAndReleases(t *testing.T) {
files := tarFiles{
{"foo.txt", "This is a foo file"},
{"bar.txt", "This is a bar file"},
{"metadata.json", `{"provider": "virtualbox"}`},
}
boxfile, err := createBox(files)
if err != nil {
t.Fatalf("%s", err)
}
defer os.Remove(boxfile.Name())
artifact := &packer.MockArtifact{
BuilderIdValue: "mitchellh.post-processor.vagrant",
FilesValue: []string{boxfile.Name()},
}
s := newStackServer(
[]stubResponse{
stubResponse{StatusCode: 200, Method: "PUT", Path: "/box-upload-path"},
},
)
defer s.Close()
stack := []stubResponse{
stubResponse{StatusCode: 200, Method: "GET", Path: "/authenticate"},
stubResponse{StatusCode: 200, Method: "GET", Path: "/box/hashicorp/precise64", Response: `{"tag": "hashicorp/precise64"}`},
stubResponse{StatusCode: 200, Method: "POST", Path: "/box/hashicorp/precise64/versions", Response: `{}`},
stubResponse{StatusCode: 200, Method: "POST", Path: "/box/hashicorp/precise64/version/0.5/providers", Response: `{}`},
stubResponse{StatusCode: 200, Method: "GET", Path: "/box/hashicorp/precise64/version/0.5/provider/id/upload/direct"},
stubResponse{StatusCode: 200, Method: "PUT", Path: "/box-upload-complete"},
stubResponse{StatusCode: 200, Method: "PUT", Path: "/box/hashicorp/precise64/version/0.5/release"},
}
server := newStackServer(stack)
defer server.Close()
config := testGoodConfig()
config["vagrant_cloud_url"] = server.URL
// Set response here so we have API server URL available
stack[4].Response = `{"upload_path": "` + s.URL + `/box-upload-path", "callback": "` + server.URL + `/box-upload-complete"}`
var p PostProcessor
err = p.Configure(config)
if err != nil {
t.Fatalf("err: %s", err)
}
_, _, _, err = p.PostProcess(context.Background(), testUi(), artifact)
if err != nil {
t.Fatalf("err: %s", 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")
}
}
func TestProviderFromVagrantBox_missing_box(t *testing.T) {
// Bad: Box does not exist
boxfile := "i_dont_exist.box"
_, err := providerFromVagrantBox(boxfile)
if err == nil {
t.Fatal("Should have error as box file does not exist")
}
t.Logf("%s", err)
}
func TestProviderFromVagrantBox_empty_box(t *testing.T) {
// Bad: Empty box file
boxfile, err := newBoxFile()
if err != nil {
t.Fatalf("%s", err)
}
defer os.Remove(boxfile.Name())
_, err = providerFromVagrantBox(boxfile.Name())
if err == nil {
t.Fatal("Should have error as box file is empty")
}
t.Logf("%s", err)
}
func TestProviderFromVagrantBox_gzip_only_box(t *testing.T) {
boxfile, err := newBoxFile()
if err != nil {
t.Fatalf("%s", err)
}
defer os.Remove(boxfile.Name())
// Bad: Box is just a plain gzip file
aw := gzip.NewWriter(boxfile)
_, err = aw.Write([]byte("foo content"))
if err != nil {
t.Fatal("Error zipping test box file")
}
aw.Close() // Flush the gzipped contents to file
_, err = providerFromVagrantBox(boxfile.Name())
if err == nil {
t.Fatalf("Should have error as box file is a plain gzip file: %s", err)
}
t.Logf("%s", err)
}
func TestProviderFromVagrantBox_no_files_in_archive(t *testing.T) {
// Bad: Box contains no files
boxfile, err := createBox(tarFiles{})
if err != nil {
t.Fatalf("Error creating test box: %s", err)
}
defer os.Remove(boxfile.Name())
_, err = providerFromVagrantBox(boxfile.Name())
if err == nil {
t.Fatalf("Should have error as box file has no contents")
}
t.Logf("%s", err)
}
func TestProviderFromVagrantBox_no_metadata(t *testing.T) {
// Bad: Box contains no metadata/metadata.json file
files := tarFiles{
{"foo.txt", "This is a foo file"},
{"bar.txt", "This is a bar file"},
}
boxfile, err := createBox(files)
if err != nil {
t.Fatalf("Error creating test box: %s", err)
}
defer os.Remove(boxfile.Name())
_, err = providerFromVagrantBox(boxfile.Name())
if err == nil {
t.Fatalf("Should have error as box file does not include metadata.json file")
}
t.Logf("%s", err)
}
func TestProviderFromVagrantBox_metadata_empty(t *testing.T) {
// Bad: Create a box with an empty metadata.json file
files := tarFiles{
{"foo.txt", "This is a foo file"},
{"bar.txt", "This is a bar file"},
{"metadata.json", ""},
}
boxfile, err := createBox(files)
if err != nil {
t.Fatalf("Error creating test box: %s", err)
}
defer os.Remove(boxfile.Name())
_, err = providerFromVagrantBox(boxfile.Name())
if err == nil {
t.Fatalf("Should have error as box files metadata.json file is empty")
}
t.Logf("%s", err)
}
func TestProviderFromVagrantBox_metadata_bad_json(t *testing.T) {
// Bad: Create a box with bad JSON in the metadata.json file
files := tarFiles{
{"foo.txt", "This is a foo file"},
{"bar.txt", "This is a bar file"},
{"metadata.json", "{provider: badjson}"},
}
boxfile, err := createBox(files)
if err != nil {
t.Fatalf("Error creating test box: %s", err)
}
defer os.Remove(boxfile.Name())
_, err = providerFromVagrantBox(boxfile.Name())
if err == nil {
t.Fatalf("Should have error as box files metadata.json file contains badly formatted JSON")
}
t.Logf("%s", err)
}
func TestProviderFromVagrantBox_metadata_no_provider_key(t *testing.T) {
// Bad: Create a box with no 'provider' key in the metadata.json file
files := tarFiles{
{"foo.txt", "This is a foo file"},
{"bar.txt", "This is a bar file"},
{"metadata.json", `{"cows":"moo"}`},
}
boxfile, err := createBox(files)
if err != nil {
t.Fatalf("Error creating test box: %s", err)
}
defer os.Remove(boxfile.Name())
_, err = providerFromVagrantBox(boxfile.Name())
if err == nil {
t.Fatalf("Should have error as provider key/value pair is missing from boxes metadata.json file")
}
t.Logf("%s", err)
}
func TestProviderFromVagrantBox_metadata_provider_value_empty(t *testing.T) {
// Bad: The boxes metadata.json file 'provider' key has an empty value
files := tarFiles{
{"foo.txt", "This is a foo file"},
{"bar.txt", "This is a bar file"},
{"metadata.json", `{"provider":""}`},
}
boxfile, err := createBox(files)
if err != nil {
t.Fatalf("Error creating test box: %s", err)
}
defer os.Remove(boxfile.Name())
_, err = providerFromVagrantBox(boxfile.Name())
if err == nil {
t.Fatalf("Should have error as value associated with 'provider' key in boxes metadata.json file is empty")
}
t.Logf("%s", err)
}
func TestProviderFromVagrantBox_metadata_ok(t *testing.T) {
// Good: The boxes metadata.json file has the 'provider' key/value pair
expectedProvider := "virtualbox"
files := tarFiles{
{"foo.txt", "This is a foo file"},
{"bar.txt", "This is a bar file"},
{"metadata.json", `{"provider":"` + expectedProvider + `"}`},
}
boxfile, err := createBox(files)
if err != nil {
t.Fatalf("Error creating test box: %s", err)
}
defer os.Remove(boxfile.Name())
provider, err := providerFromVagrantBox(boxfile.Name())
if err != nil {
t.Fatalf("error getting provider from vagrant box %s:%v", boxfile.Name(), err)
}
assert.Equal(t, expectedProvider, provider, "Error: Expected provider: '%s'. Got '%s'", expectedProvider, provider)
t.Logf("Expected provider '%s'. Got provider '%s'", expectedProvider, provider)
}
func TestGetProvider_artifice(t *testing.T) {
expectedProvider := "virtualbox"
files := tarFiles{
{"foo.txt", "This is a foo file"},
{"bar.txt", "This is a bar file"},
{"metadata.json", `{"provider":"` + expectedProvider + `"}`},
}
boxfile, err := createBox(files)
if err != nil {
t.Fatalf("Error creating test box: %s", err)
}
defer os.Remove(boxfile.Name())
provider, err := getProvider("", boxfile.Name(), "artifice")
if err != nil {
t.Fatalf("error getting provider %s:%v", boxfile.Name(), err)
}
assert.Equal(t, expectedProvider, provider, "Error: Expected provider: '%s'. Got '%s'", expectedProvider, provider)
t.Logf("Expected provider '%s'. Got provider '%s'", expectedProvider, provider)
}
func TestGetProvider_other(t *testing.T) {
expectedProvider := "virtualbox"
provider, _ := getProvider(expectedProvider, "foo.box", "other")
assert.Equal(t, expectedProvider, provider, "Error: Expected provider: '%s'. Got '%s'", expectedProvider, provider)
t.Logf("Expected provider '%s'. Got provider '%s'", expectedProvider, provider)
}
func newBoxFile() (boxfile *os.File, err error) {
boxfile, err = ioutil.TempFile(os.TempDir(), "test*.box")
if err != nil {
return boxfile, fmt.Errorf("Error creating test box file: %s", err)
}
return boxfile, nil
}
func createBox(files tarFiles) (boxfile *os.File, err error) {
boxfile, err = newBoxFile()
if err != nil {
return boxfile, err
}
// Box files are gzipped tar archives
aw := gzip.NewWriter(boxfile)
tw := tar.NewWriter(aw)
// Add each file to the box
for _, file := range files {
// Create and write the tar file header
hdr := &tar.Header{
Name: file.Name,
Mode: 0644,
Size: int64(len(file.Body)),
}
err = tw.WriteHeader(hdr)
if err != nil {
return boxfile, fmt.Errorf("Error writing box tar file header: %s", err)
}
// Write the file contents
_, err = tw.Write([]byte(file.Body))
if err != nil {
return boxfile, fmt.Errorf("Error writing box tar file contents: %s", err)
}
}
// Flush and close each writer
err = tw.Close()
if err != nil {
return boxfile, fmt.Errorf("Error flushing tar file contents: %s", err)
}
err = aw.Close()
if err != nil {
return boxfile, fmt.Errorf("Error flushing gzip file contents: %s", err)
}
return boxfile, nil
}