Repository: code-ready/crc
Pull request #2174 (2021-03-31 14:46:01)

Parse additionalFiles in crc-bundle-info.json
Newer crc versions hardcode that a bundle contain an 'oc' binary, which is not
always the case if it's handling an older bundle. 
https://github.com/code-ready/snc/pull/380 adds an explicit mention of the
presence of 'oc' in the bundle. This pr is its crc counterpart.




**Fixes:** Issue #2145

## Testing

What is the _bottom-line_ functionality that needs testing? Describe in pseudo-code or in English. Use verifiable statements that tie your changes to existing functionality.

1. Run `crc start` with crc 1.17 or older
2. Run `crc start` with latest git
3. `Bundle 'crc_libvirt_4.7.2.crcbundle' was requested, but the existing VM is using 'crc_libvirt_4.5.14.crcbundle'` should be printed rather than `Error loading bundle metadata: oc not found in bundle`

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

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

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

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

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

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

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
cfergeau: /unhold
Need to fix `make check` on travis, but should be fine otherwise.
openshift-ci[bot]: @cfergeau: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
praveenkumar: https://github.com/code-ready/crc/pull/2174/commits/d7153a8552d3fb77b4ef234891c2bbc7e927c9ae doesn't this  should be part of previous commit? 
praveenkumar: appveyor failing with following.

```
time="2021-03-31T20:19:34Z" level=info msg="bundle: &{1.0 snc {2020-10-26T04:48:26+00:00 ./openshift-install v4.6.0\nbuilt from commit ebdbda57fc18d3b73e69f0f2cc499ddfca7e6593\nrelease image registry.svc.ci.openshift.org/origin/release:4.5 git4.1.14-137-g14e7} {4.6.1 crc testing apps-crc.testing id_ecdsa_crc kubeconfig kubeadmin-password } [{[master worker] crc-h66l2-master-0 crc.qcow2    192.168.126.11}] {[{{crc.qcow2 9 245a0e5acd4f09000a9a5f37d731082ed1cf3fdcad1b5320cbe9b153c9fd82a4} qcow2}] [{{oc 72728632 983f0883a6dffd601afa663d10161bfd8033fd6d45cf587a9cb22e9a681d6047} oc-executable}]} {libvirt} C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\repo258962051\\crc_libvirt_4.6.1 crc_libvirt_4.6.1.crcbundle}"
time="2021-03-31T20:19:34Z" level=info msg="Could not find 'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\repo258962051\\crc_libvirt_4.6.1\\oc'"
--- FAIL: TestUse (0.05s)
    repository_test.go:33: 
        	Error Trace:	repository_test.go:33
        	Error:      	Received unexpected error:
        	            	oc not found in bundle
        	Test:       	TestUse
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
	panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0xa92daf]
```
openshift-ci[bot]: @cfergeau: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<details>

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

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

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

<details>

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

