Merge branch 'master' of github.com:hashicorp/terraform

This commit is contained in:
stack72 2016-09-04 02:08:42 +03:00
commit e3e4b6c2ac
No known key found for this signature in database
GPG Key ID: 8619A619B085CB16
25 changed files with 416 additions and 12 deletions

View File

@ -15,8 +15,10 @@ FEATURES:
* **New Resource:** `cloudstack_affinity_group` [GH-8360]
* **New Resource:** `librato_alert` [GH-8170]
* **New Resource:** `librato_service` [GH-8170]
* **New Remote State Backend:** `local` [GH-8647]
* Data source blocks can now have a count associated with them [GH-8635]
* The count of a resource can now be referenced for interpolations: `self.count` and `type.name.count` work [GH-8581]
* Provisioners now support connection using IPv6 in addition to IPv4 [GH-6616]
IMPROVEMENTS:
* core: Add wildcard (match all) support to `ignore_changes` [GH-8599]
@ -36,6 +38,7 @@ IMPROVEMENTS:
BUG FIXES:
* core: Changing a module source from file to VCS no longer errors [GH-8398]
* core: Configuration is now validated prior to input, fixing an obscure parse error when attempting to interpolate a count [GH-8591]
* core: JSON configuration with resources with a single key parse properly [GH-8485]
* core: States with duplicate modules are detected and an error is shown [GH-8463]
* core: Validate uniqueness of variables/outputs in a module [GH-8482]

View File

@ -0,0 +1,66 @@
package aws
import (
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccAWSS3BucketNotification_importBasic(t *testing.T) {
resourceName := "aws_s3_bucket_notification.notification"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3BucketNotificationDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSS3BucketConfigWithTopicNotification(acctest.RandInt()),
},
resource.TestStep{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"bucket"},
},
},
})
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3BucketNotificationDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSS3BucketConfigWithQueueNotification(acctest.RandInt()),
},
resource.TestStep{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"bucket"},
},
},
})
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSS3BucketNotificationDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSS3BucketConfigWithLambdaNotification(acctest.RandInt()),
},
resource.TestStep{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"bucket"},
},
},
})
}

View File

