Repository: code-ready/crc
Pull request #2265 (2021-04-22 13:22:27)

Add telemetry endpoint for the tray
Daemon side of https://github.com/code-ready/tray-macos/issues/104

It adds /telemetry to the binary and http API. The daemon decides according to the configuration to send or not the data to segment.

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

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2265#" title="Author self-approved">guillaumerose</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)~~ [guillaumerose]

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":[]} -->

Patch:
diff --git a/Makefile b/Makefile
index d6105dd18..526458a8c 100644
--- a/Makefile
+++ b/Makefile
@@ -124,10 +124,6 @@ test-rpmbuild: spec
 build_docs:
 	${CONTAINER_RUNTIME} run -v $(CURDIR)/docs:/docs:Z --rm $(DOCS_BUILD_CONTAINER) build_docs -b html5 -D /$(DOCS_BUILD_DIR) -o index.html $(DOCS_BUILD_TARGET)
 
-.PHONY: build_docs_pdf
-build_docs_pdf:
-	${CONTAINER_RUNTIME} run -v $(CURDIR)/docs:/docs:Z --rm $(DOCS_BUILD_CONTAINER) build_docs_pdf -D /$(DOCS_BUILD_DIR) -o doc.pdf $(DOCS_BUILD_TARGET)
-
 .PHONY: docs_serve
 docs_serve: build_docs
 	${CONTAINER_RUNTIME} run -it -v $(CURDIR)/docs:/docs:Z --rm -p 8088:8088/tcp $(DOCS_BUILD_CONTAINER) docs_serve 
@@ -214,19 +210,19 @@ gen_release_info:
 
 .PHONY: release
 release: LDFLAGS += $(RELEASE_VERSION_VARIABLES)
-release: cross-lint embed_bundle build_docs_pdf gen_release_info
+release: cross-lint embed_bundle gen_release_info
 	mkdir $(RELEASE_DIR)
 	
 	@mkdir -p $(BUILD_DIR)/crc-macos-$(CRC_VERSION)-amd64
-	@cp LICENSE $(DOCS_BUILD_DIR)/doc.pdf $(BUILD_DIR)/macos-amd64/crc $(BUILD_DIR)/crc-macos-$(CRC_VERSION)-amd64
+	@cp LICENSE $(BUILD_DIR)/macos-amd64/crc $(BUILD_DIR)/crc-macos-$(CRC_VERSION)-amd64
 	tar cJSf $(RELEASE_DIR)/crc-macos-amd64.tar.xz -C $(BUILD_DIR) crc-macos-$(CRC_VERSION)-amd64 --owner=0 --group=0
 
 	@mkdir -p $(BUILD_DIR)/crc-linux-$(CRC_VERSION)-amd64
-	@cp LICENSE $(DOCS_BUILD_DIR)/doc.pdf $(BUILD_DIR)/linux-amd64/crc $(BUILD_DIR)/crc-linux-$(CRC_VERSION)-amd64
+	@cp LICENSE $(BUILD_DIR)/linux-amd64/crc $(BUILD_DIR)/crc-linux-$(CRC_VERSION)-amd64
 	tar cJSf $(RELEASE_DIR)/crc-linux-amd64.tar.xz -C $(BUILD_DIR) crc-linux-$(CRC_VERSION)-amd64 --owner=0 --group=0
 	
 	@mkdir -p $(BUILD_DIR)/crc-windows-$(CRC_VERSION)-amd64
-	@cp LICENSE $(DOCS_BUILD_DIR)/doc.pdf $(BUILD_DIR)/windows-amd64/crc.exe $(BUILD_DIR)/crc-windows-$(CRC_VERSION)-amd64
+	@cp LICENSE $(BUILD_DIR)/windows-amd64/crc.exe $(BUILD_DIR)/crc-windows-$(CRC_VERSION)-amd64
 	cd $(BUILD_DIR) && zip -r $(CURDIR)/$(RELEASE_DIR)/crc-windows-amd64.zip crc-windows-$(CRC_VERSION)-amd64
 
 	@mv $(RELEASE_INFO) $(RELEASE_DIR)/$(RELEASE_INFO)
diff --git a/cmd/crc/cmd/daemon.go b/cmd/crc/cmd/daemon.go
index 7450fd887..4d6b411df 100644
--- a/cmd/crc/cmd/daemon.go
+++ b/cmd/crc/cmd/daemon.go
@@ -143,7 +143,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())))
+		mux.Handle("/api/", http.StripPrefix("/api", api.NewMux(config, newMachine(), logging.Memory, segmentClient)))
 		if err := http.Serve(listener, mux); err != nil {
 			errCh <- err
 		}
