Repository: kubernetes-sigs/controller-runtime
Pull request #3166: ✨Add RELEASE_TAG to tools/setup-envtest to show binary version with setup-envtest version
Author: login: troy0820
Labels: name: cncf-cla: yes
description: Indicates the PR's author has signed the CNCF CLA.
name: size/L
description: Denotes a PR that changes 100-499 lines, ignoring generated files.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: lgtm
description: "Looks good to me", indicates that a PR is ready to be merged.
Created: 2025-03-19T14:03:01Z | Merged: 2025-03-23T10:08:32Z

With the setup-envtest binary, it is difficult to determine the version of the binary that is installed.  This will allow the user to use the command `setup-envtest version` to see what version of the binary is installed.

~~**Note: This is targeting the branch when built and not a specific tag.  The setup-envtest instructions for installation shows to target the release branch for specific versions**~~

**This will target the release tag defined in the GitHub action when binaries are built.**
<!-- What does this do, and why do we need it? -->
Resolves https://github.com/kubernetes-sigs/controller-runtime/issues/3158

![image](https://github.com/user-attachments/assets/7262a790-1f32-4299-99d8-07580850affc)



Diff:
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 058362b586..67c9b98b08 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -14,6 +14,8 @@ jobs:
     name: Upload binaries to release
     runs-on: ubuntu-latest
     steps:
+    - name: Set env
+      run:  echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV
     - name: Check out code
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
     - name: Calculate go version
diff --git a/Makefile b/Makefile
index 5fbef45dff..b656fa0175 100644
--- a/Makefile
+++ b/Makefile
@@ -174,7 +174,7 @@ release-binary: $(RELEASE_DIR)
 		-v "$$(pwd):/workspace$(DOCKER_VOL_OPTS)" \
 		-w /workspace/tools/setup-envtest \
 		golang:$(GO_VERSION) \
-		go build -a -trimpath -ldflags "-extldflags '-static'" \
+		go build -a -trimpath -ldflags "-X 'sigs.k8s.io/controller-runtime/tools/setup-envtest/version.version=$(RELEASE_TAG)' -extldflags '-static'" \
 		-o ./out/$(RELEASE_BINARY) ./
 
 ## --------------------------------------
diff --git a/tools/setup-envtest/main.go b/tools/setup-envtest/main.go
index 3121e206fd..7eb5ec43d3 100644
--- a/tools/setup-envtest/main.go
+++ b/tools/setup-envtest/main.go
@@ -184,6 +184,9 @@ Commands:
 		reads a .tar.gz file from stdin and expand it into the store.
 		must have a concrete version and platform.
 
+	version:
+		list the installed version of setup-envtest.
+
 Versions:
 
 	Versions take the form of a small subset of semver selectors.
@@ -256,7 +259,6 @@ Environment Variables:
 		version = flag.Arg(1)
 	}
 	env := setupEnv(globalLog, version)
-
 	// perform our main set of actions
 	switch action := flag.Arg(0); action {
 	case "use":
@@ -274,6 +276,8 @@ Environment Variables:
 			Input:       os.Stdin,
 			PrintFormat: printFormat,
 		}.Do(env)
+	case "version":
+		workflows.Version{}.Do(env)
 	default:
 		flag.Usage()
 		envp.Exit(2, "unknown action %q", action)