@ -20,6 +20,9 @@ func resourceAwsS3BucketNotification() *schema.Resource {
Read: resourceAwsS3BucketNotificationRead,
Update: resourceAwsS3BucketNotificationPut,
Delete: resourceAwsS3BucketNotificationDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"bucket": &schema.Schema{

View File

@ -16,6 +16,16 @@ func dataSourceRemoteState() *schema.Resource {
"backend": {
Type: schema.TypeString,
Required: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
if vStr, ok := v.(string); ok && vStr == "_local" {
ws = append(ws, "Use of the %q backend is now officially "+
"supported as %q. Please update your configuration to ensure "+
"compatibility with future versions of Terraform.",
"_local", "local")
}
return
},
},
"config": {
@ -38,6 +48,12 @@ func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error {
config[k] = v.(string)
}
// Don't break people using the old _local syntax - but note warning above
if backend == "_local" {
log.Println(`[INFO] Switching old (unsupported) backend "_local" to "local"`)
backend = "local"
}
// Create the client to access our remote state
log.Printf("[DEBUG] Initializing remote state client: %s", backend)
client, err := remote.NewClient(backend, config)

View File

@ -32,7 +32,7 @@ func TestState_complexOutputs(t *testing.T) {
{
Config: testAccState_complexOutputs,
Check: resource.ComposeTestCheckFunc(
testAccCheckStateValue("terraform_remote_state.foo", "backend", "_local"),
testAccCheckStateValue("terraform_remote_state.foo", "backend", "local"),
testAccCheckStateValue("terraform_remote_state.foo", "config.path", "./test-fixtures/complex_outputs.tfstate"),
testAccCheckStateValue("terraform_remote_state.foo", "computed_set.#", "2"),
testAccCheckStateValue("terraform_remote_state.foo", `map.%`, "2"),
@ -65,7 +65,7 @@ func testAccCheckStateValue(id, name, value string) resource.TestCheckFunc {
const testAccState_basic = `
data "terraform_remote_state" "foo" {
backend = "_local"
backend = "local"
config {
path = "./test-fixtures/basic.tfstate"
@ -74,7 +74,7 @@ data "terraform_remote_state" "foo" {
const testAccState_complexOutputs = `
resource "terraform_remote_state" "foo" {
backend = "_local"
backend = "local"
config {
path = "./test-fixtures/complex_outputs.tfstate"

View File

@ -2,6 +2,7 @@ package command
import (
"fmt"
"log"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
@ -27,7 +28,11 @@ const DefaultBackupExtension = ".backup"
const DefaultParallelism = 10
func validateContext(ctx *terraform.Context, ui cli.Ui) bool {
if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 {
log.Println("[INFO] Validating the context...")
ws, es := ctx.Validate()
log.Printf("[INFO] Validation result: %d warnings, %d errors", len(ws), len(es))
if len(ws) > 0 || len(es) > 0 {
ui.Output(
"There are warnings and/or errors related to your configuration. Please\n" +
"fix these before continuing.\n")

View File

@ -165,6 +165,11 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
return nil, false, fmt.Errorf("Error downloading modules: %s", err)
}
// Validate the module right away
if err := mod.Validate(); err != nil {
return nil, false, err
}
opts.Module = mod
opts.Parallelism = copts.Parallelism
opts.State = state.State()

View File

@ -395,6 +395,40 @@ func TestPlan_statePast(t *testing.T) {
}
}
func TestPlan_validate(t *testing.T) {
// This is triggered by not asking for input so we have to set this to false
test = false
defer func() { test = true }()
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(testFixturePath("plan-invalid")); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
p := testProvider()
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
actual := ui.ErrorWriter.String()
if !strings.Contains(actual, "can't reference") {
t.Fatalf("bad: %s", actual)
}
}
func TestPlan_vars(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)

View File

@ -765,8 +765,7 @@ func pushTFVars() []atlas.TFVar {
return []atlas.TFVar{
{"bar", "foo", false},
{"baz", `{
A = "a"
interp = "${file("t.txt")}"
A = "a"
}`, true},
{"fob", `["a", "quotes \"in\" quotes"]`, true},
{"foo", "bar", false},

View File

@ -0,0 +1,7 @@
resource "test_instance" "foo" {
count = 5
}
resource "test_instance" "bar" {
count = "${length(test_instance.foo.*.id)}"
}

View File

@ -7,7 +7,6 @@ variable "baz" {
default = {
"A" = "a"
interp = "${file("t.txt")}"
}
}

View File

@ -0,0 +1,17 @@
package shared
import (
"fmt"
"net"
)
// IpFormat formats the IP correctly, so we don't provide IPv6 address in an IPv4 format during node communication. We return the ip parameter as is if it's an IPv4 address or a hostname.
func IpFormat(ip string) string {
ipObj := net.ParseIP(ip)
// Return the ip/host as is if it's either a hostname or an IPv4 address.
if ipObj == nil || ipObj.To4() != nil {
return ip
}
return fmt.Sprintf("[%s]", ip)
}

View File

@ -0,0 +1,26 @@
package shared
import (
"testing"
)
func TestIpFormatting_Ipv4(t *testing.T) {
formatted := IpFormat("127.0.0.1")
if formatted != "127.0.0.1" {
t.Fatal("expected", "127.0.0.1", "got", formatted)
}
}
func TestIpFormatting_Hostname(t *testing.T) {
formatted := IpFormat("example.com")
if formatted != "example.com" {
t.Fatal("expected", "example.com", "got", formatted)
}
}
func TestIpFormatting_Ipv6(t *testing.T) {
formatted := IpFormat("::1")
if formatted != "[::1]" {
t.Fatal("expected", "[::1]", "got", formatted)
}
}

View File

@ -8,6 +8,7 @@ import (
"os"
"time"
"github.com/hashicorp/terraform/communicator/shared"
"github.com/hashicorp/terraform/helper/pathorcontents"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
@ -84,6 +85,11 @@ func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
if connInfo.User == "" {
connInfo.User = DefaultUser
}
// Format the host if needed.
// Needed for IPv6 support.
connInfo.Host = shared.IpFormat(connInfo.Host)
if connInfo.Port == 0 {
connInfo.Port = DefaultPort
}
@ -107,6 +113,10 @@ func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
// Default all bastion config attrs to their non-bastion counterparts
if connInfo.BastionHost != "" {
// Format the bastion host if needed.
// Needed for IPv6 support.
connInfo.BastionHost = shared.IpFormat(connInfo.BastionHost)
if connInfo.BastionUser == "" {
connInfo.BastionUser = connInfo.User
}

View File

@ -66,6 +66,68 @@ func TestProvisioner_connInfo(t *testing.T) {
}
}
func TestProvisioner_connInfoIpv6(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"host": "::1",
"port": "22",
"timeout": "30s",
"bastion_host": "::1",
},
},
}
conf, err := parseConnectionInfo(r)
if err != nil {
t.Fatalf("err: %v", err)
}
if conf.Host != "[::1]" {
t.Fatalf("bad: %v", conf)
}
if conf.BastionHost != "[::1]" {
t.Fatalf("bad %v", conf)
}
}
func TestProvisioner_connInfoHostname(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"private_key": "someprivatekeycontents",
"host": "example.com",
"port": "22",
"timeout": "30s",
"bastion_host": "example.com",
},
},
}
conf, err := parseConnectionInfo(r)
if err != nil {
t.Fatalf("err: %v", err)
}
if conf.Host != "example.com" {
t.Fatalf("bad: %v", conf)
}
if conf.BastionHost != "example.com" {
t.Fatalf("bad %v", conf)
}
}
func TestProvisioner_connInfoLegacy(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{

View File

@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/hashicorp/terraform/communicator/shared"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
)
@ -72,6 +73,11 @@ func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
if connInfo.User == "" {
connInfo.User = DefaultUser
}
// Format the host if needed.
// Needed for IPv6 support.
connInfo.Host = shared.IpFormat(connInfo.Host)
if connInfo.Port == 0 {
connInfo.Port = DefaultPort
}

View File

@ -49,6 +49,92 @@ func TestProvisioner_connInfo(t *testing.T) {
}
}
func TestProvisioner_connInfoIpv6(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "Administrator",
"password": "supersecret",
"host": "::1",
"port": "5985",
"https": "true",
"timeout": "30s",
},
},
}
conf, err := parseConnectionInfo(r)
if err != nil {
t.Fatalf("err: %v", err)
}
if conf.User != "Administrator" {
t.Fatalf("expected: %v: got: %v", "Administrator", conf)
}
if conf.Password != "supersecret" {
t.Fatalf("expected: %v: got: %v", "supersecret", conf)
}
if conf.Host != "[::1]" {
t.Fatalf("expected: %v: got: %v", "[::1]", conf)
}
if conf.Port != 5985 {
t.Fatalf("expected: %v: got: %v", 5985, conf)
}
if conf.HTTPS != true {
t.Fatalf("expected: %v: got: %v", true, conf)
}
if conf.Timeout != "30s" {
t.Fatalf("expected: %v: got: %v", "30s", conf)
}
if conf.ScriptPath != DefaultScriptPath {
t.Fatalf("expected: %v: got: %v", DefaultScriptPath, conf)
}
}
func TestProvisioner_connInfoHostname(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "Administrator",
"password": "supersecret",
"host": "example.com",
"port": "5985",
"https": "true",
"timeout": "30s",
},
},
}
conf, err := parseConnectionInfo(r)
if err != nil {
t.Fatalf("err: %v", err)
}
if conf.User != "Administrator" {
t.Fatalf("expected: %v: got: %v", "Administrator", conf)
}
if conf.Password != "supersecret" {
t.Fatalf("expected: %v: got: %v", "supersecret", conf)
}
if conf.Host != "example.com" {
t.Fatalf("expected: %v: got: %v", "example.com", conf)
}
if conf.Port != 5985 {
t.Fatalf("expected: %v: got: %v", 5985, conf)
}
if conf.HTTPS != true {
t.Fatalf("expected: %v: got: %v", true, conf)
}
if conf.Timeout != "30s" {
t.Fatalf("expected: %v: got: %v", "30s", conf)
}
if conf.ScriptPath != DefaultScriptPath {
t.Fatalf("expected: %v: got: %v", DefaultScriptPath, conf)
}
}
func TestProvisioner_formatDuration(t *testing.T) {
cases := map[string]struct {
InstanceState *terraform.InstanceState

View File

@ -195,6 +195,13 @@ func TestConfigValidate_countResourceVar(t *testing.T) {
}
}
func TestConfigValidate_countResourceVarMulti(t *testing.T) {
c := testConfig(t, "validate-count-resource-var-multi")
if err := c.Validate(); err == nil {
t.Fatal("should not be valid")
}
}
func TestConfigValidate_countUserVar(t *testing.T) {
c := testConfig(t, "validate-count-user-var")
if err := c.Validate(); err != nil {

View File

@ -0,0 +1,5 @@
resource "aws_instance" "foo" {}
resource "aws_instance" "web" {
count = "${length(aws_instance.foo.*.bar)}"
}

View File

@ -78,7 +78,7 @@ func testStepImportState(
// Find the existing resource
var oldR *terraform.ResourceState
for _, r2 := range old {
if r2.Primary != nil && r2.Primary.ID == r.Primary.ID {
if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type {
oldR = r2
break
}

View File

@ -36,16 +36,14 @@ func NewClient(t string, conf map[string]string) (Client, error) {
// BuiltinClients is the list of built-in clients that can be used with
// NewClient.
var BuiltinClients = map[string]Factory{
"artifactory": artifactoryFactory,
"atlas": atlasFactory,
"azure": azureFactory,
"consul": consulFactory,
"etcd": etcdFactory,
"gcs": gcsFactory,
"http": httpFactory,
"local": fileFactory,
"s3": s3Factory,
"swift": swiftFactory,
"artifactory": artifactoryFactory,
// This is used for development purposes only.
"_local": fileFactory,
}

View File

@ -234,3 +234,10 @@ The `lambda_function` notification configuration supports the following:
* `filter_prefix` - (Optional) Specifies object key name prefix.
* `filter_suffix` - (Optional) Specifies object key name suffix.
## Import
S3 bucket notification can be imported using the `bucket`, e.g.
```
$ terraform import aws_s3_bucket_notification.bucket_notification bucket-name
```

View File

@ -13,6 +13,10 @@ is created. This invokes a process on the machine running Terraform, not on
the resource. See the `remote-exec` [provisioner](/docs/provisioners/remote-exec.html)
to run commands on the resource.
Note that even though the resource will be ifully created when the provisioner is run,
there is no guarantee that it will be in an operable state - for example system services
such as `sshd` may not be started yet on compute resources.
## Example usage
```

View File

@ -0,0 +1,36 @@
---
layout: "remotestate"
page_title: "Remote State Backend: local"
sidebar_current: "docs-state-remote-local"
description: |-
Remote state stored using the local file system.
---
# local
Remote state backend that uses the local file system.
## Example Usage
```
terraform remote config \
-backend=local \
-backend-config="path=/path/to/terraform.tfstate"
```
## Example Reference
```
data "terraform_remote_state" "foo" {
backend = "local"
config {
path = "${path.module}/../../terraform.tfstate"
}
}
```
## Configuration variables
The following configuration options are supported:
* `path` - (Required) The path to the `tfstate` file.

View File

@ -34,6 +34,9 @@
<li<%= sidebar_current("docs-state-remote-http") %>>
<a href="/docs/state/remote/http.html">http</a>
</li>
<li<%= sidebar_current("docs-state-remote-local") %>>
<a href="/docs/state/remote/local.html">local</a>
</li>
<li<%= sidebar_current("docs-state-remote-s3") %>>
<a href="/docs/state/remote/s3.html">s3</a>
</li>