mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-03 20:30:28 -06:00
state/remote: atlas
This commit is contained in:
parent
189e7e700a
commit
4d126998b5
216
state/remote/atlas.go
Normal file
216
state/remote/atlas.go
Normal file
@ -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),
|
||||
}
|
||||
}
|
32
state/remote/atlas_test.go
Normal file
32
state/remote/atlas_test.go
Normal file
@ -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)
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user