@@ -18,10 +18,23 @@ package cmd
1818
1919import (
2020 "fmt"
21+ "os"
22+ "os/exec"
23+ "path/filepath"
24+ "runtime"
2125 "runtime/debug"
26+ "strconv"
27+ "strings"
28+ "time"
29+
30+ "golang.org/x/mod/semver"
2231)
2332
24- const unknown = "unknown"
33+ const (
34+ unknown = "unknown"
35+ develVersion = "(devel)"
36+ pseudoVersionTimestampLayout = "20060102150405"
37+ )
2538
2639// var needs to be used instead of const as ldflags is used to fill this
2740// information in the release process
@@ -47,11 +60,7 @@ type version struct {
4760
4861// versionString returns the Full CLI version
4962func versionString () string {
50- if kubeBuilderVersion == unknown {
51- if info , ok := debug .ReadBuildInfo (); ok && info .Main .Version != "" {
52- kubeBuilderVersion = info .Main .Version
53- }
54- }
63+ kubeBuilderVersion = getKubebuilderVersion ()
5564
5665 return fmt .Sprintf ("Version: %#v" , version {
5766 kubeBuilderVersion ,
@@ -65,10 +74,184 @@ func versionString() string {
6574
6675// getKubebuilderVersion returns only the CLI version string
6776func getKubebuilderVersion () string {
68- if kubeBuilderVersion == unknown {
69- if info , ok := debug .ReadBuildInfo (); ok && info .Main .Version != "" {
70- kubeBuilderVersion = info .Main .Version
71- }
77+ if strings .Contains (kubeBuilderVersion , "dirty" ) {
78+ return develVersion
79+ }
80+ if shouldResolveVersion (kubeBuilderVersion ) {
81+ kubeBuilderVersion = resolveKubebuilderVersion ()
7282 }
7383 return kubeBuilderVersion
7484}
85+
86+ func shouldResolveVersion (v string ) bool {
87+ return v == "" || v == unknown || v == develVersion
88+ }
89+
90+ func resolveKubebuilderVersion () string {
91+ if info , ok := debug .ReadBuildInfo (); ok {
92+ mainVersion := strings .TrimSpace (info .Main .Version )
93+ if mainVersion != "" && mainVersion != develVersion {
94+ return mainVersion
95+ }
96+
97+ if v := pseudoVersionFromGit (info .Main .Path ); v != "" {
98+ return v
99+ }
100+ }
101+
102+ if v := pseudoVersionFromGit ("" ); v != "" {
103+ return v
104+ }
105+
106+ return unknown
107+ }
108+
109+ func pseudoVersionFromGit (modulePath string ) string {
110+ repoRoot , err := findRepoRoot ()
111+ if err != nil {
112+ return ""
113+ }
114+ return pseudoVersionFromGitDir (modulePath , repoRoot )
115+ }
116+
117+ func pseudoVersionFromGitDir (modulePath , repoRoot string ) string {
118+ dirty , err := repoDirty (repoRoot )
119+ if err != nil {
120+ return ""
121+ }
122+ if dirty {
123+ return develVersion
124+ }
125+
126+ commitHash , err := runGitCommand (repoRoot , "rev-parse" , "--short=12" , "HEAD" )
127+ if err != nil || commitHash == "" {
128+ return ""
129+ }
130+
131+ commitTimestamp , err := runGitCommand (repoRoot , "show" , "-s" , "--format=%ct" , "HEAD" )
132+ if err != nil || commitTimestamp == "" {
133+ return ""
134+ }
135+ seconds , err := strconv .ParseInt (commitTimestamp , 10 , 64 )
136+ if err != nil {
137+ return ""
138+ }
139+ timestamp := time .Unix (seconds , 0 ).UTC ().Format (pseudoVersionTimestampLayout )
140+
141+ if tag , err := runGitCommand (repoRoot , "describe" , "--tags" , "--exact-match" ); err == nil {
142+ tag = strings .TrimSpace (tag )
143+ if tag != "" {
144+ return tag
145+ }
146+ }
147+
148+ if baseTag , err := runGitCommand (repoRoot , "describe" , "--tags" , "--abbrev=0" ); err == nil {
149+ baseTag = strings .TrimSpace (baseTag )
150+ if semver .IsValid (baseTag ) {
151+ if next := incrementPatch (baseTag ); next != "" {
152+ return fmt .Sprintf ("%s-0.%s-%s" , next , timestamp , commitHash )
153+ }
154+ }
155+ if baseTag != "" {
156+ return baseTag
157+ }
158+ }
159+
160+ major := moduleMajorVersion (modulePath )
161+ return buildDefaultPseudoVersion (major , timestamp , commitHash )
162+ }
163+
164+ func repoDirty (repoRoot string ) (bool , error ) {
165+ status , err := runGitCommand (repoRoot , "status" , "--porcelain" , "--untracked-files=no" )
166+ if err != nil {
167+ return false , err
168+ }
169+ return status != "" , nil
170+ }
171+
172+ func incrementPatch (tag string ) string {
173+ trimmed := strings .TrimPrefix (tag , "v" )
174+ trimmed = strings .SplitN (trimmed , "-" , 2 )[0 ]
175+ parts := strings .Split (trimmed , "." )
176+ if len (parts ) < 3 {
177+ return ""
178+ }
179+ major , err := strconv .Atoi (parts [0 ])
180+ if err != nil {
181+ return ""
182+ }
183+ minor , err := strconv .Atoi (parts [1 ])
184+ if err != nil {
185+ return ""
186+ }
187+ patch , err := strconv .Atoi (parts [2 ])
188+ if err != nil {
189+ return ""
190+ }
191+ patch ++
192+ return fmt .Sprintf ("v%d.%d.%d" , major , minor , patch )
193+ }
194+
195+ func buildDefaultPseudoVersion (major int , timestamp , commitHash string ) string {
196+ if major < 0 {
197+ major = 0
198+ }
199+ return fmt .Sprintf ("v%d.0.0-%s-%s" , major , timestamp , commitHash )
200+ }
201+
202+ func moduleMajorVersion (modulePath string ) int {
203+ if modulePath == "" {
204+ return 0
205+ }
206+ lastSlash := strings .LastIndex (modulePath , "/v" )
207+ if lastSlash == - 1 || lastSlash == len (modulePath )- 2 {
208+ return 0
209+ }
210+ majorStr := modulePath [lastSlash + 2 :]
211+ if strings .Contains (majorStr , "/" ) {
212+ majorStr = majorStr [:strings .Index (majorStr , "/" )]
213+ }
214+ major , err := strconv .Atoi (majorStr )
215+ if err != nil {
216+ return 0
217+ }
218+ return major
219+ }
220+
221+ func findRepoRoot () (string , error ) {
222+ _ , currentFile , _ , ok := runtime .Caller (0 )
223+ if ! ok {
224+ return "" , fmt .Errorf ("failed to determine caller" )
225+ }
226+
227+ if ! filepath .IsAbs (currentFile ) {
228+ abs , err := filepath .Abs (currentFile )
229+ if err != nil {
230+ return "" , fmt .Errorf ("getting absolute path: %w" , err )
231+ }
232+ currentFile = abs
233+ }
234+
235+ dir := filepath .Dir (currentFile )
236+ for {
237+ if dir == "" || dir == filepath .Dir (dir ) {
238+ return "" , fmt .Errorf ("git repository root not found from %s" , currentFile )
239+ }
240+
241+ if _ , err := os .Stat (filepath .Join (dir , ".git" )); err == nil {
242+ return dir , nil
243+ }
244+ dir = filepath .Dir (dir )
245+ }
246+ }
247+
248+ func runGitCommand (dir string , args ... string ) (string , error ) {
249+ cmd := exec .Command ("git" , args ... )
250+ cmd .Dir = dir
251+ cmd .Env = append (os .Environ (), "LC_ALL=C" , "LANG=C" )
252+ output , err := cmd .CombinedOutput ()
253+ if err != nil {
254+ return "" , fmt .Errorf ("running git %v: %w" , args , err )
255+ }
256+ return strings .TrimSpace (string (output )), nil
257+ }
0 commit comments