github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/http/client.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package http 7 8 import ( 9 "bytes" 10 "crypto/md5" 11 "encoding/base64" 12 "encoding/json" 13 "fmt" 14 "io" 15 "net/http" 16 "net/url" 17 18 "github.com/hashicorp/go-retryablehttp" 19 "github.com/opentofu/opentofu/internal/states/remote" 20 "github.com/opentofu/opentofu/internal/states/statemgr" 21 ) 22 23 // httpClient is a remote client that stores data in Consul or HTTP REST. 24 type httpClient struct { 25 // Update & Retrieve 26 URL *url.URL 27 UpdateMethod string 28 29 // Locking 30 LockURL *url.URL 31 LockMethod string 32 UnlockURL *url.URL 33 UnlockMethod string 34 35 // HTTP 36 Client *retryablehttp.Client 37 Headers map[string]string 38 Username string 39 Password string 40 41 lockID string 42 jsonLockInfo []byte 43 } 44 45 func (c *httpClient) httpRequest(method string, url *url.URL, data *[]byte, what string) (*http.Response, error) { 46 // If we have data we need a reader 47 var reader io.Reader = nil 48 if data != nil { 49 reader = bytes.NewReader(*data) 50 } 51 52 // Create the request 53 req, err := retryablehttp.NewRequest(method, url.String(), reader) 54 if err != nil { 55 return nil, fmt.Errorf("Failed to make %s HTTP request: %w", what, err) 56 } 57 58 // Add user-defined headers 59 for k, v := range c.Headers { 60 req.Header.Set(k, v) 61 } 62 63 if c.Username != "" { 64 req.SetBasicAuth(c.Username, c.Password) 65 } 66 67 // Work with data/body 68 if data != nil { 69 req.Header.Set("Content-Type", "application/json") 70 req.ContentLength = int64(len(*data)) 71 72 // Generate the MD5 73 hash := md5.Sum(*data) 74 b64 := base64.StdEncoding.EncodeToString(hash[:]) 75 req.Header.Set("Content-MD5", b64) 76 } 77 78 // Make the request 79 resp, err := c.Client.Do(req) 80 if err != nil { 81 return nil, fmt.Errorf("Failed to %s: %w", what, err) 82 } 83 84 return resp, nil 85 } 86 87 func (c *httpClient) Lock(info *statemgr.LockInfo) (string, error) { 88 if c.LockURL == nil { 89 return "", nil 90 } 91 c.lockID = "" 92 93 jsonLockInfo := info.Marshal() 94 resp, err := c.httpRequest(c.LockMethod, c.LockURL, &jsonLockInfo, "lock") 95 if err != nil { 96 return "", err 97 } 98 defer resp.Body.Close() 99 100 switch resp.StatusCode { 101 case http.StatusOK: 102 c.lockID = info.ID 103 c.jsonLockInfo = jsonLockInfo 104 return info.ID, nil 105 case http.StatusUnauthorized: 106 return "", fmt.Errorf("HTTP remote state endpoint requires auth") 107 case http.StatusForbidden: 108 return "", fmt.Errorf("HTTP remote state endpoint invalid auth") 109 case http.StatusConflict, http.StatusLocked: 110 defer resp.Body.Close() 111 body, err := io.ReadAll(resp.Body) 112 if err != nil { 113 return "", &statemgr.LockError{ 114 Info: info, 115 Err: fmt.Errorf("HTTP remote state already locked, failed to read body"), 116 } 117 } 118 existing := statemgr.LockInfo{} 119 err = json.Unmarshal(body, &existing) 120 if err != nil { 121 return "", &statemgr.LockError{ 122 Info: info, 123 Err: fmt.Errorf("HTTP remote state already locked, failed to unmarshal body"), 124 } 125 } 126 return "", &statemgr.LockError{ 127 Info: info, 128 Err: fmt.Errorf("HTTP remote state already locked: ID=%s", existing.ID), 129 } 130 default: 131 return "", fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) 132 } 133 } 134 135 func (c *httpClient) Unlock(id string) error { 136 if c.UnlockURL == nil { 137 return nil 138 } 139 140 resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, &c.jsonLockInfo, "unlock") 141 if err != nil { 142 return err 143 } 144 defer resp.Body.Close() 145 146 switch resp.StatusCode { 147 case http.StatusOK: 148 return nil 149 default: 150 return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) 151 } 152 } 153 154 func (c *httpClient) Get() (*remote.Payload, error) { 155 resp, err := c.httpRequest("GET", c.URL, nil, "get state") 156 if err != nil { 157 return nil, err 158 } 159 defer resp.Body.Close() 160 161 // Handle the common status codes 162 switch resp.StatusCode { 163 case http.StatusOK: 164 // Handled after 165 case http.StatusNoContent: 166 return nil, nil 167 case http.StatusNotFound: 168 return nil, nil 169 case http.StatusUnauthorized: 170 return nil, fmt.Errorf("HTTP remote state endpoint requires auth") 171 case http.StatusForbidden: 172 return nil, fmt.Errorf("HTTP remote state endpoint invalid auth") 173 case http.StatusInternalServerError: 174 return nil, fmt.Errorf("HTTP remote state internal server error") 175 default: 176 return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) 177 } 178 179 // Read in the body 180 buf := bytes.NewBuffer(nil) 181 if _, err := io.Copy(buf, resp.Body); err != nil { 182 return nil, fmt.Errorf("Failed to read remote state: %w", err) 183 } 184 185 // Create the payload 186 payload := &remote.Payload{ 187 Data: buf.Bytes(), 188 } 189 190 // If there was no data, then return nil 191 if len(payload.Data) == 0 { 192 return nil, nil 193 } 194 195 // Check for the MD5 196 if raw := resp.Header.Get("Content-MD5"); raw != "" { 197 md5, err := base64.StdEncoding.DecodeString(raw) 198 if err != nil { 199 return nil, fmt.Errorf( 200 "Failed to decode Content-MD5 '%s': %w", raw, err) 201 } 202 203 payload.MD5 = md5 204 } else { 205 // Generate the MD5 206 hash := md5.Sum(payload.Data) 207 payload.MD5 = hash[:] 208 } 209 210 return payload, nil 211 } 212 213 func (c *httpClient) Put(data []byte) error { 214 // Copy the target URL 215 base := *c.URL 216 217 if c.lockID != "" { 218 query := base.Query() 219 query.Set("ID", c.lockID) 220 base.RawQuery = query.Encode() 221 } 222 223 /* 224 // Set the force query parameter if needed 225 if force { 226 values := base.Query() 227 values.Set("force", "true") 228 base.RawQuery = values.Encode() 229 } 230 */ 231 232 var method string = "POST" 233 if c.UpdateMethod != "" { 234 method = c.UpdateMethod 235 } 236 resp, err := c.httpRequest(method, &base, &data, "upload state") 237 if err != nil { 238 return err 239 } 240 defer resp.Body.Close() 241 242 // Handle the error codes 243 switch resp.StatusCode { 244 case http.StatusOK, http.StatusCreated, http.StatusNoContent: 245 return nil 246 default: 247 return fmt.Errorf("HTTP error: %d", resp.StatusCode) 248 } 249 } 250 251 func (c *httpClient) Delete() error { 252 // Make the request 253 resp, err := c.httpRequest("DELETE", c.URL, nil, "delete state") 254 if err != nil { 255 return err 256 } 257 defer resp.Body.Close() 258 259 // Handle the error codes 260 switch resp.StatusCode { 261 case http.StatusOK: 262 return nil 263 default: 264 return fmt.Errorf("HTTP error: %d", resp.StatusCode) 265 } 266 }