Repository: code-ready/crc
Pull request #2153 (2021-03-25 18:19:48)

[E2E] macos tray test automation
## Solution/Idea

Add E2E automation tests for macos tray. The automation for mac will use native solution based on applescripts to interact with the tray.

This PR includes the implementation for the initial set of e2e scenarios for ux components on MacOS platform. To accommodate that implementation a refactor on the testing structure was required in order to expand and reuse the functionality among all different types of test (integration and e2e) 

Downstream CI run results for this PR:

* [windows](https://crcqe-jenkins-csb-codeready.cloud.paas.psi.redhat.com/blue/organizations/jenkins/qe%2Fbundle_baremetal_windows10-brno/detail/bundle_baremetal_windows10-brno/217/pipeline)
* [mac](https://crcqe-jenkins-csb-codeready.cloud.paas.psi.redhat.com/blue/organizations/jenkins/qe%2Fbundle_baremetal_macos14-brno/detail/bundle_baremetal_macos14-brno/273/pipeline)
* [rhel](https://crcqe-jenkins-csb-codeready.cloud.paas.psi.redhat.com/blue/organizations/jenkins/qe%2Fbundle_baremetal_rhel8-brno/detail/bundle_baremetal_rhel8-brno/174/pipeline)

The required parameters to run the ux scenarios are still missing on the downstream CI, ux feature was run manually on a mac instance with the current CRC version (1.25.0):
```bash
make -C macos_tray e2e GODOG_OPTS="--godog.tags='@darwin && @ux' --godog.format=junit" \
                BUNDLE_LOCATION="--bundle-location=''" \
                PULL_SECRET_FILE="--pull-secret-file=pull-secret" \
                INSTALLER_PATH="--installer-path=crc-macos-amd64.pkg" \
                USER_PASSWORD="--user-password=XXXX"
```
resulting on:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="crc" tests="7" skipped="0" failures="1" errors="0" time="1354.117782704">
  <testsuite name="Basic test" tests="0" skipped="0" failures="0" errors="0" time="0"></testsuite>
  <testsuite name="Behind proxy test" tests="0" skipped="0" failures="0" errors="0" time="0"></testsuite>
  <testsuite name="Certificate rotation test" tests="0" skipped="0" failures="0" errors="0" time="0"></testsuite>
  <testsuite name="End-to-end health check" tests="0" skipped="0" failures="0" errors="0" time="0"></testsuite>
  <testsuite name="Local image to image-registry" tests="0" skipped="0" failures="0" errors="0" time="0"></testsuite>
  <testsuite name="Operator from marketplace" tests="0" skipped="0" failures="0" errors="0" time="0"></testsuite>
  <testsuite name="Test configuration settings" tests="0" skipped="0" failures="0" errors="0" time="0"></testsuite>
  <testsuite name="ux" tests="7" skipped="0" failures="1" errors="0" time="1354.113146961">
    <testcase name="Install CRC" status="passed" time="33.613631749"></testcase>
    <testcase name="Install tray" status="passed" time="75.117393328"></testcase>
    <testcase name="Start Cluster" status="passed" time="476.660750086"></testcase>
    <testcase name="Connect the cluster #1" status="passed" time="16.173251692"></testcase>
    <testcase name="Connect the cluster #2" status="passed" time="30.273120298"></testcase>
    <testcase name="Stop Cluster" status="failed" time="323.139393178">
      <failure message="Step user should get notified with cluster state as stopped: notification: The OpenShift Cluster was successfully stopped. Timeout"></failure>
    </testcase>
    <testcase name="Restart Cluster" status="passed" time="348.496128553"></testcase>
  </testsuite>
</testsuites>
```

*errors are directly realated with [tray-macos issue #84](https://github.com/code-ready/tray-macos/issues/84)

centos-ci: Can one of the admins verify this patch?
openshift-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2153#" title="Author self-approved">adrianriobo</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **guillaumerose** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @guillaumerose` in a comment when ready.

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

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

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

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":["guillaumerose"]} -->
openshift-ci[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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 **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2153#" title="Author self-approved">adrianriobo</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **guillaumerose** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @guillaumerose` in a comment when ready.

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

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

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

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":["guillaumerose"]} -->
adrianriobo: Related to #2190 current install scenario fails randomly  
openshift-ci[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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 **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2153#" title="Author self-approved">adrianriobo</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **guillaumerose** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @guillaumerose` in a comment when ready.

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

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

- **[OWNERS](https://github.com/open-cluster-management/applifecycle-backend-e2e/blob/master/OWNERS)**

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":["guillaumerose"]} -->
openshift-ci[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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 **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2153#" title="Author self-approved">adrianriobo</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **guillaumerose** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @guillaumerose` in a comment when ready.

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

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

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

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":["guillaumerose"]} -->
openshift-ci[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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 -->
guillaumerose: Can you rebase ? Otherwise,
/lgtm

Thanks a lot for automating this!
openshift-ci-robot: New changes are detected. LGTM label has been removed.
openshift-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2153#" title="Author self-approved">adrianriobo</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please ask for approval from **guillaumerose** after the PR has been reviewed.

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

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

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

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":["guillaumerose"]} -->
openshift-ci[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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/2153#" title="Author self-approved">adrianriobo</a>*, *<a href="https://github.com/code-ready/crc/pull/2153#pullrequestreview-646724157" title="Approved">jsliacan</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)~~ [jsliacan]

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.
openshift-ci[bot]: @adrianriobo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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[bot]: @adrianriobo: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/e2e-crc | 9123c46c4439651853e581b01eadc3d87c1b9c7c | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2153/pull-ci-code-ready-crc-master-e2e-crc/1387384750175424512) | `/test e2e-crc`
ci/prow/images | 1081d63f36dcf2ecc71bc4ac899b3e52a65f738e | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2153/pull-ci-code-ready-crc-master-images/1387663638872985600) | `/test images`

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

<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 -->

Patch:
diff --git a/Makefile b/Makefile
index 2dbd5a4eb..ed3b5dd44 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 SHELL := /bin/bash
 
-BUNDLE_VERSION = 4.7.7
+BUNDLE_VERSION = 4.7.8
 BUNDLE_EXTENSION = crcbundle
 CRC_VERSION = 1.25.0
 COMMIT_SHA=$(shell git rev-parse --short HEAD)
@@ -183,7 +183,7 @@ ifndef CRC_BINARY
 	CRC_BINARY = --crc-binary=$(GOPATH)/bin
 endif
 e2e:
-	@go test --timeout=180m $(REPOPATH)/test/e2e -v $(PULL_SECRET_FILE) $(BUNDLE_LOCATION) $(CRC_BINARY) --bundle-version=$(BUNDLE_VERSION) $(GODOG_OPTS)
+	@go test --timeout=180m $(REPOPATH)/test/e2e -v $(PULL_SECRET_FILE) $(BUNDLE_LOCATION) $(CRC_BINARY) --bundle-version=$(BUNDLE_VERSION) $(GODOG_OPTS) $(INSTALLER_PATH) $(USER_PASSWORD) 
 
 .PHONY: fmt
 fmt:
diff --git a/centos_ci.sh b/centos_ci.sh
index 15763ebae..41b960451 100644
--- a/centos_ci.sh
+++ b/centos_ci.sh
@@ -1,7 +1,7 @@
 #!/bin/bash
 
 # bundle location
-BUNDLE_VERSION=4.7.7
+BUNDLE_VERSION=4.7.8
 BUNDLE=crc_libvirt_$BUNDLE_VERSION.crcbundle
 GO_VERSION=1.14.13
 
diff --git a/cmd/crc/cmd/bundle/bundle.go b/cmd/crc/cmd/bundle/bundle.go
new file mode 100644
index 000000000..90143f600
--- /dev/null
+++ b/cmd/crc/cmd/bundle/bundle.go
@@ -0,0 +1,19 @@
+package bundle
+
+import (
+	"github.com/code-ready/crc/pkg/crc/config"
+	"github.com/spf13/cobra"
+)
+
+func GetBundleCmd(config *config.Config) *cobra.Command {
+	bundleCmd := &cobra.Command{
+		Use:   "bundle SUBCOMMAND [flags]",
+		Short: "Manage CRC bundles",
+		Long:  "Manage CRC bundles",
+		Run: func(cmd *cobra.Command, args []string) {
+			_ = cmd.Help()
+		},
+	}
+	bundleCmd.AddCommand(getGenerateCmd(config))
+	return bundleCmd
+}
diff --git a/cmd/crc/cmd/bundle/generate.go b/cmd/crc/cmd/bundle/generate.go
new file mode 100644
index 000000000..c1cd18c81
--- /dev/null
+++ b/cmd/crc/cmd/bundle/generate.go
@@ -0,0 +1,30 @@
+package bundle
+
+import (
+	"github.com/code-ready/crc/pkg/crc/config"
+	"github.com/code-ready/crc/pkg/crc/constants"
+	"github.com/code-ready/crc/pkg/crc/logging"
+	"github.com/code-ready/crc/pkg/crc/machine"
+	"github.com/spf13/cobra"
+)
+
+func getGenerateCmd(config *config.Config) *cobra.Command {
+	return &cobra.Command{
+		Use:   "generate",
+		Short: "Generate a custom bundle from the running OpenShift cluster",
+		Long:  "Generate a custom bundle from the running OpenShift cluster",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runGenerate(config)
+		},
+	}
+}
+
+func runGenerate(config *config.Config) error {
+	client := machine.NewClient(constants.DefaultName, isDebugLog(), config)
+
+	return client.GenerateBundle()
+}
+
+func isDebugLog() bool {
+	return logging.LogLevel == "debug"
+}
diff --git a/cmd/crc/cmd/root.go b/cmd/crc/cmd/root.go
index 35e24e1b4..1d219f8a9 100644
--- a/cmd/crc/cmd/root.go
+++ b/cmd/crc/cmd/root.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"time"
 
+	cmdBundle "github.com/code-ready/crc/cmd/crc/cmd/bundle"
 	cmdConfig "github.com/code-ready/crc/cmd/crc/cmd/config"
 	crcConfig "github.com/code-ready/crc/pkg/crc/config"
 	"github.com/code-ready/crc/pkg/crc/constants"
@@ -61,6 +62,7 @@ func init() {
 
 	// subcommands
 	rootCmd.AddCommand(cmdConfig.GetConfigCmd(config))
+	rootCmd.AddCommand(cmdBundle.GetBundleCmd(config))
 
 	rootCmd.PersistentFlags().StringVar(&logging.LogLevel, "log-level", constants.DefaultLogLevel, "log level (e.g. \"debug | info | warn | error\")")
 }
diff --git a/cmd/crc/cmd/setup.go b/cmd/crc/cmd/setup.go
index 35aa39649..fe18745a6 100644
--- a/cmd/crc/cmd/setup.go
+++ b/cmd/crc/cmd/setup.go
@@ -54,17 +54,13 @@ func runSetup(arguments []string) error {
 			fmt.Printf("No worry, you can still enable telemetry manually with the command 'crc config set %s yes'.\n", cmdConfig.ConsentTelemetry)
 		}
 	}
-	var err error
-	if checkOnly {
-		err = preflight.StartPreflightChecks(config)
-		if err != nil {
-			err = exec.CodeExitError{
-				Err:  err,
-				Code: preflightFailedExitCode,
-			}
+
+	err := preflight.SetupHost(config, checkOnly)
+	if err != nil && checkOnly {
+		err = exec.CodeExitError{
+			Err:  err,
+			Code: preflightFailedExitCode,
 		}
-	} else {
-		err = preflight.SetupHost(config)
 	}
 
 	return render(&setupResult{
diff --git a/pkg/compress/compress.go b/pkg/compress/compress.go
new file mode 100644
index 000000000..bed9498cd
--- /dev/null
+++ b/pkg/compress/compress.go
@@ -0,0 +1,71 @@
+package compress
+
+import (
+	"archive/tar"
+	"io"
+	"os"
+	"path/filepath"
+
+	"github.com/code-ready/crc/pkg/crc/logging"
+	"github.com/klauspost/compress/zstd"
+)
+
+func Compress(src, dest string) (err error) {
+	out, err := os.Create(dest)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		cerr := out.Close()
+		if err == nil {
+			err = cerr
+		}
+	}()
+
+	enc, err := zstd.NewWriter(out)
+	if err != nil {
+		return err
+	}
+	defer enc.Close()
+
+	tarWriter := tar.NewWriter(enc)
+	defer tarWriter.Close()
+
+	basePath, _ := filepath.Split(src)
+
+	// Just use top level directory as part of tarball
+	// $ zstdcat crc_libvirt_4.7.1_custom.zstd  | tar t
+	// crc_libvirt_4.7.1_custom
+	// crc_libvirt_4.7.1_custom/test
+	return filepath.Walk(src, func(file string, fi os.FileInfo, err1 error) error {
+		if err1 != nil {
+			return err1
+		}
+		logging.Debugf("Adding %s", file)
+		header, err := tar.FileInfoHeader(fi, file)
+		if err != nil {
+			return err
+		}
+		// must provide real name
+		// (see https://golang.org/src/archive/tar/common.go?#L626)
+		header.Name, err = filepath.Rel(basePath, filepath.ToSlash(file))
+		if err != nil {
+			return err
+		}
+
+		if err := tarWriter.WriteHeader(header); err != nil {
+			return err
+		}
+		if !fi.IsDir() {
+			data, err := os.Open(file)
+			if err != nil {
+				return err
+			}
+			defer data.Close()
+			if _, err := io.Copy(tarWriter, data); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
diff --git a/pkg/crc/cluster/cluster.go b/pkg/crc/cluster/cluster.go
index d8bb9e942..2a62bb52e 100644
--- a/pkg/crc/cluster/cluster.go
+++ b/pkg/crc/cluster/cluster.go
@@ -126,6 +126,35 @@ func EnsurePullSecretPresentInTheCluster(ocConfig oc.Config, pullSec PullSecretL
 	return nil
 }
 
+func RemovePullSecretFromCluster(ocConfig oc.Config, sshRunner *ssh.Runner) error {
+	logging.Info("Removing user's pull secret from instance disk and from cluster secret...")
+	if _, _, err := sshRunner.RunPrivileged("Removing user's pull secret", "rm", "-fr", vmPullSecretPath); err != nil {
+		return err
+	}
+	cmdArgs := []string{"patch", "secret", "pull-secret", "-p",
+		`'{"data":{".dockerconfigjson":"e30K"}}'`,
+		"-n", "openshift-config", "--type", "merge"}
+
+	_, stderr, err := ocConfig.RunOcCommand(cmdArgs...)
+	if err != nil {
+		return fmt.Errorf("Failed to remove Pull secret %w: %s", err, stderr)
+	}
+
+	cmdArgs = []string{"delete", "machineconfigs", "--all"}
+	_, stderr, err = ocConfig.RunOcCommand(cmdArgs...)
+	if err != nil {
+		return fmt.Errorf("Failed to remove machineconfigs %w: %s", err, stderr)
+	}
+
+	cmdArgs = []string{"delete", "machineconfigpools", "--all"}
+	_, stderr, err = ocConfig.RunOcCommand(cmdArgs...)
+	if err != nil {
+		return fmt.Errorf("Failed to remove machineconfigpools %w: %s", err, stderr)
+	}
+
+	return nil
+}
+
 func EnsureClusterIDIsNotEmpty(ocConfig oc.Config) error {
 	if err := WaitForOpenshiftResource(ocConfig, "clusterversion"); err != nil {
 		return err
diff --git a/pkg/crc/constants/constants_windows.go b/pkg/crc/constants/constants_windows.go
index 7fd8270e2..e67efb7e9 100644
--- a/pkg/crc/constants/constants_windows.go
+++ b/pkg/crc/constants/constants_windows.go
@@ -8,8 +8,8 @@ import (
 const (
 	OcExecutableName            = "oc.exe"
 	AdminHelperExecutableName   = "admin-helper-windows.exe"
-	TrayExecutableName          = "tray-windows.exe"
-	TrayShortcutName            = "tray-windows.lnk"
+	TrayExecutableName          = "crc-tray.exe"
+	TrayShortcutName            = "crc-tray.lnk"
 	DaemonBatchFileName         = "crc-daemon-autostart.bat"
 	DaemonPSScriptName          = "launch-crc-daemon.ps1"
 	DaemonBatchFileShortcutName = "crc-daemmon-autostart.bat.lnk"
diff --git a/pkg/crc/machine/bundle/copier.go b/pkg/crc/machine/bundle/copier.go
new file mode 100644
index 000000000..18562c577
--- /dev/null
+++ b/pkg/crc/machine/bundle/copier.go
@@ -0,0 +1,160 @@
+package bundle
+
+import (
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/code-ready/crc/pkg/compress"
+	"github.com/code-ready/crc/pkg/crc/logging"
+	crcos "github.com/code-ready/crc/pkg/os"
+	godeepcopy "github.com/jinzhu/copier"
+)
+
+type Copier struct {
+	srcBundle    *CrcBundleInfo
+	copiedBundle CrcBundleInfo
+}
+
+func NewCopier(srcBundle *CrcBundleInfo, basePath string, customBundleName string) (*Copier, error) {
+	var copier Copier
+
+	bundlePath := filepath.Join(basePath, customBundleName)
+	if err := os.Mkdir(bundlePath, 0775); err != nil {
+		return nil, err
+	}
+
+	// Creating a deepCopy of source bundle metadata so that whatever we make
+	// modification of any metadata field for copied operation should only
+	// effect the copied bundle metadata instead of source bundle metadata.
+	if err := godeepcopy.Copy(&copier.copiedBundle, srcBundle); err != nil {
+		return nil, err
+	}
+
+	copier.copiedBundle.bundleName = customBundleName
+	copier.copiedBundle.Type = "custom"
+	copier.srcBundle = srcBundle
+	copier.copiedBundle.cachedPath = bundlePath
+
+	return &copier, nil
+}
+
+func (copier *Copier) Cleanup() error {
+	return os.RemoveAll(copier.copiedBundle.cachedPath)
+}
+
+func (copier *Copier) resolvePath(filename string) string {
+	return copier.copiedBundle.resolvePath(filename)
+}
+
+func (copier *Copier) CachedPath() string {
+	return copier.copiedBundle.cachedPath
+}
+
+func (copier *Copier) CopyPrivateSSHKey(srcPath string) error {
+	sshKeyFileName := filepath.Base(copier.srcBundle.GetSSHKeyPath())
+	destPath := copier.resolvePath(sshKeyFileName)
+	return crcos.CopyFileContents(srcPath, destPath, 0400)
+}
+
+func (copier *Copier) SetKubeAdminPassword(kubePassword string) error {
+	kubeAdminPasswordFileName := copier.srcBundle.ClusterInfo.KubeadminPasswordFile
+	path := copier.resolvePath(kubeAdminPasswordFileName)
+	return ioutil.WriteFile(path, []byte(kubePassword), 0600)
+}
+
+func (copier *Copier) CopyKubeConfig() error {
+	kubeConfigFileName := filepath.Base(copier.srcBundle.GetKubeConfigPath())
+	srcPath := copier.srcBundle.GetKubeConfigPath()
+	destPath := copier.resolvePath(kubeConfigFileName)
+	return crcos.CopyFileContents(srcPath, destPath, 0640)
+}
+
+func (copier *Copier) CopyFilesFromFileList() error {
+	for _, file := range copier.srcBundle.Storage.Files {
+		srcPath := copier.srcBundle.resolvePath(file.Name)
+		destPath := copier.resolvePath(filepath.Base(srcPath))
+		if err := crcos.CopyFileContents(srcPath, destPath, 0755); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (copier *Copier) SetDiskImage(path string, format string) error {
+	fileInfo, err := getFileInfo(path)
+	if err != nil {
+		return err
+	}
+	diskImage := DiskImage{
+		File:   *fileInfo,
+		Format: format,
+	}
+
+	copier.copiedBundle.Storage.DiskImages = []DiskImage{diskImage}
+
+	return nil
+}
+
+func (copier *Copier) GenerateBundle(bundleName string) error {
+	if err := copier.copiedBundle.verify(); err != nil {
+		return err
+	}
+
+	// update bundle info
+	copier.copiedBundle.BuildInfo.BuildTime = time.Now().String()
+
+	// Create the metadata json for custom bundle
+	bundleContent, err := json.MarshalIndent(copier.copiedBundle, "", " ")
+	if err != nil {
+		return err
+	}
+	err = ioutil.WriteFile(copier.resolvePath("crc-bundle-info.json"), bundleContent, 0600)
+	if err != nil {
+		return fmt.Errorf("error copying bundle metadata  %w", err)
+	}
+
+	logging.Infof("Compressing %s...", GetBundleNameWithoutExtension(copier.copiedBundle.bundleName))
+	return compress.Compress(copier.copiedBundle.cachedPath, fmt.Sprintf("%s%s", bundleName, bundleExtension))
+}
+
+func sha256sum(path string) (string, error) {
+	logging.Infof("Generating sha256sum for %s...", filepath.Base(path))
+	f, err := os.Open(path)
+	if err != nil {
+		return "", err
+	}
+	defer f.Close()
+
+	h := sha256.New()
+	if _, err := io.Copy(h, f); err != nil {
+		return "", err
+	}
+
+	return hex.EncodeToString(h.Sum(nil)), nil
+}
+
+func getFileInfo(path string) (*File, error) {
+	fileInfo := File{}
+	fileInfo.Name = filepath.Base(path)
+
+	stat, err := os.Stat(path)
+	if err != nil {
+		return nil, err
+	}
+	fileInfo.Size = fmt.Sprintf("%d", stat.Size())
+
+	checksum, err := sha256sum(path)
+	if err != nil {
+		return nil, err
+	}
+	fileInfo.Checksum = checksum
+
+	return &fileInfo, nil
+}
diff --git a/pkg/crc/machine/bundle/metadata.go b/pkg/crc/machine/bundle/metadata.go
index 73bcef7b8..f10c638fa 100644
--- a/pkg/crc/machine/bundle/metadata.go
+++ b/pkg/crc/machine/bundle/metadata.go
@@ -103,6 +103,10 @@ func (bundle *CrcBundleInfo) GetDiskImagePath() string {
 	return bundle.resolvePath(bundle.Storage.DiskImages[0].Name)
 }
 
+func (bundle *CrcBundleInfo) GetDiskImageFormat() string {
+	return bundle.Storage.DiskImages[0].Format
+}
+
 func (bundle *CrcBundleInfo) GetKubeConfigPath() string {
 	return bundle.resolvePath(bundle.ClusterInfo.KubeConfig)
 }
@@ -145,6 +149,10 @@ func (bundle *CrcBundleInfo) GetInitramfsPath() string {
 	return bundle.resolvePath(bundle.Nodes[0].Initramfs)
 }
 
+func (bundle *CrcBundleInfo) GetKernelCommandLine() string {
+	return bundle.Nodes[0].KernelCmdLine
+}
+
 func (bundle *CrcBundleInfo) GetKubeadminPassword() (string, error) {
 	rawData, err := ioutil.ReadFile(bundle.resolvePath(bundle.ClusterInfo.KubeadminPasswordFile))
 	if err != nil {
@@ -161,6 +169,10 @@ func (bundle *CrcBundleInfo) GetOpenshiftVersion() string {
 	return bundle.ClusterInfo.OpenShiftVersion
 }
 
+func (bundle *CrcBundleInfo) GetBundleNameWithoutExtension() string {
+	return GetBundleNameWithoutExtension(bundle.GetBundleName())
+}
+
 func (bundle *CrcBundleInfo) verify() error {
 	files := []string{
 		bundle.GetOcPath(),
@@ -202,3 +214,15 @@ func (bundle *CrcBundleInfo) checkDiskImageSize() error {
 	}
 	return nil
 }
+
+func GetBundleNameWithoutExtension(bundleName string) string {
+	return strings.TrimSuffix(bundleName, bundleExtension)
+}
+
+func GetCustomBundleNameWithoutExtension(bundleName string) string {
+	return fmt.Sprintf("%s_%d", GetBundleNameWithoutExtension(bundleName), time.Now().Unix())
+}
+
+func GetCustomBundle(bundleNameWithoutExtension string) string {
+	return fmt.Sprintf("%s%s", bundleNameWithoutExtension, bundleExtension)
+}
diff --git a/pkg/crc/machine/bundle/repository.go b/pkg/crc/machine/bundle/repository.go
index 532d7ffb1..1ffed704d 100644
--- a/pkg/crc/machine/bundle/repository.go
+++ b/pkg/crc/machine/bundle/repository.go
@@ -7,7 +7,6 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
-	"strings"
 	"time"
 
 	"github.com/Masterminds/semver"
@@ -30,7 +29,7 @@ type Repository struct {
 }
 
 func (repo *Repository) Get(bundleName string) (*CrcBundleInfo, error) {
-	path := filepath.Join(repo.CacheDir, strings.TrimSuffix(bundleName, bundleExtension))
+	path := filepath.Join(repo.CacheDir, GetBundleNameWithoutExtension(bundleName))
 	if _, err := os.Stat(path); err != nil {
 		return nil, errors.Wrapf(err, "could not find cached bundle info in %s", path)
 	}
@@ -150,7 +149,7 @@ func (repo *Repository) Extract(path string) error {
 		return err
 	}
 
-	bundleBaseDir := strings.TrimSuffix(bundleName, bundleExtension)
+	bundleBaseDir := GetBundleNameWithoutExtension(bundleName)
 	bundleDir := filepath.Join(repo.CacheDir, bundleBaseDir)
 	_ = os.RemoveAll(bundleDir)
 	return crcerrors.RetryAfter(time.Minute, func() error {
diff --git a/pkg/crc/machine/client.go b/pkg/crc/machine/client.go
index a9c07a81c..6d40d6de2 100644
--- a/pkg/crc/machine/client.go
+++ b/pkg/crc/machine/client.go
@@ -22,6 +22,7 @@ type Client interface {
 	Status() (*types.ClusterStatusResult, error)
 	Stop() (state.State, error)
 	IsRunning() (bool, error)
+	GenerateBundle() error
 }
 
 type client struct {
diff --git a/pkg/crc/machine/fakemachine/client.go b/pkg/crc/machine/fakemachine/client.go
index fb53392e1..cc00373c8 100644
--- a/pkg/crc/machine/fakemachine/client.go
+++ b/pkg/crc/machine/fakemachine/client.go
@@ -68,6 +68,10 @@ func (c *Client) PowerOff() error {
 	return nil
 }
 
+func (c *Client) GenerateBundle() error {
+	return nil
+}
+
 func (c *Client) Start(ctx context.Context, startConfig types.StartConfig) (*types.StartResult, error) {
 	if c.Failing {
 		return nil, errors.New("Failed to start")
diff --git a/pkg/crc/machine/generate_bundle.go b/pkg/crc/machine/generate_bundle.go
new file mode 100644
index 000000000..78e97414a
--- /dev/null
+++ b/pkg/crc/machine/generate_bundle.go
@@ -0,0 +1,132 @@
+package machine
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/code-ready/crc/pkg/crc/cluster"
+	"github.com/code-ready/crc/pkg/crc/constants"
+	"github.com/code-ready/crc/pkg/crc/logging"
+	"github.com/code-ready/crc/pkg/crc/machine/bundle"
+	"github.com/code-ready/crc/pkg/crc/oc"
+	crcssh "github.com/code-ready/crc/pkg/crc/ssh"
+	"github.com/code-ready/machine/libmachine/state"
+	"github.com/pkg/errors"
+)
+
+func (client *client) GenerateBundle() error {
+	bundleMetadata, sshRunner, err := loadVM(client)
+	if err != nil {
+		return err
+	}
+	defer sshRunner.Close()
+
+	ocConfig := oc.UseOCWithSSH(sshRunner)
+	if err := cluster.RemovePullSecretFromCluster(ocConfig, sshRunner); err != nil {
+		return errors.Wrap(err, "Error loading bundle metadata")
+	}
+
+	// Stop the cluster
+	currentState, err := client.Stop()
+	if err != nil {
+		return err
+	}
+	if currentState != state.Stopped {
+		return fmt.Errorf("VM is not stopped, current state is %s", currentState.String())
+	}
+
+	tmpBaseDir, err := ioutil.TempDir("", "crc_custom_bundle")
+	if err != nil {
+		return err
+	}
+	defer os.RemoveAll(tmpBaseDir)
+
+	// Create the custom bundle directory which is used as top level directory for tarball during compression
+	customBundleNameWithoutExtension := bundle.GetCustomBundleNameWithoutExtension(bundleMetadata.GetBundleName())
+
+	copier, err := bundle.NewCopier(bundleMetadata, tmpBaseDir, customBundleNameWithoutExtension)
+	if err != nil {
+		return err
+	}
+	defer copier.Cleanup() //nolint
+	customBundleDir := copier.CachedPath()
+
+	if err := copier.CopyKubeConfig(); err != nil {
+		return err
+	}
+
+	kubePassword, err := cluster.GetKubeadminPassword(bundleMetadata)
+	if err != nil {
+		return err
+	}
+	if err := copier.SetKubeAdminPassword(kubePassword); err != nil {
+		return err
+	}
+
+	if err := copier.CopyPrivateSSHKey(constants.GetPrivateKeyPath()); err != nil {
+		return err
+	}
+
+	if err := copier.CopyFilesFromFileList(); err != nil {
+		return err
+	}
+
+	// Copy disk image
+	logging.Infof("Copying the disk image to %s", customBundleNameWithoutExtension)
+	logging.Debugf("Absolute path of custom bundle directory: %s", customBundleDir)
+	diskPath, diskFormat, err := copyDiskImage(customBundleDir, bundleMetadata.GetDiskImagePath())
+	if err != nil {
+		return err
+	}
+
+	if err := copier.SetDiskImage(diskPath, diskFormat); err != nil {
+		return err
+	}
+
+	if err := copier.GenerateBundle(customBundleNameWithoutExtension); err != nil {
+		return err
+	}
+	cwd, err := os.Getwd()
+	if err != nil {
+		return err
+	}
+	logging.Infof("Bundle is generated in %s", filepath.Join(cwd, bundle.GetCustomBundle(customBundleNameWithoutExtension)))
+	logging.Infof("You need to perform 'crc delete' and 'crc start -b %s' to use this bundle", filepath.Join(cwd, bundle.GetCustomBundle(customBundleNameWithoutExtension)))
+	return nil
+}
+
+func loadVM(client *client) (*bundle.CrcBundleInfo, *crcssh.Runner, error) {
+	libMachineAPIClient, cleanup := createLibMachineClient()
+	defer cleanup()
+
+	host, err := libMachineAPIClient.Load(client.name)
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "Cannot load machine")
+	}
+
+	currentState, err := host.Driver.GetState()
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "Cannot get machine state")
+	}
+	if currentState != state.Running {
+		return nil, nil, errors.New("machine is not running")
+	}
+
+	crcBundleMetadata, err := getBundleMetadataFromDriver(host.Driver)
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "Error loading bundle metadata")
+	}
+
+	instanceIP, err := getIP(host, client.useVSock())
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "Error getting the IP")
+	}
+	sshRunner, err := crcssh.CreateRunner(instanceIP, getSSHPort(client.useVSock()), crcBundleMetadata.GetSSHKeyPath(), constants.GetPrivateKeyPath(), constants.GetRsaPrivateKeyPath())
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "Error creating the ssh client")
+	}
+
+	return crcBundleMetadata, sshRunner, nil
+}
diff --git a/pkg/crc/machine/generate_bundle_linux.go b/pkg/crc/machine/generate_bundle_linux.go
new file mode 100644
index 000000000..1a35bed19
--- /dev/null
+++ b/pkg/crc/machine/generate_bundle_linux.go
@@ -0,0 +1,50 @@
+package machine
+
+import (
+	"os"
+	"path/filepath"
+
+	"github.com/code-ready/crc/pkg/crc/constants"
+	crcos "github.com/code-ready/crc/pkg/os"
+)
+
+func copyDiskImage(destDir string, srcDir string) (string, string, error) {
+	tmpDir := filepath.Join(constants.MachineCacheDir, "image-rebase")
+	_ = os.RemoveAll(tmpDir) // clean up before using it
+	if err := os.Mkdir(tmpDir, 0700); err != nil {
+		return "", "", err
+	}
+	defer func() {
+		_ = os.RemoveAll(tmpDir) // clean up after using it
+	}()
+
+	// We copy the overlay file because before doing the commit, we need to adjust the backing file.
+	baseImageName := filepath.Base(srcDir)
+	srcOverlayDiskPath := filepath.Join(constants.MachineInstanceDir, constants.DefaultName, baseImageName)
+	destOverlayDiskPath := filepath.Join(tmpDir, "overlayImage.qcow2")
+	if err := crcos.CopyFileContents(srcOverlayDiskPath, destOverlayDiskPath, 0644); err != nil {
+		return "", "", err
+	}
+
+	// We copy the base image because this is where we are going to commit the changes which were made to the VM
+	if err := crcos.CopyFileContents(srcDir,
+		filepath.Join(tmpDir, baseImageName), 0644); err != nil {
+		return "", "", err
+	}
+
+	// Use qemu-img commands to rebase and commit it.
+	format := "qcow2"
+	if _, _, err := crcos.RunWithDefaultLocale("qemu-img", "rebase", "-F", format, "-b",
+		filepath.Join(tmpDir, baseImageName), destOverlayDiskPath); err != nil {
+		return "", "", err
+	}
+	if _, _, err := crcos.RunWithDefaultLocale("qemu-img", "commit", destOverlayDiskPath); err != nil {
+		return "", "", err
+	}
+
+	// Copy the final image to destination dir
+	if err := crcos.CopyFileContents(filepath.Join(tmpDir, baseImageName), filepath.Join(destDir, baseImageName), 0644); err != nil {
+		return "", "", err
+	}
+	return filepath.Join(destDir, baseImageName), format, nil
+}
diff --git a/pkg/crc/machine/generate_bundle_nonlinux.go b/pkg/crc/machine/generate_bundle_nonlinux.go
new file mode 100644
index 000000000..f723a309a
--- /dev/null
+++ b/pkg/crc/machine/generate_bundle_nonlinux.go
@@ -0,0 +1,12 @@
+// +build !linux
+
+package machine
+
+import (
+	"fmt"
+	"runtime"
+)
+
+func copyDiskImage(dirName string, srcDir string) (string, string, error) {
+	return "", "", fmt.Errorf("Not implemented for %s", runtime.GOOS)
+}
diff --git a/pkg/crc/machine/start.go b/pkg/crc/machine/start.go
index 54004ef97..6b1e4df44 100644
--- a/pkg/crc/machine/start.go
+++ b/pkg/crc/machine/start.go
@@ -186,9 +186,9 @@ func (client *client) Start(ctx context.Context, startConfig types.StartConfig)
 
 		// Retrieve metadata info
 		machineConfig.ImageSourcePath = crcBundleMetadata.GetDiskImagePath()
-		machineConfig.ImageFormat = crcBundleMetadata.Storage.DiskImages[0].Format
+		machineConfig.ImageFormat = crcBundleMetadata.GetDiskImageFormat()
 		machineConfig.SSHKeyPath = crcBundleMetadata.GetSSHKeyPath()
-		machineConfig.KernelCmdLine = crcBundleMetadata.Nodes[0].KernelCmdLine
+		machineConfig.KernelCmdLine = crcBundleMetadata.GetKernelCommandLine()
 		machineConfig.Initramfs = crcBundleMetadata.GetInitramfsPath()
 		machineConfig.Kernel = crcBundleMetadata.GetKernelPath()
 
@@ -661,7 +661,7 @@ func ensureRoutesControllerIsRunning(sshRunner *crcssh.Runner, ocConfig oc.Confi
 			Containers: []v1.Container{
 				{
 					Name:            "routes-controller",
-					Image:           "quay.io/crcont/routes-controller:6effc2f0304d3a11c1e22c278b10630e834d0220",
+					Image:           "quay.io/crcont/routes-controller:latest",
 					ImagePullPolicy: v1.PullIfNotPresent,
 				},
 			},
diff --git a/pkg/crc/machine/sync.go b/pkg/crc/machine/sync.go
index 4fe952967..874e7b8b6 100644
--- a/pkg/crc/machine/sync.go
+++ b/pkg/crc/machine/sync.go
@@ -169,3 +169,7 @@ func (s *Synchronized) Status() (*types.ClusterStatusResult, error) {
 func (s *Synchronized) IsRunning() (bool, error) {
 	return s.underlying.IsRunning()
 }
+
+func (s *Synchronized) GenerateBundle() error {
+	return s.underlying.GenerateBundle()
+}
diff --git a/pkg/crc/preflight/preflight.go b/pkg/crc/preflight/preflight.go
index c94dd265c..a2f93eac6 100644
--- a/pkg/crc/preflight/preflight.go
+++ b/pkg/crc/preflight/preflight.go
@@ -101,7 +101,7 @@ func doPreflightChecks(config config.Storage, checks []Check) error {
 	return nil
 }
 
-func doFixPreflightChecks(config config.Storage, checks []Check) error {
+func doFixPreflightChecks(config config.Storage, checks []Check, checkOnly bool) error {
 	for _, check := range checks {
 		if check.flags&CleanUpOnly == CleanUpOnly {
 			continue
@@ -109,6 +109,8 @@ func doFixPreflightChecks(config config.Storage, checks []Check) error {
 		err := check.doCheck(config)
 		if err == nil {
 			continue
+		} else if checkOnly {
+			return err
 		}
 		if err = check.doFix(); err != nil {
 			return err
@@ -160,11 +162,11 @@ func StartPreflightChecks(config config.Storage) error {
 }
 
 // SetupHost performs the prerequisite checks and setups the host to run the cluster
-func SetupHost(config config.Storage) error {
+func SetupHost(config config.Storage, checkOnly bool) error {
 	experimentalFeatures := config.Get(cmdConfig.ExperimentalFeatures).AsBool()
 	mode := network.ParseMode(config.Get(cmdConfig.NetworkMode).AsString())
 	trayAutostart := config.Get(cmdConfig.AutostartTray).AsBool()
-	return doFixPreflightChecks(config, getPreflightChecks(experimentalFeatures, trayAutostart, mode))
+	return doFixPreflightChecks(config, getPreflightChecks(experimentalFeatures, trayAutostart, mode), checkOnly)
 }
 
 func RegisterSettings(config config.Schema) {
diff --git a/pkg/crc/preflight/preflight_checks_tray_windows.go b/pkg/crc/preflight/preflight_checks_tray_windows.go
index 9f086e10b..7f07a520b 100644
--- a/pkg/crc/preflight/preflight_checks_tray_windows.go
+++ b/pkg/crc/preflight/preflight_checks_tray_windows.go
@@ -102,10 +102,20 @@ func fixTrayInstalled() error {
 func removeTray() error {
 	trayShortcutPath := filepath.Join(constants.StartupFolder, constants.TrayShortcutName)
 	_ = stopTray()
+	/* we changed the name of the tray executable to crc-tray.exe from tray-windows.exe
+	 * this tries to remove the old tray shortcut, can be removed after a  few releases
+	 */
+	_ = os.RemoveFileIfExists(filepath.Join(constants.StartupFolder, "tray-windows.lnk"))
+
 	return os.RemoveFileIfExists(trayShortcutPath)
 }
 
 func stopTray() error {
+	/* we changed the name of the tray executable to crc-tray from tray-windows
+	 * this tries to stop the old tray process, can be removed after a  few releases
+	 */
+	_, _, _ = powershell.Execute(`Stop-Process -Name tray-windows`)
+
 	trayProcessName := strings.TrimSuffix(constants.TrayExecutableName, ".exe")
 	cmd := fmt.Sprintf(`Stop-Process -Name "%s"`, trayProcessName)
 	if _, _, err := powershell.Execute(cmd); err != nil {
@@ -162,6 +172,10 @@ func checkTrayExecutableExists() error {
 }
 
 func fixTrayExecutableExists() error {
+	/* we changed the name of the tray executable to crc-tray.exe from tray-windows.exe
+	 * this tries to remove the old tray folder, can be removed after a  few releases
+	 */
+	_ = goos.RemoveAll(filepath.Join(constants.CrcBinDir, "tray-windows"))
 	return downloadOrExtractTrayApp(constants.GetCRCWindowsTrayDownloadURL(), constants.TrayExecutableDir)
 }
 
diff --git a/pkg/crc/validation/validation.go b/pkg/crc/validation/validation.go
index 8a3bb78d1..12e6f3027 100644
--- a/pkg/crc/validation/validation.go
+++ b/pkg/crc/validation/validation.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
+	"strings"
 
 	"github.com/code-ready/crc/pkg/crc/constants"
 	"github.com/code-ready/crc/pkg/crc/logging"
@@ -58,16 +59,24 @@ func ValidateEnoughMemory(value int) error {
 }
 
 // ValidateBundlePath checks if the provided bundle path exist
-func ValidateBundlePath(bundle string) error {
-	if err := ValidatePath(bundle); err != nil {
+func ValidateBundlePath(bundlePath string) error {
+	if err := ValidatePath(bundlePath); err != nil {
 		if constants.BundleEmbedded() {
 			return fmt.Errorf("Run 'crc setup' to unpack the bundle to disk")
 		}
-		return fmt.Errorf("%s not found, please provide the path to a valid bundle using the -b option", bundle)
+		return fmt.Errorf("%s not found, please provide the path to a valid bundle using the -b option", bundlePath)
 	}
 
-	userProvidedBundle := filepath.Base(bundle)
+	userProvidedBundle := filepath.Base(bundlePath)
 	if userProvidedBundle != constants.GetDefaultBundle() {
+		// Should append underscore (_) here, as we don't want crc_libvirt_4.7.15.crcbundle
+		// to be detected as a custom bundle for crc_libvirt_4.7.1.crcbundle
+		usingCustomBundle := strings.HasPrefix(bundle.GetBundleNameWithoutExtension(userProvidedBundle),
+			fmt.Sprintf("%s_", bundle.GetBundleNameWithoutExtension(constants.GetDefaultBundle())))
+		if usingCustomBundle {
+			logging.Warnf("Using custom bundle %s", userProvidedBundle)
+			return nil
+		}
 		if !constants.IsRelease() {
 			logging.Warnf("Using unsupported bundle %s", userProvidedBundle)
 			return nil
diff --git a/pkg/crc/version/version.go b/pkg/crc/version/version.go
index 439b8af7f..7d791c283 100644
--- a/pkg/crc/version/version.go
+++ b/pkg/crc/version/version.go
@@ -31,7 +31,7 @@ const (
 	// Tray version to be embedded in executable
 	crcMacTrayVersion = "1.0.8"
 	// Windows forms application version type major.minor.buildnumber.revesion
-	crcWindowsTrayVersion = "0.4.0.0"
+	crcWindowsTrayVersion = "0.5.0.0"
 )
 
 type CrcReleaseInfo struct {


Test patch:
diff --git a/pkg/compress/compress_test.go b/pkg/compress/compress_test.go
new file mode 100644
index 000000000..1d1314490
--- /dev/null
+++ b/pkg/compress/compress_test.go
@@ -0,0 +1,144 @@
+package compress
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"runtime"
+	"testing"
+
+	"github.com/code-ready/crc/pkg/crc/logging"
+	"github.com/code-ready/crc/pkg/extract"
+
+	"github.com/stretchr/testify/require"
+)
+
+const testArchiveName = "testdata.tar.zst"
+
+var (
+	files fileMap = map[string]string{
+		filepath.Join("a"):      "a",
+		filepath.Join("c"):      "c",
+		filepath.Join("b", "d"): "d",
+	}
+)
+
+func testCompress(t *testing.T, baseDir string) {
+	// This is useful to check that the top-level directory of the archive
+	// was created with the correct permissions, and was not created by the
+	// `os.MkdirAll(0750)` call at the beginning of `untarFile`
+	require.NoError(t, os.Chmod(baseDir, 0700))
+	require.NoError(t, Compress(baseDir, testArchiveName))
+	defer os.Remove(testArchiveName)
+
+	destDir, err := ioutil.TempDir("", "testdata-extracted")
+	require.NoError(t, err)
+	defer os.RemoveAll(destDir)
+
+	fileList, err := extract.Uncompress(testArchiveName, destDir, false)
+	require.NoError(t, err)
+
+	_, d := filepath.Split(baseDir)
+	fi, err := os.Stat(filepath.Join(destDir, d))
+	require.NoError(t, err)
+	fMode := os.FileMode(0700)
+	if runtime.GOOS == "windows" {
+		// https://golang.org/pkg/os/#Chmod
+		fMode = os.FileMode(0777)
+	}
+	require.Equal(t, fi.Mode().Perm(), fMode)
+
+	require.NoError(t, checkFileList(filepath.Join(destDir, d), fileList, files))
+	require.NoError(t, checkFiles(filepath.Join(destDir, d), files))
+}
+
+func TestCompressRelative(t *testing.T) {
+	// Test with relative path
+	testCompress(t, "testdata")
+
+	// Test with absolute path
+	currentDir, err := os.Getwd()
+	require.NoError(t, err)
+	testCompress(t, filepath.Join(currentDir, "testdata"))
+}
+
+/* The code below is duplicated from pkg/extract/extract_test.go */
+type fileMap map[string]string
+
+func copyFileMap(orig fileMap) fileMap {
+	copy := fileMap{}
+	for key, value := range orig {
+		copy[key] = value
+	}
+	return copy
+}
+
+// This checks that the list of files returned by Uncompress matches what we expect
+func checkFileList(destDir string, extractedFiles []string, expectedFiles fileMap) error {
+	// We are going to remove elements from  the map, but we don't want to modify the map used by the caller
+	expectedFiles = copyFileMap(expectedFiles)
+
+	for _, file := range extractedFiles {
+		rel, err := filepath.Rel(destDir, file)
+		if err != nil {
+			return err
+		}
+		_, found := expectedFiles[rel]
+		if !found {
+			return fmt.Errorf("Unexpected file '%s' in file list %v", rel, expectedFiles)
+		}
+		delete(expectedFiles, rel)
+	}
+
+	if len(expectedFiles) != 0 {
+		return fmt.Errorf("Some expected files were not in file list: %v", expectedFiles)
+	}
+
+	return nil
+}
+
+// This checks that the files in the destination directory matches what we expect
+func checkFiles(destDir string, files fileMap) error {
+	// We are going to remove elements from  the map, but we don't want to modify the map used by the caller
+	files = copyFileMap(files)
+
+	err := filepath.Walk(destDir, func(path string, info os.FileInfo, err error) error {
+		logging.Debugf("Walking %s", path)
+		if err != nil {
+			return err
+		}
+		if info.IsDir() {
+			logging.Debugf("Skipping directory %s", path)
+			return nil
+		}
+		logging.Debugf("Checking file %s", path)
+		archivePath, err := filepath.Rel(destDir, path)
+		if err != nil {
+			return err
+		}
+		expectedContent, found := files[archivePath]
+		if !found {
+			return fmt.Errorf("Unexpected extracted file '%s'", path)
+		}
+		delete(files, archivePath)
+
+		data, err := ioutil.ReadFile(path) // #nosec G304
+		if err != nil {
+			return err
+		}
+		if string(data) != expectedContent {
+			return fmt.Errorf("Unexpected content for '%s': expected [%s], got [%s]", path, expectedContent, string(data))
+		}
+		logging.Debugf("'%s' successfully checked", path)
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+	if len(files) != 0 {
+		return fmt.Errorf("Some expected files were not extracted: %v", files)
+	}
+
+	return nil
+}
diff --git a/pkg/compress/testdata/a b/pkg/compress/testdata/a
new file mode 100644
index 000000000..2e65efe2a
--- /dev/null
+++ b/pkg/compress/testdata/a
@@ -0,0 +1,1 @@
+a
\ No newline at end of file
diff --git a/pkg/compress/testdata/b/d b/pkg/compress/testdata/b/d
new file mode 100644
index 000000000..c59d9b634
--- /dev/null
+++ b/pkg/compress/testdata/b/d
@@ -0,0 +1,1 @@
+d
\ No newline at end of file
diff --git a/pkg/compress/testdata/c b/pkg/compress/testdata/c
new file mode 100644
index 000000000..3410062ba
--- /dev/null
+++ b/pkg/compress/testdata/c
@@ -0,0 +1,1 @@
+c
\ No newline at end of file
diff --git a/pkg/crc/machine/bundle/copier_test.go b/pkg/crc/machine/bundle/copier_test.go
new file mode 100644
index 000000000..c21a3531e
--- /dev/null
+++ b/pkg/crc/machine/bundle/copier_test.go
@@ -0,0 +1,72 @@
+package bundle
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/code-ready/crc/pkg/crc/constants"
+	crcos "github.com/code-ready/crc/pkg/os"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGenerateBundle(t *testing.T) {
+	var b CrcBundleInfo
+	assert.NoError(t, json.Unmarshal([]byte(reference), &b))
+	b.bundleName = "crc_4.7.1"
+	b.Storage.Files[0].Name = constants.OcExecutableName
+
+	tmpBundleDir, err := ioutil.TempDir("", "bundle_data")
+	assert.NoError(t, err)
+	b.cachedPath = filepath.Join(tmpBundleDir, b.bundleName)
+	defer os.RemoveAll(tmpBundleDir)
+
+	createDummyBundleFiles(t, &b)
+
+	srcDir, err := ioutil.TempDir("", "testdata")
+	assert.NoError(t, err)
+	defer os.RemoveAll(srcDir)
+
+	customBundleName := "custom_bundle"
+	copier, err := NewCopier(&b, srcDir, customBundleName)
+	assert.NoError(t, err)
+
+	assert.NoError(t, copier.CopyKubeConfig())
+
+	assert.NoError(t, copier.SetKubeAdminPassword("****-****-****"))
+	assert.NoError(t, copier.CopyPrivateSSHKey(copier.srcBundle.GetSSHKeyPath()))
+	assert.NoError(t, copier.CopyFilesFromFileList())
+
+	err = crcos.CopyFileContents(copier.srcBundle.GetDiskImagePath(), copier.copiedBundle.GetDiskImagePath(), 0644)
+	assert.NoError(t, err)
+	assert.NoError(t, copier.SetDiskImage(copier.copiedBundle.GetDiskImagePath(), "qcow2"))
+
+	assert.NoError(t, copier.GenerateBundle(customBundleName))
+	defer os.Remove(fmt.Sprintf("%s%s", customBundleName, bundleExtension))
+}
+
+func createDummyBundleFiles(t *testing.T, bundle *CrcBundleInfo) {
+	assert.NoError(t, os.MkdirAll(bundle.cachedPath, 0750))
+
+	files := []string{
+		bundle.GetOcPath(),
+		bundle.resolvePath(bundle.ClusterInfo.KubeadminPasswordFile),
+		bundle.GetKubeConfigPath(),
+		bundle.GetSSHKeyPath(),
+		bundle.GetDiskImagePath(),
+		bundle.GetKernelPath(),
+		bundle.GetInitramfsPath(),
+	}
+
+	for _, file := range files {
+		if file == "" {
+			continue
+		}
+		fd, err := os.Create(file)
+		fd.Close()
+		assert.NoError(t, err)
+	}
+}
diff --git a/pkg/crc/machine/sync_test.go b/pkg/crc/machine/sync_test.go
index e29e0e9e8..461afeadd 100644
--- a/pkg/crc/machine/sync_test.go
+++ b/pkg/crc/machine/sync_test.go
@@ -165,3 +165,7 @@ func (m *waitingMachine) Stop() (state.State, error) {
 	<-m.stopCompleteCh
 	return state.Stopped, nil
 }
+
+func (m *waitingMachine) GenerateBundle() error {
+	return errors.New("not implemented")
+}
diff --git a/pkg/crc/preflight/preflight_test.go b/pkg/crc/preflight/preflight_test.go
index c624566ca..74aa312ef 100644
--- a/pkg/crc/preflight/preflight_test.go
+++ b/pkg/crc/preflight/preflight_test.go
@@ -34,11 +34,21 @@ func TestFixPreflight(t *testing.T) {
 	cfg := config.New(config.NewEmptyInMemoryStorage())
 	doRegisterSettings(cfg, []Check{*check})
 
-	assert.NoError(t, doFixPreflightChecks(cfg, []Check{*check}))
+	assert.NoError(t, doFixPreflightChecks(cfg, []Check{*check}, false))
 	assert.True(t, calls.checked)
 	assert.True(t, calls.fixed)
 }
 
+func TestFixPreflightCheckOnly(t *testing.T) {
+	check, calls := sampleCheck(errors.New("check failed"), nil)
+	cfg := config.New(config.NewEmptyInMemoryStorage())
+	doRegisterSettings(cfg, []Check{*check})
+
+	assert.Error(t, doFixPreflightChecks(cfg, []Check{*check}, true))
+	assert.True(t, calls.checked)
+	assert.False(t, calls.fixed)
+}
+
 func sampleCheck(checkErr, fixErr error) (*Check, *status) {
 	status := &status{}
 	return &Check{
diff --git a/test/e2e/crcsuite/crcsuite.go b/test/e2e/crcsuite/crcsuite.go
index 7214a346b..23ea89b4f 100644
--- a/test/e2e/crcsuite/crcsuite.go
+++ b/test/e2e/crcsuite/crcsuite.go
@@ -2,6 +2,7 @@ package crcsuite
 
 import (
 	"crypto/tls"
+	"flag"
 	"fmt"
 	"net/http"
 	"os"
@@ -16,6 +17,9 @@ import (
 	"github.com/cucumber/messages-go/v10"
 
 	clicumber "github.com/code-ready/clicumber/testsuite"
+	"github.com/code-ready/crc/test/e2e/crcsuite/ux"
+	"github.com/code-ready/crc/test/extended/crc/cmd"
+	"github.com/code-ready/crc/test/extended/util"
 )
 
 var (
@@ -36,6 +40,8 @@ func FeatureContext(s *godog.Suite) {
 		RemoveCRCHome)
 	s.Step(`^starting CRC with default bundle (succeeds|fails)$`,
 		StartCRCWithDefaultBundleSucceedsOrFails)
+	s.Step(`^starting CRC with custom bundle (succeeds|fails)$`,
+		StartCRCWithCustomBundleSucceedsOrFails)
 	s.Step(`^starting CRC with default bundle along with stopped network time synchronization (succeeds|fails)$`,
 		StartCRCWithDefaultBundleWithStopNetworkTimeSynchronizationSucceedsOrFails)
 	s.Step(`^starting CRC with default bundle and nameserver "(.*)" (succeeds|fails)$`,
@@ -43,7 +49,7 @@ func FeatureContext(s *godog.Suite) {
 	s.Step(`^setting config property "(.*)" to value "(.*)" (succeeds|fails)$`,
 		SetConfigPropertyToValueSucceedsOrFails)
 	s.Step(`^unsetting config property "(.*)" (succeeds|fails)$`,
-		UnsetConfigPropertySucceedsOrFails)
+		cmd.UnsetConfigPropertySucceedsOrFails)
 	s.Step(`^login to the oc cluster (succeeds|fails)$`,
 		LoginToOcClusterSucceedsOrFails)
 	s.Step(`^setting kubeconfig context to "(.*)" (succeeds|fails)$`,
@@ -106,7 +112,7 @@ func FeatureContext(s *godog.Suite) {
 		}
 
 		// remove $HOME/.crc
-		err = RemoveCRCHome()
+		err = util.RemoveCRCHome(CRCHome)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
@@ -120,7 +126,7 @@ func FeatureContext(s *godog.Suite) {
 				}
 				// Obtain the bundle to current dir
 				fmt.Println("Obtaining bundle...")
-				bundleLocation, err = DownloadBundle(bundleLocation, ".")
+				bundleLocation, err = util.DownloadBundle(bundleLocation, ".", bundleName)
 				if err != nil {
 					fmt.Printf("Failed to obtain CRC bundle, %v\n", err)
 					os.Exit(1)
@@ -136,7 +142,7 @@ func FeatureContext(s *godog.Suite) {
 		// copy data/config files to test dir
 		for _, tag := range pickle.GetTags() {
 			if tag.Name == "@testdata" {
-				err := CopyFilesToTestDir()
+				err := util.CopyFilesToTestDir()
 				if err != nil {
 					os.Exit(1)
 				}
@@ -146,18 +152,39 @@ func FeatureContext(s *godog.Suite) {
 
 	s.AfterScenario(func(pickle *messages.Pickle, err error) {
 		if err != nil {
-			if err := runDiagnose(filepath.Join("..", "test-results")); err != nil {
+			if err := util.RunDiagnose(filepath.Join("..", "test-results")); err != nil {
 				fmt.Printf("Failed to collect diagnostic: %v\n", err)
 			}
 		}
 	})
 
 	s.AfterSuite(func() {
-		err := DeleteCRC()
+		err := cmd.DeleteCRC()
 		if err != nil {
 			fmt.Printf("Could not delete CRC VM: %s.", err)
 		}
 	})
+
+	// Extend the context with tray when supported
+	ux.FeatureContext(s, &bundleLocation, &pullSecretFile)
+}
+
+func ParseFlags() {
+	flag.StringVar(&bundleLocation, "bundle-location", "", "Path to the bundle to be used in tests")
+	flag.StringVar(&pullSecretFile, "pull-secret-file", "", "Path to the file containing pull secret")
+	flag.StringVar(&CRCExecutable, "crc-binary", "", "Path to the CRC executable to be tested")
+	flag.StringVar(&bundleVersion, "bundle-version", "", "Version of the bundle used in tests")
+
+	// Extend the context with tray when supported
+	ux.ParseFlags()
+}
+
+func WaitForClusterInState(state string) error {
+	return cmd.WaitForClusterInState(state)
+}
+
+func RemoveCRCHome() error {
+	return util.RemoveCRCHome(CRCHome)
 }
 
 func CheckHTTPResponseWithRetry(retryCount int, retryWait string, address string, expectedStatusCode int) error {
@@ -185,7 +212,7 @@ func CheckHTTPResponseWithRetry(retryCount int, retryWait string, address string
 		time.Sleep(retryDuration)
 	}
 
-	return fmt.Errorf("Got %d as Status Code instead of expected %d", resp.StatusCode, expectedStatusCode)
+	return fmt.Errorf("got %d as Status Code instead of expected %d", resp.StatusCode, expectedStatusCode)
 }
 
 func CheckOutputMatchWithRetry(retryCount int, retryTime string, command string, expected string, expectedOutput string) error {
@@ -218,16 +245,7 @@ func CheckOutputMatchWithRetry(retryCount int, retryTime string, command string,
 // CheckCRCStatus checks that output of status command
 // matches given regex
 func CheckCRCStatus(state string) error {
-	expression := `.*OpenShift: .*Running \(v\d+\.\d+\.\d+.*\).*`
-	if state == "stopped" {
-		expression = ".*OpenShift: .*Stopped.*"
-	}
-
-	err := clicumber.ExecuteCommand("crc status --log-level debug")
-	if err != nil {
-		return err
-	}
-	return clicumber.CommandReturnShouldMatch("stdout", expression)
+	return cmd.CheckCRCStatus(state)
 }
 
 func DeleteFileFromCRCHome(fileName string) error {
@@ -239,7 +257,7 @@ func DeleteFileFromCRCHome(fileName string) error {
 	}
 
 	if err := clicumber.DeleteFile(theFile); err != nil {
-		return fmt.Errorf("Error deleting file %v", theFile)
+		return fmt.Errorf("error deleting file %v", theFile)
 	}
 	return nil
 }
@@ -278,9 +296,9 @@ func ConfigFileInCRCHomeContainsKeyMatchingValue(format string, configFile strin
 		return err
 	}
 	if (condition == "contains") && !matches {
-		return fmt.Errorf("For key '%s' config contains unexpected value '%s'", keyPath, keyValue)
+		return fmt.Errorf("for key '%s' config contains unexpected value '%s'", keyPath, keyValue)
 	} else if (condition == "does not contain") && matches {
-		return fmt.Errorf("For key '%s' config contains value '%s', which it should not contain", keyPath, keyValue)
+		return fmt.Errorf("for key '%s' config contains value '%s', which it should not contain", keyPath, keyValue)
 	}
 	return nil
 }
@@ -300,9 +318,9 @@ func ConfigFileInCRCHomeContainsKey(format string, configFile string, condition
 	}
 
 	if (condition == "contains") && (keyValue == "<nil>") {
-		return fmt.Errorf("Config does not contain any value for key %s", keyPath)
+		return fmt.Errorf("config does not contain any value for key %s", keyPath)
 	} else if (condition == "does not contain") && (keyValue != "<nil>") {
-		return fmt.Errorf("Config contains key %s with assigned value: %s", keyPath, keyValue)
+		return fmt.Errorf("config contains key %s with assigned value: %s", keyPath, keyValue)
 	}
 
 	return nil
@@ -346,6 +364,11 @@ func StartCRCWithDefaultBundleWithStopNetworkTimeSynchronizationSucceedsOrFails(
 	return err
 }
 
+func StartCRCWithCustomBundleSucceedsOrFails(expected string) error {
+	cmd := fmt.Sprintf("crc start -p '%s' -b *.crcbundle --log-level debug", pullSecretFile)
+	return clicumber.ExecuteCommandSucceedsOrFails(cmd, expected)
+}
+
 func StartCRCWithDefaultBundleAndNameServerSucceedsOrFails(nameserver string, expected string) error {
 
 	var extraBundleArgs string
@@ -372,26 +395,12 @@ func CommandReturnShouldContainIfBundleEmbeddedOrNot(commandField string, value
 }
 
 func SetConfigPropertyToValueSucceedsOrFails(property string, value string, expected string) error {
-
 	if value == "current bundle" {
-
 		if bundleEmbedded {
 			value = filepath.Join(CRCHome, "cache", bundleName)
 		} else {
 			value = bundleLocation
 		}
 	}
-
-	cmd := "crc config set " + property + " " + value
-	err := clicumber.ExecuteCommandSucceedsOrFails(cmd, expected)
-
-	return err
-}
-
-func UnsetConfigPropertySucceedsOrFails(property string, expected string) error {
-
-	cmd := "crc config unset " + property
-	err := clicumber.ExecuteCommandSucceedsOrFails(cmd, expected)
-
-	return err
+	return cmd.SetConfigPropertyToValueSucceedsOrFails(property, value, expected)
 }
diff --git a/test/e2e/crcsuite/util.go b/test/e2e/crcsuite/util.go
deleted file mode 100644
index 986df8903..000000000
--- a/test/e2e/crcsuite/util.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package crcsuite
-
-import (
-	"fmt"
-	"os"
-	"path/filepath"
-
-	clicumber "github.com/code-ready/clicumber/testsuite"
-)
-
-// Delete CRC instance
-func DeleteCRC() error {
-
-	command := "crc delete"
-	_ = clicumber.ExecuteCommand(command)
-
-	fmt.Printf("Deleted CRC instance (if one existed).\n")
-	return nil
-}
-
-// Remove CRC home folder ~/.crc
-func RemoveCRCHome() error {
-
-	keepFile := filepath.Join(CRCHome, ".keep")
-
-	_, err := os.Stat(keepFile)
-	if err != nil { // cannot get keepFile's status
-		err = os.RemoveAll(CRCHome)
-
-		if err != nil {
-			fmt.Printf("Problem deleting CRC home folder %s.\n", CRCHome)
-			return err
-		}
-
-		fmt.Printf("Deleted CRC home folder %s.\n", CRCHome)
-		return nil
-
-	}
-	// keepFile exists
-	return fmt.Errorf("Folder %s not removed as per request: %s present", CRCHome, keepFile)
-}
diff --git a/test/e2e/crcsuite/ux/uxsuite.go b/test/e2e/crcsuite/ux/uxsuite.go
new file mode 100644
index 000000000..755494d61
--- /dev/null
+++ b/test/e2e/crcsuite/ux/uxsuite.go
@@ -0,0 +1,225 @@
+package ux
+
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	"github.com/code-ready/crc/test/extended/crc/cmd"
+	"github.com/code-ready/crc/test/extended/crc/ux/installer"
+	"github.com/code-ready/crc/test/extended/crc/ux/notification"
+	"github.com/code-ready/crc/test/extended/crc/ux/tray"
+	"github.com/code-ready/crc/test/extended/util"
+	"github.com/cucumber/godog"
+	"github.com/cucumber/messages-go/v10"
+)
+
+var trayHandler tray.Tray
+var installerHandler installer.Installer
+var notificationHandler notification.Notification
+var currentUserPassword string
+var installerPath string
+
+// FeatureContext defines godog.Suite steps for the test suite.
+func FeatureContext(s *godog.Suite, bundleLocation *string, pullSecretFile *string) {
+	trayHandler = tray.NewTray(bundleLocation, pullSecretFile)
+	installerHandler = installer.NewInstaller(&currentUserPassword, &installerPath)
+	notificationHandler = notification.NewNotification()
+	if handlersAreInitialized() {
+		s.Step(`^install CRC tray$`,
+			trayHandler.Install)
+		s.Step(`^tray should be installed$`,
+			trayHandler.IsInstalled)
+		s.Step(`^tray icon should be accessible$`,
+			trayHandler.IsAccessible)
+		s.Step(`^fresh tray installation$`,
+			guaranteeFreshInstallation)
+		s.Step(`^(.*) the cluster from the tray$`,
+			clickTrayButton)
+		s.Step(`^cluster should be (.*)$`,
+			waitForClusterInState)
+		s.Step(`^set the pull secret file$`,
+			trayHandler.SetPullSecretFileLocation)
+		s.Step(`^tray should show cluster as (.*)$`,
+			checkClusterStateOnTray)
+		s.Step(`^(.*) CRC from installer$`,
+			executeActionFromInstaller)
+		s.Step(`^.*CRC (.*) installed$`,
+			crcInstalled)
+		s.Step(`^user should get notified with cluster state as (.*)$`,
+			getNotification)
+		s.Step(`^a (.*) cluster$`,
+			guaranteeClusterState)
+		s.Step(`^using copied oc login command for (.*)$`,
+			copyOCLoginCommandByUser)
+		s.Step(`^user is connected to the cluster as (.*)$`,
+			connectClusterAsUser)
+		s.BeforeScenario(func(*messages.Pickle) {
+			copyRequiredResources(tray.RequiredResourcesPath)
+			copyRequiredResources(installer.RequiredResourcesPath)
+			copyRequiredResources(notification.RequiredResourcesPath)
+		})
+	}
+}
+
+func ParseFlags() {
+	flag.StringVar(&currentUserPassword, "user-password", "",
+		"Current user password. User should be sudoer to handle the installation")
+	flag.StringVar(&installerPath, "installer-path", "",
+		"Full path for installer")
+}
+
+func handlersAreInitialized() bool {
+	return trayHandler != nil &&
+		installerHandler != nil &&
+		notificationHandler != nil
+}
+
+func copyRequiredResources(requiredResources func() (string, error)) {
+	requiredResourcesPath, err := requiredResources()
+	if err != nil {
+		os.Exit(1)
+	}
+	if requiredResourcesPath != "" {
+		err = util.CopyResourcesFromPath(requiredResourcesPath)
+		if err != nil {
+			os.Exit(1)
+		}
+	}
+}
+
+func guaranteeFreshInstallation() error {
+	err := trayHandler.IsInstalled()
+	if err != nil {
+		return err
+	}
+	err = trayHandler.IsAccessible()
+	if err != nil {
+		return err
+	}
+	err = trayHandler.IsClusterStopped()
+	if err != nil {
+		return err
+	}
+	err = cmd.CheckMachineNotExists()
+	if err != nil {
+		return err
+	}
+	return notificationHandler.ClearNotifications()
+}
+
+func guaranteeClusterState(state string) error {
+	switch state {
+	case "running", "stopped":
+		err := cmd.CheckCRCStatus(state)
+		if err != nil {
+			return err
+		}
+	default:
+		return fmt.Errorf("%s state is not defined as valid cluster state", state)
+	}
+	return notificationHandler.ClearNotifications()
+}
+
+func clickTrayButton(action string) error {
+	if trayHandler == nil {
+		return fmt.Errorf("tray handler should be initialized. Check if tray is supported on your OS")
+	}
+	switch action {
+	case "start":
+		return trayHandler.ClickStart()
+	case "stop":
+		return trayHandler.ClickStop()
+	case "delete":
+		return trayHandler.ClickDelete()
+	case "quit":
+		return trayHandler.ClickQuit()
+	default:
+		return fmt.Errorf("%s action can not be managed through the tray", action)
+	}
+}
+
+func checkClusterStateOnTray(state string) error {
+	if trayHandler == nil {
+		return fmt.Errorf("tray handler should be initialized. Check if tray is supported on your OS")
+	}
+	switch state {
+	case "running":
+		return trayHandler.IsClusterRunning()
+	case "stopped":
+		return trayHandler.IsClusterStopped()
+	default:
+		return fmt.Errorf("%s state is not defined as valid cluster state", state)
+	}
+}
+
+func executeActionFromInstaller(action string) error {
+	if installerHandler == nil {
+		return fmt.Errorf("installer handler should be initialized. Check if installer is supported on your OS")
+	}
+	switch action {
+	case "install":
+		return installerHandler.Install()
+	default:
+		return fmt.Errorf("%s action is not supported by the installer", action)
+	}
+}
+
+func waitForClusterInState(state string) error {
+	return cmd.WaitForClusterInState(state)
+}
+
+func crcInstalled(state string) error {
+	switch state {
+	case "is":
+		return cmd.CheckCRCExecutableState(cmd.CRCExecutableInstalled)
+	case "is not":
+		return cmd.CheckCRCExecutableState(cmd.CRCExecutableNotInstalled)
+	default:
+		return fmt.Errorf("%s state is not defined as valid CRC installation state", state)
+	}
+}
+
+func getNotification(action string) error {
+	if notificationHandler == nil {
+		return fmt.Errorf("notification handler should be initialized. Check if tray is supported on your OS")
+	}
+	switch action {
+	case "running":
+		return notificationHandler.GetClusterRunning()
+	case "stopped":
+		return notificationHandler.GetClusterStopped()
+	case "deleted":
+		return notificationHandler.GetClusterDeleted()
+	default:
+		return fmt.Errorf("%s action will not be notified", action)
+	}
+}
+
+func copyOCLoginCommandByUser(user string) error {
+	if trayHandler == nil {
+		return fmt.Errorf("tray handler should be initialized. Check if tray is supported on your OS")
+	}
+	switch user {
+	case "kubeadmin":
+		return trayHandler.CopyOCLoginCommandAsKubeadmin()
+	case "developer":
+		return trayHandler.CopyOCLoginCommandAsDeveloper()
+	default:
+		return fmt.Errorf("tray can not provide login command for user %s", user)
+	}
+}
+
+func connectClusterAsUser(user string) error {
+	if trayHandler == nil {
+		return fmt.Errorf("tray handler should be initialized. Check if tray is supported on your OS")
+	}
+	switch user {
+	case "kubeadmin":
+		return trayHandler.ConnectClusterAsKubeadmin()
+	case "developer":
+		return trayHandler.ConnectClusterAsDeveloper()
+	default:
+		return fmt.Errorf("can not connect to cluster as user %s", user)
+	}
+}
diff --git a/test/e2e/features/basic.feature b/test/e2e/features/basic.feature
index dd0e6cd53..bec2316f5 100644
--- a/test/e2e/features/basic.feature
+++ b/test/e2e/features/basic.feature
@@ -135,7 +135,14 @@ Feature: Basic test
         And unsetting config property "enable-cluster-monitoring" succeeds
         And unsetting config property "memory" succeeds
 
-    @darwin @linux @windows
+    @linux
+    Scenario: Bundle generation check
+        # This will remove the pull secret from the instance and from the cluster
+        # You need to provide pull secret file again if you want to start this cluster
+        # from a stopped state.
+        Given executing "crc bundle generate" succeeds
+
+    @darwin @windows
     Scenario: CRC forcible stop
         When executing "crc stop -f"
         Then stdout should match "(.*)[Ss]topped the OpenShift cluster"
@@ -156,6 +163,11 @@ Feature: Basic test
         When executing "crc delete -f" succeeds
         Then stdout should contain "Deleted the OpenShift cluster"
 
+    @linux
+    Scenario: CRC starts with generated bundle
+        Given starting CRC with custom bundle succeeds
+        Then stderr should contain "Using custom bundle"
+
     @darwin
     Scenario Outline: CRC clean-up
         When executing "crc cleanup" succeeds
diff --git a/test/e2e/features/config.feature b/test/e2e/features/config.feature
index b24d24f24..5722a2ce9 100644
--- a/test/e2e/features/config.feature
+++ b/test/e2e/features/config.feature
@@ -74,7 +74,6 @@ Feature: Test configuration settings
         Examples:
             | property                             | value1 | value2 |
             | skip-check-bundle-extracted          | true   | false  |
-            | skip-check-hosts-file-permissions    | true   | false  |
             | skip-check-hyperkit-driver           | true   | false  |
             | skip-check-hyperkit-installed        | true   | false  |
             | skip-check-resolver-file-permissions | true   | false  |
diff --git a/test/e2e/features/ux.feature b/test/e2e/features/ux.feature
new file mode 100644
index 000000000..45e7b79a8
--- /dev/null
+++ b/test/e2e/features/ux.feature
@@ -0,0 +1,56 @@
+@ux
+Feature: UX Test
+
+    Tets CRC usage with ux components. 
+        * Handle CRC install/ uninstall operations based on installer distribution
+        * Manage CRC through the tray
+
+    @darwin
+    Scenario: Install CRC 
+        Given an environment where CRC is not installed
+        When install CRC from installer
+        Then CRC is installed
+
+    @darwin
+    Scenario: Install tray  
+        When install CRC tray
+        Then tray should be installed
+        And tray icon should be accessible
+
+    @darwin
+    Scenario: Start the cluster
+        Given fresh tray installation   
+        When start the cluster from the tray
+        And set the pull secret file 
+        Then cluster should be running
+        And tray should show cluster as running
+        And user should get notified with cluster state as running
+
+    @darwin
+    Scenario Outline: Connect to the cluster
+        Given a running cluster   
+        When using copied oc login command for <ocp-user>  
+        Then user is connected to the cluster as <ocp-user> 
+        # TODO notifications inconsistent https://github.com/code-ready/tray-macos/issues/84
+        # And user should get notified with command copied
+
+    Examples:
+            | ocp-user   |
+            | kubeadmin |
+            | developer |
+
+    @darwin 
+    Scenario: Stop the cluster
+        Given a running cluster   
+        When stop the cluster from the tray 
+        Then cluster should be stopped
+        And tray should show cluster as stopped
+        And user should get notified with cluster state as stopped
+
+    @darwin 
+    Scenario: Restart the cluster
+        Given a stopped cluster   
+        When start the cluster from the tray 
+        Then cluster should be running
+        And tray should show cluster as running
+        And user should get notified with cluster state as running
diff --git a/test/extended/crc/cmd/cmd.go b/test/extended/crc/cmd/cmd.go
new file mode 100644
index 000000000..f87b6a2e5
--- /dev/null
+++ b/test/extended/crc/cmd/cmd.go
@@ -0,0 +1,89 @@
+package cmd
+
+import (
+	"fmt"
+	"time"
+
+	clicumber "github.com/code-ready/clicumber/testsuite"
+	"github.com/code-ready/crc/test/extended/util"
+)
+
+const (
+	// timeout to wait for cluster to change its state
+	clusterStateTimeout       = "900"
+	CRCExecutableInstalled    = "installed"
+	CRCExecutableNotInstalled = "notInstalled"
+)
+
+func SetConfigPropertyToValueSucceedsOrFails(property string, value string, expected string) error {
+	cmd := "crc config set " + property + " " + value
+	return clicumber.ExecuteCommandSucceedsOrFails(cmd, expected)
+}
+
+func UnsetConfigPropertySucceedsOrFails(property string, expected string) error {
+	cmd := "crc config unset " + property
+	return clicumber.ExecuteCommandSucceedsOrFails(cmd, expected)
+}
+
+func WaitForClusterInState(state string) error {
+	retryCount := 15
+	iterationDuration, extraDuration, err :=
+		util.GetRetryParametersFromTimeoutInSeconds(retryCount, clusterStateTimeout)
+	if err != nil {
+		return err
+	}
+	for i := 0; i < retryCount; i++ {
+		err := CheckCRCStatus(state)
+		if err == nil {
+			return nil
+		}
+		time.Sleep(iterationDuration)
+	}
+	if extraDuration != 0 {
+		time.Sleep(extraDuration)
+	}
+	return fmt.Errorf("the did not reach the %s state", state)
+}
+
+func CheckCRCStatus(state string) error {
+	expression := `.*OpenShift: .*Running \(v\d+\.\d+\.\d+.*\).*`
+	if state == "stopped" {
+		expression = ".*OpenShift: .*Stopped.*"
+	}
+
+	err := clicumber.ExecuteCommand("crc status")
+	if err != nil {
+		return err
+	}
+	return clicumber.CommandReturnShouldMatch("stdout", expression)
+}
+
+func CheckCRCExecutableState(state string) error {
+	command := "which crc"
+	switch state {
+	case CRCExecutableInstalled:
+		return clicumber.ExecuteCommandSucceedsOrFails(command, "succeeds")
+	case CRCExecutableNotInstalled:
+		return clicumber.ExecuteCommandSucceedsOrFails(command, "fails")
+	default:
+		return fmt.Errorf("%s state is not defined as valid crc executable state", state)
+	}
+}
+
+func CheckMachineNotExists() error {
+	expression := `.*Machine does not exist.*`
+	err := clicumber.ExecuteCommand("crc status")
+	if err != nil {
+		return err
+	}
+	return clicumber.CommandReturnShouldMatch("stderr", expression)
+}
+
+func DeleteCRC() error {
+
+	command := "crc delete"
+	_ = clicumber.ExecuteCommand(command)
+
+	fmt.Printf("Deleted CRC instance (if one existed).\n")
+	return nil
+}
diff --git a/test/extended/crc/ux/installer/applescripts/install.applescript b/test/extended/crc/ux/installer/applescripts/install.applescript
new file mode 100644
index 000000000..7c66d7a14
--- /dev/null
+++ b/test/extended/crc/ux/installer/applescripts/install.applescript
@@ -0,0 +1,41 @@
+on run {installerPath, adminPassword}
+    set installer to installerPath as POSIX file
+    set installerWindow to "Install CodeReady Containers"
+    tell application "Finder" to open installer
+    delay 1
+    tell application "System Events"
+        tell (first application process whose bundle identifier is "com.apple.installer") 
+            # Introduction
+            click button "Continue" of window installerWindow 
+            delay 2
+            # License
+            click button "Continue" of window installerWindow 
+            delay 2
+        end tell
+    end tell
+    tell application "System Events"
+        tell (first application process whose bundle identifier is "com.apple.installer")
+            # Agree the License
+            click button "Agree" of sheet 1 of window installerWindow
+            delay 2
+        end tell
+    end tell
+    tell application "System Events"
+        tell (first application process whose bundle identifier is "com.apple.installer")
+            # Select Destination
+            try
+            delay 2
+            click button "Continue" of window installerWindow
+            on error errMsg
+                log errMsg
+            end try
+            # Installation
+            click button "Install" of window installerWindow
+            delay 2
+            keystroke adminPassword & return
+            delay 15
+            # Close the installer
+            click button "Close" of window installerWindow
+        end tell
+    end tell
+end run
diff --git a/test/extended/crc/ux/installer/installer.go b/test/extended/crc/ux/installer/installer.go
new file mode 100644
index 000000000..3ecfd7022
--- /dev/null
+++ b/test/extended/crc/ux/installer/installer.go
@@ -0,0 +1,5 @@
+package installer
+
+type Installer interface {
+	Install() error
+}
diff --git a/test/extended/crc/ux/installer/installer_unix.go b/test/extended/crc/ux/installer/installer_unix.go
new file mode 100644
index 000000000..07e970771
--- /dev/null
+++ b/test/extended/crc/ux/installer/installer_unix.go
@@ -0,0 +1,41 @@
+// +build !windows
+
+package installer
+
+import (
+	"runtime"
+
+	"github.com/code-ready/crc/test/extended/os/applescript"
+)
+
+const (
+	scriptsRelativePath string = "applescripts"
+	installScript       string = "install.applescript"
+)
+
+type applescriptHandler struct {
+	currentUserPassword *string
+	installerPath       *string
+}
+
+func NewInstaller(currentUserPassword *string, installerPath *string) Installer {
+	// TODO check parameters as they are mandatory otherwise exit
+	if runtime.GOOS == "darwin" {
+		return applescriptHandler{
+			currentUserPassword: currentUserPassword,
+			installerPath:       installerPath}
+
+	}
+	return nil
+}
+
+func RequiredResourcesPath() (string, error) {
+	return applescript.GetScriptsPath(scriptsRelativePath)
+}
+
+func (a applescriptHandler) Install() error {
+	return applescript.ExecuteApplescript(
+		installScript,
+		*a.installerPath,
+		*a.currentUserPassword)
+}
diff --git a/test/extended/crc/ux/installer/installer_windows.go b/test/extended/crc/ux/installer/installer_windows.go
new file mode 100644
index 000000000..5e32d9879
--- /dev/null
+++ b/test/extended/crc/ux/installer/installer_windows.go
@@ -0,0 +1,28 @@
+// +build windows
+
+package installer
+
+import (
+	"fmt"
+)
+
+type handler struct {
+	currentUserPassword *string
+	installerPath       *string
+}
+
+func NewInstaller(currentUserPassword *string, installerPath *string) Installer {
+	// TODO check parameters as they are mandatory otherwise exit
+	return handler{
+		currentUserPassword: currentUserPassword,
+		installerPath:       installerPath}
+
+}
+
+func RequiredResourcesPath() (string, error) {
+	return "", nil
+}
+
+func (h handler) Install() error {
+	return fmt.Errorf("not implemented yet")
+}
diff --git a/test/extended/crc/ux/notification/applescripts/manageNotifications.applescript b/test/extended/crc/ux/notification/applescripts/manageNotifications.applescript
new file mode 100644
index 000000000..c0ffba7f9
--- /dev/null
+++ b/test/extended/crc/ux/notification/applescripts/manageNotifications.applescript
@@ -0,0 +1,138 @@
+on run{action}
+-- valid actions on notification
+-- get latests notification
+set action_get to "get"
+-- close all notifications
+set action_clear to "clear"
+
+-- get major version for os
+set os_version to system attribute "sys1"
+
+-- notifications should be visible to perform actions 
+if not is_notificationmenu_visible() then
+	open_notifications(os_version)
+end if
+
+-- handle action
+if action = action_get then
+	get_notification(os_version)
+else if action = action_clear then
+	clear_notifications(os_version)
+else 
+	error "not supported action"
+end if
+
+end run
+
+-- get the menua bar controller 
+-- 11.X uses controlcenter
+-- 10.X systemuiserver
+on get_systemmenu_controller(os_version)
+if os_version = 11 then
+	return "com.apple.controlcenter"
+else if os_version = 10 then
+	return "com.apple.systemuiserver"
+else
+	error "not supported os version"
+end if 
+end get_systemmenu_controller
+
+on open_notifications(os_major_version)
+set systemmenu_controler to get_systemmenu_controller(os_major_version)
+tell application "System Events"
+	tell (first application process whose bundle identifier is systemmenu_controler)
+        	click menu bar item 1 of menu bar 1
+                delay 0.25
+    	end tell
+end tell  
+end open_notifications
+
+on is_notificationmenu_visible() 
+try
+	tell application "System Events"
+    		tell window 1 of (first application process whose bundle identifier is "com.apple.notificationcenterui")
+                        name
+    		end tell
+	end tell
+    return true
+on error errMsg
+	return false
+end try
+end is_notificationmenu_visible
+
+on get_notification(os_version)
+if os_version = 11 then
+	return get_notification_11()
+else if os_version = 10 then
+	return get_notification_10()
+else
+	error "not supported os version"
+end if
+end get_notification
+
+on get_notification_11()
+try
+	tell application "System Events"
+		tell group 1 of UI element 1 of scroll area 1 of window 1 of (first application process whose bundle identifier is "com.apple.notificationcenterui")
+			return value of static text 4        
+			end tell
+	end tell
+on error
+	return ""
+end try 
+end get_notification_11
+
+on get_notification_10()
+try 
+	tell application "System Events"
+		tell (first application process whose bundle identifier is "com.apple.notificationcenterui")
+			tell UI Element 1 of row 3 of table 1 of scroll area 1 of window 1
+				return value of static text 5
+			end tell
+		end tell
+	end tell
+on error
+	return ""
+end try 
+end get_notification_10
+
+-- TODO Ensure only one vindow on notificationcenterui process
+on clear_notifications(os_version)
+if os_version = 11 then
+        clear_notifications_11()
+else if os_version = 10 then
+        clear_notifications_10()
+else
+        error "not supported os version"
+end if
+end clear_notifications
+
+on clear_notifications_11()
+tell application "System Events"
+	set allClosed to false
+	repeat until allClosed
+	try
+        tell group 1 of UI element 1 of scroll area 1 of window 1 of (first application process whose bundle identifier is "com.apple.notificationcenterui")
+                click
+        end tell
+	on error
+ 		set allClosed to true
+	end try
+	end repeat
+end tell
+end clear_notifications_11
+
+on clear_notifications_10()
+tell application "System Events"
+    try 
+    tell (first application process whose bundle identifier is "com.apple.notificationcenterui")
+		tell (first button of UI Element 1 of row 2 of table 1 of scroll area 1 of window 1 whose value of attribute "AXDescription" is "clear")
+			click
+        end tell
+    end tell
+	on error  
+	-- no notifications to clear
+	end try
+end tell
+end clear_notifications_10
+
diff --git a/test/extended/crc/ux/notification/const.go b/test/extended/crc/ux/notification/const.go
new file mode 100644
index 000000000..b24229792
--- /dev/null
+++ b/test/extended/crc/ux/notification/const.go
@@ -0,0 +1,11 @@
+// TODO support current implementation only for non windows
+// +build !windows
+
+package notification
+
+const (
+	startMessage  string = "OpenShift Cluster is running"
+	stopMessage   string = "The OpenShift Cluster was successfully stopped"
+	deleteMessage string = "The OpenShift Cluster is successfully deleted"
+	// copyCommandMessage string = "OC Login command copied to clipboard, go ahead and login to your cluster"
+)
diff --git a/test/extended/crc/ux/notification/notification.go b/test/extended/crc/ux/notification/notification.go
new file mode 100644
index 000000000..2577b7a02
--- /dev/null
+++ b/test/extended/crc/ux/notification/notification.go
@@ -0,0 +1,8 @@
+package notification
+
+type Notification interface {
+	GetClusterRunning() error
+	GetClusterStopped() error
+	GetClusterDeleted() error
+	ClearNotifications() error
+}
diff --git a/test/extended/crc/ux/notification/notification_unix.go b/test/extended/crc/ux/notification/notification_unix.go
new file mode 100644
index 000000000..3b8a579fe
--- /dev/null
+++ b/test/extended/crc/ux/notification/notification_unix.go
@@ -0,0 +1,74 @@
+// +build !windows
+
+package notification
+
+import (
+	"fmt"
+	"runtime"
+	"time"
+
+	"github.com/code-ready/crc/test/extended/os/applescript"
+	"github.com/code-ready/crc/test/extended/util"
+)
+
+type applescriptHandler struct {
+}
+
+const (
+	scriptsRelativePath           string = "applescripts"
+	manageNotifications           string = "manageNotifications.applescript"
+	manageNotificationActionGet   string = "get"
+	manageNotificationActionClear string = "clear"
+
+	notificationWaitTimeout string = "200"
+)
+
+func NewNotification() Notification {
+	if runtime.GOOS == "darwin" {
+		return applescriptHandler{}
+
+	}
+	return nil
+}
+
+func RequiredResourcesPath() (string, error) {
+	return applescript.GetScriptsPath(scriptsRelativePath)
+}
+
+func (a applescriptHandler) GetClusterRunning() error {
+	return checkNotificationMessage(startMessage)
+}
+
+func (a applescriptHandler) GetClusterStopped() error {
+	return checkNotificationMessage(stopMessage)
+
+}
+
+func (a applescriptHandler) GetClusterDeleted() error {
+	return checkNotificationMessage(deleteMessage)
+}
+
+func (a applescriptHandler) ClearNotifications() error {
+	return applescript.ExecuteApplescript(manageNotifications, manageNotificationActionClear)
+}
+
+func checkNotificationMessage(notificationMessage string) error {
+	retryCount := 10
+	iterationDuration, extraDuration, err :=
+		util.GetRetryParametersFromTimeoutInSeconds(retryCount, notificationWaitTimeout)
+	if err != nil {
+		return err
+	}
+	for i := 0; i < retryCount; i++ {
+		err := applescript.ExecuteApplescriptReturnShouldMatch(
+			notificationMessage, manageNotifications, manageNotificationActionGet)
+		if err == nil {
+			return nil
+		}
+		time.Sleep(iterationDuration)
+	}
+	if extraDuration != 0 {
+		time.Sleep(extraDuration)
+	}
+	return fmt.Errorf("notification: %s. Timeout", notificationMessage)
+}
diff --git a/test/extended/crc/ux/notification/notification_windows.go b/test/extended/crc/ux/notification/notification_windows.go
new file mode 100644
index 000000000..17019a51d
--- /dev/null
+++ b/test/extended/crc/ux/notification/notification_windows.go
@@ -0,0 +1,35 @@
+// +build windows
+
+package notification
+
+import (
+	"fmt"
+)
+
+type handler struct {
+}
+
+func NewNotification() Notification {
+	return handler{}
+}
+
+func RequiredResourcesPath() (string, error) {
+	return "", nil
+}
+
+func (h handler) GetClusterRunning() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) GetClusterStopped() error {
+	return fmt.Errorf("not implemented yet")
+
+}
+
+func (h handler) GetClusterDeleted() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) ClearNotifications() error {
+	return fmt.Errorf("not implemented yet")
+}
diff --git a/test/extended/crc/ux/tray/applescripts/checkTrayIconIsVisible.applescript b/test/extended/crc/ux/tray/applescripts/checkTrayIconIsVisible.applescript
new file mode 100644
index 000000000..f258ce591
--- /dev/null
+++ b/test/extended/crc/ux/tray/applescripts/checkTrayIconIsVisible.applescript
@@ -0,0 +1,14 @@
+on run {bundleIdentifer}
+    tell application "System Events" 
+        tell (first application process whose bundle identifier is bundleIdentifer) 
+            tell menu bar item of menu bar 2 
+                try
+                    name
+                    return true
+                on error
+                    return false
+                end try
+            end tell 
+        end tell 
+    end tell
+end run
diff --git a/test/extended/crc/ux/tray/applescripts/clickTrayMenuItem.applescript b/test/extended/crc/ux/tray/applescripts/clickTrayMenuItem.applescript
new file mode 100644
index 000000000..b21706f24
--- /dev/null
+++ b/test/extended/crc/ux/tray/applescripts/clickTrayMenuItem.applescript
@@ -0,0 +1,13 @@
+on run {bundleIdentifer, buttonAXIdentifier}
+    tell application "System Events"
+        tell (first application process whose bundle identifier is bundleIdentifer)
+            tell menu bar item of menu bar 2
+                click
+                delay 0.5
+                tell (first menu item of menu 1 whose value of attribute "AXIdentifier" is buttonAXIdentifier)
+                    click
+                end tell
+            end tell
+        end tell
+    end tell
+end run
diff --git a/test/extended/crc/ux/tray/applescripts/getOCLoginCommand.applescript b/test/extended/crc/ux/tray/applescripts/getOCLoginCommand.applescript
new file mode 100644
index 000000000..4463d5605
--- /dev/null
+++ b/test/extended/crc/ux/tray/applescripts/getOCLoginCommand.applescript
@@ -0,0 +1,31 @@
+on run {bundleIdentifer, copyLoginUserAXIdentifier}
+set systemMenuBarId to 2
+set trayMainMenuId to 1
+set copyOCLoginMenuItem to "Copy OC Login Command"
+set copyOCLoginMenuAXIdentifier to "copy_login"
+set menuClickDelay to 0.5
+try
+    tell application "System Events"
+        tell (first application process whose bundle identifier is bundleIdentifer)
+            tell menu bar item of menu bar systemMenuBarId
+                click
+                delay menuClickDelay
+            end tell
+        end tell
+end tell
+on error errMsg
+end try
+tell application "System Events"
+    tell (first application process whose bundle identifier is bundleIdentifer)
+          tell menu item copyOCLoginMenuItem of menu trayMainMenuId of menu bar item of menu bar systemMenuBarId
+            click
+            delay menuClickDelay
+            tell (first menu whose value of attribute "AXIdentifier" is copyOCLoginMenuAXIdentifier)
+                tell (first menu item whose value of attribute "AXIdentifier" is copyLoginUserAXIdentifier)
+                    click
+                end tell
+            end tell 
+        end tell
+    end tell
+end tell
+end run
diff --git a/test/extended/crc/ux/tray/applescripts/getTrayFieldlValue.applescript b/test/extended/crc/ux/tray/applescripts/getTrayFieldlValue.applescript
new file mode 100644
index 000000000..db6437ce8
--- /dev/null
+++ b/test/extended/crc/ux/tray/applescripts/getTrayFieldlValue.applescript
@@ -0,0 +1,9 @@
+on run {bundleIdentifer, elementAXIdentifier}
+    tell application "System Events"
+        tell (first application process whose bundle identifier is bundleIdentifer)
+            click menu bar item of menu bar 2
+            delay 0.5
+            return name of (first menu item of menu 1 of menu bar 2 whose value of attribute "AXIdentifier" is elementAXIdentifier)
+        end tell
+    end tell
+end run
diff --git a/test/extended/crc/ux/tray/applescripts/installTray.applescript b/test/extended/crc/ux/tray/applescripts/installTray.applescript
new file mode 100644
index 000000000..b242f860d
--- /dev/null
+++ b/test/extended/crc/ux/tray/applescripts/installTray.applescript
@@ -0,0 +1,4 @@
+on run {trayAppPath}
+    set trayApp to trayAppPath as POSIX file
+    tell application "Finder" to open trayApp
+end run
diff --git a/test/extended/crc/ux/tray/applescripts/runOCLoginCommand.applescript b/test/extended/crc/ux/tray/applescripts/runOCLoginCommand.applescript
new file mode 100644
index 000000000..f55205fe5
--- /dev/null
+++ b/test/extended/crc/ux/tray/applescripts/runOCLoginCommand.applescript
@@ -0,0 +1,45 @@
+on run
+try
+        set whoAmICommandOutput to 2
+        set terminal to get_terminal()
+        execute_command("eval $(crc oc-env)", terminal)
+        execute_command(the clipboard as text, terminal)
+        accept_insecure_connection(terminal)
+        execute_command("clear", terminal)
+        execute_command("oc whoami", terminal)
+        set result to command_output(whoAmICommandOutput)
+        return result
+on error errMsg
+        return errMsg
+end try
+end run
+
+on get_terminal()
+tell application "Terminal"
+        set crcTerminal to do script ("")
+        return crcTerminal
+end tell
+end get_terminal
+
+on execute_command(command, crcTerminal)
+tell application "Terminal"
+   do script (command) in crcTerminal
+   delay 2
+end tell
+end execute_command
+
+on command_output(commandOutputLine)
+tell application "Terminal"
+        tell front window to set theText to contents of selected tab as text
+        return (paragraph commandOutputLine of theText)
+end tell
+end command_output
+
+on accept_insecure_connection(crcTerminal)
+set insecureConnectionOutput to 6
+set insecureConnectionQuestion to "Use insecure connections? (y/n)"
+set insecure to command_output(insecureConnectionOutput)
+if insecure contains insecureConnectionQuestion
+        execute_command("y", crcTerminal)
+end if
+end accept_insecure_connection
diff --git a/test/extended/crc/ux/tray/applescripts/setPullSecretFileLocation.applescript b/test/extended/crc/ux/tray/applescripts/setPullSecretFileLocation.applescript
new file mode 100644
index 000000000..992bf823c
--- /dev/null
+++ b/test/extended/crc/ux/tray/applescripts/setPullSecretFileLocation.applescript
@@ -0,0 +1,12 @@
+on run {bundleIdentifer, pullSecretFile}
+    tell application "System Events" 
+        tell (first application process whose bundle identifier is bundleIdentifer) 
+            tell window "Select Pull Secret"
+                set value of text field 1 to pullSecretFile    
+                tell button "OK" 
+                    click
+                end tell
+            end tell 
+        end tell 
+    end tell
+end run
diff --git a/test/extended/crc/ux/tray/const.go b/test/extended/crc/ux/tray/const.go
new file mode 100644
index 000000000..3c8dda9db
--- /dev/null
+++ b/test/extended/crc/ux/tray/const.go
@@ -0,0 +1,23 @@
+// +build !windows
+
+package tray
+
+const (
+	actionStart  string = "start"
+	actionStop   string = "stop"
+	actionDelete string = "delete"
+	actionQuit   string = "quit"
+
+	fieldState string = "state"
+
+	stateRunning string = "Running"
+	stateStopped string = "Stopped"
+
+	userKubeadmin string = "kubeadmin"
+	userDeveloper string = "developer"
+)
+
+const (
+	uxCheckAccessibilityDuration = "2s"
+	uxCheckAccessibilityRetry    = 10
+)
diff --git a/test/extended/crc/ux/tray/tray.go b/test/extended/crc/ux/tray/tray.go
new file mode 100644
index 000000000..f2051084a
--- /dev/null
+++ b/test/extended/crc/ux/tray/tray.go
@@ -0,0 +1,24 @@
+package tray
+
+type Tray interface {
+	Install() error
+	IsInstalled() error
+	IsAccessible() error
+	ClickStart() error
+	ClickStop() error
+	ClickDelete() error
+	ClickQuit() error
+	SetPullSecretFileLocation() error
+	IsClusterRunning() error
+	IsClusterStopped() error
+	CopyOCLoginCommandAsKubeadmin() error
+	CopyOCLoginCommandAsDeveloper() error
+	// TODO check if make sense create a new ux component
+	ConnectClusterAsKubeadmin() error
+	ConnectClusterAsDeveloper() error
+}
+
+type Element struct {
+	Name         string
+	AXIdentifier string
+}
diff --git a/test/extended/crc/ux/tray/tray_unix.go b/test/extended/crc/ux/tray/tray_unix.go
new file mode 100644
index 000000000..c7d07a255
--- /dev/null
+++ b/test/extended/crc/ux/tray/tray_unix.go
@@ -0,0 +1,200 @@
+// +build !windows
+
+package tray
+
+import (
+	"fmt"
+	"runtime"
+	"strings"
+	"time"
+
+	clicumber "github.com/code-ready/clicumber/testsuite"
+	"github.com/code-ready/crc/test/extended/os/applescript"
+)
+
+const (
+	scriptsRelativePath       string = "applescripts"
+	checkTrayIconIsVisible    string = "checkTrayIconIsVisible.applescript"
+	clickTrayMenuItem         string = "clickTrayMenuItem.applescript"
+	setPullSecretFileLocation string = "setPullSecretFileLocation.applescript"
+	getTrayFieldlValue        string = "getTrayFieldlValue.applescript"
+	installTray               string = "installTray.applescript"
+	getOCLoginCommand         string = "getOCLoginCommand.applescript"
+	runOCLoginCommand         string = "runOCLoginCommand.applescript"
+
+	bundleIdentifier string = "com.redhat.codeready.containers"
+	appPath          string = "/Applications/CodeReady Containers.app"
+)
+
+var (
+	elements = []Element{
+		{
+			Name:         actionStart,
+			AXIdentifier: "start"},
+		{
+			Name:         actionStop,
+			AXIdentifier: "stop"},
+		{
+			Name:         actionDelete,
+			AXIdentifier: "delete"},
+		{
+			Name:         actionQuit,
+			AXIdentifier: "quit"},
+		{
+			Name:         fieldState,
+			AXIdentifier: "cluster_status"},
+		{
+			Name:         userKubeadmin,
+			AXIdentifier: "kubeadmin_login"},
+		{
+			Name:         userDeveloper,
+			AXIdentifier: "developer_login"},
+	}
+)
+
+type applescriptHandler struct {
+	bundleLocation         *string
+	pullSecretFileLocation *string
+}
+
+func NewTray(bundleLocationValue *string, pullSecretFileLocationValue *string) Tray {
+	if runtime.GOOS == "darwin" {
+		return applescriptHandler{
+			bundleLocation:         bundleLocationValue,
+			pullSecretFileLocation: pullSecretFileLocationValue}
+
+	}
+	return nil
+}
+
+func RequiredResourcesPath() (string, error) {
+	return applescript.GetScriptsPath(scriptsRelativePath)
+}
+
+func (a applescriptHandler) Install() error {
+	err := clicumber.ExecuteCommandSucceedsOrFails("crc setup", "succeeds")
+	if err != nil {
+		return err
+	}
+	// Required to pass parameters with spaces to applescript
+	sanitizedAppPath := strings.Join(append([]string{"\""}, appPath, "\""), "")
+	return applescript.ExecuteApplescript(installTray, sanitizedAppPath)
+}
+
+func (a applescriptHandler) IsInstalled() error {
+	return executeCommandSucceeds("launchctl list | grep crc", "0.*tray")
+}
+
+func (a applescriptHandler) IsAccessible() error {
+	return checkAccessible(func() error {
+		return applescript.ExecuteApplescript(
+			checkTrayIconIsVisible, bundleIdentifier)
+	}, "Tray icon")
+}
+
+func (a applescriptHandler) ClickStart() error {
+	return clickButtonByAction(actionStart)
+}
+
+func (a applescriptHandler) ClickStop() error {
+	return clickButtonByAction(actionStop)
+}
+
+func (a applescriptHandler) ClickDelete() error {
+	return clickButtonByAction(actionDelete)
+}
+
+func (a applescriptHandler) ClickQuit() error {
+	return clickButtonByAction(actionQuit)
+}
+
+func (a applescriptHandler) SetPullSecretFileLocation() error {
+	return applescript.ExecuteApplescript(
+		setPullSecretFileLocation, bundleIdentifier, *a.pullSecretFileLocation)
+}
+
+func (a applescriptHandler) IsClusterRunning() error {
+	return checkTrayShowsFieldWithValue(fieldState, stateRunning)
+}
+
+func (a applescriptHandler) IsClusterStopped() error {
+	return checkTrayShowsFieldWithValue(fieldState, stateStopped)
+}
+
+func (a applescriptHandler) CopyOCLoginCommandAsKubeadmin() error {
+	return clickCopyOCLoginCommand(userKubeadmin)
+}
+
+func (a applescriptHandler) CopyOCLoginCommandAsDeveloper() error {
+	return clickCopyOCLoginCommand(userDeveloper)
+}
+
+func (a applescriptHandler) ConnectClusterAsKubeadmin() error {
+	return applescript.ExecuteApplescriptReturnShouldMatch(
+		userKubeadmin, runOCLoginCommand)
+}
+
+func (a applescriptHandler) ConnectClusterAsDeveloper() error {
+	return applescript.ExecuteApplescriptReturnShouldMatch(
+		userDeveloper, runOCLoginCommand)
+}
+
+func clickButtonByAction(actionName string) error {
+	return clickOnElement(actionName, clickTrayMenuItem)
+}
+
+func clickCopyOCLoginCommand(userName string) error {
+	return clickOnElement(userName, getOCLoginCommand)
+}
+
+func clickOnElement(elementName string, scriptName string) error {
+	element, err := getElement(elementName, elements)
+	if err != nil {
+		return err
+	}
+	return applescript.ExecuteApplescript(
+		scriptName, bundleIdentifier, element.AXIdentifier)
+}
+
+func checkTrayShowsFieldWithValue(field string, expectedValue string) error {
+	element, err := getElement(field, elements)
+	if err != nil {
+		return err
+	}
+	return applescript.ExecuteApplescriptReturnShouldMatch(
+		expectedValue, getTrayFieldlValue, bundleIdentifier, element.AXIdentifier)
+}
+
+func getElement(name string, elements []Element) (Element, error) {
+	for _, e := range elements {
+		if name == e.Name {
+			return e, nil
+		}
+	}
+	return Element{},
+		fmt.Errorf("element '%s', Can not be accessed from the tray", name)
+}
+
+func checkAccessible(uxIsAccessible func() error, component string) error {
+	retryDuration, err := time.ParseDuration(uxCheckAccessibilityDuration)
+	if err != nil {
+		return err
+	}
+	for i := 0; i < uxCheckAccessibilityRetry; i++ {
+		err := uxIsAccessible()
+		if err == nil {
+			return nil
+		}
+		time.Sleep(retryDuration)
+	}
+	return fmt.Errorf("%s is not accessible", component)
+}
+
+// TODO review which helper use
+func executeCommandSucceeds(command string, expectedOutput string) error {
+	err := clicumber.ExecuteCommand(command)
+	if err != nil {
+		return err
+	}
+	return clicumber.CommandReturnShouldMatch("stdout", expectedOutput)
+}
diff --git a/test/extended/crc/ux/tray/tray_windows.go b/test/extended/crc/ux/tray/tray_windows.go
new file mode 100644
index 000000000..f9f968b61
--- /dev/null
+++ b/test/extended/crc/ux/tray/tray_windows.go
@@ -0,0 +1,79 @@
+// +build windows
+
+package tray
+
+import (
+	"fmt"
+)
+
+type handler struct {
+	bundleLocation         *string
+	pullSecretFileLocation *string
+}
+
+func NewTray(bundleLocationValue *string, pullSecretFileLocationValue *string) Tray {
+	return handler{
+		bundleLocation:         bundleLocationValue,
+		pullSecretFileLocation: pullSecretFileLocationValue}
+
+}
+
+func RequiredResourcesPath() (string, error) {
+	return "", nil
+}
+
+func (h handler) Install() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) IsInstalled() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) IsAccessible() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) ClickStart() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) ClickStop() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) ClickDelete() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) ClickQuit() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) SetPullSecretFileLocation() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) IsClusterRunning() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) IsClusterStopped() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) CopyOCLoginCommandAsKubeadmin() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) CopyOCLoginCommandAsDeveloper() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) ConnectClusterAsKubeadmin() error {
+	return fmt.Errorf("not implemented yet")
+}
+
+func (h handler) ConnectClusterAsDeveloper() error {
+	return fmt.Errorf("not implemented yet")
+}
diff --git a/test/extended/os/applescript/applescript.go b/test/extended/os/applescript/applescript.go
new file mode 100644
index 000000000..1b74d111c
--- /dev/null
+++ b/test/extended/os/applescript/applescript.go
@@ -0,0 +1,44 @@
+// +build !windows
+
+package applescript
+
+import (
+	"fmt"
+	"path"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	clicumber "github.com/code-ready/clicumber/testsuite"
+)
+
+func ExecuteApplescript(scriptFilename string, args ...string) error {
+	command := strings.Join(append(
+		append([]string{"osascript"}, scriptFilename),
+		args...),
+		" ")
+	return clicumber.ExecuteCommandSucceedsOrFails(command, "succeeds")
+}
+
+func ExecuteApplescriptReturnShouldMatch(expectedOutput string,
+	scriptFilename string, args ...string) error {
+	command := strings.Join(append(
+		append([]string{"osascript"}, scriptFilename),
+		args...),
+		" ")
+	err := clicumber.ExecuteCommand(command)
+	if err != nil {
+		return err
+	}
+	return clicumber.CommandReturnShouldMatch("stdout", expectedOutput)
+}
+
+func GetScriptsPath(scriptsRelativePath string) (string, error) {
+	_, filename, _, ok := runtime.Caller(1)
+	if ok {
+		return string(path.Dir(filename) +
+			string(filepath.Separator) +
+			scriptsRelativePath), nil
+	}
+	return "", fmt.Errorf("error recovering required resources for applescript installer handler")
+}
diff --git a/test/e2e/crcsuite/collect.go b/test/extended/util/collect.go
similarity index 99%
rename from test/e2e/crcsuite/collect.go
rename to test/extended/util/collect.go
index d78e47253..9bd208826 100644
--- a/test/e2e/crcsuite/collect.go
+++ b/test/extended/util/collect.go
@@ -1,4 +1,4 @@
-package crcsuite
+package util
 
 import (
 	"archive/zip"
@@ -22,7 +22,7 @@ import (
 	"golang.org/x/sync/semaphore"
 )
 
-func runDiagnose(dir string) error {
+func RunDiagnose(dir string) error {
 	filename := filepath.Join(dir, fmt.Sprintf("diagnose-%d.zip", time.Now().Unix()))
 	absolute, err := filepath.Abs(filename)
 	if err != nil {
diff --git a/test/e2e/crcsuite/prepare.go b/test/extended/util/util.go
similarity index 61%
rename from test/e2e/crcsuite/prepare.go
rename to test/extended/util/util.go
index 5cb7c9185..bb1114601 100644
--- a/test/e2e/crcsuite/prepare.go
+++ b/test/extended/util/util.go
@@ -1,64 +1,20 @@
-package crcsuite
+package util
 
 import (
-	"flag"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
 	"runtime"
+	"strconv"
 	"strings"
+	"time"
 
 	"github.com/code-ready/crc/pkg/download"
 )
 
-// Download bundle for testing
-func DownloadBundle(bundleLocation string, bundleDestination string) (string, error) {
-
-	if bundleLocation[:4] != "http" {
-
-		// copy the file locall
-
-		if bundleDestination == "." {
-			bundleDestination, _ = os.Getwd()
-		}
-		fmt.Printf("Copying bundle from %s to %s.\n", bundleLocation, bundleDestination)
-		bundleDestination = filepath.Join(bundleDestination, bundleName)
-
-		source, err := os.Open(bundleLocation)
-		if err != nil {
-			return "", err
-		}
-		defer source.Close()
-
-		destination, err := os.Create(bundleDestination)
-		if err != nil {
-			return "", err
-		}
-		defer destination.Close()
-
-		_, err = io.Copy(destination, source)
-		if err != nil {
-			return "", err
-		}
-
-		err = destination.Sync()
-
-		return bundleDestination, err
-	}
-
-	filename, err := download.Download(bundleLocation, bundleDestination, 0644)
-	fmt.Printf("Downloading bundle from %s to %s.\n", bundleLocation, bundleDestination)
-	if err != nil {
-		return "", err
-	}
-
-	return filename, nil
-}
-
 func CopyFilesToTestDir() error {
-
 	cwd, err := os.Getwd()
 	if err != nil {
 		fmt.Printf("Error retrieving current dir: %s", err)
@@ -75,16 +31,19 @@ func CopyFilesToTestDir() error {
 	dataDir := filepath.Join(dataDirPieces...)
 	dataDir = fmt.Sprintf("%s%c%s", volume, filepath.Separator, dataDir) // prepend volume back
 
-	files, err := ioutil.ReadDir(dataDir)
+	return CopyResourcesFromPath(dataDir)
+}
+
+func CopyResourcesFromPath(resourcesPath string) error {
+	files, err := ioutil.ReadDir(resourcesPath)
 	if err != nil {
 		fmt.Printf("Error occurred loading data files: %s", err)
 		return err
 	}
-
 	destLoc, _ := os.Getwd()
 	for _, file := range files {
 
-		sFileName := filepath.Join(dataDir, file.Name())
+		sFileName := filepath.Join(resourcesPath, file.Name())
 		fmt.Printf("Copying %s to %s\n", sFileName, destLoc)
 
 		sFile, err := os.Open(sFileName)
@@ -114,14 +73,92 @@ func CopyFilesToTestDir() error {
 			return err
 		}
 	}
-
 	return nil
 }
 
-func ParseFlags() {
+// Download bundle for testing
+func DownloadBundle(bundleLocation string, bundleDestination string, bundleName string) (string, error) {
+
+	if bundleLocation[:4] != "http" {
+
+		// copy the file locall
+
+		if bundleDestination == "." {
+			bundleDestination, _ = os.Getwd()
+		}
+		fmt.Printf("Copying bundle from %s to %s.\n", bundleLocation, bundleDestination)
+		bundleDestination = filepath.Join(bundleDestination, bundleName)
+
+		source, err := os.Open(bundleLocation)
+		if err != nil {
+			return "", err
+		}
+		defer source.Close()
+
+		destination, err := os.Create(bundleDestination)
+		if err != nil {
+			return "", err
+		}
+		defer destination.Close()
+
+		_, err = io.Copy(destination, source)
+		if err != nil {
+			return "", err
+		}
+
+		err = destination.Sync()
+
+		return bundleDestination, err
+	}
+
+	filename, err := download.Download(bundleLocation, bundleDestination, 0644)
+	fmt.Printf("Downloading bundle from %s to %s.\n", bundleLocation, bundleDestination)
+	if err != nil {
+		return "", err
+	}
+
+	return filename, nil
+}
+
+func RemoveCRCHome(crcHome string) error {
+	keepFile := filepath.Join(crcHome, ".keep")
+	_, err := os.Stat(keepFile)
+	if err != nil { // cannot get keepFile's status
+		err = os.RemoveAll(crcHome)
+
+		if err != nil {
+			fmt.Printf("Problem deleting CRC home folder %s.\n", crcHome)
+			return err
+		}
+
+		fmt.Printf("Deleted CRC home folder %s.\n", crcHome)
+		return nil
 
-	flag.StringVar(&bundleLocation, "bundle-location", "", "Path to the bundle to be used in tests")
-	flag.StringVar(&pullSecretFile, "pull-secret-file", "", "Path to the file containing pull secret")
-	flag.StringVar(&CRCExecutable, "crc-binary", "", "Path to the CRC executable to be tested")
-	flag.StringVar(&bundleVersion, "bundle-version", "", "Version of the bundle used in tests")
+	}
+	// keepFile exists
+	return fmt.Errorf("folder %s not removed as per request: %s present", crcHome, keepFile)
+}
+
+// Based on the number of iterations for a given timeout in seconds the function returns the duration of echa loop
+// and the extra time in case required to complete the timeout
+func GetRetryParametersFromTimeoutInSeconds(iterations int, timeout string) (time.Duration, time.Duration, error) {
+	totalTime, err := strconv.Atoi(timeout)
+	if err != nil {
+		return 0, 0, err
+	}
+	iterationDuration, err :=
+		time.ParseDuration(strconv.Itoa(totalTime/iterations) + "s")
+	if err != nil {
+		return 0, 0, err
+	}
+	extraTime := totalTime % iterations
+	if extraTime != 0 {
+		extraTimeDuration, err :=
+			time.ParseDuration(strconv.Itoa(extraTime) + "s")
+		if err != nil {
+			return 0, 0, err
+		}
+		return iterationDuration, extraTimeDuration, nil
+	}
+	return iterationDuration, 0, nil
 }