move legacy http remote state to a backend

This commit is contained in:
James Bardin 2018-03-16 21:53:21 -04:00 committed by Martin Atkins
parent 3595bd3f81
commit 18ef072325
7 changed files with 281 additions and 193 deletions

View File

@ -15,6 +15,7 @@ import (
backendetcdv2 "github.com/hashicorp/terraform/backend/remote-state/etcdv2"
backendetcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
backendGCS "github.com/hashicorp/terraform/backend/remote-state/gcs"
backendhttp "github.com/hashicorp/terraform/backend/remote-state/http"
backendinmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
backendManta "github.com/hashicorp/terraform/backend/remote-state/manta"
backendS3 "github.com/hashicorp/terraform/backend/remote-state/s3"
@ -41,17 +42,19 @@ func init() {
// Our hardcoded backends. We don't need to acquire a lock here
// since init() code is serial and can't spawn goroutines.
backends = map[string]func() backend.Backend{
"atlas": func() backend.Backend { return &backendatlas.Backend{} },
"local": func() backend.Backend { return &backendlocal.Local{} },
"consul": func() backend.Backend { return backendconsul.New() },
"inmem": func() backend.Backend { return backendinmem.New() },
"swift": func() backend.Backend { return backendSwift.New() },
"s3": func() backend.Backend { return backendS3.New() },
"azurerm": func() backend.Backend { return backendAzure.New() },
"etcd": func() backend.Backend { return backendetcdv2.New() },
"etcdv3": func() backend.Backend { return backendetcdv3.New() },
"gcs": func() backend.Backend { return backendGCS.New() },
"manta": func() backend.Backend { return backendManta.New() },
"atlas": func() backend.Backend { return &backendatlas.Backend{} },
"http": func() backend.Backend { return backendhttp.New() },
"local": func() backend.Backend { return &backendlocal.Local{} },
"consul": func() backend.Backend { return backendconsul.New() },
"inmem": func() backend.Backend { return backendinmem.New() },
"swift": func() backend.Backend { return backendSwift.New() },
"s3": func() backend.Backend { return backendS3.New() },
"azurerm": func() backend.Backend { return backendAzure.New() },
"etcd": func() backend.Backend { return backendetcdv2.New() },
"etcdv3": func() backend.Backend { return backendetcdv3.New() },
"gcs": func() backend.Backend { return backendGCS.New() },
"manta": func() backend.Backend { return backendManta.New() },
}
"azure": func() backend.Backend {
return deprecateBackend(

View File

@ -17,6 +17,7 @@ func TestBackend_impl(t *testing.T) {
}
func TestBackend(t *testing.T) {
t.Skip()
td, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)

View File

@ -0,0 +1,166 @@
package http
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"net/url"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
)
func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The address of the REST endpoint",
},
"update_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "POST",
Description: "HTTP method to use when updating state",
},
"lock_address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The address of the lock REST endpoint",
},
"unlock_address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The address of the unlock REST endpoint",
},
"lock_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "LOCK",
Description: "The HTTP method to use when locking",
},
"unlock_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "UNLOCK",
Description: "The HTTP method to use when unlocking",
},
"username": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The username for HTTP basic authentication",
},
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The password for HTTP basic authentication",
},
"skip_cert_verification": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Whether to skip TLS verification.",
},
},
}
b := &Backend{Backend: s}
b.Backend.ConfigureFunc = b.configure
return b
}
type Backend struct {
*schema.Backend
client *httpClient
}
func (b *Backend) configure(ctx context.Context) error {
data := schema.FromContextBackendConfig(ctx)
address := data.Get("address").(string)
updateURL, err := url.Parse(address)
if err != nil {
return fmt.Errorf("failed to parse address URL: %s", err)
}
if updateURL.Scheme != "http" && updateURL.Scheme != "https" {
return fmt.Errorf("address must be HTTP or HTTPS")
}
updateMethod := data.Get("update_method").(string)
var lockURL *url.URL
if v, ok := data.GetOk("lock_address"); ok && v.(string) != "" {
var err error
lockURL, err = url.Parse(v.(string))
if err != nil {
return fmt.Errorf("failed to parse lockAddress URL: %s", err)
}
if lockURL.Scheme != "http" && lockURL.Scheme != "https" {
return fmt.Errorf("lockAddress must be HTTP or HTTPS")
}
}
lockMethod := data.Get("lock_method").(string)
var unlockURL *url.URL
if v, ok := data.GetOk("unlock_address"); ok && v.(string) != "" {
var err error
unlockURL, err = url.Parse(v.(string))
if err != nil {
return fmt.Errorf("failed to parse unlockAddress URL: %s", err)
}
if unlockURL.Scheme != "http" && unlockURL.Scheme != "https" {
return fmt.Errorf("unlockAddress must be HTTP or HTTPS")
}
}
unlockMethod := data.Get("unlock_method").(string)
client := cleanhttp.DefaultPooledClient()
if data.Get("skip_cert_verification").(bool) {
// ignores TLS verification
client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
b.client = &httpClient{
URL: updateURL,
UpdateMethod: updateMethod,
LockURL: lockURL,
LockMethod: lockMethod,
UnlockURL: unlockURL,
UnlockMethod: unlockMethod,
Username: data.Get("username").(string),
Password: data.Get("password").(string),
// accessible only for testing use
Client: client,
}
return nil
}
func (b *Backend) State(name string) (state.State, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrNamedStatesNotSupported
}
return &remote.State{Client: b.client}, nil
}
func (b *Backend) States() ([]string, error) {
return nil, backend.ErrNamedStatesNotSupported
}
func (b *Backend) DeleteState(string) error {
return backend.ErrNamedStatesNotSupported
}

