Repository: code-ready/crc
Pull request #2105 (2021-03-15 16:17:39)

Improve launchd preflight checks and fix hyperkit startup crash
This makes error reporting a bit better, and makes sure plist files are recreated when their content changed.
For testing, the main change is that the path in `~/Library/LaunchAgents/crc.daemon.plist` is updated when the absolute path of crc changes.
You can either check its content after running `crc setup` with the crc binary being in different directories, or just check `ps aux |grep 'crc daemon'` after running `crc setup`

openshift-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2105#" title="Author self-approved">cfergeau</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=code-ready%2Fcrc).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/code-ready/crc/blob/master/OWNERS)~~ [cfergeau]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
openshift-ci[bot]: @cfergeau: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/e2e-crc | 322c434502352b14db24d328383bdcb7c8d16152 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2105/pull-ci-code-ready-crc-master-e2e-crc/1371495822943850496) | `/test e2e-crc`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=code-ready&repo=crc&pr=2105). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:cfergeau).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
openshift-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2105#pullrequestreview-612980268" title="Approved">anjannath</a>*, *<a href="https://github.com/code-ready/crc/pull/2105#" title="Author self-approved">cfergeau</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=code-ready%2Fcrc).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/code-ready/crc/blob/master/OWNERS)~~ [anjannath,cfergeau]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
openshift-ci-robot: New changes are detected. LGTM label has been removed.

