command: remove "terraform push"

The remote API this talks to will be going away very soon, before our next
major release, and so we'll remove the command altogether in that release.

This also removes the "encodeHCL" function, which was used only for
adding a .tfvars-formatted file to the uploaded archive.
This commit is contained in:
Martin Atkins 2018-03-28 09:47:05 -07:00
parent c51d22bdeb
commit 618883596a
5 changed files with 13 additions and 1661 deletions

View File

@ -1,196 +0,0 @@
package command
// Marshal an object as an hcl value.
import (
"bytes"
"fmt"
"regexp"
"github.com/hashicorp/hcl/hcl/printer"
)
// This will only work operate on []interface{}, map[string]interface{}, and
// primitive types.
func encodeHCL(i interface{}) ([]byte, error) {
state := &encodeState{}
err := state.encode(i)
if err != nil {
return nil, err
}
hcl := state.Bytes()
if len(hcl) == 0 {
return hcl, nil
}
// the HCL parser requires an assignment. Strip it off again later
fakeAssignment := append([]byte("X = "), hcl...)
// use the real hcl parser to verify our output, and format it canonically
hcl, err = printer.Format(fakeAssignment)
if err != nil {
return nil, err
}
// now strip that first assignment off
eq := regexp.MustCompile(`=\s+`).FindIndex(hcl)
// strip of an extra \n if it's there
end := len(hcl)
if hcl[end-1] == '\n' {
end -= 1
}
return hcl[eq[1]:end], nil
}
type encodeState struct {
bytes.Buffer
}
func (e *encodeState) encode(i interface{}) error {
switch v := i.(type) {
case []interface{}:
return e.encodeList(v)
case map[string]interface{}:
return e.encodeMap(v)
case int, int8, int32, int64, uint8, uint32, uint64:
return e.encodeInt(i)
case float32, float64:
return e.encodeFloat(i)
case string:
return e.encodeString(v)
case nil:
return nil
default:
return fmt.Errorf("invalid type %T", i)
}
}
func (e *encodeState) encodeList(l []interface{}) error {
e.WriteString("[")
for i, v := range l {
err := e.encode(v)
if err != nil {
return err
}
if i < len(l)-1 {
e.WriteString(", ")
}
}
e.WriteString("]")
return nil
}
func (e *encodeState) encodeMap(m map[string]interface{}) error {
e.WriteString("{\n")
for i, k := range sortedKeys(m) {
v := m[k]
e.WriteString(fmt.Sprintf("%q = ", k))
err := e.encode(v)
if err != nil {
return err
}
if i < len(m)-1 {
e.WriteString("\n")
}
}
e.WriteString("}")
return nil
}
func (e *encodeState) encodeInt(i interface{}) error {
_, err := fmt.Fprintf(e, "%d", i)
return err
}
func (e *encodeState) encodeFloat(f interface{}) error {
_, err := fmt.Fprintf(e, "%g", f)
return err
}
func (e *encodeState) encodeString(s string) error {
e.Write(quoteHCLString(s))
return nil
}
// Quote an HCL string, which may contain interpolations.
// Since the string was already parsed from HCL, we have to assume the
// required characters are sanely escaped. All we need to do is escape double
// quotes in the string, unless they are in an interpolation block.
func quoteHCLString(s string) []byte {
out := make([]byte, 0, len(s))
out = append(out, '"')
// our parse states
var (
outer = 1 // the starting state for the string
dollar = 2 // look for '{' in the next character
interp = 3 // inside an interpolation block
escape = 4 // take the next character and pop back to prev state
)
// we could have nested interpolations
state := stack{}
state.push(outer)
for i := 0; i < len(s); i++ {
switch state.peek() {
case outer:
switch s[i] {
case '"':
out = append(out, '\\')
case '$':
state.push(dollar)
case '\\':
state.push(escape)
}
case dollar:
state.pop()
switch s[i] {
case '{':
state.push(interp)
case '\\':
state.push(escape)
}
case interp:
switch s[i] {
case '}':
state.pop()
}
case escape:
state.pop()
}
out = append(out, s[i])
}
out = append(out, '"')
return out
}
type stack []int
func (s *stack) push(i int) {
*s = append(*s, i)
}
func (s *stack) pop() int {
last := len(*s) - 1
i := (*s)[last]
*s = (*s)[:last]
return i
}
func (s *stack) peek() int {
return (*s)[len(*s)-1]
}

