Repository: code-ready/crc
Pull request #2289 (2021-04-29 07:19:50)

[E2E] Remove e2e test for skip-check-hosts-file-permissions 
This options was recently removed on #2282.

This PR fixes the e2e which was testing that property.

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

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

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

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

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

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["jsliacan"]} -->
centos-ci: Can one of the admins verify this patch?
praveenkumar: /lgtm

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/cmd/crc/cmd/setup.go b/cmd/crc/cmd/setup.go
index 35aa39649..fe18745a6 100644
--- a/cmd/crc/cmd/setup.go
+++ b/cmd/crc/cmd/setup.go
@@ -54,17 +54,13 @@ func runSetup(arguments []string) error {
 			fmt.Printf("No worry, you can still enable telemetry manually with the command 'crc config set %s yes'.\n", cmdConfig.ConsentTelemetry)
 		}
 	}
-	var err error
-	if checkOnly {
-		err = preflight.StartPreflightChecks(config)
-		if err != nil {
-			err = exec.CodeExitError{
-				Err:  err,
-				Code: preflightFailedExitCode,
-			}
+
+	err := preflight.SetupHost(config, checkOnly)
+	if err != nil && checkOnly {
+		err = exec.CodeExitError{
+			Err:  err,
+			Code: preflightFailedExitCode,
 		}
-	} else {
-		err = preflight.SetupHost(config)
 	}
 
 	return render(&setupResult{
diff --git a/pkg/compress/compress.go b/pkg/compress/compress.go
new file mode 100644
index 000000000..bed9498cd
--- /dev/null
+++ b/pkg/compress/compress.go
@@ -0,0 +1,71 @@
+package compress
+
+import (
+	"archive/tar"
+	"io"
+	"os"
+	"path/filepath"
+
+	"github.com/code-ready/crc/pkg/crc/logging"
+	"github.com/klauspost/compress/zstd"
+)
+
+func Compress(src, dest string) (err error) {
+	out, err := os.Create(dest)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		cerr := out.Close()
+		if err == nil {
+			err = cerr
+		}
+	}()
+
+	enc, err := zstd.NewWriter(out)
+	if err != nil {
+		return err
+	}
+	defer enc.Close()
+
+	tarWriter := tar.NewWriter(enc)
+	defer tarWriter.Close()
+
+	basePath, _ := filepath.Split(src)
+
+	// Just use top level directory as part of tarball
+	// $ zstdcat crc_libvirt_4.7.1_custom.zstd  | tar t
+	// crc_libvirt_4.7.1_custom
+	// crc_libvirt_4.7.1_custom/test
+	return filepath.Walk(src, func(file string, fi os.FileInfo, err1 error) error {
+		if err1 != nil {
+			return err1
+		}
+		logging.Debugf("Adding %s", file)
+		header, err := tar.FileInfoHeader(fi, file)
+		if err != nil {
+			return err
+		}
+		// must provide real name
+		// (see https://golang.org/src/archive/tar/common.go?#L626)
+		header.Name, err = filepath.Rel(basePath, filepath.ToSlash(file))
+		if err != nil {
+			return err
+		}
+
+		if err := tarWriter.WriteHeader(header); err != nil {
+			return err
+		}
+		if !fi.IsDir() {
+			data, err := os.Open(file)
+			if err != nil {
+				return err
+			}
+			defer data.Close()
+			if _, err := io.Copy(tarWriter, data); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
diff --git a/pkg/crc/cluster/cluster.go b/pkg/crc/cluster/cluster.go
index d8bb9e942..2a62bb52e 100644
--- a/pkg/crc/cluster/cluster.go
+++ b/pkg/crc/cluster/cluster.go
@@ -126,6 +126,35 @@ func EnsurePullSecretPresentInTheCluster(ocConfig oc.Config, pullSec PullSecretL
 	return nil
 }
 
+func RemovePullSecretFromCluster(ocConfig oc.Config, sshRunner *ssh.Runner) error {
+	logging.Info("Removing user's pull secret from instance disk and from cluster secret...")
+	if _, _, err := sshRunner.RunPrivileged("Removing user's pull secret", "rm", "-fr", vmPullSecretPath); err != nil {
+		return err
+	}
+	cmdArgs := []string{"patch", "secret", "pull-secret", "-p",
+		`'{"data":{".dockerconfigjson":"e30K"}}'`,
+		"-n", "openshift-config", "--type", "merge"}
+
+	_, stderr, err := ocConfig.RunOcCommand(cmdArgs...)
+	if err != nil {
+		return fmt.Errorf("Failed to remove Pull secret %w: %s", err, stderr)
+	}
+
+	cmdArgs = []string{"delete", "machineconfigs", "--all"}
+	_, stderr, err = ocConfig.RunOcCommand(cmdArgs...)
+	if err != nil {
+		return fmt.Errorf("Failed to remove machineconfigs %w: %s", err, stderr)
+	}
+
+	cmdArgs = []string{"delete", "machineconfigpools", "--all"}
+	_, stderr, err = ocConfig.RunOcCommand(cmdArgs...)
+	if err != nil {
+		return fmt.Errorf("Failed to remove machineconfigpools %w: %s", err, stderr)
+	}
+
+	return nil
+}
+
 func EnsureClusterIDIsNotEmpty(ocConfig oc.Config) error {
 	if err := WaitForOpenshiftResource(ocConfig, "clusterversion"); err != nil {
 		return err
diff --git a/pkg/crc/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/preflight/preflight.go b/pkg/crc/preflight/preflight.go
index c94dd265c..a2f93eac6 100644
--- a/pkg/crc/preflight/preflight.go
+++ b/pkg/crc/preflight/preflight.go
@@ -101,7 +101,7 @@ func doPreflightChecks(config config.Storage, checks []Check) error {
 	return nil
 }
 
-func doFixPreflightChecks(config config.Storage, checks []Check) error {
+func doFixPreflightChecks(config config.Storage, checks []Check, checkOnly bool) error {
 	for _, check := range checks {
 		if check.flags&CleanUpOnly == CleanUpOnly {
 			continue
@@ -109,6 +109,8 @@ func doFixPreflightChecks(config config.Storage, checks []Check) error {
 		err := check.doCheck(config)
 		if err == nil {
 			continue
+		} else if checkOnly {
+			return err
 		}
 		if err = check.doFix(); err != nil {
 			return err
@@ -160,11 +162,11 @@ func StartPreflightChecks(config config.Storage) error {
 }
 
 // SetupHost performs the prerequisite checks and setups the host to run the cluster
-func SetupHost(config config.Storage) error {
+func SetupHost(config config.Storage, checkOnly bool) error {
 	experimentalFeatures := config.Get(cmdConfig.ExperimentalFeatures).AsBool()
 	mode := network.ParseMode(config.Get(cmdConfig.NetworkMode).AsString())
 	trayAutostart := config.Get(cmdConfig.AutostartTray).AsBool()
-	return doFixPreflightChecks(config, getPreflightChecks(experimentalFeatures, trayAutostart, mode))
+	return doFixPreflightChecks(config, getPreflightChecks(experimentalFeatures, trayAutostart, mode), checkOnly)
 }
 
 func RegisterSettings(config config.Schema) {
diff --git a/pkg/crc/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/pkg/crc/preflight/preflight_test.go b/pkg/crc/preflight/preflight_test.go
index c624566ca..74aa312ef 100644
--- a/pkg/crc/preflight/preflight_test.go
+++ b/pkg/crc/preflight/preflight_test.go
@@ -34,11 +34,21 @@ func TestFixPreflight(t *testing.T) {
 	cfg := config.New(config.NewEmptyInMemoryStorage())
 	doRegisterSettings(cfg, []Check{*check})
 
-	assert.NoError(t, doFixPreflightChecks(cfg, []Check{*check}))
+	assert.NoError(t, doFixPreflightChecks(cfg, []Check{*check}, false))
 	assert.True(t, calls.checked)
 	assert.True(t, calls.fixed)
 }
 
+func TestFixPreflightCheckOnly(t *testing.T) {
+	check, calls := sampleCheck(errors.New("check failed"), nil)
+	cfg := config.New(config.NewEmptyInMemoryStorage())
+	doRegisterSettings(cfg, []Check{*check})
+
+	assert.Error(t, doFixPreflightChecks(cfg, []Check{*check}, true))
+	assert.True(t, calls.checked)
+	assert.False(t, calls.fixed)
+}
+
 func sampleCheck(checkErr, fixErr error) (*Check, *status) {
 	status := &status{}
 	return &Check{
diff --git a/test/e2e/crcsuite/crcsuite.go b/test/e2e/crcsuite/crcsuite.go
index 7214a346b..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
diff --git a/test/e2e/features/config.feature b/test/e2e/features/config.feature
index b24d24f24..5722a2ce9 100644
--- a/test/e2e/features/config.feature
+++ b/test/e2e/features/config.feature
@@ -74,7 +74,6 @@ Feature: Test configuration settings
         Examples:
             | property                             | value1 | value2 |
             | skip-check-bundle-extracted          | true   | false  |
-            | skip-check-hosts-file-permissions    | true   | false  |
             | skip-check-hyperkit-driver           | true   | false  |
             | skip-check-hyperkit-installed        | true   | false  |
             | skip-check-resolver-file-permissions | true   | false  |