Add podman version to odo version output (#6913)

* Show podman version in odo version; TODO: fix test and implement json

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add integration test

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add support for JSON

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add documentation

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Fix missing OpenShift version

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Add warnings when unable to fetch version information

Signed-off-by: Parthvi Vala <pvala@redhat.com>

* Do not print warning when --client is used and review

Signed-off-by: Parthvi Vala <pvala@redhat.com>

---------

Signed-off-by: Parthvi Vala <pvala@redhat.com>
This commit is contained in:
Parthvi Vala
2023-06-25 02:16:37 +05:30
committed by GitHub
parent 08eeb2de8c
commit 2bafd31b6c
9 changed files with 304 additions and 57 deletions

View File

@@ -906,4 +906,32 @@ If odo can't find any projects on the cluster that you have access to, it will s
```shell
$ odo list projects -o json
{}
```
```
## odo version -o json
The `odo version -o json` returns the version information about `odo`, cluster server and podman client.
Use `--client` flag to only obtain version information about `odo`.
```shell
odo version -o json [--client]
```
```shell
$ odo version -o json
{
"version": "v3.11.0",
"gitCommit": "ea2d256e8",
"cluster": {
"serverURL": "https://kubernetes.docker.internal:6443",
"kubernetes": {
"version": "v1.25.9"
},
"openshift": {
"version": "4.13.0"
},
},
"podman": {
"client": {
"version": "4.5.1"
}
}
}
```

View File

@@ -0,0 +1,28 @@
---
title: odo version
---
## Description
The `odo version` command returns the version information about `odo`, cluster server and podman client.
## Running the Command
The command takes an optional `--client` flag that only returns version information about `odo`.
The command will only print Openshift version if it is available.
```shell
odo version [--client] [-o json]
```
<details>
<summary>Example</summary>
```shell
$ odo version
odo v3.11.0 (a9e6cdc34)
Server: https://ab0bc42973f0043e7a2b9c24f5acddd6-9c1554c20c1ec323.elb.us-east-1.amazonaws.com:6443
OpenShift: 4.13.0
Kubernetes: v1.27.2+b451817
Podman Client: 4.5.1
```
</details>

47
pkg/api/version.go Normal file
View File

@@ -0,0 +1,47 @@
package api
/*
{
"version": "v3.11.0",
"gitCommit": "0acf1a5af",
"cluster": {
"serverURL": "https://kubernetes.docker.internal:6443",
"kubernetes": {
"version": "v1.25.9"
},
"openshift": {
"version": "4.13.0"
}
},
"podman": {
"client": {
"version": "4.5.1"
}
}
}
*/
type OdoVersion struct {
Version string `json:"version"`
GitCommit string `json:"gitCommit"`
Cluster *ClusterInfo `json:"cluster,omitempty"`
Podman *PodmanInfo `json:"podman,omitempty"`
}
type ClusterInfo struct {
ServerURL string `json:"serverURL,omitempty"`
Kubernetes *ClusterClientInfo `json:"kubernetes,omitempty"`
OpenShift *ClusterClientInfo `json:"openshift,omitempty"`
}
type ClusterClientInfo struct {
Version string `json:"version,omitempty"`
}
type PodmanInfo struct {
Client *PodmanClientInfo `json:"client,omitempty"`
}
type PodmanClientInfo struct {
Version string `json:"version,omitempty"`
}

View File

@@ -61,15 +61,15 @@ func (c *Client) GetServerVersion(timeout time.Duration) (*ServerInfo, error) {
// This will fetch the information about OpenShift Version
coreGet := c.GetClient().CoreV1().RESTClient().Get()
rawOpenShiftVersion, err := coreGet.AbsPath("/version/openshift").Do(context.TODO()).Raw()
rawOpenShiftVersion, err := coreGet.AbsPath("/apis/config.openshift.io/v1/clusterversions/version").Do(context.TODO()).Raw()
if err != nil {
klog.V(3).Info("Unable to get OpenShift Version: ", err)
} else {
var openShiftVersion version.Info
var openShiftVersion configv1.ClusterVersion
if e := json.Unmarshal(rawOpenShiftVersion, &openShiftVersion); e != nil {
return nil, fmt.Errorf("unable to unmarshal OpenShift version %v: %w", string(rawOpenShiftVersion), e)
}
info.OpenShiftVersion = openShiftVersion.GitVersion
info.OpenShiftVersion = openShiftVersion.Status.Desired.Version
}
// This will fetch the information about Kubernetes Version

View File

@@ -3,6 +3,10 @@ package version
import (
"context"
"fmt"
"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/log"
"github.com/redhat-developer/odo/pkg/odo/commonflags"
"github.com/redhat-developer/odo/pkg/podman"
"os"
"strings"
@@ -39,11 +43,12 @@ type VersionOptions struct {
// serverInfo contains the remote server information if the user asked for it, nil otherwise
serverInfo *kclient.ServerInfo
clientset *clientset.Clientset
podmanInfo podman.SystemVersionReport
clientset *clientset.Clientset
}
var _ genericclioptions.Runnable = (*VersionOptions)(nil)
var _ genericclioptions.JsonOutputter = (*VersionOptions)(nil)
// NewVersionOptions creates a new VersionOptions instance
func NewVersionOptions() *VersionOptions {
@@ -56,17 +61,31 @@ func (o *VersionOptions) SetClientset(clientset *clientset.Clientset) {
// Complete completes VersionOptions after they have been created
func (o *VersionOptions) Complete(ctx context.Context, cmdline cmdline.Cmdline, args []string) (err error) {
if !o.clientFlag {
// Let's fetch the info about the server, ignoring errors
client, err := kclient.New()
if o.clientFlag {
return nil
}
if err == nil {
o.serverInfo, err = client.GetServerVersion(o.clientset.PreferenceClient.GetTimeout())
if err != nil {
klog.V(4).Info("unable to fetch the server version: ", err)
}
// Fetch the info about the server, ignoring errors
if o.clientset.KubernetesClient != nil {
o.serverInfo, err = o.clientset.KubernetesClient.GetServerVersion(o.clientset.PreferenceClient.GetTimeout())
if err != nil {
klog.V(4).Info("unable to fetch the server version: ", err)
}
}
if o.clientset.PodmanClient != nil {
o.podmanInfo, err = o.clientset.PodmanClient.Version(ctx)
if err != nil {
klog.V(4).Info("unable to fetch the podman client version: ", err)
}
}
if o.serverInfo == nil {
log.Warning("unable to fetch the cluster server version")
}
if o.podmanInfo.Client == nil {
log.Warning("unable to fetch the podman client version")
}
return nil
}
@@ -75,33 +94,76 @@ func (o *VersionOptions) Validate(ctx context.Context) (err error) {
return nil
}
func (o *VersionOptions) RunForJsonOutput(ctx context.Context) (out interface{}, err error) {
return o.run(), nil
}
func (o *VersionOptions) run() api.OdoVersion {
result := api.OdoVersion{
Version: odoversion.VERSION,
GitCommit: odoversion.GITCOMMIT,
}
if o.clientFlag {
return result
}
if o.serverInfo != nil {
clusterInfo := &api.ClusterInfo{
ServerURL: o.serverInfo.Address,
Kubernetes: &api.ClusterClientInfo{Version: o.serverInfo.KubernetesVersion},
}
if o.serverInfo.OpenShiftVersion != "" {
clusterInfo.OpenShift = &api.ClusterClientInfo{Version: o.serverInfo.OpenShiftVersion}
}
result.Cluster = clusterInfo
}
if o.podmanInfo.Client != nil {
podmanInfo := &api.PodmanInfo{Client: &api.PodmanClientInfo{Version: o.podmanInfo.Client.Version}}
result.Podman = podmanInfo
}
return result
}
// Run contains the logic for the odo service create command
func (o *VersionOptions) Run(ctx context.Context) (err error) {
// If verbose mode is enabled, dump all KUBECLT_* env variables
// this is usefull for debuging oc plugin integration
// If verbose mode is enabled, dump all KUBECTL_* env variables
// this is useful for debugging oc plugin integration
for _, v := range os.Environ() {
if strings.HasPrefix(v, "KUBECTL_") {
klog.V(4).Info(v)
}
}
fmt.Println("odo " + odoversion.VERSION + " (" + odoversion.GITCOMMIT + ")")
odoVersion := o.run()
fmt.Println("odo " + odoVersion.Version + " (" + odoVersion.GitCommit + ")")
if !o.clientFlag && o.serverInfo != nil {
// make sure we only include OpenShift info if we actually have it
openshiftStr := ""
if len(o.serverInfo.OpenShiftVersion) > 0 {
openshiftStr = fmt.Sprintf("OpenShift: %v\n", o.serverInfo.OpenShiftVersion)
}
fmt.Printf("\n"+
"Server: %v\n"+
"%v"+
"Kubernetes: %v\n",
o.serverInfo.Address,
openshiftStr,
o.serverInfo.KubernetesVersion)
if o.clientFlag {
return nil
}
message := "\n"
if odoVersion.Cluster != nil {
cluster := odoVersion.Cluster
message += fmt.Sprintf("Server: %v\n", cluster.ServerURL)
// make sure we only include OpenShift info if we actually have it
if cluster.OpenShift != nil && cluster.OpenShift.Version != "" {
message += fmt.Sprintf("OpenShift: %v\n", cluster.OpenShift.Version)
}
if cluster.Kubernetes != nil {
message += fmt.Sprintf("Kubernetes: %v\n", cluster.Kubernetes.Version)
}
}
if odoVersion.Podman != nil && odoVersion.Podman.Client != nil {
message += fmt.Sprintf("Podman Client: %v\n", odoVersion.Podman.Client.Version)
}
fmt.Print(message)
return nil
}
@@ -118,7 +180,8 @@ func NewCmdVersion(name, fullName string, testClientset clientset.Clientset) *co
return genericclioptions.GenericRun(o, testClientset, cmd, args)
},
}
clientset.Add(versionCmd, clientset.PREFERENCE)
commonflags.UseOutputFlag(versionCmd)
clientset.Add(versionCmd, clientset.PREFERENCE, clientset.KUBERNETES_NULLABLE, clientset.PODMAN_NULLABLE)
util.SetCommandGroup(versionCmd, util.UtilityGroup)
versionCmd.SetUsageTemplate(util.CmdUsageTemplate)

View File

@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/onsi/gomega/types"
"os"
"path/filepath"
"regexp"
@@ -324,6 +325,12 @@ func JsonPathContentContain(json string, path string, value string) {
Expect(result.String()).To(ContainSubstring(value), fmt.Sprintf("content of path %q should contain %q but is %q", path, value, result.String()))
}
// JsonPathSatisfies expects content of the path to satisfy all the matchers passed to it
func JsonPathSatisfies(json string, path string, matchers ...types.GomegaMatcher) {
result := gjson.Get(json, path)
Expect(result.String()).Should(SatisfyAll(matchers...))
}
// JsonPathDoesNotExist expects that the content of the path does not exist in the JSON string
func JsonPathDoesNotExist(json string, path string) {
result := gjson.Get(json, path)

View File

@@ -74,3 +74,18 @@ func GetPodmanVersion() string {
Expect(err).ToNot(HaveOccurred())
return result.Client.Version
}
// GenerateDelayedPodman returns a podman cmd that sleeps for delaySecond before responding;
// this function is usually used in combination with PODMAN_CMD_INIT_TIMEOUT odo preference
func GenerateDelayedPodman(commonVarContext string, delaySecond int) string {
delayer := filepath.Join(commonVarContext, "podman-cmd-delayer")
fileContent := fmt.Sprintf(`#!/bin/bash
echo Delaying command execution... >&2
sleep %d
echo "$@"
`, delaySecond)
err := CreateFileWithContentAndPerm(delayer, fileContent, 0755)
Expect(err).ToNot(HaveOccurred())
return delayer
}

View File

@@ -100,14 +100,7 @@ var _ = Describe("odo dev command tests", func() {
// odo dev on cluster should not wait for the Podman client to initialize properly, if this client takes very long.
// See https://github.com/redhat-developer/odo/issues/6575.
// StartDevMode will time out if Podman client takes too long to initialize.
delayer := filepath.Join(commonVar.Context, "podman-cmd-delayer")
err = helper.CreateFileWithContentAndPerm(delayer, `#!/bin/bash
echo Delaying command execution... >&2
sleep 10
echo "$@"
`, 0755)
Expect(err).ShouldNot(HaveOccurred())
delayer := helper.GenerateDelayedPodman(commonVar.Context, 10)
var devSession helper.DevSession
devSession, err = helper.StartDevMode(helper.DevSessionOpts{

View File

@@ -3,7 +3,6 @@ package integration
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/redhat-developer/odo/tests/helper"
)
@@ -112,27 +111,94 @@ var _ = Describe("odo generic", func() {
})
})
When("executing odo version command", func() {
var odoVersion string
BeforeEach(func() {
odoVersion = helper.Cmd("odo", "version").ShouldPass().Out()
Context("executing odo version command", func() {
const (
reOdoVersion = `^odo\s*v[0-9]+.[0-9]+.[0-9]+(?:-\w+)?\s*\(\w+\)`
reKubernetesVersion = `Kubernetes:\s*v[0-9]+.[0-9]+.[0-9]+((-\w+\.[0-9]+)?\+\w+)?`
rePodmanVersion = `Podman Client:\s*[0-9]+.[0-9]+.[0-9]+((-\w+\.[0-9]+)?\+\w+)?`
reJSONVersion = `^v{0,1}[0-9]+.[0-9]+.[0-9]+((-\w+\.[0-9]+)?\+\w+)?`
)
When("executing the complete command with server info", func() {
var odoVersion string
BeforeEach(func() {
odoVersion = helper.Cmd("odo", "version").ShouldPass().Out()
})
for _, podman := range []bool{true, false} {
podman := podman
It("should show the version of odo major components including server login URL", helper.LabelPodmanIf(podman, func() {
By("checking the human readable output", func() {
Expect(odoVersion).Should(MatchRegexp(reOdoVersion))
// odo tests setup (CommonBeforeEach) is designed in a way that if a test is labelled with 'podman', it will not have cluster configuration
// so we only test podman info on podman labelled test, and clsuter info otherwise
// TODO (pvala): Change this behavior when we write tests that should be tested on both podman and cluster simultaneously
// Ref: https://github.com/redhat-developer/odo/issues/6719
if podman {
Expect(odoVersion).Should(MatchRegexp(rePodmanVersion))
Expect(odoVersion).To(ContainSubstring(helper.GetPodmanVersion()))
} else {
Expect(odoVersion).Should(MatchRegexp(reKubernetesVersion))
serverURL := oc.GetCurrentServerURL()
Expect(odoVersion).Should(ContainSubstring("Server: " + serverURL))
if !helper.IsKubernetesCluster() {
Expect(odoVersion).Should(ContainSubstring("OpenShift: "))
}
}
})
By("checking the JSON output", func() {
odoVersion = helper.Cmd("odo", "version", "-o", "json").ShouldPass().Out()
Expect(helper.IsJSON(odoVersion)).To(BeTrue())
helper.JsonPathSatisfies(odoVersion, "version", MatchRegexp(reJSONVersion))
helper.JsonPathExist(odoVersion, "gitCommit")
if podman {
helper.JsonPathSatisfies(odoVersion, "podman.client.version", MatchRegexp(reJSONVersion), Equal(helper.GetPodmanVersion()))
} else {
helper.JsonPathSatisfies(odoVersion, "cluster.kubernetes.version", MatchRegexp(reJSONVersion))
serverURL := oc.GetCurrentServerURL()
helper.JsonPathContentIs(odoVersion, "cluster.serverURL", serverURL)
if !helper.IsKubernetesCluster() {
helper.JsonPathSatisfies(odoVersion, "cluster.openshift", Not(BeEmpty()))
}
}
})
}))
}
for _, label := range []string{helper.LabelNoCluster, helper.LabelUnauth} {
label := label
It("should show the version of odo major components", Label(label), func() {
Expect(odoVersion).Should(MatchRegexp(reOdoVersion))
})
}
})
It("should show the version of odo major components including server login URL", func() {
reOdoVersion := `^odo\s*v[0-9]+.[0-9]+.[0-9]+(?:-\w+)?\s*\(\w+\)`
rekubernetesVersion := `Kubernetes:\s*v[0-9]+.[0-9]+.[0-9]+((-\w+\.[0-9]+)?\+\w+)?`
Expect(odoVersion).Should(SatisfyAll(MatchRegexp(reOdoVersion), MatchRegexp(rekubernetesVersion)))
serverURL := oc.GetCurrentServerURL()
Expect(odoVersion).Should(ContainSubstring("Server: " + serverURL))
When("podman client is bound to delay and odo version is run", Label(helper.LabelPodman), func() {
var odoVersion string
BeforeEach(func() {
delayer := helper.GenerateDelayedPodman(commonVar.Context, 2)
odoVersion = helper.Cmd("odo", "version").WithEnv("PODMAN_CMD="+delayer, "PODMAN_CMD_INIT_TIMEOUT=1s").ShouldPass().Out()
})
It("should not print podman version if podman cmd timeout has been reached", func() {
Expect(odoVersion).Should(MatchRegexp(reOdoVersion))
Expect(odoVersion).ToNot(ContainSubstring("Podman Client:"))
})
})
It("should only print client info when using --client flag", func() {
By("checking human readable output", func() {
odoVersion := helper.Cmd("odo", "version", "--client").ShouldPass().Out()
Expect(odoVersion).Should(MatchRegexp(reOdoVersion))
Expect(odoVersion).ToNot(SatisfyAll(ContainSubstring("Server"), ContainSubstring("Kubernetes"), ContainSubstring("Podman Client")))
})
It("should show the version of odo major components", Label(helper.LabelNoCluster), func() {
reOdoVersion := `^odo\s*v[0-9]+.[0-9]+.[0-9]+(?:-\w+)?\s*\(\w+\)`
Expect(odoVersion).Should(MatchRegexp(reOdoVersion))
})
It("should show the version of odo major components", Label(helper.LabelUnauth), func() {
reOdoVersion := `^odo\s*v[0-9]+.[0-9]+.[0-9]+(?:-\w+)?\s*\(\w+\)`
Expect(odoVersion).Should(MatchRegexp(reOdoVersion))
By("checking JSON output", func() {
odoVersion := helper.Cmd("odo", "version", "--client", "-o", "json").ShouldPass().Out()
Expect(helper.IsJSON(odoVersion)).To(BeTrue())
helper.JsonPathSatisfies(odoVersion, "version", MatchRegexp(reJSONVersion))
helper.JsonPathExist(odoVersion, "gitCommit")
helper.JsonPathSatisfies(odoVersion, "cluster", BeEmpty())
helper.JsonPathSatisfies(odoVersion, "podman", BeEmpty())
})
})
})