View File

@ -1,52 +0,0 @@
package command
import "testing"
// The command package has it's own HCL encoder to encode variables to push.
// Make sure the variable we encode parse correctly
func TestHCLEncoder_parse(t *testing.T) {
cases := []struct {
Name string
Val interface{}
Error bool
}{
{
Name: "int",
Val: 12345,
},
{
Name: "float",
Val: 1.2345,
},
{
Name: "string",
Val: "terraform",
},
{
Name: "list",
Val: []interface{}{"a", "b", "c"},
},
{
Name: "map",
Val: map[string]interface{}{
"a": 1,
},
},
// a numeric looking identifier requires quotes
{
Name: "map_with_quoted_key",
Val: map[string]interface{}{
"0.0.0.0/24": "mask",
},
},
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
_, err := encodeHCL(c.Val)
if err != nil {
t.Fatal(err)
}
})
}
}

View File

@ -1,560 +1,36 @@
package command
import (
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/hashicorp/atlas-go/archive"
"github.com/hashicorp/atlas-go/v1"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/version"
"github.com/hashicorp/terraform/tfdiags"
)
type PushCommand struct {
Meta
// client is the client to use for the actual push operations.
// If this isn't set, then the Atlas client is used. This should
// really only be set for testing reasons (and is hence not exported).
client pushClient
}
func (c *PushCommand) Run(args []string) int {
var atlasAddress, atlasToken string
var archiveVCS, moduleUpload bool
var name string
var overwrite []string
args, err := c.Meta.process(args, true)
if err != nil {
return 1
}
cmdFlags := c.Meta.flagSet("push")
cmdFlags.StringVar(&atlasAddress, "atlas-address", "", "")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&atlasToken, "token", "", "")
cmdFlags.BoolVar(&moduleUpload, "upload-modules", true, "")
cmdFlags.StringVar(&name, "name", "", "")
cmdFlags.BoolVar(&archiveVCS, "vcs", true, "")
cmdFlags.Var((*FlagStringSlice)(&overwrite), "overwrite", "")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// Make a map of the set values
overwriteMap := make(map[string]struct{}, len(overwrite))
for _, v := range overwrite {
overwriteMap[v] = struct{}{}
}
// This is a map of variables specifically from the CLI that we want to overwrite.
// We need this because there is a chance that the user is trying to modify
// a variable we don't see in our context, but which exists in this Terraform
// Enterprise workspace.
cliVars := make(map[string]string)
for k, v := range c.variables {
if _, ok := overwriteMap[k]; ok {
if val, ok := v.(string); ok {
cliVars[k] = val
} else {
c.Ui.Error(fmt.Sprintf("Error reading value for variable: %s", k))
return 1
}
}
}
// Get the path to the configuration depending on the args.
configPath, err := ModulePath(cmdFlags.Args())
if err != nil {
c.Ui.Error(err.Error())
return 1
}
// Check if the path is a plan
plan, err := c.Plan(configPath)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if plan != nil {
c.Ui.Error(
"A plan file cannot be given as the path to the configuration.\n" +
"A path to a module (directory with configuration) must be given.")
return 1
}
// Load the module
mod, diags := c.Module(configPath)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
if mod == nil {
c.Ui.Error(fmt.Sprintf(
"No configuration files found in the directory: %s\n\n"+
"This command requires configuration to run.",
configPath))
return 1
}
var conf *config.Config
if mod != nil {
conf = mod.Config()
}
// Load the backend
b, err := c.Backend(&BackendOpts{
Config: conf,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// We require a non-local backend
if c.IsLocalBackend(b) {
c.Ui.Error(
"A remote backend is not enabled. For Atlas to run Terraform\n" +
"for you, remote state must be used and configured. Remote \n" +
"state via any backend is accepted, not just Atlas. To configure\n" +
"a backend, please see the documentation at the URL below:\n\n" +
"https://www.terraform.io/docs/state/remote.html")
return 1
}
// We require a local backend
local, ok := b.(backend.Local)
if !ok {
c.Ui.Error(ErrUnsupportedLocalOp)
return 1
}
// Build the operation
opReq := c.Operation()
opReq.Module = mod
opReq.Plan = plan
// Get the context
ctx, _, err := local.Context(opReq)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
defer func() {
err := opReq.StateLocker.Unlock(nil)
if err != nil {
c.Ui.Error(err.Error())
}
}()
// Get the configuration
config := ctx.Module().Config()
if name == "" {
if config.Atlas == nil || config.Atlas.Name == "" {
c.Ui.Error(
"The name of this Terraform configuration in Atlas must be\n" +
"specified within your configuration or the command-line. To\n" +
"set it on the command-line, use the `-name` parameter.")
return 1
}
name = config.Atlas.Name
}
// Initialize the client if it isn't given.
if c.client == nil {
// Make sure to nil out our client so our token isn't sitting around
defer func() { c.client = nil }()
// Initialize it to the default client, we set custom settings later
client := atlas.DefaultClient()
if atlasAddress != "" {
client, err = atlas.NewClient(atlasAddress)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing Atlas client: %s", err))
return 1
}
}
client.DefaultHeader.Set(version.Header, version.Version)
if atlasToken != "" {
client.Token = atlasToken
}
c.client = &atlasPushClient{Client: client}
}
// Get the variables we already have in atlas
atlasVars, err := c.client.Get(name)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error looking up previously pushed configuration: %s", err))
return 1
}
// Set remote variables in the context if we don't have a value here. These
// don't have to be correct, it just prevents the Input walk from prompting
// the user for input.
ctxVars := ctx.Variables()
atlasVarSentry := "ATLAS_78AC153CA649EAA44815DAD6CBD4816D"
for k, _ := range atlasVars {
if _, ok := ctxVars[k]; !ok {
ctx.SetVariable(k, atlasVarSentry)
}
}
// Ask for input
if err := ctx.Input(c.InputMode()); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error while asking for variable input:\n\n%s", err))
return 1
}
// Now that we've gone through the input walk, we can be sure we have all
// the variables we're going to get.
// We are going to keep these separate from the atlas variables until
// upload, so we can notify the user which local variables we're sending.
serializedVars, err := tfVars(ctx.Variables())
if err != nil {
c.Ui.Error(fmt.Sprintf(
"An error has occurred while serializing the variables for uploading:\n"+
"%s", err))
return 1
}
// Get the absolute path for our data directory, since the Extra field
// value below needs to be absolute.
dataDirAbs, err := filepath.Abs(c.DataDir())
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error while expanding the data directory %q: %s", c.DataDir(), err))
return 1
}
// Build the archiving options, which includes everything it can
// by default according to VCS rules but forcing the data directory.
archiveOpts := &archive.ArchiveOpts{
VCS: archiveVCS,
Extra: map[string]string{
DefaultDataDir: archive.ExtraEntryDir,
},
}
// Always store the state file in here so we can find state
statePathKey := fmt.Sprintf("%s/%s", DefaultDataDir, DefaultStateFilename)
archiveOpts.Extra[statePathKey] = filepath.Join(dataDirAbs, DefaultStateFilename)
if moduleUpload {
// If we're uploading modules, explicitly add that directory if exists.
moduleKey := fmt.Sprintf("%s/%s", DefaultDataDir, "modules")
moduleDir := filepath.Join(dataDirAbs, "modules")
_, err := os.Stat(moduleDir)
if err == nil {
archiveOpts.Extra[moduleKey] = filepath.Join(dataDirAbs, "modules")
}
if err != nil && !os.IsNotExist(err) {
c.Ui.Error(fmt.Sprintf(
"Error checking for module dir %q: %s", moduleDir, err))
return 1
}
} else {
// If we're not uploading modules, explicitly exclude add that
archiveOpts.Exclude = append(
archiveOpts.Exclude,
filepath.Join(c.DataDir(), "modules"))
}
archiveR, err := archive.CreateArchive(configPath, archiveOpts)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"An error has occurred while archiving the module for uploading:\n"+
"%s", err))
return 1
}
// List of the vars we're uploading to display to the user.
// We always upload all vars from atlas, but only report them if they are overwritten.
var setVars []string
// variables to upload
var uploadVars []atlas.TFVar
// first add all the variables we want to send which have been serialized
// from the local context.
for _, sv := range serializedVars {
_, inOverwrite := overwriteMap[sv.Key]
_, inAtlas := atlasVars[sv.Key]
// We have a variable that's not in atlas, so always send it.
if !inAtlas {
uploadVars = append(uploadVars, sv)
setVars = append(setVars, sv.Key)
}
// We're overwriting an atlas variable.
// We also want to check that we
// don't send the dummy sentry value back to atlas. This could happen
// if it's specified as an overwrite on the cli, but we didn't set a
// new value.
if inAtlas && inOverwrite && sv.Value != atlasVarSentry {
uploadVars = append(uploadVars, sv)
setVars = append(setVars, sv.Key)
// remove this value from the atlas vars, because we're going to
// send back the remainder regardless.
delete(atlasVars, sv.Key)
}
}
// now send back all the existing atlas vars, inserting any overwrites from the cli.
for k, av := range atlasVars {
if v, ok := cliVars[k]; ok {
av.Value = v
setVars = append(setVars, k)
}
uploadVars = append(uploadVars, av)
}
sort.Strings(setVars)
if len(setVars) > 0 {
c.Ui.Output(
"The following variables will be set or overwritten within Atlas from\n" +
"their local values. All other variables are already set within Atlas.\n" +
"If you want to modify the value of a variable, use the Atlas web\n" +
"interface or set it locally and use the -overwrite flag.\n\n")
for _, v := range setVars {
c.Ui.Output(fmt.Sprintf(" * %s", v))
}
// Newline
c.Ui.Output("")
}
// Upsert!
opts := &pushUpsertOptions{
Name: name,
Archive: archiveR,
Variables: ctx.Variables(),
TFVars: uploadVars,
}
c.Ui.Output("Uploading Terraform configuration...")
vsn, err := c.client.Upsert(opts)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"An error occurred while uploading the module:\n\n%s", err))
return 1
}
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][bold][green]Configuration %q uploaded! (v%d)",
name, vsn)))
c.showDiagnostics(diags)
if diags.HasErrors() {
return 1
}
return 0
// This command is no longer supported, but we'll retain it just to
// give the user some next-steps after upgrading.
c.showDiagnostics(tfdiags.Sourceless(
tfdiags.Error,
"Command \"terraform push\" is no longer supported",
"This command was used to push configuration to Terraform Enterprise legacy (v1), which has now reached end-of-life. To push configuration to Terraform Enterprise v2, use its REST API. Contact Terraform Enterprise support for more information.",
))
return 1
}
func (c *PushCommand) Help() string {
helpText := `
Usage: terraform push [options] [DIR]
Upload this Terraform module to an Atlas server for remote
infrastructure management.
Options:
-atlas-address=<url> An alternate address to an Atlas instance. Defaults
to https://atlas.hashicorp.com
-upload-modules=true If true (default), then the modules are locked at
their current checkout and uploaded completely. This
prevents Atlas from running "terraform get".
-name=<name> Name of the configuration in Atlas. This can also
be set in the configuration itself. Format is
typically: "username/name".
-token=<token> Access token to use to upload. If blank or unspecified,
the ATLAS_TOKEN environmental variable will be used.
-overwrite=foo Variable keys that should overwrite values in Atlas.
Otherwise, variables already set in Atlas will overwrite
local values. This flag can be repeated.
-var 'foo=bar' Set a variable in the Terraform configuration. This
flag can be set multiple times.
-var-file=foo Set variables in the Terraform configuration from
a file. If "terraform.tfvars" or any ".auto.tfvars"
files are present, they will be automatically loaded.
-vcs=true If true (default), push will upload only files
committed to your VCS, if detected.
-no-color If specified, output won't contain any color.
This command was for the legacy version of Terraform Enterprise (v1), which
has now reached end-of-life. Therefore this command is no longer supported.
`
return strings.TrimSpace(helpText)
}
func sortedKeys(m map[string]interface{}) []string {
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// build the set of TFVars for push
func tfVars(vars map[string]interface{}) ([]atlas.TFVar, error) {
var tfVars []atlas.TFVar
var err error
RANGE:
for _, k := range sortedKeys(vars) {
v := vars[k]
var hcl []byte
tfv := atlas.TFVar{Key: k}
switch v := v.(type) {
case string:
tfv.Value = v
default:
// everything that's not a string is now HCL encoded
hcl, err = encodeHCL(v)
if err != nil {
break RANGE
}
tfv.Value = string(hcl)
tfv.IsHCL = true
}
tfVars = append(tfVars, tfv)
}
return tfVars, err
}
func (c *PushCommand) Synopsis() string {
return "Upload this Terraform module to Atlas to run"
}
// pushClient is implemented internally to control where pushes go. This is
// either to Atlas or a mock for testing. We still return a map to make it
// easier to check for variable existence when filtering the overrides.
type pushClient interface {
Get(string) (map[string]atlas.TFVar, error)
Upsert(*pushUpsertOptions) (int, error)
}
type pushUpsertOptions struct {
Name string
Archive *archive.Archive
Variables map[string]interface{}
TFVars []atlas.TFVar
}
type atlasPushClient struct {
Client *atlas.Client
}
func (c *atlasPushClient) Get(name string) (map[string]atlas.TFVar, error) {
user, name, err := atlas.ParseSlug(name)
if err != nil {
return nil, err
}
version, err := c.Client.TerraformConfigLatest(user, name)
if err != nil {
return nil, err
}
variables := make(map[string]atlas.TFVar)
if version == nil {
return variables, nil
}
// Variables is superseded by TFVars
if version.TFVars == nil {
for k, v := range version.Variables {
variables[k] = atlas.TFVar{Key: k, Value: v}
}
} else {
for _, v := range version.TFVars {
variables[v.Key] = v
}
}
return variables, nil
}
func (c *atlasPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
user, name, err := atlas.ParseSlug(opts.Name)
if err != nil {
return 0, err
}
data := &atlas.TerraformConfigVersion{
TFVars: opts.TFVars,
}
version, err := c.Client.CreateTerraformConfigVersion(
user, name, data, opts.Archive, opts.Archive.Size)
if err != nil {
return 0, err
}
return version, nil
}
type mockPushClient struct {
File string
GetCalled bool
GetName string
GetResult map[string]atlas.TFVar
GetError error
UpsertCalled bool
UpsertOptions *pushUpsertOptions
UpsertVersion int
UpsertError error
}
func (c *mockPushClient) Get(name string) (map[string]atlas.TFVar, error) {
c.GetCalled = true
c.GetName = name
return c.GetResult, c.GetError
}
func (c *mockPushClient) Upsert(opts *pushUpsertOptions) (int, error) {
f, err := os.Create(c.File)
if err != nil {
return 0, err
}
defer f.Close()
data := opts.Archive
size := opts.Archive.Size
if _, err := io.CopyN(f, data, size); err != nil {
return 0, err
}
c.UpsertCalled = true
c.UpsertOptions = opts
return c.UpsertVersion, c.UpsertError
return "Obsolete command for Terraform Enterprise legacy (v1)"
}

View File

@ -1,877 +0,0 @@
package command
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
atlas "github.com/hashicorp/atlas-go/v1"
"github.com/hashicorp/terraform/helper/copy"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
func TestPush_good(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
testStateFileRemote(t, s)
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
}
args := []string{
"-vcs=false",
testFixturePath("push"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testArchiveStr(t, archivePath)
expected := []string{
".terraform/",
".terraform/terraform.tfstate",
"main.tf",
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
variables := make(map[string]interface{})
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
if client.UpsertOptions.Name != "foo" {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
}
func TestPush_goodBackendInit(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("push-backend-new"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
// init backend
ui := new(cli.MockUi)
ci := &InitCommand{
Meta: Meta{
Ui: ui,
},
}
if code := ci.Run(nil); code != 0 {
t.Fatalf("bad: %d\n%s", code, ui.ErrorWriter)
}
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
ui = new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
}
args := []string{
"-vcs=false",
td,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testArchiveStr(t, archivePath)
expected := []string{
// Expected weird behavior, doesn't affect unpackaging
".terraform/",
".terraform/",
// this config contains no plugins
// TODO: we should add one or more plugins to this test config, just to
// verfy the pushed data. The expected additional files are listed below:
//
//".terraform/plugins/",
//fmt.Sprintf(".terraform/plugins/%s_%s/", runtime.GOOS, runtime.GOARCH),
//fmt.Sprintf(".terraform/plugins/%s_%s/lock.json", runtime.GOOS, runtime.GOARCH),
".terraform/terraform.tfstate",
".terraform/terraform.tfstate",
"main.tf",
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("expected: %#v\ngot: %#v", expected, actual)
}
variables := make(map[string]interface{})
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
if client.UpsertOptions.Name != "hello" {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
}
func TestPush_noUploadModules(t *testing.T) {
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
}
// Path of the test. We have to do some renaming to avoid our own
// VCS getting in the way.
path := testFixturePath("push-no-upload")
defer os.RemoveAll(filepath.Join(path, ".terraform"))
// Move into that directory
defer testChdir(t, path)()
// Do a "terraform get"
{
ui := new(cli.MockUi)
c := &GetCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
if code := c.Run([]string{}); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
defer os.Remove(testStateFileRemote(t, s))
args := []string{
"-vcs=false",
"-name=mitchellh/tf-test",
"-upload-modules=false",
path,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// NOTE: The duplicates below are not ideal but are how things work
// currently due to how we manually add the files to the archive. This
// is definitely a "bug" we can fix in the future.
actual := testArchiveStr(t, archivePath)
expected := []string{
".terraform/",
".terraform/",
".terraform/terraform.tfstate",
".terraform/terraform.tfstate",
"child/",
"child/main.tf",
"main.tf",
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestPush_input(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
testStateFileRemote(t, s)
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
}
// Disable test mode so input would be asked and setup the
// input reader/writers.
test = false
defer func() { test = true }()
defaultInputReader = bytes.NewBufferString("foo\n")
defaultInputWriter = new(bytes.Buffer)
args := []string{
"-vcs=false",
testFixturePath("push-input"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
variables := map[string]interface{}{
"foo": "foo",
}
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions.Variables)
}
}
// We want a variable from atlas to fill a missing variable locally
func TestPush_inputPartial(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
testStateFileRemote(t, s)
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{
File: archivePath,
GetResult: map[string]atlas.TFVar{
"foo": atlas.TFVar{Key: "foo", Value: "bar"},
},
}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
}
// Disable test mode so input would be asked and setup the
// input reader/writers.
test = false
defer func() { test = true }()
defaultInputReader = bytes.NewBufferString("foo\n")
defaultInputWriter = new(bytes.Buffer)
args := []string{
"-vcs=false",
testFixturePath("push-input-partial"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
expectedTFVars := []atlas.TFVar{
{Key: "bar", Value: "foo"},
{Key: "foo", Value: "bar"},
}
if !reflect.DeepEqual(client.UpsertOptions.TFVars, expectedTFVars) {
t.Logf("expected: %#v", expectedTFVars)
t.Fatalf("got: %#v", client.UpsertOptions.TFVars)
}
}
// This tests that the push command will override Atlas variables
// if requested.
func TestPush_localOverride(t *testing.T) {
// Disable test mode so input would be asked and setup the
// input reader/writers.
test = false
defer func() { test = true }()
defaultInputReader = bytes.NewBufferString("nope\n")
defaultInputWriter = new(bytes.Buffer)
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
testStateFileRemote(t, s)
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
// Provided vars should override existing ones
client.GetResult = map[string]atlas.TFVar{
"foo": atlas.TFVar{
Key: "foo",
Value: "old",
},
}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
}
path := testFixturePath("push-tfvars")
args := []string{
"-var-file", path + "/terraform.tfvars",
"-vcs=false",
"-overwrite=foo",
path,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testArchiveStr(t, archivePath)
expected := []string{
".terraform/",
".terraform/terraform.tfstate",
"main.tf",
"terraform.tfvars",
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
if client.UpsertOptions.Name != "foo" {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
expectedTFVars := pushTFVars()
if !reflect.DeepEqual(client.UpsertOptions.TFVars, expectedTFVars) {
t.Logf("expected: %#v", expectedTFVars)
t.Fatalf("got: %#v", client.UpsertOptions.TFVars)
}
}
// This tests that the push command will override Atlas variables
// even if we don't have it defined locally
func TestPush_remoteOverride(t *testing.T) {
// Disable test mode so input would be asked and setup the
// input reader/writers.
test = false
defer func() { test = true }()
defaultInputReader = bytes.NewBufferString("nope\n")
defaultInputWriter = new(bytes.Buffer)
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
testStateFileRemote(t, s)
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
// Provided vars should override existing ones
client.GetResult = map[string]atlas.TFVar{
"remote": atlas.TFVar{
Key: "remote",
Value: "old",
},
}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
}
path := testFixturePath("push-tfvars")
args := []string{
"-var-file", path + "/terraform.tfvars",
"-vcs=false",
"-overwrite=remote",
"-var",
"remote=new",
path,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testArchiveStr(t, archivePath)
expected := []string{
".terraform/",
".terraform/terraform.tfstate",
"main.tf",
"terraform.tfvars",
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
if client.UpsertOptions.Name != "foo" {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
found := false
// find the "remote" var and make sure we're going to set it
for _, tfVar := range client.UpsertOptions.TFVars {
if tfVar.Key == "remote" {
found = true
if tfVar.Value != "new" {
t.Log("'remote' variable should be set to 'new'")
t.Fatalf("sending instead: %#v", tfVar)
}
}
}
if !found {
t.Fatal("'remote' variable not being sent to atlas")
}
}
// This tests that the push command prefers Atlas variables over
// local ones.
func TestPush_preferAtlas(t *testing.T) {
// Disable test mode so input would be asked and setup the
// input reader/writers.
test = false
defer func() { test = true }()
defaultInputReader = bytes.NewBufferString("nope\n")
defaultInputWriter = new(bytes.Buffer)
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
testStateFileRemote(t, s)
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
// Provided vars should override existing ones
client.GetResult = map[string]atlas.TFVar{
"foo": atlas.TFVar{
Key: "foo",
Value: "old",
},
}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
}
path := testFixturePath("push-tfvars")
args := []string{
"-var-file", path + "/terraform.tfvars",
"-vcs=false",
path,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testArchiveStr(t, archivePath)
expected := []string{
".terraform/",
".terraform/terraform.tfstate",
"main.tf",
"terraform.tfvars",
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
if client.UpsertOptions.Name != "foo" {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
// change the expected response to match our change
expectedTFVars := pushTFVars()
for i, v := range expectedTFVars {
if v.Key == "foo" {
expectedTFVars[i] = atlas.TFVar{Key: "foo", Value: "old"}
}
}
if !reflect.DeepEqual(expectedTFVars, client.UpsertOptions.TFVars) {
t.Logf("expected: %#v", expectedTFVars)
t.Fatalf("got: %#v", client.UpsertOptions.TFVars)
}
}
// This tests that the push command will send the variables in tfvars
func TestPush_tfvars(t *testing.T) {
// Disable test mode so input would be asked and setup the
// input reader/writers.
test = false
defer func() { test = true }()
defaultInputReader = bytes.NewBufferString("nope\n")
defaultInputWriter = new(bytes.Buffer)
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
testStateFileRemote(t, s)
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
}
path := testFixturePath("push-tfvars")
args := []string{
"-var-file", path + "/terraform.tfvars",
"-vcs=false",
"-var",
"bar=[1,2]",
path,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := testArchiveStr(t, archivePath)
expected := []string{
".terraform/",
".terraform/terraform.tfstate",
"main.tf",
"terraform.tfvars",
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
if client.UpsertOptions.Name != "foo" {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
//now check TFVars
tfvars := pushTFVars()
// update bar to match cli value
for i, v := range tfvars {
if v.Key == "bar" {
tfvars[i].Value = "[1, 2]"
tfvars[i].IsHCL = true
}
}
for i, expected := range tfvars {
got := client.UpsertOptions.TFVars[i]
if got != expected {
t.Logf("%2d expected: %#v", i, expected)
t.Fatalf(" got: %#v", got)
}
}
}
func TestPush_name(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
testStateFileRemote(t, s)
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
}
args := []string{
"-name", "bar",
"-vcs=false",
testFixturePath("push"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if client.UpsertOptions.Name != "bar" {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
}
func TestPush_noState(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestPush_noRemoteState(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("push-no-remote"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
state := &terraform.State{
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
},
},
},
},
},
}
statePath := testStateFile(t, state)
// Path where the archive will be "uploaded" to
archivePath := testTempFile(t)
defer os.Remove(archivePath)
client := &mockPushClient{File: archivePath}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
Ui: ui,
},
client: client,
}
args := []string{
"-vcs=false",
"-state", statePath,
td,
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
errStr := ui.ErrorWriter.String()
if !strings.Contains(errStr, "remote backend") {
t.Fatalf("bad: %s", errStr)
}
}
func TestPush_plan(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create remote state file, this should be pulled
conf, srv := testRemoteState(t, testState(), 200)
defer srv.Close()
// Persist local remote state
s := terraform.NewState()
s.Serial = 5
s.Remote = conf
testStateFileRemote(t, s)
// Create a plan
planPath := testPlanFile(t, &terraform.Plan{
Module: testModule(t, "apply"),
})
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{planPath}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func testArchiveStr(t *testing.T, path string) []string {
f, err := os.Open(path)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
// Ungzip
gzipR, err := gzip.NewReader(f)
if err != nil {
t.Fatalf("err: %s", err)
}
// Accumulator
result := make([]string, 0, 10)
// Untar
tarR := tar.NewReader(gzipR)
for {
header, err := tarR.Next()
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("err: %s", err)
}
result = append(result, header.Name)
}
sort.Strings(result)
return result
}
// we always quote map keys to be safe
func pushTFVars() []atlas.TFVar {
return []atlas.TFVar{
{Key: "bar", Value: "foo", IsHCL: false},
{Key: "baz", Value: `{
"A" = "a"
}`, IsHCL: true},
{Key: "fob", Value: `["a", "quotes \"in\" quotes"]`, IsHCL: true},
{Key: "foo", Value: "bar", IsHCL: false},
}
}
// the structure returned from the push-tfvars test fixture
func pushTFVarsMap() map[string]atlas.TFVar {
vars := make(map[string]atlas.TFVar)
for _, v := range pushTFVars() {
vars[v.Key] = v
}
return vars
}

View File

@ -73,6 +73,7 @@ func initCommands(config *Config, services *disco.Disco) {
"state": struct{}{}, // includes all subcommands
"debug": struct{}{}, // includes all subcommands
"force-unlock": struct{}{},
"push": struct{}{},
}
Commands = map[string]cli.CommandFactory{