Skip to content

Commit

Permalink
Add workspaces support to HTTP backend
Browse files Browse the repository at this point in the history
  • Loading branch information
lenaing committed Nov 23, 2022
1 parent 2b14670 commit a51ab9e
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 34 deletions.
77 changes: 69 additions & 8 deletions internal/backend/remote-state/http/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ func New() backend.Backend {
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_ADDRESS", nil),
Description: "The address of the unlock REST endpoint",
},
"workspaces_address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_WORKSPACES_ADDRESS", nil),
Description: "The address of the workspaces REST endpoint",
},
"lock_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Expand All @@ -57,6 +63,18 @@ func New() backend.Backend {
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_METHOD", "UNLOCK"),
Description: "The HTTP method to use when unlocking",
},
"workspaces_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_WORKSPACES_METHOD", "OPTIONS"),
Description: "The HTTP method to use when listing workspaces",
},
"workspaces": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_WORKSPACES", false),
Description: "Wether to enable workspaces use on REST endpoint",
},
"username": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -149,6 +167,27 @@ func (b *Backend) configure(ctx context.Context) error {

unlockMethod := data.Get("unlock_method").(string)

var workspacesURL *url.URL
if v, ok := data.GetOk("workspaces_address"); ok && v.(string) != "" {
var err error
workspacesURL, err = url.Parse(v.(string))
if err != nil {
return fmt.Errorf("failed to parse workspacesAddress URL: %s", err)
}
if workspacesURL.Scheme != "http" && workspacesURL.Scheme != "https" {
return fmt.Errorf("workspacesAddress must be HTTP or HTTPS")
}
}

workspacesMethod := data.Get("workspaces_method").(string)

if data.Get("workspaces").(bool) {
// Use default address if no workspaces_address is provided
if workspacesURL == nil {
workspacesURL = updateURL
}
}

client := cleanhttp.DefaultPooledClient()

if data.Get("skip_cert_verification").(bool) {
Expand All @@ -169,10 +208,12 @@ func (b *Backend) configure(ctx context.Context) error {
URL: updateURL,
UpdateMethod: updateMethod,

LockURL: lockURL,
LockMethod: lockMethod,
UnlockURL: unlockURL,
UnlockMethod: unlockMethod,
LockURL: lockURL,
LockMethod: lockMethod,
UnlockURL: unlockURL,
UnlockMethod: unlockMethod,
WorkspacesURL: workspacesURL,
WorkspacesMethod: workspacesMethod,

Username: data.Get("username").(string),
Password: data.Get("password").(string),
Expand All @@ -185,16 +226,36 @@ func (b *Backend) configure(ctx context.Context) error {

func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrWorkspacesNotSupported
b.client.workspace = name
}

return &remote.State{Client: b.client}, nil
}

func (b *Backend) Workspaces() ([]string, error) {
return nil, backend.ErrWorkspacesNotSupported
if b.client.WorkspacesURL == nil {
return nil, backend.ErrWorkspacesNotSupported
}

workspaces, err := b.client.Workspaces()
if err != nil {
return nil, err
}

result := make([]string, 1, len(workspaces)+1)
result[0] = backend.DefaultStateName
result = append(result, workspaces...)
return result, nil
}

func (b *Backend) DeleteWorkspace(string, bool) error {
return backend.ErrWorkspacesNotSupported
func (b *Backend) DeleteWorkspace(name string, force bool) error {
if b.client.WorkspacesURL == nil {
return backend.ErrWorkspacesNotSupported
}

if name == backend.DefaultStateName || name == "" {
return fmt.Errorf("can't delete default state")
}

return b.client.Delete()
}
62 changes: 40 additions & 22 deletions internal/backend/remote-state/http/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,28 @@ func TestHTTPClientFactory(t *testing.T) {
if client.UnlockURL != nil || client.UnlockMethod != "UNLOCK" {
t.Fatal("Unexpected unlock_address or unlock_method")
}
if client.WorkspacesURL != nil || client.WorkspacesMethod != "OPTIONS" {
t.Fatal("Unexpected workspaces_address or workspaces_method")
}
if client.Username != "" || client.Password != "" {
t.Fatal("Unexpected username or password")
}

// custom
conf = map[string]cty.Value{
"address": cty.StringVal("http://127.0.0.1:8888/foo"),
"update_method": cty.StringVal("BLAH"),
"lock_address": cty.StringVal("http://127.0.0.1:8888/bar"),
"lock_method": cty.StringVal("BLIP"),
"unlock_address": cty.StringVal("http://127.0.0.1:8888/baz"),
"unlock_method": cty.StringVal("BLOOP"),
"username": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"retry_max": cty.StringVal("999"),
"retry_wait_min": cty.StringVal("15"),
"retry_wait_max": cty.StringVal("150"),
"address": cty.StringVal("http://127.0.0.1:8888/foo"),
"update_method": cty.StringVal("BLAH"),
"lock_address": cty.StringVal("http://127.0.0.1:8888/bar"),
"lock_method": cty.StringVal("BLIP"),
"unlock_address": cty.StringVal("http://127.0.0.1:8888/baz"),
"unlock_method": cty.StringVal("BLOOP"),
"workspaces_address": cty.StringVal("http://127.0.0.1:8888/qux"),
"workspaces_method": cty.StringVal("BLUUP"),
"username": cty.StringVal("user"),
"password": cty.StringVal("pass"),
"retry_max": cty.StringVal("999"),
"retry_wait_min": cty.StringVal("15"),
"retry_wait_max": cty.StringVal("150"),
}

b = backend.TestBackendConfig(t, New(), configs.SynthBody("synth", conf)).(*Backend)
Expand All @@ -75,6 +80,10 @@ func TestHTTPClientFactory(t *testing.T) {
t.Fatalf("Unexpected unlock_address \"%s\" vs \"%s\" or unlock_method \"%s\" vs \"%s\"", client.UnlockURL.String(),
conf["unlock_address"].AsString(), client.UnlockMethod, conf["unlock_method"])
}
if client.WorkspacesURL.String() != conf["workspaces_address"].AsString() || client.WorkspacesMethod != "BLUUP" {
t.Fatalf("Unexpected workspaces_address \"%s\" vs \"%s\" or workspaces_method \"%s\" vs \"%s\"", client.WorkspacesURL.String(),
conf["workspaces_address"].AsString(), client.WorkspacesMethod, conf["workspaces_method"])
}
if client.Username != "user" || client.Password != "pass" {
t.Fatalf("Unexpected username \"%s\" vs \"%s\" or password \"%s\" vs \"%s\"", client.Username, conf["username"],
client.Password, conf["password"])
Expand All @@ -93,25 +102,30 @@ func TestHTTPClientFactory(t *testing.T) {
func TestHTTPClientFactoryWithEnv(t *testing.T) {
// env
conf := map[string]string{
"address": "http://127.0.0.1:8888/foo",
"update_method": "BLAH",
"lock_address": "http://127.0.0.1:8888/bar",
"lock_method": "BLIP",
"unlock_address": "http://127.0.0.1:8888/baz",
"unlock_method": "BLOOP",
"username": "user",
"password": "pass",
"retry_max": "999",
"retry_wait_min": "15",
"retry_wait_max": "150",
"address": "http://127.0.0.1:8888/foo",
"update_method": "BLAH",
"lock_address": "http://127.0.0.1:8888/bar",
"lock_method": "BLIP",
"unlock_address": "http://127.0.0.1:8888/baz",
"unlock_method": "BLOOP",
"workspaces_address": "http://127.0.0.1:8888/qux",
"workspaces_method": "BLUUP",
"username": "user",
"password": "pass",
"retry_max": "999",
"retry_wait_min": "15",
"retry_wait_max": "150",
}

defer testWithEnv(t, "TF_HTTP_ADDRESS", conf["address"])()
defer testWithEnv(t, "TF_HTTP_UPDATE_METHOD", conf["update_method"])()
defer testWithEnv(t, "TF_HTTP_LOCK_ADDRESS", conf["lock_address"])()
defer testWithEnv(t, "TF_HTTP_UNLOCK_ADDRESS", conf["unlock_address"])()
defer testWithEnv(t, "TF_HTTP_WORKSPACES_ADDRESS", conf["workspaces_address"])()
defer testWithEnv(t, "TF_HTTP_LOCK_METHOD", conf["lock_method"])()
defer testWithEnv(t, "TF_HTTP_UNLOCK_METHOD", conf["unlock_method"])()
defer testWithEnv(t, "TF_HTTP_WORKSPACES_METHOD", conf["workspaces_method"])()
defer testWithEnv(t, "TF_HTTP_WORKSPACES", conf["workspaces"])()
defer testWithEnv(t, "TF_HTTP_USERNAME", conf["username"])()
defer testWithEnv(t, "TF_HTTP_PASSWORD", conf["password"])()
defer testWithEnv(t, "TF_HTTP_RETRY_MAX", conf["retry_max"])()
Expand All @@ -135,6 +149,10 @@ func TestHTTPClientFactoryWithEnv(t *testing.T) {
t.Fatalf("Unexpected unlock_address \"%s\" vs \"%s\" or unlock_method \"%s\" vs \"%s\"", client.UnlockURL.String(),
conf["unlock_address"], client.UnlockMethod, conf["unlock_method"])
}
if client.WorkspacesURL.String() != conf["workspaces_address"] || client.WorkspacesMethod != "BLUUP" {
t.Fatalf("Unexpected workspaces_address \"%s\" vs \"%s\" or workspaces_method \"%s\" vs \"%s\"", client.WorkspacesURL.String(),
conf["workspaces_address"], client.WorkspacesMethod, conf["workspaces_method"])
}
if client.Username != "user" || client.Password != "pass" {
t.Fatalf("Unexpected username \"%s\" vs \"%s\" or password \"%s\" vs \"%s\"", client.Username, conf["username"],
client.Password, conf["password"])
Expand Down
87 changes: 83 additions & 4 deletions internal/backend/remote-state/http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,16 @@ type httpClient struct {
UnlockURL *url.URL
UnlockMethod string

// Workspaces
WorkspacesURL *url.URL
WorkspacesMethod string

// HTTP
Client *retryablehttp.Client
Username string
Password string

workspace string
lockID string
jsonLockInfo []byte
}
Expand Down Expand Up @@ -80,8 +85,17 @@ func (c *httpClient) Lock(info *statemgr.LockInfo) (string, error) {
}
c.lockID = ""

// Copy the target URL
base := *c.LockURL

if c.workspace != "" {
query := base.Query()
query.Set("workspace", c.workspace)
base.RawQuery = query.Encode()
}

jsonLockInfo := info.Marshal()
resp, err := c.httpRequest(c.LockMethod, c.LockURL, &jsonLockInfo, "lock")
resp, err := c.httpRequest(c.LockMethod, &base, &jsonLockInfo, "lock")
if err != nil {
return "", err
}
Expand Down Expand Up @@ -127,7 +141,16 @@ func (c *httpClient) Unlock(id string) error {
return nil
}

resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, &c.jsonLockInfo, "unlock")
// Copy the target URL
base := *c.UnlockURL

if c.workspace != "" {
query := base.Query()
query.Set("workspace", c.workspace)
base.RawQuery = query.Encode()
}

resp, err := c.httpRequest(c.UnlockMethod, &base, &c.jsonLockInfo, "unlock")
if err != nil {
return err
}
Expand All @@ -142,7 +165,16 @@ func (c *httpClient) Unlock(id string) error {
}

func (c *httpClient) Get() (*remote.Payload, error) {
resp, err := c.httpRequest("GET", c.URL, nil, "get state")
// Copy the target URL
base := *c.URL

if c.workspace != "" {
query := base.Query()
query.Set("workspace", c.workspace)
base.RawQuery = query.Encode()
}

resp, err := c.httpRequest("GET", &base, nil, "get state")
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -210,6 +242,12 @@ func (c *httpClient) Put(data []byte) error {
base.RawQuery = query.Encode()
}

if c.workspace != "" {
query := base.Query()
query.Set("workspace", c.workspace)
base.RawQuery = query.Encode()
}

/*
// Set the force query parameter if needed
if force {
Expand Down Expand Up @@ -239,8 +277,17 @@ func (c *httpClient) Put(data []byte) error {
}

func (c *httpClient) Delete() error {
// Copy the target URL
base := *c.URL

if c.workspace != "" {
query := base.Query()
query.Set("workspace", c.workspace)
base.RawQuery = query.Encode()
}

// Make the request
resp, err := c.httpRequest("DELETE", c.URL, nil, "delete state")
resp, err := c.httpRequest("DELETE", &base, nil, "delete state")
if err != nil {
return err
}
Expand All @@ -254,3 +301,35 @@ func (c *httpClient) Delete() error {
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
}
}

func (c *httpClient) Workspaces() ([]string, error) {
if c.WorkspacesURL == nil {
return nil, nil
}

resp, err := c.httpRequest(c.WorkspacesMethod, c.WorkspacesURL, nil, "get workspaces")
if err != nil {
return nil, err
}

switch resp.StatusCode {
case http.StatusOK:
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("HTTP remote workspaces: failed to read body")
}
var workspaces []string
err = json.Unmarshal(body, &workspaces)
if err != nil {
return nil, fmt.Errorf("HTTP remote workspaces: failed to unmarshal body")
}
return workspaces, nil
case http.StatusUnauthorized:
return nil, fmt.Errorf("HTTP remote workspaces endpoint requires auth")
case http.StatusForbidden:
return nil, fmt.Errorf("HTTP remote workspaces endpoint invalid auth")
default:
return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
}
Loading

0 comments on commit a51ab9e

Please sign in to comment.