Repository: code-ready/crc
Pull request #1793 (2020-12-22 10:21:29)

Add a controller in the cluster keeping in sync routes and hosts file
* The daemon has an API available in the VM: curl http://gateway/hosts/add or remove.
* A controller is running the cluster and calls the daemon API.

For Windows, in the future, the daemon will forward request to the adminhelper daemon. Currently, a UAC prompt is shown.

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

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

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

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

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

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

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
gbraad: This 'routes-controller' is a separate binary? What is it's function?
Please describe this more clearly in the description of the PR

The problem I see is that this becomes another binary we need to maintain the lifecycle of. How will this work? Who maintains this? Why can't the/a (slightly more generic) daemon handle this?

openshift-ci-robot: @guillaumerose: PR needs rebase.

<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.
</details>
cfergeau: Could the controller be running on the host since we have the crc daemon which is running in the background anyway?
guillaumerose: I have 4 choices in fact:
* Use this controller + API
* Make crc start run in the daemon so that I can know when the VM is started and kubeconfig is here. Then I can launch the controller
* Add events to the network stack so that, when a new VM connects, I try to run the controller
* Poll the kubeconfig file. When it appears, run the controller.

I guess the good solution is to make crc start the client of the daemon.
cfergeau: > I have 4 choices in fact:
> 
>     * Use this controller + API
> 
>     * Make crc start run in the daemon so that I can know when the VM is started and kubeconfig is here. Then I can launch the controller
> 
>     * Add events to the network stack so that, when a new VM connects, I try to run the controller
> 
>     * Poll the kubeconfig file. When it appears, run the controller.
> 
> 
> I guess the good solution is to make crc start the client of the daemon.

What I had in mind was to rework `machine.Start()` a bit:
```
        // Post-VM start
        vmState, err := host.Driver.GetState()
        if err != nil {                                                                                                                   
                return nil, errors.Wrap(err, "Error getting the state")                                                                  
        }
        if vmState != state.Running {
                return nil, errors.Wrap(err, "CodeReady Containers VM is not running")
        }
                
        instanceIP, err := getIP(host, client.useVSock())
        if err != nil {
                return 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, errors.Wrap(err, "Error creating the ssh client")
        }
        defer sshRunner.Close()
        
        logging.Debug("Waiting until ssh is available")
        if err := cluster.WaitForSSH(sshRunner); err != nil {
                return nil, errors.Wrap(err, "Failed to connect to the CRC VM with SSH -- host might be unreachable")
        }
        logging.Info("CodeReady Containers VM is running")
```
The `kubeconfig` file could be copied to `~/.crc/machines/crc` around that time, rather than being done later as being done now. And the host DNS configuration (`addOpenShiftHosts` / `runPostStartForOS`) could be also be done after checking the VM is up, but before waiting for SSH. In the vsock case, `addOpenShiftHosts` would talk to the daemon, starting it if necessary.
guillaumerose: After a long discussion, it seems that embedding this controller in the daemon is really complicated as machine.Start can be called from the crc CLI and from the daemon itself. 
I propose we start with this controller in a pod and then, when crc CLI will always be a client of the daemon, move the controller to the daemon.
guillaumerose: I extracted the controller code to https://github.com/code-ready/routes-controller and modified everything to get it working on Windows. 

UAC prompts only appear two times at `crc start` time and then each time a route is created/updated/deleted.

cc @gbraad 

gbraad: So a UAC prompt is given twice on start, and each time on a route? That sounds like a lot (too much).

guillaumerose: > twice on start, and each time on a route

Yes twice (clean + add the default ones) and then, when you add/update/delete a route, you get a prompt. 
Compared to now, this is just one eventual prompt if you run `oc new-app`. 

It will be 0 when the adminhelper will be a daemon. 

For now:
* I can disable this for Windows and let the user do it by hand,
* or keep as it is.
gbraad: > It will be 0 when the adminhelper will be a daemon.

That is what I am looking for. Can you outline the needed steps for this in a new issue or on the admin-helper issue?
Some estimation or timeline is appreciated. Especially to conclude if this should be merged or wait for to coincide with another possible follow-up.
guillaumerose: This is already good to take. It makes the cluster work fully. The debate for the daemon will be more intense for sure. Let's start with this.
openshift-ci-robot: @guillaumerose: PR needs rebase.

<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.
</details>
cfergeau: I rebased this in https://github.com/cfergeau/crc/tree/routescontroller but haven't tested this yet.
openshift-ci[bot]: @guillaumerose: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

<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: /retest

Patch:
diff --git a/Makefile b/Makefile
index 7f0d7ed8f..d6105dd18 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 SHELL := /bin/bash
 
-BUNDLE_VERSION = 4.7.5
+BUNDLE_VERSION = 4.7.7
 BUNDLE_EXTENSION = crcbundle
 CRC_VERSION = 1.25.0
 COMMIT_SHA=$(shell git rev-parse --short HEAD)
diff --git a/centos_ci.sh b/centos_ci.sh
index a60cbfadb..15763ebae 100644
--- a/centos_ci.sh
+++ b/centos_ci.sh
@@ -1,7 +1,7 @@
 #!/bin/bash
 
 # bundle location
-BUNDLE_VERSION=4.7.5
+BUNDLE_VERSION=4.7.7
 BUNDLE=crc_libvirt_$BUNDLE_VERSION.crcbundle
 GO_VERSION=1.14.13
 
diff --git a/cmd/crc/cmd/daemon.go b/cmd/crc/cmd/daemon.go
index e878ce3c0..7450fd887 100644
--- a/cmd/crc/cmd/daemon.go
+++ b/cmd/crc/cmd/daemon.go
@@ -1,6 +1,7 @@
 package cmd
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io/ioutil"
@@ -14,6 +15,7 @@ import (
 	"time"
 
 	cmdConfig "github.com/code-ready/crc/cmd/crc/cmd/config"
+	"github.com/code-ready/crc/pkg/crc/adminhelper"
 	"github.com/code-ready/crc/pkg/crc/api"
 	"github.com/code-ready/crc/pkg/crc/constants"
 	"github.com/code-ready/crc/pkg/crc/daemonclient"
@@ -147,6 +149,17 @@ func run(configuration *types.Configuration) error {
 		}
 	}()
 
+	ln, err := vn.Listen("tcp", fmt.Sprintf("%s:80", configuration.GatewayIP))
+	if err != nil {
+		return err
+	}
+	go func() {
+		mux := gatewayAPIMux()
+		if err := http.Serve(ln, mux); err != nil {
+			errCh <- err
+		}
+	}()
+
 	go func() {
 		mux := http.NewServeMux()
 		mux.Handle(types.ConnectPath, vn.Mux())
@@ -191,6 +204,40 @@ func run(configuration *types.Configuration) error {
 	}
 }
 
