Skip to content

Commit

Permalink
cluster: settings endpoint moved to backend /app/settings (#250)
Browse files Browse the repository at this point in the history
Signed-off-by: Nick Sieger <[email protected]>
  • Loading branch information
nicksieger authored Aug 30, 2022
1 parent a680629 commit 96c684c
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 54 deletions.
191 changes: 139 additions & 52 deletions pkg/cluster/docker_desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,30 +119,24 @@ func (c DockerDesktopClient) Quit(ctx context.Context) error {
}

func (c DockerDesktopClient) ResetCluster(ctx context.Context) error {
klog.V(7).Infof("POST /kubernetes/reset\n")

req, err := http.NewRequest("POST", "http://localhost/kubernetes/reset", nil)
if err != nil {
return errors.Wrap(err, "reset docker-desktop kubernetes")
}

req.Header.Add("Content-Type", "application/json")
resp, err := c.guiClient.Do(req)
resp, err := c.tryRequests("reset docker-desktop kubernetes", []clientRequest{
{
client: c.backendClient,
method: "POST",
url: "http://localhost/kubernetes/reset",
headers: map[string]string{"Content-Type": "application/json"},
},
{
client: c.guiClient,
method: "POST",
url: "http://localhost/kubernetes/reset",
headers: map[string]string{"Content-Type": "application/json"},
},
})
if err != nil {
return errors.Wrap(err, "reset docker-desktop kubernetes")
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
resp2, err := c.backendClient.Do(req)
if err != nil {
return errors.Wrap(err, "reset docker-desktop kubernetes")
}
defer resp2.Body.Close()
if resp2.StatusCode != http.StatusOK && resp2.StatusCode != http.StatusCreated {
return fmt.Errorf("reset docker-desktop kubernetes: status code %d", resp2.StatusCode)
}
return err
}
resp.Body.Close()
return nil
}

Expand Down Expand Up @@ -265,54 +259,57 @@ func (c DockerDesktopClient) applySet(settings map[string]interface{}, key, newV
}

func (c DockerDesktopClient) writeSettings(ctx context.Context, settings map[string]interface{}) error {
klog.V(7).Infof("POST /settings\n")
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(c.settingsForWrite(settings))
if err != nil {
return errors.Wrap(err, "writing docker-desktop settings")
}

klog.V(8).Infof("Request body: %s\n", buf.String())
req, err := http.NewRequest("POST", "http://localhost/settings", buf)
if err != nil {
return errors.Wrap(err, "writing docker-desktop settings")
}

req.Header.Add("Content-Type", "application/json")
resp, err := c.guiClient.Do(req)
body := buf.Bytes()
resp, err := c.tryRequests("writing docker-desktop settings", []clientRequest{
{
client: c.backendClient,
method: "POST",
url: "http://localhost/app/settings",
headers: map[string]string{"Content-Type": "application/json"},
body: body,
},
{
client: c.guiClient,
method: "POST",
url: "http://localhost/settings",
headers: map[string]string{"Content-Type": "application/json"},
body: body,
},
})
if err != nil {
return errors.Wrap(err, "writing docker-desktop settings")
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("writing docker-desktop settings: status code %d", resp.StatusCode)
return err
}
resp.Body.Close()
return nil
}

func (c DockerDesktopClient) settings(ctx context.Context) (map[string]interface{}, error) {
klog.V(7).Infof("GET /settings\n")
req, err := http.NewRequest("GET", "http://localhost/settings", nil)
if err != nil {
return nil, errors.Wrap(err, "reading docker-desktop settings")
}

resp, err := c.guiClient.Do(req)
resp, err := c.tryRequests("reading docker-desktop settings", []clientRequest{
{
client: c.backendClient,
method: "GET",
url: "http://localhost/app/settings",
},
{
client: c.guiClient,
method: "GET",
url: "http://localhost/settings",
},
})
if err != nil {
return nil, fmt.Errorf("could not connect to Docker Desktop. "+
"Please ensure Docker is installed and up to date.\n (caused by: %v)", err)
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("reading docker-desktop settings: status code %d", resp.StatusCode)
}

settings := make(map[string]interface{})
err = json.NewDecoder(resp.Body).Decode(&settings)
if err != nil {
return nil, errors.Wrap(err, "reading docker settings")
return nil, errors.Wrap(err, "reading docker-desktop settings")
}
klog.V(8).Infof("Response body: %+v\n", settings)
return settings, nil
Expand Down Expand Up @@ -403,3 +400,93 @@ func (c DockerDesktopClient) settingsForWrite(settings interface{}) interface{}

return settings
}

type clientRequest struct {
client HTTPClient
method string
url string
headers map[string]string
body []byte
}

func status2xx(resp *http.Response) bool {
return resp.StatusCode >= 200 && resp.StatusCode <= 204
}

type withStatusCode struct {
error
statusCode int
}

func (w withStatusCode) Cause() error { return w.error }

// tryRequest either returns a 2xx response or an error, but not both.
// If a response is returned, the caller must close its body.
func (c DockerDesktopClient) tryRequest(label string, creq clientRequest) (*http.Response, error) {
klog.V(7).Infof("%s %s\n", creq.method, creq.url)

body := []byte{}
if creq.body != nil {
body = creq.body
klog.V(8).Infof("Request body: %s\n", string(body))
}
req, err := http.NewRequest(creq.method, creq.url, bytes.NewReader(body))
if err != nil {
return nil, errors.Wrap(err, label)
}

for k, v := range creq.headers {
req.Header.Add(k, v)
}

resp, err := creq.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, label)
}
if !status2xx(resp) {
resp.Body.Close()
return nil, withStatusCode{errors.Errorf("%s: status code %d", label, resp.StatusCode), resp.StatusCode}
}

