206 lines
5.1 KiB
Go
206 lines
5.1 KiB
Go
package remote
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/md5"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
var (
|
|
// ErrConflict is used to indicate the upload was rejected
|
|
// due to a conflict on the state
|
|
ErrConflict = fmt.Errorf("Conflicting state file")
|
|
|
|
// ErrServerNewer is used to indicate the serial number of
|
|
// the state is newer on the server side
|
|
ErrServerNewer = fmt.Errorf("Server-side Serial is newer")
|
|
|
|
// ErrRequireAuth is used if the remote server requires
|
|
// authentication and none is provided
|
|
ErrRequireAuth = fmt.Errorf("Remote server requires authentication")
|
|
|
|
// ErrInvalidAuth is used if we provide authentication which
|
|
// is not valid
|
|
ErrInvalidAuth = fmt.Errorf("Invalid authentication")
|
|
|
|
// ErrRemoteInternal is used if we get an internal error
|
|
// from the remote server
|
|
ErrRemoteInternal = fmt.Errorf("Remote server reporting internal error")
|
|
)
|
|
|
|
// RemoteStatePayload is used to return the remote state
|
|
// along with associated meta data when we do a remote fetch.
|
|
type RemoteStatePayload struct {
|
|
MD5 []byte
|
|
State []byte
|
|
}
|
|
|
|
// remoteStateClient is used to interact with a remote state store
|
|
// using the API
|
|
type remoteStateClient struct {
|
|
conf *terraform.RemoteState
|
|
}
|
|
|
|
// URL is used to return an appropriate URL to hit for the
|
|
// given server and remote name
|
|
func (r *remoteStateClient) URL() (*url.URL, error) {
|
|
// Get the base URL configuration
|
|
base, err := url.Parse(r.conf.Server)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parse remote server '%s': %v", r.conf.Server, err)
|
|
}
|
|
|
|
// Compute the full path by just appending the name
|
|
base.Path = path.Join(base.Path, r.conf.Name)
|
|
|
|
// Add the request token if any
|
|
if r.conf.AuthToken != "" {
|
|
values := base.Query()
|
|
values.Set("access_token", r.conf.AuthToken)
|
|
base.RawQuery = values.Encode()
|
|
}
|
|
return base, nil
|
|
}
|
|
|
|
// GetState is used to read the remote state
|
|
func (r *remoteStateClient) GetState() (*RemoteStatePayload, error) {
|
|
// Get the target URL
|
|
base, err := r.URL()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Request the url
|
|
resp, err := http.Get(base.String())
|
|
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, ErrRequireAuth
|
|
case http.StatusForbidden:
|
|
return nil, ErrInvalidAuth
|
|
case http.StatusInternalServerError:
|
|
return nil, ErrRemoteInternal
|
|
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 := &RemoteStatePayload{
|
|
State: buf.Bytes(),
|
|
}
|
|
|
|
// Check if this is Consul
|
|
if raw := resp.Header.Get("X-Consul-Index"); raw != "" {
|
|
// Check if we used the ?raw query param, otherwise decode
|
|
if _, ok := base.Query()["raw"]; !ok {
|
|
type kv struct {
|
|
Value []byte
|
|
}
|
|
var values []*kv
|
|
if err := json.Unmarshal(buf.Bytes(), &values); err != nil {
|
|
return nil, fmt.Errorf("Failed to decode Consul response: %v", err)
|
|
}
|
|
|
|
// Setup the reader to pull the value from Consul
|
|
payload.State = values[0].Value
|
|
}
|
|
}
|
|
|
|
// 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.State)
|
|
payload.MD5 = hash[:md5.Size]
|
|
}
|
|
|
|
return payload, nil
|
|
}
|
|
|
|
// Put is used to update the remote state
|
|
func (r *remoteStateClient) PutState(state []byte, force bool) error {
|
|
// Get the target URL
|
|
base, err := r.URL()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 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
|
|
client := http.Client{}
|
|
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.ContentLength = int64(len(state))
|
|
|
|
// Make the request
|
|
resp, err := client.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
|
|
case http.StatusConflict:
|
|
return ErrConflict
|
|
case http.StatusPreconditionFailed:
|
|
return ErrServerNewer
|
|
case http.StatusUnauthorized:
|
|
return ErrRequireAuth
|
|
case http.StatusForbidden:
|
|
return ErrInvalidAuth
|
|
case http.StatusInternalServerError:
|
|
return ErrRemoteInternal
|
|
default:
|
|
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
|
|
}
|
|
}
|