Merge pull request #117688 from liggitt/dynamic-version

Allow override of prerelease/buildID portions of version at runtime
This commit is contained in:
Kubernetes Prow Robot 2023-05-09 07:35:58 -07:00 committed by GitHub
commit d96993fc1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 281 additions and 23 deletions

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}
})
}
}

View File

@ -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,

View File

@ -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() {