return resp, nil
}

func errorPriority(err error) int {
switch e := err.(type) {
case withStatusCode:
return e.statusCode / 100
default: // give actual errors higher priority than non-2xx status codes
return 10
}
}

func chooseWorstError(errs []error) error {
err := errs[0]
prio := errorPriority(err)
for _, e := range errs[1:] {
if p := errorPriority(e); p > prio {
err = e
prio = p
}
}
return err
}

// tryRequests returns the first 2xx response for the given requests, in order,
// or the "highest priority" error (based on errorPriority) from response
// errors. If a response is returned, the caller must close its body.
func (c DockerDesktopClient) tryRequests(label string, requests []clientRequest) (*http.Response, error) {
if len(requests) == 0 {
panic(fmt.Sprintf("%s: no requests provided", label))
}

errs := []error{}
for _, creq := range requests {
resp, err := c.tryRequest(label, creq)
if err == nil {
return resp, nil
}
errs = append(errs, err)
}
return nil, chooseWorstError(errs)
}
42 changes: 40 additions & 2 deletions pkg/cluster/docker_desktop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
"testing"

"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -224,6 +226,42 @@ func TestSetSettingValueFileSharing(t *testing.T) {

}

func TestChooseWorstError(t *testing.T) {
tt := []struct {
expected string
errors []error
}{
{
"connection error",
[]error{
errors.Wrap(errors.New("connection error"), ""),
withStatusCode{errors.New("404 error"), 404},
},
},
{
"500 error",
[]error{
withStatusCode{errors.New("500 error"), 500},
withStatusCode{errors.New("404 error"), 404},
},
},
{
"first error",
[]error{
errors.Wrap(errors.New("first error"), ""),
errors.Wrap(errors.New("second error"), ""),
},
},
}

for i, tc := range tt {
t.Run(strconv.Itoa(i)+" "+tc.expected, func(t *testing.T) {
err := chooseWorstError(tc.errors)
assert.EqualError(t, errors.Cause(err), tc.expected)
})
}
}

