Repository: code-ready/crc
Pull request #1441 (2020-08-05 15:24:40)

sshRunner API cleanups and fix small error in NTP logs
The first 3 commits are merely minor code cleanups/improvements, the last commit fixes an erroneous log message, which says the NTP service is being stopped, when it's being started.

## Proposed changes

List main as well as consequential changes you introduced or had to introduce.

1. Add ssh.CopyFile
2. Merge ssh.CopyData and ssh.SetTextContentAsRoot
3. Improve variable name
4. Fix log message

## Testing

Check that during cluster startup (creation?), there's a `Starting network time synchronization in CodeReady Containers VM` log message, not a `Stopping network time synchronization in CodeReady Containers VM` log.

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

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

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

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

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

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

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
openshift-ci-robot: @cfergeau: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<details>

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

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

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

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

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

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

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

Patch:
diff --git a/cmd/crc/cmd/json.go b/cmd/crc/cmd/json.go
new file mode 100644
index 000000000..e988d901c
--- /dev/null
+++ b/cmd/crc/cmd/json.go
@@ -0,0 +1,36 @@
+package cmd
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+
+	"github.com/spf13/cobra"
+)
+
+const jsonFormat = "json"
+
+var (
+	outputFormat string
+)
+
+func addOutputFormatFlag(cmd *cobra.Command) {
+	cmd.Flags().StringVarP(&outputFormat, "output", "o", "", "Output format. One of: json")
+}
+
+type prettyPrintable interface {
+	prettyPrintTo(writer io.Writer) error
+}
+
+func render(obj prettyPrintable, writer io.Writer, outputFormat string) error {
+	switch outputFormat {
+	case jsonFormat:
+		encoder := json.NewEncoder(writer)
+		encoder.SetIndent("", "  ")
+		return encoder.Encode(obj)
+	case "":
+		return obj.prettyPrintTo(writer)
+	default:
+		return fmt.Errorf("invalid format: %s", outputFormat)
+	}
+}
diff --git a/cmd/crc/cmd/root.go b/cmd/crc/cmd/root.go
index 3c05bcd05..b7433b413 100644
--- a/cmd/crc/cmd/root.go
+++ b/cmd/crc/cmd/root.go
@@ -57,7 +57,8 @@ func runPrerun() {
 	// Setting up logrus
 	logging.InitLogrus(logging.LogLevel, constants.LogFilePath)
 	setProxyDefaults()
-	for _, str := range GetVersionStrings() {
+
+	for _, str := range defaultVersion().lines() {
 		logging.Debugf(str)
 	}
 }
diff --git a/cmd/crc/cmd/start.go b/cmd/crc/cmd/start.go
index 64f3d51fa..1db24f7a0 100644
--- a/cmd/crc/cmd/start.go
+++ b/cmd/crc/cmd/start.go
@@ -15,7 +15,7 @@ import (
 	"github.com/code-ready/crc/pkg/crc/output"
 	"github.com/code-ready/crc/pkg/crc/preflight"
 	"github.com/code-ready/crc/pkg/crc/validation"
-	"github.com/code-ready/crc/pkg/crc/version"
+	crcversion "github.com/code-ready/crc/pkg/crc/version"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 )
@@ -141,7 +141,7 @@ func checkIfNewVersionAvailable(noUpdateCheck bool) {
 	if noUpdateCheck {
 		return
 	}
-	isNewVersionAvailable, newVersion, err := version.NewVersionAvailable()
+	isNewVersionAvailable, newVersion, err := crcversion.NewVersionAvailable()
 	if err != nil {
 		logging.Debugf("Unable to find out if a new version is available: %v", err)
 		return
diff --git a/cmd/crc/cmd/status.go b/cmd/crc/cmd/status.go
index f259d9534..5dadfd01d 100644
--- a/cmd/crc/cmd/status.go
+++ b/cmd/crc/cmd/status.go
@@ -1,7 +1,6 @@
 package cmd
 
 import (
-	"encoding/json"
 	"fmt"
 	"io"
 	"os"
@@ -15,14 +14,8 @@ import (
 	"github.com/spf13/cobra"
 )
 
-const jsonFormat = "json"
-
-var (
-	outputFormat string
-)
-
 func init() {
-	statusCmd.Flags().StringVarP(&outputFormat, "output", "o", "", "Output format. One of: json")
+	addOutputFormatFlag(statusCmd)
 	rootCmd.AddCommand(statusCmd)
 }
 
@@ -37,7 +30,7 @@ var statusCmd = &cobra.Command{
 	},
 }
 
-type Status struct {
+type status struct {
 	CrcStatus        string `json:"crcStatus"`
 	OpenShiftStatus  string `json:"openshiftStatus"`
 	OpenShiftVersion string `json:"openshiftVersion"`
@@ -68,7 +61,7 @@ func runStatus(writer io.Writer, client client, cacheDir, outputFormat string) e
 	if err != nil {
 		return fmt.Errorf("Error finding size of cache: %s", err.Error())
 	}
-	status := Status{
+	status := &status{
 		CrcStatus:        clusterStatus.CrcStatus,
 		OpenShiftStatus:  clusterStatus.OpenshiftStatus,
 		OpenShiftVersion: clusterStatus.OpenshiftVersion,
@@ -77,29 +70,23 @@ func runStatus(writer io.Writer, client client, cacheDir, outputFormat string) e
 		CacheUsage:       size,
 		CacheDir:         cacheDir,
 	}
-
-	if outputFormat == jsonFormat {
-		encoder := json.NewEncoder(writer)
-		encoder.SetIndent("", "  ")
-		return encoder.Encode(status)
-	}
-	return printStatus(writer, status)
+	return render(status, writer, outputFormat)
 }
 
-func printStatus(writer io.Writer, status Status) error {
+func (s *status) prettyPrintTo(writer io.Writer) error {
 	w := tabwriter.NewWriter(writer, 0, 0, 1, ' ', 0)
 
 	lines := []struct {
 		left, right string
 	}{
-		{"CRC VM", status.CrcStatus},
-		{"OpenShift", openshiftStatus(status)},
+		{"CRC VM", s.CrcStatus},
+		{"OpenShift", openshiftStatus(s)},
 		{"Disk Usage", fmt.Sprintf(
 			"%s of %s (Inside the CRC VM)",
-			units.HumanSize(float64(status.DiskUsage)),
-			units.HumanSize(float64(status.DiskSize)))},
-		{"Cache Usage", units.HumanSize(float64(status.CacheUsage))},
-		{"Cache Directory", status.CacheDir},
+			units.HumanSize(float64(s.DiskUsage)),
+			units.HumanSize(float64(s.DiskSize)))},
+		{"Cache Usage", units.HumanSize(float64(s.CacheUsage))},
+		{"Cache Directory", s.CacheDir},
 	}
 	for _, line := range lines {
 		if err := printLine(w, line.left, line.right); err != nil {
@@ -109,7 +96,7 @@ func printStatus(writer io.Writer, status Status) error {
 	return w.Flush()
 }
 
-func openshiftStatus(status Status) string {
+func openshiftStatus(status *status) string {
 	if status.OpenShiftVersion != "" {
 		return fmt.Sprintf("%s (v%s)", status.OpenShiftStatus, status.OpenShiftVersion)
 	}
diff --git a/cmd/crc/cmd/version.go b/cmd/crc/cmd/version.go
index 2e8e59897..8f26caf60 100644
--- a/cmd/crc/cmd/version.go
+++ b/cmd/crc/cmd/version.go
@@ -2,14 +2,17 @@ package cmd
 
 import (
 	"fmt"
-	"strings"
+	"io"
+	"os"
 
 	"github.com/code-ready/crc/pkg/crc/constants"
-	"github.com/code-ready/crc/pkg/crc/version"
+	"github.com/code-ready/crc/pkg/crc/exit"
+	crcversion "github.com/code-ready/crc/pkg/crc/version"
 	"github.com/spf13/cobra"
 )
 
 func init() {
+	addOutputFormatFlag(versionCmd)
 	rootCmd.AddCommand(versionCmd)
 }
 
@@ -18,21 +21,48 @@ var versionCmd = &cobra.Command{
 	Short: "Print version information",
 	Long:  "Print version information",
 	Run: func(cmd *cobra.Command, args []string) {
-		runPrintVersion(args)
+		if err := runPrintVersion(os.Stdout, defaultVersion(), outputFormat); err != nil {
+			exit.WithMessage(1, err.Error())
+		}
 	},
 }
 
-func GetVersionStrings() []string {
+func runPrintVersion(writer io.Writer, version *version, outputFormat string) error {
+	return render(version, writer, outputFormat)
+}
+
+type version struct {
+	Version          string `json:"version"`
+	Commit           string `json:"commit"`
+	OpenshiftVersion string `json:"openshiftVersion"`
+	Embedded         bool   `json:"embedded"`
+}
+
+func defaultVersion() *version {
+	return &version{
+		Version:          crcversion.GetCRCVersion(),
+		Commit:           crcversion.GetCommitSha(),
+		OpenshiftVersion: crcversion.GetBundleVersion(),
+		Embedded:         constants.BundleEmbedded(),
+	}
+}
+
+func (v *version) prettyPrintTo(writer io.Writer) error {
+	for _, line := range v.lines() {
+		if _, err := fmt.Fprint(writer, line); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (v *version) lines() []string {
 	var embedded string
-	if !constants.BundleEmbedded() {
+	if !v.Embedded {
 		embedded = "not "
 	}
 	return []string{
-		fmt.Sprintf("CodeReady Containers version: %s+%s", version.GetCRCVersion(), version.GetCommitSha()),
-		fmt.Sprintf("OpenShift version: %s (%sembedded in binary)", version.GetBundleVersion(), embedded),
+		fmt.Sprintf("CodeReady Containers version: %s+%s\n", v.Version, v.Commit),
+		fmt.Sprintf("OpenShift version: %s (%sembedded in binary)\n", v.OpenshiftVersion, embedded),
 	}
 }
-
-func runPrintVersion(arguments []string) {
-	fmt.Println(strings.Join(GetVersionStrings(), "\n"))
-}
diff --git a/pkg/crc/cluster/cluster.go b/pkg/crc/cluster/cluster.go
index 6e0f667ec..ed1770160 100644
--- a/pkg/crc/cluster/cluster.go
+++ b/pkg/crc/cluster/cluster.go
@@ -145,11 +145,11 @@ Environment=HTTPS_PROXY=%s
 Environment=NO_PROXY=.cluster.local,.svc,10.128.0.0/14,172.30.0.0/16,%s`
 	p := fmt.Sprintf(proxyTemplate, proxy.HTTPProxy, proxy.HTTPSProxy, proxy.GetNoProxyString())
 	// This will create a systemd drop-in configuration for proxy (both for kubelet and crio services) on the VM.
-	err := sshRunner.SetTextContentAsRoot("/etc/systemd/system/crio.service.d/10-default-env.conf", p, 0644)
+	err := sshRunner.CopyData([]byte(p), "/etc/systemd/system/crio.service.d/10-default-env.conf", 0644)
 	if err != nil {
 		return err
 	}
-	err = sshRunner.SetTextContentAsRoot("/etc/systemd/system/kubelet.service.d/10-default-env.conf", p, 0644)
+	err = sshRunner.CopyData([]byte(p), "/etc/systemd/system/kubelet.service.d/10-default-env.conf", 0644)
 	if err != nil {
 		return err
 	}
@@ -157,7 +157,7 @@ Environment=NO_PROXY=.cluster.local,.svc,10.128.0.0/14,172.30.0.0/16,%s`
 }
 
 func addPullSecretToInstanceDisk(sshRunner *ssh.Runner, pullSec string) error {
-	err := sshRunner.SetTextContentAsRoot("/var/lib/kubelet/config.json", pullSec, 0600)
+	err := sshRunner.CopyData([]byte(pullSec), "/var/lib/kubelet/config.json", 0600)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/crc/exit/atexit.go b/pkg/crc/exit/atexit.go
index aaf83ba99..0c657ec34 100644
--- a/pkg/crc/exit/atexit.go
+++ b/pkg/crc/exit/atexit.go
@@ -4,27 +4,10 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/code-ready/crc/pkg/crc/output"
 )
 
-const ExitHandlerPanicMessage = "At least one exit handler vetoed to exit program execution"
-
-type exitHandlerFunc func(int) bool
-
-// exitHandlers keeps track of the list of registered exit handlers. Handlers are applied in the order defined in this list.
-var exitHandlers = []exitHandlerFunc{}
-
-// WithoutMessage runs all registered exit handlers and then exits the program with the specified exit code using os.Exit.
-func WithoutMessage(code int) {
-	veto := runHandlers(code)
-	if veto {
-		panic(ExitHandlerPanicMessage)
-	}
-	os.Exit(code)
-}
-
-// WithMessage runs all registered exit handlers, prints the specified message and then exits the program with the specified exit code.
+// WithMessage prints the specified message and then exits the program with the specified exit code.
 // If the exit code is 0, the message is prints to stdout, otherwise to stderr.
 func WithMessage(code int, text string, args ...interface{}) {
 	if code == 0 {
@@ -32,38 +15,5 @@ func WithMessage(code int, text string, args ...interface{}) {
 	} else {
 		_, _ = output.Fout(os.Stderr, fmt.Sprintf(text, args...))
 	}
-	WithoutMessage(code)
-}
-
-// Register registers an exit handler function which is run when WithoutMessage is called
-func RegisterExitHandler(exitHandler exitHandlerFunc) {
-	exitHandlers = append(exitHandlers, exitHandler)
-}
-
-// ClearExitHandler clears all registered exit handlers
-func ClearExitHandler() {
-	exitHandlers = []exitHandlerFunc{}
-}
-
-// runHandlers runs all registered exit handlers, passing on the intended exit code.
-// Handlers can veto to exit the program. If at least one exit handlers casts a veto, the program does panic instead of exiting
-// allowing for a potential recovering.
-func runHandlers(code int) bool {
-	var veto bool
-	for _, handler := range exitHandlers {
-		veto = veto || runHandler(handler, code)
-	}
-	return veto
-}
-
-// runHandler runs the single specified exit handler, returning whether this handler vetos the exit or not.
-func runHandler(exitHandler exitHandlerFunc, code int) bool {
-	defer func() {
-		err := recover()
-		if err != nil {
-			logging.Errorf("Error running exit handler: %v", err)
-		}
-	}()
-
-	return exitHandler(code)
+	os.Exit(code)
 }
diff --git a/pkg/crc/machine/machine.go b/pkg/crc/machine/machine.go
index 04311dfe3..987446692 100644
--- a/pkg/crc/machine/machine.go
+++ b/pkg/crc/machine/machine.go
@@ -246,11 +246,11 @@ func Start(startConfig StartConfig) (StartResult, error) {
 	}
 	logging.Info("CodeReady Containers VM is running")
 
-	// Start network time synchronization if `CRC_DEBUG_ENABLE_STOP_NTP` not set
-	if b, _ := strconv.ParseBool(os.Getenv("CRC_DEBUG_ENABLE_STOP_NTP")); !b {
-		logging.Info("Stopping network time synchronization in CodeReady Containers VM")
+	// Start network time synchronization if `CRC_DEBUG_ENABLE_STOP_NTP` is not set
+	if stopNtp, _ := strconv.ParseBool(os.Getenv("CRC_DEBUG_ENABLE_STOP_NTP")); !stopNtp {
+		logging.Info("Starting network time synchronization in CodeReady Containers VM")
 		if _, err := sshRunner.Run("sudo timedatectl set-ntp on"); err != nil {
-			return startError(startConfig.Name, "Failed to stop network time synchronization", err)
+			return startError(startConfig.Name, "Failed to start network time synchronization", err)
 		}
 	}
 
@@ -347,9 +347,9 @@ func Start(startConfig StartConfig) (StartResult, error) {
 		if err != nil {
 			return startError(startConfig.Name, "Error copying kubeconfig file", err)
 		}
-		// Copy kubeconfig file inside the VM
-		kubeconfigContent, _ := ioutil.ReadFile(kubeConfigFilePath)
-		_, err = sshRunner.RunPrivate(fmt.Sprintf("cat <<EOF | sudo tee /opt/kubeconfig\n%s\nEOF", string(kubeconfigContent)))
+
+		logging.Info("Copying kubeconfig file to CRC VM ...")
+		err = sshRunner.CopyFile(kubeConfigFilePath, "/opt/kubeconfig", 0644)
 		if err != nil {
 			return startError(startConfig.Name, "Error copying kubeconfig file in VM", err)
 		}
diff --git a/pkg/crc/network/nameservers.go b/pkg/crc/network/nameservers.go
index 60282cd94..8bdb6bb79 100644
--- a/pkg/crc/network/nameservers.go
+++ b/pkg/crc/network/nameservers.go
@@ -34,7 +34,7 @@ func GetResolvValuesFromInstance(sshRunner *ssh.Runner) (*ResolvFileValues, erro
 func CreateResolvFileOnInstance(sshRunner *ssh.Runner, resolvFileValues ResolvFileValues) error {
 	resolvFile, _ := CreateResolvFile(resolvFileValues)
 
-	err := sshRunner.CopyData([]byte(resolvFile), "/etc/resolv.conf")
+	err := sshRunner.CopyData([]byte(resolvFile), "/etc/resolv.conf", 0644)
 	if err != nil {
 		return fmt.Errorf("Error creating /etc/resolv on instance: %s", err.Error())
 	}
diff --git a/pkg/crc/services/dns/template.go b/pkg/crc/services/dns/template.go
index 6009277c5..56be88a44 100644
--- a/pkg/crc/services/dns/template.go
+++ b/pkg/crc/services/dns/template.go
@@ -52,7 +52,7 @@ func createDnsmasqDNSConfig(serviceConfig services.ServicePostStartConfig) error
 		return err
 	}
 
-	return serviceConfig.SSHRunner.CopyData([]byte(dnsConfig), "/var/srv/dnsmasq.conf")
+	return serviceConfig.SSHRunner.CopyData([]byte(dnsConfig), "/var/srv/dnsmasq.conf", 0644)
 }
 
 func createDNSConfigFile(values dnsmasqConfFileValues, tmpl string) (string, error) {
diff --git a/pkg/crc/ssh/ssh.go b/pkg/crc/ssh/ssh.go
index 61c21a22d..c80ca4b3d 100644
--- a/pkg/crc/ssh/ssh.go
+++ b/pkg/crc/ssh/ssh.go
@@ -3,6 +3,7 @@ package ssh
 import (
 	"encoding/base64"
 	"fmt"
+	"io/ioutil"
 	"os"
 
 	"github.com/code-ready/crc/pkg/crc/constants"
@@ -28,14 +29,6 @@ func (runner *Runner) Run(command string) (string, error) {
 	return runner.runSSHCommandFromDriver(command, false)
 }
 
-func (runner *Runner) SetTextContentAsRoot(destFilename string, content string, mode os.FileMode) error {
-	logging.Debugf("Creating %s with permissions 0%o in the CRC VM", destFilename, mode)
-	command := fmt.Sprintf("sudo install -m 0%o /dev/null %s && cat <<EOF | sudo tee %s\n%s\nEOF", mode, destFilename, destFilename, content)
-	_, err := runner.RunPrivate(command)
-
-	return err
-}
-
 func (runner *Runner) RunPrivate(command string) (string, error) {
 	return runner.runSSHCommandFromDriver(command, true)
 }
@@ -44,14 +37,23 @@ func (runner *Runner) SetPrivateKeyPath(path string) {
 	runner.privateSSHKey = path
 }
 
-func (runner *Runner) CopyData(data []byte, destFilename string) error {
+func (runner *Runner) CopyData(data []byte, destFilename string, mode os.FileMode) error {
+	logging.Debugf("Creating %s with permissions 0%o in the CRC VM", destFilename, mode)
 	base64Data := base64.StdEncoding.EncodeToString(data)
-	command := fmt.Sprintf("echo %s | base64 --decode | sudo tee %s > /dev/null", base64Data, destFilename)
-	_, err := runner.Run(command)
+	command := fmt.Sprintf("sudo install -m 0%o /dev/null %s && cat <<EOF | base64 --decode | sudo tee %s\n%s\nEOF", mode, destFilename, destFilename, base64Data)
+	_, err := runner.RunPrivate(command)
 
 	return err
 }
 
+func (runner *Runner) CopyFile(srcFilename string, destFilename string, mode os.FileMode) error {
+	data, err := ioutil.ReadFile(srcFilename)
+	if err != nil {
+		return err
+	}
+	return runner.CopyData(data, destFilename, mode)
+}
+
 func (runner *Runner) runSSHCommandFromDriver(command string, runPrivate bool) (string, error) {
 	client, err := drivers.GetSSHClientFromDriver(runner.driver, runner.privateSSHKey)
 	if err != nil {


Test patch:
diff --git a/cmd/crc/cmd/version_test.go b/cmd/crc/cmd/version_test.go
new file mode 100644
index 000000000..dd2e0c34e
--- /dev/null
+++ b/cmd/crc/cmd/version_test.go
@@ -0,0 +1,40 @@
+package cmd
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPlainVersion(t *testing.T) {
+	out := new(bytes.Buffer)
+	assert.NoError(t, runPrintVersion(out, &version{
+		Version:          "1.13",
+		Commit:           "aabbcc",
+		OpenshiftVersion: "4.5.4",
+		Embedded:         false,
+	}, ""))
+	assert.Equal(t, `CodeReady Containers version: 1.13+aabbcc
+OpenShift version: 4.5.4 (not embedded in binary)
+`, out.String())
+}
+
+func TestJsonVersion(t *testing.T) {
+	out := new(bytes.Buffer)
+	assert.NoError(t, runPrintVersion(out, &version{
+		Version:          "1.13",
+		Commit:           "aabbcc",
+		OpenshiftVersion: "4.5.4",
+		Embedded:         false,
+	}, "json"))
+
+	expected := `{
+  "version": "1.13",
+  "commit": "aabbcc",
+  "openshiftVersion": "4.5.4",
+  "embedded": false
+}
+`
+	assert.Equal(t, expected, out.String())
+}