diff --git a/state/remote/remote.go b/state/remote/remote.go index cb525e6a3..7ebea3222 100644 --- a/state/remote/remote.go +++ b/state/remote/remote.go @@ -40,6 +40,7 @@ var BuiltinClients = map[string]Factory{ "consul": consulFactory, "http": httpFactory, "s3": s3Factory, + "swift": swiftFactory, // This is used for development purposes only. "_local": fileFactory, diff --git a/state/remote/swift.go b/state/remote/swift.go new file mode 100644 index 000000000..a4293f737 --- /dev/null +++ b/state/remote/swift.go @@ -0,0 +1,114 @@ +package remote + +import ( + "bytes" + "crypto/md5" + "fmt" + "os" + "strings" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects" +) + +const TFSTATE_NAME = "tfstate.tf" + +// SwiftClient implements the Client interface for an Openstack Swift server. +type SwiftClient struct { + client *gophercloud.ServiceClient + path string +} + +func swiftFactory(conf map[string]string) (Client, error) { + client := &SwiftClient{} + + if err := client.validateConfig(conf); err != nil { + return nil, err + } + + return client, nil +} + +func (c *SwiftClient) validateConfig(conf map[string]string) (err error) { + if val := os.Getenv("OS_AUTH_URL"); val == "" { + return fmt.Errorf("missing OS_AUTH_URL environment variable") + } + if val := os.Getenv("OS_USERNAME"); val == "" { + return fmt.Errorf("missing OS_USERNAME environment variable") + } + if val := os.Getenv("OS_TENANT_NAME"); val == "" { + return fmt.Errorf("missing OS_TENANT_NAME environment variable") + } + if val := os.Getenv("OS_PASSWORD"); val == "" { + return fmt.Errorf("missing OS_PASSWORD environment variable") + } + path, ok := conf["path"] + if !ok || path == "" { + return fmt.Errorf("missing 'path' configuration") + } + + provider, err := openstack.AuthenticatedClient(gophercloud.AuthOptions{ + IdentityEndpoint: os.Getenv("OS_AUTH_URL"), + Username: os.Getenv("OS_USERNAME"), + TenantName: os.Getenv("OS_TENANT_NAME"), + Password: os.Getenv("OS_PASSWORD"), + }) + + if err != nil { + return err + } + + c.path = path + c.client, err = openstack.NewObjectStorageV1(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + + return err +} + +func (c *SwiftClient) Get() (*Payload, error) { + result := objects.Download(c.client, c.path, TFSTATE_NAME, nil) + bytes, err := result.ExtractContent() + + if err != nil { + if strings.Contains(err.Error(), "but got 404 instead") { + return nil, nil + } + return nil, err + } + + hash := md5.Sum(bytes) + payload := &Payload{ + Data: bytes, + MD5: hash[:md5.Size], + } + + return payload, nil +} + +func (c *SwiftClient) Put(data []byte) error { + if err := c.ensureContainerExists(); err != nil { + return err + } + + reader := bytes.NewReader(data) + result := objects.Create(c.client, c.path, TFSTATE_NAME, reader, nil) + + return result.Err +} + +func (c *SwiftClient) Delete() error { + result := objects.Delete(c.client, c.path, TFSTATE_NAME, nil) + return result.Err +} + +func (c *SwiftClient) ensureContainerExists() error { + result := containers.Create(c.client, c.path, nil) + if result.Err != nil { + return result.Err + } + + return nil +} diff --git a/state/remote/swift_test.go b/state/remote/swift_test.go new file mode 100644 index 000000000..54666ca29 --- /dev/null +++ b/state/remote/swift_test.go @@ -0,0 +1,25 @@ +package remote + +import ( + "net/http" + "testing" +) + +func TestSwiftClient_impl(t *testing.T) { + var _ Client = new(SwiftClient) +} + +func TestSwiftClient(t *testing.T) { + if _, err := http.Get("http://google.com"); err != nil { + t.Skipf("skipping, internet seems to not be available: %s", err) + } + + client, err := swiftFactory(map[string]string{ + "path": "swift_test", + }) + if err != nil { + t.Fatalf("bad: %s", err) + } + + testClient(t, client) +}