var getSettingsOldJSON = `{"vm":{"proxy":{"exclude":{"value":"","locked":false},"http":{"value":"","locked":false},"https":{"value":"","locked":false},"mode":{"value":"system","locked":false}},"daemon":{"locks":[],"json":"{\"debug\":true,\"experimental\":false}"},"resources":{"cpus":{"value":2,"min":1,"locked":false,"max":8},"memoryMiB":{"value":8192,"min":1024,"locked":false,"max":16384},"diskSizeMiB":{"value":61035,"used":18486,"locked":false},"dataFolder":{"value":"\/Users\/nick\/Library\/Containers\/com.docker.docker\/Data\/vms\/0\/data","locked":false},"swapMiB":{"value":1024,"min":512,"locked":false,"max":4096}},"fileSharing":{"value":[{"path":"\/Users","cached":false},{"path":"\/Volumes","cached":false},{"path":"\/private","cached":false},{"path":"\/tmp","cached":false}],"locked":false},"kubernetes":{"enabled":{"value":false,"locked":false},"stackOrchestrator":{"value":false,"locked":false},"showSystemContainers":{"value":false,"locked":false}},"network":{"dns":{"locked":false},"vpnkitCIDR":{"value":"192.168.65.0\/24","locked":false},"automaticDNS":{"value":true,"locked":false}}},"desktop":{"exportInsecureDaemon":{"value":false,"locked":false},"useGrpcfuse":{"value":true,"locked":false},"backupData":{"value":false,"locked":false},"checkForUpdates":{"value":true,"locked":false},"useCredentialHelper":{"value":true,"locked":false},"autoStart":{"value":false,"locked":false},"analyticsEnabled":{"value":true,"locked":false}},"wslIntegration":{"distros":{"value":[],"locked":false},"enableIntegrationWithDefaultWslDistro":{"value":false,"locked":false}},"cli":{"useCloudCli":{"value":true,"locked":false},"experimental":{"value":true,"locked":false}}}`

var getSettingsJSON = `{"vm":{"proxy":{"exclude":"","http":"","https":"","mode":"system"},"daemon":{"locks":[],"json":"{\"debug\":true,\"experimental\":false}"},"resources":{"cpus":{"value":2,"min":1,"locked":false,"max":8},"memoryMiB":{"value":8192,"min":1024,"locked":false,"max":16384},"diskSizeMiB":{"value":61035,"used":18486,"locked":false},"dataFolder":"\/Users\/nick\/Library\/Containers\/com.docker.docker\/Data\/vms\/0\/data","swapMiB":{"value":1024,"min":512,"locked":false,"max":4096}},"fileSharing":{"value":[{"path":"\/Users","cached":false},{"path":"\/Volumes","cached":false},{"path":"\/private","cached":false},{"path":"\/tmp","cached":false}],"locked":false},"kubernetes":{"enabled":false,"stackOrchestrator":false,"showSystemContainers":false},"network":{"dns":{"locked":false},"vpnkitCIDR":"192.168.65.0\/24","automaticDNS":true}},"desktop":{"exportInsecureDaemon":false,"useGrpcfuse":true,"backupData":false,"checkForUpdates":true,"useCredentialHelper":true,"autoStart":false,"analyticsEnabled":true},"wslIntegration":{"distros":[],"enableIntegrationWithDefaultWslDistro":false},"cli":{"useCloudCli":true,"experimental":true}}`
Expand All @@ -240,7 +278,7 @@ type d4mFixture struct {
func newD4MFixture(t *testing.T) *d4mFixture {
f := &d4mFixture{t: t}
f.settings = getSettingsJSON
f.d4m = &DockerDesktopClient{guiClient: f}
f.d4m = &DockerDesktopClient{guiClient: f, backendClient: f}
return f
}

Expand All @@ -252,7 +290,7 @@ func (f *d4mFixture) readerToMap(r io.Reader) map[string]interface{} {
}

func (f *d4mFixture) Do(r *http.Request) (*http.Response, error) {
require.Equal(f.t, r.URL.Path, "/settings")
require.Equal(f.t, r.URL.Path, "/app/settings")
if r.Method == "POST" {
f.postSettings = f.readerToMap(r.Body)

Expand Down

0 comments on commit 96c684c

Please sign in to comment.