github.com/atsaki/terraform@v0.4.3-0.20150919165407-25bba5967654/state/remote/atlas.go (about) 1 package remote 2 3 import ( 4 "bytes" 5 "crypto/md5" 6 "encoding/base64" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "os" 12 "path" 13 "strings" 14 ) 15 16 const ( 17 // defaultAtlasServer is used when no address is given 18 defaultAtlasServer = "https://atlas.hashicorp.com/" 19 ) 20 21 func atlasFactory(conf map[string]string) (Client, error) { 22 var client AtlasClient 23 24 server, ok := conf["address"] 25 if !ok || server == "" { 26 server = defaultAtlasServer 27 } 28 29 url, err := url.Parse(server) 30 if err != nil { 31 return nil, err 32 } 33 34 token, ok := conf["access_token"] 35 if token == "" { 36 token = os.Getenv("ATLAS_TOKEN") 37 ok = true 38 } 39 if !ok || token == "" { 40 return nil, fmt.Errorf( 41 "missing 'access_token' configuration or ATLAS_TOKEN environmental variable") 42 } 43 44 name, ok := conf["name"] 45 if !ok || name == "" { 46 return nil, fmt.Errorf("missing 'name' configuration") 47 } 48 49 parts := strings.Split(name, "/") 50 if len(parts) != 2 { 51 return nil, fmt.Errorf("malformed name '%s', expected format '<account>/<name>'", name) 52 } 53 54 // If it exists, add the `ATLAS_RUN_ID` environment 55 // variable as a param, which is injected during Atlas Terraform 56 // runs. This is completely optional. 57 client.RunId = os.Getenv("ATLAS_RUN_ID") 58 59 client.Server = server 60 client.ServerURL = url 61 client.AccessToken = token 62 client.User = parts[0] 63 client.Name = parts[1] 64 65 return &client, nil 66 } 67 68 // AtlasClient implements the Client interface for an Atlas compatible server. 69 type AtlasClient struct { 70 Server string 71 ServerURL *url.URL 72 User string 73 Name string 74 AccessToken string 75 RunId string 76 } 77 78 func (c *AtlasClient) Get() (*Payload, error) { 79 // Make the HTTP request 80 req, err := http.NewRequest("GET", c.url().String(), nil) 81 if err != nil { 82 return nil, fmt.Errorf("Failed to make HTTP request: %v", err) 83 } 84 85 // Request the url 86 resp, err := http.DefaultClient.Do(req) 87 if err != nil { 88 return nil, err 89 } 90 defer resp.Body.Close() 91 92 // Handle the common status codes 93 switch resp.StatusCode { 94 case http.StatusOK: 95 // Handled after 96 case http.StatusNoContent: 97 return nil, nil 98 case http.StatusNotFound: 99 return nil, nil 100 case http.StatusUnauthorized: 101 return nil, fmt.Errorf("HTTP remote state endpoint requires auth") 102 case http.StatusForbidden: 103 return nil, fmt.Errorf("HTTP remote state endpoint invalid auth") 104 case http.StatusInternalServerError: 105 return nil, fmt.Errorf("HTTP remote state internal server error") 106 default: 107 return nil, fmt.Errorf( 108 "Unexpected HTTP response code: %d\n\nBody: %s", 109 resp.StatusCode, c.readBody(resp.Body)) 110 } 111 112 // Read in the body 113 buf := bytes.NewBuffer(nil) 114 if _, err := io.Copy(buf, resp.Body); err != nil { 115 return nil, fmt.Errorf("Failed to read remote state: %v", err) 116 } 117 118 // Create the payload 119 payload := &Payload{ 120 Data: buf.Bytes(), 121 } 122 123 if len(payload.Data) == 0 { 124 return nil, nil 125 } 126 127 // Check for the MD5 128 if raw := resp.Header.Get("Content-MD5"); raw != "" { 129 md5, err := base64.StdEncoding.DecodeString(raw) 130 if err != nil { 131 return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err) 132 } 133 134 payload.MD5 = md5 135 } else { 136 // Generate the MD5 137 hash := md5.Sum(payload.Data) 138 payload.MD5 = hash[:] 139 } 140 141 return payload, nil 142 } 143 144 func (c *AtlasClient) Put(state []byte) error { 145 // Get the target URL 146 base := c.url() 147 148 // Generate the MD5 149 hash := md5.Sum(state) 150 b64 := base64.StdEncoding.EncodeToString(hash[:]) 151 152 // Make the HTTP client and request 153 req, err := http.NewRequest("PUT", base.String(), bytes.NewReader(state)) 154 if err != nil { 155 return fmt.Errorf("Failed to make HTTP request: %v", err) 156 } 157 158 // Prepare the request 159 req.Header.Set("Content-MD5", b64) 160 req.Header.Set("Content-Type", "application/json") 161 req.ContentLength = int64(len(state)) 162 163 // Make the request 164 resp, err := http.DefaultClient.Do(req) 165 if err != nil { 166 return fmt.Errorf("Failed to upload state: %v", err) 167 } 168 defer resp.Body.Close() 169 170 // Handle the error codes 171 switch resp.StatusCode { 172 case http.StatusOK: 173 return nil 174 default: 175 return fmt.Errorf( 176 "HTTP error: %d\n\nBody: %s", 177 resp.StatusCode, c.readBody(resp.Body)) 178 } 179 } 180 181 func (c *AtlasClient) Delete() error { 182 // Make the HTTP request 183 req, err := http.NewRequest("DELETE", c.url().String(), nil) 184 if err != nil { 185 return fmt.Errorf("Failed to make HTTP request: %v", err) 186 } 187 188 // Make the request 189 resp, err := http.DefaultClient.Do(req) 190 if err != nil { 191 return fmt.Errorf("Failed to delete state: %v", err) 192 } 193 defer resp.Body.Close() 194 195 // Handle the error codes 196 switch resp.StatusCode { 197 case http.StatusOK: 198 return nil 199 case http.StatusNoContent: 200 return nil 201 case http.StatusNotFound: 202 return nil 203 default: 204 return fmt.Errorf( 205 "HTTP error: %d\n\nBody: %s", 206 resp.StatusCode, c.readBody(resp.Body)) 207 } 208 209 return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) 210 } 211 212 func (c *AtlasClient) readBody(b io.Reader) string { 213 var buf bytes.Buffer 214 if _, err := io.Copy(&buf, b); err != nil { 215 return fmt.Sprintf("Error reading body: %s", err) 216 } 217 218 result := buf.String() 219 if result == "" { 220 result = "<empty>" 221 } 222 223 return result 224 } 225 226 func (c *AtlasClient) url() *url.URL { 227 values := url.Values{} 228 229 values.Add("atlas_run_id", c.RunId) 230 values.Add("access_token", c.AccessToken) 231 232 return &url.URL{ 233 Scheme: c.ServerURL.Scheme, 234 Host: c.ServerURL.Host, 235 Path: path.Join("api/v1/terraform/state", c.User, c.Name), 236 RawQuery: values.Encode(), 237 } 238 }