diff --git a/staging/src/k8s.io/component-base/version/dynamic.go b/staging/src/k8s.io/component-base/version/dynamic.go new file mode 100644 index 00000000000..46ade9f5ec1 --- /dev/null +++ b/staging/src/k8s.io/component-base/version/dynamic.go @@ -0,0 +1,77 @@ +/* +Copyright 2023 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 ( + "fmt" + "sync/atomic" + + utilversion "k8s.io/apimachinery/pkg/util/version" +) + +var dynamicGitVersion atomic.Value + +func init() { + // initialize to static gitVersion + dynamicGitVersion.Store(gitVersion) +} + +// SetDynamicVersion overrides the version returned as the GitVersion from Get(). +// The specified version must be non-empty, a valid semantic version, and must +// match the major/minor/patch version of the default gitVersion. +func SetDynamicVersion(dynamicVersion string) error { + if err := ValidateDynamicVersion(dynamicVersion); err != nil { + return err + } + dynamicGitVersion.Store(dynamicVersion) + return nil +} + +// ValidateDynamicVersion ensures the given version is non-empty, a valid semantic version, +// and matched the major/minor/patch version of the default gitVersion. +func ValidateDynamicVersion(dynamicVersion string) error { + return validateDynamicVersion(dynamicVersion, gitVersion) +} + +func validateDynamicVersion(dynamicVersion, defaultVersion string) error { + if len(dynamicVersion) == 0 { + return fmt.Errorf("version must not be empty") + } + if dynamicVersion == defaultVersion { + // allow no-op + return nil + } + vRuntime, err := utilversion.ParseSemantic(dynamicVersion) + if err != nil { + return err + } + // must match major/minor/patch of default version + var vDefault *utilversion.Version + if defaultVersion == "v0.0.0-master+$Format:%H$" { + // special-case the placeholder value which doesn't parse as a semantic version + vDefault, err = utilversion.ParseSemantic("v0.0.0-master") + } else { + vDefault, err = utilversion.ParseSemantic(defaultVersion) + } + if err != nil { + return err + } + if vRuntime.Major() != vDefault.Major() || vRuntime.Minor() != vDefault.Minor() || vRuntime.Patch() != vDefault.Patch() { + return fmt.Errorf("version %q must match major/minor/patch of default version %q", dynamicVersion, defaultVersion) + } + return nil +} diff --git a/staging/src/k8s.io/component-base/version/verflag/verflag.go b/staging/src/k8s.io/component-base/version/verflag/verflag.go index 106e3450ff0..46edab46f33 100644 --- a/staging/src/k8s.io/component-base/version/verflag/verflag.go +++ b/staging/src/k8s.io/component-base/version/verflag/verflag.go @@ -20,20 +20,22 @@ package verflag import ( "fmt" + "io" "os" "strconv" + "strings" flag "github.com/spf13/pflag" "k8s.io/component-base/version" ) -type versionValue int +type versionValue string const ( - VersionFalse versionValue = 0 - VersionTrue versionValue = 1 - VersionRaw versionValue = 2 + VersionFalse versionValue = "false" + VersionTrue versionValue = "true" + VersionRaw versionValue = "raw" ) const strRawVersion string = "raw" @@ -51,20 +53,28 @@ func (v *versionValue) Set(s string) error { *v = VersionRaw return nil } + + if strings.HasPrefix(s, "v") { + err := version.SetDynamicVersion(s) + if err == nil { + *v = versionValue(s) + } + return err + } + boolVal, err := strconv.ParseBool(s) - if boolVal { - *v = VersionTrue - } else { - *v = VersionFalse + if err == nil { + if boolVal { + *v = VersionTrue + } else { + *v = VersionFalse + } } return err } func (v *versionValue) String() string { - if *v == VersionRaw { - return strRawVersion - } - return fmt.Sprintf("%v", bool(*v == VersionTrue)) + return string(*v) } // The type of the flag as required by the pflag.Value interface @@ -88,7 +98,7 @@ func Version(name string, value versionValue, usage string) *versionValue { const versionFlagName = "version" var ( - versionFlag = Version(versionFlagName, VersionFalse, "Print version information and quit") + versionFlag = Version(versionFlagName, VersionFalse, "--version, --version=raw prints version information and quits; --version=vX.Y.Z... sets the reported version") programName = "Kubernetes" ) @@ -98,14 +108,20 @@ func AddFlags(fs *flag.FlagSet) { fs.AddFlag(flag.Lookup(versionFlagName)) } -// PrintAndExitIfRequested will check if the -version flag was passed +// variables for unit testing PrintAndExitIfRequested +var ( + output = io.Writer(os.Stdout) + exit = os.Exit +) + +// PrintAndExitIfRequested will check if --version or --version=raw was passed // and, if so, print the version and exit. func PrintAndExitIfRequested() { if *versionFlag == VersionRaw { - fmt.Printf("%#v\n", version.Get()) - os.Exit(0) + fmt.Fprintf(output, "%#v\n", version.Get()) + exit(0) } else if *versionFlag == VersionTrue { - fmt.Printf("%s %s\n", programName, version.Get()) - os.Exit(0) + fmt.Fprintf(output, "%s %s\n", programName, version.Get()) + exit(0) } } diff --git a/staging/src/k8s.io/component-base/version/verflag/verflag_test.go b/staging/src/k8s.io/component-base/version/verflag/verflag_test.go new file mode 100644 index 00000000000..8f2e59a29cd --- /dev/null +++ b/staging/src/k8s.io/component-base/version/verflag/verflag_test.go @@ -0,0 +1,166 @@ +/* +Copyright 2023 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 verflag + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/spf13/pflag" + + "k8s.io/component-base/version" +) + +func TestVersionFlag(t *testing.T) { + initialFlagValue := string(*versionFlag) + initialVersion := version.Get() + + testcases := []struct { + name string + flags []string + expectError string + expectExit bool + expectPrintVersion string + expectGitVersion string + }{ + { + name: "no flag", + flags: []string{}, + expectGitVersion: initialVersion.GitVersion, + }, + { + name: "false", + flags: []string{"--version=false"}, + expectGitVersion: initialVersion.GitVersion, + }, + + { + name: "valueless", + flags: []string{"--version"}, + expectGitVersion: initialVersion.GitVersion, + expectExit: true, + expectPrintVersion: "Kubernetes " + initialVersion.GitVersion, + }, + { + name: "true", + flags: []string{"--version=true"}, + expectGitVersion: initialVersion.GitVersion, + expectExit: true, + expectPrintVersion: "Kubernetes " + initialVersion.GitVersion, + }, + { + name: "raw", + flags: []string{"--version=raw"}, + expectGitVersion: initialVersion.GitVersion, + expectExit: true, + expectPrintVersion: fmt.Sprintf("%#v", initialVersion), + }, + { + name: "truthy", + flags: []string{"--version=T"}, + expectGitVersion: initialVersion.GitVersion, + expectExit: true, + expectPrintVersion: "Kubernetes " + initialVersion.GitVersion, + }, + + { + name: "override", + flags: []string{"--version=v0.0.0-custom"}, + expectGitVersion: "v0.0.0-custom", + }, + { + name: "invalid override semver", + flags: []string{"--version=vX"}, + expectError: `could not parse "vX"`, + }, + { + name: "invalid override major", + flags: []string{"--version=v1.0.0"}, + expectError: `must match major/minor/patch`, + }, + { + name: "invalid override minor", + flags: []string{"--version=v0.1.0"}, + expectError: `must match major/minor/patch`, + }, + { + name: "invalid override patch", + flags: []string{"--version=v0.0.1"}, + expectError: `must match major/minor/patch`, + }, + + { + name: "override and exit", + flags: []string{"--version=v0.0.0-custom", "--version"}, + expectGitVersion: "v0.0.0-custom", + expectExit: true, + expectPrintVersion: "Kubernetes v0.0.0-custom", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + + originalOutput := output + originalExit := exit + + outputBuffer := &bytes.Buffer{} + output = outputBuffer + exitCalled := false + exit = func(code int) { exitCalled = true } + + t.Cleanup(func() { + output = originalOutput + exit = originalExit + *versionFlag = versionValue(initialFlagValue) + err := version.SetDynamicVersion(initialVersion.GitVersion) + if err != nil { + t.Fatal(err) + } + }) + + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + AddFlags(fs) + err := fs.Parse(tc.flags) + if tc.expectError != "" { + if err == nil { + t.Fatal("expected error, got none") + } + if !strings.Contains(err.Error(), tc.expectError) { + t.Fatalf("expected error containing %q, got %q", tc.expectError, err.Error()) + } + return + } else if err != nil { + t.Fatalf("unexpected parse error: %v", err) + } + + if e, a := tc.expectGitVersion, version.Get().GitVersion; e != a { + t.Fatalf("gitversion: expected %v, got %v", e, a) + } + + PrintAndExitIfRequested() + if e, a := tc.expectExit, exitCalled; e != a { + t.Fatalf("exit(): expected %v, got %v", e, a) + } + if e, a := tc.expectPrintVersion, strings.TrimSpace(outputBuffer.String()); e != a { + t.Fatalf("print version: expected %v, got %v", e, a) + } + }) + } +} diff --git a/staging/src/k8s.io/component-base/version/version.go b/staging/src/k8s.io/component-base/version/version.go index d1e76dc00e0..1d268d4c680 100644 --- a/staging/src/k8s.io/component-base/version/version.go +++ b/staging/src/k8s.io/component-base/version/version.go @@ -31,7 +31,7 @@ func Get() apimachineryversion.Info { return apimachineryversion.Info{ Major: gitMajor, Minor: gitMinor, - GitVersion: gitVersion, + GitVersion: dynamicGitVersion.Load().(string), GitCommit: gitCommit, GitTreeState: gitTreeState, BuildDate: buildDate, diff --git a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go index 3864f094d64..cf83c3a6fe5 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go +++ b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go @@ -30,15 +30,13 @@ import ( "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" genericfeatures "k8s.io/apiserver/pkg/features" genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/apiserver/pkg/server/egressselector" serverstorage "k8s.io/apiserver/pkg/server/storage" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/pkg/version" "k8s.io/client-go/transport" - openapicommon "k8s.io/kube-openapi/pkg/common" - - "k8s.io/apiserver/pkg/server/dynamiccertificates" + "k8s.io/component-base/version" v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" v1helper "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper" "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1" @@ -52,6 +50,7 @@ import ( openapiv3aggregator "k8s.io/kube-aggregator/pkg/controllers/openapiv3/aggregator" statuscontrollers "k8s.io/kube-aggregator/pkg/controllers/status" apiservicerest "k8s.io/kube-aggregator/pkg/registry/apiservice/rest" + openapicommon "k8s.io/kube-openapi/pkg/common" ) func init() {