Repository: code-ready/crc
Pull request #2147 (2021-03-25 06:01:22)

Add generate command to create custom bundle from running openshift cluster
This patch implements `crc generate` command which can be used to
regenerate the bundle from the running openshift cluster.

- As of now it is tested on linux and follow-up patch will be created for
windows and mac support.
- In linux, it is depend on `qemu-img` to merge and commit the backup
  disk image (which is part of cache) with running cluster disk image
  (which is part of machine directory)
- As part of compression, we use top level directory to create same kind
  of bundles as snc repo
- Remove pull secret from the disk and from the cluster before stop it
  and creating the disk image.
- Update the bundle metadata json info (sha256sum is not updated since
  it is not used during any verification but will add it in a follow up
  PR)
- Another dependency is having around 10GB disk space in `/tmp`
  directory because we copy all the bits there even the custom disk
  image.
- $USER home should have enough free space for `qemu-img` operations
  (like rebase and commit disk image).

```
$ crc start
[...]
<-- deploy some applications -->
$ crc generate
<-- create the custom bundle on same directory -->
$ crc start -b <custom_bundle>
```

fixes: #2026

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

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

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: Is the current crc instance still functional after `crc generate`? Ie can I do
```
$ crc start
// do stuff
$ crc generate
$ crc start
```
?

We need to be more explicit during startup regarding which bundle is being used. This can be a message saying that a custom bundle is being used, or a more neutral `logging.Infof("Using bundle %s")` which does not seem to be in our logs at the moment.
praveenkumar: > Is the current crc instance still functional after `crc generate`? Ie can I do
> 
> ```
> $ crc start
> // do stuff
> $ crc generate
> $ crc start
> ```

Yes that you can do, the current instance of crc will be there until you delete it.

> 
> We need to be more explicit during startup regarding which bundle is being used. This can be a message saying that a custom bundle is being used, or a more neutral `logging.Infof("Using bundle %s")` which does not seem to be in our logs at the moment.

We do have a warning message which says an `unsupported bundle in use` but yes we do need an neutral logging around it.
cfergeau: > Yes that you can do, the current instance of crc will be there until you delete it.

But  we ran `RemovePullSecretFromCluster` and similar methods on this instance, so I'm not sure how usable it will be?

> We do have a warning message which says an unsupported bundle in use but yes we do need an neutral logging around it.

You add a `custom` field to the bundle metadata, would also be useful to have somethnig in the logs when it is found.
praveenkumar: > But we ran RemovePullSecretFromCluster and similar methods on this instance, so I'm not sure how usable it will be?

Starting the stop cluster again add the user pull secret so it doesn't have any issue.

> You add a custom field to the bundle metadata, would also be useful to have something in the logs when it is found.

Sure, this is something we can add.
guillaumerose: Is it possible to put this code and use it in snc ?
We can put the Go definition of the bundle json and this code, then vendor it here. 

The best CI we can have for this is in snc. I am afraid that with this code here, it will either extend the CI time by a good amount of time or it will never be launched/tested. 
cfergeau: > > But we ran RemovePullSecretFromCluster and similar methods on this instance, so I'm not sure how usable it will be?
> 
> Starting the stop cluster again add the user pull secret so it doesn't have any issue.

The cluster will recover, but this means the user may be asked to enter their pull secret again in some cases. Dunno if we want to document this somewhere.

openshift-ci[bot]: @praveenkumar: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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 -->
cfergeau: > Another dependency is having around 10GB disk space in /tmp directory because we copy all the bits there even the custom disk image.

`/tmp` is `tmpfs on /tmp type tmpfs` on fedora, `/var/tmp` might be a better location, as using 10GB RAM even temporarily is quite a lot.
praveenkumar: > > Another dependency is having around 10GB disk space in /tmp directory because we copy all the bits there even the custom disk image.
> 
> `/tmp` is `tmpfs on /tmp type tmpfs` on fedora, `/var/tmp` might be a better location, as using 10GB RAM even temporarily is quite a lot.

@cfergeau This is created using `ioutil.TempDir` not something we create manually to have platform independent support.
praveenkumar: > Is it possible to put this code and use it in snc ?
> We can put the Go definition of the bundle json and this code, then vendor it here.
> 
> The best CI we can have for this is in snc. I am afraid that with this code here, it will either extend the CI time by a good amount of time or it will never be launched/tested.

@guillaumerose We do need to have a scenario for this on CI (can be only run downstream, if timing is issue) because this is going to be part of each platform and snc mostly can test linux use case.
cfergeau: > I've experimented with this in https://github.com/cfergeau/crc/tree/praveen/generate , but I did not get yet to writing a unit test for the new bundle code, so it's not working :)

Did more work on this (basic unit test now) in https://github.com/cfergeau/crc/tree/praveen/generate , but this is turning into quite a bit of code, not sure how to make this simpler :-/
gbraad: > has lots of knowledge it should not have (about bundle metadata, and disk images).
> The creation of the disk image can be improved when windows/macos support is added,
> but I'd really prefer that the bundle metadata is maintained in ...

Can these be refactor commits as follow up, or are they blocking further progress? I would consider this more ov a priority when windows and macOS is added.

Also, Amy comments about command pattern?
cfergeau: > > has lots of knowledge it should not have (about bundle metadata, and disk images).
> > The creation of the disk image can be improved when windows/macos support is added,
> > but I'd really prefer that the bundle metadata is maintained in ...
> 
> Can these be refactor commits as follow up, or are they blocking further progress? I would consider this more ov a priority when windows and macOS is added.

