From bb84dc75b764c295bfd481eba62956027e7ed10e Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 4 Aug 2016 11:20:11 -0400 Subject: [PATCH 01/13] Fix improper wait group usage in test --- builtin/providers/template/datasource_template_file_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/providers/template/datasource_template_file_test.go b/builtin/providers/template/datasource_template_file_test.go index 7b13f69e6..43dda582c 100644 --- a/builtin/providers/template/datasource_template_file_test.go +++ b/builtin/providers/template/datasource_template_file_test.go @@ -122,8 +122,8 @@ func TestValidateVarsAttribute(t *testing.T) { func TestTemplateSharedMemoryRace(t *testing.T) { var wg sync.WaitGroup for i := 0; i < 100; i++ { - go func(wg *sync.WaitGroup, t *testing.T, i int) { - wg.Add(1) + wg.Add(1) + go func(t *testing.T, i int) { out, err := execute("don't panic!", map[string]interface{}{}) if err != nil { t.Fatalf("err: %s", err) @@ -132,7 +132,7 @@ func TestTemplateSharedMemoryRace(t *testing.T) { t.Fatalf("bad output: %s", out) } wg.Done() - }(&wg, t, i) + }(t, i) } wg.Wait() } From 895383ac92036373f151f9ffb022c9aa50035edd Mon Sep 17 00:00:00 2001 From: Davide Agnello Date: Wed, 3 Aug 2016 11:53:59 -0700 Subject: [PATCH 02/13] vSphere file resource: extending functionality to copy files in vSphere * Enables copy of files within vSphere * Can copy files between different datacenters and datastores * Update can move uploaded or copied files between datacenters and datastores * Preserves original functionality for backward compatibility --- .../vsphere/resource_vsphere_file.go | 167 ++++++++++++++---- .../vsphere/resource_vsphere_file_test.go | 153 +++++++++++++++- .../providers/vsphere/r/file.html.markdown | 37 +++- 3 files changed, 308 insertions(+), 49 deletions(-) diff --git a/builtin/providers/vsphere/resource_vsphere_file.go b/builtin/providers/vsphere/resource_vsphere_file.go index 55d3d6cbb..c8afe05d9 100644 --- a/builtin/providers/vsphere/resource_vsphere_file.go +++ b/builtin/providers/vsphere/resource_vsphere_file.go @@ -3,6 +3,7 @@ package vsphere import ( "fmt" "log" + "strings" "github.com/hashicorp/terraform/helper/schema" "github.com/vmware/govmomi" @@ -13,10 +14,14 @@ import ( ) type file struct { - datacenter string - datastore string - sourceFile string - destinationFile string + sourceDatacenter string + datacenter string + sourceDatastore string + datastore string + sourceFile string + destinationFile string + createDirectories bool + copyFile bool } func resourceVSphereFile() *schema.Resource { @@ -30,10 +35,20 @@ func resourceVSphereFile() *schema.Resource { "datacenter": { Type: schema.TypeString, Optional: true, + }, + + "source_datacenter": { + Type: schema.TypeString, + Optional: true, ForceNew: true, }, "datastore": { + Type: schema.TypeString, + Required: true, + }, + + "source_datastore": { Type: schema.TypeString, Optional: true, ForceNew: true, @@ -49,6 +64,11 @@ func resourceVSphereFile() *schema.Resource { Type: schema.TypeString, Required: true, }, + + "create_directories": { + Type: schema.TypeBool, + Optional: true, + }, }, } } @@ -60,10 +80,20 @@ func resourceVSphereFileCreate(d *schema.ResourceData, meta interface{}) error { f := file{} + if v, ok := d.GetOk("source_datacenter"); ok { + f.sourceDatacenter = v.(string) + f.copyFile = true + } + if v, ok := d.GetOk("datacenter"); ok { f.datacenter = v.(string) } + if v, ok := d.GetOk("source_datastore"); ok { + f.sourceDatastore = v.(string) + f.copyFile = true + } + if v, ok := d.GetOk("datastore"); ok { f.datastore = v.(string) } else { @@ -82,6 +112,10 @@ func resourceVSphereFileCreate(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("destination_file argument is required") } + if v, ok := d.GetOk("create_directories"); ok { + f.createDirectories = v.(bool) + } + err := createFile(client, &f) if err != nil { return err @@ -108,16 +142,53 @@ func createFile(client *govmomi.Client, f *file) error { return fmt.Errorf("error %s", err) } - dsurl, err := ds.URL(context.TODO(), dc, f.destinationFile) - if err != nil { - return err + if f.copyFile { + // Copying file from withing vSphere + source_dc, err := finder.Datacenter(context.TODO(), f.sourceDatacenter) + if err != nil { + return fmt.Errorf("error %s", err) + } + finder = finder.SetDatacenter(dc) + + source_ds, err := getDatastore(finder, f.sourceDatastore) + if err != nil { + return fmt.Errorf("error %s", err) + } + + fm := object.NewFileManager(client.Client) + if f.createDirectories { + directoryPathIndex := strings.LastIndex(f.destinationFile, "/") + path := f.destinationFile[0:directoryPathIndex] + err = fm.MakeDirectory(context.TODO(), ds.Path(path), dc, true) + if err != nil { + return fmt.Errorf("error %s", err) + } + } + task, err := fm.CopyDatastoreFile(context.TODO(), source_ds.Path(f.sourceFile), source_dc, ds.Path(f.destinationFile), dc, true) + + if err != nil { + return fmt.Errorf("error %s", err) + } + + _, err = task.WaitForResult(context.TODO(), nil) + if err != nil { + return fmt.Errorf("error %s", err) + } + + } else { + // Uploading file to vSphere + dsurl, err := ds.URL(context.TODO(), dc, f.destinationFile) + if err != nil { + return fmt.Errorf("error %s", err) + } + + p := soap.DefaultUpload + err = client.Client.UploadFile(f.sourceFile, dsurl, &p) + if err != nil { + return fmt.Errorf("error %s", err) + } } - p := soap.DefaultUpload - err = client.Client.UploadFile(f.sourceFile, dsurl, &p) - if err != nil { - return fmt.Errorf("error %s", err) - } return nil } @@ -126,10 +197,18 @@ func resourceVSphereFileRead(d *schema.ResourceData, meta interface{}) error { log.Printf("[DEBUG] reading file: %#v", d) f := file{} + if v, ok := d.GetOk("source_datacenter"); ok { + f.sourceDatacenter = v.(string) + } + if v, ok := d.GetOk("datacenter"); ok { f.datacenter = v.(string) } + if v, ok := d.GetOk("source_datastore"); ok { + f.sourceDatastore = v.(string) + } + if v, ok := d.GetOk("datastore"); ok { f.datastore = v.(string) } else { @@ -179,57 +258,69 @@ func resourceVSphereFileRead(d *schema.ResourceData, meta interface{}) error { func resourceVSphereFileUpdate(d *schema.ResourceData, meta interface{}) error { log.Printf("[DEBUG] updating file: %#v", d) - if d.HasChange("destination_file") { - oldDestinationFile, newDestinationFile := d.GetChange("destination_file") - f := file{} - if v, ok := d.GetOk("datacenter"); ok { - f.datacenter = v.(string) - } - - if v, ok := d.GetOk("datastore"); ok { - f.datastore = v.(string) + if d.HasChange("destination_file") || d.HasChange("datacenter") || d.HasChange("datastore") { + // File needs to be moved, get old and new destination changes + var oldDataceneter, newDatacenter, oldDatastore, newDatastore, oldDestinationFile, newDestinationFile string + if d.HasChange("datacenter") { + tmpOldDataceneter, tmpNewDatacenter := d.GetChange("datacenter") + oldDataceneter = tmpOldDataceneter.(string) + newDatacenter = tmpNewDatacenter.(string) } else { - return fmt.Errorf("datastore argument is required") + if v, ok := d.GetOk("datacenter"); ok { + oldDataceneter = v.(string) + newDatacenter = oldDataceneter + } } - - if v, ok := d.GetOk("source_file"); ok { - f.sourceFile = v.(string) + if d.HasChange("datastore") { + tmpOldDatastore, tmpNewDatastore := d.GetChange("datastore") + oldDatastore = tmpOldDatastore.(string) + newDatastore = tmpNewDatastore.(string) } else { - return fmt.Errorf("source_file argument is required") + oldDatastore = d.Get("datastore").(string) + newDatastore = oldDatastore } - - if v, ok := d.GetOk("destination_file"); ok { - f.destinationFile = v.(string) + if d.HasChange("destination_file") { + tmpOldDestinationFile, tmpNewDestinationFile := d.GetChange("destination_file") + oldDestinationFile = tmpOldDestinationFile.(string) + newDestinationFile = tmpNewDestinationFile.(string) } else { - return fmt.Errorf("destination_file argument is required") + oldDestinationFile = d.Get("destination_file").(string) + newDestinationFile = oldDestinationFile } + // Get old and new dataceter and datastore client := meta.(*govmomi.Client) - dc, err := getDatacenter(client, f.datacenter) + dcOld, err := getDatacenter(client, oldDataceneter) + if err != nil { + return err + } + dcNew, err := getDatacenter(client, newDatacenter) if err != nil { return err } - finder := find.NewFinder(client.Client, true) - finder = finder.SetDatacenter(dc) - - ds, err := getDatastore(finder, f.datastore) + finder = finder.SetDatacenter(dcOld) + dsOld, err := getDatastore(finder, oldDatastore) + if err != nil { + return fmt.Errorf("error %s", err) + } + finder = finder.SetDatacenter(dcNew) + dsNew, err := getDatastore(finder, newDatastore) if err != nil { return fmt.Errorf("error %s", err) } + // Move file between old/new dataceter, datastore and path (destination_file) fm := object.NewFileManager(client.Client) - task, err := fm.MoveDatastoreFile(context.TODO(), ds.Path(oldDestinationFile.(string)), dc, ds.Path(newDestinationFile.(string)), dc, true) + task, err := fm.MoveDatastoreFile(context.TODO(), dsOld.Path(oldDestinationFile), dcOld, dsNew.Path(newDestinationFile), dcNew, true) if err != nil { return err } - _, err = task.WaitForResult(context.TODO(), nil) if err != nil { return err } - } return nil diff --git a/builtin/providers/vsphere/resource_vsphere_file_test.go b/builtin/providers/vsphere/resource_vsphere_file_test.go index 81520b0cb..7e5aa44e7 100644 --- a/builtin/providers/vsphere/resource_vsphere_file_test.go +++ b/builtin/providers/vsphere/resource_vsphere_file_test.go @@ -14,7 +14,7 @@ import ( "golang.org/x/net/context" ) -// Basic file creation +// Basic file creation (upload to vSphere) func TestAccVSphereFile_basic(t *testing.T) { testVmdkFileData := []byte("# Disk DescriptorFile\n") testVmdkFile := "/tmp/tf_test.vmdk" @@ -55,6 +55,59 @@ func TestAccVSphereFile_basic(t *testing.T) { os.Remove(testVmdkFile) } +// Basic file copy within vSphere +func TestAccVSphereFile_basicUploadAndCopy(t *testing.T) { + testVmdkFileData := []byte("# Disk DescriptorFile\n") + sourceFile := "/tmp/tf_test.vmdk" + uploadResourceName := "myfileupload" + copyResourceName := "myfilecopy" + sourceDatacenter := os.Getenv("VSPHERE_DATACENTER") + datacenter := sourceDatacenter + sourceDatastore := os.Getenv("VSPHERE_DATASTORE") + datastore := sourceDatastore + destinationFile := "tf_file_test.vmdk" + sourceFileCopy := "${vsphere_file." + uploadResourceName + ".destination_file}" + destinationFileCopy := "tf_file_test_copy.vmdk" + + err := ioutil.WriteFile(sourceFile, testVmdkFileData, 0644) + if err != nil { + t.Errorf("error %s", err) + return + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVSphereFileDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf( + testAccCheckVSphereFileCopyConfig, + uploadResourceName, + datacenter, + datastore, + sourceFile, + destinationFile, + copyResourceName, + datacenter, + datacenter, + datastore, + datastore, + sourceFileCopy, + destinationFileCopy, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists("vsphere_file."+uploadResourceName, destinationFile, true), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileCopy, true), + resource.TestCheckResourceAttr("vsphere_file."+uploadResourceName, "destination_file", destinationFile), + resource.TestCheckResourceAttr("vsphere_file."+copyResourceName, "destination_file", destinationFileCopy), + ), + }, + }, + }) + os.Remove(sourceFile) +} + // file creation followed by a rename of file (update) func TestAccVSphereFile_renamePostCreation(t *testing.T) { testVmdkFileData := []byte("# Disk DescriptorFile\n") @@ -67,7 +120,7 @@ func TestAccVSphereFile_renamePostCreation(t *testing.T) { datacenter := os.Getenv("VSPHERE_DATACENTER") datastore := os.Getenv("VSPHERE_DATASTORE") - testMethod := "basic" + testMethod := "create_upgrade" resourceName := "vsphere_file." + testMethod destinationFile := "tf_test_file.vmdk" destinationFileMoved := "tf_test_file_moved.vmdk" @@ -76,7 +129,7 @@ func TestAccVSphereFile_renamePostCreation(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheckVSphereFolderDestroy, + CheckDestroy: testAccCheckVSphereFileDestroy, Steps: []resource.TestStep{ { Config: fmt.Sprintf( @@ -113,6 +166,84 @@ func TestAccVSphereFile_renamePostCreation(t *testing.T) { os.Remove(testVmdkFile) } +// file upload, then copy, finally the copy is renamed (moved) (update) +func TestAccVSphereFile_uploadAndCopyAndUpdate(t *testing.T) { + testVmdkFileData := []byte("# Disk DescriptorFile\n") + sourceFile := "/tmp/tf_test.vmdk" + uploadResourceName := "myfileupload" + copyResourceName := "myfilecopy" + sourceDatacenter := os.Getenv("VSPHERE_DATACENTER") + datacenter := sourceDatacenter + sourceDatastore := os.Getenv("VSPHERE_DATASTORE") + datastore := sourceDatastore + destinationFile := "tf_file_test.vmdk" + sourceFileCopy := "${vsphere_file." + uploadResourceName + ".destination_file}" + destinationFileCopy := "tf_file_test_copy.vmdk" + destinationFileMoved := "tf_test_file_moved.vmdk" + + err := ioutil.WriteFile(sourceFile, testVmdkFileData, 0644) + if err != nil { + t.Errorf("error %s", err) + return + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVSphereFileDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf( + testAccCheckVSphereFileCopyConfig, + uploadResourceName, + datacenter, + datastore, + sourceFile, + destinationFile, + copyResourceName, + datacenter, + datacenter, + datastore, + datastore, + sourceFileCopy, + destinationFileCopy, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists("vsphere_file."+uploadResourceName, destinationFile, true), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileCopy, true), + resource.TestCheckResourceAttr("vsphere_file."+uploadResourceName, "destination_file", destinationFile), + resource.TestCheckResourceAttr("vsphere_file."+copyResourceName, "destination_file", destinationFileCopy), + ), + }, + { + Config: fmt.Sprintf( + testAccCheckVSphereFileCopyConfig, + uploadResourceName, + datacenter, + datastore, + sourceFile, + destinationFile, + copyResourceName, + datacenter, + datacenter, + datastore, + datastore, + sourceFileCopy, + destinationFileMoved, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists("vsphere_file."+uploadResourceName, destinationFile, true), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileCopy, false), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileMoved, true), + resource.TestCheckResourceAttr("vsphere_file."+uploadResourceName, "destination_file", destinationFile), + resource.TestCheckResourceAttr("vsphere_file."+copyResourceName, "destination_file", destinationFileMoved), + ), + }, + }, + }) + os.Remove(sourceFile) +} + func testAccCheckVSphereFileDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*govmomi.Client) finder := find.NewFinder(client.Client, true) @@ -201,3 +332,19 @@ resource "vsphere_file" "%s" { destination_file = "%s" } ` +const testAccCheckVSphereFileCopyConfig = ` +resource "vsphere_file" "%s" { + datacenter = "%s" + datastore = "%s" + source_file = "%s" + destination_file = "%s" +} +resource "vsphere_file" "%s" { + source_datacenter = "%s" + datacenter = "%s" + source_datastore = "%s" + datastore = "%s" + source_file = "%s" + destination_file = "%s" +} +` diff --git a/website/source/docs/providers/vsphere/r/file.html.markdown b/website/source/docs/providers/vsphere/r/file.html.markdown index 443aa3046..9bd4c4b17 100644 --- a/website/source/docs/providers/vsphere/r/file.html.markdown +++ b/website/source/docs/providers/vsphere/r/file.html.markdown @@ -3,28 +3,49 @@ layout: "vsphere" page_title: "VMware vSphere: vsphere_file" sidebar_current: "docs-vsphere-resource-file" description: |- - Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere. + Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere or copy fields withing vSphere. --- # vsphere\_file -Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere. +Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere. The file resource can also be used to copy files within vSphere. Files can be copied between Datacenters and/or Datastores. -## Example Usage +Updates to file resources will handle moving a file to a new destination (datacenter and/or datastore and/or destination_file). If any source parameter (e.g. `source_datastore`, `source_datacenter` or `source_file`) are changed, this results in a new resource (new file uploaded or copied and old one being deleted). +## Example Usages + +**Upload file to vSphere:** ``` -resource "vsphere_file" "ubuntu_disk" { +resource "vsphere_file" "ubuntu_disk_upload" { + datacenter = "my_datacenter" datastore = "local" source_file = "/home/ubuntu/my_disks/custom_ubuntu.vmdk" destination_file = "/my_path/disks/custom_ubuntu.vmdk" } ``` +**Copy file within vSphere:** +``` +resource "vsphere_file" "ubuntu_disk_copy" { + source_datacenter = "my_datacenter" + datacenter = "my_datacenter" + source_datastore = "local" + datastore = "local" + source_file = "/my_path/disks/custom_ubuntu.vmdk" + destination_file = "/my_path/custom_ubuntu_id.vmdk" +} +``` + ## Argument Reference +If `source_datacenter` and `source_datastore` are not provided, the file resource will upload the file from Terraform host. If either `source_datacenter` or `source_datastore` are provided, the file resource will copy from within specified locations in vSphere. + The following arguments are supported: -* `source_file` - (Required) The path to the file on the Terraform host that will be uploaded to vSphere. -* `destination_file` - (Required) The path to where the file should be uploaded to on vSphere. -* `datacenter` - (Optional) The name of a Datacenter in which the file will be created/uploaded to. -* `datastore` - (Required) The name of the Datastore in which to create/upload the file to. +* `source_file` - (Required) The path to the file being uploaded from the Terraform host to vSphere or copied within vSphere. +* `destination_file` - (Required) The path to where the file should be uploaded or copied to on vSphere. +* `source_datacenter` - (Optional) The name of a Datacenter in which the file will be copied from. +* `datacenter` - (Optional) The name of a Datacenter in which the file will be uploaded to. +* `source_datastore` - (Optional) The name of the Datastore in which file will be copied from. +* `datastore` - (Required) The name of the Datastore in which to upload the file to. +* `create_directories` - (Optional) Create directories in `destination_file` path parameter if any missing for copy operation. *Note: Directories are not deleted on destroy operation. \ No newline at end of file From 67bd4f29e0aac83a6e310ffaea551f511f844df1 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 4 Aug 2016 16:48:31 -0400 Subject: [PATCH 03/13] Override atlas variables even if they aren't local Some Atlas usage patterns expect to be able to override a variable set in Atlas, even if it's not seen in the local context. This allows overwriting a variable that is returned from atlas, and sends it back. Also use a unique sential value in the context where we have variables from atlas. This way atals variables aren't combined with the local variables, and we don't do something like inadvertantly change the type, double encode/escape, etc. --- command/push.go | 71 +++++++++++++++++++++++++--------- command/push_test.go | 91 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 18 deletions(-) diff --git a/command/push.go b/command/push.go index d7845ddb4..959070858 100644 --- a/command/push.go +++ b/command/push.go @@ -47,6 +47,22 @@ func (c *PushCommand) Run(args []string) int { 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 atlas + // environment. + 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 + } + } + } + // The pwd is used for the configuration path if one is not given pwd, err := os.Getwd() if err != nil { @@ -145,19 +161,14 @@ func (c *PushCommand) Run(args []string) int { return 1 } - // filter any overwrites from the atlas vars - for k := range overwriteMap { - delete(atlasVars, k) - } - // 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, The atlas variable may be an hcl-encoded object, but - // we're just going to set it as the raw string value. + // the user for input. ctxVars := ctx.Variables() - for k, av := range atlasVars { + atlasVarSentry := "ATLAS_78AC153CA649EAA44815DAD6CBD4816D" + for k, _ := range atlasVars { if _, ok := ctxVars[k]; !ok { - ctx.SetVariable(k, av.Value) + ctx.SetVariable(k, atlasVarSentry) } } @@ -203,23 +214,47 @@ func (c *PushCommand) Run(args []string) int { return 1 } - // Output to the user the variables that will be uploaded + // 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 - // Now we can combine the vars for upload to atlas and list the variables - // we're uploading for the user + // first add all the variables we want to send which have been serialized + // from the local context. for _, sv := range serializedVars { - if av, ok := atlasVars[sv.Key]; ok { - // this belongs to Atlas - uploadVars = append(uploadVars, av) - } else { - // we're uploading our local version - setVars = append(setVars, sv.Key) + _, 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) diff --git a/command/push_test.go b/command/push_test.go index 60270169e..9bb702381 100644 --- a/command/push_test.go +++ b/command/push_test.go @@ -264,6 +264,97 @@ func TestPush_localOverride(t *testing.T) { } } +// 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{ + ContextOpts: testCtxConfig(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) { From 744b266995d9a4be137148f50f84abfcdc7cf076 Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Fri, 5 Aug 2016 17:59:15 +1000 Subject: [PATCH 04/13] provider/aws: Support `aws_elasticsearch_domain` upgrades to (#7860) `elasticsearch_version` 2.3 Fixes #7836 This will allow ElasticSearch domains to be deployed with version 2.3 of ElasticSearch The other slight modifications are to stop dereferencing values before passing to d.Set in the Read func. It is safer to pass the pointer to d.Set and allow that to dereference if there is a value ``` % make testacc TEST=./builtin/providers/aws TESTARGS='-run=TestAccAWSElasticSearchDomain_' ==> Checking that code complies with gofmt requirements... go generate $(go list ./... | grep -v /terraform/vendor/) TF_ACC=1 go test ./builtin/providers/aws -v -run=TestAccAWSElasticSearchDomain_ -timeout 120m === RUN TestAccAWSElasticSearchDomain_basic --- PASS: TestAccAWSElasticSearchDomain_basic (1611.74s) === RUN TestAccAWSElasticSearchDomain_v23 --- PASS: TestAccAWSElasticSearchDomain_v23 (1898.80s) === RUN TestAccAWSElasticSearchDomain_complex --- PASS: TestAccAWSElasticSearchDomain_complex (1802.44s) PASS ok github.com/hashicorp/terraform/builtin/providers/aws 5313.006s ``` Update resource_aws_elasticsearch_domain.go --- .../aws/resource_aws_elasticsearch_domain.go | 17 +++-- .../resource_aws_elasticsearch_domain_test.go | 68 +++++++++++++++---- .../aws/r/elasticsearch_domain.html.markdown | 2 + 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/builtin/providers/aws/resource_aws_elasticsearch_domain.go b/builtin/providers/aws/resource_aws_elasticsearch_domain.go index 35bffc89a..b7ba0a843 100644 --- a/builtin/providers/aws/resource_aws_elasticsearch_domain.go +++ b/builtin/providers/aws/resource_aws_elasticsearch_domain.go @@ -129,6 +129,13 @@ func resourceAwsElasticSearchDomain() *schema.Resource { }, }, }, + "elasticsearch_version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "1.5", + ForceNew: true, + }, + "tags": tagsSchema(), }, } @@ -138,7 +145,8 @@ func resourceAwsElasticSearchDomainCreate(d *schema.ResourceData, meta interface conn := meta.(*AWSClient).esconn input := elasticsearch.CreateElasticsearchDomainInput{ - DomainName: aws.String(d.Get("domain_name").(string)), + DomainName: aws.String(d.Get("domain_name").(string)), + ElasticsearchVersion: aws.String(d.Get("elasticsearch_version").(string)), } if v, ok := d.GetOk("access_policies"); ok { @@ -262,8 +270,9 @@ func resourceAwsElasticSearchDomainRead(d *schema.ResourceData, meta interface{} if err != nil { return err } - d.Set("domain_id", *ds.DomainId) - d.Set("domain_name", *ds.DomainName) + d.Set("domain_id", ds.DomainId) + d.Set("domain_name", ds.DomainName) + d.Set("elasticsearch_version", ds.ElasticsearchVersion) if ds.Endpoint != nil { d.Set("endpoint", *ds.Endpoint) } @@ -282,7 +291,7 @@ func resourceAwsElasticSearchDomainRead(d *schema.ResourceData, meta interface{} }) } - d.Set("arn", *ds.ARN) + d.Set("arn", ds.ARN) listOut, err := conn.ListTags(&elasticsearch.ListTagsInput{ ARN: ds.ARN, diff --git a/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go b/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go index 881d92322..85dd37289 100644 --- a/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go +++ b/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go @@ -7,12 +7,14 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice" + "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) func TestAccAWSElasticSearchDomain_basic(t *testing.T) { var domain elasticsearch.ElasticsearchDomainStatus + ri := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -20,9 +22,32 @@ func TestAccAWSElasticSearchDomain_basic(t *testing.T) { CheckDestroy: testAccCheckESDomainDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccESDomainConfig, + Config: testAccESDomainConfig(ri), Check: resource.ComposeTestCheckFunc( testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), + resource.TestCheckResourceAttr( + "aws_elasticsearch_domain.example", "elasticsearch_version", "1.5"), + ), + }, + }, + }) +} + +func TestAccAWSElasticSearchDomain_v23(t *testing.T) { + var domain elasticsearch.ElasticsearchDomainStatus + ri := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckESDomainDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccESDomainConfigV23(ri), + Check: resource.ComposeTestCheckFunc( + testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), + resource.TestCheckResourceAttr( + "aws_elasticsearch_domain.example", "elasticsearch_version", "2.3"), ), }, }, @@ -31,6 +56,7 @@ func TestAccAWSElasticSearchDomain_basic(t *testing.T) { func TestAccAWSElasticSearchDomain_complex(t *testing.T) { var domain elasticsearch.ElasticsearchDomainStatus + ri := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -38,7 +64,7 @@ func TestAccAWSElasticSearchDomain_complex(t *testing.T) { CheckDestroy: testAccCheckESDomainDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccESDomainConfig_complex, + Config: testAccESDomainConfig_complex(ri), Check: resource.ComposeTestCheckFunc( testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), ), @@ -50,6 +76,7 @@ func TestAccAWSElasticSearchDomain_complex(t *testing.T) { func TestAccAWSElasticSearch_tags(t *testing.T) { var domain elasticsearch.ElasticsearchDomainStatus var td elasticsearch.ListTagsOutput + ri := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -57,14 +84,14 @@ func TestAccAWSElasticSearch_tags(t *testing.T) { CheckDestroy: testAccCheckAWSELBDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccESDomainConfig, + Config: testAccESDomainConfig(ri), Check: resource.ComposeTestCheckFunc( testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), ), }, resource.TestStep{ - Config: testAccESDomainConfig_TagUpdate, + Config: testAccESDomainConfig_TagUpdate(ri), Check: resource.ComposeTestCheckFunc( testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), testAccLoadESTags(&domain, &td), @@ -144,26 +171,31 @@ func testAccCheckESDomainDestroy(s *terraform.State) error { return nil } -const testAccESDomainConfig = ` +func testAccESDomainConfig(randInt int) string { + return fmt.Sprintf(` resource "aws_elasticsearch_domain" "example" { - domain_name = "tf-test-1" + domain_name = "tf-test-%d" +} +`, randInt) } -` -const testAccESDomainConfig_TagUpdate = ` +func testAccESDomainConfig_TagUpdate(randInt int) string { + return fmt.Sprintf(` resource "aws_elasticsearch_domain" "example" { - domain_name = "tf-test-1" + domain_name = "tf-test-%d" tags { foo = "bar" new = "type" } } -` +`, randInt) +} -const testAccESDomainConfig_complex = ` +func testAccESDomainConfig_complex(randInt int) string { + return fmt.Sprintf(` resource "aws_elasticsearch_domain" "example" { - domain_name = "tf-test-2" + domain_name = "tf-test-%d" advanced_options { "indices.fielddata.cache.size" = 80 @@ -186,4 +218,14 @@ resource "aws_elasticsearch_domain" "example" { bar = "complex" } } -` +`, randInt) +} + +func testAccESDomainConfigV23(randInt int) string { + return fmt.Sprintf(` +resource "aws_elasticsearch_domain" "example" { + domain_name = "tf-test-%d" + elasticsearch_version = "2.3" +} +`, randInt) +} diff --git a/website/source/docs/providers/aws/r/elasticsearch_domain.html.markdown b/website/source/docs/providers/aws/r/elasticsearch_domain.html.markdown index dac78a87c..6dd083f0f 100644 --- a/website/source/docs/providers/aws/r/elasticsearch_domain.html.markdown +++ b/website/source/docs/providers/aws/r/elasticsearch_domain.html.markdown @@ -14,6 +14,7 @@ description: |- ``` resource "aws_elasticsearch_domain" "es" { domain_name = "tf-test" + elasticsearch_version = "1.5" advanced_options { "rest.action.multi.allow_explicit_index" = true } @@ -54,6 +55,7 @@ The following arguments are supported: * `ebs_options` - (Optional) EBS related options, see below. * `cluster_config` - (Optional) Cluster configuration of the domain, see below. * `snapshot_options` - (Optional) Snapshot related options, see below. +* `elasticsearch_version` - (Optional) The version of ElasticSearch to deploy. Only valid values are `1.5` and `2.3`. Defaults to `1.5` * `tags` - (Optional) A mapping of tags to assign to the resource **ebs_options** supports the following attributes: From fa2d6e35a705a9c6a92a6b58393f359106b93bba Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 5 Aug 2016 09:01:41 +0100 Subject: [PATCH 05/13] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6465ec47b..ffb5ee77f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ FEATURES: IMPROVEMENTS: * provider/vsphere: Improved SCSI controller handling in `vsphere_virtual_machine` [GH-7908] + * provider/aws: Introduce `aws_elasticsearch_domain` `elasticsearch_version` field (to specify ES version) [GH-7860] BUG FIXES: * provider/aws: guard against missing image_digest in `aws_ecs_task_definition` [GH-7966] From 6899246b98d2b103a7eeaeb1608ad814aef80188 Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Fri, 5 Aug 2016 18:44:10 +1000 Subject: [PATCH 06/13] provider/aws: Updates `aws_cloudformation_stack` Update timeout (#7997) Fixes #7996 The Create func was using the timeout that we were passing to the resource. Update func was not. ``` % make testacc TEST=./builtin/providers/aws % TESTARGS='-run=TestAccAWSCloudFormation_' ==> Checking that code complies with gofmt requirements... go generate $(go list ./... | grep -v /terraform/vendor/) TF_ACC=1 go test ./builtin/providers/aws -v -run=TestAccAWSCloudFormation_ -timeout 120m === RUN TestAccAWSCloudFormation_basic --- PASS: TestAccAWSCloudFormation_basic (120.61s) === RUN TestAccAWSCloudFormation_defaultParams --- PASS: TestAccAWSCloudFormation_defaultParams (121.40s) === RUN TestAccAWSCloudFormation_allAttributes --- PASS: TestAccAWSCloudFormation_allAttributes (263.29s) === RUN TestAccAWSCloudFormation_withParams --- PASS: TestAccAWSCloudFormation_withParams (205.52s) === RUN TestAccAWSCloudFormation_withUrl_withParams --- PASS: TestAccAWSCloudFormation_withUrl_withParams (402.71s) PASS ok github.com/hashicorp/terraform/builtin/providers/aws 1113.552s ``` --- .../providers/aws/resource_aws_cloudformation_stack.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_cloudformation_stack.go b/builtin/providers/aws/resource_aws_cloudformation_stack.go index 28935e33c..56249587b 100644 --- a/builtin/providers/aws/resource_aws_cloudformation_stack.go +++ b/builtin/providers/aws/resource_aws_cloudformation_stack.go @@ -268,6 +268,7 @@ func resourceAwsCloudFormationStackRead(d *schema.ResourceData, meta interface{} } func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface{}) error { + retryTimeout := int64(30) conn := meta.(*AWSClient).cfconn input := &cloudformation.UpdateStackInput{ @@ -314,6 +315,13 @@ func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface return err } + if v, ok := d.GetOk("timeout_in_minutes"); ok { + m := int64(v.(int)) + if m > retryTimeout { + retryTimeout = m + 5 + log.Printf("[DEBUG] CloudFormation timeout: %d", retryTimeout) + } + } wait := resource.StateChangeConf{ Pending: []string{ "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", @@ -323,7 +331,7 @@ func resourceAwsCloudFormationStackUpdate(d *schema.ResourceData, meta interface "UPDATE_ROLLBACK_COMPLETE", }, Target: []string{"UPDATE_COMPLETE"}, - Timeout: 15 * time.Minute, + Timeout: time.Duration(retryTimeout) * time.Minute, MinTimeout: 5 * time.Second, Refresh: func() (interface{}, string, error) { resp, err := conn.DescribeStacks(&cloudformation.DescribeStacksInput{ From 4c6e0d94f9ed22a192064b420d25a95f110ec3ba Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 5 Aug 2016 09:46:44 +0100 Subject: [PATCH 07/13] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffb5ee77f..c9f3d694d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ IMPROVEMENTS: BUG FIXES: * provider/aws: guard against missing image_digest in `aws_ecs_task_definition` [GH-7966] + * provider/aws: `aws_cloudformation_stack` now respects `timeout_in_minutes` field when waiting for CF API to finish an update operation [GH-7997] * provider/aws: Add state filter to `aws_availability_zone`s data source [GH-7965] * provider/aws: Handle lack of snapshot ID for a volume in `ami_copy` [GH-7995] * provider/aws: Retry association of IAM Role & instance profile [GH-7938] From c990e386015a07cb679ef99e387dd7a278c89b0d Mon Sep 17 00:00:00 2001 From: Simon Westcott Date: Fri, 5 Aug 2016 14:01:28 +0100 Subject: [PATCH 08/13] Correct AWS secret key variable name --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8720a0888..29b8554fa 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -373,7 +373,7 @@ to a single resource. Most tests follow a similar structure. 1. Pre-flight checks are made to ensure that sufficient provider configuration is available to be able to proceed - for example in an acceptance test - targetting AWS, `AWS_ACCESS_KEY_ID` and `AWS_SECRET_KEY` must be set prior + targetting AWS, `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` must be set prior to running acceptance tests. This is common to all tests exercising a single provider. From 0a3714eaac8d785ef22f3545c059adcb2b86ae98 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 5 Aug 2016 11:38:10 -0400 Subject: [PATCH 09/13] Don't send access_token in request params Always send the access_token in the X-Atlas-Token header. --- state/remote/atlas.go | 6 +++++- state/remote/atlas_test.go | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/state/remote/atlas.go b/state/remote/atlas.go index 24e81f177..5343c0236 100644 --- a/state/remote/atlas.go +++ b/state/remote/atlas.go @@ -23,6 +23,7 @@ import ( const ( // defaultAtlasServer is used when no address is given defaultAtlasServer = "https://atlas.hashicorp.com/" + atlasTokenHeader = "X-Atlas-Token" ) func atlasFactory(conf map[string]string) (Client, error) { @@ -92,6 +93,8 @@ func (c *AtlasClient) Get() (*Payload, error) { return nil, fmt.Errorf("Failed to make HTTP request: %v", err) } + req.Header.Set(atlasTokenHeader, c.AccessToken) + // Request the url client, err := c.http() if err != nil { @@ -170,6 +173,7 @@ func (c *AtlasClient) Put(state []byte) error { } // Prepare the request + req.Header.Set(atlasTokenHeader, c.AccessToken) req.Header.Set("Content-MD5", b64) req.Header.Set("Content-Type", "application/json") req.ContentLength = int64(len(state)) @@ -204,6 +208,7 @@ func (c *AtlasClient) Delete() error { if err != nil { return fmt.Errorf("Failed to make HTTP request: %v", err) } + req.Header.Set(atlasTokenHeader, c.AccessToken) // Make the request client, err := c.http() @@ -249,7 +254,6 @@ func (c *AtlasClient) url() *url.URL { values := url.Values{} values.Add("atlas_run_id", c.RunId) - values.Add("access_token", c.AccessToken) return &url.URL{ Scheme: c.ServerURL.Scheme, diff --git a/state/remote/atlas_test.go b/state/remote/atlas_test.go index 1d73540a4..9d4f226fe 100644 --- a/state/remote/atlas_test.go +++ b/state/remote/atlas_test.go @@ -218,6 +218,17 @@ func (f *fakeAtlas) NoConflictAllowed(b bool) { } func (f *fakeAtlas) handler(resp http.ResponseWriter, req *http.Request) { + // access tokens should only be sent as a header + if req.FormValue("access_token") != "" { + http.Error(resp, "access_token in request params", http.StatusBadRequest) + return + } + + if req.Header.Get(atlasTokenHeader) == "" { + http.Error(resp, "missing access token", http.StatusBadRequest) + return + } + switch req.Method { case "GET": // Respond with the current stored state. From 9c54e9c955104863e9de7266ea383795f6dd834b Mon Sep 17 00:00:00 2001 From: Krzysztof Wilczynski Date: Sun, 7 Aug 2016 08:29:51 +0900 Subject: [PATCH 10/13] Add aws_vpn_gateway_attachment resource. (#7870) This commit adds VPN Gateway attachment resource, and also an initial tests and documentation stubs. Signed-off-by: Krzysztof Wilczynski --- builtin/providers/aws/provider.go | 1 + .../providers/aws/resource_aws_vpn_gateway.go | 21 +- .../resource_aws_vpn_gateway_attachment.go | 210 ++++++++++++++++++ ...esource_aws_vpn_gateway_attachment_test.go | 163 ++++++++++++++ .../aws/resource_aws_vpn_gateway_test.go | 124 ++++++++--- .../r/vpn_gateway_attachment.html.markdown | 57 +++++ website/source/layouts/aws.erb | 4 + 7 files changed, 540 insertions(+), 40 deletions(-) create mode 100644 builtin/providers/aws/resource_aws_vpn_gateway_attachment.go create mode 100644 builtin/providers/aws/resource_aws_vpn_gateway_attachment_test.go create mode 100644 website/source/docs/providers/aws/r/vpn_gateway_attachment.html.markdown diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 69e264dd9..234a9c90e 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -282,6 +282,7 @@ func Provider() terraform.ResourceProvider { "aws_vpn_connection": resourceAwsVpnConnection(), "aws_vpn_connection_route": resourceAwsVpnConnectionRoute(), "aws_vpn_gateway": resourceAwsVpnGateway(), + "aws_vpn_gateway_attachment": resourceAwsVpnGatewayAttachment(), }, ConfigureFunc: providerConfigure, } diff --git a/builtin/providers/aws/resource_aws_vpn_gateway.go b/builtin/providers/aws/resource_aws_vpn_gateway.go index 27f4a45f7..845e11cd3 100644 --- a/builtin/providers/aws/resource_aws_vpn_gateway.go +++ b/builtin/providers/aws/resource_aws_vpn_gateway.go @@ -32,6 +32,7 @@ func resourceAwsVpnGateway() *schema.Resource { "vpc_id": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, }, "tags": tagsSchema(), @@ -80,17 +81,18 @@ func resourceAwsVpnGatewayRead(d *schema.ResourceData, meta interface{}) error { } vpnGateway := resp.VpnGateways[0] - if vpnGateway == nil { + if vpnGateway == nil || *vpnGateway.State == "deleted" { // Seems we have lost our VPN gateway d.SetId("") return nil } - if len(vpnGateway.VpcAttachments) == 0 || *vpnGateway.VpcAttachments[0].State == "detached" || *vpnGateway.VpcAttachments[0].State == "deleted" { + vpnAttachment := vpnGatewayGetAttachment(vpnGateway) + if len(vpnGateway.VpcAttachments) == 0 || *vpnAttachment.State == "detached" { // Gateway exists but not attached to the VPC d.Set("vpc_id", "") } else { - d.Set("vpc_id", vpnGateway.VpcAttachments[0].VpcId) + d.Set("vpc_id", *vpnAttachment.VpcId) } d.Set("availability_zone", vpnGateway.AvailabilityZone) d.Set("tags", tagsToMap(vpnGateway.Tags)) @@ -301,12 +303,21 @@ func vpnGatewayAttachStateRefreshFunc(conn *ec2.EC2, id string, expected string) } vpnGateway := resp.VpnGateways[0] - if len(vpnGateway.VpcAttachments) == 0 { // No attachments, we're detached return vpnGateway, "detached", nil } - return vpnGateway, *vpnGateway.VpcAttachments[0].State, nil + vpnAttachment := vpnGatewayGetAttachment(vpnGateway) + return vpnGateway, *vpnAttachment.State, nil } } + +func vpnGatewayGetAttachment(vgw *ec2.VpnGateway) *ec2.VpcAttachment { + for _, v := range vgw.VpcAttachments { + if *v.State == "attached" { + return v + } + } + return &ec2.VpcAttachment{State: aws.String("detached")} +} diff --git a/builtin/providers/aws/resource_aws_vpn_gateway_attachment.go b/builtin/providers/aws/resource_aws_vpn_gateway_attachment.go new file mode 100644 index 000000000..b19393bfb --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpn_gateway_attachment.go @@ -0,0 +1,210 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsVpnGatewayAttachment() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsVpnGatewayAttachmentCreate, + Read: resourceAwsVpnGatewayAttachmentRead, + Delete: resourceAwsVpnGatewayAttachmentDelete, + + Schema: map[string]*schema.Schema{ + "vpc_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "vpn_gateway_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsVpnGatewayAttachmentCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + vpcId := d.Get("vpc_id").(string) + vgwId := d.Get("vpn_gateway_id").(string) + + createOpts := &ec2.AttachVpnGatewayInput{ + VpcId: aws.String(vpcId), + VpnGatewayId: aws.String(vgwId), + } + log.Printf("[DEBUG] VPN Gateway attachment options: %#v", *createOpts) + + _, err := conn.AttachVpnGateway(createOpts) + if err != nil { + return fmt.Errorf("Error attaching VPN Gateway %q to VPC %q: %s", + vgwId, vpcId, err) + } + + d.SetId(vpnGatewayAttachmentId(vpcId, vgwId)) + log.Printf("[INFO] VPN Gateway %q attachment ID: %s", vgwId, d.Id()) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"detached", "attaching"}, + Target: []string{"attached"}, + Refresh: vpnGatewayAttachmentStateRefresh(conn, vpcId, vgwId), + Timeout: 5 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for VPN Gateway %q to attach to VPC %q: %s", + vgwId, vpcId, err) + } + log.Printf("[DEBUG] VPN Gateway %q attached to VPC %q.", vgwId, vpcId) + + return resourceAwsVpnGatewayAttachmentRead(d, meta) +} + +func resourceAwsVpnGatewayAttachmentRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + vgwId := d.Get("vpn_gateway_id").(string) + + resp, err := conn.DescribeVpnGateways(&ec2.DescribeVpnGatewaysInput{ + VpnGatewayIds: []*string{aws.String(vgwId)}, + }) + + if err != nil { + awsErr, ok := err.(awserr.Error) + if ok && awsErr.Code() == "InvalidVPNGatewayID.NotFound" { + log.Printf("[WARN] VPN Gateway %q not found.", vgwId) + d.SetId("") + return nil + } + return err + } + + vgw := resp.VpnGateways[0] + if *vgw.State == "deleted" { + log.Printf("[INFO] VPN Gateway %q appears to have been deleted.", vgwId) + d.SetId("") + return nil + } + + vga := vpnGatewayGetAttachment(vgw) + if len(vgw.VpcAttachments) == 0 || *vga.State == "detached" { + d.Set("vpc_id", "") + return nil + } + + d.Set("vpc_id", *vga.VpcId) + return nil +} + +func resourceAwsVpnGatewayAttachmentDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + vpcId := d.Get("vpc_id").(string) + vgwId := d.Get("vpn_gateway_id").(string) + + if vpcId == "" { + log.Printf("[DEBUG] Not detaching VPN Gateway %q as no VPC ID is set.", vgwId) + return nil + } + + _, err := conn.DetachVpnGateway(&ec2.DetachVpnGatewayInput{ + VpcId: aws.String(vpcId), + VpnGatewayId: aws.String(vgwId), + }) + + if err != nil { + awsErr, ok := err.(awserr.Error) + if ok { + switch awsErr.Code() { + case "InvalidVPNGatewayID.NotFound": + log.Printf("[WARN] VPN Gateway %q not found.", vgwId) + d.SetId("") + return nil + case "InvalidVpnGatewayAttachment.NotFound": + log.Printf( + "[WARN] VPN Gateway %q attachment to VPC %q not found.", + vgwId, vpcId) + d.SetId("") + return nil + } + } + + return fmt.Errorf("Error detaching VPN Gateway %q from VPC %q: %s", + vgwId, vpcId, err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"attached", "detaching"}, + Target: []string{"detached"}, + Refresh: vpnGatewayAttachmentStateRefresh(conn, vpcId, vgwId), + Timeout: 5 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for VPN Gateway %q to detach from VPC %q: %s", + vgwId, vpcId, err) + } + log.Printf("[DEBUG] VPN Gateway %q detached from VPC %q.", vgwId, vpcId) + + d.SetId("") + return nil +} + +func vpnGatewayAttachmentStateRefresh(conn *ec2.EC2, vpcId, vgwId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + resp, err := conn.DescribeVpnGateways(&ec2.DescribeVpnGatewaysInput{ + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("attachment.vpc-id"), + Values: []*string{aws.String(vpcId)}, + }, + }, + VpnGatewayIds: []*string{aws.String(vgwId)}, + }) + + if err != nil { + awsErr, ok := err.(awserr.Error) + if ok { + switch awsErr.Code() { + case "InvalidVPNGatewayID.NotFound": + fallthrough + case "InvalidVpnGatewayAttachment.NotFound": + return nil, "", nil + } + } + + return nil, "", err + } + + vgw := resp.VpnGateways[0] + if len(vgw.VpcAttachments) == 0 { + return vgw, "detached", nil + } + + vga := vpnGatewayGetAttachment(vgw) + + log.Printf("[DEBUG] VPN Gateway %q attachment status: %s", vgwId, *vga.State) + return vgw, *vga.State, nil + } +} + +func vpnGatewayAttachmentId(vpcId, vgwId string) string { + return fmt.Sprintf("vpn-attachment-%x", hashcode.String(fmt.Sprintf("%s-%s", vpcId, vgwId))) +} diff --git a/builtin/providers/aws/resource_aws_vpn_gateway_attachment_test.go b/builtin/providers/aws/resource_aws_vpn_gateway_attachment_test.go new file mode 100644 index 000000000..5f12d6fb8 --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpn_gateway_attachment_test.go @@ -0,0 +1,163 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSVpnGatewayAttachment_basic(t *testing.T) { + var vpc ec2.Vpc + var vgw ec2.VpnGateway + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_vpn_gateway_attachment.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckVpnGatewayAttachmentDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccVpnGatewayAttachmentConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists( + "aws_vpc.test", + &vpc), + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.test", + &vgw), + testAccCheckVpnGatewayAttachmentExists( + "aws_vpn_gateway_attachment.test", + &vpc, &vgw), + ), + }, + }, + }) +} + +func TestAccAWSVpnGatewayAttachment_deleted(t *testing.T) { + var vpc ec2.Vpc + var vgw ec2.VpnGateway + + testDeleted := func(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[n] + if ok { + return fmt.Errorf("Expected VPN Gateway attachment resource %q to be deleted.", n) + } + return nil + } + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_vpn_gateway_attachment.test", + Providers: testAccProviders, + CheckDestroy: testAccCheckVpnGatewayAttachmentDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccVpnGatewayAttachmentConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists( + "aws_vpc.test", + &vpc), + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.test", + &vgw), + testAccCheckVpnGatewayAttachmentExists( + "aws_vpn_gateway_attachment.test", + &vpc, &vgw), + ), + }, + resource.TestStep{ + Config: testAccNoVpnGatewayAttachmentConfig, + Check: resource.ComposeTestCheckFunc( + testDeleted("aws_vpn_gateway_attachment.test"), + ), + }, + }, + }) +} + +func testAccCheckVpnGatewayAttachmentExists(n string, vpc *ec2.Vpc, vgw *ec2.VpnGateway) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + vpcId := rs.Primary.Attributes["vpc_id"] + vgwId := rs.Primary.Attributes["vpn_gateway_id"] + + if len(vgw.VpcAttachments) == 0 { + return fmt.Errorf("VPN Gateway %q has no attachments.", vgwId) + } + + if *vgw.VpcAttachments[0].State != "attached" { + return fmt.Errorf("Expected VPN Gateway %q to be in attached state, but got: %q", + vgwId, *vgw.VpcAttachments[0].State) + } + + if *vgw.VpcAttachments[0].VpcId != *vpc.VpcId { + return fmt.Errorf("Expected VPN Gateway %q to be attached to VPC %q, but got: %q", + vgwId, vpcId, *vgw.VpcAttachments[0].VpcId) + } + + return nil + } +} + +func testAccCheckVpnGatewayAttachmentDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpn_gateway_attachment" { + continue + } + + vgwId := rs.Primary.Attributes["vpn_gateway_id"] + + resp, err := conn.DescribeVpnGateways(&ec2.DescribeVpnGatewaysInput{ + VpnGatewayIds: []*string{aws.String(vgwId)}, + }) + if err != nil { + return err + } + + vgw := resp.VpnGateways[0] + if *vgw.VpcAttachments[0].State != "detached" { + return fmt.Errorf("Expected VPN Gateway %q to be in detached state, but got: %q", + vgwId, *vgw.VpcAttachments[0].State) + } + } + + return nil +} + +const testAccNoVpnGatewayAttachmentConfig = ` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpn_gateway" "test" { } +` + +const testAccVpnGatewayAttachmentConfig = ` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpn_gateway" "test" { } + +resource "aws_vpn_gateway_attachment" "test" { + vpc_id = "${aws_vpc.test.id}" + vpn_gateway_id = "${aws_vpn_gateway.test.id}" +} +` diff --git a/builtin/providers/aws/resource_aws_vpn_gateway_test.go b/builtin/providers/aws/resource_aws_vpn_gateway_test.go index 0e3677d6f..c9d2d921a 100644 --- a/builtin/providers/aws/resource_aws_vpn_gateway_test.go +++ b/builtin/providers/aws/resource_aws_vpn_gateway_test.go @@ -16,10 +16,10 @@ func TestAccAWSVpnGateway_basic(t *testing.T) { testNotEqual := func(*terraform.State) error { if len(v.VpcAttachments) == 0 { - return fmt.Errorf("VPN gateway A is not attached") + return fmt.Errorf("VPN Gateway A is not attached") } if len(v2.VpcAttachments) == 0 { - return fmt.Errorf("VPN gateway B is not attached") + return fmt.Errorf("VPN Gateway B is not attached") } id1 := v.VpcAttachments[0].VpcId @@ -58,20 +58,38 @@ func TestAccAWSVpnGateway_basic(t *testing.T) { } func TestAccAWSVpnGateway_reattach(t *testing.T) { - var v ec2.VpnGateway + var vpc1, vpc2 ec2.Vpc + var vgw1, vgw2 ec2.VpnGateway - genTestStateFunc := func(expectedState string) func(*terraform.State) error { + testAttachmentFunc := func(vgw *ec2.VpnGateway, vpc *ec2.Vpc) func(*terraform.State) error { return func(*terraform.State) error { - if len(v.VpcAttachments) == 0 { - if expectedState != "detached" { - return fmt.Errorf("VPN gateway has no VPC attachments") + if len(vgw.VpcAttachments) == 0 { + return fmt.Errorf("VPN Gateway %q has no VPC attachments.", + *vgw.VpnGatewayId) + } + + if len(vgw.VpcAttachments) > 1 { + count := 0 + for _, v := range vgw.VpcAttachments { + if *v.State == "attached" { + count += 1 + } } - } else if len(v.VpcAttachments) == 1 { - if *v.VpcAttachments[0].State != expectedState { - return fmt.Errorf("Expected VPC gateway VPC attachment to be in '%s' state, but was not: %s", expectedState, v) + if count > 1 { + return fmt.Errorf( + "VPN Gateway %q has an unexpected number of VPC attachments (more than 1): %#v", + *vgw.VpnGatewayId, vgw.VpcAttachments) } - } else { - return fmt.Errorf("VPN gateway has unexpected number of VPC attachments(more than 1): %s", v) + } + + if *vgw.VpcAttachments[0].State != "attached" { + return fmt.Errorf("Expected VPN Gateway %q to be attached.", + *vgw.VpnGatewayId) + } + + if *vgw.VpcAttachments[0].VpcId != *vpc.VpcId { + return fmt.Errorf("Expected VPN Gateway %q to be attached to VPC %q, but got: %q", + *vgw.VpnGatewayId, *vpc.VpcId, *vgw.VpcAttachments[0].VpcId) } return nil } @@ -84,27 +102,38 @@ func TestAccAWSVpnGateway_reattach(t *testing.T) { CheckDestroy: testAccCheckVpnGatewayDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccVpnGatewayConfig, + Config: testAccCheckVpnGatewayConfigReattach, Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists("aws_vpc.foo", &vpc1), + testAccCheckVpcExists("aws_vpc.bar", &vpc2), testAccCheckVpnGatewayExists( - "aws_vpn_gateway.foo", &v), - genTestStateFunc("attached"), + "aws_vpn_gateway.foo", &vgw1), + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.bar", &vgw2), + testAttachmentFunc(&vgw1, &vpc1), + testAttachmentFunc(&vgw2, &vpc2), ), }, resource.TestStep{ - Config: testAccVpnGatewayConfigDetach, + Config: testAccCheckVpnGatewayConfigReattachChange, Check: resource.ComposeTestCheckFunc( testAccCheckVpnGatewayExists( - "aws_vpn_gateway.foo", &v), - genTestStateFunc("detached"), + "aws_vpn_gateway.foo", &vgw1), + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.bar", &vgw2), + testAttachmentFunc(&vgw2, &vpc1), + testAttachmentFunc(&vgw1, &vpc2), ), }, resource.TestStep{ - Config: testAccVpnGatewayConfig, + Config: testAccCheckVpnGatewayConfigReattach, Check: resource.ComposeTestCheckFunc( testAccCheckVpnGatewayExists( - "aws_vpn_gateway.foo", &v), - genTestStateFunc("attached"), + "aws_vpn_gateway.foo", &vgw1), + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.bar", &vgw2), + testAttachmentFunc(&vgw1, &vpc1), + testAttachmentFunc(&vgw2, &vpc2), ), }, }, @@ -118,7 +147,7 @@ func TestAccAWSVpnGateway_delete(t *testing.T) { return func(s *terraform.State) error { _, ok := s.RootModule().Resources[r] if ok { - return fmt.Errorf("VPN Gateway %q should have been deleted", r) + return fmt.Errorf("VPN Gateway %q should have been deleted.", r) } return nil } @@ -159,7 +188,6 @@ func TestAccAWSVpnGateway_tags(t *testing.T) { testAccCheckTags(&v.Tags, "foo", "bar"), ), }, - resource.TestStep{ Config: testAccCheckVpnGatewayConfigTagsUpdate, Check: resource.ComposeTestCheckFunc( @@ -198,7 +226,7 @@ func testAccCheckVpnGatewayDestroy(s *terraform.State) error { } if *v.State != "deleted" { - return fmt.Errorf("Expected VpnGateway to be in deleted state, but was not: %s", v) + return fmt.Errorf("Expected VPN Gateway to be in deleted state, but was not: %s", v) } return nil } @@ -235,7 +263,7 @@ func testAccCheckVpnGatewayExists(n string, ig *ec2.VpnGateway) resource.TestChe return err } if len(resp.VpnGateways) == 0 { - return fmt.Errorf("VPNGateway not found") + return fmt.Errorf("VPN Gateway not found") } *ig = *resp.VpnGateways[0] @@ -270,16 +298,6 @@ resource "aws_vpn_gateway" "foo" { } ` -const testAccVpnGatewayConfigDetach = ` -resource "aws_vpc" "foo" { - cidr_block = "10.1.0.0/16" -} - -resource "aws_vpn_gateway" "foo" { - vpc_id = "" -} -` - const testAccCheckVpnGatewayConfigTags = ` resource "aws_vpc" "foo" { cidr_block = "10.1.0.0/16" @@ -305,3 +323,39 @@ resource "aws_vpn_gateway" "foo" { } } ` + +const testAccCheckVpnGatewayConfigReattach = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_vpc" "bar" { + cidr_block = "10.2.0.0/16" +} + +resource "aws_vpn_gateway" "foo" { + vpc_id = "${aws_vpc.foo.id}" +} + +resource "aws_vpn_gateway" "bar" { + vpc_id = "${aws_vpc.bar.id}" +} +` + +const testAccCheckVpnGatewayConfigReattachChange = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_vpc" "bar" { + cidr_block = "10.2.0.0/16" +} + +resource "aws_vpn_gateway" "foo" { + vpc_id = "${aws_vpc.bar.id}" +} + +resource "aws_vpn_gateway" "bar" { + vpc_id = "${aws_vpc.foo.id}" +} +` diff --git a/website/source/docs/providers/aws/r/vpn_gateway_attachment.html.markdown b/website/source/docs/providers/aws/r/vpn_gateway_attachment.html.markdown new file mode 100644 index 000000000..809912831 --- /dev/null +++ b/website/source/docs/providers/aws/r/vpn_gateway_attachment.html.markdown @@ -0,0 +1,57 @@ +--- +layout: "aws" +page_title: "AWS: aws_vpn_gateway_attachment" +sidebar_current: "docs-aws-resource-vpn-gateway-attachment" +description: |- + Provides a Virtual Private Gateway attachment resource. +--- + +# aws\_vpn\_gateway\_attachment + +Provides a Virtual Private Gateway attachment resource, allowing for an existing +hardware VPN gateway to be attached and/or detached from a VPC. + +-> **Note:** The [`aws_vpn_gateway`](vpn_gateway.html) +resource can also automatically attach the Virtual Private Gateway it creates +to an existing VPC by setting the [`vpc_id`](vpn_gateway.html#vpc_id) attribute accordingly. + +## Example Usage + +``` +resource "aws_vpc" "network" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpn_gateway" "vpn" { + tags { + Name = "example-vpn-gateway" + } +} + +resource "aws_vpn_gateway_attachment" "vpn_attachment" { + vpc_id = "${aws_vpc.network.id}" + vpn_gateway_id = "${aws_vpn_gateway.vpn.id}" +} +``` + +See [Virtual Private Cloud](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Introduction.html) +and [Virtual Private Gateway](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_VPN.html) user +guides for more information. + +## Argument Reference + +The following arguments are supported: + +* `vpc_id` - (Required) The ID of the VPC. +* `vpn_gateway_id` - (Required) The ID of the Virtual Private Gateway. + +## Attributes Reference + +The following attributes are exported: + +* `vpc_id` - The ID of the VPC that Virtual Private Gateway is attached to. +* `vpn_gateway_id` - The ID of the Virtual Private Gateway. + +## Import + +This resource does not support importing. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 8ba2ae80e..4eec00ae6 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -885,6 +885,10 @@ aws_vpn_gateway + > + aws_vpn_gateway_attachment + + From 6aff11e664b10db8f3158de5272dbc635671093a Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Sun, 7 Aug 2016 09:34:53 +1000 Subject: [PATCH 11/13] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f3d694d..a26631291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.7.1 (Unreleased) FEATURES: + * **New Resource:** `aws_vpn_gateway_attachment` [GH-7870] IMPROVEMENTS: * provider/vsphere: Improved SCSI controller handling in `vsphere_virtual_machine` [GH-7908] From 09f7fb0c34e0581b4ef8615bd3dfd4579bb58827 Mon Sep 17 00:00:00 2001 From: Jared Biel Date: Sun, 7 Aug 2016 02:16:31 -0500 Subject: [PATCH 12/13] Fix S3 provider redirect_all_requests_to behavior. #5142 (#7883) The S3 API has two parameters that can be passed to it (HostName and Protocol) for the RedirectAllRequestsTo functionality. HostName is somewhat poorly named because it need not be only a hostname (it can contain a path too.) The terraform code for this was treating the API as the parameter name suggests and was truncating out any paths that were passed. --- .../providers/aws/resource_aws_s3_bucket.go | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/builtin/providers/aws/resource_aws_s3_bucket.go b/builtin/providers/aws/resource_aws_s3_bucket.go index 6897f1e7f..29ac708f5 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket.go +++ b/builtin/providers/aws/resource_aws_s3_bucket.go @@ -495,8 +495,20 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { if v.Protocol == nil { w["redirect_all_requests_to"] = *v.HostName } else { + var host string + var path string + parsedHostName, err := url.Parse(*v.HostName) + if err == nil { + host = parsedHostName.Host + path = parsedHostName.Path + } else { + host = *v.HostName + path = "" + } + w["redirect_all_requests_to"] = (&url.URL{ - Host: *v.HostName, + Host: host, + Path: path, Scheme: *v.Protocol, }).String() } @@ -947,7 +959,12 @@ func resourceAwsS3BucketWebsitePut(s3conn *s3.S3, d *schema.ResourceData, websit if redirectAllRequestsTo != "" { redirect, err := url.Parse(redirectAllRequestsTo) if err == nil && redirect.Scheme != "" { - websiteConfiguration.RedirectAllRequestsTo = &s3.RedirectAllRequestsTo{HostName: aws.String(redirect.Host), Protocol: aws.String(redirect.Scheme)} + var redirectHostBuf bytes.Buffer + redirectHostBuf.WriteString(redirect.Host) + if redirect.Path != "" { + redirectHostBuf.WriteString(redirect.Path) + } + websiteConfiguration.RedirectAllRequestsTo = &s3.RedirectAllRequestsTo{HostName: aws.String(redirectHostBuf.String()), Protocol: aws.String(redirect.Scheme)} } else { websiteConfiguration.RedirectAllRequestsTo = &s3.RedirectAllRequestsTo{HostName: aws.String(redirectAllRequestsTo)} } From ccb4907eef3130cb41c221d02b6f48c5050ecef2 Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Sun, 7 Aug 2016 19:17:51 +1200 Subject: [PATCH 13/13] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a26631291..20f7a236a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ BUG FIXES: * provider/aws: Add state filter to `aws_availability_zone`s data source [GH-7965] * provider/aws: Handle lack of snapshot ID for a volume in `ami_copy` [GH-7995] * provider/aws: Retry association of IAM Role & instance profile [GH-7938] + * provider/aws: Fix `aws_s3_bucket` resource `redirect_all_requests_to` action [GH-7883] * provider/google: Use resource specific project when making queries/changes [GH-7029] ## 0.7.0 (August 2, 2016)