Patch:
diff --git a/go.mod b/go.mod
index dfa0f312c..c6fbbaaa4 100644
--- a/go.mod
+++ b/go.mod
@@ -20,6 +20,7 @@ require (
 	github.com/h2non/filetype v1.1.2-0.20210202002709-95e28344e08c
 	github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95
 	github.com/imdario/mergo v0.3.11 // indirect
+	github.com/jinzhu/copier v0.2.8
 	github.com/klauspost/compress v1.11.7
 	github.com/libvirt/libvirt-go-xml v6.10.0+incompatible
 	github.com/magiconair/properties v1.8.4 // indirect
diff --git a/go.sum b/go.sum
index 005a97b07..c2a742c95 100644
--- a/go.sum
+++ b/go.sum
@@ -511,6 +511,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
 github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jinzhu/copier v0.2.8 h1:N8MbL5niMwE3P4dOwurJixz5rMkKfujmMRFmAanSzWE=
+github.com/jinzhu/copier v0.2.8/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
diff --git a/pkg/crc/machine/bundle/metadata.go b/pkg/crc/machine/bundle/metadata.go
index e4f874178..92f614697 100644
--- a/pkg/crc/machine/bundle/metadata.go
+++ b/pkg/crc/machine/bundle/metadata.go
@@ -54,16 +54,26 @@ type Node struct {
 }
 
 type Storage struct {
-	DiskImages []DiskImage `json:"diskImages"`
+	DiskImages []DiskImage    `json:"diskImages"`
+	Files      []FileListItem `json:"fileList"`
 }
 
-type DiskImage struct {
+type File struct {
 	Name     string `json:"name"`
-	Format   string `json:"format"`
 	Size     string `json:"size"`
 	Checksum string `json:"sha256sum"`
 }
 
+type DiskImage struct {
+	File
+	Format string `json:"format"`
+}
+
+type FileListItem struct {
+	File
+	Type string `json:"type"`
+}
+
 type DriverInfo struct {
 	Name string `json:"name"`
 }
@@ -92,6 +102,16 @@ func (bundle *CrcBundleInfo) GetKubeConfigPath() string {
 	return bundle.resolvePath(bundle.ClusterInfo.KubeConfig)
 }
 
+func (bundle *CrcBundleInfo) GetOcPath() string {
+	for _, file := range bundle.Storage.Files {
+		if file.Type == "oc-executable" {
+			return bundle.resolvePath(file.Name)
+		}
+	}
+
+	return ""
+}
+
 func (bundle *CrcBundleInfo) GetSSHKeyPath() string {
 	return bundle.resolvePath(bundle.ClusterInfo.SSHPrivateKeyFile)
 }
@@ -142,7 +162,7 @@ func (bundle *CrcBundleInfo) GetOpenshiftVersion() string {
 
 func (bundle *CrcBundleInfo) verify() error {
 	files := []string{
-		bundle.resolvePath(constants.OcExecutableName),
+		bundle.GetOcPath(),
 		bundle.resolvePath(bundle.ClusterInfo.KubeadminPasswordFile),
 		bundle.GetKubeConfigPath(),
 		bundle.GetSSHKeyPath(),
diff --git a/pkg/crc/machine/bundle/repository.go b/pkg/crc/machine/bundle/repository.go
index 9dde4846c..db688f000 100644
--- a/pkg/crc/machine/bundle/repository.go
+++ b/pkg/crc/machine/bundle/repository.go
@@ -88,7 +88,16 @@ func (repo *Repository) Use(bundleName string) (*CrcBundleInfo, error) {
 }
 
 func (bundle *CrcBundleInfo) createSymlinkOrCopyOpenShiftClient(ocBinDir string) error {
-	ocInBundle := bundle.resolvePath(constants.OcExecutableName)
+	ocInBundle := bundle.GetOcPath()
+	// For backward-compatibility with bundles which shipped an oc binary in the bundle
+	// before fileList was added to crc-bundle-info.json
+	if ocInBundle == "" {
+		ocInBundle = bundle.resolvePath(constants.OcExecutableName)
+		if !crcos.FileExists(ocInBundle) {
+			// This is not an error, bundles for OpenShift 4.5 and older did not contain oc
+			return nil
+		}
+	}
 	ocInBinDir := filepath.Join(ocBinDir, constants.OcExecutableName)
 
 	// this is needed when upgrading from crc versions anterior to commit 1.11.0~5
@@ -121,9 +130,11 @@ func (repo *Repository) Extract(path string) error {
 		return err
 	}
 
-	bundleDir := strings.TrimSuffix(bundleName, bundleExtension)
+	bundleBaseDir := strings.TrimSuffix(bundleName, bundleExtension)
+	bundleDir := filepath.Join(repo.CacheDir, bundleBaseDir)
+	_ = os.RemoveAll(bundleDir)
 	return crcerrors.RetryAfter(time.Minute, func() error {
-		if err := os.Rename(filepath.Join(tmpDir, bundleDir), filepath.Join(repo.CacheDir, bundleDir)); err != nil {
+		if err := os.Rename(filepath.Join(tmpDir, bundleBaseDir), bundleDir); err != nil {
 			return &crcerrors.RetriableError{Err: err}
 		}
 		return nil
diff --git a/vendor/github.com/jinzhu/copier/License b/vendor/github.com/jinzhu/copier/License
new file mode 100644
index 000000000..e2dc5381e
--- /dev/null
+++ b/vendor/github.com/jinzhu/copier/License
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Jinzhu
+
+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/jinzhu/copier/README.md b/vendor/github.com/jinzhu/copier/README.md
new file mode 100644
index 000000000..cff72405c
--- /dev/null
+++ b/vendor/github.com/jinzhu/copier/README.md
@@ -0,0 +1,131 @@
+# Copier
+
+  I am a copier, I copy everything from one to another
+
+[![test status](https://github.com/jinzhu/copier/workflows/tests/badge.svg?branch=master "test status")](https://github.com/jinzhu/copier/actions)
+
+## Features
+
+* Copy from field to field with same name
+* Copy from method to field with same name
+* Copy from field to method with same name
+* Copy from slice to slice
+* Copy from struct to slice
+* Copy from map to map
+* Enforce copying a field with a tag
+* Ignore a field with a tag
+* Deep Copy
+
+## Usage
+
+```go
+package main
+
+import (
+	"fmt"
+	"github.com/jinzhu/copier"
+)
+
+type User struct {
+	Name string
+	Role string
+	Age  int32
+
+	// Explicitly ignored in the destination struct.
+	Salary   int
+}
+
+func (user *User) DoubleAge() int32 {
+	return 2 * user.Age
+}
+
+// Tags in the destination Struct provide instructions to copier.Copy to ignore
+// or enforce copying and to panic or return an error if a field was not copied.
+type Employee struct {
+	// Tell copier.Copy to panic if this field is not copied.
+	Name      string `copier:"must"`
+
+	// Tell copier.Copy to return an error if this field is not copied.
+	Age       int32  `copier:"must,nopanic"`
+
+	// Tell copier.Copy to explicitly ignore copying this field.
+	Salary    int    `copier:"-"`
+
+	DoubleAge int32
+	EmployeId int64
+	SuperRole string
+}
+
+func (employee *Employee) Role(role string) {
+	employee.SuperRole = "Super " + role
+}
+
+func main() {
+	var (
+		user      = User{Name: "Jinzhu", Age: 18, Role: "Admin", Salary: 200000}
+		users     = []User{{Name: "Jinzhu", Age: 18, Role: "Admin", Salary: 100000}, {Name: "jinzhu 2", Age: 30, Role: "Dev", Salary: 60000}}
+		employee  = Employee{Salary: 150000}
+		employees = []Employee{}
+	)
+
+	copier.Copy(&employee, &user)
+
+	fmt.Printf("%#v \n", employee)
+	// Employee{
+	//    Name: "Jinzhu",           // Copy from field
+	//    Age: 18,                  // Copy from field
+	//    Salary:150000,            // Copying explicitly ignored
+	//    DoubleAge: 36,            // Copy from method
+	//    EmployeeId: 0,            // Ignored
+	//    SuperRole: "Super Admin", // Copy to method
+	// }
+
+	// Copy struct to slice
+	copier.Copy(&employees, &user)
+
+	fmt.Printf("%#v \n", employees)
+	// []Employee{
+	//   {Name: "Jinzhu", Age: 18, Salary:0, DoubleAge: 36, EmployeId: 0, SuperRole: "Super Admin"}
+	// }
+
+	// Copy slice to slice
+	employees = []Employee{}
+	copier.Copy(&employees, &users)
+
+	fmt.Printf("%#v \n", employees)
+	// []Employee{
+	//   {Name: "Jinzhu", Age: 18, Salary:0, DoubleAge: 36, EmployeId: 0, SuperRole: "Super Admin"},
+	//   {Name: "jinzhu 2", Age: 30, Salary:0, DoubleAge: 60, EmployeId: 0, SuperRole: "Super Dev"},
+	// }
+
+ 	// Copy map to map
+	map1 := map[int]int{3: 6, 4: 8}
+	map2 := map[int32]int8{}
+	copier.Copy(&map2, map1)
+
+	fmt.Printf("%#v \n", map2)
+	// map[int32]int8{3:6, 4:8}
+}
+```
+
+### Copy with Option
+
+```go
+copier.CopyWithOption(&to, &from, copier.Option{IgnoreEmpty: true, DeepCopy: true})
+```
+
+## Contributing
+
+You can help to make the project better, check out [http://gorm.io/contribute.html](http://gorm.io/contribute.html) for things you can do.
+
+# Author
+
+**jinzhu**
+
+* <http://github.com/jinzhu>
+* <wosmvp@gmail.com>
+* <http://twitter.com/zhangjinzhu>
+
+## License
+
+Released under the [MIT License](https://github.com/jinzhu/copier/blob/master/License).
diff --git a/vendor/github.com/jinzhu/copier/copier.go b/vendor/github.com/jinzhu/copier/copier.go
new file mode 100644
index 000000000..12b74ff67
--- /dev/null
+++ b/vendor/github.com/jinzhu/copier/copier.go
@@ -0,0 +1,481 @@
+package copier
+
+import (
+	"database/sql"
+	"database/sql/driver"
+	"fmt"
+	"reflect"
+	"strings"
+)
+
+// These flags define options for tag handling
+const (
+	// Denotes that a destination field must be copied to. If copying fails then a panic will ensue.
+	tagMust uint8 = 1 << iota
+
+	// Denotes that the program should not panic when the must flag is on and
+	// value is not copied. The program will return an error instead.
+	tagNoPanic
+
+	// Ignore a destination field from being copied to.
+	tagIgnore
+
+	// Denotes that the value as been copied
+	hasCopied
+)
+
+// Option sets copy options
+type Option struct {
+	// setting this value to true will ignore copying zero values of all the fields, including bools, as well as a
+	// struct having all it's fields set to their zero values respectively (see IsZero() in reflect/value.go)
+	IgnoreEmpty bool
+	DeepCopy    bool
+}
+
+// Copy copy things
+func Copy(toValue interface{}, fromValue interface{}) (err error) {
+	return copier(toValue, fromValue, Option{})
+}
+
+// CopyWithOption copy with option
+func CopyWithOption(toValue interface{}, fromValue interface{}, opt Option) (err error) {
+	return copier(toValue, fromValue, opt)
+}
+
+func copier(toValue interface{}, fromValue interface{}, opt Option) (err error) {
+	var (
+		isSlice bool
+		amount  = 1
+		from    = indirect(reflect.ValueOf(fromValue))
+		to      = indirect(reflect.ValueOf(toValue))
+	)
+
+	if !to.CanAddr() {
+		return ErrInvalidCopyDestination
+	}
+
+	// Return is from value is invalid
+	if !from.IsValid() {
+		return ErrInvalidCopyFrom
+	}
+
+	fromType, isPtrFrom := indirectType(from.Type())
+	toType, _ := indirectType(to.Type())
+
+	if fromType.Kind() == reflect.Interface {
+		fromType = reflect.TypeOf(from.Interface())
+	}
+
+	if toType.Kind() == reflect.Interface {
+		toType, _ = indirectType(reflect.TypeOf(to.Interface()))
+		oldTo := to
+		to = reflect.New(reflect.TypeOf(to.Interface())).Elem()
+		defer func() {
+			oldTo.Set(to)
+		}()
+	}
+
+	// Just set it if possible to assign for normal types
+	if from.Kind() != reflect.Slice && from.Kind() != reflect.Struct && from.Kind() != reflect.Map && (from.Type().AssignableTo(to.Type()) || from.Type().ConvertibleTo(to.Type())) {
+		if !isPtrFrom || !opt.DeepCopy {
+			to.Set(from.Convert(to.Type()))
+		} else {
+			fromCopy := reflect.New(from.Type())
+			fromCopy.Set(from.Elem())
+			to.Set(fromCopy.Convert(to.Type()))
+		}
+		return
+	}
+
+	if fromType.Kind() == reflect.Map && toType.Kind() == reflect.Map {
+		if !fromType.Key().ConvertibleTo(toType.Key()) {
+			return ErrMapKeyNotMatch
+		}
+
+		if to.IsNil() {
+			to.Set(reflect.MakeMapWithSize(toType, from.Len()))
+		}
+
+		for _, k := range from.MapKeys() {
+			toKey := indirect(reflect.New(toType.Key()))
+			if !set(toKey, k, opt.DeepCopy) {
+				return fmt.Errorf("%w map, old key: %v, new key: %v", ErrNotSupported, k.Type(), toType.Key())
+			}
+
+			elemType, _ := indirectType(toType.Elem())
+			toValue := indirect(reflect.New(elemType))
+			if !set(toValue, from.MapIndex(k), opt.DeepCopy) {
+				if err = copier(toValue.Addr().Interface(), from.MapIndex(k).Interface(), opt); err != nil {
+					return err
+				}
+			}
+
+			for {
+				if elemType == toType.Elem() {
+					to.SetMapIndex(toKey, toValue)
+					break
+				}
+				elemType = reflect.PtrTo(elemType)
+				toValue = toValue.Addr()
+			}
+		}
+		return
+	}
+
+	if from.Kind() == reflect.Slice && to.Kind() == reflect.Slice && fromType.ConvertibleTo(toType) {
+		if to.IsNil() {
+			slice := reflect.MakeSlice(reflect.SliceOf(to.Type().Elem()), from.Len(), from.Cap())
+			to.Set(slice)
+		}
+
+		for i := 0; i < from.Len(); i++ {
+			if to.Len() < i+1 {
+				to.Set(reflect.Append(to, reflect.New(to.Type().Elem()).Elem()))
+			}
+
+			if !set(to.Index(i), from.Index(i), opt.DeepCopy) {
+				err = CopyWithOption(to.Index(i).Addr().Interface(), from.Index(i).Interface(), opt)
+				if err != nil {
+					continue
+				}
+			}
+		}
+		return
+	}
+
+	if fromType.Kind() != reflect.Struct || toType.Kind() != reflect.Struct {
+		// skip not supported type
+		return
+	}
+
+	if to.Kind() == reflect.Slice {
+		isSlice = true
+		if from.Kind() == reflect.Slice {
+			amount = from.Len()
+		}
+	}
+
+	for i := 0; i < amount; i++ {
+		var dest, source reflect.Value
+
+		if isSlice {
+			// source
+			if from.Kind() == reflect.Slice {
+				source = indirect(from.Index(i))
+			} else {
+				source = indirect(from)
+			}
+			// dest
+			dest = indirect(reflect.New(toType).Elem())
+		} else {
+			source = indirect(from)
+			dest = indirect(to)
+		}
+
+		destKind := dest.Kind()
+		initDest := false
+		if destKind == reflect.Interface {
+			initDest = true
+			dest = indirect(reflect.New(toType))
+		}
+
+		// Get tag options
+		tagBitFlags := map[string]uint8{}
+		if dest.IsValid() {
+			tagBitFlags = getBitFlags(toType)
+		}
+
+		// check source
+		if source.IsValid() {
+			// Copy from source field to dest field or method
+			fromTypeFields := deepFields(fromType)
+			for _, field := range fromTypeFields {
+				name := field.Name
+
+				// Get bit flags for field
+				fieldFlags, _ := tagBitFlags[name]
+
+				// Check if we should ignore copying
+				if (fieldFlags & tagIgnore) != 0 {
+					continue
+				}
+
+				if fromField := source.FieldByName(name); fromField.IsValid() && !shouldIgnore(fromField, opt.IgnoreEmpty) {
+					// process for nested anonymous field
+					destFieldNotSet := false
+					if f, ok := dest.Type().FieldByName(name); ok {
+						for idx := range f.Index {
+							destField := dest.FieldByIndex(f.Index[:idx+1])
+
+							if destField.Kind() != reflect.Ptr {
+								continue
+							}
+
+							if !destField.IsNil() {
+								continue
+							}
+							if !destField.CanSet() {
+								destFieldNotSet = true
+								break
+							}
+
+							// destField is a nil pointer that can be set
+							newValue := reflect.New(destField.Type().Elem())
+							destField.Set(newValue)
+						}
+					}
+
+					if destFieldNotSet {
+						break
+					}
+
+					toField := dest.FieldByName(name)
+					if toField.IsValid() {
+						if toField.CanSet() {
+							if !set(toField, fromField, opt.DeepCopy) {
+								if err := copier(toField.Addr().Interface(), fromField.Interface(), opt); err != nil {
+									return err
+								}
+							}
+							if fieldFlags != 0 {
+								// Note that a copy was made
+								tagBitFlags[name] = fieldFlags | hasCopied
+							}
+						}
+					} else {
+						// try to set to method
+						var toMethod reflect.Value
+						if dest.CanAddr() {
+							toMethod = dest.Addr().MethodByName(name)
+						} else {
+							toMethod = dest.MethodByName(name)
+						}
+
+						if toMethod.IsValid() && toMethod.Type().NumIn() == 1 && fromField.Type().AssignableTo(toMethod.Type().In(0)) {
+							toMethod.Call([]reflect.Value{fromField})
+						}
+					}
+				}
+			}
+
+			// Copy from from method to dest field
+			for _, field := range deepFields(toType) {
+				name := field.Name
+
+				var fromMethod reflect.Value
+				if source.CanAddr() {
+					fromMethod = source.Addr().MethodByName(name)
+				} else {
+					fromMethod = source.MethodByName(name)
+				}
+
+				if fromMethod.IsValid() && fromMethod.Type().NumIn() == 0 && fromMethod.Type().NumOut() == 1 && !shouldIgnore(fromMethod, opt.IgnoreEmpty) {
+					if toField := dest.FieldByName(name); toField.IsValid() && toField.CanSet() {
+						values := fromMethod.Call([]reflect.Value{})
+						if len(values) >= 1 {
+							set(toField, values[0], opt.DeepCopy)
+						}
+					}
+				}
+			}
+		}
+
+		if isSlice {
+			if dest.Addr().Type().AssignableTo(to.Type().Elem()) {
+				to.Set(reflect.Append(to, dest.Addr()))
+			} else if dest.Type().AssignableTo(to.Type().Elem()) {
+				to.Set(reflect.Append(to, dest))
+			}
+		} else if initDest {
+			to.Set(dest)
+		}
+
+		err = checkBitFlags(tagBitFlags)
+	}
+
+	return
+}
+
+func shouldIgnore(v reflect.Value, ignoreEmpty bool) bool {
+	if !ignoreEmpty {
+		return false
+	}
+
+	return v.IsZero()
+}
+
+func deepFields(reflectType reflect.Type) []reflect.StructField {
+	if reflectType, _ = indirectType(reflectType); reflectType.Kind() == reflect.Struct {
+		fields := make([]reflect.StructField, 0, reflectType.NumField())
+
+		for i := 0; i < reflectType.NumField(); i++ {
+			v := reflectType.Field(i)
+			if v.Anonymous {
+				fields = append(fields, deepFields(v.Type)...)
+			} else {
+				fields = append(fields, v)
+			}
+		}
+
+		return fields
+	}
+
+	return nil
+}
+
+func indirect(reflectValue reflect.Value) reflect.Value {
+	for reflectValue.Kind() == reflect.Ptr {
+		reflectValue = reflectValue.Elem()
+	}
+	return reflectValue
+}
+
+func indirectType(reflectType reflect.Type) (_ reflect.Type, isPtr bool) {
+	for reflectType.Kind() == reflect.Ptr || reflectType.Kind() == reflect.Slice {
+		reflectType = reflectType.Elem()
+		isPtr = true
+	}
+	return reflectType, isPtr
+}
+
+func set(to, from reflect.Value, deepCopy bool) bool {
+	if from.IsValid() {
+		if to.Kind() == reflect.Ptr {
+			// set `to` to nil if from is nil
+			if from.Kind() == reflect.Ptr && from.IsNil() {
+				to.Set(reflect.Zero(to.Type()))
+				return true
+			} else if to.IsNil() {
+				// `from`         -> `to`
+				// sql.NullString -> *string
+				if fromValuer, ok := driverValuer(from); ok {
+					v, err := fromValuer.Value()
+					if err != nil {
+						return false
+					}
+					// if `from` is not valid do nothing with `to`
+					if v == nil {
+						return true
+					}
+				}
+				// allocate new `to` variable with default value (eg. *string -> new(string))
+				to.Set(reflect.New(to.Type().Elem()))
+			}
+			// depointer `to`
+			to = to.Elem()
+		}
+
+		if deepCopy {
+			toKind := to.Kind()
+			if toKind == reflect.Interface && to.IsNil() {
+				to.Set(reflect.New(reflect.TypeOf(from.Interface())).Elem())
+				toKind = reflect.TypeOf(to.Interface()).Kind()
+			}
+			if toKind == reflect.Struct || toKind == reflect.Map || toKind == reflect.Slice {
+				return false
+			}
+		}
+
+		if from.Type().ConvertibleTo(to.Type()) {
+			to.Set(from.Convert(to.Type()))
+		} else if toScanner, ok := to.Addr().Interface().(sql.Scanner); ok {
+			// `from`  -> `to`
+			// *string -> sql.NullString
+			if from.Kind() == reflect.Ptr {
+				// if `from` is nil do nothing with `to`
+				if from.IsNil() {
+					return true
+				}
+				// depointer `from`
+				from = indirect(from)
+			}
+			// `from` -> `to`
+			// string -> sql.NullString
+			// set `to` by invoking method Scan(`from`)
+			err := toScanner.Scan(from.Interface())
+			if err != nil {
+				return false
+			}
+		} else if fromValuer, ok := driverValuer(from); ok {
+			// `from`         -> `to`
+			// sql.NullString -> string
+			v, err := fromValuer.Value()
+			if err != nil {
+				return false
+			}
+			// if `from` is not valid do nothing with `to`
+			if v == nil {
+				return true
+			}
+			rv := reflect.ValueOf(v)
+			if rv.Type().AssignableTo(to.Type()) {
+				to.Set(rv)
+			}
+		} else if from.Kind() == reflect.Ptr {
+			return set(to, from.Elem(), deepCopy)
+		} else {
+			return false
+		}
+	}
+
+	return true
+}
+
+// parseTags Parses struct tags and returns uint8 bit flags.
+func parseTags(tag string) (flags uint8) {
+	for _, t := range strings.Split(tag, ",") {
+		switch t {
+		case "-":
+			flags = tagIgnore
+			return
+		case "must":
+			flags = flags | tagMust
+		case "nopanic":
+			flags = flags | tagNoPanic
+		}
+	}
+	return
+}
+
+// getBitFlags Parses struct tags for bit flags.
+func getBitFlags(toType reflect.Type) map[string]uint8 {
+	flags := map[string]uint8{}
+	toTypeFields := deepFields(toType)
+
+	// Get a list dest of tags
+	for _, field := range toTypeFields {
+		tags := field.Tag.Get("copier")
+		if tags != "" {
+			flags[field.Name] = parseTags(tags)
+		}
+	}
+	return flags
+}
+
+// checkBitFlags Checks flags for error or panic conditions.
+func checkBitFlags(flagsList map[string]uint8) (err error) {
+	// Check flag conditions were met
+	for name, flags := range flagsList {
+		if flags&hasCopied == 0 {
+			switch {
+			case flags&tagMust != 0 && flags&tagNoPanic != 0:
+				err = fmt.Errorf("field %s has must tag but was not copied", name)
+				return
+			case flags&(tagMust) != 0:
+				panic(fmt.Sprintf("Field %s has must tag but was not copied", name))
+			}
+		}
+	}
+	return
+}
+
+func driverValuer(v reflect.Value) (i driver.Valuer, ok bool) {
+
+	if !v.CanAddr() {
+		i, ok = v.Interface().(driver.Valuer)
+		return
+	}
+
+	i, ok = v.Addr().Interface().(driver.Valuer)
+	return
+}
diff --git a/vendor/github.com/jinzhu/copier/errors.go b/vendor/github.com/jinzhu/copier/errors.go
new file mode 100644
index 000000000..cf7c5e74b
--- /dev/null
+++ b/vendor/github.com/jinzhu/copier/errors.go
@@ -0,0 +1,10 @@
+package copier
+
+import "errors"
+
+var (
+	ErrInvalidCopyDestination = errors.New("copy destination is invalid")
+	ErrInvalidCopyFrom        = errors.New("copy from is invalid")
+	ErrMapKeyNotMatch         = errors.New("map's key type doesn't match")
+	ErrNotSupported           = errors.New("not supported")
+)
diff --git a/vendor/github.com/jinzhu/copier/go.mod b/vendor/github.com/jinzhu/copier/go.mod
new file mode 100644
index 000000000..531422dcb
--- /dev/null
+++ b/vendor/github.com/jinzhu/copier/go.mod
@@ -0,0 +1,3 @@
+module github.com/jinzhu/copier
+
+go 1.15
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 28dfc399d..d36d4a27a 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -138,6 +138,9 @@ github.com/hpcloud/tail/winfile
 github.com/imdario/mergo
 # github.com/inconshreveable/mousetrap v1.0.0
 github.com/inconshreveable/mousetrap
+# github.com/jinzhu/copier v0.2.8
+## explicit
+github.com/jinzhu/copier
 # github.com/json-iterator/go v1.1.10
 github.com/json-iterator/go
 # github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51


Test patch:
diff --git a/pkg/crc/machine/bundle/metadata_test.go b/pkg/crc/machine/bundle/metadata_test.go
index bc0202dd8..68bc3efde 100644
--- a/pkg/crc/machine/bundle/metadata_test.go
+++ b/pkg/crc/machine/bundle/metadata_test.go
@@ -43,6 +43,14 @@ const reference = `{
 	"size": "9",
         "sha256sum": "245a0e5acd4f09000a9a5f37d731082ed1cf3fdcad1b5320cbe9b153c9fd82a4"
       }
+    ],
+    "fileList": [
+      {
+        "name": "oc",
+        "type": "oc-executable",
+        "size": "72728632",
+        "sha256sum": "983f0883a6dffd601afa663d10161bfd8033fd6d45cf587a9cb22e9a681d6047"
+      }
     ]
   },
   "driverInfo": {
@@ -77,10 +85,22 @@ var parsedReference = CrcBundleInfo{
 	Storage: Storage{
 		DiskImages: []DiskImage{
 			{
-				Name:     "crc.qcow2",
-				Format:   "qcow2",
-				Size:     "9",
-				Checksum: "245a0e5acd4f09000a9a5f37d731082ed1cf3fdcad1b5320cbe9b153c9fd82a4",
+				File: File{
+					Name:     "crc.qcow2",
+					Size:     "9",
+					Checksum: "245a0e5acd4f09000a9a5f37d731082ed1cf3fdcad1b5320cbe9b153c9fd82a4",
+				},
+				Format: "qcow2",
+			},
+		},
+		Files: []FileListItem{
+			{
+				File: File{
+					Name:     "oc",
+					Size:     "72728632",
+					Checksum: "983f0883a6dffd601afa663d10161bfd8033fd6d45cf587a9cb22e9a681d6047",
+				},
+				Type: "oc-executable",
 			},
 		},
 	},
diff --git a/pkg/crc/machine/bundle/repository_test.go b/pkg/crc/machine/bundle/repository_test.go
index 2c902f432..07f4ca052 100644
--- a/pkg/crc/machine/bundle/repository_test.go
+++ b/pkg/crc/machine/bundle/repository_test.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/code-ready/crc/pkg/crc/constants"
 
+	"github.com/jinzhu/copier"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -22,7 +23,7 @@ func TestUse(t *testing.T) {
 	assert.NoError(t, err)
 	defer os.RemoveAll(ocBinDir)
 
-	addBundle(t, dir, "crc_libvirt_4.6.1")
+	createDummyBundleContent(t, filepath.Join(dir, "crc_libvirt_4.6.1"), "")
 
 	repo := &Repository{
 		CacheDir: dir,
@@ -48,7 +49,7 @@ func TestUseWithOCFile(t *testing.T) {
 	fd.Close()
 	defer os.RemoveAll(fd.Name())
 
-	addBundle(t, dir, "crc_libvirt_4.6.1")
+	createDummyBundleContent(t, filepath.Join(dir, "crc_libvirt_4.6.1"), "")
 
 	repo := &Repository{
 		CacheDir: dir,
@@ -108,42 +109,40 @@ func TestVersionCheck(t *testing.T) {
 		CacheDir: dir,
 	}
 
-	assert.NoError(t, os.Mkdir(filepath.Join(dir, "crc_libvirt_4.6.1"), 0755))
-
-	addVersionedBundle(t, filepath.Join(dir, "crc_libvirt_4.6.1"), "0.9")
+	bundlePath := filepath.Join(dir, "crc_libvirt_4.6.1")
+	createDummyBundleContent(t, bundlePath, "0.9")
 	_, err = repo.Get("crc_libvirt_4.6.1.crcbundle")
 	assert.EqualError(t, err, "cannot use bundle with version 0.9, bundle version must satisfy ^1.0 constraint")
+	os.RemoveAll(bundlePath)
 
-	addVersionedBundle(t, filepath.Join(dir, "crc_libvirt_4.6.1"), "1.1")
+	createDummyBundleContent(t, bundlePath, "1.1")
 	_, err = repo.Get("crc_libvirt_4.6.1.crcbundle")
 	assert.NoError(t, err)
+	os.RemoveAll(bundlePath)
 
-	addVersionedBundle(t, filepath.Join(dir, "crc_libvirt_4.6.1"), "2.0")
+	createDummyBundleContent(t, bundlePath, "2.0")
 	_, err = repo.Get("crc_libvirt_4.6.1.crcbundle")
 	assert.EqualError(t, err, "cannot use bundle with version 2.0, bundle version must satisfy ^1.0 constraint")
+	os.RemoveAll(bundlePath)
 }
 
-func addVersionedBundle(t *testing.T, dir string, version string) {
-	oldVersion := parsedReference.Version
-	parsedReference.Version = version
-	metadataStr, err := json.Marshal(parsedReference)
+func writeMetadata(t *testing.T, dir string, bundleInfo *CrcBundleInfo) bool {
+	metadataStr, err := json.Marshal(bundleInfo)
 	assert.NoError(t, err)
-	parsedReference.Version = oldVersion
-	writeMetadata(t, dir, string(metadataStr))
-	createDummyBundleContent(t, dir)
-}
-
-func writeMetadata(t *testing.T, dir string, s string) bool {
-	return assert.NoError(t, ioutil.WriteFile(filepath.Join(dir, metadataFilename), []byte(s), 0600))
+	return assert.NoError(t, ioutil.WriteFile(filepath.Join(dir, metadataFilename), []byte(metadataStr), 0600))
 }
 
-func addBundle(t *testing.T, dir, name string) {
-	bundlePath := filepath.Join(dir, name)
+func createDummyBundleContent(t *testing.T, bundlePath string, version string) {
 	assert.NoError(t, os.Mkdir(bundlePath, 0755))
-	writeMetadata(t, bundlePath, reference)
-	createDummyBundleContent(t, bundlePath)
-}
-func createDummyBundleContent(t *testing.T, bundlePath string) {
+
+	var bundleInfo CrcBundleInfo
+	assert.NoError(t, copier.Copy(&bundleInfo, &parsedReference))
+	bundleInfo.Storage.Files[0].Name = constants.OcExecutableName
+	if version != "" {
+		bundleInfo.Version = version
+	}
+	writeMetadata(t, bundlePath, &bundleInfo)
+
 	assert.NoError(t, ioutil.WriteFile(filepath.Join(bundlePath, constants.OcExecutableName), []byte("openshift-client"), 0600))
 	assert.NoError(t, ioutil.WriteFile(filepath.Join(bundlePath, "kubeadmin-password"), []byte("kubeadmin-password"), 0600))
 	assert.NoError(t, ioutil.WriteFile(filepath.Join(bundlePath, "kubeconfig"), []byte("kubeconfig"), 0600))