The disk image generation, as I said, this is a part of the code we will have to change again for windows/macos support, so fine with me as a followup (we'll have to remember to plan for it though, and not just go for `io.Copy` in `generate_non-linux.go`)

Regarding bundle metadata, the current PR is generating invalid metadata (checksums don't match), and is hardcoding a lot of things regarding the bundle format as it is right now. For example, if we add a `podman-remote` binary to our bundles, and forget to update this code, generation will still seem to be working fine, but crc won't be able to load the generated bundle. If we change the ssh key name (this happened in the past), the code will break in the same way if we forget to update it.

> Also, any comments about command pattern?


openshift-ci[bot]: @praveenkumar: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

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

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

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

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

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

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

<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: > SSHPrivateKeyFile is still not updated

@cfergeau Forgot to remove the `SSHPrivateKeyFile` code, since this is again going to same as source bundle `SSHPrivateKey` file name.
openshift-ci[bot]: @praveenkumar: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

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

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

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

<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: /retest this please
openshift-ci-robot: @praveenkumar: The `/retest` command does not accept any targets.
The following commands are available to trigger jobs:
* `/test e2e-crc`
* `/test images`
* `/test integration-crc`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/code-ready/crc/pull/2147#issuecomment-822214426):

>/retest this please


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

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

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

<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: > * after bundle generation, crc could print where the generated bundle is

Can be taken care of.

> 
> * after bundle generation, it's not obvious we need to run `crc delete && crc start ...`

Sure, at the end of bundle generate command we can have some info message if that helps.

> 
> * if `crc_libvirt_4.7.5_custom.crcbundle` already exists in the current directory, it will be silently overwritten

Yes, that is expected.

> 
> * if I'm running a custom bundle, and then try to create a new one, it will be named `crc_libvirt_4.7.5_custom_custom.crcbundle` (unexpected to me, but maybe you did this on purpose)

Yes, because we shouldn't stop user to create another custom bundle from existing custom bundle. 

> 
> * if there's already a ~/.crc/cache/crc_libvirt_4.7.5_custom/ directory, then trying to use a different bundle with the same name will never overwrite the cache directory

This is even case with release bits if I have `~/.crc/cache/crc_libvirt_4.7.5` directory then using doesn't override it or you want to differ this in case of custom bundle?


cfergeau: > > if crc_libvirt_4.7.5_custom.crcbundle already exists in the current directory, it will be silently overwritten
> Yes, that is expected.

I don't think that's  desirable, I'd require the use of `-f` to allow overwriting a file of the same name.

> > if I'm running a custom bundle, and then try to create a new one, it will be named crc_libvirt_4.7.5_custom_custom.crcbundle (unexpected to me, but maybe you did this on purpose)
> Yes, because we shouldn't stop user to create another custom bundle from existing custom bundle.

I'd keep the same name,  `crc_libvirt_4.7.5_custom.crcbundle`, or append a number to the end `crc_libvirt_4.7.5_custom_2.crcbundle`. Appending `custom` multiple times look odd 

> This is even case with release bits if I have ~/.crc/cache/crc_libvirt_4.7.5 directory then using doesn't override it or you want to differ this in case of custom bundle?

But released bundles never change, so it's not a problem that once unpacked, we never update the unpacked content. With custom bundles, the user is in control, they can recreate the custom bundle whenever they want. And this behaviour of `~/.crc/cache` will be very unexpected, and very user unfriendly (people will spend hours not understanding why an old bundle is being used, and will file bugs/ask on slack). So this will need to be addressed somehow.

cfergeau: By the way, this code needs to be updated for bundles generated with https://github.com/code-ready/snc/commit/9a70944f5b64071d4791a5a3dd987e2a5fd66e59 
gbraad: > > if I'm running a custom bundle, and then try to create a new one, it will be named crc_libvirt_4.7.5_custom_custom.crcbundle (unexpected to me, but maybe you did this on purpose)

> Yes, because we shouldn't stop user to create another custom bundle from existing custom bundle.

actually, expected... or should become `custom_2` ;-), but that would need more code to handle ;-)
gbraad: > I don't think that's desirable, I'd require the use of -f to allow overwriting a file with the same name.

`-f` being a `force` or `file` specification?
cfergeau: > > I don't think that's desirable, I'd require the use of -f to allow overwriting a file with the same name.
> 
> `-f` being a `force` or `file` specification?

`--force` as already done in a few other commands, ambiguous indeed :)
gbraad: > but I think at least this issue with ~/.crc/cache/crc_libvirt_4.7.5_custom/ never being overwritten needs to be fixed before bundle generation is released to users.

This is a difficult one to answer. This might be a 'known limitation' for now. I am not sure if this should prevent this merge... as follow-up issues related to this might be more related to how the cache code handles this situation; the metadata would need to be checked for a possible `"creation-timestamp"` or so, but this would mean an extraction of the metadata from the bundle is needed.
praveenkumar: > These can be addressed in follow-ups to this PR, but I think at least this issue with ~/.crc/cache/crc_libvirt_4.7.5_custom/ never being overwritten needs to be fixed before bundle generation is released to users.

That means even nothing change on the `custom` bundle side we have to extract it freshly for each crc start?
praveenkumar: > By the way, this code needs to be updated for bundles generated with [code-ready/snc@9a70944](https://github.com/code-ready/snc/commit/9a70944f5b64071d4791a5a3dd987e2a5fd66e59)

Should only happen when #2234 merges or if this PR merge before then it should be handled by #2234
openshift-ci[bot]: @praveenkumar: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/e2e-crc | 62a89b56ee2c4ab85daadf313a29df4ccf256722 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2147/pull-ci-code-ready-crc-master-e2e-crc/1385148141430378496) | `/test e2e-crc`
ci/prow/integration-crc | 62a89b56ee2c4ab85daadf313a29df4ccf256722 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2147/pull-ci-code-ready-crc-master-integration-crc/1385148141501681664) | `/test integration-crc`

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

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/e2e-crc | 62a89b56ee2c4ab85daadf313a29df4ccf256722 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2147/pull-ci-code-ready-crc-master-e2e-crc/1385148141430378496) | `/test e2e-crc`
ci/prow/integration-crc | 62a89b56ee2c4ab85daadf313a29df4ccf256722 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2147/pull-ci-code-ready-crc-master-integration-crc/1385148141501681664) | `/test integration-crc`
ci/prow/images | 62a89b56ee2c4ab85daadf313a29df4ccf256722 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2147/pull-ci-code-ready-crc-master-images/1385148141463932928) | `/test images`

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

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

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

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

<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 -->
cfergeau: > > By the way, this code needs to be updated for bundles generated with [code-ready/snc@9a70944](https://github.com/code-ready/snc/commit/9a70944f5b64071d4791a5a3dd987e2a5fd66e59)
> 
> Should only happen when #2234 merges or if this PR merge before then it should be handled by #2234

I did not try it, but I think `fileList` in the generated bundle metadata will have an entry for a `podman` binary which is not present in the bundle. This will be the case even if we never merge #2234.
cfergeau: > > These can be addressed in follow-ups to this PR, but I think at least this issue with ~/.crc/cache/crc_libvirt_4.7.5_custom/ never being overwritten needs to be fixed before bundle generation is released to users.
> 
> That means even nothing change on the `custom` bundle side we have to extract it freshly for each crc start?

Extracting custom bundle each time the user runs `crc start` is one easy way of dealing with it, but it's suboptimal :-/
A better way of dealing with this would be to read the metadata file from the archive, and if this metadata does not match what is unpacked, then we unpack it again. But that's more work, not sure it fits in this sprint :)
praveenkumar: > I did not try it, but I think fileList in the generated bundle metadata will have an entry for a podman binary which is not present in the bundle. In other words, either the bundle metadata or the bundle content is invalid. This is the case even if we never merge #2234.

@cfergeau file is present but until it is part of `verify` function it will not fails and as soon as #2234 merges then the generation will automatic start failing because of this so that's the reason I didn't added it here yet.
gbraad: @praveenkumar does the bundle contain the podman-remote binary? if this is not the case, the bundle is invalid.
openshift-ci[bot]: @praveenkumar: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/integration-crc | 631e5fbd149282abd546a064c47df86da9f2aec1 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2147/pull-ci-code-ready-crc-master-integration-crc/1385538387590516736) | `/test integration-crc`

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

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/integration-crc | 631e5fbd149282abd546a064c47df86da9f2aec1 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2147/pull-ci-code-ready-crc-master-integration-crc/1385538387590516736) | `/test integration-crc`
ci/prow/e2e-crc | 631e5fbd149282abd546a064c47df86da9f2aec1 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2147/pull-ci-code-ready-crc-master-e2e-crc/1385538387515019264) | `/test e2e-crc`

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

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

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

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

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

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

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

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

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

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

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

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

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

