diff --git a/docs/website/docs/command-reference/json-output.md b/docs/website/docs/command-reference/json-output.md index fb05ed65650..c389f7658dc 100644 --- a/docs/website/docs/command-reference/json-output.md +++ b/docs/website/docs/command-reference/json-output.md @@ -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 {} -``` \ No newline at end of file +``` + +## 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" + } + } +} +``` diff --git a/docs/website/docs/command-reference/version.md b/docs/website/docs/command-reference/version.md new file mode 100644 index 00000000000..6689e107e10 --- /dev/null +++ b/docs/website/docs/command-reference/version.md @@ -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] +``` + +
+Example + +```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 +``` +
diff --git a/pkg/api/version.go b/pkg/api/version.go new file mode 100644 index 00000000000..9df9ef11ed3 --- /dev/null +++ b/pkg/api/version.go @@ -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"` +} diff --git a/pkg/kclient/oc_server.go b/pkg/kclient/oc_server.go index eb539246fa4..985c7d392f2 100644 --- a/pkg/kclient/oc_server.go +++ b/pkg/kclient/oc_server.go @@ -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 diff --git a/pkg/odo/cli/version/version.go b/pkg/odo/cli/version/version.go index e8acb137100..054c889fa7c 100644 --- a/pkg/odo/cli/version/version.go +++ b/pkg/odo/cli/version/version.go @@ -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 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) - } + if o.clientFlag { + return nil + } + + // 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 { + return nil + } + + message := "\n" + if odoVersion.Cluster != nil { + cluster := odoVersion.Cluster + message += fmt.Sprintf("Server: %v\n", cluster.ServerURL) - 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) + 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) } - fmt.Printf("\n"+ - "Server: %v\n"+ - "%v"+ - "Kubernetes: %v\n", - o.serverInfo.Address, - openshiftStr, - o.serverInfo.KubernetesVersion) } + 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) diff --git a/tests/helper/helper_generic.go b/tests/helper/helper_generic.go index 9e74f10c7be..f0c0880a79a 100644 --- a/tests/helper/helper_generic.go +++ b/tests/helper/helper_generic.go @@ -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) diff --git a/tests/helper/helper_podman.go b/tests/helper/helper_podman.go index e89b94ded6f..bbd957a96b6 100644 --- a/tests/helper/helper_podman.go +++ b/tests/helper/helper_podman.go @@ -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 +} diff --git a/tests/integration/cmd_dev_test.go b/tests/integration/cmd_dev_test.go index 14655c19d76..57bc3126803 100644 --- a/tests/integration/cmd_dev_test.go +++ b/tests/integration/cmd_dev_test.go @@ -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{ diff --git a/tests/integration/generic_test.go b/tests/integration/generic_test.go index b76873c8f2b..4a03c015875 100644 --- a/tests/integration/generic_test.go +++ b/tests/integration/generic_test.go @@ -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()) + }) }) })