View File

@ -0,0 +1,74 @@
package http
import (
"testing"
"github.com/hashicorp/terraform/backend"
)
func TestBackend_impl(t *testing.T) {
var _ backend.Backend = new(Backend)
}
func TestHTTPClientFactory(t *testing.T) {
// defaults
conf := map[string]interface{}{
"address": "http://127.0.0.1:8888/foo",
}
b := backend.TestBackendConfig(t, New(), conf).(*Backend)
client := b.client
if client == nil {
t.Fatal("Unexpected failure, address")
}
if client.URL.String() != conf["address"] {
t.Fatalf("Expected address \"%s\", got \"%s\"", conf["address"], client.URL.String())
}
if client.UpdateMethod != "POST" {
t.Fatalf("Expected update_method \"%s\", got \"%s\"", "POST", client.UpdateMethod)
}
if client.LockURL != nil || client.LockMethod != "LOCK" {
t.Fatal("Unexpected lock_address or lock_method")
}
if client.UnlockURL != nil || client.UnlockMethod != "UNLOCK" {
t.Fatal("Unexpected unlock_address or unlock_method")
}
if client.Username != "" || client.Password != "" {
t.Fatal("Unexpected username or password")
}
// custom
conf = map[string]interface{}{
"address": "http://127.0.0.1:8888/foo",
"update_method": "BLAH",
"lock_address": "http://127.0.0.1:8888/bar",
"lock_method": "BLIP",
"unlock_address": "http://127.0.0.1:8888/baz",
"unlock_method": "BLOOP",
"username": "user",
"password": "pass",
}
b = backend.TestBackendConfig(t, New(), conf).(*Backend)
client = b.client
if client == nil {
t.Fatal("Unexpected failure, update_method")
}
if client.UpdateMethod != "BLAH" {
t.Fatalf("Expected update_method \"%s\", got \"%s\"", "BLAH", client.UpdateMethod)
}
if client.LockURL.String() != conf["lock_address"] || client.LockMethod != "BLIP" {
t.Fatalf("Unexpected lock_address \"%s\" vs \"%s\" or lock_method \"%s\" vs \"%s\"", client.LockURL.String(),
conf["lock_address"], client.LockMethod, conf["lock_method"])
}
if client.UnlockURL.String() != conf["unlock_address"] || client.UnlockMethod != "BLOOP" {
t.Fatalf("Unexpected unlock_address \"%s\" vs \"%s\" or unlock_method \"%s\" vs \"%s\"", client.UnlockURL.String(),
conf["unlock_address"], client.UnlockMethod, conf["unlock_method"])
}
if client.Username != "user" || client.Password != "pass" {
t.Fatalf("Unexpected username \"%s\" vs \"%s\" or password \"%s\" vs \"%s\"", client.Username, conf["username"],
client.Password, conf["password"])
}
}

View File

