diff --git a/state/remote/atlas.go b/state/remote/atlas.go new file mode 100644 index 0000000000..4714974d64 --- /dev/null +++ b/state/remote/atlas.go @@ -0,0 +1,216 @@ +package remote + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "strings" +) + +const ( + // defaultAtlasServer is used when no address is given + defaultAtlasServer = "https://atlas.hashicorp.com/" +) + +func atlasFactory(conf map[string]string) (Client, error) { + var client AtlasClient + + server, ok := conf["address"] + if !ok || server == "" { + server = defaultAtlasServer + } + + url, err := url.Parse(server) + if err != nil { + return nil, err + } + + token, ok := conf["access_token"] + if token == "" { + token = os.Getenv("ATLAS_TOKEN") + ok = true + } + if !ok || token == "" { + return nil, fmt.Errorf( + "missing 'access_token' configuration or ATLAS_TOKEN environmental variable") + } + + name, ok := conf["name"] + if !ok || name == "" { + return nil, fmt.Errorf("missing 'name' configuration") + } + + parts := strings.Split(name, "/") + if len(parts) != 2 { + return nil, fmt.Errorf("malformed name '%s'", name) + } + + client.Server = server + client.ServerURL = url + client.AccessToken = token + client.User = parts[0] + client.Name = parts[1] + + return &client, nil +} + +// AtlasClient implements the Client interface for an Atlas compatible server. +type AtlasClient struct { + Server string + ServerURL *url.URL + User string + Name string + AccessToken string +} + +func (c *AtlasClient) Get() (*Payload, error) { + // Make the HTTP request + req, err := http.NewRequest("GET", c.url().String(), nil) + if err != nil { + return nil, fmt.Errorf("Failed to make HTTP request: %v", err) + } + + // Request the url + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Handle the common status codes + switch resp.StatusCode { + case http.StatusOK: + // Handled after + case http.StatusNoContent: + return nil, nil + case http.StatusNotFound: + return nil, nil + case http.StatusUnauthorized: + return nil, fmt.Errorf("HTTP remote state endpoint requires auth") + case http.StatusForbidden: + return nil, fmt.Errorf("HTTP remote state endpoint invalid auth") + case http.StatusInternalServerError: + return nil, fmt.Errorf("HTTP remote state internal server error") + default: + return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) + } + + // Read in the body + buf := bytes.NewBuffer(nil) + if _, err := io.Copy(buf, resp.Body); err != nil { + return nil, fmt.Errorf("Failed to read remote state: %v", err) + } + + // Create the payload + payload := &Payload{ + Data: buf.Bytes(), + } + + if len(payload.Data) == 0 { + return nil, nil + } + + // Check for the MD5 + if raw := resp.Header.Get("Content-MD5"); raw != "" { + md5, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err) + } + + payload.MD5 = md5 + } else { + // Generate the MD5 + hash := md5.Sum(payload.Data) + payload.MD5 = hash[:] + } + + return payload, nil +} + +func (c *AtlasClient) Put(state []byte) error { + // Get the target URL + base := c.url() + + // Generate the MD5 + hash := md5.Sum(state) + b64 := base64.StdEncoding.EncodeToString(hash[:md5.Size]) + + /* + // Set the force query parameter if needed + if force { + values := base.Query() + values.Set("force", "true") + base.RawQuery = values.Encode() + } + */ + + // Make the HTTP client and request + req, err := http.NewRequest("PUT", base.String(), bytes.NewReader(state)) + if err != nil { + return fmt.Errorf("Failed to make HTTP request: %v", err) + } + + // Prepare the request + req.Header.Set("Content-MD5", b64) + req.Header.Set("Content-Type", "application/json") + req.ContentLength = int64(len(state)) + + // Make the request + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("Failed to upload state: %v", err) + } + defer resp.Body.Close() + + // Handle the error codes + switch resp.StatusCode { + case http.StatusOK: + return nil + default: + return fmt.Errorf("HTTP error: %d", resp.StatusCode) + } +} + +func (c *AtlasClient) Delete() error { + // Make the HTTP request + req, err := http.NewRequest("DELETE", c.url().String(), nil) + if err != nil { + return fmt.Errorf("Failed to make HTTP request: %v", err) + } + + // Make the request + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("Failed to delete state: %v", err) + } + defer resp.Body.Close() + + // Handle the error codes + switch resp.StatusCode { + case http.StatusOK: + return nil + case http.StatusNoContent: + return nil + case http.StatusNotFound: + return nil + default: + return fmt.Errorf("HTTP error: %d", resp.StatusCode) + } + + return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) +} + +func (c *AtlasClient) url() *url.URL { + return &url.URL{ + Scheme: c.ServerURL.Scheme, + Host: c.ServerURL.Host, + Path: path.Join("api/v1/terraform/state", c.User, c.Name), + RawQuery: fmt.Sprintf("access_token=%s", c.AccessToken), + } +} diff --git a/state/remote/atlas_test.go b/state/remote/atlas_test.go new file mode 100644 index 0000000000..202e15dad9 --- /dev/null +++ b/state/remote/atlas_test.go @@ -0,0 +1,32 @@ +package remote + +import ( + "net/http" + "os" + "testing" +) + +func TestAtlasClient_impl(t *testing.T) { + var _ Client = new(AtlasClient) +} + +func TestAtlasClient(t *testing.T) { + if _, err := http.Get("http://google.com"); err != nil { + t.Skipf("skipping, internet seems to not be available: %s", err) + } + + token := os.Getenv("ATLAS_TOKEN") + if token == "" { + t.Skipf("skipping, ATLAS_TOKEN must be set") + } + + client, err := atlasFactory(map[string]string{ + "access_token": token, + "name": "hashicorp/test-remote-state", + }) + if err != nil { + t.Fatalf("bad: %s", err) + } + + testClient(t, client) +} diff --git a/state/remote/remote.go b/state/remote/remote.go index b148dd3f50..fe730531eb 100644 --- a/state/remote/remote.go +++ b/state/remote/remote.go @@ -36,6 +36,7 @@ func NewClient(t string, conf map[string]string) (Client, error) { // BuiltinClients is the list of built-in clients that can be used with // NewClient. var BuiltinClients = map[string]Factory{ + "atlas": atlasFactory, "consul": consulFactory, "http": httpFactory, }