diff --git a/Makefile b/Makefile index 717f6330525ec..d52a9685af61d 100644 --- a/Makefile +++ b/Makefile @@ -85,12 +85,12 @@ BUILDFLAGS="-trimpath" # Go exports: LDFLAGS := -ldflags=all= +EXTRA_LDFLAGS := $(shell go run $(KOPS_ROOT)/tools/gen-ldflags.go $(VERSION)) ifdef STATIC_BUILD CGO_ENABLED=0 export CGO_ENABLED EXTRA_BUILDFLAGS=-installsuffix cgo - EXTRA_LDFLAGS=-s -w endif @@ -192,18 +192,18 @@ kops: crossbuild-kops-$(shell go env GOOS)-$(shell go env GOARCH) .PHONY: crossbuild-kops-linux-amd64 crossbuild-kops-linux-arm64 crossbuild-kops-linux-amd64 crossbuild-kops-linux-arm64: crossbuild-kops-linux-%: mkdir -p ${DIST}/linux/$* - GOOS=linux GOARCH=$* go build ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/linux/$*/kops ${LDFLAGS}"${EXTRA_LDFLAGS} -X k8s.io/kops.Version=${VERSION} -X k8s.io/kops.GitVersion=${GITSHA}" k8s.io/kops/cmd/kops + GOOS=linux GOARCH=$* go build ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/linux/$*/kops ${LDFLAGS}"${EXTRA_LDFLAGS}" k8s.io/kops/cmd/kops .PHONY: crossbuild-kops-darwin-amd64 crossbuild-kops-darwin-arm64 crossbuild-kops-darwin-amd64 crossbuild-kops-darwin-arm64: crossbuild-kops-darwin-%: mkdir -p ${DIST}/darwin/$* - GOOS=darwin GOARCH=$* go build ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/darwin/$*/kops ${LDFLAGS}"${EXTRA_LDFLAGS} -X k8s.io/kops.Version=${VERSION} -X k8s.io/kops.GitVersion=${GITSHA}" k8s.io/kops/cmd/kops + GOOS=darwin GOARCH=$* go build ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/darwin/$*/kops ${LDFLAGS}"${EXTRA_LDFLAGS}" k8s.io/kops/cmd/kops .PHONY: crossbuild-kops-windows-amd64 crossbuild-kops-windows-amd64: mkdir -p ${DIST}/windows/amd64 - GOOS=windows GOARCH=amd64 go build ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/windows/amd64/kops.exe ${LDFLAGS}"${EXTRA_LDFLAGS} -X k8s.io/kops.Version=${VERSION} -X k8s.io/kops.GitVersion=${GITSHA}" k8s.io/kops/cmd/kops + GOOS=windows GOARCH=amd64 go build ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/windows/amd64/kops.exe ${LDFLAGS}"${EXTRA_LDFLAGS}" k8s.io/kops/cmd/kops .PHONY: crossbuild crossbuild: crossbuild-kops @@ -214,7 +214,7 @@ crossbuild: crossbuild-kops-linux-amd64 crossbuild-kops-linux-arm64 crossbuild-k .PHONY: nodeup-amd64 nodeup-arm64 nodeup-amd64 nodeup-arm64: nodeup-%: mkdir -p ${DIST}/linux/$* - GOOS=linux GOARCH=$* go build ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/linux/$*/nodeup ${LDFLAGS}"${EXTRA_LDFLAGS} -X k8s.io/kops.Version=${VERSION} -X k8s.io/kops.GitVersion=${GITSHA}" k8s.io/kops/cmd/nodeup + GOOS=linux GOARCH=$* go build ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/linux/$*/nodeup ${LDFLAGS}"${EXTRA_LDFLAGS}" k8s.io/kops/cmd/nodeup .PHONY: nodeup nodeup: nodeup-amd64 @@ -225,7 +225,7 @@ crossbuild-nodeup: nodeup-amd64 nodeup-arm64 .PHONY: protokube-amd64 protokube-arm64 protokube-amd64 protokube-arm64: protokube-%: mkdir -p ${DIST}/linux/$* - GOOS=linux GOARCH=$* go build -tags=peer_name_alternative,peer_name_hash ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/linux/$*/protokube ${LDFLAGS}"${EXTRA_LDFLAGS} -X k8s.io/kops.Version=${VERSION} -X k8s.io/kops.GitVersion=${GITSHA}" k8s.io/kops/protokube/cmd/protokube + GOOS=linux GOARCH=$* go build -tags=peer_name_alternative,peer_name_hash ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/linux/$*/protokube ${LDFLAGS}"${EXTRA_LDFLAGS}" k8s.io/kops/protokube/cmd/protokube .PHONY: protokube protokube: protokube-amd64 @@ -236,7 +236,7 @@ crossbuild-protokube: protokube-amd64 protokube-arm64 .PHONY: channels-amd64 channels-arm64 channels-amd64 channels-arm64: channels-%: mkdir -p ${DIST}/linux/$* - GOOS=linux GOARCH=$* go build ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/linux/$*/channels ${LDFLAGS}"${EXTRA_LDFLAGS} -X k8s.io/kops.Version=${VERSION} -X k8s.io/kops.GitVersion=${GITSHA}" k8s.io/kops/channels/cmd/channels + GOOS=linux GOARCH=$* go build ${GCFLAGS} ${BUILDFLAGS} ${EXTRA_BUILDFLAGS} -o ${DIST}/linux/$*/channels ${LDFLAGS}"${EXTRA_LDFLAGS}" k8s.io/kops/channels/cmd/channels .PHONY: channels channels: channels-amd64 @@ -301,7 +301,7 @@ push-aws-run-amd64 push-aws-run-arm64: push-aws-run-%: push-% .PHONY: ${NODEUP} ${NODEUP}: - go build ${GCFLAGS} ${EXTRA_BUILDFLAGS} ${LDFLAGS}"${EXTRA_LDFLAGS} -X k8s.io/kops.Version=${VERSION} -X k8s.io/kops.GitVersion=${GITSHA}" -o $@ k8s.io/kops/cmd/nodeup + go build ${GCFLAGS} ${EXTRA_BUILDFLAGS} ${LDFLAGS}"${EXTRA_LDFLAGS}" -o $@ k8s.io/kops/cmd/nodeup .PHONY: dns-controller-push dns-controller-push: ko-dns-controller-push diff --git a/cmd/kops/version.go b/cmd/kops/version.go index 1d789490c192d..8d956a13e554f 100644 --- a/cmd/kops/version.go +++ b/cmd/kops/version.go @@ -18,9 +18,12 @@ package main import ( "context" + "encoding/json" "fmt" "io" + "sigs.k8s.io/yaml" + "github.com/spf13/cobra" "k8s.io/kops" "k8s.io/kops/cmd/kops/util" @@ -57,6 +60,7 @@ func NewCmdVersion(f *util.Factory, out io.Writer) *cobra.Command { cmd.Flags().BoolVar(&options.short, "short", options.short, "only print the main kOps version. Useful for scripting.") cmd.Flags().BoolVar(&options.server, "server", options.server, "show the kOps version that made the last change to the state store.") + cmd.Flags().StringVarP(&options.Output, "output", "o", options.Output, "One of 'yaml' or 'json'.") return cmd } @@ -65,43 +69,69 @@ type VersionOptions struct { short bool server bool ClusterName string + Output string +} + +// Version is a struct for version information +type Version struct { + ClientVersion *kops.Info `json:"clientVersion,omitempty" yaml:"clientVersion,omitempty"` + ServerVersion string `json:"serverVersion,omitempty" yaml:"serverVersion,omitempty"` } // RunVersion implements the version command logic func RunVersion(f *util.Factory, out io.Writer, options *VersionOptions) error { - if options.short { - s := kops.Version - _, err := fmt.Fprintf(out, "%s\n", s) - if err != nil { - return err - } - if options.server { - server := serverVersion(f, options) - - _, err := fmt.Fprintf(out, "%s\n", server) - return err - } - - return nil - } else { - client := kops.Version - if kops.GitVersion != "" { - client += " (git-" + kops.GitVersion + ")" - } - - { - _, err := fmt.Fprintf(out, "Client version: %s\n", client) + var versionInfo Version + clientVersion := kops.Get() + versionInfo.ClientVersion = &clientVersion + if options.server { + versionInfo.ServerVersion = serverVersion(f, options) + } + switch options.Output { + case "": + if options.short { + _, err := fmt.Fprintf(out, "%s\n", versionInfo.ClientVersion.Version) if err != nil { return err } - } - if options.server { - server := serverVersion(f, options) + if options.server { + _, err := fmt.Fprintf(out, "%s\n", versionInfo.ServerVersion) + return err + } + return nil + } else { + client := versionInfo.ClientVersion.Version + if versionInfo.ClientVersion.GitVersion != "" { + client += " (git-" + versionInfo.ClientVersion.GitVersion + ")" + } - _, err := fmt.Fprintf(out, "Last applied server version: %s\n", server) + { + _, err := fmt.Fprintf(out, "Client Version: %s\n", client) + if err != nil { + return err + } + } + if options.server { + _, err := fmt.Fprintf(out, "Last applied server version: %s\n", versionInfo.ServerVersion) + return err + } + return nil + } + case OutputYaml: + marshalled, err := yaml.Marshal(&versionInfo) + if err != nil { + return err + } + _, err = fmt.Fprintln(out, string(marshalled)) + return err + case OutputJSON: + marshalled, err := json.MarshalIndent(&versionInfo, "", " ") + if err != nil { return err } - return nil + _, err = fmt.Fprintln(out, string(marshalled)) + return err + default: + return fmt.Errorf("VersionOptions were not validated: --output=%q should have been rejected", options.Output) } } diff --git a/cmd/kops/version_test.go b/cmd/kops/version_test.go new file mode 100644 index 0000000000000..de20d3d7120c2 --- /dev/null +++ b/cmd/kops/version_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2025 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 main + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/kops/cmd/kops/util" +) + +func TestRunVersion(t *testing.T) { + factoryOptions := &util.FactoryOptions{} + factory := util.NewFactory(factoryOptions) + + tests := []struct { + name string + opt *VersionOptions + expectedOutput string + wantErr error + }{ + { + name: "client Version", + opt: &VersionOptions{ + Output: "", + }, + expectedOutput: "Client Version", + }, + { + name: "output yaml format", + opt: &VersionOptions{ + Output: OutputYaml, + }, + expectedOutput: "clientVersion", + }, + { + name: "output json format", + opt: &VersionOptions{ + Output: OutputJSON, + }, + expectedOutput: "\"clientVersion\"", + }, + { + name: "unknown output format", + opt: &VersionOptions{ + Output: "Xml", + }, + wantErr: fmt.Errorf("VersionOptions were not validated: --output=%q should have been rejected", "Xml"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var stdout bytes.Buffer + err := RunVersion(factory, &stdout, tt.opt) + require.Equal(t, tt.wantErr, err) + if err != nil { + return + } + assert.Containsf( + t, + stdout.String(), + tt.expectedOutput, + "%s : Unexpected output! Expected\n%s\ngot\n%s", + tt.name, + tt.expectedOutput, + stdout, + ) + }) + } +} diff --git a/kops-version.go b/kops-version.go index 9f210af00ff4d..f83d2ff295df7 100644 --- a/kops-version.go +++ b/kops-version.go @@ -16,14 +16,74 @@ limitations under the License. package kops -// Version can be replaced by build tooling -var Version = KOPS_RELEASE_VERSION +import ( + "fmt" + "runtime" + "runtime/debug" +) // These constants are parsed by build tooling - be careful about changing the formats const ( - KOPS_RELEASE_VERSION = "1.33.0-beta.1" - KOPS_CI_VERSION = "1.33.0-beta.2" + KOPS_RELEASE_VERSION = "1.33.0-alpha.1" + KOPS_CI_VERSION = "1.33.0-alpha.2" +) + +var ( + // Version can be replaced by build tooling + Version = KOPS_RELEASE_VERSION + // GitVersion is semantic version. + GitVersion = "v0.0.0-master+$Format:%h$" + // GitTreeState state of git tree, either "clean" or "dirty". + GitTreeState = "" + // gitCommit sha1 from git + gitCommit = "" + // gitCommitDate date from git + gitCommitDate = "" +) + +const ( + commitKey = "vcs.revision" + commitDateKey = "vcs.time" ) -// GitVersion should be replaced by the makefile -var GitVersion = "" +func init() { + info, ok := debug.ReadBuildInfo() + if !ok { + return + } + for _, setting := range info.Settings { + if setting.Key == commitKey { + gitCommit = setting.Value + } + if setting.Key == commitDateKey { + gitCommitDate = setting.Value + } + } +} + +// Info contains versioning information. +type Info struct { + Version string `json:"version"` + GitVersion string `json:"gitVersion"` + GitCommit string `json:"gitCommit"` + GitCommitDate string `json:"gitCommitDate"` + GitTreeState string `json:"gitTreeState"` + GoVersion string `json:"goVersion"` + Compiler string `json:"compiler"` + Platform string `json:"platform"` +} + +// Get returns the overall codebase version. It's for detecting +// what code a binary was built from. +func Get() Info { + return Info{ + Version: Version, + GitVersion: GitVersion, + GitTreeState: GitTreeState, + GitCommit: gitCommit, + GitCommitDate: gitCommitDate, + GoVersion: runtime.Version(), + Compiler: runtime.Compiler, + Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + } +} diff --git a/tools/gen-ldflags.go b/tools/gen-ldflags.go new file mode 100644 index 0000000000000..213e2990561ca --- /dev/null +++ b/tools/gen-ldflags.go @@ -0,0 +1,81 @@ +//go:build ignore +// +build ignore + +/* +Copyright 2025 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 main + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +// genLDFlags generates linker flags (-ldflags) for injecting version info into the binary. +func genLDFlags(ver string) string { + var ldflagsStr string + ldflagsStr = "-s -w -X k8s.io/kops.Version=" + ver + " " + ldflagsStr = ldflagsStr + "-X k8s.io/kops.GitVersion=" + version() + " " + ldflagsStr = ldflagsStr + "-X k8s.io/kops.GitTreeState=" + treeState() + " " + return ldflagsStr +} + +// version returns the version string from Git. +// Equivalent to: git describe --tags --always --match 'v*' +func version() string { + var ( + tag []byte + e error + ) + cmdName := "git" + cmdArgs := []string{"describe", "--tags", "--always", "--match", "v*"} + if tag, e = exec.Command(cmdName, cmdArgs...).Output(); e != nil { + fmt.Fprintln(os.Stderr, "Error generating git version: ", e) + os.Exit(1) + } + return strings.TrimSpace(string(tag)) +} + +// treeState returns the working tree state: "clean" or "dirty". +// Equivalent to: git status --porcelain +func treeState() string { + var ( + out []byte + e error + ) + cmdName := "git" + cmdArgs := []string{"status", "--porcelain"} + if out, e = exec.Command(cmdName, cmdArgs...).Output(); e != nil { + fmt.Fprintln(os.Stderr, "Error generating git tree-state: ", e) + os.Exit(1) + } + if strings.TrimSpace(string(out)) == "" { + return "clean" + } + return "dirty" +} + +func main() { + var ver string + if len(os.Args) > 1 { + ver = strings.TrimSpace(os.Args[1]) + } else { + ver = version() + } + fmt.Println(genLDFlags(ver)) +}