<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 -->
cfergeau: ```
INFO Removing user's pull secret from instance disk and from cluster secret... 
INFO Stopping the OpenShift cluster, this may take a few minutes... 
INFO Copy the disk image to crc_libvirt_4.7.5_1619459287 
INFO Generating sha256sum for crc.qcow2...        
INFO Compressing /tmp/crc_custom_bundle642695715/crc_libvirt_4.7.5_1619459287... 
INFO Bundle is generated in /home/teuf/crc/crc/crc_libvirt_4.7.5_1619459287 
INFO You need to perform 'crc delete' and 'crc start -b /home/teuf/crc/crc/crc_libvirt_4.7.5_1619459287' to use this bundle
```
The last 2 lines are missing the `.crcbundle` extension.
openshift-ci[bot]: @praveenkumar: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/e2e-crc | 02a7c0e7ef33177949a5c6a3481b64776ef5d956 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2147/pull-ci-code-ready-crc-master-e2e-crc/1385826363956531200) | `/test e2e-crc`
ci/prow/integration-crc | 72e86f73eff964ffb41117ea45e738d147be08eb | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/2147/pull-ci-code-ready-crc-master-integration-crc/1386961910284423168) | `/test integration-crc`

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

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

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

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

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

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

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

<details>

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

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2147#pullrequestreview-647080171" title="Approved">cfergeau</a>*, *<a href="https://github.com/code-ready/crc/pull/2147#" title="Author self-approved">praveenkumar</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,praveenkumar]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
openshift-ci-robot: New changes are detected. LGTM label has been removed.
openshift-ci[bot]: @praveenkumar: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<details>

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

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/2147#pullrequestreview-647867842" title="Approved">cfergeau</a>*, *<a href="https://github.com/code-ready/crc/pull/2147#" title="Author self-approved">praveenkumar</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,praveenkumar]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/cmd/crc/cmd/bundle/bundle.go b/cmd/crc/cmd/bundle/bundle.go
new file mode 100644
index 000000000..90143f600
--- /dev/null
+++ b/cmd/crc/cmd/bundle/bundle.go
@@ -0,0 +1,19 @@
+package bundle
+
+import (
+	"github.com/code-ready/crc/pkg/crc/config"
+	"github.com/spf13/cobra"
+)
+
+func GetBundleCmd(config *config.Config) *cobra.Command {
+	bundleCmd := &cobra.Command{
+		Use:   "bundle SUBCOMMAND [flags]",
+		Short: "Manage CRC bundles",
+		Long:  "Manage CRC bundles",
+		Run: func(cmd *cobra.Command, args []string) {
+			_ = cmd.Help()
+		},
+	}
+	bundleCmd.AddCommand(getGenerateCmd(config))
+	return bundleCmd
+}
diff --git a/cmd/crc/cmd/bundle/generate.go b/cmd/crc/cmd/bundle/generate.go
new file mode 100644
index 000000000..c1cd18c81
--- /dev/null
+++ b/cmd/crc/cmd/bundle/generate.go
@@ -0,0 +1,30 @@
+package bundle
+
+import (
+	"github.com/code-ready/crc/pkg/crc/config"
+	"github.com/code-ready/crc/pkg/crc/constants"
+	"github.com/code-ready/crc/pkg/crc/logging"
+	"github.com/code-ready/crc/pkg/crc/machine"
+	"github.com/spf13/cobra"
+)
+
+func getGenerateCmd(config *config.Config) *cobra.Command {
+	return &cobra.Command{
+		Use:   "generate",
+		Short: "Generate a custom bundle from the running OpenShift cluster",
+		Long:  "Generate a custom bundle from the running OpenShift cluster",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runGenerate(config)
+		},
+	}
+}
+
+func runGenerate(config *config.Config) error {
+	client := machine.NewClient(constants.DefaultName, isDebugLog(), config)
+
+	return client.GenerateBundle()
+}
+
+func isDebugLog() bool {
+	return logging.LogLevel == "debug"
+}
diff --git a/cmd/crc/cmd/root.go b/cmd/crc/cmd/root.go
index 35e24e1b4..1d219f8a9 100644
--- a/cmd/crc/cmd/root.go
+++ b/cmd/crc/cmd/root.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"time"
 
+	cmdBundle "github.com/code-ready/crc/cmd/crc/cmd/bundle"
 	cmdConfig "github.com/code-ready/crc/cmd/crc/cmd/config"
 	crcConfig "github.com/code-ready/crc/pkg/crc/config"
 	"github.com/code-ready/crc/pkg/crc/constants"
@@ -61,6 +62,7 @@ func init() {
 
 	// subcommands
 	rootCmd.AddCommand(cmdConfig.GetConfigCmd(config))
+	rootCmd.AddCommand(cmdBundle.GetBundleCmd(config))
 
 	rootCmd.PersistentFlags().StringVar(&logging.LogLevel, "log-level", constants.DefaultLogLevel, "log level (e.g. \"debug | info | warn | error\")")
 }
