Merge branch 'master' into stable-website

This commit is contained in:
James Bardin 2018-03-21 21:22:18 +00:00
commit 3b18741004
36 changed files with 554 additions and 87 deletions

View File

@ -1,10 +1,22 @@
## 0.11.5 (March 21, 2018)
IMPROVEMENTS:
* provisioner/chef: Allow specifying a channel ([#17355](https://github.com/hashicorp/terraform/issues/17355))
BUG FIXES:
* core: Fix the timeout handling for provisioners ([#17646](https://github.com/hashicorp/terraform/issues/17646))
* core: Ensure that state is unlocked after running console, import, graph or push commands ([#17645](https://github.com/hashicorp/terraform/issues/17645))
* core: Don't open multiple file descriptors for local state files, which would cause reading the state to fail on Windows [[#17636](https://github.com/hashicorp/terraform/issues/17636)]
## 0.11.4 (March 15, 2018) ## 0.11.4 (March 15, 2018)
IMPROVEMENTS: IMPROVEMENTS:
* cli: `terraform state list` now accepts a new argument `-id=...` for filtering resources for display by their remote ids ([#17221](https://github.com/hashicorp/terraform/issues/17221)) * cli: `terraform state list` now accepts a new argument `-id=...` for filtering resources for display by their remote ids ([#17221](https://github.com/hashicorp/terraform/issues/17221))
* cli: `terraform destroy` now uses the option `-auto-approve` instead of `-force`, for consistency with `terraform apply`. The old flag is preserved for backward-compatibility, but is now deprecated; it will be retained for at least one major release. ([#17218](https://github.com/hashicorp/terraform/issues/17218)) * cli: `terraform destroy` now uses the option `-auto-approve` instead of `-force`, for consistency with `terraform apply`. The old flag is preserved for backward-compatibility, but is now deprecated; it will be retained for at least one major release. ([#17218](https://github.com/hashicorp/terraform/issues/17218))
* connection/ssh: Add support for host key verifiation ([#17354](https://github.com/hashicorp/terraform/issues/17354)) * connection/ssh: Add support for host key verification ([#17354](https://github.com/hashicorp/terraform/issues/17354))
* backend/s3: add support for the cn-northwest-1 region ([#17216](https://github.com/hashicorp/terraform/issues/17216)) * backend/s3: add support for the cn-northwest-1 region ([#17216](https://github.com/hashicorp/terraform/issues/17216))
* provisioner/local-exec: Allow setting custom environment variables when running commands ([#13880](https://github.com/hashicorp/terraform/issues/13880)) * provisioner/local-exec: Allow setting custom environment variables when running commands ([#13880](https://github.com/hashicorp/terraform/issues/13880))
* provisioner/habitat: Detect if hab user exists and only create if necessary ([#17195](https://github.com/hashicorp/terraform/issues/17195)) * provisioner/habitat: Detect if hab user exists and only create if necessary ([#17195](https://github.com/hashicorp/terraform/issues/17195))
@ -14,7 +26,6 @@ IMPROVEMENTS:
BUG FIXES: BUG FIXES:
* core: Make sure state is locked during initial refresh ([#17422](https://github.com/hashicorp/terraform/issues/17422)) * core: Make sure state is locked during initial refresh ([#17422](https://github.com/hashicorp/terraform/issues/17422))
* core: Fix interpolation error when count references another interpolated count value ([#17368](https://github.com/hashicorp/terraform/issues/17368))
* core: Halt on fatal provisioner errors, rather than retrying until a timeout ([#17359](https://github.com/hashicorp/terraform/issues/17359)) * core: Halt on fatal provisioner errors, rather than retrying until a timeout ([#17359](https://github.com/hashicorp/terraform/issues/17359))
* core: When handling a forced exit due to multiple interrupts, prevent the process from exiting while the state is being written ([#17323](https://github.com/hashicorp/terraform/issues/17323)) * core: When handling a forced exit due to multiple interrupts, prevent the process from exiting while the state is being written ([#17323](https://github.com/hashicorp/terraform/issues/17323))
* core: Fix handling of locals and outputs at destroy time ([#17241](https://github.com/hashicorp/terraform/issues/17241)) * core: Fix handling of locals and outputs at destroy time ([#17241](https://github.com/hashicorp/terraform/issues/17241))

View File

@ -11,7 +11,7 @@ import (
const ( const (
chmod = "find %s -maxdepth 1 -type f -exec /bin/chmod %d {} +" chmod = "find %s -maxdepth 1 -type f -exec /bin/chmod %d {} +"
installURL = "https://www.chef.io/chef/install.sh" installURL = "https://omnitruck.chef.io/install.sh"
) )
func (p *provisioner) linuxInstallChefClient(o terraform.UIOutput, comm communicator.Communicator) error { func (p *provisioner) linuxInstallChefClient(o terraform.UIOutput, comm communicator.Communicator) error {
@ -34,7 +34,7 @@ func (p *provisioner) linuxInstallChefClient(o terraform.UIOutput, comm communic
} }
// Then execute the install.sh scrip to download and install Chef Client // Then execute the install.sh scrip to download and install Chef Client
err = p.runCommand(o, comm, fmt.Sprintf("%sbash ./install.sh -v %q", prefix, p.Version)) err = p.runCommand(o, comm, fmt.Sprintf("%sbash ./install.sh -v %q -c %s", prefix, p.Version, p.Channel))
if err != nil { if err != nil {
return err return err
} }

View File

@ -25,9 +25,9 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) {
}, },
Commands: map[string]bool{ Commands: map[string]bool{
"sudo curl -LO https://www.chef.io/chef/install.sh": true, "sudo curl -LO https://omnitruck.chef.io/install.sh": true,
"sudo bash ./install.sh -v \"\"": true, "sudo bash ./install.sh -v \"\" -c stable": true,
"sudo rm -f install.sh": true, "sudo rm -f install.sh": true,
}, },
}, },
@ -43,9 +43,9 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) {
}, },
Commands: map[string]bool{ Commands: map[string]bool{
"curl -LO https://www.chef.io/chef/install.sh": true, "curl -LO https://omnitruck.chef.io/install.sh": true,
"bash ./install.sh -v \"\"": true, "bash ./install.sh -v \"\" -c stable": true,
"rm -f install.sh": true, "rm -f install.sh": true,
}, },
}, },
@ -61,9 +61,9 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) {
}, },
Commands: map[string]bool{ Commands: map[string]bool{
"http_proxy='http://proxy.local' curl -LO https://www.chef.io/chef/install.sh": true, "http_proxy='http://proxy.local' curl -LO https://omnitruck.chef.io/install.sh": true,
"http_proxy='http://proxy.local' bash ./install.sh -v \"\"": true, "http_proxy='http://proxy.local' bash ./install.sh -v \"\" -c stable": true,
"http_proxy='http://proxy.local' rm -f install.sh": true, "http_proxy='http://proxy.local' rm -f install.sh": true,
}, },
}, },
@ -79,9 +79,9 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) {
}, },
Commands: map[string]bool{ Commands: map[string]bool{
"https_proxy='https://proxy.local' curl -LO https://www.chef.io/chef/install.sh": true, "https_proxy='https://proxy.local' curl -LO https://omnitruck.chef.io/install.sh": true,
"https_proxy='https://proxy.local' bash ./install.sh -v \"\"": true, "https_proxy='https://proxy.local' bash ./install.sh -v \"\" -c stable": true,
"https_proxy='https://proxy.local' rm -f install.sh": true, "https_proxy='https://proxy.local' rm -f install.sh": true,
}, },
}, },
@ -99,9 +99,9 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) {
Commands: map[string]bool{ Commands: map[string]bool{
"http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " + "http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
"curl -LO https://www.chef.io/chef/install.sh": true, "curl -LO https://omnitruck.chef.io/install.sh": true,
"http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " + "http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
"bash ./install.sh -v \"\"": true, "bash ./install.sh -v \"\" -c stable": true,
"http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " + "http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
"rm -f install.sh": true, "rm -f install.sh": true,
}, },
@ -119,9 +119,28 @@ func TestResourceProvider_linuxInstallChefClient(t *testing.T) {
}, },
Commands: map[string]bool{ Commands: map[string]bool{
"curl -LO https://www.chef.io/chef/install.sh": true, "curl -LO https://omnitruck.chef.io/install.sh": true,
"bash ./install.sh -v \"11.18.6\"": true, "bash ./install.sh -v \"11.18.6\" -c stable": true,
"rm -f install.sh": true, "rm -f install.sh": true,
},
},
"Channel": {
Config: map[string]interface{}{
"channel": "current",
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"version": "11.18.6",
},
Commands: map[string]bool{
"curl -LO https://omnitruck.chef.io/install.sh": true,
"bash ./install.sh -v \"11.18.6\" -c current": true,
"rm -f install.sh": true,
}, },
}, },
} }

View File

@ -86,6 +86,7 @@ type provisionFn func(terraform.UIOutput, communicator.Communicator) error
type provisioner struct { type provisioner struct {
Attributes map[string]interface{} Attributes map[string]interface{}
Channel string
ClientOptions []string ClientOptions []string
DisableReporting bool DisableReporting bool
Environment string Environment string
@ -149,6 +150,11 @@ func Provisioner() terraform.ResourceProvisioner {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"channel": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "stable",
},
"client_options": &schema.Schema{ "client_options": &schema.Schema{
Type: schema.TypeList, Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString}, Elem: &schema.Schema{Type: schema.TypeString},
@ -306,11 +312,11 @@ func applyFn(ctx context.Context) error {
return err return err
} }
ctx, cancel := context.WithTimeout(ctx, comm.Timeout()) retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel() defer cancel()
// Wait and retry until we establish the connection // Wait and retry until we establish the connection
err = communicator.Retry(ctx, func() error { err = communicator.Retry(retryCtx, func() error {
return comm.Connect(o) return comm.Connect(o)
}) })
if err != nil { if err != nil {
@ -725,6 +731,7 @@ func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<
func decodeConfig(d *schema.ResourceData) (*provisioner, error) { func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
p := &provisioner{ p := &provisioner{
Channel: d.Get("channel").(string),
ClientOptions: getStringList(d.Get("client_options")), ClientOptions: getStringList(d.Get("client_options")),
DisableReporting: d.Get("disable_reporting").(bool), DisableReporting: d.Get("disable_reporting").(bool),
Environment: d.Get("environment").(string), Environment: d.Get("environment").(string),

View File

@ -23,7 +23,7 @@ switch ($winver)
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"} if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
$url = "http://www.chef.io/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=%s" $url = "http://omnitruck.chef.io/%s/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=%s"
$dest = [System.IO.Path]::GetTempFileName() $dest = [System.IO.Path]::GetTempFileName()
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi") $dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
$downloader = New-Object System.Net.WebClient $downloader = New-Object System.Net.WebClient
@ -48,7 +48,7 @@ Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
func (p *provisioner) windowsInstallChefClient(o terraform.UIOutput, comm communicator.Communicator) error { func (p *provisioner) windowsInstallChefClient(o terraform.UIOutput, comm communicator.Communicator) error {
script := path.Join(path.Dir(comm.ScriptPath()), "ChefClient.ps1") script := path.Join(path.Dir(comm.ScriptPath()), "ChefClient.ps1")
content := fmt.Sprintf(installScript, p.Version, p.HTTPProxy, strings.Join(p.NOProxy, ",")) content := fmt.Sprintf(installScript, p.Channel, p.Version, p.HTTPProxy, strings.Join(p.NOProxy, ","))
// Copy the script to the new instance // Copy the script to the new instance
if err := comm.UploadScript(script, strings.NewReader(content)); err != nil { if err := comm.UploadScript(script, strings.NewReader(content)); err != nil {

View File

@ -72,6 +72,26 @@ func TestResourceProvider_windowsInstallChefClient(t *testing.T) {
"ChefClient.ps1": versionWindowsInstallScript, "ChefClient.ps1": versionWindowsInstallScript,
}, },
}, },
"Channel": {
Config: map[string]interface{}{
"channel": "current",
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"version": "11.18.6",
},
Commands: map[string]bool{
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
},
UploadScripts: map[string]string{
"ChefClient.ps1": channelWindowsInstallScript,
},
},
} }
o := new(terraform.MockUIOutput) o := new(terraform.MockUIOutput)
@ -219,7 +239,7 @@ switch ($winver)
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"} if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
$url = "http://www.chef.io/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=" $url = "http://omnitruck.chef.io/stable/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v="
$dest = [System.IO.Path]::GetTempFileName() $dest = [System.IO.Path]::GetTempFileName()
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi") $dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
$downloader = New-Object System.Net.WebClient $downloader = New-Object System.Net.WebClient
@ -256,7 +276,7 @@ switch ($winver)
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"} if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
$url = "http://www.chef.io/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=" $url = "http://omnitruck.chef.io/stable/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v="
$dest = [System.IO.Path]::GetTempFileName() $dest = [System.IO.Path]::GetTempFileName()
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi") $dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
$downloader = New-Object System.Net.WebClient $downloader = New-Object System.Net.WebClient
@ -293,7 +313,43 @@ switch ($winver)
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"} if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
$url = "http://www.chef.io/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=11.18.6" $url = "http://omnitruck.chef.io/stable/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=11.18.6"
$dest = [System.IO.Path]::GetTempFileName()
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
$downloader = New-Object System.Net.WebClient
$http_proxy = ''
if ($http_proxy -ne '') {
$no_proxy = ''
if ($no_proxy -eq ''){
$no_proxy = "127.0.0.1"
}
$proxy = New-Object System.Net.WebProxy($http_proxy, $true, ,$no_proxy.Split(','))
$downloader.proxy = $proxy
}
Write-Host 'Downloading Chef Client...'
$downloader.DownloadFile($url, $dest)
Write-Host 'Installing Chef Client...'
Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
`
const channelWindowsInstallScript = `
$winver = [System.Environment]::OSVersion.Version | % {"{0}.{1}" -f $_.Major,$_.Minor}
switch ($winver)
{
"6.0" {$machine_os = "2008"}
"6.1" {$machine_os = "2008r2"}
"6.2" {$machine_os = "2012"}
"6.3" {$machine_os = "2012"}
default {$machine_os = "2008r2"}
}
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
$url = "http://omnitruck.chef.io/current/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=11.18.6"
$dest = [System.IO.Path]::GetTempFileName() $dest = [System.IO.Path]::GetTempFileName()
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi") $dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
$downloader = New-Object System.Net.WebClient $downloader = New-Object System.Net.WebClient

View File

@ -48,9 +48,6 @@ func applyFn(ctx context.Context) error {
return err return err
} }
ctx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel()
// Get the source // Get the source
src, deleteSource, err := getSrc(data) src, deleteSource, err := getSrc(data)
if err != nil { if err != nil {
@ -99,8 +96,11 @@ func getSrc(data *schema.ResourceData) (string, bool, error) {
// copyFiles is used to copy the files from a source to a destination // copyFiles is used to copy the files from a source to a destination
func copyFiles(ctx context.Context, comm communicator.Communicator, src, dst string) error { func copyFiles(ctx context.Context, comm communicator.Communicator, src, dst string) error {
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel()
// Wait and retry until we establish the connection // Wait and retry until we establish the connection
err := communicator.Retry(ctx, func() error { err := communicator.Retry(retryCtx, func() error {
return comm.Connect(nil) return comm.Connect(nil)
}) })
if err != nil { if err != nil {

View File

@ -231,10 +231,10 @@ func applyFn(ctx context.Context) error {
return err return err
} }
ctx, cancel := context.WithTimeout(ctx, comm.Timeout()) retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel() defer cancel()
err = communicator.Retry(ctx, func() error { err = communicator.Retry(retryCtx, func() error {
return comm.Connect(o) return comm.Connect(o)
}) })

View File

@ -157,20 +157,23 @@ func runScripts(
comm communicator.Communicator, comm communicator.Communicator,
scripts []io.ReadCloser) error { scripts []io.ReadCloser) error {
// Wait for the context to end and then disconnect retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
go func() { defer cancel()
<-ctx.Done()
comm.Disconnect()
}()
// Wait and retry until we establish the connection // Wait and retry until we establish the connection
err := communicator.Retry(ctx, func() error { err := communicator.Retry(retryCtx, func() error {
return comm.Connect(o) return comm.Connect(o)
}) })
if err != nil { if err != nil {
return err return err
} }
// Wait for the context to end and then disconnect
go func() {
<-ctx.Done()
comm.Disconnect()
}()
for _, script := range scripts { for _, script := range scripts {
var cmd *remote.Cmd var cmd *remote.Cmd

View File

@ -2,11 +2,15 @@ package remoteexec
import ( import (
"bytes" "bytes"
"context"
"io" "io"
"testing" "testing"
"time"
"strings" "strings"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
@ -206,6 +210,59 @@ func TestResourceProvider_CollectScripts_scriptsEmpty(t *testing.T) {
} }
} }
func TestProvisionerTimeout(t *testing.T) {
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
disconnected := make(chan struct{})
c.DisconnectFunc = func() error {
close(disconnected)
return nil
}
completed := make(chan struct{})
c.CommandFunc = func(cmd *remote.Cmd) error {
defer close(completed)
cmd.Init()
time.Sleep(2 * time.Second)
cmd.SetExitStatus(0, nil)
return nil
}
c.ConnTimeout = time.Second
c.UploadScripts = map[string]string{"hello": "echo hello"}
c.RemoteScriptPath = "hello"
p := Provisioner().(*schema.Provisioner)
conf := map[string]interface{}{
"inline": []interface{}{"echo hello"},
}
scripts, err := collectScripts(schema.TestResourceDataRaw(
t, p.Schema, conf))
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
done := make(chan struct{})
go func() {
defer close(done)
if err := runScripts(ctx, o, c, scripts); err != nil {
t.Fatal(err)
}
}()
select {
case <-disconnected:
t.Fatal("communicator disconnected before command completed")
case <-completed:
}
<-done
}
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig { func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
r, err := config.NewRawConfig(c) r, err := config.NewRawConfig(c)
if err != nil { if err != nil {

View File

@ -131,17 +131,11 @@ func applyFn(ctx context.Context) error {
return err return err
} }
ctx, cancelFunc := context.WithTimeout(ctx, comm.Timeout()) retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancelFunc() defer cancel()
// Wait for the context to end and then disconnect
go func() {
<-ctx.Done()
comm.Disconnect()
}()
// Wait and retry until we establish the connection // Wait and retry until we establish the connection
err = communicator.Retry(ctx, func() error { err = communicator.Retry(retryCtx, func() error {
return comm.Connect(o) return comm.Connect(o)
}) })
@ -149,6 +143,12 @@ func applyFn(ctx context.Context) error {
return err return err
} }
// Wait for the context to end and then disconnect
go func() {
<-ctx.Done()
comm.Disconnect()
}()
var src, dst string var src, dst string
o.Output("Provisioning with Salt...") o.Output("Provisioning with Salt...")

View File

@ -81,6 +81,13 @@ func (c *ConsoleCommand) Run(args []string) int {
return 1 return 1
} }
defer func() {
err := opReq.StateLocker.Unlock(nil)
if err != nil {
c.Ui.Error(err.Error())
}
}()
// Setup the UI so we can output directly to stdout // Setup the UI so we can output directly to stdout
ui := &cli.BasicUi{ ui := &cli.BasicUi{
Writer: wrappedstreams.Stdout(), Writer: wrappedstreams.Stdout(),

View File

@ -112,6 +112,13 @@ func (c *GraphCommand) Run(args []string) int {
return 1 return 1
} }
defer func() {
err := opReq.StateLocker.Unlock(nil)
if err != nil {
c.Ui.Error(err.Error())
}
}()
// Determine the graph type // Determine the graph type
graphType := terraform.GraphTypePlan graphType := terraform.GraphTypePlan
if plan != nil { if plan != nil {

View File

@ -184,6 +184,13 @@ func (c *ImportCommand) Run(args []string) int {
return 1 return 1
} }
defer func() {
err := opReq.StateLocker.Unlock(nil)
if err != nil {
c.Ui.Error(err.Error())
}
}()
// Perform the import. Note that as you can see it is possible for this // Perform the import. Note that as you can see it is possible for this
// API to import more than one resource at once. For now, we only allow // API to import more than one resource at once. For now, we only allow
// one while we stabilize this feature. // one while we stabilize this feature.

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"strings" "strings"
"testing" "testing"
@ -175,11 +176,17 @@ func TestImport_remoteState(t *testing.T) {
"test_instance.foo", "test_instance.foo",
"bar", "bar",
} }
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
fmt.Println(ui.OutputWriter) fmt.Println(ui.OutputWriter)
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
} }
// verify that the local state was unlocked after import
if _, err := os.Stat(filepath.Join(td, fmt.Sprintf(".%s.lock.info", statePath))); !os.IsNotExist(err) {
t.Fatal("state left locked after import")
}
// Verify that we were called // Verify that we were called
if !configured { if !configured {
t.Fatal("Configure should be called") t.Fatal("Configure should be called")

View File

@ -146,6 +146,13 @@ func (c *PushCommand) Run(args []string) int {
return 1 return 1
} }
defer func() {
err := opReq.StateLocker.Unlock(nil)
if err != nil {
c.Ui.Error(err.Error())
}
}()
// Get the configuration // Get the configuration
config := ctx.Module().Config() config := ctx.Module().Config()
if name == "" { if name == "" {

View File

@ -18,6 +18,9 @@ type MockCommunicator struct {
Uploads map[string]string Uploads map[string]string
UploadScripts map[string]string UploadScripts map[string]string
UploadDirs map[string]string UploadDirs map[string]string
CommandFunc func(*remote.Cmd) error
DisconnectFunc func() error
ConnTimeout time.Duration
} }
// Connect implementation of communicator.Communicator interface // Connect implementation of communicator.Communicator interface
@ -27,11 +30,17 @@ func (c *MockCommunicator) Connect(o terraform.UIOutput) error {
// Disconnect implementation of communicator.Communicator interface // Disconnect implementation of communicator.Communicator interface
func (c *MockCommunicator) Disconnect() error { func (c *MockCommunicator) Disconnect() error {
if c.DisconnectFunc != nil {
return c.DisconnectFunc()
}
return nil return nil
} }
// Timeout implementation of communicator.Communicator interface // Timeout implementation of communicator.Communicator interface
func (c *MockCommunicator) Timeout() time.Duration { func (c *MockCommunicator) Timeout() time.Duration {
if c.ConnTimeout != 0 {
return c.ConnTimeout
}
return time.Duration(5 * time.Second) return time.Duration(5 * time.Second)
} }
@ -44,6 +53,10 @@ func (c *MockCommunicator) ScriptPath() string {
func (c *MockCommunicator) Start(r *remote.Cmd) error { func (c *MockCommunicator) Start(r *remote.Cmd) error {
r.Init() r.Init()
if c.CommandFunc != nil {
return c.CommandFunc(r)
}
if !c.Commands[r.Command] { if !c.Commands[r.Command] {
return fmt.Errorf("Command not found!") return fmt.Errorf("Command not found!")
} }

View File

@ -10,7 +10,7 @@ import (
"github.com/hashicorp/terraform/version" "github.com/hashicorp/terraform/version"
) )
func TestUserAgent(t *testing.T) { func TestNew_userAgent(t *testing.T) {
var actualUserAgent string var actualUserAgent string
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
actualUserAgent = req.UserAgent() actualUserAgent = req.UserAgent()

View File

@ -2,15 +2,29 @@ package httpclient
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"os"
"strings"
"github.com/hashicorp/terraform/version" "github.com/hashicorp/terraform/version"
) )
const userAgentFormat = "Terraform/%s" const userAgentFormat = "Terraform/%s"
const uaEnvVar = "TF_APPEND_USER_AGENT"
func UserAgentString() string { func UserAgentString() string {
return fmt.Sprintf(userAgentFormat, version.Version) ua := fmt.Sprintf(userAgentFormat, version.Version)
if add := os.Getenv(uaEnvVar); add != "" {
add = strings.TrimSpace(add)
if len(add) > 0 {
ua += " " + add
log.Printf("[DEBUG] Using modified User-Agent: %s", ua)
}
}
return ua
} }
type userAgentRoundTripper struct { type userAgentRoundTripper struct {

View File

@ -0,0 +1,47 @@
package httpclient
import (
"fmt"
"os"
"testing"
"github.com/hashicorp/terraform/version"
)
func TestUserAgentString_env(t *testing.T) {
expectedBase := fmt.Sprintf(userAgentFormat, version.Version)
if oldenv, isSet := os.LookupEnv(uaEnvVar); isSet {
defer os.Setenv(uaEnvVar, oldenv)
} else {
defer os.Unsetenv(uaEnvVar)
}
for i, c := range []struct {
expected string
additional string
}{
{expectedBase, ""},
{expectedBase, " "},
{expectedBase, " \n"},
{fmt.Sprintf("%s test/1", expectedBase), "test/1"},
{fmt.Sprintf("%s test/2", expectedBase), "test/2 "},
{fmt.Sprintf("%s test/3", expectedBase), " test/3 "},
{fmt.Sprintf("%s test/4", expectedBase), "test/4 \n"},
} {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
if c.additional == "" {
os.Unsetenv(uaEnvVar)
} else {
os.Setenv(uaEnvVar, c.additional)
}
actual := UserAgentString()
if c.expected != actual {
t.Fatalf("Expected User-Agent '%s' does not match '%s'", c.expected, actual)
}
})
}
}

View File

@ -119,8 +119,20 @@ func (s *LocalState) RefreshState() error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if s.PathOut == "" {
s.PathOut = s.Path
}
var reader io.Reader var reader io.Reader
if !s.written {
// The s.Path file is only OK to read if we have not written any state out
// (in which case the same state needs to be read in), and no state output file
// has been opened (possibly via a lock) or the input path is different
// than the output path.
// This is important for Windows, as if the input file is the same as the
// output file, and the output file has been locked already, we can't open
// the file again.
if !s.written && (s.stateFileOut == nil || s.Path != s.PathOut) {
// we haven't written a state file yet, so load from Path // we haven't written a state file yet, so load from Path
f, err := os.Open(s.Path) f, err := os.Open(s.Path)
if err != nil { if err != nil {

View File

@ -166,3 +166,42 @@ func testLocalState(t *testing.T) *LocalState {
return ls return ls
} }
// Make sure we can refresh while the state is locked
func TestLocalState_refreshWhileLocked(t *testing.T) {
f, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(TestStateInitial(), f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
s := &LocalState{Path: f.Name()}
defer os.Remove(s.Path)
// lock first
info := NewLockInfo()
info.Operation = "test"
lockID, err := s.Lock(info)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := s.Unlock(lockID); err != nil {
t.Fatal(err)
}
}()
if err := s.RefreshState(); err != nil {
t.Fatal(err)
}
readState := s.State()
if readState == nil || readState.Lineage == "" {
t.Fatal("missing state")
}
}

View File

@ -2969,6 +2969,28 @@ STATE:
} }
} }
// ensure that outputs missing references due to targetting are removed from
// the graph.
func TestContext2Plan_outputContainsTargetedResource(t *testing.T) {
m := testModule(t, "plan-untargeted-resource-output")
p := testProvider("aws")
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Module: m,
ProviderResolver: ResourceProviderResolverFixed(
map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
),
Targets: []string{"module.mod.aws_instance.a"},
})
_, err := ctx.Plan()
if err != nil {
t.Fatalf("err: %s", err)
}
}
// https://github.com/hashicorp/terraform/issues/4515 // https://github.com/hashicorp/terraform/issues/4515
func TestContext2Plan_targetedOverTen(t *testing.T) { func TestContext2Plan_targetedOverTen(t *testing.T) {
m := testModule(t, "plan-targeted-over-ten") m := testModule(t, "plan-targeted-over-ten")

View File

@ -9,6 +9,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"os"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
@ -1876,13 +1877,21 @@ var ErrNoState = errors.New("no state")
// ReadState reads a state structure out of a reader in the format that // ReadState reads a state structure out of a reader in the format that
// was written by WriteState. // was written by WriteState.
func ReadState(src io.Reader) (*State, error) { func ReadState(src io.Reader) (*State, error) {
buf := bufio.NewReader(src) // check for a nil file specifically, since that produces a platform
if _, err := buf.Peek(1); err != nil { // specific error if we try to use it in a bufio.Reader.
// the error is either io.EOF or "invalid argument", and both are from if f, ok := src.(*os.File); ok && f == nil {
// an empty state.
return nil, ErrNoState return nil, ErrNoState
} }
buf := bufio.NewReader(src)
if _, err := buf.Peek(1); err != nil {
if err == io.EOF {
return nil, ErrNoState
}
return nil, err
}
if err := testForV0State(buf); err != nil { if err := testForV0State(buf); err != nil {
return nil, err return nil, err
} }

View File

@ -0,0 +1,8 @@
module "mod" {
source = "./mod"
}
resource "aws_instance" "c" {
name = "${module.mod.output}"
}

View File

@ -0,0 +1,15 @@
locals {
"one" = 1
}
resource "aws_instance" "a" {
count = "${local.one}"
}
resource "aws_instance" "b" {
count = "${local.one}"
}
output "output" {
value = "${join("", coalescelist(aws_instance.a.*.id, aws_instance.b.*.id))}"
}

View File

@ -217,6 +217,12 @@ func filterPartialOutputs(v interface{}, targetedNodes *dag.Set, g *Graph) bool
if _, ok := d.(*NodeCountBoundary); ok { if _, ok := d.(*NodeCountBoundary); ok {
continue continue
} }
if !targetedNodes.Include(d) {
// this one is going to be removed, so it doesn't count
continue
}
// as soon as we see a real dependency, we mark this as // as soon as we see a real dependency, we mark this as
// non-removable // non-removable
return true return true

View File

@ -53,6 +53,12 @@ providers {
# two expressions match different versions then _both_ are included in # two expressions match different versions then _both_ are included in
# the bundle archive. # the bundle archive.
google = ["~> 1.0", "~> 2.0"] google = ["~> 1.0", "~> 2.0"]
# Include a custom plugin to the bundle. Will search for the plugin in the
# plugins directory, and package it with the bundle archive. Plugin must have
# a name of the form: terraform-provider-*, and must be build with the operating
# system and architecture that terraform enterprise is running, e.g. linux and amd64
customplugin = ["0.1"]
} }
``` ```
@ -100,6 +106,13 @@ this composite version number so that bundle archives can be easily
distinguished from official release archives and from each other when multiple distinguished from official release archives and from each other when multiple
bundles contain the same core Terraform version. bundles contain the same core Terraform version.
To include custom plugins in the bundle file, create a local directory "./plugins"
and put all the plugins you want to include there. Optionally, you can use the
`-plugin-dir` flag to specify a location where to find the plugins. To be recognized
as a valid plugin, the file must have a name of the form: "terraform-provider-*-v*". In
addition, ensure that the plugin is build using the same operating system and
architecture used for terraform enterprise. Typically this will be linux and amd64.
## Provider Resolution Behavior ## Provider Resolution Behavior
Terraform's provider resolution behavior is such that if a given constraint Terraform's provider resolution behavior is such that if a given constraint
@ -112,13 +125,6 @@ of the versions available from the bundle. If a suitable version cannot be
found in the bundle, Terraform _will_ attempt to satisfy that dependency by found in the bundle, Terraform _will_ attempt to satisfy that dependency by
automatic installation from the official repository. automatic installation from the official repository.
To disable automatic installation altogether -- and thus cause a hard failure
if no local plugins match -- the `-plugin-dir` option can be passed to
`terraform init`, giving the directory into which the bundle was extracted.
The presence of this option overrides all of the normal automatic discovery
and installation behavior, and thus forces the use of only the plugins that
can be found in the directory indicated.
The downloaded provider archives are verified using the same signature check The downloaded provider archives are verified using the same signature check
that is used for auto-installed plugins, using Hashicorp's release key. At that is used for auto-installed plugins, using Hashicorp's release key. At
this time, the core Terraform archive itself is _not_ verified in this way; this time, the core Terraform archive itself is _not_ verified in this way;

View File

@ -15,7 +15,7 @@ import (
getter "github.com/hashicorp/go-getter" getter "github.com/hashicorp/go-getter"
"github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/plugin/discovery" discovery "github.com/hashicorp/terraform/plugin/discovery"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -23,10 +23,66 @@ type PackageCommand struct {
ui cli.Ui ui cli.Ui
} }
// shameless stackoverflow copy + pasta https://stackoverflow.com/questions/21060945/simple-way-to-copy-a-file-in-golang
func CopyFile(src, dst string) (err error) {
sfi, err := os.Stat(src)
if err != nil {
return
}
if !sfi.Mode().IsRegular() {
// cannot copy non-regular files (e.g., directories,
// symlinks, devices, etc.)
return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
}
dfi, err := os.Stat(dst)
if err != nil {
if !os.IsNotExist(err) {
return
}
} else {
if !(dfi.Mode().IsRegular()) {
return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
}
if os.SameFile(sfi, dfi) {
return
}
}
if err = os.Link(src, dst); err == nil {
return
}
err = copyFileContents(src, dst)
return
}
// see above
func copyFileContents(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
cerr := out.Close()
if err == nil {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return
}
err = out.Sync()
return
}
func (c *PackageCommand) Run(args []string) int { func (c *PackageCommand) Run(args []string) int {
flags := flag.NewFlagSet("package", flag.ExitOnError) flags := flag.NewFlagSet("package", flag.ExitOnError)
osPtr := flags.String("os", "", "Target operating system") osPtr := flags.String("os", "", "Target operating system")
archPtr := flags.String("arch", "", "Target CPU architecture") archPtr := flags.String("arch", "", "Target CPU architecture")
pluginDirPtr := flags.String("plugin-dir", "", "Path to custom plugins directory")
err := flags.Parse(args) err := flags.Parse(args)
if err != nil { if err != nil {
c.ui.Error(err.Error()) c.ui.Error(err.Error())
@ -35,12 +91,16 @@ func (c *PackageCommand) Run(args []string) int {
osName := runtime.GOOS osName := runtime.GOOS
archName := runtime.GOARCH archName := runtime.GOARCH
pluginDir := "./plugins"
if *osPtr != "" { if *osPtr != "" {
osName = *osPtr osName = *osPtr
} }
if *archPtr != "" { if *archPtr != "" {
archName = *archPtr archName = *archPtr
} }
if *pluginDirPtr != "" {
pluginDir = *pluginDirPtr
}
if flags.NArg() != 1 { if flags.NArg() != 1 {
c.ui.Error("Configuration filename is required") c.ui.Error("Configuration filename is required")
@ -70,10 +130,17 @@ func (c *PackageCommand) Run(args []string) int {
coreZipURL := c.coreURL(config.Terraform.Version, osName, archName) coreZipURL := c.coreURL(config.Terraform.Version, osName, archName)
err = getter.Get(workDir, coreZipURL) err = getter.Get(workDir, coreZipURL)
if err != nil { if err != nil {
c.ui.Error(fmt.Sprintf("Failed to fetch core package from %s: %s", coreZipURL, err)) c.ui.Error(fmt.Sprintf("Failed to fetch core package from %s: %s", coreZipURL, err))
} }
c.ui.Info(fmt.Sprintf("Fetching 3rd party plugins in directory: %s", pluginDir))
dirs := []string{pluginDir} //FindPlugins requires an array
localPlugins := discovery.FindPlugins("provider", dirs)
for k, _ := range localPlugins {
c.ui.Info(fmt.Sprintf("plugin: %s (%s)", k.Name, k.Version))
}
installer := &discovery.ProviderInstaller{ installer := &discovery.ProviderInstaller{
Dir: workDir, Dir: workDir,
@ -92,19 +159,29 @@ func (c *PackageCommand) Run(args []string) int {
Ui: c.ui, Ui: c.ui,
} }
if len(config.Providers) > 0 { for name, constraintStrs := range config.Providers {
c.ui.Output(fmt.Sprintf("Checking for available provider plugins on %s...", for _, constraintStr := range constraintStrs {
discovery.GetReleaseHost()))
}
for name, constraints := range config.Providers {
for _, constraint := range constraints {
c.ui.Output(fmt.Sprintf("- Resolving %q provider (%s)...", c.ui.Output(fmt.Sprintf("- Resolving %q provider (%s)...",
name, constraint)) name, constraintStr))
_, err := installer.Get(name, constraint.MustParse()) foundPlugins := discovery.PluginMetaSet{}
if err != nil { constraint := constraintStr.MustParse()
c.ui.Error(fmt.Sprintf("- Failed to resolve %s provider %s: %s", name, constraint, err)) for plugin, _ := range localPlugins {
return 1 if plugin.Name == name && constraint.Allows(plugin.Version.MustParse()) {
foundPlugins.Add(plugin)
}
}
if len(foundPlugins) > 0 {
plugin := foundPlugins.Newest()
CopyFile(plugin.Path, workDir+"/terraform-provider-"+plugin.Name+"-v"+plugin.Version.MustParse().String()) //put into temp dir
} else { //attempt to get from the public registry if not found locally
c.ui.Output(fmt.Sprintf("- Checking for provider plugin on %s...",
discovery.GetReleaseHost()))
_, err := installer.Get(name, constraint)
if err != nil {
c.ui.Error(fmt.Sprintf("- Failed to resolve %s provider %s: %s", name, constraint, err))
return 1
}
} }
} }
} }
@ -202,11 +279,13 @@ current working directory containing a Terraform binary along with zero or
more provider plugin binaries. more provider plugin binaries.
Options: Options:
-os=name Target operating system the archive will be built for. Defaults -os=name Target operating system the archive will be built for. Defaults
to that of the system where the command is being run. to that of the system where the command is being run.
-arch=name Target CPU architecture the archive will be built for. Defaults -arch=name Target CPU architecture the archive will be built for. Defaults
to that of the system where the command is being run. to that of the system where the command is being run.
-plugin-dir=path The path to the custom plugins directory. Defaults to "./plugins".
The resulting zip file can be used to more easily install Terraform and The resulting zip file can be used to more easily install Terraform and
a fixed set of providers together on a server, so that Terraform's provider a fixed set of providers together on a server, so that Terraform's provider
@ -233,7 +312,13 @@ not a normal Terraform configuration file. The file format looks like this:
# Each item in these lists allows a distinct version to be added. If the # Each item in these lists allows a distinct version to be added. If the
# two expressions match different versions then _both_ are included in # two expressions match different versions then _both_ are included in
# the bundle archive. # the bundle archive.
google = ["~> 1.0", "~> 2.0"] google = ["~> 1.0", "~> 2.0"]
#Include a custom plugin to the bundle. Will search for the plugin in the
#plugins directory, and package it with the bundle archive. Plugin must have
#a name of the form: terraform-provider-*-v*, and must be built with the operating
#system and architecture that terraform enterprise is running, e.g. linux and amd64
customplugin = ["0.1"]
} }
` `

View File

@ -11,7 +11,7 @@ import (
) )
// The main version number that is being run at the moment. // The main version number that is being run at the moment.
const Version = "0.11.4" const Version = "0.11.5"
// A pre-release marker for the version. If this is "" (empty string) // A pre-release marker for the version. If this is "" (empty string)
// then it means that it is a final release. Otherwise, this is a pre-release // then it means that it is a final release. Otherwise, this is a pre-release

View File

@ -51,7 +51,7 @@ a configuration in the future: create the new configuration and run
You do not need to specify every required argument in the backend configuration. You do not need to specify every required argument in the backend configuration.
Omitting certain arguments may be desirable to avoid storing secrets, such as Omitting certain arguments may be desirable to avoid storing secrets, such as
access keys, within the main configuration. When some or all of the arguments access keys, within the main configuration. When some or all of the arguments
are ommitted, we call this a _partial configuration_. are omitted, we call this a _partial configuration_.
With a partial configuration, the remaining configuration arguments must be With a partial configuration, the remaining configuration arguments must be
provided as part of provided as part of

View File

@ -3,7 +3,7 @@ layout: "docs"
page_title: "Community Providers" page_title: "Community Providers"
sidebar_current: "docs-providers-community" sidebar_current: "docs-providers-community"
description: |- description: |-
Category for database vendors. Category for community-built providers.
--- ---
# Community Providers # Community Providers

View File

@ -3,7 +3,7 @@ layout: "docs"
page_title: "Infrastructure Software Providers" page_title: "Infrastructure Software Providers"
sidebar_current: "docs-providers-infra" sidebar_current: "docs-providers-infra"
description: |- description: |-
Category for standard cloud vendors. Category for infrastructure management vendors.
--- ---
# Infrastructure Software Providers # Infrastructure Software Providers

View File

@ -3,7 +3,7 @@ layout: "docs"
page_title: "Misc Providers" page_title: "Misc Providers"
sidebar_current: "docs-providers-misc" sidebar_current: "docs-providers-misc"
description: |- description: |-
Category for database vendors. Category for miscellaneous vendors.
--- ---
# Miscellaneous Providers # Miscellaneous Providers

View File

@ -3,7 +3,7 @@ layout: "docs"
page_title: "Network Providers" page_title: "Network Providers"
sidebar_current: "docs-providers-network" sidebar_current: "docs-providers-network"
description: |- description: |-
Category for netowrk vendors. Category for network vendors.
--- ---
# Network Providers # Network Providers

View File

@ -65,6 +65,9 @@ The following arguments are supported:
for the new node. These can also be loaded from a file on disk using the [`file()` for the new node. These can also be loaded from a file on disk using the [`file()`
interpolation function](/docs/configuration/interpolation.html#file_path_). interpolation function](/docs/configuration/interpolation.html#file_path_).
* `channel (string)` - (Optional) The Chef Client release channel to install from. If not
set, the `stable` channel will be used.
* `client_options (array)` - (Optional) A list of optional Chef Client configuration * `client_options (array)` - (Optional) A list of optional Chef Client configuration
options. See the [Chef Client ](https://docs.chef.io/config_rb_client.html) documentation options. See the [Chef Client ](https://docs.chef.io/config_rb_client.html) documentation
for all available options. for all available options.