diff --git a/tools/setup-envtest/version/version.go b/tools/setup-envtest/version/version.go
new file mode 100644
index 0000000000..58e7e309a4
--- /dev/null
+++ b/tools/setup-envtest/version/version.go
@@ -0,0 +1,21 @@
+package version
+
+import "runtime/debug"
+
+// Version to be set using ldflags:
+// -ldflags "-X sigs.k8s.io/controller-tools/pkg/version.version=v1.0.0"
+// falls back to module information is unse
+var version = ""
+
+// Version returns the version of the main module
+func Version() string {
+	if version != "" {
+		return version
+	}
+	info, ok := debug.ReadBuildInfo()
+	if !ok || info == nil || info.Main.Version == "" {
+		// binary has not been built with module support or doesn't contain a version.
+		return "(unknown)"
+	}
+	return info.Main.Version
+}
diff --git a/tools/setup-envtest/version/version_suite_test.go b/tools/setup-envtest/version/version_suite_test.go
new file mode 100644
index 0000000000..99c623e8d4
--- /dev/null
+++ b/tools/setup-envtest/version/version_suite_test.go
@@ -0,0 +1,27 @@
+/*
+Copyright 2024 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 version
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestVersioning(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Test Version Suite")
+}
diff --git a/tools/setup-envtest/version/version_test.go b/tools/setup-envtest/version/version_test.go
new file mode 100644
index 0000000000..4178cac870
--- /dev/null
+++ b/tools/setup-envtest/version/version_test.go
@@ -0,0 +1,54 @@
+/*
+Copyright 2024 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 version
+
+import (
+	"runtime/debug"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("TestVersion", func() {
+
+	info, ok := debug.ReadBuildInfo()
+	Expect(ok).To(BeTrue())
+	tests := map[string]struct {
+		version  string
+		expected string
+	}{
+		"empty returns build info": {
+			version:  "",
+			expected: info.Main.Version,
+		},
+		"set to a value returns it": {
+			version:  "1.2.3",
+			expected: "1.2.3",
+		},
+	}
+	for name, tc := range tests {
+		It("Version set to "+name, func() {
+			versionBackup := version
+			defer func() {
+				version = versionBackup
+			}()
+			version = tc.version
+			result := Version()
+			Expect(result).To(Equal(tc.expected))
+		})
+	}
+})
diff --git a/tools/setup-envtest/workflows/workflows.go b/tools/setup-envtest/workflows/workflows.go
index fdabd995ae..fb9123d269 100644
--- a/tools/setup-envtest/workflows/workflows.go
+++ b/tools/setup-envtest/workflows/workflows.go
@@ -5,11 +5,13 @@ package workflows
 
 import (
 	"context"
+	"fmt"
 	"io"
 
 	"github.com/go-logr/logr"
 
 	envp "sigs.k8s.io/controller-runtime/tools/setup-envtest/env"
+	"sigs.k8s.io/controller-runtime/tools/setup-envtest/version"
 )
 
 // Use is a workflow that prints out information about stored
@@ -85,3 +87,12 @@ func (f Sideload) Do(env *envp.Env) {
 	env.Sideload(ctx, f.Input)
 	env.PrintInfo(f.PrintFormat)
 }
+
+// Version is the workflow that shows the current binary version
+// of setup-envtest.
+type Version struct{}
+
+// Do executes the workflow.
+func (v Version) Do(env *envp.Env) {
+	fmt.Fprintf(env.Out, "setup-envtest version: %s\n", version.Version())
+}
diff --git a/tools/setup-envtest/workflows/workflows_test.go b/tools/setup-envtest/workflows/workflows_test.go
index 27d4ec6770..435ae24285 100644
--- a/tools/setup-envtest/workflows/workflows_test.go
+++ b/tools/setup-envtest/workflows/workflows_test.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"io/fs"
 	"path/filepath"
+	"runtime/debug"
 	"sort"
 	"strings"
 
@@ -443,4 +444,16 @@ var _ = Describe("Workflows", func() {
 			Expect(string(outContents)).To(HavePrefix(expectedPrefix), "should have the debugging prefix")
 		})
 	})
+
+	Describe("version", func() {
+		It("should print out the version if the RELEASE_TAG is empty", func() {
+			v := wf.Version{}
+			v.Do(env)
+			info, ok := debug.ReadBuildInfo()
+			Expect(ok).To(BeTrue())
+			Expect(out.String()).ToNot(BeEmpty())
+			Expect(out.String()).To(Equal(fmt.Sprintf("setup-envtest version: %s\n", info.Main.Version)))
+		})
+	})
+
 })


Reviews:
user: login: troy0820
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T18:53:33Z
user: login: sbueringer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T15:56:08Z
user: login: troy0820
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T17:19:02Z
user: login: troy0820
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T13:27:24Z
user: login: troy0820
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T13:27:51Z
user: login: sbueringer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T13:33:47Z

Review comments:
diff_hunk: @@ -85,3 +87,23 @@ func (f Sideload) Do(env *envp.Env) {
 	env.Sideload(ctx, f.Input)
 	env.PrintInfo(f.PrintFormat)
 }
+
+// Version is the workflow that shows the current binary version
+// of setup-envtest.
+type Version struct {
+	BinaryVersion string
+}
+
+// Do executes the workflow.
+func (v Version) Do(env *envp.Env) {
+	version := "(unknown)"
+	if v.BinaryVersion == "" {
+		info, ok := debug.ReadBuildInfo()
+		if (ok && info != nil) || info.Main.Version != "" {
+			// binary has not been built with module support or doesn't contain a version.
path: tools/setup-envtest/workflows/workflows.go
user: login: troy0820
body: ```suggestion
			// binary has been built with module support.
```
created_at: 2025-03-19T18:53:32Z
author_association: MEMBER
diff_hunk: @@ -174,7 +174,7 @@ release-binary: $(RELEASE_DIR)
 		-v "$$(pwd):/workspace$(DOCKER_VOL_OPTS)" \
 		-w /workspace/tools/setup-envtest \
 		golang:$(GO_VERSION) \
-		go build -a -trimpath -ldflags "-extldflags '-static'" \
+		go build -a -trimpath -ldflags "-X 'main.BranchVersion=$(RELEASE_TAG)' -extldflags '-static'" \
path: Makefile
user: login: sbueringer
body: Let's call this version instead of BranchVersion

I assume this works, do you know if we can also use something more like in controller-gen?

e.g. sigs.k8s.io/controller-runtime/tools/setup-envtest/main.version=

Maybe also move the const to a tools/setup-envtest/version package, but not sure what the best practices are there
created_at: 2025-03-21T15:48:36Z
author_association: MEMBER
diff_hunk: @@ -51,6 +51,9 @@ var (
 		"directory to store binary assets (default: $OS_SPECIFIC_DATA_DIR/envtest-binaries)")
 
 	index = flag.String("index", remote.DefaultIndexURL, "index to discover envtest binaries")
+
+	// BranchVersion is the current setup-envtest branch that is used for the binary version of setup-envtest.
path: tools/setup-envtest/main.go
user: login: sbueringer
body: Let's add a godoc similar to controller-gen

```
// version to be set using ldflags:
// -ldflags "-X sigs.k8s.io/controller-tools/pkg/version.version=v1.0.0"
// falls back to module information is unset
```
created_at: 2025-03-21T15:51:30Z
author_association: MEMBER
diff_hunk: @@ -274,6 +276,10 @@ Environment Variables:
 			Input:       os.Stdin,
 			PrintFormat: printFormat,
 		}.Do(env)
+	case "version":
path: tools/setup-envtest/main.go
user: login: sbueringer
body: Just to double check. This will result in a `setup-envtest version` command right?
created_at: 2025-03-21T15:52:34Z
author_association: MEMBER
diff_hunk: @@ -85,3 +87,23 @@ func (f Sideload) Do(env *envp.Env) {
 	env.Sideload(ctx, f.Input)
 	env.PrintInfo(f.PrintFormat)
 }
+
+// Version is the workflow that shows the current binary version
+// of setup-envtest.
+type Version struct {
+	BinaryVersion string
+}
+
+// Do executes the workflow.
+func (v Version) Do(env *envp.Env) {
+	version := "(unknown)"
path: tools/setup-envtest/workflows/workflows.go
user: login: sbueringer
body: Maybe also move this logic to the version pkg I suggested in another comment. Then the stamping and this logic is in the same place

Maybe we can just use literally the same version.go as in controller-gen (+/- controller-tools specific godoc)? and then call version.Version() here instead of passing the version through?

(this func here would then ~ only contain the print statement)
created_at: 2025-03-21T15:53:42Z
author_association: MEMBER
diff_hunk: @@ -274,6 +276,10 @@ Environment Variables:
 			Input:       os.Stdin,
 			PrintFormat: printFormat,
 		}.Do(env)
+	case "version":
path: tools/setup-envtest/main.go
user: login: troy0820
body: This is correct. I need to update the commands to express this.
created_at: 2025-03-21T17:19:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,21 @@
+package version
+
+import "runtime/debug"
+
+// Version to be set using ldflags:
+// -ldflags "-X sigs.k8s.io/controller-tools/pkg/version.version=v1.0.0"
path: tools/setup-envtest/version/version.go
user: login: troy0820
body: ```suggestion
// -ldflags "-X sigs.k8s.io/controller-runtime/tools/setup-envtest/version.version=v1.0.0"

````
created_at: 2025-03-24T13:27:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,21 @@
+package version
+
+import "runtime/debug"
+
+// Version to be set using ldflags:
+// -ldflags "-X sigs.k8s.io/controller-tools/pkg/version.version=v1.0.0"
path: tools/setup-envtest/version/version.go
user: login: troy0820
body: @sbueringer I can make a PR for this to fix the godoc rendering with the correct comment.
created_at: 2025-03-24T13:27:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,21 @@
+package version
+
+import "runtime/debug"
+
+// Version to be set using ldflags:
+// -ldflags "-X sigs.k8s.io/controller-tools/pkg/version.version=v1.0.0"
path: tools/setup-envtest/version/version.go
user: login: sbueringer
body: Ups. Yup, thx!
created_at: 2025-03-24T13:33:47Z
author_association: MEMBER

PR comments:
user: login: sbueringer
created_at: 2025-03-19T16:07:44Z
body: I think setup-envtest should show the exact version. We are also publishing setup-envtest binaries on every release and it's not ideal if that just shows the branch

(there was some similar work in controller-gen recently: https://github.com/kubernetes-sigs/controller-tools/issues/1046. In general I would expect setup-envtest and controller-gen to behave the same)
author_association: MEMBER
user: login: troy0820
created_at: 2025-03-19T16:14:28Z
body: > I think setup-envtest should show the exact version. We are also publishing setup-envtest binaries on every release and it's not ideal if that just shows the branch
> 
> (there was some similar work in controller-gen recently: [kubernetes-sigs/controller-tools#1046](https://github.com/kubernetes-sigs/controller-tools/issues/1046). In general I would expect setup-envtest and controller-gen to behave the same)

This makes sense.  I made the assumption with how we install to provide the same installation version technique.  I can change this to provide the same as controller-gen.
author_association: MEMBER
user: login: sbueringer
created_at: 2025-03-23T09:56:57Z
body: Thx!

I'll cherry-pick so we can verify this e2e with the next patch release

/cherry-pick release-0.20
author_association: MEMBER
user: login: k8s-infra-cherrypick-robot
created_at: 2025-03-23T09:56:59Z
body: @sbueringer: once the present PR merges, I will cherry-pick it on top of `release-0.20` in a new PR and assign it to you.

<details>

In response to [this](https://github.com/kubernetes-sigs/controller-runtime/pull/3166#issuecomment-2746122261):

>Thx!
>
>I'll cherry-pick so we can verify this e2e with the next patch release
>
>/cherry-pick release-0.20


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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: sbueringer
created_at: 2025-03-23T09:57:09Z
body: /lgtm
/approve
author_association: MEMBER
user: login: k8s-ci-robot
created_at: 2025-03-23T09:57:15Z
body: LGTM label has been added.  <details>Git tree hash: cc081df330d194cd9bc15b0206524629dec178d5</details>
author_association: CONTRIBUTOR
user: login: k8s-ci-robot
created_at: 2025-03-23T09:57:16Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/controller-runtime/pull/3166#issuecomment-2746122327" title="Approved">sbueringer</a>*, *<a href="https://github.com/kubernetes-sigs/controller-runtime/pull/3166#" title="Author self-approved">troy0820</a>*

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

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/kubernetes-sigs/controller-runtime/blob/main/OWNERS)~~ [sbueringer]

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":[]} -->
author_association: CONTRIBUTOR
user: login: k8s-infra-cherrypick-robot
created_at: 2025-03-23T10:09:08Z
body: @sbueringer: new pull request created: #3175

<details>

In response to [this](https://github.com/kubernetes-sigs/controller-runtime/pull/3166#issuecomment-2746122261):

>Thx!
>
>I'll cherry-pick so we can verify this e2e with the next patch release
>
>/cherry-pick release-0.20


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-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE