Merge pull request #1294 from hashicorp/f-push

command/push: for remote TF configuration runs
This commit is contained in:
Mitchell Hashimoto 2015-03-24 17:55:40 -07:00
commit 28ecdacdf9
25 changed files with 1040 additions and 8 deletions

View File

@ -148,6 +148,27 @@ func testStateFileDefault(t *testing.T, s *terraform.State) string {
return DefaultStateFilename
}
// testStateFileRemote writes the state out to the remote statefile
// in the cwd. Use `testCwd` to change into a temp cwd.
func testStateFileRemote(t *testing.T, s *terraform.State) string {
path := filepath.Join(DefaultDataDir, DefaultStateFilename)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatalf("err: %s", err)
}
f, err := os.Create(path)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
if err := terraform.WriteState(s, f); err != nil {
t.Fatalf("err: %s", err)
}
return path
}
// testStateOutput tests that the state at the given path contains
// the expected state string.
func testStateOutput(t *testing.T, path string, expected string) {

View File

@ -138,11 +138,7 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
return nil, false, fmt.Errorf("Error loading config: %s", err)
}
dataDir := DefaultDataDirectory
if m.dataDir != "" {
dataDir = m.dataDir
}
err = mod.Load(m.moduleStorage(dataDir), copts.GetMode)
err = mod.Load(m.moduleStorage(m.DataDir()), copts.GetMode)
if err != nil {
return nil, false, fmt.Errorf("Error downloading modules: %s", err)
}
@ -153,6 +149,16 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
return ctx, false, nil
}
// DataDir returns the directory where local data will be stored.
func (m *Meta) DataDir() string {
dataDir := DefaultDataDirectory
if m.dataDir != "" {
dataDir = m.dataDir
}
return dataDir
}
// InputMode returns the type of input we should ask for in the form of
// terraform.InputMode which is passed directly to Context.Input.
func (m *Meta) InputMode() terraform.InputMode {
@ -164,6 +170,7 @@ func (m *Meta) InputMode() terraform.InputMode {
mode |= terraform.InputModeProvider
if len(m.variables) == 0 && m.autoKey == "" {
mode |= terraform.InputModeVar
mode |= terraform.InputModeVarUnset
}
return mode
@ -205,7 +212,7 @@ func (m *Meta) StateOpts() *StateOpts {
if localPath == "" {
localPath = DefaultStateFilename
}
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
remotePath := filepath.Join(m.DataDir(), DefaultStateFilename)
return &StateOpts{
LocalPath: localPath,

View File

@ -65,7 +65,7 @@ func TestMetaInputMode(t *testing.T) {
t.Fatalf("err: %s", err)
}
if m.InputMode() != terraform.InputModeStd {
if m.InputMode() != terraform.InputModeStd|terraform.InputModeVarUnset {
t.Fatalf("bad: %#v", m.InputMode())
}
}

312
command/push.go Normal file
View File

@ -0,0 +1,312 @@
package command
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/atlas-go/archive"
"github.com/hashicorp/atlas-go/v1"
)
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
args = c.Meta.process(args, false)
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.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// The pwd is used for the configuration path if one is not given
pwd, err := os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
return 1
}
// Get the path to the configuration depending on the args.
var configPath string
args = cmdFlags.Args()
if len(args) > 1 {
c.Ui.Error("The apply command expects at most one argument.")
cmdFlags.Usage()
return 1
} else if len(args) == 1 {
configPath = args[0]
} else {
configPath = pwd
}
// Verify the state is remote, we can't push without a remote state
s, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
return 1
}
if !s.State().IsRemote() {
c.Ui.Error(
"Remote state 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\n" +
"configure remote state, use the `terraform remote config`\n" +
"command.")
return 1
}
// Build the context based on the arguments given
ctx, planned, err := c.Context(contextOpts{
Path: configPath,
StatePath: c.Meta.statePath,
})
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if planned {
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
}
// 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
}
}
if atlasToken != "" {
client.Token = atlasToken
}
c.client = &atlasPushClient{Client: client}
}
// Get the variables we might already have
vars, err := c.client.Get(name)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error looking up previously pushed configuration: %s", err))
return 1
}
for k, v := range vars {
ctx.SetVariable(k, v)
}
// 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
}
// 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: c.DataDir(),
},
}
if !moduleUpload {
// If we're not uploading modules, then exclude the modules dir.
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
}
// Upsert!
opts := &pushUpsertOptions{
Name: name,
Archive: archiveR,
Variables: ctx.Variables(),
}
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)))
return 0
}
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.
-vcs=true If true (default), push will upload only files
comitted to your VCS, if detected.
`
return strings.TrimSpace(helpText)
}
func (c *PushCommand) Synopsis() string {
return "Upload this Terraform module to Atlas to run"
}
// pushClient is implementd internally to control where pushes go. This is
// either to Atlas or a mock for testing.
type pushClient interface {
Get(string) (map[string]string, error)
Upsert(*pushUpsertOptions) (int, error)
}
type pushUpsertOptions struct {
Name string
Archive *archive.Archive
Variables map[string]string
}
type atlasPushClient struct {
Client *atlas.Client
}
func (c *atlasPushClient) Get(name string) (map[string]string, 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
}
var variables map[string]string
if version != nil {
variables = version.Variables
}
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{
Variables: opts.Variables,
}
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]string
GetError error
UpsertCalled bool
UpsertOptions *pushUpsertOptions
UpsertVersion int
UpsertError error
}
func (c *mockPushClient) Get(name string) (map[string]string, 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
}

337
command/push_test.go Normal file
View File

@ -0,0 +1,337 @@
package command
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"os"
"reflect"
"sort"
"testing"
"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{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
client: client,
}
args := []string{
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]string)
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_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{
ContextOpts: testCtxConfig(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{
testFixturePath("push-input"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
variables := map[string]string{
"foo": "foo",
}
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
}
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]string{"foo": "bar"},
}
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(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{
testFixturePath("push-input-partial"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
variables := map[string]string{
"foo": "bar",
"bar": "foo",
}
if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) {
t.Fatalf("bad: %#v", client.UpsertOptions)
}
}
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{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
client: client,
}
args := []string{
"-name", "bar",
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{
ContextOpts: testCtxConfig(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) {
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)
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
Ui: ui,
},
}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
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{
ContextOpts: testCtxConfig(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
}

View File

@ -0,0 +1,8 @@
variable "foo" {}
variable "bar" {}
resource "test_instance" "foo" {}
atlas {
name = "foo"
}

View File

@ -0,0 +1,7 @@
variable "foo" {}
resource "test_instance" "foo" {}
atlas {
name = "foo"
}

View File

@ -0,0 +1,5 @@
resource "aws_instance" "foo" {}
atlas {
name = "foo"
}

View File

@ -80,6 +80,12 @@ func init() {
}, nil
},
"push": func() (cli.Command, error) {
return &command.PushCommand{
Meta: meta,
}, nil
},
"refresh": func() (cli.Command, error) {
return &command.RefreshCommand{
Meta: meta,

View File

@ -21,6 +21,7 @@ func Append(c1, c2 *Config) (*Config, error) {
c.unknownKeys = append(c.unknownKeys, k)
}
}
for _, k := range c2.unknownKeys {
_, present := unknowns[k]
if !present {
@ -29,6 +30,11 @@ func Append(c1, c2 *Config) (*Config, error) {
}
}
c.Atlas = c1.Atlas
if c2.Atlas != nil {
c.Atlas = c2.Atlas
}
if len(c1.Modules) > 0 || len(c2.Modules) > 0 {
c.Modules = make(
[]*Module, 0, len(c1.Modules)+len(c2.Modules))

View File

@ -12,6 +12,9 @@ func TestAppend(t *testing.T) {
}{
{
&Config{
Atlas: &AtlasConfig{
Name: "foo",
},
Modules: []*Module{
&Module{Name: "foo"},
},
@ -32,6 +35,9 @@ func TestAppend(t *testing.T) {
},
&Config{
Atlas: &AtlasConfig{
Name: "bar",
},
Modules: []*Module{
&Module{Name: "bar"},
},
@ -52,6 +58,9 @@ func TestAppend(t *testing.T) {
},
&Config{
Atlas: &AtlasConfig{
Name: "bar",
},
Modules: []*Module{
&Module{Name: "foo"},
&Module{Name: "bar"},

View File

@ -28,6 +28,7 @@ type Config struct {
// any meaningful directory.
Dir string
Atlas *AtlasConfig
Modules []*Module
ProviderConfigs []*ProviderConfig
Resources []*Resource
@ -39,6 +40,13 @@ type Config struct {
unknownKeys []string
}
// AtlasConfig is the configuration for building in HashiCorp's Atlas.
type AtlasConfig struct {
Name string
Include []string
Exclude []string
}
// Module is a module used within a configuration.
//
// This does not represent a module itself, this represents a module

View File

@ -17,6 +17,7 @@ type hclConfigurable struct {
func (t *hclConfigurable) Config() (*Config, error) {
validKeys := map[string]struct{}{
"atlas": struct{}{},
"module": struct{}{},
"output": struct{}{},
"provider": struct{}{},
@ -70,6 +71,15 @@ func (t *hclConfigurable) Config() (*Config, error) {
}
}
// Get Atlas configuration
if atlas := t.Object.Get("atlas", false); atlas != nil {
var err error
config.Atlas, err = loadAtlasHcl(atlas)
if err != nil {
return nil, err
}
}
// Build the modules
if modules := t.Object.Get("module", false); modules != nil {
var err error
@ -187,6 +197,19 @@ func loadFileHcl(root string) (configurable, []string, error) {
return result, nil, nil
}
// Given a handle to a HCL object, this transforms it into the Atlas
// configuration.
func loadAtlasHcl(obj *hclobj.Object) (*AtlasConfig, error) {
var config AtlasConfig
if err := hcl.DecodeObject(&config, obj); err != nil {
return nil, fmt.Errorf(
"Error reading atlas config: %s",
err)
}
return &config, nil
}
// Given a handle to a HCL object, this recurses into the structure
// and pulls out a list of modules.
//

View File

@ -2,6 +2,7 @@ package config
import (
"path/filepath"
"reflect"
"strings"
"testing"
)
@ -57,6 +58,11 @@ func TestLoadBasic(t *testing.T) {
t.Fatalf("bad: %#v", c.Dir)
}
expectedAtlas := &AtlasConfig{Name: "mitchellh/foo"}
if !reflect.DeepEqual(c.Atlas, expectedAtlas) {
t.Fatalf("bad: %#v", c.Atlas)
}
actual := variablesStr(c.Variables)
if actual != strings.TrimSpace(basicVariablesStr) {
t.Fatalf("bad:\n%s", actual)
@ -132,6 +138,11 @@ func TestLoadBasic_json(t *testing.T) {
t.Fatalf("bad: %#v", c.Dir)
}
expectedAtlas := &AtlasConfig{Name: "mitchellh/foo"}
if !reflect.DeepEqual(c.Atlas, expectedAtlas) {
t.Fatalf("bad: %#v", c.Atlas)
}
actual := variablesStr(c.Variables)
if actual != strings.TrimSpace(basicVariablesStr) {
t.Fatalf("bad:\n%s", actual)

View File

@ -25,6 +25,13 @@ func Merge(c1, c2 *Config) (*Config, error) {
}
}
// Merge Atlas configuration. This is a dumb one overrides the other
// sort of merge.
c.Atlas = c1.Atlas
if c2.Atlas != nil {
c.Atlas = c2.Atlas
}
// NOTE: Everything below is pretty gross. Due to the lack of generics
// in Go, there is some hoop-jumping involved to make this merging a
// little more test-friendly and less repetitive. Ironically, making it

View File

@ -13,6 +13,9 @@ func TestMerge(t *testing.T) {
// Normal good case.
{
&Config{
Atlas: &AtlasConfig{
Name: "foo",
},
Modules: []*Module{
&Module{Name: "foo"},
},
@ -33,6 +36,9 @@ func TestMerge(t *testing.T) {
},
&Config{
Atlas: &AtlasConfig{
Name: "bar",
},
Modules: []*Module{
&Module{Name: "bar"},
},
@ -53,6 +59,9 @@ func TestMerge(t *testing.T) {
},
&Config{
Atlas: &AtlasConfig{
Name: "bar",
},
Modules: []*Module{
&Module{Name: "foo"},
&Module{Name: "bar"},

View File

@ -49,3 +49,7 @@ resource "aws_instance" "db" {
output "web_ip" {
value = "${aws_instance.web.private_ip}"
}
atlas {
name = "mitchellh/foo"
}

View File

@ -63,5 +63,9 @@
"web_ip": {
"value": "${aws_instance.web.private_ip}"
}
},
"atlas": {
"name": "mitchellh/foo"
}
}

View File

@ -16,9 +16,12 @@ import (
type InputMode byte
const (
// InputModeVar asks for variables
// InputModeVar asks for all variables
InputModeVar InputMode = 1 << iota
// InputModeVarUnset asks for variables which are not set yet
InputModeVarUnset
// InputModeProvider asks for provider variables
InputModeProvider
@ -154,6 +157,14 @@ func (c *Context) Input(mode InputMode) error {
}
sort.Strings(names)
for _, n := range names {
// If we only care about unset variables, then if the variabel
// is set, continue on.
if mode&InputModeVarUnset != 0 {
if _, ok := c.variables[n]; ok {
continue
}
}
v := m[n]
switch v.Type() {
case config.VariableTypeMap:
@ -365,6 +376,23 @@ func (c *Context) Validate() ([]string, []error) {
return walker.ValidationWarnings, rerrs.Errors
}
// Module returns the module tree associated with this context.
func (c *Context) Module() *module.Tree {
return c.module
}
// Variables will return the mapping of variables that were defined
// for this Context. If Input was called, this mapping may be different
// than what was given.
func (c *Context) Variables() map[string]string {
return c.variables
}
// SetVariable sets a variable after a context has already been built.
func (c *Context) SetVariable(k, v string) {
c.variables[k] = v
}
func (c *Context) acquireRun() chan<- struct{} {
c.l.Lock()
defer c.l.Unlock()

View File

@ -2758,6 +2758,48 @@ func TestContext2Input_varOnly(t *testing.T) {
}
}
func TestContext2Input_varOnlyUnset(t *testing.T) {
input := new(MockUIInput)
m := testModule(t, "input-vars-unset")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Variables: map[string]string{
"foo": "foovalue",
},
UIInput: input,
})
input.InputReturnMap = map[string]string{
"var.foo": "nope",
"var.bar": "baz",
}
if err := ctx.Input(InputModeVar | InputModeVarUnset); err != nil {
t.Fatalf("err: %s", err)
}
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
actualStr := strings.TrimSpace(state.String())
expectedStr := strings.TrimSpace(testTerraformInputVarOnlyUnsetStr)
if actualStr != expectedStr {
t.Fatalf("bad: \n%s", actualStr)
}
}
func TestContext2Apply(t *testing.T) {
m := testModule(t, "apply-good")
p := testProvider("aws")

View File

@ -150,6 +150,14 @@ aws_instance.foo:
type = aws_instance
`
const testTerraformInputVarOnlyUnsetStr = `
aws_instance.foo:
ID = foo
bar = baz
foo = foovalue
type = aws_instance
`
const testTerraformInputVarsStr = `
aws_instance.bar:
ID = foo

View File

@ -0,0 +1,7 @@
variable "foo" {}
variable "bar" {}
resource "aws_instance" "foo" {
foo = "${var.foo}"
bar = "${var.bar}"
}

View File

@ -0,0 +1,97 @@
---
layout: "docs"
page_title: "Command: push"
sidebar_current: "docs-commands-push"
description: |-
The `terraform push` command is used to upload the Terraform configuration to HashiCorp's Atlas service for automatically managing your infrastructure in the cloud.
---
# Command: push
The `terraform push` command uploads your Terraform configuration to
be managed by HashiCorp's [Atlas](https://atlas.hashicorp.com).
By uploading your configuration to Atlas, Atlas can automatically run
Terraform for you, will save all state transitions, will save plans,
and will keep a history of all Terraform runs.
This makes it significantly easier to use Terraform as a team: team
members modify the Terraform configurations locally and continue to
use normal version control. When the Terraform configurations are ready
to be run, they are pushed to Atlas, and any member of your team can
run Terraform with the push of a button.
Atlas can also be used to set ACLs on who can run Terraform, and a
future update of Atlas will allow parallel Terraform runs and automatically
perform infrastructure locking so only one run is modifying the same
infrastructure at a time.
## Usage
Usage: `terraform push [options] [path]`
The `path` argument is the same as for the
[apply](/docs/commands/apply.html) command.
The command-line flags are all optional. The list of available flags are:
* `-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](/docs/modules/index.html)
being used are all locked at their current checkout and uploaded
completely to Atlas. This prevents Atlas from running `terraform get`
for you.
* `-name=<name>` - Name of the infrastructure configuration in Atlas.
The format of this is: "username/name" so that you can upload
configurations not just to your account but to other accounts and
organizations. This setting can also be set in the configuration
in the
[Atlas section](/docs/configuration/atlas.html).
* `-no-color` - Disables output with coloring
* `-token=<token>` - Atlas API token to use to authorize the upload.
If blank or unspecified, the `ATLAS_TOKEN` environmental variable
will be used.
* `-vcs=true` - If true (default), then Terraform will detect if a VCS
is in use, such as Git, and will only upload files that are comitted to
version control. If no version control system is detected, Terraform will
upload all files in `path` (parameter to the command).
## Packaged Files
The files that are uploaded and packaged with a `push` are all the
files in the `path` given as the parameter to the command, recursively.
By default (unless `-vcs=false` is specified), Terraform will automatically
detect when a VCS such as Git is being used, and in that case will only
upload the files that are comitted. Because of this built-in intelligence,
you don't have to worry about excluding folders such as ".git" or ".hg" usually.
If Terraform doesn't detect a VCS, it will upload all files.
The reason Terraform uploads all of these files is because Terraform
cannot know what is and isn't being used for provisioning, so it uploads
all the files to be safe. To exclude certain files, specify the `-exclude`
flag when pushing, or specify the `exclude` parameter in the
[Atlas configuration section](/docs/configuration/atlas.html).
## Remote State Requirement
`terraform push` requires that
[remote state](/docs/commands/remote-config.html)
is enabled. The reasoning for this is simple: `terraform push` sends your
configuration to be managed remotely. For it to keep the state in sync
and for you to be able to easily access that state, remote state must
be enabled instead of juggling local files.
While `terraform push` sends your configuration to be managed by Atlas,
the remote state backend _does not_ have to be Atlas. It can be anything
as long as it is accessible by the public internet, since Atlas will need
to be able to communicate to it.
**Warning:** The credentials for accessing the remote state will be
sent up to Atlas as well. Therefore, we recommend you use access keys
that are restricted if possible.

View File

@ -0,0 +1,58 @@
---
layout: "docs"
page_title: "Configuring Atlas"
sidebar_current: "docs-config-atlas"
description: |-
Atlas is the ideal way to use Terraform in a team environment. Atlas will run Terraform for you, safely handle parallelization across different team members, save run history along with plans, and more.
---
# Atlas Configuration
Terraform can be configured to be able to upload to HashiCorp's
[Atlas](https://atlas.hashicorp.com). This configuration doesn't change
the behavior of Terraform itself, it only configures your Terraform
configuration to support being uploaded to Atlas via the
[push command](/docs/commands/push.html).
For more information on the benefits of uploading your Terraform
configuration to Atlas, please see the
[push command documentation](/docs/commands/push.html).
This page assumes you're familiar with the
[configuration syntax](/docs/configuration/syntax.html)
already.
## Example
Atlas configuration looks like the following:
```
atlas {
name = "mitchellh/production-example"
}
```
## Description
The `atlas` block configures the settings when Terraform is
[pushed](/docs/commands/push.html) to Atlas. Only one `atlas` block
is allowed.
Within the block (the `{ }`) is configuration for Atlas uploading.
No keys are required, but the key typically set is `name`.
**No value within the `atlas` block can use interpolations.** Due
to the nature of this configuration, interpolations are not possible.
If you want to parameterize these settings, use the Atlas block to
set defaults, then use the command-line flags of the
[push command](/docs/commands/push.html) to override.
## Syntax
The full syntax is:
```
atlas {
name = VALUE
}
```

View File

@ -45,6 +45,10 @@
<a href="/docs/configuration/modules.html">Modules</a>
</li>
<li<%= sidebar_current("docs-config-atlas") %>>
<a href="/docs/configuration/atlas.html">Atlas</a>
</li>
</ul>
</li>
@ -79,6 +83,10 @@
<a href="/docs/commands/plan.html">plan</a>
</li>
<li<%= sidebar_current("docs-commands-push") %>>
<a href="/docs/commands/push.html">push</a>
</li>
<li<%= sidebar_current("docs-commands-refresh") %>>
<a href="/docs/commands/refresh.html">refresh</a>
</li>