Kubectl: check version skew (#127365)

Signed-off-by: Omer Aplatony <omerap12@gmail.com>

Kubernetes-commit: 35307319740a3a52cf4632c24b7f99d675537bdf
This commit is contained in:
Omer Aplatony 2025-05-19 20:19:14 +03:00 committed by Kubernetes Publisher
parent 4afda566a9
commit 5ff92a69e3
4 changed files with 40 additions and 28 deletions

View File

@ -18,10 +18,10 @@ package version
import ( import (
"fmt" "fmt"
"io" "math"
"k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/version"
apimachineryversion "k8s.io/apimachinery/pkg/version" apimachineryversion "k8s.io/apimachinery/pkg/version"
"math"
) )
// supportedMinorVersionSkew is the maximum supported difference between the client and server minor versions. // supportedMinorVersionSkew is the maximum supported difference between the client and server minor versions.
@ -29,26 +29,26 @@ import (
// and server versions 1.18, 1.19, and 1.20 would be within the supported version skew for client version 1.19. // and server versions 1.18, 1.19, and 1.20 would be within the supported version skew for client version 1.19.
const supportedMinorVersionSkew = 1 const supportedMinorVersionSkew = 1
// printVersionSkewWarning prints a warning message if the difference between the client and version is greater than // getVersionSkewWarning returns a warning message if the difference between the client and version is greater than
// the supported version skew. // the supported version skew.
func printVersionSkewWarning(w io.Writer, clientVersion, serverVersion apimachineryversion.Info) error { func getVersionSkewWarning(clientVersion, serverVersion apimachineryversion.Info) (string, error) {
parsedClientVersion, err := version.ParseSemantic(clientVersion.GitVersion) parsedClientVersion, err := version.ParseSemantic(clientVersion.GitVersion)
if err != nil { if err != nil {
return err return "", fmt.Errorf("client version error: %w", err)
} }
parsedServerVersion, err := version.ParseSemantic(serverVersion.GitVersion) parsedServerVersion, err := version.ParseSemantic(serverVersion.GitVersion)
if err != nil { if err != nil {
return err return "", fmt.Errorf("server version error: %w", err)
} }
majorVersionDifference := math.Abs(float64(parsedClientVersion.Major()) - float64(parsedServerVersion.Major())) majorVersionDifference := math.Abs(float64(parsedClientVersion.Major()) - float64(parsedServerVersion.Major()))
minorVersionDifference := math.Abs(float64(parsedClientVersion.Minor()) - float64(parsedServerVersion.Minor())) minorVersionDifference := math.Abs(float64(parsedClientVersion.Minor()) - float64(parsedServerVersion.Minor()))
if majorVersionDifference > 0 || minorVersionDifference > supportedMinorVersionSkew { if majorVersionDifference > 0 || minorVersionDifference > supportedMinorVersionSkew {
fmt.Fprintf(w, "WARNING: version difference between client (%d.%d) and server (%d.%d) exceeds the supported minor version skew of +/-%d\n", warningMessage := fmt.Sprintf("version difference between client (%d.%d) and server (%d.%d) exceeds the supported minor version skew of +/-%d",
parsedClientVersion.Major(), parsedClientVersion.Minor(), parsedServerVersion.Major(), parsedServerVersion.Minor(), supportedMinorVersionSkew) parsedClientVersion.Major(), parsedClientVersion.Minor(), parsedServerVersion.Major(), parsedServerVersion.Minor(), supportedMinorVersionSkew)
return warningMessage, nil
} }
return "", nil
return nil
} }

View File

@ -17,14 +17,12 @@ limitations under the License.
package version package version
import ( import (
"bytes"
apimachineryversion "k8s.io/apimachinery/pkg/version"
"testing" "testing"
apimachineryversion "k8s.io/apimachinery/pkg/version"
) )
func TestPrintVersionSkewWarning(t *testing.T) { func TestPrintVersionSkewWarning(t *testing.T) {
output := &bytes.Buffer{}
testCases := []struct { testCases := []struct {
name string name string
clientVersion apimachineryversion.Info clientVersion apimachineryversion.Info
@ -82,14 +80,15 @@ func TestPrintVersionSkewWarning(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
output.Reset() warningMessage, err := getVersionSkewWarning(tc.clientVersion, tc.serverVersion)
if err != nil {
t.Errorf("error: %s", err)
}
printVersionSkewWarning(output, tc.clientVersion, tc.serverVersion) if tc.isWarningExpected && warningMessage == "" {
t.Error("warning was expected")
if tc.isWarningExpected && output.Len() == 0 { } else if !tc.isWarningExpected && warningMessage != "" {
t.Error("warning was expected, but not written to the output") t.Errorf("warning was not expected. but got %s", warningMessage)
} else if !tc.isWarningExpected && output.Len() > 0 {
t.Errorf("warning was not expected, but was written to the output: %s", output.String())
} }
}) })
} }

View File

@ -55,6 +55,7 @@ var (
type Options struct { type Options struct {
ClientOnly bool ClientOnly bool
Output string Output string
WarningsAsErrors bool
args []string args []string
@ -93,6 +94,9 @@ func NewCmdVersion(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cob
// Complete completes all the required options // Complete completes all the required options
func (o *Options) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { func (o *Options) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
var err error var err error
o.WarningsAsErrors = cmd.Flags().Lookup("warnings-as-errors").Value.String() == "true"
if o.ClientOnly { if o.ClientOnly {
return nil return nil
} }
@ -162,11 +166,17 @@ func (o *Options) Run() error {
} }
if versionInfo.ServerVersion != nil { if versionInfo.ServerVersion != nil {
if err := printVersionSkewWarning(o.ErrOut, *versionInfo.ClientVersion, *versionInfo.ServerVersion); err != nil { warningMessage, err := getVersionSkewWarning(*versionInfo.ClientVersion, *versionInfo.ServerVersion)
if err != nil {
return err return err
} }
if warningMessage != "" {
if o.WarningsAsErrors {
return errors.New(warningMessage)
}
fmt.Fprintf(o.ErrOut, "Warning: %s\n", warningMessage) //nolint:errcheck
}
} }
return serverErr return serverErr
} }

View File

@ -20,8 +20,6 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
@ -32,13 +30,18 @@ func TestNewCmdVersionClientVersion(t *testing.T) {
defer tf.Cleanup() defer tf.Cleanup()
streams, _, buf, _ := genericiooptions.NewTestIOStreams() streams, _, buf, _ := genericiooptions.NewTestIOStreams()
o := NewOptions(streams) o := NewOptions(streams)
if err := o.Complete(tf, &cobra.Command{}, nil); err != nil {
cmd := NewCmdVersion(tf, streams)
cmd.Flags().Bool("warnings-as-errors", false, "")
if err := o.Complete(tf, cmd, nil); err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
if err := o.Validate(); err != nil { if err := o.Validate(); err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
if err := o.Complete(tf, &cobra.Command{}, []string{"extraParameter0"}); err != nil {
if err := o.Complete(tf, cmd, []string{"extraParameter0"}); err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
if err := o.Validate(); !strings.Contains(err.Error(), "extra arguments") { if err := o.Validate(); !strings.Contains(err.Error(), "extra arguments") {