Skip to content

✨ Improve version command output: add runtime fallbacks and unit tests. #4898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func Run() {
c, err := cli.New(
cli.WithCommandName("kubebuilder"),
cli.WithVersion(versionString()),
cli.WithCliVersion(getKubebuilderVersion()),
cli.WithCliVersion(getKubeBuilderVersion()),
cli.WithPlugins(
golangv4.Plugin{},
gov4Bundle,
Expand Down
83 changes: 58 additions & 25 deletions cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,58 +17,91 @@ limitations under the License.
package cmd

import (
"encoding/json"
"fmt"
"runtime"
"runtime/debug"
)

const unknown = "unknown"

// var needs to be used instead of const as ldflags is used to fill this
// information in the release process
// These are filled via ldflags during build
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep the previous comments?

var (
kubeBuilderVersion = unknown
kubernetesVendorVersion = "1.33.0"
goos = unknown
goarch = unknown
gitCommit = "$Format:%H$" // sha1 from git, output of $(git rev-parse HEAD)

buildDate = "1970-01-01T00:00:00Z" // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep the previous comments?

gitCommit = "$Format:%H$"
buildDate = "1970-01-01T00:00:00Z"
)

// version contains all the information related to the CLI version
type version struct {
// VersionInfo holds all CLI version-related information
type VersionInfo struct {
Comment on lines +38 to +39
Copy link
Member

@camilamacedo86 camilamacedo86 Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// VersionInfo holds all CLI version-related information
type VersionInfo struct {
// version holds all CLI version-related information
type version struct {

Can we avoid externalising this one?
If we externalise it (e.g., by using v instead of V), it becomes accessible outside of Kubebuilder and effectively becomes part of its public API.

We intentionally do not want to expose this structure for use by other projects. Let's keep it internal to maintain tighter control and avoid unintended usage.

KubeBuilderVersion string `json:"kubeBuilderVersion"`
KubernetesVendor string `json:"kubernetesVendor"`
GitCommit string `json:"gitCommit"`
BuildDate string `json:"buildDate"`
GoOs string `json:"goOs"`
GoOS string `json:"goOs"`
GoArch string `json:"goArch"`
}

// versionString returns the Full CLI version
func versionString() string {
// resolveBuildInfo ensures dynamic fields are populated
func resolveBuildInfo() {
if kubeBuilderVersion == unknown {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" {
kubeBuilderVersion = info.Main.Version
}
}

return fmt.Sprintf("Version: %#v", version{
kubeBuilderVersion,
kubernetesVendorVersion,
gitCommit,
buildDate,
goos,
goarch,
})
if goos == unknown {
goos = runtime.GOOS
}
if goarch == unknown {
goarch = runtime.GOARCH
}
Comment on lines +55 to +60
Copy link
Member

@camilamacedo86 camilamacedo86 Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for the suggestion! 🙌

However, I think we should not fallback to runtime.GOOS / runtime.GOARCH here.

if goos == unknown {
	goos = runtime.GOOS
}
if goarch == unknown {
	goarch = runtime.GOARCH
}

This will always reflect the build-time platform — not the actual target platform of the binary.
For example, in our case, all builds run on Ubuntu (see our GoReleaser config), so this will return:

GOOS = "linux"
GOARCH = "amd64"

—even for macOS binaries like darwin/amd64. This results in incorrect output when users run:

kubebuilder version

We already inject the correct values using -ldflags at build time, and if something goes wrong with that injection, it's better to leave the values as "unknown" to surface the issue, rather than silently reporting incorrect metadata.

Could we please revert it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, see : https://github.com/kubernetes-sigs/kubebuilder/pull/4516/files
It can either be used if kubebuilder is installed with go install module
(another behaviour that we need to consider)

if gitCommit == "$Format:%H$" || gitCommit == "" {
gitCommit = unknown
}
}

// getKubebuilderVersion returns only the CLI version string
func getKubebuilderVersion() string {
if kubeBuilderVersion == unknown {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" {
kubeBuilderVersion = info.Main.Version
}
// getVersionInfo returns populated VersionInfo
func getVersionInfo() VersionInfo {
resolveBuildInfo()
return VersionInfo{
KubeBuilderVersion: kubeBuilderVersion,
KubernetesVendor: kubernetesVendorVersion,
GitCommit: gitCommit,
BuildDate: buildDate,
GoOS: goos,
GoArch: goarch,
}
}

// versionString returns a human-friendly string version
func versionString() string {
v := getVersionInfo()
return fmt.Sprintf(`KubeBuilder Version: %s
Kubernetes Vendor: %s
Git Commit: %s
Build Date: %s
Go OS/Arch: %s/%s`,
v.KubeBuilderVersion,
v.KubernetesVendor,
v.GitCommit,
v.BuildDate,
v.GoOS,
v.GoArch,
)
}
Comment on lines +79 to +94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@camilamacedo86 @Thedarkmatter10 could we move the output formatting to cli/version.go?

I think that separating the data gathering logic from the presentation logic could give us more flexibility to deal with output formatting, add flags, and leave room for other tools/commands to define how they want to display the version. The same goes for the JSON output. We could turn that into a flag in cli/version.go.

This separation was discussed briefly in a previous PR conversation (before I messed up and closed the PR).

WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that make sense we could do something like

if jsonOutput {
    printAsJSON(GetVersionInfo())
} else {
    printPretty(GetVersionInfo())
}

But I would accept it in a follow up PR as well
I think the goal of this one ( unless I misunderstood) would be add only the printPretty part


// getKubeBuilderVersion returns just the CLI version
func getKubeBuilderVersion() string {
resolveBuildInfo()
return kubeBuilderVersion
}

// versionJSON returns version as JSON string
func versionJSON() string {
v := getVersionInfo()
b, _ := json.MarshalIndent(v, "", " ")
Copy link
Preview

Copilot AI Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error from json.MarshalIndent is being ignored. While this is unlikely to fail for a simple struct, it's better practice to handle the error or document why it's safe to ignore.

Copilot uses AI. Check for mistakes.

return string(b)
}
65 changes: 65 additions & 0 deletions cmd/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cmd

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
)

func TestVersionStringIncludesExpectedFields(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Amazing 🚀

output := versionString()

assert.Contains(t, output, "KubeBuilder Version:")
assert.Contains(t, output, "Kubernetes Vendor:")
assert.Contains(t, output, "Git Commit:")
assert.Contains(t, output, "Build Date:")
assert.Contains(t, output, "Go OS/Arch:")
}

func TestVersionJSONFormatAndKeys(t *testing.T) {
jsonStr := versionJSON()

var result map[string]string
err := json.Unmarshal([]byte(jsonStr), &result)
assert.NoError(t, err)

assert.Contains(t, result, "kubeBuilderVersion")
assert.Contains(t, result, "kubernetesVendor")
assert.Contains(t, result, "gitCommit")
assert.Contains(t, result, "buildDate")
assert.Contains(t, result, "goOs")
assert.Contains(t, result, "goArch")
}

func TestGetVersionInfoFieldsArePopulated(t *testing.T) {
v := getVersionInfo()

assert.NotEmpty(t, v.KubeBuilderVersion)
assert.NotEmpty(t, v.KubernetesVendor)
assert.NotEmpty(t, v.GitCommit)
assert.NotEmpty(t, v.BuildDate)
assert.NotEmpty(t, v.GoOS)
assert.NotEmpty(t, v.GoArch)

assert.NotEqual(t, "unknown", v.KubeBuilderVersion, "KubeBuilderVersion should not be 'unknown'")
assert.NotEqual(t, "unknown", v.GoOS, "GoOS should not be 'unknown'")
assert.NotEqual(t, "unknown", v.GoArch, "GoArch should not be 'unknown'")
assert.NotEqual(t, "$Format:%H$", v.GitCommit, "GitCommit should not be default placeholder")
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/spf13/afero v1.14.0
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.10.0
golang.org/x/mod v0.26.0
golang.org/x/text v0.27.0
golang.org/x/tools v0.35.0
Expand All @@ -19,13 +20,15 @@ require (

require (
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
Expand Down
Loading