diff --git a/pkg/compress/compress.go b/pkg/compress/compress.go
new file mode 100644
index 000000000..bed9498cd
--- /dev/null
+++ b/pkg/compress/compress.go
@@ -0,0 +1,71 @@
+package compress
+
+import (
+	"archive/tar"
+	"io"
+	"os"
+	"path/filepath"
+
+	"github.com/code-ready/crc/pkg/crc/logging"
+	"github.com/klauspost/compress/zstd"
+)
+
+func Compress(src, dest string) (err error) {
+	out, err := os.Create(dest)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		cerr := out.Close()
+		if err == nil {
+			err = cerr
+		}
+	}()
+
+	enc, err := zstd.NewWriter(out)
+	if err != nil {
+		return err
+	}
+	defer enc.Close()
+
+	tarWriter := tar.NewWriter(enc)
+	defer tarWriter.Close()
+
+	basePath, _ := filepath.Split(src)
+
+	// Just use top level directory as part of tarball
+	// $ zstdcat crc_libvirt_4.7.1_custom.zstd  | tar t
+	// crc_libvirt_4.7.1_custom
+	// crc_libvirt_4.7.1_custom/test
+	return filepath.Walk(src, func(file string, fi os.FileInfo, err1 error) error {
+		if err1 != nil {
+			return err1
+		}
+		logging.Debugf("Adding %s", file)
+		header, err := tar.FileInfoHeader(fi, file)
+		if err != nil {
+			return err
+		}
+		// must provide real name
+		// (see https://golang.org/src/archive/tar/common.go?#L626)
+		header.Name, err = filepath.Rel(basePath, filepath.ToSlash(file))
+		if err != nil {
+			return err
+		}
+
+		if err := tarWriter.WriteHeader(header); err != nil {
+			return err
+		}
+		if !fi.IsDir() {
+			data, err := os.Open(file)
+			if err != nil {
+				return err
+			}
+			defer data.Close()
+			if _, err := io.Copy(tarWriter, data); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
diff --git a/pkg/crc/cluster/cluster.go b/pkg/crc/cluster/cluster.go
index d8bb9e942..2a62bb52e 100644
--- a/pkg/crc/cluster/cluster.go
+++ b/pkg/crc/cluster/cluster.go
@@ -126,6 +126,35 @@ func EnsurePullSecretPresentInTheCluster(ocConfig oc.Config, pullSec PullSecretL
 	return nil
 }
 
+func RemovePullSecretFromCluster(ocConfig oc.Config, sshRunner *ssh.Runner) error {
+	logging.Info("Removing user's pull secret from instance disk and from cluster secret...")
+	if _, _, err := sshRunner.RunPrivileged("Removing user's pull secret", "rm", "-fr", vmPullSecretPath); err != nil {
+		return err
+	}
+	cmdArgs := []string{"patch", "secret", "pull-secret", "-p",
+		`'{"data":{".dockerconfigjson":"e30K"}}'`,
+		"-n", "openshift-config", "--type", "merge"}
+
+	_, stderr, err := ocConfig.RunOcCommand(cmdArgs...)
+	if err != nil {
+		return fmt.Errorf("Failed to remove Pull secret %w: %s", err, stderr)
+	}
+
+	cmdArgs = []string{"delete", "machineconfigs", "--all"}
+	_, stderr, err = ocConfig.RunOcCommand(cmdArgs...)
+	if err != nil {
+		return fmt.Errorf("Failed to remove machineconfigs %w: %s", err, stderr)
+	}
+
+	cmdArgs = []string{"delete", "machineconfigpools", "--all"}
+	_, stderr, err = ocConfig.RunOcCommand(cmdArgs...)
+	if err != nil {
+		return fmt.Errorf("Failed to remove machineconfigpools %w: %s", err, stderr)
+	}
+
+	return nil
+}
+
 func EnsureClusterIDIsNotEmpty(ocConfig oc.Config) error {
 	if err := WaitForOpenshiftResource(ocConfig, "clusterversion"); err != nil {
 		return err
diff --git a/pkg/crc/machine/bundle/copier.go b/pkg/crc/machine/bundle/copier.go
new file mode 100644
index 000000000..18562c577
--- /dev/null
+++ b/pkg/crc/machine/bundle/copier.go
@@ -0,0 +1,160 @@
+package bundle
+
+import (
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/code-ready/crc/pkg/compress"
+	"github.com/code-ready/crc/pkg/crc/logging"
+	crcos "github.com/code-ready/crc/pkg/os"
+	godeepcopy "github.com/jinzhu/copier"
+)
+
+type Copier struct {
+	srcBundle    *CrcBundleInfo
+	copiedBundle CrcBundleInfo
+}
+
+func NewCopier(srcBundle *CrcBundleInfo, basePath string, customBundleName string) (*Copier, error) {
+	var copier Copier
+
+	bundlePath := filepath.Join(basePath, customBundleName)
+	if err := os.Mkdir(bundlePath, 0775); err != nil {
+		return nil, err
+	}
+
+	// Creating a deepCopy of source bundle metadata so that whatever we make
+	// modification of any metadata field for copied operation should only
+	// effect the copied bundle metadata instead of source bundle metadata.
+	if err := godeepcopy.Copy(&copier.copiedBundle, srcBundle); err != nil {
+		return nil, err
+	}
+
+	copier.copiedBundle.bundleName = customBundleName
+	copier.copiedBundle.Type = "custom"
+	copier.srcBundle = srcBundle
+	copier.copiedBundle.cachedPath = bundlePath
+
+	return &copier, nil
+}
+
+func (copier *Copier) Cleanup() error {
+	return os.RemoveAll(copier.copiedBundle.cachedPath)
+}
+
+func (copier *Copier) resolvePath(filename string) string {
+	return copier.copiedBundle.resolvePath(filename)
+}
+
+func (copier *Copier) CachedPath() string {
+	return copier.copiedBundle.cachedPath
+}
+
+func (copier *Copier) CopyPrivateSSHKey(srcPath string) error {
+	sshKeyFileName := filepath.Base(copier.srcBundle.GetSSHKeyPath())
+	destPath := copier.resolvePath(sshKeyFileName)
+	return crcos.CopyFileContents(srcPath, destPath, 0400)
+}
+
+func (copier *Copier) SetKubeAdminPassword(kubePassword string) error {
+	kubeAdminPasswordFileName := copier.srcBundle.ClusterInfo.KubeadminPasswordFile
+	path := copier.resolvePath(kubeAdminPasswordFileName)
+	return ioutil.WriteFile(path, []byte(kubePassword), 0600)
+}
+
+func (copier *Copier) CopyKubeConfig() error {
+	kubeConfigFileName := filepath.Base(copier.srcBundle.GetKubeConfigPath())
+	srcPath := copier.srcBundle.GetKubeConfigPath()
+	destPath := copier.resolvePath(kubeConfigFileName)
+	return crcos.CopyFileContents(srcPath, destPath, 0640)
+}
+
+func (copier *Copier) CopyFilesFromFileList() error {
+	for _, file := range copier.srcBundle.Storage.Files {
+		srcPath := copier.srcBundle.resolvePath(file.Name)
+		destPath := copier.resolvePath(filepath.Base(srcPath))
+		if err := crcos.CopyFileContents(srcPath, destPath, 0755); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (copier *Copier) SetDiskImage(path string, format string) error {
+	fileInfo, err := getFileInfo(path)
+	if err != nil {
+		return err
+	}
+	diskImage := DiskImage{
+		File:   *fileInfo,
+		Format: format,
+	}
+
+	copier.copiedBundle.Storage.DiskImages = []DiskImage{diskImage}
+
+	return nil
+}
+
+func (copier *Copier) GenerateBundle(bundleName string) error {
+	if err := copier.copiedBundle.verify(); err != nil {
+		return err
+	}
+
+	// update bundle info
+	copier.copiedBundle.BuildInfo.BuildTime = time.Now().String()
+
+	// Create the metadata json for custom bundle
+	bundleContent, err := json.MarshalIndent(copier.copiedBundle, "", " ")
+	if err != nil {
+		return err
+	}
+	err = ioutil.WriteFile(copier.resolvePath("crc-bundle-info.json"), bundleContent, 0600)
+	if err != nil {
+		return fmt.Errorf("error copying bundle metadata  %w", err)
+	}
+
+	logging.Infof("Compressing %s...", GetBundleNameWithoutExtension(copier.copiedBundle.bundleName))
+	return compress.Compress(copier.copiedBundle.cachedPath, fmt.Sprintf("%s%s", bundleName, bundleExtension))
+}
+
+func sha256sum(path string) (string, error) {
+	logging.Infof("Generating sha256sum for %s...", filepath.Base(path))
+	f, err := os.Open(path)
+	if err != nil {
+		return "", err
+	}
+	defer f.Close()
+
+	h := sha256.New()
+	if _, err := io.Copy(h, f); err != nil {
+		return "", err
+	}
+
+	return hex.EncodeToString(h.Sum(nil)), nil
+}
+
+func getFileInfo(path string) (*File, error) {
+	fileInfo := File{}
+	fileInfo.Name = filepath.Base(path)
+
+	stat, err := os.Stat(path)
+	if err != nil {
+		return nil, err
+	}
+	fileInfo.Size = fmt.Sprintf("%d", stat.Size())
+
+	checksum, err := sha256sum(path)
+	if err != nil {
+		return nil, err
+	}
+	fileInfo.Checksum = checksum
+
+	return &fileInfo, nil
+}
diff --git a/pkg/crc/machine/bundle/metadata.go b/pkg/crc/machine/bundle/metadata.go
index 73bcef7b8..859efc299 100644
--- a/pkg/crc/machine/bundle/metadata.go
+++ b/pkg/crc/machine/bundle/metadata.go
@@ -103,6 +103,10 @@ func (bundle *CrcBundleInfo) GetDiskImagePath() string {
 	return bundle.resolvePath(bundle.Storage.DiskImages[0].Name)
 }
 
+func (bundle *CrcBundleInfo) GetDiskImageFormat() string {
+	return bundle.Storage.DiskImages[0].Format
+}
+
 func (bundle *CrcBundleInfo) GetKubeConfigPath() string {
 	return bundle.resolvePath(bundle.ClusterInfo.KubeConfig)
 }
@@ -145,6 +149,10 @@ func (bundle *CrcBundleInfo) GetInitramfsPath() string {
 	return bundle.resolvePath(bundle.Nodes[0].Initramfs)
 }
 
+func (bundle *CrcBundleInfo) GetKernelCommandLine() string {
+	return bundle.Nodes[0].KernelCmdLine
+}
+
 func (bundle *CrcBundleInfo) GetKubeadminPassword() (string, error) {
 	rawData, err := ioutil.ReadFile(bundle.resolvePath(bundle.ClusterInfo.KubeadminPasswordFile))
 	if err != nil {
@@ -161,6 +169,10 @@ func (bundle *CrcBundleInfo) GetOpenshiftVersion() string {
 	return bundle.ClusterInfo.OpenShiftVersion
 }
 
+func (bundle *CrcBundleInfo) GetBundleNameWithoutExtension() string {
+	return GetBundleNameWithoutExtension(bundle.GetBundleName())
+}
+
 func (bundle *CrcBundleInfo) verify() error {
 	files := []string{
 		bundle.GetOcPath(),
@@ -202,3 +214,15 @@ func (bundle *CrcBundleInfo) checkDiskImageSize() error {
 	}
 	return nil
 }
+
+func GetBundleNameWithoutExtension(bundleName string) string {
+	return strings.TrimSuffix(bundleName, bundleExtension)
+}
+
+func GetCustomBundleNameWithoutExtension(bundleName string) string {
+	return fmt.Sprintf("%s_%d", GetBundleNameWithoutExtension(bundleName), time.Now().Unix())
+}
+
+func GetCustomBundle(bundleNameWithoutExtension string) string {
+	return fmt.Sprintf("%s.%s", bundleNameWithoutExtension, bundleExtension)
+}
diff --git a/pkg/crc/machine/bundle/repository.go b/pkg/crc/machine/bundle/repository.go
index 532d7ffb1..1ffed704d 100644
--- a/pkg/crc/machine/bundle/repository.go
+++ b/pkg/crc/machine/bundle/repository.go
@@ -7,7 +7,6 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
-	"strings"
 	"time"
 
 	"github.com/Masterminds/semver"
@@ -30,7 +29,7 @@ type Repository struct {
 }
 
 func (repo *Repository) Get(bundleName string) (*CrcBundleInfo, error) {
-	path := filepath.Join(repo.CacheDir, strings.TrimSuffix(bundleName, bundleExtension))
+	path := filepath.Join(repo.CacheDir, GetBundleNameWithoutExtension(bundleName))
 	if _, err := os.Stat(path); err != nil {
 		return nil, errors.Wrapf(err, "could not find cached bundle info in %s", path)
 	}
@@ -150,7 +149,7 @@ func (repo *Repository) Extract(path string) error {
 		return err
 	}
 
-	bundleBaseDir := strings.TrimSuffix(bundleName, bundleExtension)
+	bundleBaseDir := GetBundleNameWithoutExtension(bundleName)
 	bundleDir := filepath.Join(repo.CacheDir, bundleBaseDir)
 	_ = os.RemoveAll(bundleDir)
 	return crcerrors.RetryAfter(time.Minute, func() error {
diff --git a/pkg/crc/machine/client.go b/pkg/crc/machine/client.go
index a9c07a81c..6d40d6de2 100644
--- a/pkg/crc/machine/client.go
+++ b/pkg/crc/machine/client.go
@@ -22,6 +22,7 @@ type Client interface {
 	Status() (*types.ClusterStatusResult, error)
 	Stop() (state.State, error)
 	IsRunning() (bool, error)
+	GenerateBundle() error
 }
 
 type client struct {
diff --git a/pkg/crc/machine/fakemachine/client.go b/pkg/crc/machine/fakemachine/client.go
index fb53392e1..cc00373c8 100644
--- a/pkg/crc/machine/fakemachine/client.go
+++ b/pkg/crc/machine/fakemachine/client.go
@@ -68,6 +68,10 @@ func (c *Client) PowerOff() error {
 	return nil
 }
 
+func (c *Client) GenerateBundle() error {
+	return nil
+}
+
 func (c *Client) Start(ctx context.Context, startConfig types.StartConfig) (*types.StartResult, error) {
 	if c.Failing {
 		return nil, errors.New("Failed to start")
diff --git a/pkg/crc/machine/generate_bundle.go b/pkg/crc/machine/generate_bundle.go
new file mode 100644
index 000000000..78e97414a
--- /dev/null
+++ b/pkg/crc/machine/generate_bundle.go
@@ -0,0 +1,132 @@
+package machine
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/code-ready/crc/pkg/crc/cluster"
+	"github.com/code-ready/crc/pkg/crc/constants"
+	"github.com/code-ready/crc/pkg/crc/logging"
+	"github.com/code-ready/crc/pkg/crc/machine/bundle"
+	"github.com/code-ready/crc/pkg/crc/oc"
+	crcssh "github.com/code-ready/crc/pkg/crc/ssh"
+	"github.com/code-ready/machine/libmachine/state"
+	"github.com/pkg/errors"
+)
+
+func (client *client) GenerateBundle() error {
+	bundleMetadata, sshRunner, err := loadVM(client)
+	if err != nil {
+		return err
+	}
+	defer sshRunner.Close()
+
+	ocConfig := oc.UseOCWithSSH(sshRunner)
+	if err := cluster.RemovePullSecretFromCluster(ocConfig, sshRunner); err != nil {
+		return errors.Wrap(err, "Error loading bundle metadata")
+	}
+
+	// Stop the cluster
+	currentState, err := client.Stop()
+	if err != nil {
+		return err
+	}
+	if currentState != state.Stopped {
+		return fmt.Errorf("VM is not stopped, current state is %s", currentState.String())
+	}
+
+	tmpBaseDir, err := ioutil.TempDir("", "crc_custom_bundle")
+	if err != nil {
+		return err
+	}
+	defer os.RemoveAll(tmpBaseDir)
+
+	// Create the custom bundle directory which is used as top level directory for tarball during compression
+	customBundleNameWithoutExtension := bundle.GetCustomBundleNameWithoutExtension(bundleMetadata.GetBundleName())
+
+	copier, err := bundle.NewCopier(bundleMetadata, tmpBaseDir, customBundleNameWithoutExtension)
+	if err != nil {
+		return err
+	}
+	defer copier.Cleanup() //nolint
+	customBundleDir := copier.CachedPath()
+
+	if err := copier.CopyKubeConfig(); err != nil {
+		return err
+	}
+
+	kubePassword, err := cluster.GetKubeadminPassword(bundleMetadata)
+	if err != nil {
+		return err
+	}
+	if err := copier.SetKubeAdminPassword(kubePassword); err != nil {
+		return err
+	}
+
+	if err := copier.CopyPrivateSSHKey(constants.GetPrivateKeyPath()); err != nil {
+		return err
+	}
+
+	if err := copier.CopyFilesFromFileList(); err != nil {
+		return err
+	}
+
+	// Copy disk image
+	logging.Infof("Copying the disk image to %s", customBundleNameWithoutExtension)
+	logging.Debugf("Absolute path of custom bundle directory: %s", customBundleDir)
+	diskPath, diskFormat, err := copyDiskImage(customBundleDir, bundleMetadata.GetDiskImagePath())
+	if err != nil {
+		return err
+	}
+
+	if err := copier.SetDiskImage(diskPath, diskFormat); err != nil {
+		return err
+	}
+
+	if err := copier.GenerateBundle(customBundleNameWithoutExtension); err != nil {
+		return err
+	}
+	cwd, err := os.Getwd()
+	if err != nil {
+		return err
+	}
+	logging.Infof("Bundle is generated in %s", filepath.Join(cwd, bundle.GetCustomBundle(customBundleNameWithoutExtension)))
+	logging.Infof("You need to perform 'crc delete' and 'crc start -b %s' to use this bundle", filepath.Join(cwd, bundle.GetCustomBundle(customBundleNameWithoutExtension)))
+	return nil
+}
+
+func loadVM(client *client) (*bundle.CrcBundleInfo, *crcssh.Runner, error) {
+	libMachineAPIClient, cleanup := createLibMachineClient()
+	defer cleanup()
+
+	host, err := libMachineAPIClient.Load(client.name)
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "Cannot load machine")
+	}
+
+	currentState, err := host.Driver.GetState()
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "Cannot get machine state")
+	}
+	if currentState != state.Running {
+		return nil, nil, errors.New("machine is not running")
+	}
+
+	crcBundleMetadata, err := getBundleMetadataFromDriver(host.Driver)
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "Error loading bundle metadata")
+	}
+
+	instanceIP, err := getIP(host, client.useVSock())
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "Error getting the IP")
+	}
+	sshRunner, err := crcssh.CreateRunner(instanceIP, getSSHPort(client.useVSock()), crcBundleMetadata.GetSSHKeyPath(), constants.GetPrivateKeyPath(), constants.GetRsaPrivateKeyPath())
+	if err != nil {
+		return nil, nil, errors.Wrap(err, "Error creating the ssh client")
+	}
+
+	return crcBundleMetadata, sshRunner, nil
+}
diff --git a/pkg/crc/machine/generate_bundle_linux.go b/pkg/crc/machine/generate_bundle_linux.go
new file mode 100644
index 000000000..1a35bed19
--- /dev/null
+++ b/pkg/crc/machine/generate_bundle_linux.go
@@ -0,0 +1,50 @@
+package machine
+
+import (
+	"os"
+	"path/filepath"
+
+	"github.com/code-ready/crc/pkg/crc/constants"
+	crcos "github.com/code-ready/crc/pkg/os"
+)
+
+func copyDiskImage(destDir string, srcDir string) (string, string, error) {
+	tmpDir := filepath.Join(constants.MachineCacheDir, "image-rebase")
+	_ = os.RemoveAll(tmpDir) // clean up before using it
+	if err := os.Mkdir(tmpDir, 0700); err != nil {
+		return "", "", err
+	}
+	defer func() {
+		_ = os.RemoveAll(tmpDir) // clean up after using it
+	}()
+
+	// We copy the overlay file because before doing the commit, we need to adjust the backing file.
+	baseImageName := filepath.Base(srcDir)
+	srcOverlayDiskPath := filepath.Join(constants.MachineInstanceDir, constants.DefaultName, baseImageName)
+	destOverlayDiskPath := filepath.Join(tmpDir, "overlayImage.qcow2")
+	if err := crcos.CopyFileContents(srcOverlayDiskPath, destOverlayDiskPath, 0644); err != nil {
+		return "", "", err
+	}
+
+	// We copy the base image because this is where we are going to commit the changes which were made to the VM
+	if err := crcos.CopyFileContents(srcDir,
+		filepath.Join(tmpDir, baseImageName), 0644); err != nil {
+		return "", "", err
+	}
+
+	// Use qemu-img commands to rebase and commit it.
+	format := "qcow2"
+	if _, _, err := crcos.RunWithDefaultLocale("qemu-img", "rebase", "-F", format, "-b",
+		filepath.Join(tmpDir, baseImageName), destOverlayDiskPath); err != nil {
+		return "", "", err
+	}
+	if _, _, err := crcos.RunWithDefaultLocale("qemu-img", "commit", destOverlayDiskPath); err != nil {
+		return "", "", err
+	}
+
+	// Copy the final image to destination dir
+	if err := crcos.CopyFileContents(filepath.Join(tmpDir, baseImageName), filepath.Join(destDir, baseImageName), 0644); err != nil {
+		return "", "", err
+	}
+	return filepath.Join(destDir, baseImageName), format, nil
+}
diff --git a/pkg/crc/machine/generate_bundle_nonlinux.go b/pkg/crc/machine/generate_bundle_nonlinux.go
new file mode 100644
index 000000000..f723a309a
--- /dev/null
+++ b/pkg/crc/machine/generate_bundle_nonlinux.go
@@ -0,0 +1,12 @@
+// +build !linux
+
+package machine
+
+import (
+	"fmt"
+	"runtime"
+)
+
+func copyDiskImage(dirName string, srcDir string) (string, string, error) {
+	return "", "", fmt.Errorf("Not implemented for %s", runtime.GOOS)
+}
diff --git a/pkg/crc/machine/start.go b/pkg/crc/machine/start.go
index 54004ef97..025fe3ce1 100644
--- a/pkg/crc/machine/start.go
+++ b/pkg/crc/machine/start.go
@@ -186,9 +186,9 @@ func (client *client) Start(ctx context.Context, startConfig types.StartConfig)
 
 		// Retrieve metadata info
 		machineConfig.ImageSourcePath = crcBundleMetadata.GetDiskImagePath()
-		machineConfig.ImageFormat = crcBundleMetadata.Storage.DiskImages[0].Format
+		machineConfig.ImageFormat = crcBundleMetadata.GetDiskImageFormat()
 		machineConfig.SSHKeyPath = crcBundleMetadata.GetSSHKeyPath()
-		machineConfig.KernelCmdLine = crcBundleMetadata.Nodes[0].KernelCmdLine
+		machineConfig.KernelCmdLine = crcBundleMetadata.GetKernelCommandLine()
 		machineConfig.Initramfs = crcBundleMetadata.GetInitramfsPath()
 		machineConfig.Kernel = crcBundleMetadata.GetKernelPath()
 
diff --git a/pkg/crc/machine/sync.go b/pkg/crc/machine/sync.go
index 4fe952967..874e7b8b6 100644
--- a/pkg/crc/machine/sync.go
+++ b/pkg/crc/machine/sync.go
@@ -169,3 +169,7 @@ func (s *Synchronized) Status() (*types.ClusterStatusResult, error) {
 func (s *Synchronized) IsRunning() (bool, error) {
 	return s.underlying.IsRunning()
 }
+
+func (s *Synchronized) GenerateBundle() error {
+	return s.underlying.GenerateBundle()
+}
diff --git a/pkg/crc/validation/validation.go b/pkg/crc/validation/validation.go
index 8a3bb78d1..12e6f3027 100644
--- a/pkg/crc/validation/validation.go
+++ b/pkg/crc/validation/validation.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
+	"strings"
 
 	"github.com/code-ready/crc/pkg/crc/constants"
 	"github.com/code-ready/crc/pkg/crc/logging"
@@ -58,16 +59,24 @@ func ValidateEnoughMemory(value int) error {
 }
 
 // ValidateBundlePath checks if the provided bundle path exist
-func ValidateBundlePath(bundle string) error {
-	if err := ValidatePath(bundle); err != nil {
+func ValidateBundlePath(bundlePath string) error {
+	if err := ValidatePath(bundlePath); err != nil {
 		if constants.BundleEmbedded() {
 			return fmt.Errorf("Run 'crc setup' to unpack the bundle to disk")
 		}
-		return fmt.Errorf("%s not found, please provide the path to a valid bundle using the -b option", bundle)
+		return fmt.Errorf("%s not found, please provide the path to a valid bundle using the -b option", bundlePath)
 	}
 
-	userProvidedBundle := filepath.Base(bundle)
+	userProvidedBundle := filepath.Base(bundlePath)
 	if userProvidedBundle != constants.GetDefaultBundle() {
+		// Should append underscore (_) here, as we don't want crc_libvirt_4.7.15.crcbundle
+		// to be detected as a custom bundle for crc_libvirt_4.7.1.crcbundle
+		usingCustomBundle := strings.HasPrefix(bundle.GetBundleNameWithoutExtension(userProvidedBundle),
+			fmt.Sprintf("%s_", bundle.GetBundleNameWithoutExtension(constants.GetDefaultBundle())))
+		if usingCustomBundle {
+			logging.Warnf("Using custom bundle %s", userProvidedBundle)
+			return nil
+		}
 		if !constants.IsRelease() {
 			logging.Warnf("Using unsupported bundle %s", userProvidedBundle)
 			return nil


Test patch:
diff --git a/pkg/compress/compress_test.go b/pkg/compress/compress_test.go
new file mode 100644
index 000000000..1d1314490
--- /dev/null
+++ b/pkg/compress/compress_test.go
@@ -0,0 +1,144 @@
+package compress
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"runtime"
+	"testing"
+
+	"github.com/code-ready/crc/pkg/crc/logging"
+	"github.com/code-ready/crc/pkg/extract"
+
+	"github.com/stretchr/testify/require"
+)
+
+const testArchiveName = "testdata.tar.zst"
+
+var (
+	files fileMap = map[string]string{
+		filepath.Join("a"):      "a",
+		filepath.Join("c"):      "c",
+		filepath.Join("b", "d"): "d",
+	}
+)
+
+func testCompress(t *testing.T, baseDir string) {
+	// This is useful to check that the top-level directory of the archive
+	// was created with the correct permissions, and was not created by the
+	// `os.MkdirAll(0750)` call at the beginning of `untarFile`
+	require.NoError(t, os.Chmod(baseDir, 0700))
+	require.NoError(t, Compress(baseDir, testArchiveName))
+	defer os.Remove(testArchiveName)
+
+	destDir, err := ioutil.TempDir("", "testdata-extracted")
+	require.NoError(t, err)
+	defer os.RemoveAll(destDir)
+
+	fileList, err := extract.Uncompress(testArchiveName, destDir, false)
+	require.NoError(t, err)
+
+	_, d := filepath.Split(baseDir)
+	fi, err := os.Stat(filepath.Join(destDir, d))
+	require.NoError(t, err)
+	fMode := os.FileMode(0700)
+	if runtime.GOOS == "windows" {
+		// https://golang.org/pkg/os/#Chmod
+		fMode = os.FileMode(0777)
+	}
+	require.Equal(t, fi.Mode().Perm(), fMode)
+
+	require.NoError(t, checkFileList(filepath.Join(destDir, d), fileList, files))
+	require.NoError(t, checkFiles(filepath.Join(destDir, d), files))
+}
+
+func TestCompressRelative(t *testing.T) {
+	// Test with relative path
+	testCompress(t, "testdata")
+
+	// Test with absolute path
+	currentDir, err := os.Getwd()
+	require.NoError(t, err)
+	testCompress(t, filepath.Join(currentDir, "testdata"))
+}
+
+/* The code below is duplicated from pkg/extract/extract_test.go */
+type fileMap map[string]string
+
+func copyFileMap(orig fileMap) fileMap {
+	copy := fileMap{}
+	for key, value := range orig {
+		copy[key] = value
+	}
+	return copy
+}
+
+// This checks that the list of files returned by Uncompress matches what we expect
+func checkFileList(destDir string, extractedFiles []string, expectedFiles fileMap) error {
+	// We are going to remove elements from  the map, but we don't want to modify the map used by the caller
+	expectedFiles = copyFileMap(expectedFiles)
+
+	for _, file := range extractedFiles {
+		rel, err := filepath.Rel(destDir, file)
+		if err != nil {
+			return err
+		}
+		_, found := expectedFiles[rel]
+		if !found {
+			return fmt.Errorf("Unexpected file '%s' in file list %v", rel, expectedFiles)
+		}
+		delete(expectedFiles, rel)
+	}
+
+	if len(expectedFiles) != 0 {
+		return fmt.Errorf("Some expected files were not in file list: %v", expectedFiles)
+	}
+
+	return nil
+}
+
+// This checks that the files in the destination directory matches what we expect
+func checkFiles(destDir string, files fileMap) error {
+	// We are going to remove elements from  the map, but we don't want to modify the map used by the caller
+	files = copyFileMap(files)
+
+	err := filepath.Walk(destDir, func(path string, info os.FileInfo, err error) error {
+		logging.Debugf("Walking %s", path)
+		if err != nil {
+			return err
+		}
+		if info.IsDir() {
+			logging.Debugf("Skipping directory %s", path)
+			return nil
+		}
+		logging.Debugf("Checking file %s", path)
+		archivePath, err := filepath.Rel(destDir, path)
+		if err != nil {
+			return err
+		}
+		expectedContent, found := files[archivePath]
+		if !found {
+			return fmt.Errorf("Unexpected extracted file '%s'", path)
+		}
+		delete(files, archivePath)
+
+		data, err := ioutil.ReadFile(path) // #nosec G304
+		if err != nil {
+			return err
+		}
+		if string(data) != expectedContent {
+			return fmt.Errorf("Unexpected content for '%s': expected [%s], got [%s]", path, expectedContent, string(data))
+		}
+		logging.Debugf("'%s' successfully checked", path)
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+	if len(files) != 0 {
+		return fmt.Errorf("Some expected files were not extracted: %v", files)
+	}
+
+	return nil
+}
diff --git a/pkg/compress/testdata/a b/pkg/compress/testdata/a
new file mode 100644
index 000000000..2e65efe2a
--- /dev/null
+++ b/pkg/compress/testdata/a
@@ -0,0 +1,1 @@
+a
\ No newline at end of file
diff --git a/pkg/compress/testdata/b/d b/pkg/compress/testdata/b/d
new file mode 100644
index 000000000..c59d9b634
--- /dev/null
+++ b/pkg/compress/testdata/b/d
@@ -0,0 +1,1 @@
+d
\ No newline at end of file
diff --git a/pkg/compress/testdata/c b/pkg/compress/testdata/c
new file mode 100644
index 000000000..3410062ba
--- /dev/null
+++ b/pkg/compress/testdata/c
@@ -0,0 +1,1 @@
+c
\ No newline at end of file
diff --git a/pkg/crc/machine/bundle/copier_test.go b/pkg/crc/machine/bundle/copier_test.go
new file mode 100644
index 000000000..c21a3531e
--- /dev/null
+++ b/pkg/crc/machine/bundle/copier_test.go
@@ -0,0 +1,72 @@
+package bundle
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/code-ready/crc/pkg/crc/constants"
+	crcos "github.com/code-ready/crc/pkg/os"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGenerateBundle(t *testing.T) {
+	var b CrcBundleInfo
+	assert.NoError(t, json.Unmarshal([]byte(reference), &b))
+	b.bundleName = "crc_4.7.1"
+	b.Storage.Files[0].Name = constants.OcExecutableName
+
+	tmpBundleDir, err := ioutil.TempDir("", "bundle_data")
+	assert.NoError(t, err)
+	b.cachedPath = filepath.Join(tmpBundleDir, b.bundleName)
+	defer os.RemoveAll(tmpBundleDir)
+
+	createDummyBundleFiles(t, &b)
+
+	srcDir, err := ioutil.TempDir("", "testdata")
+	assert.NoError(t, err)
+	defer os.RemoveAll(srcDir)
+
+	customBundleName := "custom_bundle"
+	copier, err := NewCopier(&b, srcDir, customBundleName)
+	assert.NoError(t, err)
+
+	assert.NoError(t, copier.CopyKubeConfig())
+
+	assert.NoError(t, copier.SetKubeAdminPassword("****-****-****"))
+	assert.NoError(t, copier.CopyPrivateSSHKey(copier.srcBundle.GetSSHKeyPath()))
+	assert.NoError(t, copier.CopyFilesFromFileList())
+
+	err = crcos.CopyFileContents(copier.srcBundle.GetDiskImagePath(), copier.copiedBundle.GetDiskImagePath(), 0644)
+	assert.NoError(t, err)
+	assert.NoError(t, copier.SetDiskImage(copier.copiedBundle.GetDiskImagePath(), "qcow2"))
+
+	assert.NoError(t, copier.GenerateBundle(customBundleName))
+	defer os.Remove(fmt.Sprintf("%s%s", customBundleName, bundleExtension))
+}
+
+func createDummyBundleFiles(t *testing.T, bundle *CrcBundleInfo) {
+	assert.NoError(t, os.MkdirAll(bundle.cachedPath, 0750))
+
+	files := []string{
+		bundle.GetOcPath(),
+		bundle.resolvePath(bundle.ClusterInfo.KubeadminPasswordFile),
+		bundle.GetKubeConfigPath(),
+		bundle.GetSSHKeyPath(),
+		bundle.GetDiskImagePath(),
+		bundle.GetKernelPath(),
+		bundle.GetInitramfsPath(),
+	}
+
+	for _, file := range files {
+		if file == "" {
+			continue
+		}
+		fd, err := os.Create(file)
+		fd.Close()
+		assert.NoError(t, err)
+	}
+}
diff --git a/pkg/crc/machine/sync_test.go b/pkg/crc/machine/sync_test.go
index e29e0e9e8..461afeadd 100644
--- a/pkg/crc/machine/sync_test.go
+++ b/pkg/crc/machine/sync_test.go
@@ -165,3 +165,7 @@ func (m *waitingMachine) Stop() (state.State, error) {
 	<-m.stopCompleteCh
 	return state.Stopped, nil
 }
+
+func (m *waitingMachine) GenerateBundle() error {
+	return errors.New("not implemented")
+}
diff --git a/test/e2e/crcsuite/crcsuite.go b/test/e2e/crcsuite/crcsuite.go
index 7214a346b..e96aa7431 100644
--- a/test/e2e/crcsuite/crcsuite.go
+++ b/test/e2e/crcsuite/crcsuite.go
@@ -36,6 +36,8 @@ func FeatureContext(s *godog.Suite) {
 		RemoveCRCHome)
 	s.Step(`^starting CRC with default bundle (succeeds|fails)$`,
 		StartCRCWithDefaultBundleSucceedsOrFails)
+	s.Step(`^starting CRC with custom bundle (succeeds|fails)$`,
+		StartCRCWithCustomBundleSucceedsOrFails)
 	s.Step(`^starting CRC with default bundle along with stopped network time synchronization (succeeds|fails)$`,
 		StartCRCWithDefaultBundleWithStopNetworkTimeSynchronizationSucceedsOrFails)
 	s.Step(`^starting CRC with default bundle and nameserver "(.*)" (succeeds|fails)$`,
@@ -346,6 +348,11 @@ func StartCRCWithDefaultBundleWithStopNetworkTimeSynchronizationSucceedsOrFails(
 	return err
 }
 
+func StartCRCWithCustomBundleSucceedsOrFails(expected string) error {
+	cmd := fmt.Sprintf("crc start -p '%s' -b *.crcbundle --log-level debug", pullSecretFile)
+	return clicumber.ExecuteCommandSucceedsOrFails(cmd, expected)
+}
+
 func StartCRCWithDefaultBundleAndNameServerSucceedsOrFails(nameserver string, expected string) error {
 
 	var extraBundleArgs string
diff --git a/test/e2e/features/basic.feature b/test/e2e/features/basic.feature
index dd0e6cd53..42ed9e7b9 100644
--- a/test/e2e/features/basic.feature
+++ b/test/e2e/features/basic.feature
@@ -135,7 +135,14 @@ Feature: Basic test
         And unsetting config property "enable-cluster-monitoring" succeeds
         And unsetting config property "memory" succeeds
 
-    @darwin @linux @windows
+    @linux
+    Scenario: Bundle generation check
+        # This will remove the pull secret from the instance and from the cluster
+        # You need to provide pull secret file again if you want to start this cluster
+        # from an stopped state.
+        Given executing "crc bundle generate" succeeds
+
+    @darwin @windows
     Scenario: CRC forcible stop
         When executing "crc stop -f"
         Then stdout should match "(.*)[Ss]topped the OpenShift cluster"
@@ -156,6 +163,11 @@ Feature: Basic test
         When executing "crc delete -f" succeeds
         Then stdout should contain "Deleted the OpenShift cluster"
 
+    @linux
+    Scenario: CRC starts with generated bundle
+        Given starting CRC with custom bundle succeeds
+        Then stderr should contain "Using custom bundle"
+
     @darwin
     Scenario Outline: CRC clean-up
         When executing "crc cleanup" succeeds