github.com/chalford/terraform@v0.3.7-0.20150113080010-a78c69a8c81f/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 // AtlasRemoteClient implements the RemoteClient interface 22 // for an Atlas compatible server. 23 type AtlasRemoteClient struct { 24 server string 25 serverURL *url.URL 26 user string 27 name string 28 accessToken string 29 } 30 31 func NewAtlasRemoteClient(conf map[string]string) (*AtlasRemoteClient, error) { 32 client := &AtlasRemoteClient{} 33 if err := client.validateConfig(conf); err != nil { 34 return nil, err 35 } 36 return client, nil 37 } 38 39 func (c *AtlasRemoteClient) validateConfig(conf map[string]string) error { 40 server, ok := conf["address"] 41 if !ok || server == "" { 42 server = defaultAtlasServer 43 } 44 url, err := url.Parse(server) 45 if err != nil { 46 return err 47 } 48 c.server = server 49 c.serverURL = url 50 51 token, ok := conf["access_token"] 52 if token == "" { 53 token = os.Getenv("ATLAS_TOKEN") 54 ok = true 55 } 56 if !ok || token == "" { 57 return fmt.Errorf( 58 "missing 'access_token' configuration or ATLAS_TOKEN environmental variable") 59 } 60 c.accessToken = token 61 62 name, ok := conf["name"] 63 if !ok || name == "" { 64 return fmt.Errorf("missing 'name' configuration") 65 } 66 67 parts := strings.Split(name, "/") 68 if len(parts) != 2 { 69 return fmt.Errorf("malformed name '%s'", name) 70 } 71 c.user = parts[0] 72 c.name = parts[1] 73 return nil 74 } 75 76 func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) { 77 // Make the HTTP request 78 req, err := http.NewRequest("GET", c.url().String(), nil) 79 if err != nil { 80 return nil, fmt.Errorf("Failed to make HTTP request: %v", err) 81 } 82 83 // Request the url 84 resp, err := http.DefaultClient.Do(req) 85 if err != nil { 86 return nil, err 87 } 88 defer resp.Body.Close() 89 90 // Handle the common status codes 91 switch resp.StatusCode { 92 case http.StatusOK: 93 // Handled after 94 case http.StatusNoContent: 95 return nil, nil 96 case http.StatusNotFound: 97 return nil, nil 98 case http.StatusUnauthorized: 99 return nil, ErrRequireAuth 100 case http.StatusForbidden: 101 return nil, ErrInvalidAuth 102 case http.StatusInternalServerError: 103 return nil, ErrRemoteInternal 104 default: 105 return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) 106 } 107 108 // Read in the body 109 buf := bytes.NewBuffer(nil) 110 if _, err := io.Copy(buf, resp.Body); err != nil { 111 return nil, fmt.Errorf("Failed to read remote state: %v", err) 112 } 113 114 // Create the payload 115 payload := &RemoteStatePayload{ 116 State: buf.Bytes(), 117 } 118 119 // Check for the MD5 120 if raw := resp.Header.Get("Content-MD5"); raw != "" { 121 md5, err := base64.StdEncoding.DecodeString(raw) 122 if err != nil { 123 return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err) 124 } 125 payload.MD5 = md5 126 127 } else { 128 // Generate the MD5 129 hash := md5.Sum(payload.State) 130 payload.MD5 = hash[:md5.Size] 131 } 132 133 return payload, nil 134 } 135 136 func (c *AtlasRemoteClient) PutState(state []byte, force bool) error { 137 // Get the target URL 138 base := c.url() 139 140 // Generate the MD5 141 hash := md5.Sum(state) 142 b64 := base64.StdEncoding.EncodeToString(hash[:md5.Size]) 143 144 // Set the force query parameter if needed 145 if force { 146 values := base.Query() 147 values.Set("force", "true") 148 base.RawQuery = values.Encode() 149 } 150 151 // Make the HTTP client and request 152 req, err := http.NewRequest("PUT", base.String(), bytes.NewReader(state)) 153 if err != nil { 154 return fmt.Errorf("Failed to make HTTP request: %v", err) 155 } 156 157 // Prepare the request 158 req.Header.Set("Content-MD5", b64) 159 req.Header.Set("Content-Type", "application/json") 160 req.ContentLength = int64(len(state)) 161 162 // Make the request 163 resp, err := http.DefaultClient.Do(req) 164 if err != nil { 165 return fmt.Errorf("Failed to upload state: %v", err) 166 } 167 defer resp.Body.Close() 168 169 // Handle the error codes 170 switch resp.StatusCode { 171 case http.StatusOK: 172 return nil 173 case http.StatusConflict: 174 return ErrConflict 175 case http.StatusPreconditionFailed: 176 return ErrServerNewer 177 case http.StatusUnauthorized: 178 return ErrRequireAuth 179 case http.StatusForbidden: 180 return ErrInvalidAuth 181 case http.StatusInternalServerError: 182 return ErrRemoteInternal 183 default: 184 return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) 185 } 186 } 187 188 func (c *AtlasRemoteClient) DeleteState() error { 189 // Make the HTTP request 190 req, err := http.NewRequest("DELETE", c.url().String(), nil) 191 if err != nil { 192 return fmt.Errorf("Failed to make HTTP request: %v", err) 193 } 194 195 // Make the request 196 resp, err := http.DefaultClient.Do(req) 197 if err != nil { 198 return fmt.Errorf("Failed to delete state: %v", err) 199 } 200 defer resp.Body.Close() 201 202 // Handle the error codes 203 switch resp.StatusCode { 204 case http.StatusOK: 205 return nil 206 case http.StatusNoContent: 207 return nil 208 case http.StatusNotFound: 209 return nil 210 case http.StatusUnauthorized: 211 return ErrRequireAuth 212 case http.StatusForbidden: 213 return ErrInvalidAuth 214 case http.StatusInternalServerError: 215 return ErrRemoteInternal 216 default: 217 return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) 218 } 219 return nil 220 } 221 222 func (c *AtlasRemoteClient) url() *url.URL { 223 return &url.URL{ 224 Scheme: c.serverURL.Scheme, 225 Host: c.serverURL.Host, 226 Path: path.Join("api/v1/terraform/state", c.user, c.name), 227 RawQuery: fmt.Sprintf("access_token=%s", c.accessToken), 228 } 229 }