@@ -241,7 +241,7 @@ func acceptJSONStringArray(w http.ResponseWriter, r *http.Request, fun func(host
 func runDaemon() error {
 	// Remove if an old socket is present
 	os.Remove(constants.DaemonSocketPath)
-	apiServer, err := api.CreateServer(constants.DaemonSocketPath, config, newMachine(), logging.Memory)
+	apiServer, err := api.CreateServer(constants.DaemonSocketPath, config, newMachine(), logging.Memory, segmentClient)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/crc/cmd/ip.go b/cmd/crc/cmd/ip.go
index 477040f60..315652c83 100644
--- a/cmd/crc/cmd/ip.go
+++ b/cmd/crc/cmd/ip.go
@@ -25,10 +25,10 @@ func runIP(arguments []string) error {
 		return err
 	}
 
-	ip, err := client.IP()
+	connectionDetails, err := client.ConnectionDetails()
 	if err != nil {
 		return err
 	}
-	fmt.Println(ip)
+	fmt.Println(connectionDetails.IP)
 	return nil
 }
diff --git a/cmd/crc/cmd/podman_env.go b/cmd/crc/cmd/podman_env.go
index 06ca12138..bb7d3ebdd 100644
--- a/cmd/crc/cmd/podman_env.go
+++ b/cmd/crc/cmd/podman_env.go
@@ -1,7 +1,6 @@
 package cmd
 
 import (
-	"errors"
 	"fmt"
 
 	"github.com/code-ready/crc/pkg/crc/constants"
@@ -14,12 +13,11 @@ var podmanEnvCmd = &cobra.Command{
 	Short: "Setup podman environment",
 	Long:  `Setup environment for 'podman' executable to access podman on CRC VM`,
 	RunE: func(cmd *cobra.Command, args []string) error {
-		// See issue #961; Currently does not work on Windows in combination with the CRC vm.
-		return errors.New("currently not supported")
+		return runPodmanEnv()
 	},
 }
 
-func RunPodmanEnv(args []string) error {
+func runPodmanEnv() error {
 	userShell, err := shell.GetShell(forceShell)
 	if err != nil {
 		return fmt.Errorf("Error running the podman-env command: %s", err.Error())
@@ -30,16 +28,18 @@ func RunPodmanEnv(args []string) error {
 		return err
 	}
 
-	ip, err := client.IP()
+	connectionDetails, err := client.ConnectionDetails()
 	if err != nil {
 		return err
 	}
 
-	fmt.Println(shell.GetPathEnvString(userShell, constants.CrcBinDir))
-	fmt.Println(shell.GetEnvString(userShell, "PODMAN_USER", constants.DefaultSSHUser))
-	fmt.Println(shell.GetEnvString(userShell, "PODMAN_HOST", ip))
-	fmt.Println(shell.GetEnvString(userShell, "PODMAN_IDENTITY_FILE", constants.GetPrivateKeyPath()))
-	fmt.Println(shell.GetEnvString(userShell, "PODMAN_IGNORE_HOSTS", "1"))
+	fmt.Println(shell.GetPathEnvString(userShell, constants.CrcOcBinDir))
+	fmt.Println(shell.GetEnvString(userShell, "CONTAINER_SSHKEY", connectionDetails.SSHKeys[0]))
+	fmt.Println(shell.GetEnvString(userShell, "CONTAINER_HOST",
+		fmt.Sprintf("ssh://%s@%s:%d/run/user/1000/podman/podman.sock",
+			connectionDetails.SSHUsername,
+			connectionDetails.IP,
+			connectionDetails.SSHPort)))
 	fmt.Println(shell.GenerateUsageHintWithComment(userShell, "crc podman-env"))
 	return nil
 }
diff --git a/cmd/crc/cmd/root.go b/cmd/crc/cmd/root.go
index 76c8fbac1..35e24e1b4 100644
--- a/cmd/crc/cmd/root.go
+++ b/cmd/crc/cmd/root.go
@@ -187,7 +187,7 @@ func executeWithLogging(fullCmd string, input func(cmd *cobra.Command, args []st
 		logging.Debugf("Running '%s'", fullCmd)
 		startTime := time.Now()
 		err := input(cmd, args)
-		if serr := segmentClient.Upload(cmd.Context(), fullCmd, time.Since(startTime), err); serr != nil {
+		if serr := segmentClient.UploadCmd(cmd.Context(), fullCmd, time.Since(startTime), err); serr != nil {
 			logging.Debugf("Cannot send data to telemetry: %v", serr)
 		}
 		return err
diff --git a/images/docs-builder/Dockerfile b/images/docs-builder/Dockerfile
index e55bd0be5..33b3e12fa 100644
--- a/images/docs-builder/Dockerfile
+++ b/images/docs-builder/Dockerfile
@@ -7,7 +7,7 @@ RUN yum update -y && \
     yum module install nodejs:14 -y && \
     yum clean all && rm -rf /var/cache/yum/*
 
-RUN gem install asciidoctor asciidoctor-pdf --pre && \
+RUN gem install asciidoctor && \
     npm install -g asciidoc-link-check
 
 COPY entrypoint.sh /sbin/entrypoint.sh
diff --git a/images/docs-builder/entrypoint.sh b/images/docs-builder/entrypoint.sh
index b84a7a38d..a100c266c 100644
--- a/images/docs-builder/entrypoint.sh
+++ b/images/docs-builder/entrypoint.sh
@@ -2,7 +2,7 @@
 set -e
 
 if [ $# -lt 1 ]; then
-	echo "You need to provide an argument [build_docs, build_docs_pdf, docs_check_links, docs_serve]"
+	echo "You need to provide an argument [build_docs, docs_check_links, docs_serve]"
 fi
 
 case $1 in
@@ -11,11 +11,6 @@ case $1 in
 		shift 
 		asciidoctor $@
 		;;
-	build_docs_pdf)
-		echo "Generating pdf from docs source"
-		shift 
-		asciidoctor-pdf $@
-		;;
 	docs_check_links)
 		echo "Checking if all links are alive in docs source"
 		find . -name \*.adoc | xargs -n1 asciidoc-link-check -c /links_ignorelist.json -q
@@ -25,7 +20,7 @@ case $1 in
 		python3 -m http.server 8088
 		;;
 	*)
-		echo "Need to provide one of: build_docs, build_docs_pdf, docs_check_links, docs_serve"
+		echo "Need to provide one of: build_docs, docs_check_links, docs_serve"
 		exit 1
 		;;
 esac
diff --git a/packaging/package.sh b/packaging/package.sh
index 065d2dfdd..ac5296045 100755
--- a/packaging/package.sh
+++ b/packaging/package.sh
@@ -31,6 +31,8 @@ sign "${BASEDIR}/root/Applications/CodeReady Containers.app"
 
 codesign --verify --verbose "${binDir}/hyperkit"
 
+sudo chmod +sx "${binDir}/hyperkit" "${binDir}/admin-helper-darwin" "${binDir}/crc-driver-hyperkit"
+
 pkgbuild --identifier com.redhat.crc --version ${version} \
   --scripts "${BASEDIR}/darwin/scripts" \
   --root "${BASEDIR}/root" \
diff --git a/pkg/crc/adminhelper/hosts.go b/pkg/crc/adminhelper/hosts.go
index a88d9f177..1b338a881 100644
--- a/pkg/crc/adminhelper/hosts.go
+++ b/pkg/crc/adminhelper/hosts.go
@@ -8,7 +8,7 @@ import (
 )
 
 var (
-	adminHelperPath = filepath.Join(constants.CrcBinDir, constants.AdminHelperExecutableName)
+	adminHelperPath = filepath.Join(constants.BinDir(), constants.AdminHelperExecutableName)
 )
 
 // UpdateHostsFile updates the host's /etc/hosts file with Instance IP.
diff --git a/pkg/crc/api/api.go b/pkg/crc/api/api.go
index d55e0ff34..f56f56846 100644
--- a/pkg/crc/api/api.go
+++ b/pkg/crc/api/api.go
@@ -11,23 +11,19 @@ import (
 	"github.com/code-ready/crc/pkg/crc/machine"
 )
 
-func CreateServer(socketPath string, config crcConfig.Storage, machine machine.Client, logger Logger) (Server, error) {
+func CreateServer(socketPath string, config crcConfig.Storage, machine machine.Client, logger Logger, telemetry Telemetry) (Server, error) {
 	listener, err := net.Listen("unix", socketPath)
 	if err != nil {
 		logging.Error("Failed to create socket: ", err.Error())
 		return Server{}, err
 	}
-	return createServerWithListener(listener, config, machine, logger)
+	return createServerWithListener(listener, config, machine, logger, telemetry)
 }
 
-func createServerWithListener(listener net.Listener, config crcConfig.Storage, machine machine.Client, logger Logger) (Server, error) {
+func createServerWithListener(listener net.Listener, config crcConfig.Storage, machine machine.Client, logger Logger, telemetry Telemetry) (Server, error) {
 	apiServer := Server{
 		listener: listener,
-		handler: &Handler{
-			Config:        config,
-			MachineClient: &Adapter{Underlying: machine},
-			Logger:        logger,
-		},
+		handler:  NewHandler(config, machine, logger, telemetry),
 	}
 	return apiServer, nil
 }
@@ -71,6 +67,8 @@ func (api Server) handleRequest(req commandRequest, conn net.Conn) {
 		result = api.handler.GetWebconsoleInfo()
 	case "logs":
 		result = api.handler.Logs()
+	case "telemetry":
+		result = api.handler.UploadTelemetry(req.Args)
 	default:
 		result = encodeErrorToJSON(fmt.Sprintf("Unknown command supplied: %s", req.Command))
 	}
diff --git a/pkg/crc/api/api_http.go b/pkg/crc/api/api_http.go
index f0d0eb7e6..423bd1c6d 100644
--- a/pkg/crc/api/api_http.go
+++ b/pkg/crc/api/api_http.go
@@ -12,8 +12,8 @@ import (
 	"github.com/code-ready/crc/pkg/crc/machine"
 )
 
-func NewMux(config crcConfig.Storage, machine machine.Client) http.Handler {
-	handler := NewHandler(config, machine)
+func NewMux(config crcConfig.Storage, machine machine.Client, logger Logger, telemetry Telemetry) http.Handler {
+	handler := NewHandler(config, machine, logger, telemetry)
 
 	mux := http.NewServeMux()
 	mux.HandleFunc("/start", func(w http.ResponseWriter, r *http.Request) {
@@ -72,6 +72,17 @@ func NewMux(config crcConfig.Storage, machine machine.Client) http.Handler {
 
 	mux.Handle("/config/", http.StripPrefix("/config", newConfigMux(handler)))
 
+	mux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
+		sendResponse(w, handler.Logs())
+	})
+
+	mux.HandleFunc("/telemetry", func(w http.ResponseWriter, r *http.Request) {
+		data, err := verifyRequestAndReadBody(w, r, http.MethodGet, http.MethodPost)
+		if err != nil {
+			return
+		}
+		sendResponse(w, handler.UploadTelemetry(data))
+	})
 	return mux
 }
 
diff --git a/pkg/crc/api/client/client.go b/pkg/crc/api/client/client.go
index a6c22bc42..36a7182f8 100644
--- a/pkg/crc/api/client/client.go
+++ b/pkg/crc/api/client/client.go
@@ -3,6 +3,7 @@ package client
 import (
 	"bytes"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -174,6 +175,29 @@ func (c *Client) UnsetConfig(configs []string) (SetOrUnsetConfigResult, error) {
 	return ucr, nil
 }
 
+func (c *Client) Telemetry(action string) error {
+	data, err := json.Marshal(TelemetryRequest{
+		Action: action,
+	})
+	if err != nil {
+		return fmt.Errorf("Failed to encode data to JSON: %w", err)
+	}
+
+	body, err := c.sendPostRequest("/telemetry", bytes.NewReader(data))
+	if err != nil {
+		return err
+	}
+
+	var res Result
+	if err = json.Unmarshal(body, &res); err != nil {
+		return err
+	}
+	if res.Error != "" {
+		return errors.New(res.Error)
+	}
+	return nil
+}
+
 func (c *Client) sendGetRequest(url string) ([]byte, error) {
 	res, err := c.client.Get(fmt.Sprintf("%s%s", c.base, url))
 	if err != nil {
diff --git a/pkg/crc/api/client/types.go b/pkg/crc/api/client/types.go
index 9acd8922b..4c8cd354a 100644
--- a/pkg/crc/api/client/types.go
+++ b/pkg/crc/api/client/types.go
@@ -66,3 +66,8 @@ type SetConfigRequest struct {
 type getOrUnsetConfigRequest struct {
 	Properties []string `json:"properties"`
 }
+
+type TelemetryRequest struct {
+	Action string `json:"action"`
+	Source string `json:"source"`
+}
diff --git a/pkg/crc/api/handlers.go b/pkg/crc/api/handlers.go
index 729f08f5e..2f7a50870 100644
--- a/pkg/crc/api/handlers.go
+++ b/pkg/crc/api/handlers.go
@@ -22,12 +22,17 @@ type Handler struct {
 	Logger        Logger
 	MachineClient AdaptedClient
 	Config        crcConfig.Storage
+	Telemetry     Telemetry
 }
 
 type Logger interface {
 	Messages() []string
 }
 
+type Telemetry interface {
+	UploadAction(action, source string) error
+}
+
 func (h *Handler) Logs() string {
 	return encodeStructToJSON(&loggerResult{
 		Success:  true,
@@ -35,12 +40,14 @@ func (h *Handler) Logs() string {
 	})
 }
 
-func NewHandler(config crcConfig.Storage, machine machine.Client) *Handler {
+func NewHandler(config crcConfig.Storage, machine machine.Client, logger Logger, telemetry Telemetry) *Handler {
 	return &Handler{
 		MachineClient: &Adapter{
 			Underlying: machine,
 		},
-		Config: config,
+		Config:    config,
+		Logger:    logger,
+		Telemetry: telemetry,
 	}
 }
 
@@ -234,6 +241,19 @@ func (h *Handler) GetConfig(args json.RawMessage) string {
 	return encodeStructToJSON(configResult)
 }
 
+func (h *Handler) UploadTelemetry(args json.RawMessage) string {
+	var req client.TelemetryRequest
+	if err := json.Unmarshal(args, &req); err != nil {
+		return encodeErrorToJSON(err.Error())
+	}
+	if err := h.Telemetry.UploadAction(req.Action, req.Source); err != nil {
+		return encodeErrorToJSON(err.Error())
+	}
+	return encodeStructToJSON(client.Result{
+		Success: true,
+	})
+}
+
 func encodeStructToJSON(v interface{}) string {
 	s, err := json.Marshal(v)
 	if err != nil {
diff --git a/pkg/crc/api/types.go b/pkg/crc/api/types.go
index 6bcee06d7..1b57aa25b 100644
--- a/pkg/crc/api/types.go
+++ b/pkg/crc/api/types.go
@@ -6,23 +6,10 @@ import (
 )
 
 type Server struct {
-	handler  RequestHandler
+	handler  *Handler
 	listener net.Listener
 }
 
-type RequestHandler interface {
-	Start(json.RawMessage) string
-	Stop() string
-	Status() string
-	Delete() string
-	GetVersion() string
-	SetConfig(json.RawMessage) string
-	UnsetConfig(json.RawMessage) string
-	GetConfig(json.RawMessage) string
-	GetWebconsoleInfo() string
-	Logs() string
-}
-
 // commandRequest struct is used to decode the json request from tray
 type commandRequest struct {
 	Command string          `json:"command"`
diff --git a/pkg/crc/constants/constants.go b/pkg/crc/constants/constants.go
index 29b377659..01c8e8479 100644
--- a/pkg/crc/constants/constants.go
+++ b/pkg/crc/constants/constants.go
@@ -114,6 +114,13 @@ func defaultBundlePath() string {
 	return filepath.Join(MachineCacheDir, GetDefaultBundle())
 }
 
+func BinDir() string {
+	if runtime.GOOS == "darwin" && version.IsMacosInstallPathSet() {
+		return version.GetMacosInstallPath()
+	}
+	return CrcBinDir
+}
+
 // GetHomeDir returns the home directory for the current user
 func GetHomeDir() string {
 	if runtime.GOOS == "windows" {
diff --git a/pkg/crc/machine/bundle/metadata.go b/pkg/crc/machine/bundle/metadata.go
index 0a05e0f1e..73bcef7b8 100644
--- a/pkg/crc/machine/bundle/metadata.go
+++ b/pkg/crc/machine/bundle/metadata.go
@@ -67,9 +67,16 @@ type DiskImage struct {
 	Format string `json:"format"`
 }
 
+type FileType string
+
+const (
+	OcExecutable     FileType = "oc-executable"
+	PodmanExecutable FileType = "podman-executable"
+)
+
 type FileListItem struct {
 	File
-	Type string `json:"type"`
+	Type FileType `json:"type"`
 }
 
 type DriverInfo struct {
@@ -102,7 +109,17 @@ func (bundle *CrcBundleInfo) GetKubeConfigPath() string {
 
 func (bundle *CrcBundleInfo) GetOcPath() string {
 	for _, file := range bundle.Storage.Files {
-		if file.Type == "oc-executable" {
+		if file.Type == OcExecutable {
+			return bundle.resolvePath(file.Name)
+		}
+	}
+
+	return ""
+}
+
+func (bundle *CrcBundleInfo) GetPodmanPath() string {
+	for _, file := range bundle.Storage.Files {
+		if file.Type == PodmanExecutable {
 			return bundle.resolvePath(file.Name)
 		}
 	}
diff --git a/pkg/crc/machine/bundle/repository.go b/pkg/crc/machine/bundle/repository.go
index db688f000..532d7ffb1 100644
--- a/pkg/crc/machine/bundle/repository.go
+++ b/pkg/crc/machine/bundle/repository.go
@@ -84,6 +84,9 @@ func (repo *Repository) Use(bundleName string) (*CrcBundleInfo, error) {
 	if err := bundleInfo.createSymlinkOrCopyOpenShiftClient(repo.OcBinDir); err != nil {
 		return nil, err
 	}
+	if err := bundleInfo.createSymlinkOrCopyPodmanClient(repo.OcBinDir); err != nil {
+		return nil, err
+	}
 	return bundleInfo, nil
 }
 
@@ -117,6 +120,23 @@ func (bundle *CrcBundleInfo) createSymlinkOrCopyOpenShiftClient(ocBinDir string)
 	return os.Symlink(ocInBundle, ocInBinDir)
 }
 
+func (bundle *CrcBundleInfo) createSymlinkOrCopyPodmanClient(ocBinDir string) error {
+	podmanInBundle := bundle.GetPodmanPath()
+	if podmanInBundle == "" {
+		return nil
+	}
+	podmanInBinDir := filepath.Join(ocBinDir, filepath.Base(podmanInBundle))
+
+	if err := os.MkdirAll(ocBinDir, 0750); err != nil {
+		return err
+	}
+	_ = os.Remove(podmanInBinDir)
+	if runtime.GOOS == "windows" {
+		return crcos.CopyFileContents(podmanInBundle, podmanInBinDir, 0750)
+	}
+	return os.Symlink(podmanInBundle, podmanInBinDir)
+}
+
 func (repo *Repository) Extract(path string) error {
 	bundleName := filepath.Base(path)
 
diff --git a/pkg/crc/machine/client.go b/pkg/crc/machine/client.go
index 58101cd2e..a9c07a81c 100644
--- a/pkg/crc/machine/client.go
+++ b/pkg/crc/machine/client.go
@@ -12,11 +12,11 @@ import (
 
 type Client interface {
 	GetName() string
+	GetConsoleURL() (*types.ConsoleResult, error)
+	ConnectionDetails() (*types.ConnectionDetails, error)
 
 	Delete() error
 	Exists() (bool, error)
-	GetConsoleURL() (*types.ConsoleResult, error)
-	IP() (string, error)
 	PowerOff() error
 	Start(ctx context.Context, startConfig types.StartConfig) (*types.StartResult, error)
 	Status() (*types.ClusterStatusResult, error)
diff --git a/pkg/crc/machine/driver_darwin.go b/pkg/crc/machine/driver_darwin.go
index 2c1387a6d..a35f6f4c8 100644
--- a/pkg/crc/machine/driver_darwin.go
+++ b/pkg/crc/machine/driver_darwin.go
@@ -17,7 +17,7 @@ func newHost(api libmachine.API, machineConfig config.MachineConfig) (*host.Host
 	if err != nil {
 		return nil, errors.New("Failed to marshal driver options")
 	}
-	return api.NewHost("hyperkit", constants.CrcBinDir, json)
+	return api.NewHost("hyperkit", constants.BinDir(), json)
 }
 
 func loadDriverConfig(host *host.Host) (*machineHyperkit.Driver, error) {
diff --git a/pkg/crc/machine/fakemachine/client.go b/pkg/crc/machine/fakemachine/client.go
index 0e9ecb9ec..fb53392e1 100644
--- a/pkg/crc/machine/fakemachine/client.go
+++ b/pkg/crc/machine/fakemachine/client.go
@@ -57,8 +57,8 @@ func (c *Client) GetProxyConfig(machineName string) (*network.ProxyConfig, error
 	return nil, errors.New("not implemented")
 }
 
-func (c *Client) IP() (string, error) {
-	return "", errors.New("not implemented")
+func (c *Client) ConnectionDetails() (*types.ConnectionDetails, error) {
+	return nil, errors.New("not implemented")
 }
 
 func (c *Client) PowerOff() error {
diff --git a/pkg/crc/machine/hyperkit/driver_darwin.go b/pkg/crc/machine/hyperkit/driver_darwin.go
index f48cde9e5..4419a51f1 100644
--- a/pkg/crc/machine/hyperkit/driver_darwin.go
+++ b/pkg/crc/machine/hyperkit/driver_darwin.go
@@ -18,7 +18,7 @@ func CreateHost(machineConfig config.MachineConfig) *hyperkit.Driver {
 	hyperkitDriver.Cmdline = machineConfig.KernelCmdLine
 	hyperkitDriver.VmlinuzPath = machineConfig.Kernel
 	hyperkitDriver.InitrdPath = machineConfig.Initramfs
-	hyperkitDriver.HyperKitPath = filepath.Join(constants.CrcBinDir, HyperKitCommand)
+	hyperkitDriver.HyperKitPath = filepath.Join(constants.BinDir(), HyperKitCommand)
 
 	hyperkitDriver.VMNet = machineConfig.NetworkMode == network.DefaultMode
 
diff --git a/pkg/crc/machine/ip.go b/pkg/crc/machine/ip.go
index 9cff84da3..9e847b03c 100644
--- a/pkg/crc/machine/ip.go
+++ b/pkg/crc/machine/ip.go
@@ -1,18 +1,32 @@
 package machine
 
-import "github.com/pkg/errors"
+import (
+	"github.com/code-ready/crc/pkg/crc/constants"
+	"github.com/code-ready/crc/pkg/crc/machine/types"
+	"github.com/pkg/errors"
+)
 
-func (client *client) IP() (string, error) {
+func (client *client) ConnectionDetails() (*types.ConnectionDetails, error) {
 	libMachineAPIClient, cleanup := createLibMachineClient()
 	defer cleanup()
 	host, err := libMachineAPIClient.Load(client.name)
+	if err != nil {
+		return nil, errors.Wrap(err, "Cannot load machine")
+	}
 
+	bundle, err := getBundleMetadataFromDriver(host.Driver)
 	if err != nil {
-		return "", errors.Wrap(err, "Cannot load machine")
+		return nil, errors.Wrap(err, "Error loading bundle metadata")
 	}
+
 	ip, err := getIP(host, client.useVSock())
 	if err != nil {
-		return "", errors.Wrap(err, "Cannot get IP")
+		return nil, errors.Wrap(err, "Cannot get IP")
 	}
-	return ip, nil
+	return &types.ConnectionDetails{
+		IP:          ip,
+		SSHPort:     getSSHPort(client.useVSock()),
+		SSHUsername: constants.DefaultSSHUser,
+		SSHKeys:     []string{constants.GetPrivateKeyPath(), constants.GetRsaPrivateKeyPath(), bundle.GetSSHKeyPath()},
+	}, nil
 }
diff --git a/pkg/crc/machine/start.go b/pkg/crc/machine/start.go
index e80e884bf..54004ef97 100644
--- a/pkg/crc/machine/start.go
+++ b/pkg/crc/machine/start.go
@@ -350,7 +350,6 @@ func (client *client) Start(ctx context.Context, startConfig types.StartConfig)
 		if !client.useVSock() {
 			return nil, errors.Wrapf(err, "Failed internal DNS query: %s", queryOutput)
 		}
-		logging.Warn(fmt.Sprintf("Failed internal DNS query: %s: %v", queryOutput, err))
 	}
 	logging.Info("Check internal and public DNS query ...")
 
diff --git a/pkg/crc/machine/sync.go b/pkg/crc/machine/sync.go
index 5099e5781..4fe952967 100644
--- a/pkg/crc/machine/sync.go
+++ b/pkg/crc/machine/sync.go
@@ -154,8 +154,8 @@ func (s *Synchronized) GetConsoleURL() (*types.ConsoleResult, error) {
 	return s.underlying.GetConsoleURL()
 }
 
-func (s *Synchronized) IP() (string, error) {
-	return s.underlying.IP()
+func (s *Synchronized) ConnectionDetails() (*types.ConnectionDetails, error) {
+	return s.underlying.ConnectionDetails()
 }
 
 func (s *Synchronized) PowerOff() error {
diff --git a/pkg/crc/machine/types/types.go b/pkg/crc/machine/types/types.go
index 6bb212e75..87e8feac5 100644
--- a/pkg/crc/machine/types/types.go
+++ b/pkg/crc/machine/types/types.go
@@ -56,3 +56,10 @@ type ConsoleResult struct {
 	ClusterConfig ClusterConfig
 	State         state.State
 }
+
+type ConnectionDetails struct {
+	IP          string
+	SSHPort     int
+	SSHUsername string
+	SSHKeys     []string
+}
diff --git a/pkg/crc/preflight/preflight_checks_common.go b/pkg/crc/preflight/preflight_checks_common.go
index f9608ff78..e0e10cb61 100644
--- a/pkg/crc/preflight/preflight_checks_common.go
+++ b/pkg/crc/preflight/preflight_checks_common.go
@@ -12,6 +12,7 @@ import (
 	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/code-ready/crc/pkg/crc/machine/bundle"
 	"github.com/code-ready/crc/pkg/crc/validation"
+	"github.com/code-ready/crc/pkg/crc/version"
 	"github.com/code-ready/crc/pkg/embed"
 	crcos "github.com/code-ready/crc/pkg/os"
 	"github.com/docker/go-units"
@@ -140,6 +141,10 @@ func fixPodmanExecutableCached() error {
 
 // Check if helper executable is cached or not
 func checkAdminHelperExecutableCached() error {
+	if version.IsMacosInstallPathSet() {
+		return nil
+	}
+
 	helper := cache.NewAdminHelperCache()
 	if !helper.IsCached() {
 		return errors.New("admin-helper executable is not cached")
@@ -152,6 +157,10 @@ func checkAdminHelperExecutableCached() error {
 }
 
 func fixAdminHelperExecutableCached() error {
+	if version.IsMacosInstallPathSet() {
+		return nil
+	}
+
 	helper := cache.NewAdminHelperCache()
 	if err := helper.EnsureIsCached(); err != nil {
 		return errors.Wrap(err, "Unable to download admin-helper executable")
diff --git a/pkg/crc/preflight/preflight_checks_darwin.go b/pkg/crc/preflight/preflight_checks_darwin.go
index f7f2e0cd7..8f01b2410 100644
--- a/pkg/crc/preflight/preflight_checks_darwin.go
+++ b/pkg/crc/preflight/preflight_checks_darwin.go
@@ -12,6 +12,7 @@ import (
 	"github.com/code-ready/crc/pkg/crc/constants"
 	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/code-ready/crc/pkg/crc/network"
+	"github.com/code-ready/crc/pkg/crc/version"
 	crcos "github.com/code-ready/crc/pkg/os"
 	"golang.org/x/sys/unix"
 )
@@ -24,6 +25,10 @@ const (
 
 func checkHyperKitInstalled(networkMode network.Mode) func() error {
 	return func() error {
+		if version.IsMacosInstallPathSet() {
+			return nil
+		}
+
 		h := cache.NewHyperKitCache()
 		if !h.IsCached() {
 			return fmt.Errorf("%s executable is not cached", h.GetExecutableName())
@@ -45,6 +50,10 @@ func checkHyperKitInstalled(networkMode network.Mode) func() error {
 
 func fixHyperKitInstallation(networkMode network.Mode) func() error {
 	return func() error {
+		if version.IsMacosInstallPathSet() {
+			return nil
+		}
+
 		h := cache.NewHyperKitCache()
 
 		logging.Debugf("Installing %s", h.GetExecutableName())
@@ -61,6 +70,10 @@ func fixHyperKitInstallation(networkMode network.Mode) func() error {
 
 func checkMachineDriverHyperKitInstalled(networkMode network.Mode) func() error {
 	return func() error {
+		if version.IsMacosInstallPathSet() {
+			return nil
+		}
+
 		hyperkitDriver := cache.NewMachineDriverHyperKitCache()
 
 		logging.Debugf("Checking if %s is installed", hyperkitDriver.GetExecutableName())
@@ -80,6 +93,10 @@ func checkMachineDriverHyperKitInstalled(networkMode network.Mode) func() error
 
 func fixMachineDriverHyperKitInstalled(networkMode network.Mode) func() error {
 	return func() error {
+		if version.IsMacosInstallPathSet() {
+			return nil
+		}
+
 		hyperkitDriver := cache.NewMachineDriverHyperKitCache()
 
 		logging.Debugf("Installing %s", hyperkitDriver.GetExecutableName())
@@ -192,7 +209,7 @@ func stopCRCHyperkitProcess() error {
 	if err != nil {
 		return fmt.Errorf("Could not find 'pgrep'. %w", err)
 	}
-	if _, _, err := crcos.RunWithDefaultLocale(pgrepPath, "-f", filepath.Join(constants.CrcBinDir, "hyperkit")); err != nil {
+	if _, _, err := crcos.RunWithDefaultLocale(pgrepPath, "-f", filepath.Join(constants.BinDir(), "hyperkit")); err != nil {
 		var exitErr *exec.ExitError
 		if errors.As(err, &exitErr) {
 			/* 1: no processes matched */
@@ -209,7 +226,7 @@ func stopCRCHyperkitProcess() error {
 	if err != nil {
 		return fmt.Errorf("Could not find 'pkill'. %w", err)
 	}
-	if _, _, err := crcos.RunWithDefaultLocale(pkillPath, "-SIGKILL", "-f", filepath.Join(constants.CrcBinDir, "hyperkit")); err != nil {
+	if _, _, err := crcos.RunWithDefaultLocale(pkillPath, "-SIGKILL", "-f", filepath.Join(constants.BinDir(), "hyperkit")); err != nil {
 		return fmt.Errorf("Failed to kill 'hyperkit' process. %w", err)
 	}
 	return nil
diff --git a/pkg/crc/segment/segment.go b/pkg/crc/segment/segment.go
index 325e10a04..0b9900030 100644
--- a/pkg/crc/segment/segment.go
+++ b/pkg/crc/segment/segment.go
@@ -60,7 +60,15 @@ func (c *Client) Close() error {
 	return c.segmentClient.Close()
 }
 
-func (c *Client) Upload(ctx context.Context, action string, duration time.Duration, err error) error {
+func (c *Client) UploadAction(action, source string) error {
+	return c.upload(action, baseProperties(source))
+}
+
+func (c *Client) UploadCmd(ctx context.Context, action string, duration time.Duration, err error) error {
+	return c.upload(action, properties(ctx, err, duration))
+}
+
+func (c *Client) upload(action string, a analytics.Properties) error {
 	if c.config.Get(config.ConsentTelemetry).AsString() != "yes" {
 		return nil
 	}
@@ -77,12 +85,26 @@ func (c *Client) Upload(ctx context.Context, action string, duration time.Durati
 		return err
 	}
 
-	properties := analytics.NewProperties()
+	return c.segmentClient.Enqueue(analytics.Track{
+		UserId:     userID,
+		Event:      action,
+		Properties: a,
+	})
+}
+
+func baseProperties(source string) analytics.Properties {
+	return analytics.NewProperties().
+		Set("version", version.GetCRCVersion()).
+		Set("source", source)
+}
+
+func properties(ctx context.Context, err error, duration time.Duration) analytics.Properties {
+	properties := baseProperties("cli")
 	for k, v := range telemetry.GetContextProperties(ctx) {
 		properties = properties.Set(k, v)
 	}
 
-	properties = properties.Set("version", version.GetCRCVersion()).
+	properties = properties.
 		Set("success", err == nil).
 		Set("duration", duration.Milliseconds()).
 		Set("tty", crcos.RunningInTerminal()).
@@ -91,12 +113,7 @@ func (c *Client) Upload(ctx context.Context, action string, duration time.Durati
 		properties = properties.Set("error", telemetry.SetError(err)).
 			Set("error-type", errorType(err))
 	}
-
-	return c.segmentClient.Enqueue(analytics.Track{
-		UserId:     userID,
-		Event:      action,
-		Properties: properties,
-	})
+	return properties
 }
 
 func addConfigTraits(c *crcConfig.Config, in analytics.Traits) analytics.Traits {


Test patch:
diff --git a/pkg/crc/api/api_http_test.go b/pkg/crc/api/api_http_test.go
index 8b1025dec..f02566b2b 100644
--- a/pkg/crc/api/api_http_test.go
+++ b/pkg/crc/api/api_http_test.go
@@ -16,7 +16,7 @@ func TestHTTPApi(t *testing.T) {
 	fakeMachine := fakemachine.NewClient()
 	config := setupNewInMemoryConfig()
 
-	ts := httptest.NewServer(NewMux(config, fakeMachine))
+	ts := httptest.NewServer(NewMux(config, fakeMachine, &mockLogger{}, &mockTelemetry{}))
 	defer ts.Close()
 
 	client := apiClient.New(http.DefaultClient, ts.URL)
@@ -123,3 +123,19 @@ func TestHTTPApi(t *testing.T) {
 		configSetResult,
 	)
 }
+
+func TestTelemetry(t *testing.T) {
+	fakeMachine := fakemachine.NewClient()
+	config := setupNewInMemoryConfig()
+
+	telemetry := &mockTelemetry{}
+	ts := httptest.NewServer(NewMux(config, fakeMachine, &mockLogger{}, telemetry))
+	defer ts.Close()
+
+	client := apiClient.New(http.DefaultClient, ts.URL)
+
+	_ = client.Telemetry("click start")
+	_ = client.Telemetry("click stop")
+
+	assert.Equal(t, []string{"click start", "click stop"}, telemetry.actions)
+}
diff --git a/pkg/crc/api/api_test.go b/pkg/crc/api/api_test.go
index 67b58de27..fe1f03a86 100644
--- a/pkg/crc/api/api_test.go
+++ b/pkg/crc/api/api_test.go
@@ -28,7 +28,7 @@ func TestApi(t *testing.T) {
 	require.NoError(t, err)
 
 	client := fakemachine.NewClient()
-	api, err := createServerWithListener(listener, setupNewInMemoryConfig(), client, &mockLogger{})
+	api, err := createServerWithListener(listener, setupNewInMemoryConfig(), client, &mockLogger{}, &mockTelemetry{})
 	require.NoError(t, err)
 	go func() {
 		if err := api.Serve(); err != nil {
@@ -122,6 +122,15 @@ func TestApi(t *testing.T) {
 				"Success":  true,
 			},
 		},
+		{
+			command: "telemetry",
+			args:    json.RawMessage(`{"action":"click start"}`),
+			expected: map[string]interface{}{
+				"Success": true,
+				"Name":    "",
+				"Error":   "",
+			},
+		},
 	}
 	for _, test := range tt {
 		client.Failing = test.clientFailing
@@ -211,7 +220,7 @@ func setupAPIServer(t *testing.T) (string, func()) {
 	require.NoError(t, err)
 
 	client := fakemachine.NewClient()
-	api, err := createServerWithListener(listener, setupNewInMemoryConfig(), client, &mockLogger{})
+	api, err := createServerWithListener(listener, setupNewInMemoryConfig(), client, &mockLogger{}, &mockTelemetry{})
 	require.NoError(t, err)
 	go func() {
 		if err := api.Serve(); err != nil {
@@ -221,10 +230,3 @@ func setupAPIServer(t *testing.T) (string, func()) {
 
 	return socket, func() { os.RemoveAll(dir) }
 }
-
-type mockLogger struct {
-}
-
-func (*mockLogger) Messages() []string {
-	return []string{"message 1", "message 2", "message 3"}
-}
diff --git a/pkg/crc/api/apitest.go b/pkg/crc/api/helpers_test.go
similarity index 73%
rename from pkg/crc/api/apitest.go
rename to pkg/crc/api/helpers_test.go
index f28a6996b..e303d3df2 100644
--- a/pkg/crc/api/apitest.go
+++ b/pkg/crc/api/helpers_test.go
@@ -37,3 +37,19 @@ func (s *skipPreflights) Set(key string, value interface{}) error {
 func (s *skipPreflights) Unset(key string) error {
 	return s.storage.Unset(key)
 }
+
+type mockLogger struct {
+}
+
+func (*mockLogger) Messages() []string {
+	return []string{"message 1", "message 2", "message 3"}
+}
+
+type mockTelemetry struct {
+	actions []string
+}
+
+func (m *mockTelemetry) UploadAction(action, _ string) error {
+	m.actions = append(m.actions, action)
+	return nil
+}
diff --git a/pkg/crc/machine/sync_test.go b/pkg/crc/machine/sync_test.go
index 7214a78d6..e29e0e9e8 100644
--- a/pkg/crc/machine/sync_test.go
+++ b/pkg/crc/machine/sync_test.go
@@ -135,8 +135,8 @@ func (m *waitingMachine) GetConsoleURL() (*types.ConsoleResult, error) {
 	return nil, errors.New("not implemented")
 }
 
-func (m *waitingMachine) IP() (string, error) {
-	return "", errors.New("not implemented")
+func (m *waitingMachine) ConnectionDetails() (*types.ConnectionDetails, error) {
+	return nil, errors.New("not implemented")
 }
 
 func (m *waitingMachine) PowerOff() error {
diff --git a/pkg/crc/segment/segment_test.go b/pkg/crc/segment/segment_test.go
index d0c3522ac..a9e7c91b7 100644
--- a/pkg/crc/segment/segment_test.go
+++ b/pkg/crc/segment/segment_test.go
@@ -92,7 +92,7 @@ func TestClientUploadWithConsentAndWithSerializableError(t *testing.T) {
 	c, err := newCustomClient(config, uuidFile, server.URL)
 	require.NoError(t, err)
 
-	require.NoError(t, c.Upload(context.Background(), "start", time.Minute, crcErr.ToSerializableError(crcErr.VMNotExist)))
+	require.NoError(t, c.UploadCmd(context.Background(), "start", time.Minute, crcErr.ToSerializableError(crcErr.VMNotExist)))
 	require.NoError(t, c.Close())
 
 	uuid, err := ioutil.ReadFile(uuidFile)
@@ -132,7 +132,7 @@ func TestClientUploadWithConsentAndWithoutSerializableError(t *testing.T) {
 	c, err := newCustomClient(config, filepath.Join(dir, "telemetry"), server.URL)
 	require.NoError(t, err)
 
-	require.NoError(t, c.Upload(context.Background(), "start", time.Minute, errors.New("an error occurred")))
+	require.NoError(t, c.UploadCmd(context.Background(), "start", time.Minute, errors.New("an error occurred")))
 	require.NoError(t, c.Close())
 
 	select {
@@ -169,7 +169,7 @@ func TestClientUploadWithContext(t *testing.T) {
 
 	ctx := telemetry.NewContext(context.Background())
 	telemetry.SetCPUs(ctx, 6)
-	require.NoError(t, c.Upload(ctx, "start", time.Minute, nil))
+	require.NoError(t, c.UploadCmd(ctx, "start", time.Minute, nil))
 	require.NoError(t, c.Close())
 
 	select {
@@ -197,7 +197,7 @@ func TestClientUploadWithOutConsent(t *testing.T) {
 	c, err := newCustomClient(config, filepath.Join(dir, "telemetry"), server.URL)
 	require.NoError(t, err)
 
-	require.NoError(t, c.Upload(context.Background(), "start", time.Second, errors.New("an error occurred")))
+	require.NoError(t, c.UploadCmd(context.Background(), "start", time.Second, errors.New("an error occurred")))
 	require.NoError(t, c.Close())
 
 	select {
diff --git a/test/e2e/crcsuite/collect.go b/test/e2e/crcsuite/collect.go
index 5822abcde..d78e47253 100644
--- a/test/e2e/crcsuite/collect.go
+++ b/test/e2e/crcsuite/collect.go
@@ -177,12 +177,7 @@ type VMCommandCollector struct {
 }
 
 func (collector *VMCommandCollector) Collect(w Writer) error {
-	client := machine.NewClient(constants.DefaultName, true, crcConfig.New(crcConfig.NewEmptyInMemoryStorage()))
-	ip, err := client.IP()
-	if err != nil {
-		return err
-	}
-	ssh, err := ssh.NewClient(constants.DefaultSSHUser, ip, 22, constants.GetPrivateKeyPath())
+	ssh, err := sshClient()
 	if err != nil {
 		return err
 	}
@@ -198,12 +193,7 @@ type ContainerLogCollector struct {
 }
 
 func (collector *ContainerLogCollector) Collect(w Writer) error {
-	client := machine.NewClient(constants.DefaultName, true, crcConfig.New(crcConfig.NewEmptyInMemoryStorage()))
-	ip, err := client.IP()
-	if err != nil {
-		return err
-	}
-	ssh, err := ssh.NewClient(constants.DefaultSSHUser, ip, 22, constants.GetPrivateKeyPath())
+	ssh, err := sshClient()
 	if err != nil {
 		return err
 	}
@@ -234,6 +224,15 @@ func (collector *ContainerLogCollector) Collect(w Writer) error {
 	return nil
 }
 
+func sshClient() (ssh.Client, error) {
+	client := machine.NewClient(constants.DefaultName, true, crcConfig.New(crcConfig.NewEmptyInMemoryStorage()))
+	connectionDetails, err := client.ConnectionDetails()
+	if err != nil {
+		return nil, err
+	}
+	return ssh.NewClient(connectionDetails.SSHUsername, connectionDetails.IP, connectionDetails.SSHPort, connectionDetails.SSHKeys...)
+}
+
 func command(command, target string) *CommandCollector {
 	return &CommandCollector{
 		Command: command,
diff --git a/test/integration/utilities_test.go b/test/integration/utilities_test.go
index d40972e97..0233111f1 100644
--- a/test/integration/utilities_test.go
+++ b/test/integration/utilities_test.go
@@ -136,12 +136,11 @@ func RunCRCExpectFail(args ...string) (string, error) {
 // Send command to CRC VM via SSH
 func SendCommandToVM(cmd string) (string, error) {
 	client := machine.NewClient(constants.DefaultName, false, crcConfig.New(crcConfig.NewEmptyInMemoryStorage()))
-	ip, err := client.IP()
+	connectionDetails, err := client.ConnectionDetails()
 	if err != nil {
 		return "", err
 	}
-
-	ssh, err := ssh.NewClient(constants.DefaultSSHUser, ip, 22, constants.GetPrivateKeyPath())
+	ssh, err := ssh.NewClient(connectionDetails.SSHUsername, connectionDetails.IP, connectionDetails.SSHPort, connectionDetails.SSHKeys...)
 	if err != nil {
 		return "", err
 	}