Patch:
diff --git a/cmd/crc/cmd/config/config.go b/cmd/crc/cmd/config/config.go
index b27cc2e5e..317ec9d2f 100644
--- a/cmd/crc/cmd/config/config.go
+++ b/cmd/crc/cmd/config/config.go
@@ -3,6 +3,7 @@ package config
 import (
 	"bytes"
 	"fmt"
+	"runtime"
 	"sort"
 	"strings"
 	"text/tabwriter"
@@ -36,6 +37,16 @@ const (
 )
 
 func RegisterSettings(cfg *config.Config) {
+	validateTrayAutostart := func(value interface{}) (bool, string) {
+		if runtime.GOOS != "darwin" {
+			return false, "Tray autostart is only supported on macOS"
+		}
+		if !cfg.Get(ExperimentalFeatures).AsBool() {
+			return false, fmt.Sprintf("'%s' must be enabled in order to use the tray", ExperimentalFeatures)
+		}
+		return config.ValidateBool(value)
+	}
+
 	validateHostNetworkAccess := func(value interface{}) (bool, string) {
 		mode := network.ParseMode(cfg.Get(NetworkMode).AsString())
 		if mode != network.VSockMode {
@@ -84,7 +95,7 @@ func RegisterSettings(cfg *config.Config) {
 	cfg.AddSetting(HostNetworkAccess, false, validateHostNetworkAccess, config.SuccessfullyApplied,
 		"Allow TCP/IP connections from the CodeReady Containers VM to services running on the host (true/false, default: false)")
 	// System tray auto-start config
-	cfg.AddSetting(AutostartTray, true, tray.ValidateTrayAutostart, disableEnableTrayAutostart,
+	cfg.AddSetting(AutostartTray, true, validateTrayAutostart, disableEnableTrayAutostart,
 		"Automatically start the tray (true/false, default: true)")
 	// Proxy Configuration
 	cfg.AddSetting(HTTPProxy, "", config.ValidateURI, config.SuccessfullyApplied,
diff --git a/cmd/crc/cmd/daemon.go b/cmd/crc/cmd/daemon.go
index a5e7bea32..fec7cd2a3 100644
--- a/cmd/crc/cmd/daemon.go
+++ b/cmd/crc/cmd/daemon.go
@@ -122,6 +122,7 @@ func run(configuration *types.Configuration) error {
 		}
 		mux := http.NewServeMux()
 		mux.Handle("/network/", http.StripPrefix("/network", vn.Mux()))
+		mux.Handle("/api/", http.StripPrefix("/api", api.NewMux(config, newMachine())))
 		if err := http.Serve(listener, mux); err != nil {
 			errCh <- err
 		}
diff --git a/cmd/crc/cmd/delete.go b/cmd/crc/cmd/delete.go
index 01a31ea31..80dba55e7 100644
--- a/cmd/crc/cmd/delete.go
+++ b/cmd/crc/cmd/delete.go
@@ -9,6 +9,7 @@ import (
 	"github.com/code-ready/crc/pkg/crc/constants"
 	crcErrors "github.com/code-ready/crc/pkg/crc/errors"
 	"github.com/code-ready/crc/pkg/crc/input"
+	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/code-ready/crc/pkg/crc/machine"
 	"github.com/spf13/cobra"
 )
@@ -53,7 +54,10 @@ func deleteMachine(client machine.Client, clearCache bool, cacheDir string, inte
 
 	yes := input.PromptUserForYesOrNo("Do you want to delete the OpenShift cluster", force)
 	if yes {
-		return true, client.Delete()
+		if err := client.Delete(); err != nil {
+			return true, err
+		}
+		return true, logging.BackupLogFile()
 	}
 	return false, nil
 }
diff --git a/docs/source/meta/product-attributes.adoc b/docs/source/meta/product-attributes.adoc
index 4aa3b7356..77db6ab14 100644
--- a/docs/source/meta/product-attributes.adoc
+++ b/docs/source/meta/product-attributes.adoc
@@ -36,3 +36,5 @@
 :odo-docs-url: https://docs.openshift.com/container-platform/latest/cli_reference/developer_cli_odo/understanding-odo.html
 :odo-docs-url-installing: https://docs.openshift.com/container-platform/latest/cli_reference/developer_cli_odo/installing-odo.html
 :odo-docs-url-single-component: https://docs.openshift.com/container-platform/latest/cli_reference/developer_cli_odo/creating_and_deploying_applications_with_odo/creating-a-single-component-application-with-odo.html
+
+:rhel-resolved-docs: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/using-different-dns-servers-for-different-domains_configuring-and-managing-networking
diff --git a/docs/source/topics/proc_accessing-the-internal-openshift-registry.adoc b/docs/source/topics/proc_accessing-the-internal-openshift-registry.adoc
index 1353604a8..cb05c6cb8 100644
--- a/docs/source/topics/proc_accessing-the-internal-openshift-registry.adoc
+++ b/docs/source/topics/proc_accessing-the-internal-openshift-registry.adoc
@@ -26,7 +26,7 @@ $ oc whoami
 +
 [NOTE]
 ====
-For demonstration purposes, the current user is assumed to be `kube:admin`.
+For demonstration purposes, the current user is assumed to be `kubeadmin`.
 ====
 
 . Log in to the registry as that user with its token:
@@ -71,11 +71,11 @@ $ podman push default-route-openshift-image-registry.apps-crc.testing/demo/alpin
 $ oc get is
 ----
 
-. Enable image lookup for all tags in the imagestream:
+. Enable image lookup in the imagestream:
 +
 [subs="+quotes,attributes"]
 ----
-$ oc set image-lookup
+$ oc set image-lookup alpine
 ----
 +
 This setting allows the imagestream to be the source of images without having to provide the full URL to the internal registry.
diff --git a/docs/source/topics/ref_dns-configuration.adoc b/docs/source/topics/ref_dns-configuration.adoc
index b80b654f8..1ff4d1c5e 100644
--- a/docs/source/topics/ref_dns-configuration.adoc
+++ b/docs/source/topics/ref_dns-configuration.adoc
@@ -29,6 +29,7 @@ This configuration is used on Fedora 33 or newer, and on Ubuntu Desktop editions
 * `systemd-resolved` configuration is done through a NetworkManager dispatcher script in `/etc/NetworkManager/dispatcher.d/99-crc.sh`:
 +
 ----
+#!/bin/sh
 resolvectl domain crc ~testing
 resolvectl dns crc 192.168.130.11
 resolvectl default-route crc false
@@ -36,6 +37,19 @@ resolvectl default-route crc false
 exit 0
 ----
 
+[NOTE]
+====
+`systemd-resolved` is also available as an unsupported Technology Preview on {rhel} and {centos} 8.3. After {rhel-resolved-docs}[configuring the host] to use `systemd-resolved`, any {prod} `dnsmasq` configurations need to be removed and the above script needs to be created with executable permissions.
+
+Configure the {prod} binary to skip startup checks for `dnsmasq` and NetworkManager configuration:
+
+[subs="+quotes,attributes"]
+----
+$ {bin} config set skip-check-crc-dnsmasq-file true
+$ {bin} config set skip-check-network-manager-config true
+----
+====
+
 === NetworkManager + dnsmasq
 
 This configuration is used on Fedora 32 or older, on {rhel}, and on {centos}.
diff --git a/pkg/crc/api/adapter.go b/pkg/crc/api/adapter.go
index 1334d592c..4abcb41e8 100644
--- a/pkg/crc/api/adapter.go
+++ b/pkg/crc/api/adapter.go
@@ -3,6 +3,7 @@ package api
 import (
 	"context"
 
+	"github.com/code-ready/crc/pkg/crc/api/client"
 	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/code-ready/crc/pkg/crc/machine"
 	"github.com/code-ready/machine/libmachine/state"
@@ -10,43 +11,11 @@ import (
 
 type AdaptedClient interface {
 	GetName() string
-
-	Delete() Result
-	GetConsoleURL() ConsoleResult
-	Start(ctx context.Context, startConfig machine.StartConfig) StartResult
-	Status() ClusterStatusResult
-	Stop() Result
-}
-
-type Result struct {
-	Name    string
-	Success bool
-	Error   string
-}
-
-type StartResult struct {
-	Name           string
-	Status         string
-	Error          string
-	ClusterConfig  machine.ClusterConfig
-	KubeletStarted bool
-}
-
-type ClusterStatusResult struct {
-	Name             string
-	CrcStatus        string
-	OpenshiftStatus  string
-	OpenshiftVersion string
-	DiskUse          int64
-	DiskSize         int64
-	Error            string
-	Success          bool
-}
-
-type ConsoleResult struct {
-	ClusterConfig machine.ClusterConfig
-	Success       bool
-	Error         string
+	Delete() client.Result
+	GetConsoleURL() client.ConsoleResult
+	Start(ctx context.Context, startConfig machine.StartConfig) client.StartResult
+	Status() client.ClusterStatusResult
+	Stop() client.Result
 }
 
 type Adapter struct {
@@ -57,47 +26,47 @@ func (a *Adapter) GetName() string {
 	return a.Underlying.GetName()
 }
 
-func (a *Adapter) Delete() Result {
+func (a *Adapter) Delete() client.Result {
 	err := a.Underlying.Delete()
 	if err != nil {
 		logging.Error(err)
-		return Result{
+		return client.Result{
 			Name:    a.Underlying.GetName(),
 			Success: false,
 			Error:   err.Error(),
 		}
 	}
-	return Result{
+	return client.Result{
 		Name:    a.Underlying.GetName(),
 		Success: true,
 	}
 }
 
-func (a *Adapter) GetConsoleURL() ConsoleResult {
+func (a *Adapter) GetConsoleURL() client.ConsoleResult {
 	res, err := a.Underlying.GetConsoleURL()
 	if err != nil {
 		logging.Error(err)
-		return ConsoleResult{
+		return client.ConsoleResult{
 			Success: false,
 			Error:   err.Error(),
 		}
 	}
-	return ConsoleResult{
+	return client.ConsoleResult{
 		ClusterConfig: res.ClusterConfig,
 		Success:       true,
 	}
 }
 
-func (a *Adapter) Start(ctx context.Context, startConfig machine.StartConfig) StartResult {
+func (a *Adapter) Start(ctx context.Context, startConfig machine.StartConfig) client.StartResult {
 	res, err := a.Underlying.Start(ctx, startConfig)
 	if err != nil {
 		logging.Error(err)
-		return StartResult{
+		return client.StartResult{
 			Name:  a.Underlying.GetName(),
 			Error: err.Error(),
 		}
 	}
-	return StartResult{
+	return client.StartResult{
 		Name:           a.Underlying.GetName(),
 		Status:         res.Status.String(),
 		ClusterConfig:  res.ClusterConfig,
@@ -105,17 +74,17 @@ func (a *Adapter) Start(ctx context.Context, startConfig machine.StartConfig) St
 	}
 }
 
-func (a *Adapter) Status() ClusterStatusResult {
+func (a *Adapter) Status() client.ClusterStatusResult {
 	res, err := a.Underlying.Status()
 	if err != nil {
 		logging.Error(err)
-		return ClusterStatusResult{
+		return client.ClusterStatusResult{
 			Name:    a.Underlying.GetName(),
 			Error:   err.Error(),
 			Success: false,
 		}
 	}
-	return ClusterStatusResult{
+	return client.ClusterStatusResult{
 		Name:             a.Underlying.GetName(),
 		CrcStatus:        res.CrcStatus.String(),
 		OpenshiftStatus:  res.OpenshiftStatus,
@@ -126,7 +95,7 @@ func (a *Adapter) Status() ClusterStatusResult {
 	}
 }
 
-func (a *Adapter) Stop() Result {
+func (a *Adapter) Stop() client.Result {
 	vmState, err := a.Underlying.Stop()
 	if err != nil {
 		logging.Error(err)
@@ -134,7 +103,7 @@ func (a *Adapter) Stop() Result {
 			err := a.Underlying.PowerOff()
 			if err != nil {
 				logging.Error(err)
-				return Result{
+				return client.Result{
 					Name:    a.Underlying.GetName(),
 					Success: false,
 					Error:   err.Error(),
@@ -142,7 +111,7 @@ func (a *Adapter) Stop() Result {
 			}
 		}
 	}
-	return Result{
+	return client.Result{
 		Name:    a.Underlying.GetName(),
 		Success: true,
 	}
diff --git a/pkg/crc/api/api_http.go b/pkg/crc/api/api_http.go
new file mode 100644
index 000000000..f0d0eb7e6
--- /dev/null
+++ b/pkg/crc/api/api_http.go
@@ -0,0 +1,150 @@
+package api
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"strings"
+
+	crcConfig "github.com/code-ready/crc/pkg/crc/config"
+	"github.com/code-ready/crc/pkg/crc/logging"
+	"github.com/code-ready/crc/pkg/crc/machine"
+)
+
+func NewMux(config crcConfig.Storage, machine machine.Client) http.Handler {
+	handler := NewHandler(config, machine)
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/start", func(w http.ResponseWriter, r *http.Request) {
+		var startResult string
+		data, err := verifyRequestAndReadBody(w, r, http.MethodGet, http.MethodPost)
+		if err != nil {
+			return
+		}
+		if len(data) > 0 {
+			startResult = handler.Start(data)
+		} else {
+			startResult = handler.Start(nil)
+		}
+		sendResponse(w, startResult)
+	})
+
+	mux.HandleFunc("/stop", func(w http.ResponseWriter, r *http.Request) {
+		if wrongHTTPMethodUsed(r, w, http.MethodGet) {
+			return
+		}
+		stopResult := handler.Stop()
+		sendResponse(w, stopResult)
+	})
+
+	mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
+		if wrongHTTPMethodUsed(r, w, http.MethodGet) {
+			return
+		}
+		status := handler.Status()
+		sendResponse(w, status)
+	})
+
+	mux.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) {
+		if wrongHTTPMethodUsed(r, w, http.MethodGet) {
+			return
+		}
+		deleteResult := handler.Delete()
+		sendResponse(w, deleteResult)
+	})
+
+	mux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
+		if wrongHTTPMethodUsed(r, w, http.MethodGet) {
+			return
+		}
+		versionResult := handler.GetVersion()
+		sendResponse(w, versionResult)
+	})
+
+	mux.HandleFunc("/webconsoleurl", func(w http.ResponseWriter, r *http.Request) {
+		if wrongHTTPMethodUsed(r, w, http.MethodGet) {
+			return
+		}
+		webconsoleInfo := handler.GetWebconsoleInfo()
+		sendResponse(w, webconsoleInfo)
+	})
+
+	mux.Handle("/config/", http.StripPrefix("/config", newConfigMux(handler)))
+
+	return mux
+}
+
+func newConfigMux(handler *Handler) http.Handler {
+	mux := http.NewServeMux()
+
+	mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
+		var configGetResult string
+		data, err := verifyRequestAndReadBody(w, r, http.MethodGet, http.MethodPost)
+		if err != nil {
+			return
+		}
+		if len(data) > 0 {
+			configGetResult = handler.GetConfig(data)
+		} else {
+			configGetResult = handler.GetConfig(nil)
+		}
+		sendResponse(w, configGetResult)
+	})
+
+	mux.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) {
+		handleConfigSetUnset(w, r, func(data []byte) string { return handler.SetConfig(data) })
+	})
+
+	mux.HandleFunc("/unset", func(w http.ResponseWriter, r *http.Request) {
+		handleConfigSetUnset(w, r, func(data []byte) string { return handler.UnsetConfig(data) })
+	})
+
+	return mux
+}
+
+func wrongHTTPMethodUsed(r *http.Request, w http.ResponseWriter, allowedMethods ...string) bool {
+	for _, method := range allowedMethods {
+		if r.Method == method {
+			return false
+		}
+	}
+	logging.Debugf("Wrong method: %s used for request to: %s", r.Method, r.RequestURI)
+	http.Error(w, fmt.Sprintf("Only %s is allowed.", strings.Join(allowedMethods, ",")), http.StatusMethodNotAllowed)
+	return true
+}
+
+func sendResponse(w http.ResponseWriter, response string) {
+	w.Header().Set("Content-Type", "application/json")
+	if _, err := io.WriteString(w, response); err != nil {
+		logging.Error("Failed to send response: ", err)
+	}
+}
+
+func handleConfigSetUnset(w http.ResponseWriter, r *http.Request, fn func([]byte) string) {
+	data, err := verifyRequestAndReadBody(w, r, http.MethodPost)
+	if err != nil {
+		return
+	}
+	if len(data) < 1 {
+		http.Error(w, "Not enough arguments for /config/(un)set", http.StatusBadRequest)
+		return
+	}
+
+	result := fn(data)
+	sendResponse(w, result)
+}
+
+func verifyRequestAndReadBody(w http.ResponseWriter, r *http.Request, method ...string) ([]byte, error) {
+	if wrongHTTPMethodUsed(r, w, method...) {
+		return nil, fmt.Errorf("Wrong HTTP method used")
+	}
+
+	data, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		logging.Debugf("Unable to read request body for URI: %s", r.RequestURI)
+		http.Error(w, "Unable to read request body", http.StatusInternalServerError)
+		return nil, err
+	}
+	return data, nil
+}
diff --git a/pkg/crc/api/client/client.go b/pkg/crc/api/client/client.go
new file mode 100644
index 000000000..ed34ef7de
--- /dev/null
+++ b/pkg/crc/api/client/client.go
@@ -0,0 +1,236 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/code-ready/crc/pkg/crc/logging"
+)
+
+type Client struct {
+	client *http.Client
+	base   string
+}
+
+func New(client *http.Client, baseURL string) *Client {
+	return &Client{
+		client: client,
+		base:   baseURL,
+	}
+}
+
+func (c *Client) Version() (VersionResult, error) {
+	var vr = VersionResult{}
+	body, err := c.sendGetRequest("/version")
+	if err != nil {
+		logging.Error(err)
+		return vr, err
+	}
+	err = json.Unmarshal(body, &vr)
+	if err != nil {
+		logging.Error(err)
+		return vr, err
+	}
+	return vr, nil
+}
+
+func (c *Client) Status() (ClusterStatusResult, error) {
+	var sr = ClusterStatusResult{}
+	body, err := c.sendGetRequest("/status")
+	if err != nil {
+		logging.Error(err)
+		return sr, err
+	}
+	err = json.Unmarshal(body, &sr)
+	if err != nil {
+		logging.Error(err)
+		return sr, err
+	}
+	return sr, nil
+}
+
+func (c *Client) Start(config StartConfig) (StartResult, error) {
+	var sr = StartResult{}
+	var data = new(bytes.Buffer)
+
+	if config != (StartConfig{}) {
+		if err := json.NewEncoder(data).Encode(config); err != nil {
+			return sr, fmt.Errorf("Failed to encode data to JSON: %w", err)
+		}
+	}
+	body, err := c.sendPostRequest("/start", data)
+	if err != nil {
+		logging.Error(err)
+		return sr, err
+	}
+	err = json.Unmarshal(body, &sr)
+	if err != nil {
+		logging.Error(err)
+		return sr, err
+	}
+	return sr, nil
+}
+
+func (c *Client) Stop() (Result, error) {
+	var sr = Result{}
+	body, err := c.sendGetRequest("/stop")
+	if err != nil {
+		logging.Error(err)
+		return sr, err
+	}
+	err = json.Unmarshal(body, &sr)
+	if err != nil {
+		logging.Error(err)
+		return sr, err
+	}
+	return sr, nil
+}
+
+func (c *Client) Delete() (Result, error) {
+	var dr = Result{}
+	body, err := c.sendGetRequest("/delete")
+	if err != nil {
+		logging.Error(err)
+		return dr, err
+	}
+	err = json.Unmarshal(body, &dr)
+	if err != nil {
+		logging.Error(err)
+		return dr, err
+	}
+	return dr, nil
+}
+
+func (c *Client) WebconsoleURL() (ConsoleResult, error) {
+	var cr = ConsoleResult{}
+	body, err := c.sendGetRequest("/webconsoleurl")
+	if err != nil {
+		logging.Error(err)
+		return cr, err
+	}
+	err = json.Unmarshal(body, &cr)
+	if err != nil {
+		logging.Error(err)
+		return cr, err
+	}
+	return cr, nil
+}
+
+func (c *Client) GetConfig(configs []string) (GetConfigResult, error) {
+	var gcr = GetConfigResult{}
+	var data = new(bytes.Buffer)
+
+	if len(configs) > 0 {
+		cfg := getOrUnsetConfigRequest{
+			Properties: configs,
+		}
+		if err := json.NewEncoder(data).Encode(cfg); err != nil {
+			return gcr, fmt.Errorf("Failed to encode data to JSON: %w", err)
+		}
+	}
+	body, err := c.sendPostRequest("/config/get", data)
+	if err != nil {
+		logging.Error(err)
+		return gcr, err
+	}
+	err = json.Unmarshal(body, &gcr)
+	if err != nil {
+		logging.Error(err)
+		return gcr, err
+	}
+	return gcr, nil
+}
+
+func (c *Client) SetConfig(configs SetConfigRequest) (SetOrUnsetConfigResult, error) {
+	var scr = SetOrUnsetConfigResult{}
+	var data = new(bytes.Buffer)
+
+	if len(configs.Properties) == 0 {
+		return scr, fmt.Errorf("No config key value pair provided to set")
+	}
+
+	if err := json.NewEncoder(data).Encode(configs); err != nil {
+		return scr, fmt.Errorf("Failed to encode data to JSON: %w", err)
+	}
+
+	body, err := c.sendPostRequest("/config/set", data)
+	if err != nil {
+		logging.Error(err)
+		return scr, err
+	}
+
+	err = json.Unmarshal(body, &scr)
+	if err != nil {
+		return scr, err
+	}
+	return scr, nil
+}
+
+func (c *Client) UnsetConfig(configs []string) (SetOrUnsetConfigResult, error) {
+	var ucr = SetOrUnsetConfigResult{}
+	var data = new(bytes.Buffer)
+
+	cfg := getOrUnsetConfigRequest{
+		Properties: configs,
+	}
+	if err := json.NewEncoder(data).Encode(cfg); err != nil {
+		return ucr, fmt.Errorf("Failed to encode data to JSON: %w", err)
+	}
+	body, err := c.sendPostRequest("/config/unset", data)
+	if err != nil {
+		logging.Error(err)
+		return ucr, err
+	}
+	err = json.Unmarshal(body, &ucr)
+	if err != nil {
+		logging.Error(err)
+		return ucr, err
+	}
+	return ucr, nil
+}
+
+func (c *Client) sendGetRequest(url string) ([]byte, error) {
+	res, err := c.client.Get(fmt.Sprintf("%s%s", c.base, url))
+	if err != nil {
+		logging.Error(err)
+		return nil, err
+	}
+	if res.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("Error occurred sending GET request to : %s : %d", url, res.StatusCode)
+	}
+	defer res.Body.Close()
+	body, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return nil, fmt.Errorf("Unknown error reading response: %w", err)
+	}
+	return body, nil
+}
+
+func (c *Client) sendPostRequest(url string, data io.Reader) ([]byte, error) {
+	req, err := http.NewRequest("POST", fmt.Sprintf("%s%s", c.base, url), data)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	res, err := c.client.Do(req)
+	if err != nil {
+		logging.Error(err)
+		return nil, err
+	}
+	if res.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("Error occurred sending POST request to : %s : %d", url, res.StatusCode)
+	}
+
+	defer res.Body.Close()
+
+	body, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return nil, fmt.Errorf("Unknown error reading response: %w", err)
+	}
+	return body, nil
+}
diff --git a/pkg/crc/api/client/types.go b/pkg/crc/api/client/types.go
new file mode 100644
index 000000000..5767ae969
--- /dev/null
+++ b/pkg/crc/api/client/types.go
@@ -0,0 +1,68 @@
+package client
+
+import (
+	"github.com/code-ready/crc/pkg/crc/machine"
+)
+
+type VersionResult struct {
+	CrcVersion       string
+	CommitSha        string
+	OpenshiftVersion string
+	Success          bool
+}
+
+type Result struct {
+	Name    string
+	Success bool
+	Error   string
+}
+
+type StartResult struct {
+	Name           string
+	Status         string
+	Error          string
+	ClusterConfig  machine.ClusterConfig
+	KubeletStarted bool
+}
+
+type ClusterStatusResult struct {
+	Name             string
+	CrcStatus        string
+	OpenshiftStatus  string
+	OpenshiftVersion string
+	DiskUse          int64
+	DiskSize         int64
+	Error            string
+	Success          bool
+}
+
+type ConsoleResult struct {
+	ClusterConfig machine.ClusterConfig
+	Success       bool
+	Error         string
+}
+
+// setOrUnsetConfigResult struct is used to return the result of
+// setconfig/unsetconfig command
+type SetOrUnsetConfigResult struct {
+	Error      string
+	Properties []string
+}
+
+// getConfigResult struct is used to return the result of getconfig command
+type GetConfigResult struct {
+	Error   string
+	Configs map[string]interface{}
+}
+
+type StartConfig struct {
+	PullSecretPath string
+}
+
+type SetConfigRequest struct {
+	Properties map[string]interface{} `json:"properties"`
+}
+
+type getOrUnsetConfigRequest struct {
+	Properties []string `json:"properties"`
+}
diff --git a/pkg/crc/api/handlers.go b/pkg/crc/api/handlers.go
index 5eb57df20..d862cb54b 100644
--- a/pkg/crc/api/handlers.go
+++ b/pkg/crc/api/handlers.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 
 	"github.com/code-ready/crc/cmd/crc/cmd/config"
+	"github.com/code-ready/crc/pkg/crc/api/client"
 	"github.com/code-ready/crc/pkg/crc/cluster"
 	crcConfig "github.com/code-ready/crc/pkg/crc/config"
 	"github.com/code-ready/crc/pkg/crc/errors"
@@ -33,6 +34,15 @@ func (h *Handler) Logs() string {
 	})
 }
 
+func NewHandler(config crcConfig.Storage, machine machine.Client) *Handler {
+	return &Handler{
+		MachineClient: &Adapter{
+			Underlying: machine,
+		},
+		Config: config,
+	}
+}
+
 func (h *Handler) Status() string {
 	clusterStatus := h.MachineClient.Status()
 	return encodeStructToJSON(clusterStatus)
@@ -49,7 +59,7 @@ func (h *Handler) Start(args json.RawMessage) string {
 	if args != nil {
 		parsedArgs, err = parseStartArgs(args)
 		if err != nil {
-			startErr := &StartResult{
+			startErr := &client.StartResult{
 				Name:  h.MachineClient.GetName(),
 				Error: fmt.Sprintf("Incorrect arguments given: %s", err.Error()),
 			}
@@ -57,7 +67,7 @@ func (h *Handler) Start(args json.RawMessage) string {
 		}
 	}
 	if err := preflight.StartPreflightChecks(h.Config); err != nil {
-		startErr := &StartResult{
+		startErr := &client.StartResult{
 			Name:  h.MachineClient.GetName(),
 			Error: err.Error(),
 		}
@@ -89,15 +99,8 @@ func getStartConfig(cfg crcConfig.Storage, args startArgs) machine.StartConfig {
 	}
 }
 
-type VersionResult struct {
-	CrcVersion       string
-	CommitSha        string
-	OpenshiftVersion string
-	Success          bool
-}
-
 func (h *Handler) GetVersion() string {
-	v := &VersionResult{
+	v := &client.VersionResult{
 		CrcVersion:       version.GetCRCVersion(),
 		CommitSha:        version.GetCommitSha(),
 		OpenshiftVersion: version.GetBundleVersion(),
@@ -117,7 +120,7 @@ func (h *Handler) GetWebconsoleInfo() string {
 }
 
 func (h *Handler) SetConfig(args json.RawMessage) string {
-	setConfigResult := setOrUnsetConfigResult{}
+	setConfigResult := client.SetOrUnsetConfigResult{}
 	if args == nil {
 		setConfigResult.Error = "No config keys provided"
 		return encodeStructToJSON(setConfigResult)
@@ -155,7 +158,7 @@ func (h *Handler) SetConfig(args json.RawMessage) string {
 }
 
 func (h *Handler) UnsetConfig(args json.RawMessage) string {
-	unsetConfigResult := setOrUnsetConfigResult{}
+	unsetConfigResult := client.SetOrUnsetConfigResult{}
 	if args == nil {
 		unsetConfigResult.Error = "No config keys provided"
 		return encodeStructToJSON(unsetConfigResult)
@@ -190,7 +193,7 @@ func (h *Handler) UnsetConfig(args json.RawMessage) string {
 }
 
 func (h *Handler) GetConfig(args json.RawMessage) string {
-	configResult := getConfigResult{}
+	configResult := client.GetConfigResult{}
 	if args == nil {
 		allConfigs := h.Config.AllConfigs()
 		configResult.Error = ""
@@ -234,7 +237,7 @@ func encodeStructToJSON(v interface{}) string {
 	s, err := json.Marshal(v)
 	if err != nil {
 		logging.Error(err.Error())
-		err := Result{
+		err := client.Result{
 			Success: false,
 			Error:   "Failed while encoding JSON to string",
 		}
@@ -245,7 +248,7 @@ func encodeStructToJSON(v interface{}) string {
 }
 
 func encodeErrorToJSON(errMsg string) string {
-	err := Result{
+	err := client.Result{
 		Success: false,
 		Error:   errMsg,
 	}
diff --git a/pkg/crc/api/types.go b/pkg/crc/api/types.go
index 4909dfc29..c4aa6cac2 100644
--- a/pkg/crc/api/types.go
+++ b/pkg/crc/api/types.go
@@ -36,19 +36,6 @@ type commandRequest struct {
 	Args    json.RawMessage `json:"args,omitempty"`
 }
 
-// setOrUnsetConfigResult struct is used to return the result of
-// setconfig/unsetconfig command
-type setOrUnsetConfigResult struct {
-	Error      string
-	Properties []string
-}
-
-// getConfigResult struct is used to return the result of getconfig command
-type getConfigResult struct {
-	Error   string
-	Configs map[string]interface{}
-}
-
 // startArgs is used to get the pull secret file path as argument for start handler
 type startArgs struct {
 	PullSecretFile string `json:"pullSecretFile"`
diff --git a/pkg/crc/constants/constants.go b/pkg/crc/constants/constants.go
index ff968e833..447f77687 100644
--- a/pkg/crc/constants/constants.go
+++ b/pkg/crc/constants/constants.go
@@ -27,10 +27,11 @@ const (
 	DaemonLogFile             = "crcd.log"
 	CrcLandingPageURL         = "https://cloud.redhat.com/openshift/create/local" // #nosec G101
 	DefaultPodmanURLBase      = "https://storage.googleapis.com/libpod-master-releases"
-	DefaultAdminHelperCliBase = "https://github.com/code-ready/admin-helper/releases/download/0.0.2"
+	DefaultAdminHelperCliBase = "https://github.com/code-ready/admin-helper/releases/download/0.0.3"
 	CRCMacTrayDownloadURL     = "https://github.com/code-ready/tray-macos/releases/download/v%s/crc-tray-macos.tar.gz"
 	CRCWindowsTrayDownloadURL = "https://github.com/code-ready/tray-windows/releases/download/v%s/crc-tray-windows.zip"
 	DefaultContext            = "admin"
+	DaemonHTTPEndpoint        = "http://unix/api"
 
 	VSockGateway = "192.168.127.1"
 	VsockSSHPort = 2222
diff --git a/pkg/crc/logging/logging.go b/pkg/crc/logging/logging.go
index 8645dc33d..f5d7bb797 100644
--- a/pkg/crc/logging/logging.go
+++ b/pkg/crc/logging/logging.go
@@ -34,8 +34,11 @@ func CloseLogging() {
 	logrus.StandardLogger().ReplaceHooks(make(logrus.LevelHooks))
 }
 
-func BackupLogFile() {
-	os.Rename(logfile.Name(), fmt.Sprintf("%s_%s", logfile.Name(), time.Now().Format("20060102150405"))) // nolint
+func BackupLogFile() error {
+	if logfile == nil {
+		return nil
+	}
+	return os.Rename(logfile.Name(), fmt.Sprintf("%s_%s", logfile.Name(), time.Now().Format("20060102150405")))
 }
 
 func InitLogrus(logLevel, logFilePath string) {
diff --git a/pkg/crc/machine/bundle/repository.go b/pkg/crc/machine/bundle/repository.go
index 135195128..00e680fce 100644
--- a/pkg/crc/machine/bundle/repository.go
+++ b/pkg/crc/machine/bundle/repository.go
@@ -8,9 +8,11 @@ import (
 	"path/filepath"
 	"runtime"
 	"strings"
+	"time"
 
 	"github.com/Masterminds/semver"
 	"github.com/code-ready/crc/pkg/crc/constants"
+	crcerrors "github.com/code-ready/crc/pkg/crc/errors"
 	"github.com/code-ready/crc/pkg/extract"
 	crcos "github.com/code-ready/crc/pkg/os"
 	"github.com/pkg/errors"
@@ -120,7 +122,12 @@ func (repo *Repository) Extract(path string) error {
 	}
 
 	bundleDir := strings.TrimSuffix(bundleName, bundleExtension)
-	return os.Rename(filepath.Join(tmpDir, bundleDir), filepath.Join(repo.CacheDir, bundleDir))
+	return crcerrors.RetryAfter(time.Minute, func() error {
+		if err := os.Rename(filepath.Join(tmpDir, bundleDir), filepath.Join(repo.CacheDir, bundleDir)); err != nil {
+			return crcerrors.RetriableError{Err: err}
+		}
+		return nil
+	}, 5*time.Second)
 }
 
 var defaultRepo = &Repository{
diff --git a/pkg/crc/machine/delete.go b/pkg/crc/machine/delete.go
index ca24ad5f1..535ea55ec 100644
--- a/pkg/crc/machine/delete.go
+++ b/pkg/crc/machine/delete.go
@@ -1,13 +1,11 @@
 package machine
 
 import (
-	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/pkg/errors"
 )
 
 func (client *client) Delete() error {
 	libMachineAPIClient, cleanup := createLibMachineClient()
-	defer logging.BackupLogFile()
 	defer cleanup()
 	host, err := libMachineAPIClient.Load(client.name)
 
diff --git a/pkg/crc/machine/hyperkit/constants.go b/pkg/crc/machine/hyperkit/constants.go
index a3cfe45f8..c11fcca8a 100644
--- a/pkg/crc/machine/hyperkit/constants.go
+++ b/pkg/crc/machine/hyperkit/constants.go
@@ -6,7 +6,7 @@ import "fmt"
 
 const (
 	MachineDriverCommand = "crc-driver-hyperkit"
-	MachineDriverVersion = "0.12.11"
+	MachineDriverVersion = "0.12.12"
 	HyperKitCommand      = "hyperkit"
 	HyperKitVersion      = "v0.20200224-44-gb54460"
 )
diff --git a/pkg/crc/machine/start.go b/pkg/crc/machine/start.go
index 84e54b0bc..c63ae7cee 100644
--- a/pkg/crc/machine/start.go
+++ b/pkg/crc/machine/start.go
@@ -21,7 +21,6 @@ import (
 	"github.com/code-ready/crc/pkg/crc/oc"
 	"github.com/code-ready/crc/pkg/crc/services"
 	"github.com/code-ready/crc/pkg/crc/services/dns"
-	"github.com/code-ready/crc/pkg/crc/ssh"
 	crcssh "github.com/code-ready/crc/pkg/crc/ssh"
 	"github.com/code-ready/crc/pkg/crc/systemd"
 	"github.com/code-ready/crc/pkg/crc/telemetry"
@@ -32,6 +31,8 @@ import (
 	"github.com/code-ready/machine/libmachine/state"
 	"github.com/docker/go-units"
 	"github.com/pkg/errors"
+
+	"golang.org/x/crypto/ssh"
 )
 
 const minimumMemoryForMonitoring = 14336
@@ -92,6 +93,21 @@ func (client *client) updateVMConfig(startConfig StartConfig, api libmachine.API
 	return nil
 }
 
+func growRootFileSystem(sshRunner *crcssh.Runner) error {
+	// With 4.7, this is quite a manual process until https://github.com/openshift/installer/pull/4746 gets fixed
+	// See https://github.com/code-ready/crc/issues/2104 for details
+	if _, _, err := sshRunner.Run("sudo /usr/bin/growpart /dev/vda 4"); err != nil {
+		var sshErr *ssh.ExitError
+		if errors.As(err, &sshErr) {
+			logging.Debugf("/dev/vda4 already has the expected size, nothing to do")
+			return nil
+		}
+
+		return err
+	}
+	_, _, err := sshRunner.Run("sudo mount -o remount,rw /sysroot && sudo xfs_growfs /dev/vda4")
+	return err
+}
 func (client *client) Start(ctx context.Context, startConfig StartConfig) (*StartResult, error) {
 	telemetry.SetCPUs(ctx, startConfig.CPUs)
 	telemetry.SetMemory(ctx, uint64(startConfig.Memory)*1024*1024)
@@ -268,7 +284,7 @@ func (client *client) Start(ctx context.Context, startConfig StartConfig) (*Star
 	}
 
 	// Trigger disk resize, this will be a no-op if no disk size change is needed
-	if _, _, err = sshRunner.Run("sudo xfs_growfs / >/dev/null"); err != nil {
+	if err := growRootFileSystem(sshRunner); err != nil {
 		return nil, errors.Wrap(err, "Error updating filesystem size")
 	}
 
@@ -510,7 +526,7 @@ func updateSSHKeyPair(sshRunner *crcssh.Runner) error {
 
 		// Generate ssh key pair
 		logging.Info("Generating new SSH Key pair ...")
-		if err := ssh.GenerateSSHKey(constants.GetPrivateKeyPath()); err != nil {
+		if err := crcssh.GenerateSSHKey(constants.GetPrivateKeyPath()); err != nil {
 			return fmt.Errorf("Error generating ssh key pair: %v", err)
 		}
 	}
diff --git a/pkg/crc/preflight/preflight_checks_tray_darwin.go b/pkg/crc/preflight/preflight_checks_tray_darwin.go
index bc565bf36..2c3e3a187 100644
--- a/pkg/crc/preflight/preflight_checks_tray_darwin.go
+++ b/pkg/crc/preflight/preflight_checks_tray_darwin.go
@@ -21,33 +21,35 @@ const (
 	trayAgentLabel   = "crc.tray"
 )
 
-var (
-	stdOutFilePathDaemon = filepath.Join(constants.CrcBaseDir, ".crcd-agent.log")
-	stdOutFilePathTray   = filepath.Join(constants.CrcBaseDir, ".crct-agent.log")
-)
-
 type TrayVersion struct {
 	ShortVersion string `plist:"CFBundleShortVersionString"`
 }
 
 func checkIfDaemonPlistFileExists() error {
-	// crc setup can be ran from any location in the
-	// users computer, and we need to update the plist
-	// file with the path of the crc executable which was
-	// used to run setup, to force it this check needs
-	// to always fail so the fix routine is triggered
+	// we have no way of detecting if a running crc daemon instance uses
+	// the same binary as the one running 'crc setup'
+	// Safer for now to keep the previous behaviour of always
+	// recreating the plist/restarting the daemon
+	// When we have a way of checking the version of the running
+	// daemon instance, we can recreate the plist less often.
+
+	return fmt.Errorf("Ignoring this check and triggering re-creation of daemon plist")
 
-	return fmt.Errorf("Ignoring this check and triggering creation of daemon plist")
+	/*
+		daemonConfig, err := getDaemonConfig()
+		if err != nil {
+			return err
+		}
+		return launchd.CheckPlist(*daemonConfig)
+	*/
 }
 
-func fixDaemonPlistFileExists() error {
-	// Try to remove the daemon agent from launchd
-	// and recreate its plist
-	_ = launchd.Remove(daemonAgentLabel)
+func getDaemonConfig() (*launchd.AgentConfig, error) {
+	stdOutFilePathDaemon := filepath.Join(constants.CrcBaseDir, ".crcd-agent.log")
 
 	currentExecutablePath, err := goos.Executable()
 	if err != nil {
-		return err
+		return nil, err
 	}
 	daemonConfig := launchd.AgentConfig{
 		Label:          daemonAgentLabel,
@@ -55,7 +57,33 @@ func fixDaemonPlistFileExists() error {
 		StdOutFilePath: stdOutFilePathDaemon,
 		Args:           []string{"daemon"},
 	}
-	return fixPlistFileExists(daemonConfig)
+
+	return &daemonConfig, nil
+}
+
+func getTrayConfig() (*launchd.AgentConfig, error) {
+	stdOutFilePathTray := filepath.Join(constants.CrcBaseDir, ".crct-agent.log")
+
+	trayConfig := launchd.AgentConfig{
+		Label:          trayAgentLabel,
+		ExecutablePath: constants.TrayExecutablePath,
+		StdOutFilePath: stdOutFilePathTray,
+	}
+
+	return &trayConfig, nil
+}
+
+func fixDaemonPlistFileExists() error {
+	// Try to remove the daemon agent from launchd
+	// and recreate its plist
+	_ = launchd.Remove(daemonAgentLabel)
+
+	daemonConfig, err := getDaemonConfig()
+	if err != nil {
+		return nil
+	}
+
+	return fixPlistFileExists(*daemonConfig)
 }
 
 func removeDaemonPlistFile() error {
@@ -63,19 +91,19 @@ func removeDaemonPlistFile() error {
 }
 
 func checkIfTrayPlistFileExists() error {
-	if !launchd.PlistExists(trayAgentLabel) {
-		return fmt.Errorf("Tray plist file does not exist")
+	trayConfig, err := getTrayConfig()
+	if err != nil {
+		return err
 	}
-	return nil
+	return launchd.CheckPlist(*trayConfig)
 }
 
 func fixTrayPlistFileExists() error {
-	trayConfig := launchd.AgentConfig{
-		Label:          trayAgentLabel,
-		ExecutablePath: constants.TrayExecutablePath,
-		StdOutFilePath: stdOutFilePathTray,
+	trayConfig, err := getTrayConfig()
+	if err != nil {
+		return err
 	}
-	return fixPlistFileExists(trayConfig)
+	return fixPlistFileExists(*trayConfig)
 }
 
 func removeTrayPlistFile() error {
diff --git a/pkg/crc/preflight/preflight_darwin.go b/pkg/crc/preflight/preflight_darwin.go
index a61486ea7..47ab3b07e 100644
--- a/pkg/crc/preflight/preflight_darwin.go
+++ b/pkg/crc/preflight/preflight_darwin.go
@@ -113,7 +113,7 @@ func getAllPreflightChecks() []Check {
 	return getPreflightChecks(true, true, network.DefaultMode)
 }
 
-func getPreflightChecks(_ bool, trayAutostart bool, mode network.Mode) []Check {
+func getPreflightChecks(experimentalFeatures bool, trayAutostart bool, mode network.Mode) []Check {
 	checks := []Check{}
 
 	checks = append(checks, nonWinPreflightChecks[:]...)
@@ -121,18 +121,20 @@ func getPreflightChecks(_ bool, trayAutostart bool, mode network.Mode) []Check {
 	checks = append(checks, hyperkitPreflightChecks(mode)...)
 	checks = append(checks, dnsPreflightChecks[:]...)
 
-	if trayAutostart || mode == network.VSockMode {
+	switch mode {
+	case network.DefaultMode:
+		checks = append(checks, resolverPreflightChecks[:]...)
+	case network.VSockMode:
 		checks = append(checks, daemonSetupChecks[:]...)
 	}
 
-	if trayAutostart {
+	if experimentalFeatures && trayAutostart {
+		if mode != network.VSockMode {
+			checks = append(checks, daemonSetupChecks[:]...)
+		}
 		checks = append(checks, traySetupChecks[:]...)
 	}
 
-	if mode == network.DefaultMode {
-		checks = append(checks, resolverPreflightChecks[:]...)
-	}
-
 	checks = append(checks, bundleCheck)
 	return checks
 }
diff --git a/pkg/crc/ssh/ssh.go b/pkg/crc/ssh/ssh.go
index 5d5a4c81c..54e6ad8ea 100644
--- a/pkg/crc/ssh/ssh.go
+++ b/pkg/crc/ssh/ssh.go
@@ -86,7 +86,7 @@ func (runner *Runner) runSSHCommand(command string, runPrivate bool) (string, st
 	if err != nil {
 		return string(stdout), string(stderr), fmt.Errorf(`ssh command error:
 command : %s
-err     : %v\n`, command, err)
+err     : %w\n`, command, err)
 	}
 
 	return string(stdout), string(stderr), nil
diff --git a/pkg/crc/version/version.go b/pkg/crc/version/version.go
index 445a9f0ca..f74991867 100644
--- a/pkg/crc/version/version.go
+++ b/pkg/crc/version/version.go
@@ -29,7 +29,7 @@ var (
 const (
 	releaseInfoLink = "https://mirror.openshift.com/pub/openshift-v4/clients/crc/latest/release-info.json"
 	// Tray version to be embedded in executable
-	crcMacTrayVersion = "1.0.3"
+	crcMacTrayVersion = "1.0.4"
 	// Windows forms application version type major.minor.buildnumber.revesion
 	crcWindowsTrayVersion = "0.4.0.0"
 )
diff --git a/pkg/os/launchd/launchd_darwin.go b/pkg/os/launchd/launchd_darwin.go
index 879af8a3b..3c7befb9d 100644
--- a/pkg/os/launchd/launchd_darwin.go
+++ b/pkg/os/launchd/launchd_darwin.go
@@ -2,6 +2,7 @@ package launchd
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	goos "os"
@@ -35,6 +36,8 @@ const (
 			<false/>
 			<key>RunAtLoad</key>
 			<true/>
+			<key>ProcessType</key>
+			<string>Interactive</string>
 		</dict>
 	</plist>`
 )
@@ -64,23 +67,31 @@ func getPlistPath(label string) string {
 	return filepath.Join(launchAgentsDir, plistName)
 }
 
+func generatePlistContent(config AgentConfig) ([]byte, error) {
+	var plistContent bytes.Buffer
+	t, err := template.New("plist").Parse(plistTemplate)
+	if err != nil {
+		return nil, err
+	}
+	err = t.Execute(&plistContent, config)
+	if err != nil {
+		return nil, err
+	}
+
+	return plistContent.Bytes(), nil
+}
+
 // CreatePlist creates a launchd agent plist config file
 func CreatePlist(config AgentConfig) error {
 	if err := ensureLaunchAgentsDirExists(); err != nil {
 		return err
 	}
 
-	var plistContent bytes.Buffer
-	t, err := template.New("plist").Parse(plistTemplate)
-	if err != nil {
-		return err
-	}
-	err = t.Execute(&plistContent, config)
+	plist, err := generatePlistContent(config)
 	if err != nil {
 		return err
 	}
-	// #nosec G306
-	err = ioutil.WriteFile(getPlistPath(config.Label), plistContent.Bytes(), 0644)
+	err = ioutil.WriteFile(getPlistPath(config.Label), plist, 0600)
 	return err
 }
 
@@ -88,14 +99,22 @@ func PlistExists(label string) bool {
 	return os.FileExists(getPlistPath(label))
 }
 
+func CheckPlist(config AgentConfig) error {
+	plist, err := generatePlistContent(config)
+	if err != nil {
+		return err
+	}
+	return os.FileContentMatches(getPlistPath(config.Label), plist)
+}
+
 // LoadPlist loads a launchd agents' plist file
 func LoadPlist(label string) error {
-	return exec.Command("launchctl", "load", getPlistPath(label)).Run() // #nosec G204
+	return runLaunchCtl("load", getPlistPath(label))
 }
 
 // UnloadPlist Unloads a launchd agent's service
 func UnloadPlist(label string) error {
-	return exec.Command("launchctl", "unload", getPlistPath(label)).Run() // #nosec G204
+	return runLaunchCtl("unload", getPlistPath(label))
 }
 
 // RemovePlist removes a launchd agent plist config file
@@ -108,12 +127,12 @@ func RemovePlist(label string) error {
 
 // StartAgent starts a launchd agent
 func StartAgent(label string) error {
-	return exec.Command("launchctl", "start", label).Run() // #nosec G204
+	return runLaunchCtl("start", label)
 }
 
 // StopAgent stops a launchd agent
 func StopAgent(label string) error {
-	return exec.Command("launchctl", "stop", label).Run() // #nosec G204
+	return runLaunchCtl("stop", label)
 }
 
 // RestartAgent restarts a launchd agent
@@ -132,7 +151,7 @@ func AgentRunning(label string) bool {
 	// output if the agent is not loaded in launchd
 	launchctlListCommand := `launchctl list | grep %s | awk '{print $1}'`
 	cmd := fmt.Sprintf(launchctlListCommand, label)
-	out, err := exec.Command("bash", "-c", cmd).Output() // #nosec G204
+	out, _, err := os.RunWithDefaultLocale("bash", "-c", cmd)
 	if err != nil {
 		return false
 	}
@@ -145,5 +164,24 @@ func AgentRunning(label string) bool {
 
 // Remove removes the agent from launchd
 func Remove(label string) error {
-	return exec.Command("launchctl", "remove", label).Run() // #nosec G204
+	return runLaunchCtl("remove", label)
+}
+
+func runLaunchCtl(args ...string) error {
+	_, _, err := os.RunWithDefaultLocale("launchctl", args...)
+	return exitCodeToError(err)
+}
+
+func exitCodeToError(err error) error {
+	var exitErr *exec.ExitError
+	if !errors.As(err, &exitErr) {
+		return err
+	}
+
+	stdout, _, localErr := os.RunWithDefaultLocale("launchctl", "error", fmt.Sprintf("%d", exitErr.ExitCode()))
+	if localErr != nil {
+		return err
+	}
+
+	return errors.New(stdout)
 }


Test patch:
diff --git a/pkg/crc/api/api_http_test.go b/pkg/crc/api/api_http_test.go
new file mode 100644
index 000000000..9be5e88f4
--- /dev/null
+++ b/pkg/crc/api/api_http_test.go
@@ -0,0 +1,125 @@
+package api
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	apiClient "github.com/code-ready/crc/pkg/crc/api/client"
+	"github.com/code-ready/crc/pkg/crc/machine"
+	"github.com/code-ready/crc/pkg/crc/machine/fakemachine"
+	"github.com/code-ready/crc/pkg/crc/version"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestHTTPApi(t *testing.T) {
+	fakeMachine := fakemachine.NewClient()
+	config := setupNewInMemoryConfig()
+
+	ts := httptest.NewServer(NewMux(config, fakeMachine))
+	defer ts.Close()
+
+	client := apiClient.New(http.DefaultClient, ts.URL)
+	vr, err := client.Version()
+	assert.NoError(t, err)
+	assert.Equal(
+		t,
+		apiClient.VersionResult{
+			CrcVersion:       version.GetCRCVersion(),
+			OpenshiftVersion: version.GetBundleVersion(),
+			CommitSha:        version.GetCommitSha(),
+			Success:          true,
+		},
+		vr,
+	)
+
+	statusResult, err := client.Status()
+	assert.NoError(t, err)
+	assert.Equal(
+		t,
+		apiClient.ClusterStatusResult{
+			Name:             "crc",
+			CrcStatus:        "Running",
+			OpenshiftStatus:  "Running",
+			OpenshiftVersion: "4.5.1",
+			DiskUse:          int64(10000000000),
+			DiskSize:         int64(20000000000),
+			Success:          true,
+		},
+		statusResult,
+	)
+
+	var startConfig = apiClient.StartConfig{}
+	startResult, err := client.Start(startConfig)
+	assert.NoError(t, err)
+	assert.Equal(
+		t,
+		apiClient.StartResult{
+			Name:           "crc",
+			Status:         "",
+			Error:          "",
+			KubeletStarted: true,
+			ClusterConfig: machine.ClusterConfig{
+				ClusterCACert: "MIIDODCCAiCgAwIBAgIIRVfCKNUa1wIwDQYJ",
+				KubeConfig:    "/tmp/kubeconfig",
+				KubeAdminPass: "foobar",
+				ClusterAPI:    "https://foo.testing:6443",
+				WebConsoleURL: "https://console.foo.testing:6443",
+				ProxyConfig:   nil,
+			},
+		},
+		startResult,
+	)
+
+	stopResult, err := client.Stop()
+	assert.NoError(t, err)
+	assert.Equal(
+		t,
+		apiClient.Result{
+			Name:    "crc",
+			Success: true,
+			Error:   "",
+		},
+		stopResult,
+	)
+
+	deleteResult, err := client.Delete()
+	assert.NoError(t, err)
+	assert.Equal(
+		t,
+		apiClient.Result{
+			Name:    "crc",
+			Success: true,
+			Error:   "",
+		},
+		deleteResult,
+	)
+
+	configGetResult, err := client.GetConfig([]string{"cpus"})
+	assert.NoError(t, err)
+	assert.Equal(
+		t,
+		apiClient.GetConfigResult{
+			Error: "",
+			Configs: map[string]interface{}{
+				"cpus": float64(4),
+			},
+		},
+		configGetResult,
+	)
+
+	configSetResult, err := client.SetConfig(apiClient.SetConfigRequest{
+		Properties: map[string]interface{}{
+			"cpus": float64(5),
+		},
+	})
+	assert.NoError(t, err)
+	assert.Equal(
+		t,
+		apiClient.SetOrUnsetConfigResult{
+			Error:      "",
+			Properties: []string{"cpus"},
+		},
+		configSetResult,
+	)
+}
diff --git a/pkg/crc/api/api_test.go b/pkg/crc/api/api_test.go
index 76822ad87..67b58de27 100644
--- a/pkg/crc/api/api_test.go
+++ b/pkg/crc/api/api_test.go
@@ -8,13 +8,10 @@ import (
 	"net"
 	"os"
 	"path/filepath"
-	"strings"
 	"testing"
 
-	cmdConfig "github.com/code-ready/crc/cmd/crc/cmd/config"
-	"github.com/code-ready/crc/pkg/crc/config"
+	apiClient "github.com/code-ready/crc/pkg/crc/api/client"
 	"github.com/code-ready/crc/pkg/crc/machine/fakemachine"
-	"github.com/code-ready/crc/pkg/crc/preflight"
 	"github.com/code-ready/crc/pkg/crc/version"
 	log "github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
@@ -167,9 +164,9 @@ func TestSetconfigApi(t *testing.T) {
 	n, err := client.Read(payload)
 	assert.NoError(t, err)
 
-	var setconfigRes setOrUnsetConfigResult
+	var setconfigRes apiClient.SetOrUnsetConfigResult
 	assert.NoError(t, json.Unmarshal(payload[:n], &setconfigRes))
-	assert.Equal(t, setOrUnsetConfigResult{
+	assert.Equal(t, apiClient.SetOrUnsetConfigResult{
 		Error:      "",
 		Properties: []string{"cpus"},
 	}, setconfigRes)
@@ -193,29 +190,18 @@ func TestGetconfigApi(t *testing.T) {
 	n, err := client.Read(payload)
 	assert.NoError(t, err)
 
-	var getconfigRes getConfigResult
+	var getconfigRes apiClient.GetConfigResult
 	assert.NoError(t, json.Unmarshal(payload[:n], &getconfigRes))
 
 	configs := make(map[string]interface{})
 	configs["cpus"] = 4.0
 
-	assert.Equal(t, getConfigResult{
+	assert.Equal(t, apiClient.GetConfigResult{
 		Error:   "",
 		Configs: configs,
 	}, getconfigRes)
 }
 
-func setupNewInMemoryConfig() config.Storage {
-	storage := config.NewEmptyInMemoryStorage()
-	cfg := config.New(&skipPreflights{
-		storage: storage,
-	})
-	cmdConfig.RegisterSettings(cfg)
-	preflight.RegisterSettings(cfg)
-
-	return cfg
-}
-
 func setupAPIServer(t *testing.T) (string, func()) {
 	dir, err := ioutil.TempDir("", "api")
 	require.NoError(t, err)
@@ -236,25 +222,6 @@ func setupAPIServer(t *testing.T) (string, func()) {
 	return socket, func() { os.RemoveAll(dir) }
 }
 
-type skipPreflights struct {
-	storage config.RawStorage
-}
-
-func (s *skipPreflights) Get(key string) interface{} {
-	if strings.HasPrefix(key, "skip-") {
-		return "true"
-	}
-	return s.storage.Get(key)
-}
-
-func (s *skipPreflights) Set(key string, value interface{}) error {
-	return s.storage.Set(key, value)
-}
-
-func (s *skipPreflights) Unset(key string) error {
-	return s.storage.Unset(key)
-}
-
 type mockLogger struct {
 }
 
diff --git a/pkg/crc/api/apitest.go b/pkg/crc/api/apitest.go
new file mode 100644
index 000000000..f28a6996b
--- /dev/null
+++ b/pkg/crc/api/apitest.go
@@ -0,0 +1,39 @@
+package api
+
+import (
+	"strings"
+
+	cmdConfig "github.com/code-ready/crc/cmd/crc/cmd/config"
+	"github.com/code-ready/crc/pkg/crc/config"
+	"github.com/code-ready/crc/pkg/crc/preflight"
+)
+
+func setupNewInMemoryConfig() config.Storage {
+	storage := config.NewEmptyInMemoryStorage()
+	cfg := config.New(&skipPreflights{
+		storage: storage,
+	})
+	cmdConfig.RegisterSettings(cfg)
+	preflight.RegisterSettings(cfg)
+
+	return cfg
+}
+
+type skipPreflights struct {
+	storage config.RawStorage
+}
+
+func (s *skipPreflights) Get(key string) interface{} {
+	if strings.HasPrefix(key, "skip-") {
+		return "true"
+	}
+	return s.storage.Get(key)
+}
+
+func (s *skipPreflights) Set(key string, value interface{}) error {
+	return s.storage.Set(key, value)
+}
+
+func (s *skipPreflights) Unset(key string) error {
+	return s.storage.Unset(key)
+}
diff --git a/pkg/crc/preflight/preflight_darwin_test.go b/pkg/crc/preflight/preflight_darwin_test.go
index a56f7ac31..214b967ce 100644
--- a/pkg/crc/preflight/preflight_darwin_test.go
+++ b/pkg/crc/preflight/preflight_darwin_test.go
@@ -23,8 +23,8 @@ func TestCountPreflights(t *testing.T) {
 	assert.Len(t, getPreflightChecks(true, true, network.VSockMode), 19)
 
 	assert.Len(t, getPreflightChecks(true, false, network.VSockMode), 15)
-	assert.Len(t, getPreflightChecks(false, true, network.VSockMode), 19)
+	assert.Len(t, getPreflightChecks(false, true, network.VSockMode), 15)
 
-	assert.Len(t, getPreflightChecks(false, true, network.DefaultMode), 20)
+	assert.Len(t, getPreflightChecks(false, true, network.DefaultMode), 14)
 	assert.Len(t, getPreflightChecks(true, false, network.DefaultMode), 14)
 }