Sanitize lock path for the Consul backend when it ends with a /

When the path ends with / (e.g. `path = "tfstate/"), the lock
path used will contain two consecutive slashes (e.g. `tfstate//.lock`) which
Consul does not accept.

This change the lock path so it is sanitized to `tfstate/.lock`.

If the user has two different Terraform project, one with `path = "tfstate"` and
the other with `path = "tfstate/"`, the paths for the locks will be the same
which will be confusing as locking one project will lock both. I wish it were
possible to forbid ending slashes altogether but doing so would require all
users currently having an ending slash in the path to manually move their
Terraform state and would be a poor user experience.

Closes https://github.com/hashicorp/terraform/issues/15747
This commit is contained in:
Rémi Lapeyre 2020-08-13 16:29:43 +02:00
parent ce2bbb151a
commit 032d339915
3 changed files with 97 additions and 60 deletions

View File

@ -1,6 +1,7 @@
package consul package consul
import ( import (
"flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -39,6 +40,10 @@ func newConsulTestServer() (*testutil.TestServer, error) {
srv, err := testutil.NewTestServerConfig(func(c *testutil.TestServerConfig) { srv, err := testutil.NewTestServerConfig(func(c *testutil.TestServerConfig) {
c.LogLevel = "warn" c.LogLevel = "warn"
if !flag.Parsed() {
flag.Parse()
}
if !testing.Verbose() { if !testing.Verbose() {
c.Stdout = ioutil.Discard c.Stdout = ioutil.Discard
c.Stderr = ioutil.Discard c.Stderr = ioutil.Discard

View File

@ -9,6 +9,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"strings"
"sync" "sync"
"time" "time"
@ -161,13 +162,19 @@ func (c *RemoteClient) Delete() error {
return err return err
} }
func (c *RemoteClient) lockPath() string {
// we sanitize the path for the lock as Consul does not like having
// two consecutive slashes for the lock path
return strings.TrimRight(c.Path, "/")
}
func (c *RemoteClient) putLockInfo(info *statemgr.LockInfo) error { func (c *RemoteClient) putLockInfo(info *statemgr.LockInfo) error {
info.Path = c.Path info.Path = c.Path
info.Created = time.Now().UTC() info.Created = time.Now().UTC()
kv := c.Client.KV() kv := c.Client.KV()
_, err := kv.Put(&consulapi.KVPair{ _, err := kv.Put(&consulapi.KVPair{
Key: c.Path + lockInfoSuffix, Key: c.lockPath() + lockInfoSuffix,
Value: info.Marshal(), Value: info.Marshal(),
}, nil) }, nil)
@ -175,7 +182,7 @@ func (c *RemoteClient) putLockInfo(info *statemgr.LockInfo) error {
} }
func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) { func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) {
path := c.Path + lockInfoSuffix path := c.lockPath() + lockInfoSuffix
pair, _, err := c.Client.KV().Get(path, nil) pair, _, err := c.Client.KV().Get(path, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -234,7 +241,7 @@ func (c *RemoteClient) lock() (string, error) {
c.info.Info = "consul session: " + lockSession c.info.Info = "consul session: " + lockSession
opts := &consulapi.LockOptions{ opts := &consulapi.LockOptions{
Key: c.Path + lockSuffix, Key: c.lockPath() + lockSuffix,
Session: lockSession, Session: lockSession,
// only wait briefly, so terraform has the choice to fail fast or // only wait briefly, so terraform has the choice to fail fast or
@ -419,7 +426,7 @@ func (c *RemoteClient) unlock(id string) error {
var errs error var errs error
if _, err := kv.Delete(c.Path+lockInfoSuffix, nil); err != nil { if _, err := kv.Delete(c.lockPath()+lockInfoSuffix, nil); err != nil {
errs = multierror.Append(errs, err) errs = multierror.Append(errs, err)
} }

View File

@ -19,10 +19,17 @@ func TestRemoteClient_impl(t *testing.T) {
} }
func TestRemoteClient(t *testing.T) { func TestRemoteClient(t *testing.T) {
testCases := []string{
fmt.Sprintf("tf-unit/%s", time.Now().String()),
fmt.Sprintf("tf-unit/%s/", time.Now().String()),
}
for _, path := range testCases {
t.Run(path, func(*testing.T) {
// Get the backend // Get the backend
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr, "address": srv.HTTPAddr,
"path": fmt.Sprintf("tf-unit/%s", time.Now().String()), "path": path,
})) }))
// Grab the client // Grab the client
@ -33,6 +40,8 @@ func TestRemoteClient(t *testing.T) {
// Test // Test
remote.TestClient(t, state.(*remote.State).Client) remote.TestClient(t, state.(*remote.State).Client)
})
}
} }
// test the gzip functionality of the client // test the gzip functionality of the client
@ -72,8 +81,13 @@ func TestRemoteClient_gzipUpgrade(t *testing.T) {
} }
func TestConsul_stateLock(t *testing.T) { func TestConsul_stateLock(t *testing.T) {
path := fmt.Sprintf("tf-unit/%s", time.Now().String()) testCases := []string{
fmt.Sprintf("tf-unit/%s", time.Now().String()),
fmt.Sprintf("tf-unit/%s/", time.Now().String()),
}
for _, path := range testCases {
t.Run(path, func(*testing.T) {
// create 2 instances to get 2 remote.Clients // create 2 instances to get 2 remote.Clients
sA, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ sA, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr, "address": srv.HTTPAddr,
@ -92,13 +106,22 @@ func TestConsul_stateLock(t *testing.T) {
} }
remote.TestRemoteLocks(t, sA.(*remote.State).Client, sB.(*remote.State).Client) remote.TestRemoteLocks(t, sA.(*remote.State).Client, sB.(*remote.State).Client)
})
}
} }
func TestConsul_destroyLock(t *testing.T) { func TestConsul_destroyLock(t *testing.T) {
testCases := []string{
fmt.Sprintf("tf-unit/%s", time.Now().String()),
fmt.Sprintf("tf-unit/%s/", time.Now().String()),
}
for _, path := range testCases {
t.Run(path, func(*testing.T) {
// Get the backend // Get the backend
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"address": srv.HTTPAddr, "address": srv.HTTPAddr,
"path": fmt.Sprintf("tf-unit/%s", time.Now().String()), "path": path,
})) }))
// Grab the client // Grab the client
@ -129,6 +152,8 @@ func TestConsul_destroyLock(t *testing.T) {
if pair != nil { if pair != nil {
t.Fatalf("lock key not cleaned up at: %s", pair.Key) t.Fatalf("lock key not cleaned up at: %s", pair.Key)
} }
})
}
} }
func TestConsul_lostLock(t *testing.T) { func TestConsul_lostLock(t *testing.T) {