diff --git a/Makefile b/Makefile index fffc414a6b..f42c601e05 100644 --- a/Makefile +++ b/Makefile @@ -25,12 +25,13 @@ BIN_GOIMPORTS ?= "$(PWD)/bin/goimports" # If the current commit does not have a semver tag, 'tip' is used, unless there # is a TAG environment variable. Precedence is git tag, environment variable, 'tip' HASH := $(shell git rev-parse --short HEAD 2>/dev/null) +DATE := $(shell git log -1 --format=%cI HEAD 2>/dev/null) VTAG := $(shell git tag --points-at HEAD | head -1) VTAG := $(shell [ -z $(VTAG) ] && echo $(ETAG) || echo $(VTAG)) VERS ?= $(shell git describe --tags --match 'v*') KVER ?= $(shell git describe --tags --match 'knative-*') -LDFLAGS := -X knative.dev/func/pkg/version.Vers=$(VERS) -X knative.dev/func/pkg/version.Kver=$(KVER) -X knative.dev/func/pkg/version.Hash=$(HASH) +LDFLAGS := -X knative.dev/func/pkg/version.Vers=$(VERS) -X knative.dev/func/pkg/version.Kver=$(KVER) -X knative.dev/func/pkg/version.Hash=$(HASH) -X knative.dev/func/pkg/version.BuildDate=$(DATE) FUNC_UTILS_IMG ?= ghcr.io/knative/func-utils:v2 LDFLAGS += -X knative.dev/func/pkg/k8s.SocatImage=$(FUNC_UTILS_IMG) diff --git a/cmd/root_test.go b/cmd/root_test.go index 555e1e6a83..21a22a5ba9 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -161,54 +161,207 @@ func TestRoot_CommandNameParameterized(t *testing.T) { } func TestVerbose(t *testing.T) { - tests := []struct { - name string - args []string - want string - wantLF int - }{ - { - name: "verbose as version's flag", - args: []string{"version", "-v"}, - want: "Version: v0.42.0", - wantLF: 24, - }, - { - name: "no verbose", - args: []string{"version"}, - want: "v0.42.0", - wantLF: 1, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - viper.Reset() - - var out bytes.Buffer - - cmd := NewRootCmd(RootCommandConfig{ - Name: "func", - Version: Version{ - Vers: "v0.42.0", - Hash: "cafe", - Kver: "v1.10.0", - }}) - - cmd.SetArgs(tt.args) - cmd.SetOut(&out) - if err := cmd.Execute(); err != nil { - t.Fatal(err) + t.Run("no verbose", func(t *testing.T) { + viper.Reset() + var out bytes.Buffer + cmd := NewRootCmd(RootCommandConfig{ + Name: "func", + Version: Version{Vers: "v0.42.0"}, + }) + cmd.SetArgs([]string{"version"}) + cmd.SetOut(&out) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + if got := strings.TrimRight(out.String(), "\n"); got != "v0.42.0" { + t.Errorf("expected %q but got %q", "v0.42.0", got) + } + }) + + t.Run("verbose includes populated fields", func(t *testing.T) { + viper.Reset() + var out bytes.Buffer + cmd := NewRootCmd(RootCommandConfig{ + Name: "func", + Version: Version{ + Vers: "v0.42.0", + Hash: "cafe", + Kver: "v1.10.0", + BuildDate: "2024-01-01T00:00:00Z", + }, + }) + cmd.SetArgs([]string{"version", "-v"}) + cmd.SetOut(&out) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + output := out.String() + for _, want := range []string{ + "Version: v0.42.0", + "Knative: v1.10.0", + "Commit: cafe", + "BuildDate: 2024-01-01T00:00:00Z", + } { + if !strings.Contains(output, want) { + t.Errorf("expected output to contain %q but got:\n%s", want, output) } + } + }) - outLines := strings.Split(out.String(), "\n") - if len(outLines)-1 != tt.wantLF { - t.Errorf("expected output with %v line breaks but got %v:", tt.wantLF, len(outLines)-1) + t.Run("verbose omits empty fields", func(t *testing.T) { + viper.Reset() + var out bytes.Buffer + cmd := NewRootCmd(RootCommandConfig{ + Name: "func", + // All fields except Vers are intentionally left empty. + Version: Version{Vers: "v0.42.0"}, + }) + cmd.SetArgs([]string{"version", "-v"}) + cmd.SetOut(&out) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + output := out.String() + for _, absent := range []string{"Knative:", "Commit:", "BuildDate:"} { + if strings.Contains(output, absent) { + t.Errorf("expected output to omit %q but got:\n%s", absent, output) } - if outLines[0] != tt.want { - t.Errorf("expected output: %q but got: %q", tt.want, outLines[0]) + } + if !strings.HasPrefix(output, "Version: v0.42.0\n") { + t.Errorf("expected output to start with %q but got:\n%s", "Version: v0.42.0\n", output) + } + }) + + // kver prefix stripped verifies that a Knative version tag with the + // "knative-" prefix is stripped correctly for an exact release tag. + t.Run("kver prefix stripped exact tag", func(t *testing.T) { + v := Version{Vers: "v0.42.0", Kver: "knative-v1.10.0"} + output := v.StringVerbose() + if !strings.Contains(output, "Knative: v1.10.0") { + t.Errorf("expected 'knative-' prefix stripped for exact tag, got:\n%s", output) + } + }) + + // kver prefix stripped also verifies commit-distance suffixes are preserved + // (e.g. knative-v1.10.0-5-gabcdef1 → v1.10.0-5-gabcdef1). + t.Run("kver prefix stripped with commit distance", func(t *testing.T) { + v := Version{Vers: "v0.42.0", Kver: "knative-v1.10.0-5-gabcdef1"} + output := v.StringVerbose() + if !strings.Contains(output, "Knative: v1.10.0-5-gabcdef1") { + t.Errorf("expected 'knative-' prefix stripped with commit distance preserved, got:\n%s", output) + } + }) + + t.Run("output json", func(t *testing.T) { + viper.Reset() + var out bytes.Buffer + cmd := NewRootCmd(RootCommandConfig{ + Name: "func", + Version: Version{ + Vers: "v0.42.0", + BuildDate: "2024-01-01T00:00:00Z", + }, + }) + cmd.SetArgs([]string{"version", "--output", "json"}) + cmd.SetOut(&out) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + output := out.String() + for _, want := range []string{ + `"version": "v0.42.0"`, + `"buildDate": "2024-01-01T00:00:00Z"`, + } { + if !strings.Contains(output, want) { + t.Errorf("expected JSON to contain %q, got:\n%s", want, output) } + } + }) + + t.Run("output yaml", func(t *testing.T) { + viper.Reset() + var out bytes.Buffer + cmd := NewRootCmd(RootCommandConfig{ + Name: "func", + Version: Version{Vers: "v0.42.0"}, }) - } + cmd.SetArgs([]string{"version", "--output", "yaml"}) + cmd.SetOut(&out) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "version: v0.42.0") { + t.Errorf("expected YAML to contain version field, got:\n%s", out.String()) + } + }) + + t.Run("url unsupported", func(t *testing.T) { + v := Version{Vers: "v0.42.0"} + var buf bytes.Buffer + if err := v.URL(&buf); err == nil { + t.Error("expected URL format to return an error, got nil") + } + }) + + // exact output guards against extra blank lines or whitespace being + // reintroduced between populated fields. + t.Run("verbose exact output", func(t *testing.T) { + v := Version{ + Vers: "v0.42.0", + Kver: "knative-v1.10.0", + Hash: "cafe", + BuildDate: "2024-01-01T00:00:00Z", + SocatImage: "ghcr.io/knative/func-utils:v2", + TarImage: "ghcr.io/knative/func-utils:v2", + } + want := "Version: v0.42.0\n" + + "Knative: v1.10.0\n" + + "Commit: cafe\n" + + "BuildDate: 2024-01-01T00:00:00Z\n" + + "SocatImage: ghcr.io/knative/func-utils:v2\n" + + "TarImage: ghcr.io/knative/func-utils:v2\n" + if got := v.StringVerbose(); got != want { + t.Errorf("unexpected verbose output:\nwant:\n%s\ngot:\n%s", want, got) + } + }) + + t.Run("middleware versions omitted when empty", func(t *testing.T) { + v := Version{ + Vers: "v0.42.0", + MiddlewareVersions: MiddlewareVersions{}, + } + output := v.StringVerbose() + if strings.Contains(output, "Middleware Versions:") { + t.Errorf("expected empty MiddlewareVersions to be omitted, got:\n%s", output) + } + }) + + t.Run("socat and tar images omitted when empty", func(t *testing.T) { + v := Version{Vers: "v0.42.0"} + output := v.StringVerbose() + for _, absent := range []string{"SocatImage:", "TarImage:"} { + if strings.Contains(output, absent) { + t.Errorf("expected %q to be omitted when empty, got:\n%s", absent, output) + } + } + }) + + t.Run("socat and tar images present when populated", func(t *testing.T) { + v := Version{ + Vers: "v0.42.0", + SocatImage: "ghcr.io/knative/func-utils:v2", + TarImage: "ghcr.io/knative/func-utils:v2", + } + output := v.StringVerbose() + for _, want := range []string{ + "SocatImage: ghcr.io/knative/func-utils:v2", + "TarImage: ghcr.io/knative/func-utils:v2", + } { + if !strings.Contains(output, want) { + t.Errorf("expected output to contain %q, got:\n%s", want, output) + } + } + }) } // TestRoot_effectivePath ensures that the path method returns the effective path diff --git a/cmd/version.go b/cmd/version.go index e909a21ebb..df3caaea65 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -83,7 +83,8 @@ func runVersion(cmd *cobra.Command, v Version) error { v.Vers = DefaultVersion } - // Kver, Hash are already set from build + // Kver, Hash, BuildDate are already set from build via ldflags, + // injected into the Version struct at startup (see pkg/app/app.go). // Populate image fields from k8s package constants v.SocatImage = k8s.SocatImage v.TarImage = k8s.TarImage @@ -110,6 +111,8 @@ type Version struct { Kver string `json:"knative,omitempty" yaml:"knative,omitempty"` // Hash of the currently active git commit on build. Hash string `json:"commit,omitempty" yaml:"commit,omitempty"` + // BuildDate is the UTC timestamp at which the binary was built. + BuildDate string `json:"buildDate,omitempty" yaml:"buildDate,omitempty"` // SocatImage is the socat image used by the function. SocatImage string `json:"socatImage,omitempty" yaml:"socatImage,omitempty"` // TarImage is the tar image used by the function. @@ -132,28 +135,29 @@ func (v Version) String() string { } // StringVerbose returns the version along with extended version metadata. +// Fields with empty values are omitted. func (v Version) StringVerbose() string { - var ( - vers = v.Vers - kver = v.Kver - hash = v.Hash - ) - if strings.HasPrefix(kver, "knative-") { - kver = strings.Split(kver, "-")[1] + var sb strings.Builder + sb.WriteString("Version: " + v.Vers + "\n") + if v.Kver != "" { + sb.WriteString("Knative: " + strings.TrimPrefix(v.Kver, "knative-") + "\n") } - return fmt.Sprintf( - "Version: %s\n"+ - "Knative: %s\n"+ - "Commit: %s\n"+ - "SocatImage: %s\n"+ - "TarImage: %s\n"+ - "Middleware Versions: \n%s", - vers, - kver, - hash, - v.SocatImage, - v.TarImage, - v.MiddlewareVersions) + if v.Hash != "" { + sb.WriteString("Commit: " + v.Hash + "\n") + } + if v.BuildDate != "" { + sb.WriteString("BuildDate: " + v.BuildDate + "\n") + } + if v.SocatImage != "" { + sb.WriteString("SocatImage: " + v.SocatImage + "\n") + } + if v.TarImage != "" { + sb.WriteString("TarImage: " + v.TarImage + "\n") + } + if mw := v.MiddlewareVersions.String(); mw != "" { + sb.WriteString("Middleware Versions:\n" + mw) + } + return sb.String() } // Human prints version information in human-readable format. diff --git a/pkg/app/app.go b/pkg/app/app.go index eae55e63a8..a45669ca06 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -32,9 +32,10 @@ func Main() { cfg := cmd.RootCommandConfig{ Name: "func", Version: cmd.Version{ - Vers: version.Vers, - Kver: version.Kver, - Hash: version.Hash, + Vers: version.Vers, + Kver: version.Kver, + Hash: version.Hash, + BuildDate: version.BuildDate, }} if err := cmd.NewRootCmd(cfg).ExecuteContext(ctx); err != nil { diff --git a/pkg/version/version.go b/pkg/version/version.go index 114c9658bc..a678deb98f 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,3 +1,3 @@ package version -var Vers, Kver, Hash string +var Vers, Kver, Hash, BuildDate string