+// This API is only exposed in the virtual network (only the VM can reach this).
+// Any process inside the VM can reach it by connecting to gateway.crc.testing:80.
+func gatewayAPIMux() *http.ServeMux {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/hosts/add", func(w http.ResponseWriter, r *http.Request) {
+		acceptJSONStringArray(w, r, func(hostnames []string) error {
+			return adminhelper.AddToHostsFile("127.0.0.1", hostnames...)
+		})
+	})
+	mux.HandleFunc("/hosts/remove", func(w http.ResponseWriter, r *http.Request) {
+		acceptJSONStringArray(w, r, func(hostnames []string) error {
+			return adminhelper.RemoveFromHostsFile(hostnames...)
+		})
+	})
+	return mux
+}
+
+func acceptJSONStringArray(w http.ResponseWriter, r *http.Request, fun func(hostnames []string) error) {
+	if r.Method != http.MethodPost {
+		http.Error(w, "post only", http.StatusBadRequest)
+		return
+	}
+	var req []string
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	if err := fun(req); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	w.WriteHeader(http.StatusOK)
+}
+
 func runDaemon() error {
 	// Remove if an old socket is present
 	os.Remove(constants.DaemonSocketPath)
diff --git a/docs/source/topics/proc_troubleshooting-expired-certificates.adoc b/docs/source/topics/proc_troubleshooting-expired-certificates.adoc
index 92d51aada..28ea3bebb 100644
--- a/docs/source/topics/proc_troubleshooting-expired-certificates.adoc
+++ b/docs/source/topics/proc_troubleshooting-expired-certificates.adoc
@@ -1,20 +1,13 @@
 [id="troubleshooting-expired-certificates_{context}"]
 = Troubleshooting expired certificates
 
-[NOTE]
-====
-As of the {prod} 1.10.0 release, the certificate renewal process is not working as intended.
-Follow this procedure to avoid potential errors due to certificate expiration.
-====
-
 The system bundle in each released [command]`{bin}` executable expires 30 days after the release.
 This expiration is due to certificates embedded in the OpenShift cluster.
-As a result, using an older [command]`{bin}` executable or system bundle can result in an expired certificates error.
-
-Starting from {prod} 1.2.0, the embedded certificates can be automatically renewed by [command]`{bin}`.
-The [command]`{bin} start` command triggers the certificate renewal process when needed.
+The [command]`{bin} start` command triggers an automatic certificate renewal process when needed.
 Certificate renewal can add up to five minutes to the start time of the cluster.
 
+In order to avoid this additional startup time, or in case of failures in the certificate renewal process, use the following procedure:
+
 .Procedure
 
 To resolve expired certificate errors that cannot be automatically renewed:
diff --git a/docs/source/topics/ref_minimum-system-requirements.adoc b/docs/source/topics/ref_minimum-system-requirements.adoc
index 9c5e721c7..53317655a 100644
--- a/docs/source/topics/ref_minimum-system-requirements.adoc
+++ b/docs/source/topics/ref_minimum-system-requirements.adoc
@@ -8,7 +8,7 @@
 
 {prod} requires the following system resources:
 
-* 4 virtual CPUs (vCPUs)
+* 4 physical CPU cores
 * 9 GB of free memory
 * 35 GB of storage space
 
diff --git a/go.mod b/go.mod
index 0707ad5a0..5da1402d4 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
 	github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
 	github.com/cavaliercoder/grab v2.0.0+incompatible
 	github.com/cheggaaa/pb/v3 v3.0.5
+	github.com/code-ready/admin-helper v0.0.0-20210317085515-2d7f65e95985
 	github.com/code-ready/clicumber v0.0.0-20210201104241-cecb794bdf9a
 	github.com/code-ready/gvisor-tap-vsock v0.0.0-20210308122700-d61f9aac135c
 	github.com/code-ready/machine v0.0.0-20210122113819-281ccfbb4566
@@ -61,6 +62,7 @@ require (
 	gopkg.in/ini.v1 v1.62.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	k8s.io/api v0.20.1
+	k8s.io/apimachinery v0.20.1
 	k8s.io/client-go v0.20.1
 )
 
diff --git a/go.sum b/go.sum
index 92bfa2517..5a0ff0f8d 100644
--- a/go.sum
+++ b/go.sum
@@ -142,6 +142,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
 github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
 github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/code-ready/admin-helper v0.0.0-20210317085515-2d7f65e95985 h1:SWez+YUYr+AMTuqZFNxQ3L9KrG3ABNojr7+458Lwj5E=
+github.com/code-ready/admin-helper v0.0.0-20210317085515-2d7f65e95985/go.mod h1:4HSCLxIh62FmyT+XNNdPT7svzEcdcFvrlyzeUa1sqHY=
 github.com/code-ready/clicumber v0.0.0-20210201104241-cecb794bdf9a h1:VOQmCj9Y8pAPU4C0rIvF2ByUBuuqnaR9JSvb9C8uBQE=
 github.com/code-ready/clicumber v0.0.0-20210201104241-cecb794bdf9a/go.mod h1:ZPZUcYGpv/FQGnwakUuhiHP9x/IIcU2SKbn8xPe4ROc=
 github.com/code-ready/gvisor-tap-vsock v0.0.0-20210308122700-d61f9aac135c h1:UOg6w7Uv5hWvFVABjJpawCEmAK99vG96SozUaulqVCk=
@@ -222,6 +224,9 @@ github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zA
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc=
+github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
+github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
+github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
 github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v0.0.0-20180920194744-16128bbac47f/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -399,6 +404,8 @@ github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2
 github.com/gonum/mathext v0.0.0-20181121095525-8a4bf007ea55/go.mod h1:fmo8aiSEWkJeiGXUJf+sPvuDgEFgqIoZSs843ePKrGg=
 github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw=
 github.com/gonum/stat v0.0.0-20181125101827-41a0da705a5b/go.mod h1:Z4GIJBJO3Wa4gD4vbwQxXXZ+WHmW6E9ixmNrwvs0iZs=
+github.com/goodhosts/hostsfile v0.0.7 h1:5yBaORuv1dybDhDRju32bQQ1l4iHKJs+h6GIgFV4qJQ=
+github.com/goodhosts/hostsfile v0.0.7/go.mod h1:MAfdBdP0f9MVmfhmNP4EjQxPu7J/WnncHv8p/J8hkLs=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
diff --git a/pkg/crc/adminhelper/hosts.go b/pkg/crc/adminhelper/hosts.go
index a98670964..a88d9f177 100644
--- a/pkg/crc/adminhelper/hosts.go
+++ b/pkg/crc/adminhelper/hosts.go
@@ -3,6 +3,7 @@ package adminhelper
 import (
 	"path/filepath"
 
+	"github.com/code-ready/admin-helper/pkg/hosts"
 	"github.com/code-ready/crc/pkg/crc/constants"
 )
 
@@ -22,6 +23,19 @@ func UpdateHostsFile(instanceIP string, hostnames ...string) error {
 }
 
 func AddToHostsFile(instanceIP string, hostnames ...string) error {
+	hosts, err := hosts.New()
+	if err != nil {
+		return err
+	}
+	var filtered []string
+	for _, hostname := range hostnames {
+		if !hosts.Contains(instanceIP, hostname) {
+			filtered = append(filtered, hostname)
+		}
+	}
+	if len(filtered) == 0 {
+		return nil
+	}
 	return execute(append([]string{"add", instanceIP}, hostnames...)...)
 }
 
@@ -30,5 +44,5 @@ func RemoveFromHostsFile(hostnames ...string) error {
 }
 
 func CleanHostsFile() error {
-	return execute([]string{"clean", constants.ClusterDomain, constants.AppsDomain}...)
+	return execute("clean", constants.ClusterDomain, constants.AppsDomain)
 }
diff --git a/pkg/crc/machine/start.go b/pkg/crc/machine/start.go
index 928102bc1..e80e884bf 100644
--- a/pkg/crc/machine/start.go
+++ b/pkg/crc/machine/start.go
@@ -2,6 +2,7 @@ package machine
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -34,6 +35,8 @@ import (
 	"github.com/pkg/errors"
 
 	"golang.org/x/crypto/ssh"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 const minimumMemoryForMonitoring = 14336
@@ -416,6 +419,12 @@ func (client *client) Start(ctx context.Context, startConfig types.StartConfig)
 		return nil, errors.Wrap(err, "Failed to update cluster ID")
 	}
 
+	if client.useVSock() {
+		if err := ensureRoutesControllerIsRunning(sshRunner, ocConfig); err != nil {
+			return nil, err
+		}
+	}
+
 	if client.monitoringEnabled() {
 		logging.Info("Enabling cluster monitoring operator...")
 		if err := cluster.StartMonitoring(ocConfig); err != nil {
@@ -637,3 +646,37 @@ func logBundleDate(crcBundleMetadata *bundle.CrcBundleInfo) {
 		}
 	}
 }
+
+func ensureRoutesControllerIsRunning(sshRunner *crcssh.Runner, ocConfig oc.Config) error {
+	bin, err := json.Marshal(v1.Pod{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Pod",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "routes-controller",
+			Namespace: "openshift-ingress",
+		},
+		Spec: v1.PodSpec{
+			ServiceAccountName: "router",
+			Containers: []v1.Container{
+				{
+					Name:            "routes-controller",
+					Image:           "quay.io/crcont/routes-controller:6effc2f0304d3a11c1e22c278b10630e834d0220",
+					ImagePullPolicy: v1.PullIfNotPresent,
+				},
+			},
+		},
+	})
+	if err != nil {
+		return err
+	}
+	if err := sshRunner.CopyData(bin, "/tmp/routes-controller.json", 0444); err != nil {
+		return err
+	}
+	_, _, err = ocConfig.RunOcCommand("apply", "-f", "/tmp/routes-controller.json")
+	if err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/pkg/crc/preflight/preflight_checks_common.go b/pkg/crc/preflight/preflight_checks_common.go
index 235d7773d..f9608ff78 100644
--- a/pkg/crc/preflight/preflight_checks_common.go
+++ b/pkg/crc/preflight/preflight_checks_common.go
@@ -5,6 +5,7 @@ import (
 	"os"
 	"path/filepath"
 
+	"github.com/code-ready/crc/pkg/crc/adminhelper"
 	"github.com/code-ready/crc/pkg/crc/cache"
 	"github.com/code-ready/crc/pkg/crc/cluster"
 	"github.com/code-ready/crc/pkg/crc/constants"
@@ -55,6 +56,11 @@ var genericPreflightChecks = [...]Check{
 		cleanup:            removeCRCMachinesDir,
 		flags:              CleanUpOnly,
 	},
+	{
+		cleanupDescription: "Removing hosts file records added by CRC",
+		cleanup:            removeHostsFileEntry,
+		flags:              CleanUpOnly,
+	},
 	{
 		cleanupDescription: "Removing older logs",
 		cleanup:            removeOldLogs,
@@ -175,3 +181,11 @@ func removeOldLogs() error {
 	}
 	return nil
 }
+
+func removeHostsFileEntry() error {
+	err := adminhelper.CleanHostsFile()
+	if errors.Is(err, os.ErrNotExist) {
+		return nil
+	}
+	return err
+}
diff --git a/pkg/crc/preflight/preflight_checks_nonwin.go b/pkg/crc/preflight/preflight_checks_nonwin.go
index 77098d85c..2845e01ef 100644
--- a/pkg/crc/preflight/preflight_checks_nonwin.go
+++ b/pkg/crc/preflight/preflight_checks_nonwin.go
@@ -8,7 +8,6 @@ import (
 	"os"
 	"syscall"
 
-	"github.com/code-ready/crc/pkg/crc/adminhelper"
 	"github.com/code-ready/crc/pkg/crc/logging"
 	crcos "github.com/code-ready/crc/pkg/os"
 )
@@ -21,11 +20,6 @@ var nonWinPreflightChecks = [...]Check{
 		fixDescription:   "crc should not be run as root",
 		flags:            NoFix,
 	},
-	{
-		cleanupDescription: "Removing hosts file records added by CRC",
-		cleanup:            removeHostsFileEntry,
-		flags:              CleanUpOnly,
-	},
 }
 
 func checkIfRunningAsNormalUser() error {
@@ -67,11 +61,3 @@ func checkSuid(path string) error {
 
 	return nil
 }
-
-func removeHostsFileEntry() error {
-	err := adminhelper.CleanHostsFile()
-	if errors.Is(err, os.ErrNotExist) {
-		return nil
-	}
-	return err
-}
diff --git a/pkg/crc/services/dns/dns.go b/pkg/crc/services/dns/dns.go
index 4b9d8a80d..25a7138cc 100644
--- a/pkg/crc/services/dns/dns.go
+++ b/pkg/crc/services/dns/dns.go
@@ -124,5 +124,7 @@ func addOpenShiftHosts(serviceConfig services.ServicePostStartConfig) error {
 	return adminhelper.UpdateHostsFile(serviceConfig.IP, serviceConfig.BundleMetadata.GetAPIHostname(),
 		serviceConfig.BundleMetadata.GetAppHostname("oauth-openshift"),
 		serviceConfig.BundleMetadata.GetAppHostname("console-openshift-console"),
+		serviceConfig.BundleMetadata.GetAppHostname("downloads-openshift-console"),
+		serviceConfig.BundleMetadata.GetAppHostname("canary-openshift-ingress-canary"),
 		serviceConfig.BundleMetadata.GetAppHostname("default-route-openshift-image-registry"))
 }
diff --git a/vendor/github.com/code-ready/admin-helper/LICENSE b/vendor/github.com/code-ready/admin-helper/LICENSE
new file mode 100644
index 000000000..e21ed71c5
--- /dev/null
+++ b/vendor/github.com/code-ready/admin-helper/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 - Luther Monson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/vendor/github.com/code-ready/admin-helper/pkg/hosts/hosts.go b/vendor/github.com/code-ready/admin-helper/pkg/hosts/hosts.go
new file mode 100644
index 000000000..c0f2cd8e0
--- /dev/null
+++ b/vendor/github.com/code-ready/admin-helper/pkg/hosts/hosts.go
@@ -0,0 +1,158 @@
+package hosts
+
+import (
+	"fmt"
+	"regexp"
+	"sort"
+	"strings"
+
+	"github.com/goodhosts/hostsfile"
+)
+
+const (
+	// source https://github.com/kubernetes/apimachinery/blob/603e04655e9f537eb01238cdbce4891f832a4f27/pkg/util/validation/validation.go#L208
+	dns1123SubdomainRegexp = `[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*`
+	clusterDomain          = ".crc.testing"
+	appsDomain             = ".apps-crc.testing"
+)
+
+var (
+	clusterRegexp = regexp.MustCompile("^" + dns1123SubdomainRegexp + regexp.QuoteMeta(clusterDomain) + "$")
+	appRegexp     = regexp.MustCompile("^" + dns1123SubdomainRegexp + regexp.QuoteMeta(appsDomain) + "$")
+)
+
+type Hosts struct {
+	File       *hostsfile.Hosts
+	HostFilter func(string) bool
+}
+
+func New() (*Hosts, error) {
+	file, err := hostsfile.NewHosts()
+	if err != nil {
+		return nil, err
+	}
+
+	return &Hosts{
+		File:       &file,
+		HostFilter: defaultFilter,
+	}, nil
+}
+
+func defaultFilter(s string) bool {
+	return clusterRegexp.MatchString(s) || appRegexp.MatchString(s)
+}
+
+func (h *Hosts) Add(ip string, hosts []string) error {
+	if err := h.verifyHosts(hosts); err != nil {
+		return err
+	}
+
+	if err := h.checkIsWritable(); err != nil {
+		return err
+	}
+
+	uniqueHosts := map[string]bool{}
+	for i := 0; i < len(hosts); i++ {
+		uniqueHosts[hosts[i]] = true
+	}
+
+	var hostEntries []string
+	for key := range uniqueHosts {
+		hostEntries = append(hostEntries, key)
+	}
+
+	sort.Strings(hostEntries)
+
+	if err := h.File.Add(ip, hostEntries...); err != nil {
+		return err
+	}
+	return h.File.Flush()
+}
+
+func (h *Hosts) Remove(hosts []string) error {
+	if err := h.verifyHosts(hosts); err != nil {
+		return err
+	}
+
+	if err := h.checkIsWritable(); err != nil {
+		return err
+	}
+
+	uniqueHosts := map[string]bool{}
+	for i := 0; i < len(hosts); i++ {
+		uniqueHosts[hosts[i]] = true
+	}
+
+	var hostEntries []string
+	for key := range uniqueHosts {
+		hostEntries = append(hostEntries, key)
+	}
+
+	for _, host := range hostEntries {
+		if err := h.File.RemoveByHostname(host); err != nil {
+			return err
+		}
+	}
+	return h.File.Flush()
+}
+
+func (h *Hosts) Clean(rawSuffixes []string) error {
+	if err := h.checkIsWritable(); err != nil {
+		return err
+	}
+
+	var suffixes []string
+	for _, suffix := range rawSuffixes {
+		if !strings.HasPrefix(suffix, ".") {
+			return fmt.Errorf("suffix should start with a dot")
+		}
+		suffixes = append(suffixes, suffix)
+	}
+
+	var toDelete []string
+	for _, line := range h.File.Lines {
+		for _, host := range line.Hosts {
+			for _, suffix := range suffixes {
+				if strings.HasSuffix(host, suffix) {
+					toDelete = append(toDelete, host)
+					break
+				}
+			}
+		}
+	}
+
+	if err := h.verifyHosts(toDelete); err != nil {
+		return err
+	}
+
+	for _, host := range toDelete {
+		if err := h.File.RemoveByHostname(host); err != nil {
+			return err
+		}
+	}
+	return h.File.Flush()
+}
+
+func (h *Hosts) checkIsWritable() error {
+	if !h.File.IsWritable() {
+		return fmt.Errorf("host file not writable, try running with elevated privileges")
+	}
+	return nil
+}
+
+func (h *Hosts) Contains(ip, host string) bool {
+	if err := h.verifyHosts([]string{host}); err != nil {
+		return false
+	}
+
+	return h.File.Has(ip, host)
+}
+
+func (h *Hosts) verifyHosts(hosts []string) error {
+	for _, host := range hosts {
+		if !h.HostFilter(host) {
+			return fmt.Errorf("input %s rejected", host)
+		}
+	}
+	return nil
+}
diff --git a/vendor/github.com/dimchansky/utfbom/.gitignore b/vendor/github.com/dimchansky/utfbom/.gitignore
new file mode 100644
index 000000000..d7ec5cebb
--- /dev/null
+++ b/vendor/github.com/dimchansky/utfbom/.gitignore
@@ -0,0 +1,37 @@
+# Binaries for programs and plugins
+*.exe
+*.dll
+*.so
+*.dylib
+*.o
+*.a
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.prof
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
+.glide/
+
+# Gogland
+.idea/
\ No newline at end of file
diff --git a/vendor/github.com/dimchansky/utfbom/.travis.yml b/vendor/github.com/dimchansky/utfbom/.travis.yml
new file mode 100644
index 000000000..77be8b345
--- /dev/null
+++ b/vendor/github.com/dimchansky/utfbom/.travis.yml
@@ -0,0 +1,29 @@
+language: go
+sudo: false
+
+go:
+  - 1.10.x
+  - 1.11.x
+  - 1.12.x  
+  - 1.13.x
+  - 1.14.x
+  - 1.15.x
+
+cache:
+  directories:
+    - $HOME/.cache/go-build
+    - $HOME/gopath/pkg/mod
+
+env:
+  global:
+    - GO111MODULE=on
+
+before_install:
+  - go get github.com/mattn/goveralls
+  - go get golang.org/x/tools/cmd/cover
+  - go get golang.org/x/tools/cmd/goimports
+  - go get golang.org/x/lint/golint
+script:
+  - gofiles=$(find ./ -name '*.go') && [ -z "$gofiles" ] || unformatted=$(goimports -l $gofiles) && [ -z "$unformatted" ] || (echo >&2 "Go files must be formatted with gofmt. Following files has problem:\n $unformatted" && false)
+  - golint ./... # This won't break the build, just show warnings
+  - $HOME/gopath/bin/goveralls -service=travis-ci
diff --git a/vendor/github.com/dimchansky/utfbom/LICENSE b/vendor/github.com/dimchansky/utfbom/LICENSE
new file mode 100644
index 000000000..6279cb87f
--- /dev/null
+++ b/vendor/github.com/dimchansky/utfbom/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright (c) 2018-2020, Dmitrij Koniajev (dimchansky@gmail.com)
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/vendor/github.com/dimchansky/utfbom/README.md b/vendor/github.com/dimchansky/utfbom/README.md
new file mode 100644
index 000000000..8ece28008
--- /dev/null
+++ b/vendor/github.com/dimchansky/utfbom/README.md
@@ -0,0 +1,66 @@
+# utfbom [![Godoc](https://godoc.org/github.com/dimchansky/utfbom?status.png)](https://godoc.org/github.com/dimchansky/utfbom) [![License](https://img.shields.io/:license-apache-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Build Status](https://travis-ci.org/dimchansky/utfbom.svg?branch=master)](https://travis-ci.org/dimchansky/utfbom) [![Go Report Card](https://goreportcard.com/badge/github.com/dimchansky/utfbom)](https://goreportcard.com/report/github.com/dimchansky/utfbom) [![Coverage Status](https://coveralls.io/repos/github/dimchansky/utfbom/badge.svg?branch=master)](https://coveralls.io/github/dimchansky/utfbom?branch=master)
+
+The package utfbom implements the detection of the BOM (Unicode Byte Order Mark) and removing as necessary. It can also return the encoding detected by the BOM.
+
+## Installation
+
+    go get -u github.com/dimchansky/utfbom
+    
+## Example
+
+```go
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+
+	"github.com/dimchansky/utfbom"
+)
+
+func main() {
+	trySkip([]byte("\xEF\xBB\xBFhello"))
+	trySkip([]byte("hello"))
+}
+
+func trySkip(byteData []byte) {
+	fmt.Println("Input:", byteData)
+
+	// just skip BOM
+	output, err := ioutil.ReadAll(utfbom.SkipOnly(bytes.NewReader(byteData)))
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println("ReadAll with BOM skipping", output)
+
+	// skip BOM and detect encoding
+	sr, enc := utfbom.Skip(bytes.NewReader(byteData))
+	fmt.Printf("Detected encoding: %s\n", enc)
+	output, err = ioutil.ReadAll(sr)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println("ReadAll with BOM detection and skipping", output)
+	fmt.Println()
+}
+```
+
+Output:
+
+```
+$ go run main.go
+Input: [239 187 191 104 101 108 108 111]
+ReadAll with BOM skipping [104 101 108 108 111]
+Detected encoding: UTF8
+ReadAll with BOM detection and skipping [104 101 108 108 111]
+
+Input: [104 101 108 108 111]
+ReadAll with BOM skipping [104 101 108 108 111]
+Detected encoding: Unknown
+ReadAll with BOM detection and skipping [104 101 108 108 111]
+```
+
+
diff --git a/vendor/github.com/dimchansky/utfbom/go.mod b/vendor/github.com/dimchansky/utfbom/go.mod
new file mode 100644
index 000000000..8f8620af3
--- /dev/null
+++ b/vendor/github.com/dimchansky/utfbom/go.mod
@@ -0,0 +1,1 @@
+module github.com/dimchansky/utfbom
diff --git a/vendor/github.com/dimchansky/utfbom/utfbom.go b/vendor/github.com/dimchansky/utfbom/utfbom.go
new file mode 100644
index 000000000..77a303e56
--- /dev/null
+++ b/vendor/github.com/dimchansky/utfbom/utfbom.go
@@ -0,0 +1,192 @@
+// Package utfbom implements the detection of the BOM (Unicode Byte Order Mark) and removing as necessary.
+// It wraps an io.Reader object, creating another object (Reader) that also implements the io.Reader
+// interface but provides automatic BOM checking and removing as necessary.
+package utfbom
+
+import (
+	"errors"
+	"io"
+)
+
+// Encoding is type alias for detected UTF encoding.
+type Encoding int
+
+// Constants to identify detected UTF encodings.
+const (
+	// Unknown encoding, returned when no BOM was detected
+	Unknown Encoding = iota
+
+	// UTF8, BOM bytes: EF BB BF
+	UTF8
+
+	// UTF-16, big-endian, BOM bytes: FE FF
+	UTF16BigEndian
+
+	// UTF-16, little-endian, BOM bytes: FF FE
+	UTF16LittleEndian
+
+	// UTF-32, big-endian, BOM bytes: 00 00 FE FF
+	UTF32BigEndian
+
+	// UTF-32, little-endian, BOM bytes: FF FE 00 00
+	UTF32LittleEndian
+)
+
+// String returns a user-friendly string representation of the encoding. Satisfies fmt.Stringer interface.
+func (e Encoding) String() string {
+	switch e {
+	case UTF8:
+		return "UTF8"
+	case UTF16BigEndian:
+		return "UTF16BigEndian"
+	case UTF16LittleEndian:
+		return "UTF16LittleEndian"
+	case UTF32BigEndian:
+		return "UTF32BigEndian"
+	case UTF32LittleEndian:
+		return "UTF32LittleEndian"
+	default:
+		return "Unknown"
+	}
+}
+
+const maxConsecutiveEmptyReads = 100
+
+// Skip creates Reader which automatically detects BOM (Unicode Byte Order Mark) and removes it as necessary.
+// It also returns the encoding detected by the BOM.
+// If the detected encoding is not needed, you can call the SkipOnly function.
+func Skip(rd io.Reader) (*Reader, Encoding) {
+	// Is it already a Reader?
+	b, ok := rd.(*Reader)
+	if ok {
+		return b, Unknown
+	}
+
+	enc, left, err := detectUtf(rd)
+	return &Reader{
+		rd:  rd,
+		buf: left,
+		err: err,
+	}, enc
+}
+
+// SkipOnly creates Reader which automatically detects BOM (Unicode Byte Order Mark) and removes it as necessary.
+func SkipOnly(rd io.Reader) *Reader {
+	r, _ := Skip(rd)
+	return r
+}
+
+// Reader implements automatic BOM (Unicode Byte Order Mark) checking and
+// removing as necessary for an io.Reader object.
+type Reader struct {
+	rd  io.Reader // reader provided by the client
+	buf []byte    // buffered data
+	err error     // last error
+}
+
+// Read is an implementation of io.Reader interface.
+// The bytes are taken from the underlying Reader, but it checks for BOMs, removing them as necessary.
+func (r *Reader) Read(p []byte) (n int, err error) {
+	if len(p) == 0 {
+		return 0, nil
+	}
+
+	if r.buf == nil {
+		if r.err != nil {
+			return 0, r.readErr()
+		}
+
+		return r.rd.Read(p)
+	}
+
+	// copy as much as we can
+	n = copy(p, r.buf)
+	r.buf = nilIfEmpty(r.buf[n:])
+	return n, nil
+}
+
+func (r *Reader) readErr() error {
+	err := r.err
+	r.err = nil
+	return err
+}
+
+var errNegativeRead = errors.New("utfbom: reader returned negative count from Read")
+
+func detectUtf(rd io.Reader) (enc Encoding, buf []byte, err error) {
+	buf, err = readBOM(rd)
+
+	if len(buf) >= 4 {
+		if isUTF32BigEndianBOM4(buf) {
+			return UTF32BigEndian, nilIfEmpty(buf[4:]), err
+		}
+		if isUTF32LittleEndianBOM4(buf) {
+			return UTF32LittleEndian, nilIfEmpty(buf[4:]), err
+		}
+	}
+
+	if len(buf) > 2 && isUTF8BOM3(buf) {
+		return UTF8, nilIfEmpty(buf[3:]), err
+	}
+
+	if (err != nil && err != io.EOF) || (len(buf) < 2) {
+		return Unknown, nilIfEmpty(buf), err
+	}
+
+	if isUTF16BigEndianBOM2(buf) {
+		return UTF16BigEndian, nilIfEmpty(buf[2:]), err
+	}
+	if isUTF16LittleEndianBOM2(buf) {
+		return UTF16LittleEndian, nilIfEmpty(buf[2:]), err
+	}
+
+	return Unknown, nilIfEmpty(buf), err
+}
+
+func readBOM(rd io.Reader) (buf []byte, err error) {
+	const maxBOMSize = 4
+	var bom [maxBOMSize]byte // used to read BOM
+
+	// read as many bytes as possible
+	for nEmpty, n := 0, 0; err == nil && len(buf) < maxBOMSize; buf = bom[:len(buf)+n] {
+		if n, err = rd.Read(bom[len(buf):]); n < 0 {
+			panic(errNegativeRead)
+		}
+		if n > 0 {
+			nEmpty = 0
+		} else {
+			nEmpty++
+			if nEmpty >= maxConsecutiveEmptyReads {
+				err = io.ErrNoProgress
+			}
+		}
+	}
+	return
+}
+
+func isUTF32BigEndianBOM4(buf []byte) bool {
+	return buf[0] == 0x00 && buf[1] == 0x00 && buf[2] == 0xFE && buf[3] == 0xFF
+}
+
+func isUTF32LittleEndianBOM4(buf []byte) bool {
+	return buf[0] == 0xFF && buf[1] == 0xFE && buf[2] == 0x00 && buf[3] == 0x00
+}
+
+func isUTF8BOM3(buf []byte) bool {
+	return buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF
+}
+
+func isUTF16BigEndianBOM2(buf []byte) bool {
+	return buf[0] == 0xFE && buf[1] == 0xFF
+}
+
+func isUTF16LittleEndianBOM2(buf []byte) bool {
+	return buf[0] == 0xFF && buf[1] == 0xFE
+}
+
+func nilIfEmpty(buf []byte) (res []byte) {
+	if len(buf) > 0 {
+		res = buf
+	}
+	return
+}
diff --git a/vendor/github.com/goodhosts/hostsfile/.gitignore b/vendor/github.com/goodhosts/hostsfile/.gitignore
new file mode 100644
index 000000000..723ef36f4
--- /dev/null
+++ b/vendor/github.com/goodhosts/hostsfile/.gitignore
@@ -0,0 +1,1 @@
+.idea
\ No newline at end of file
diff --git a/vendor/github.com/goodhosts/hostsfile/LICENSE b/vendor/github.com/goodhosts/hostsfile/LICENSE
new file mode 100644
index 000000000..d9a10c0d8
--- /dev/null
+++ b/vendor/github.com/goodhosts/hostsfile/LICENSE
@@ -0,0 +1,176 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
diff --git a/vendor/github.com/goodhosts/hostsfile/Makefile b/vendor/github.com/goodhosts/hostsfile/Makefile
new file mode 100644
index 000000000..74b429609
--- /dev/null
+++ b/vendor/github.com/goodhosts/hostsfile/Makefile
@@ -0,0 +1,5 @@
+ci:
+	@goimports -l . || (goimports -d . && exit 1)
+	@golangci-lint run
+	@go test -v .
+.DEFAULT_GOAL := ci
\ No newline at end of file
diff --git a/vendor/github.com/goodhosts/hostsfile/const.go b/vendor/github.com/goodhosts/hostsfile/const.go
new file mode 100644
index 000000000..75b5d6928
--- /dev/null
+++ b/vendor/github.com/goodhosts/hostsfile/const.go
@@ -0,0 +1,9 @@
+// +build !windows
+
+package hostsfile
+
+const (
+	HostsPerLine  = -1 // unlimited
+	HostsFilePath = "/etc/hosts"
+	eol           = "\n"
+)
diff --git a/vendor/github.com/goodhosts/hostsfile/const_windows.go b/vendor/github.com/goodhosts/hostsfile/const_windows.go
new file mode 100644
index 000000000..9e526caa1
--- /dev/null
+++ b/vendor/github.com/goodhosts/hostsfile/const_windows.go
@@ -0,0 +1,7 @@
+package hostsfile
+
+const (
+	HostsFilePath = "${SystemRoot}/System32/drivers/etc/hosts"
+	HostsPerLine  = 9
+	eol           = "\r\n"
+)
diff --git a/vendor/github.com/goodhosts/hostsfile/go.mod b/vendor/github.com/goodhosts/hostsfile/go.mod
new file mode 100644
index 000000000..a2f729f2e
--- /dev/null
+++ b/vendor/github.com/goodhosts/hostsfile/go.mod
@@ -0,0 +1,5 @@
+module github.com/goodhosts/hostsfile
+
+go 1.13
+
+require github.com/dimchansky/utfbom v1.1.0
diff --git a/vendor/github.com/goodhosts/hostsfile/go.sum b/vendor/github.com/goodhosts/hostsfile/go.sum
new file mode 100644
index 000000000..b302f536f
--- /dev/null
+++ b/vendor/github.com/goodhosts/hostsfile/go.sum
@@ -0,0 +1,2 @@
+github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
+github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
diff --git a/vendor/github.com/goodhosts/hostsfile/hosts.go b/vendor/github.com/goodhosts/hostsfile/hosts.go
new file mode 100644
index 000000000..97c1b85ed
--- /dev/null
+++ b/vendor/github.com/goodhosts/hostsfile/hosts.go
@@ -0,0 +1,350 @@
+package hostsfile
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"net"
+	"os"
+	"path/filepath"
+	"sort"
+
+	"github.com/dimchansky/utfbom"
+)
+
+type Hosts struct {
+	Path  string
+	Lines []HostsLine
+}
+
+// Return a new instance of ``Hosts`` using the default hosts file path.
+func NewHosts() (Hosts, error) {
+	osHostsFilePath := os.ExpandEnv(filepath.FromSlash(HostsFilePath))
+
+	if env, isset := os.LookupEnv("HOSTS_PATH"); isset && len(env) > 0 {
+		osHostsFilePath = os.ExpandEnv(filepath.FromSlash(env))
+	}
+
+	return NewCustomHosts(osHostsFilePath)
+}
+
+// Return a new instance of ``Hosts`` using a custom hosts file path.
+func NewCustomHosts(osHostsFilePath string) (Hosts, error) {
+	hosts := Hosts{
+		Path: osHostsFilePath,
+	}
+
+	if err := hosts.Load(); err != nil {
+		return hosts, err
+	}
+
+	return hosts, nil
+}
+
+// Return ```true``` if hosts file is writable.
+func (h *Hosts) IsWritable() bool {
+	_, err := os.OpenFile(h.Path, os.O_WRONLY, 0660)
+	return err == nil
+}
+
+// Load the hosts file into ```l.Lines```.
+// ```Load()``` is called by ```NewHosts()``` and ```Hosts.Flush()``` so you
+// generally you won't need to call this yourself.
+func (h *Hosts) Load() error {
+	file, err := os.Open(h.Path)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(utfbom.SkipOnly(file))
+	for scanner.Scan() {
+		h.Lines = append(h.Lines, NewHostsLine(scanner.Text()))
+	}
+
+	if err := scanner.Err(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Flush any changes made to hosts file.
+func (h Hosts) Flush() error {
+	file, err := os.Create(h.Path)
+	if err != nil {
+		return err
+	}
+
+	defer file.Close()
+
+	w := bufio.NewWriter(file)
+
+	for _, line := range h.Lines {
+		if _, err := fmt.Fprintf(w, "%s%s", line.ToRaw(), eol); err != nil {
+			return err
+		}
+	}
+
+	err = w.Flush()
+	if err != nil {
+		return err
+	}
+
+	return h.Load()
+}
+
+// Add an entry to the hosts file.
+func (h *Hosts) Add(ip string, hosts ...string) error {
+	if net.ParseIP(ip) == nil {
+		return fmt.Errorf("%q is an invalid IP address", ip)
+	}
+
+	position := h.getIpPosition(ip)
+	if position == -1 {
+		// ip not already in hostsfile
+		h.Lines = append(h.Lines, HostsLine{
+			Raw:   buildRawLine(ip, hosts),
+			IP:    ip,
+			Hosts: hosts,
+		})
+	} else {
+		// add new hosts to the correct position for the ip
+		hostsCopy := h.Lines[position].Hosts
+		for _, addHost := range hosts {
+			if itemInSlice(addHost, hostsCopy) {
+				continue // host exists for ip already
+			}
+
+			hostsCopy = append(hostsCopy, addHost)
+		}
+		h.Lines[position].Hosts = hostsCopy
+		h.Lines[position].Raw = h.Lines[position].ToRaw() // reset raw
+	}
+
+	return nil
+}
+
+// merge dupelicate ips and hosts per ip
+func (h *Hosts) Clean() {
+	h.RemoveDuplicateIps()
+	for pos, line := range h.Lines {
+		line.RemoveDuplicateHosts()
+		line.SortHosts()
+		h.Lines[pos] = line
+	}
+	h.SortByIp()
+	h.HostsPerLine(HostsPerLine)
+}
+
+// Return a bool if ip/host combo in hosts file.
+func (h Hosts) Has(ip string, host string) bool {
+	return h.getHostPosition(ip, host) != -1
+}
+
+// Return a bool if hostname in hosts file.
+func (h Hosts) HasHostname(host string) bool {
+	return h.getHostnamePosition(host) != -1
+}
+
+func (h Hosts) HasIp(ip string) bool {
+	return h.getIpPosition(ip) != -1
+}
+
+// Remove an entry from the hosts file.
+func (h *Hosts) Remove(ip string, hosts ...string) error {
+	var outputLines []HostsLine
+	if net.ParseIP(ip) == nil {
+		return fmt.Errorf("%q is an invalid IP address", ip)
+	}
+
+	for _, line := range h.Lines {
+		// Bad lines or comments just get readded.
+		if line.Err != nil || line.IsComment() || line.IP != ip {
+			outputLines = append(outputLines, line)
+			continue
+		}
+
+		var newHosts []string
+		for _, checkHost := range line.Hosts {
+			if !itemInSlice(checkHost, hosts) {
+				newHosts = append(newHosts, checkHost)
+			}
+		}
+
+		// If hosts is empty, skip the line completely.
+		if len(newHosts) > 0 {
+			newLineRaw := line.IP
+
+			for _, host := range newHosts {
+				newLineRaw = fmt.Sprintf("%s %s", newLineRaw, host)
+			}
+			newLine := NewHostsLine(newLineRaw)
+			outputLines = append(outputLines, newLine)
+		}
+	}
+
+	h.Lines = outputLines
+	return nil
+}
+
+// Remove  entries by hostname from the hosts file.
+func (h *Hosts) RemoveByHostname(host string) error {
+	pos := h.getHostnamePosition(host)
+	for pos > -1 {
+		if len(h.Lines[pos].Hosts) > 1 {
+			h.Lines[pos].Hosts = removeFromSlice(host, h.Lines[pos].Hosts)
+			h.Lines[pos].RegenRaw()
+		} else {
+			h.removeByPosition(pos)
+		}
+		pos = h.getHostnamePosition(host)
+	}
+
+	return nil
+}
+
+func (h *Hosts) RemoveByIp(ip string) error {
+	pos := h.getIpPosition(ip)
+	for pos > -1 {
+		h.removeByPosition(pos)
+		pos = h.getIpPosition(ip)
+	}
+
+	return nil
+}
+
+func (h *Hosts) RemoveDuplicateIps() {
+	ipCount := make(map[string]int)
+	for _, line := range h.Lines {
+		ipCount[line.IP]++
+	}
+	for ip, count := range ipCount {
+		if count > 1 {
+			h.combineIp(ip)
+		}
+	}
+}
+
+// convert to net.IP and byte.Compare
+func (h *Hosts) SortByIp() {
+	sortedIps := make([]net.IP, 0, len(h.Lines))
+	for _, l := range h.Lines {
+		sortedIps = append(sortedIps, net.ParseIP(l.IP))
+	}
+	sort.Slice(sortedIps, func(i, j int) bool {
+		return bytes.Compare(sortedIps[i], sortedIps[j]) < 0
+	})
+
+	var sortedLines []HostsLine
+	for _, ip := range sortedIps {
+		for _, l := range h.Lines {
+			if ip.String() == l.IP {
+				sortedLines = append(sortedLines, l)
+			}
+		}
+	}
+	h.Lines = sortedLines
+}
+
+func (h *Hosts) HostsPerLine(count int) {
+	if count <= 0 {
+		return
+	}
+	var newLines []HostsLine
+	for _, line := range h.Lines {
+		if len(line.Hosts) <= count {
+			newLines = append(newLines, line)
+			continue
+		}
+
+		for i := 0; i < len(line.Hosts); i += count {
+			lineCopy := line
+			end := len(line.Hosts)
+			if end > i+count {
+				end = i + count
+			}
+			lineCopy.Hosts = line.Hosts[i:end]
+			lineCopy.Raw = lineCopy.ToRaw()
+			newLines = append(newLines, lineCopy)
+		}
+	}
+	h.Lines = newLines
+}
+
+func (h *Hosts) combineIp(ip string) {
+	newLine := HostsLine{
+		IP: ip,
+	}
+
+	linesCopy := make([]HostsLine, len(h.Lines))
+	copy(linesCopy, h.Lines)
+	for _, line := range linesCopy {
+		if line.IP == ip {
+			newLine.Combine(line)
+		}
+	}
+	newLine.SortHosts()
+	h.removeIp(ip)
+	h.Lines = append(h.Lines, newLine)
+}
+
+func (h *Hosts) removeByPosition(pos int) {
+	if len(h.Lines) == 1 {
+		h.Lines = []HostsLine{}
+		return
+	}
+	if pos == len(h.Lines) {
+		h.Lines = h.Lines[:pos-1]
+		return
+	}
+	h.Lines = append(h.Lines[:pos], h.Lines[pos+1:]...)
+}
+
+func (h *Hosts) removeIp(ip string) {
+	var newLines []HostsLine
+	for _, line := range h.Lines {
+		if line.IP != ip {
+			newLines = append(newLines, line)
+		}
+	}
+
+	h.Lines = newLines
+}
+
+func (h Hosts) getHostPosition(ip string, host string) int {
+	for i := range h.Lines {
+		line := h.Lines[i]
+		if !line.IsComment() && line.Raw != "" {
+			if ip == line.IP && itemInSlice(host, line.Hosts) {
+				return i
+			}
+		}
+	}
+
+	return -1
+}
+
+func (h Hosts) getHostnamePosition(host string) int {
+	for i := range h.Lines {
+		line := h.Lines[i]
+		if !line.IsComment() && line.Raw != "" {
+			if itemInSlice(host, line.Hosts) {
+				return i
+			}
+		}
+	}
+
+	return -1
+}
+
+func (h Hosts) getIpPosition(ip string) int {
+	for i := range h.Lines {
+		line := h.Lines[i]
+		if !line.IsComment() && line.Raw != "" && line.IP == ip {
+			return i
+		}
+	}
+
+	return -1
+}
diff --git a/vendor/github.com/goodhosts/hostsfile/hostsline.go b/vendor/github.com/goodhosts/hostsfile/hostsline.go
new file mode 100644
index 000000000..f5f3765df
--- /dev/null
+++ b/vendor/github.com/goodhosts/hostsfile/hostsline.go
@@ -0,0 +1,108 @@
+package hostsfile
+
+import (
+	"fmt"
+	"net"
+	"sort"
+	"strings"
+)
+
+type HostsLine struct {
+	IP      string
+	Hosts   []string
+	Raw     string
+	Err     error
+	Comment string
+}
+
+const commentChar string = "#"
+
+// Return a new instance of ```HostsLine```.
+func NewHostsLine(raw string) HostsLine {
+	output := HostsLine{Raw: raw}
+	if output.IsComment() { //whole line is comment
+		return output
+	}
+
+	if output.HasComment() { //trailing comment
+		commentSplit := strings.Split(output.Raw, commentChar)
+		raw = commentSplit[0]
+		output.Comment = commentSplit[1]
+	}
+
+	fields := strings.Fields(raw)
+	if len(fields) == 0 {
+		return output
+	}
+
+	rawIP := fields[0]
+	if net.ParseIP(rawIP) == nil {
+		output.Err = fmt.Errorf("bad hosts line: %q", raw)
+	}
+
+	output.IP = rawIP
+	output.Hosts = fields[1:]
+
+	return output
+}
+
+func (l *HostsLine) ToRaw() string {
+	var comment string
+	if l.IsComment() { //Whole line is comment
+		return l.Raw
+	}
+
+	if l.Comment != "" {
+		comment = fmt.Sprintf(" %s%s", commentChar, l.Comment)
+	}
+
+	return fmt.Sprintf("%s %s%s", l.IP, strings.Join(l.Hosts, " "), comment)
+}
+
+func (l *HostsLine) RemoveDuplicateHosts() {
+	unique := make(map[string]struct{})
+	for _, h := range l.Hosts {
+		unique[h] = struct{}{}
+	}
+
+	l.Hosts = []string{}
+	for k := range unique {
+		l.Hosts = append(l.Hosts, k)
+	}
+	l.Raw = l.ToRaw()
+}
+
+func (l *HostsLine) Combine(hostline HostsLine) {
+	l.Hosts = append(l.Hosts, hostline.Hosts...)
+	if l.Comment == "" {
+		l.Comment = hostline.Comment
+	} else {
+		l.Comment = fmt.Sprintf("%s %s", l.Comment, hostline.Comment)
+	}
+	l.Raw = l.ToRaw()
+}
+
+func (l *HostsLine) SortHosts() {
+	sort.Strings(l.Hosts)
+	l.Raw = l.ToRaw()
+}
+
+func (l *HostsLine) IsComment() bool {
+	return strings.HasPrefix(strings.TrimSpace(l.Raw), commentChar)
+}
+
+func (l *HostsLine) HasComment() bool {
+	return strings.Contains(l.Raw, commentChar)
+}
+
+func (l *HostsLine) IsValid() bool {
+	return l.IP != ""
+}
+
+func (l *HostsLine) IsMalformed() bool {
+	return l.Err != nil
+}
+
+func (l *HostsLine) RegenRaw() {
+	l.Raw = fmt.Sprintf("%s %s", l.IP, strings.Join(l.Hosts, " "))
+}
diff --git a/vendor/github.com/goodhosts/hostsfile/make.bat b/vendor/github.com/goodhosts/hostsfile/make.bat
new file mode 100644
index 000000000..c4f756618
--- /dev/null
+++ b/vendor/github.com/goodhosts/hostsfile/make.bat
@@ -0,0 +1,27 @@
+@echo off
+SETLOCAL ENABLEDELAYEDEXPANSION
+set LF=^
+
+echo %1%
+if "%1%"=="" (
+    set cmd=ci
+) else (
+    set cmd=%1%
+)
+
+if "%cmd%" == "ci" (
+  for /F %%i in ('goimports -l .') do (
+    set "line=%%i"
+    set goimports=%goimports%!line!!LF!
+  )
+
+  if not "!goimports!" == "" (
+    goimports -d .
+    goto :eof
+  )
+
+  golangci-lint run || goto :eof
+  go test -v . || goto :eof
+
+  goto :eof
+)
diff --git a/vendor/github.com/goodhosts/hostsfile/utils.go b/vendor/github.com/goodhosts/hostsfile/utils.go
new file mode 100644
index 000000000..30c676a0a
--- /dev/null
+++ b/vendor/github.com/goodhosts/hostsfile/utils.go
@@ -0,0 +1,43 @@
+package hostsfile
+
+import (
+	"fmt"
+)
+
+func itemInSlice(item string, list []string) bool {
+	for _, i := range list {
+		if i == item {
+			return true
+		}
+	}
+
+	return false
+}
+
+func removeFromSlice(s string, slice []string) []string {
+	for key, value := range slice {
+		if value == s {
+			return append(slice[:key], slice[key+1:]...)
+		}
+	}
+	return nil
+}
+
+//func sliceContainsItem(item string, list []string) bool {
+//	for _, i := range list {
+//		if strings.Contains(i, item) {
+//			return true
+//		}
+//	}
+//
+//	return false
+//}
+
+func buildRawLine(ip string, hosts []string) string {
+	output := ip
+	for _, host := range hosts {
+		output = fmt.Sprintf("%s %s", output, host)
+	}
+
+	return output
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index ce45ac996..3c4c7fe6d 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -32,6 +32,9 @@ github.com/cavaliercoder/grab
 ## explicit
 github.com/cheggaaa/pb/v3
 github.com/cheggaaa/pb/v3/termutil
+# github.com/code-ready/admin-helper v0.0.0-20210317085515-2d7f65e95985
+## explicit
+github.com/code-ready/admin-helper/pkg/hosts
 # github.com/code-ready/clicumber v0.0.0-20210201104241-cecb794bdf9a
 ## explicit
 github.com/code-ready/clicumber/testsuite
@@ -67,6 +70,8 @@ github.com/cucumber/messages-go/v10
 github.com/danieljoos/wincred
 # github.com/davecgh/go-spew v1.1.1
 github.com/davecgh/go-spew/spew
+# github.com/dimchansky/utfbom v1.1.1
+github.com/dimchansky/utfbom
 # github.com/docker/docker v17.12.0-ce-rc1.0.20200916142827-bd33bbf0497b+incompatible => github.com/docker/docker v1.4.2-0.20191121165722-d1d5f6476656
 github.com/docker/docker/pkg/term
 github.com/docker/docker/pkg/term/windows
@@ -93,6 +98,8 @@ github.com/gogo/protobuf/proto
 github.com/gogo/protobuf/sortkeys
 # github.com/golang/protobuf v1.4.3
 github.com/golang/protobuf/proto
+# github.com/goodhosts/hostsfile v0.0.7
+github.com/goodhosts/hostsfile
 # github.com/google/btree v1.0.0
 github.com/google/btree
 # github.com/google/go-cmp v0.5.4
@@ -455,6 +462,7 @@ gvisor.dev/gvisor/pkg/waiter
 k8s.io/api/certificates/v1beta1
 k8s.io/api/core/v1
 # k8s.io/apimachinery v0.20.1 => github.com/openshift/kubernetes-apimachinery v0.0.0-20210108114224-194a87c5b03a
+## explicit
 k8s.io/apimachinery/pkg/api/errors
 k8s.io/apimachinery/pkg/api/resource
 k8s.io/apimachinery/pkg/apis/meta/v1
diff --git a/verify-vendor.sh b/verify-vendor.sh
index 4817308a8..882070818 100755
--- a/verify-vendor.sh
+++ b/verify-vendor.sh
@@ -29,7 +29,7 @@ make vendor
 
 echo "Diffing ${REPO_ROOT_DIR} against freshly generated codegen"
 ret=0
-diff -Naupr "${REPO_ROOT_DIR}/vendor" "${TMP_DIFFROOT}/vendor" || ret=1
+diff --strip-trailing-cr -Naupr "${REPO_ROOT_DIR}/vendor" "${TMP_DIFFROOT}/vendor" || ret=1
 
 # Restore working tree state
 rm -fr "${REPO_ROOT_DIR}/vendor"


Test patch:
diff --git a/pkg/crc/preflight/preflight_linux_test.go b/pkg/crc/preflight/preflight_linux_test.go
index a54bc1805..6dab6ea17 100644
--- a/pkg/crc/preflight/preflight_linux_test.go
+++ b/pkg/crc/preflight/preflight_linux_test.go
@@ -63,11 +63,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: true,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
@@ -96,11 +96,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: false,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
@@ -128,11 +128,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: false,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
@@ -154,11 +154,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: true,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
@@ -187,11 +187,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: false,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
@@ -219,11 +219,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: false,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
@@ -245,11 +245,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: true,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
@@ -278,11 +278,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: false,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
@@ -310,11 +310,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: false,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
@@ -336,11 +336,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: true,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
@@ -370,11 +370,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: false,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
@@ -403,11 +403,11 @@ var checkListForDistros = []checkListForDistro{
 		systemdResolved: false,
 		checks: []Check{
 			{check: checkIfRunningAsNormalUser},
-			{cleanup: removeHostsFileEntry},
 			{check: checkPodmanExecutableCached},
 			{check: checkAdminHelperExecutableCached},
 			{configKeySuffix: "check-ram"},
 			{cleanup: removeCRCMachinesDir},
+			{cleanup: removeHostsFileEntry},
 			{cleanup: removeOldLogs},
 			{cleanup: cluster.ForgetPullSecret},
 			{check: checkVirtualizationEnabled},
diff --git a/pkg/crc/preflight/preflight_windows_test.go b/pkg/crc/preflight/preflight_windows_test.go
index 77724abe0..40ba6336c 100644
--- a/pkg/crc/preflight/preflight_windows_test.go
+++ b/pkg/crc/preflight/preflight_windows_test.go
@@ -15,9 +15,9 @@ func TestCountConfigurationOptions(t *testing.T) {
 }
 
 func TestCountPreflights(t *testing.T) {
-	assert.Len(t, getPreflightChecks(false, false, network.DefaultMode), 16)
-	assert.Len(t, getPreflightChecks(true, true, network.DefaultMode), 19)
+	assert.Len(t, getPreflightChecks(false, false, network.DefaultMode), 17)
+	assert.Len(t, getPreflightChecks(true, true, network.DefaultMode), 20)
 
-	assert.Len(t, getPreflightChecks(false, false, network.VSockMode), 17)
-	assert.Len(t, getPreflightChecks(true, true, network.VSockMode), 20)
+	assert.Len(t, getPreflightChecks(false, false, network.VSockMode), 18)
+	assert.Len(t, getPreflightChecks(true, true, network.VSockMode), 21)
 }