@ -1,9 +1,8 @@
package remote
package http
import (
"bytes"
"crypto/md5"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
@ -11,104 +10,13 @@ import (
"io/ioutil"
"net/http"
"net/url"
"strconv"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
)
func httpFactory(conf map[string]string) (Client, error) {
address, ok := conf["address"]
if !ok {
return nil, fmt.Errorf("missing 'address' configuration")
}
updateURL, err := url.Parse(address)
if err != nil {
return nil, fmt.Errorf("failed to parse address URL: %s", err)
}
if updateURL.Scheme != "http" && updateURL.Scheme != "https" {
return nil, fmt.Errorf("address must be HTTP or HTTPS")
}
updateMethod, ok := conf["update_method"]
if !ok {
updateMethod = "POST"
}
var lockURL *url.URL
if lockAddress, ok := conf["lock_address"]; ok {
var err error
lockURL, err = url.Parse(lockAddress)
if err != nil {
return nil, fmt.Errorf("failed to parse lockAddress URL: %s", err)
}
if lockURL.Scheme != "http" && lockURL.Scheme != "https" {
return nil, fmt.Errorf("lockAddress must be HTTP or HTTPS")
}
} else {
lockURL = nil
}
lockMethod, ok := conf["lock_method"]
if !ok {
lockMethod = "LOCK"
}
var unlockURL *url.URL
if unlockAddress, ok := conf["unlock_address"]; ok {
var err error
unlockURL, err = url.Parse(unlockAddress)
if err != nil {
return nil, fmt.Errorf("failed to parse unlockAddress URL: %s", err)
}
if unlockURL.Scheme != "http" && unlockURL.Scheme != "https" {
return nil, fmt.Errorf("unlockAddress must be HTTP or HTTPS")
}
} else {
unlockURL = nil
}
unlockMethod, ok := conf["unlock_method"]
if !ok {
unlockMethod = "UNLOCK"
}
client := &http.Client{}
if skipRaw, ok := conf["skip_cert_verification"]; ok {
skip, err := strconv.ParseBool(skipRaw)
if err != nil {
return nil, fmt.Errorf("skip_cert_verification must be boolean")
}
if skip {
// Replace the client with one that ignores TLS verification
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
}
}
ret := &HTTPClient{
URL: updateURL,
UpdateMethod: updateMethod,
LockURL: lockURL,
LockMethod: lockMethod,
UnlockURL: unlockURL,
UnlockMethod: unlockMethod,
Username: conf["username"],
Password: conf["password"],
// accessible only for testing use
Client: client,
}
return ret, nil
}
// HTTPClient is a remote client that stores data in Consul or HTTP REST.
type HTTPClient struct {
// httpClient is a remote client that stores data in Consul or HTTP REST.
type httpClient struct {
// Update & Retrieve
URL *url.URL
UpdateMethod string
@ -128,7 +36,7 @@ type HTTPClient struct {
jsonLockInfo []byte
}
func (c *HTTPClient) httpRequest(method string, url *url.URL, data *[]byte, what string) (*http.Response, error) {
func (c *httpClient) httpRequest(method string, url *url.URL, data *[]byte, what string) (*http.Response, error) {
// If we have data we need a reader
var reader io.Reader = nil
if data != nil {
@ -165,7 +73,7 @@ func (c *HTTPClient) httpRequest(method string, url *url.URL, data *[]byte, what
return resp, nil
}
func (c *HTTPClient) Lock(info *state.LockInfo) (string, error) {
func (c *httpClient) Lock(info *state.LockInfo) (string, error) {
if c.LockURL == nil {
return "", nil
}
@ -204,7 +112,7 @@ func (c *HTTPClient) Lock(info *state.LockInfo) (string, error) {
}
}
func (c *HTTPClient) Unlock(id string) error {
func (c *httpClient) Unlock(id string) error {
if c.UnlockURL == nil {
return nil
}
@ -223,7 +131,7 @@ func (c *HTTPClient) Unlock(id string) error {
}
}
func (c *HTTPClient) Get() (*Payload, error) {
func (c *httpClient) Get() (*remote.Payload, error) {
resp, err := c.httpRequest("GET", c.URL, nil, "get state")
if err != nil {
return nil, err
@ -255,7 +163,7 @@ func (c *HTTPClient) Get() (*Payload, error) {
}
// Create the payload
payload := &Payload{
payload := &remote.Payload{
Data: buf.Bytes(),
}
@ -282,7 +190,7 @@ func (c *HTTPClient) Get() (*Payload, error) {
return payload, nil
}
func (c *HTTPClient) Put(data []byte) error {
func (c *httpClient) Put(data []byte) error {
// Copy the target URL
base := *c.URL
@ -320,7 +228,7 @@ func (c *HTTPClient) Put(data []byte) error {
}
}
func (c *HTTPClient) Delete() error {
func (c *httpClient) Delete() error {
// Make the request
resp, err := c.httpRequest("DELETE", c.URL, nil, "delete state")
if err != nil {

View File

@ -1,4 +1,4 @@
package remote
package http
import (
"bytes"
@ -10,12 +10,13 @@ import (
"reflect"
"testing"
"github.com/hashicorp/go-cleanhttp"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/terraform/state/remote"
)
func TestHTTPClient_impl(t *testing.T) {
var _ Client = new(HTTPClient)
var _ ClientLocker = new(HTTPClient)
var _ remote.Client = new(httpClient)
var _ remote.ClientLocker = new(httpClient)
}
func TestHTTPClient(t *testing.T) {
@ -29,19 +30,19 @@ func TestHTTPClient(t *testing.T) {
}
// Test basic get/update
client := &HTTPClient{URL: url, Client: cleanhttp.DefaultClient()}
testClient(t, client)
client := &httpClient{URL: url, Client: cleanhttp.DefaultClient()}
remote.TestClient(t, client)
// test just a single PUT
p := &HTTPClient{
p := &httpClient{
URL: url,
UpdateMethod: "PUT",
Client: cleanhttp.DefaultClient(),
}
testClient(t, p)
remote.TestClient(t, p)
// Test locking and alternative UpdateMethod
a := &HTTPClient{
a := &httpClient{
URL: url,
UpdateMethod: "PUT",
LockURL: url,
@ -50,7 +51,7 @@ func TestHTTPClient(t *testing.T) {
UnlockMethod: "UNLOCK",
Client: cleanhttp.DefaultClient(),
}
b := &HTTPClient{
b := &httpClient{
URL: url,
UpdateMethod: "PUT",
LockURL: url,
@ -59,7 +60,7 @@ func TestHTTPClient(t *testing.T) {
UnlockMethod: "UNLOCK",
Client: cleanhttp.DefaultClient(),
}
TestRemoteLocks(t, a, b)
remote.TestRemoteLocks(t, a, b)
// test a WebDAV-ish backend
davhandler := new(testHTTPHandler)
@ -67,13 +68,13 @@ func TestHTTPClient(t *testing.T) {
defer ts.Close()
url, err = url.Parse(ts.URL)
c := &HTTPClient{
c := &httpClient{
URL: url,
UpdateMethod: "PUT",
Client: cleanhttp.DefaultClient(),
}
testClient(t, c) // first time through: 201
testClient(t, c) // second time, with identical data: 204
remote.TestClient(t, c) // first time through: 201
remote.TestClient(t, c) // second time, with identical data: 204
}
func assertError(t *testing.T, err error, expected string) {
@ -84,69 +85,6 @@ func assertError(t *testing.T, err error, expected string) {
}
}
func TestHTTPClientFactory(t *testing.T) {
// missing address
_, err := httpFactory(map[string]string{})
assertError(t, err, "missing 'address' configuration")
// defaults
conf := map[string]string{
"address": "http://127.0.0.1:8888/foo",
}
c, err := httpFactory(conf)
client, _ := c.(*HTTPClient)
if client == nil || err != nil {
t.Fatal("Unexpected failure, address")
}
if client.URL.String() != conf["address"] {
t.Fatalf("Expected address \"%s\", got \"%s\"", conf["address"], client.URL.String())
}
if client.UpdateMethod != "POST" {
t.Fatalf("Expected update_method \"%s\", got \"%s\"", "POST", client.UpdateMethod)
}
if client.LockURL != nil || client.LockMethod != "LOCK" {
t.Fatal("Unexpected lock_address or lock_method")
}
if client.UnlockURL != nil || client.UnlockMethod != "UNLOCK" {
t.Fatal("Unexpected unlock_address or unlock_method")
}
if client.Username != "" || client.Password != "" {
t.Fatal("Unexpected username or password")
}
// custom
conf = map[string]string{
"address": "http://127.0.0.1:8888/foo",
"update_method": "BLAH",
"lock_address": "http://127.0.0.1:8888/bar",
"lock_method": "BLIP",
"unlock_address": "http://127.0.0.1:8888/baz",
"unlock_method": "BLOOP",
"username": "user",
"password": "pass",
}
c, err = httpFactory(conf)
client, _ = c.(*HTTPClient)
if client == nil || err != nil {
t.Fatal("Unexpected failure, update_method")
}
if client.UpdateMethod != "BLAH" {
t.Fatalf("Expected update_method \"%s\", got \"%s\"", "BLAH", client.UpdateMethod)
}
if client.LockURL.String() != conf["lock_address"] || client.LockMethod != "BLIP" {
t.Fatalf("Unexpected lock_address \"%s\" vs \"%s\" or lock_method \"%s\" vs \"%s\"", client.LockURL.String(),
conf["lock_address"], client.LockMethod, conf["lock_method"])
}
if client.UnlockURL.String() != conf["unlock_address"] || client.UnlockMethod != "BLOOP" {
t.Fatalf("Unexpected unlock_address \"%s\" vs \"%s\" or unlock_method \"%s\" vs \"%s\"", client.UnlockURL.String(),
conf["unlock_address"], client.UnlockMethod, conf["unlock_method"])
}
if client.Username != "user" || client.Password != "pass" {
t.Fatalf("Unexpected username \"%s\" vs \"%s\" or password \"%s\" vs \"%s\"", client.Username, conf["username"],
client.Password, conf["password"])
}
}
type testHTTPHandler struct {
Data []byte
Locked bool

View File

@ -46,6 +46,4 @@ func NewClient(t string, conf map[string]string) (Client, error) {
// NewClient.
var BuiltinClients = map[string]Factory{
"artifactory": artifactoryFactory,
"http": httpFactory,
"local": fileFactory,
}