Skip to content

Commit

Permalink
Update version check for compatibility with kubectl v1.28+ (fixes #61) (
Browse files Browse the repository at this point in the history
  • Loading branch information
jathinjd authored Sep 20, 2024
1 parent 96cbc85 commit f26b2d8
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 35 deletions.
56 changes: 38 additions & 18 deletions kube/client.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package kube

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math"
"os/exec"
"regexp"
"strconv"
"strings"

Expand Down Expand Up @@ -37,6 +37,16 @@ type Client struct {
LogLevel int
}

type KubeVersion struct {
ClientVersion KubeInfo `json:"clientVersion"`
ServerVersion KubeInfo `json:"serverVersion"`
}

type KubeInfo struct {
Major string `json:"major"`
Minor string `json:"minor"`
}

// Configure writes the kubeconfig file to be used for authenticating kubectl commands.
func (c *Client) Configure() error {
// No need to write a kubeconfig file if Server is not specified (API server will be discovered via kube-proxy).
Expand Down Expand Up @@ -78,37 +88,47 @@ func (c *Client) Configure() error {

// CheckVersion returns an error if the server and client have incompatible versions, otherwise returns nil.
func (c *Client) CheckVersion() error {
args := []string{"kubectl", "version"}
args := []string{"kubectl", "version", "--output=json"}
if c.LogLevel > -1 {
args = append(args, fmt.Sprintf("-v=%d", c.LogLevel))
}
if c.Server != "" {
args = append(args, fmt.Sprintf("--kubeconfig=%s", c.kubeconfigFilePath))
}
stdout, err := exec.Command(args[0], args[1:]...).CombinedOutput()
output := strings.TrimSuffix(string(stdout), "\n")
if err != nil {
return fmt.Errorf("Error checking kubectl version: %v", output)
return fmt.Errorf("Error executing kubectl version command: %v", stdout)
}
return isCompatible(stdout)
}

// Using regular expressions, parse for the Major and Minor version numbers for both client and server.
clientPattern := regexp.MustCompile("(?:Client Version: version.Info{Major:\"([0-9+]+)\", Minor:\"([0-9+]+)\")")
serverPattern := regexp.MustCompile("(?:Server Version: version.Info{Major:\"([0-9+]+)\", Minor:\"([0-9+]+)\")")
// isCompatible compares the major and minor release numbers for the client and server, returning nil if they are compatible and an error otherwise.
func isCompatible(stdout []byte) error {
if !json.Valid(stdout) {
return fmt.Errorf("Error: kube versions output is not of valid format\n%s\n", stdout)
}
var kubeVersion KubeVersion
fmt.Printf("stdout value: %s\nstdout type: %T\n", stdout, stdout)
err := json.Unmarshal(stdout, &kubeVersion)
if err != nil {
return fmt.Errorf("Error unmarshaling kubectl version output: %v", err)
}

clientInfo := clientPattern.FindAllStringSubmatch(output, -1)
clientMajor := clientInfo[0][1]
clientMinor := clientInfo[0][2]
log.Printf("Kube Versions unmarshaled: %s\n", kubeVersion)

serverInfo := serverPattern.FindAllStringSubmatch(output, -1)
serverMajor := serverInfo[0][1]
serverMinor := serverInfo[0][2]
clientInfo := kubeVersion.ClientVersion
serverInfo := kubeVersion.ServerVersion

return isCompatible(clientMajor, clientMinor, serverMajor, serverMinor)
}
clientMajor := clientInfo.Major
log.Printf("Client major version obtained: %s\n", clientMajor)
clientMinor := clientInfo.Minor
log.Printf("Client minor version obtained: %s\n", clientMinor)
serverMajor := serverInfo.Major
log.Printf("Server major version obtained: %s\n", serverMajor)
serverMinor := serverInfo.Minor
log.Printf("Server minor version obtained: %s\n", serverMinor)

// isCompatible compares the major and minor release numbers for the client and server, returning nil if they are compatible and an error otherwise.
func isCompatible(clientMajor, clientMinor, serverMajor, serverMinor string) error {
incompatible := fmt.Errorf("Error: kubectl client and server versions are incompatible. Client is %s.%s; server is %s.%s. Client must be same minor release as server or one minor release behind server.", clientMajor, clientMinor, serverMajor, serverMinor)
incompatible := fmt.Errorf("Error: kubectl client and server versions are incompatible. Client is %s.%s; server is %s.%s. Client must be same minor release as server or one minor release behind server", clientMajor, clientMinor, serverMajor, serverMinor)

if strings.Replace(clientMajor, "+", "", -1) != strings.Replace(serverMajor, "+", "", -1) {
return incompatible
Expand Down
88 changes: 71 additions & 17 deletions kube/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,62 +8,116 @@ import (
)

type testCase struct {
clientMajor string
clientMinor string
serverMajor string
serverMinor string
expected error
kubectlStdout []byte
expected error
}

func stdoutGenerator(clientMajor, clientMinor, serverMajor, serverMinor string) []byte {
return []byte(fmt.Sprintf(`{
"clientVersion": {
"major": "%s",
"minor": "%s",
"gitVersion": "v1.27.16",
"gitCommit": "cbb86e0d7f4a049666fac0551e8b02ef3d6c3d9a",
"gitTreeState": "clean",
"buildDate": "2024-07-17T01:53:56Z",
"goVersion": "go1.22.5",
"compiler": "gc",
"platform": "linux/amd64"
},
"kustomizeVersion": "v5.0.1",
"serverVersion": {
"major": "%s",
"minor": "%s",
"gitVersion": "v1.27.11-gke.1062001",
"gitCommit": "2cefeadcb4ec7d21d775d15012b02d3393a53548",
"gitTreeState": "clean",
"buildDate": "2024-04-16T20:17:53Z",
"goVersion": "go1.21.7 X:boringcrypto",
"compiler": "gc",
"platform": "linux/amd64"
}
}`, clientMajor, clientMinor, serverMajor, serverMinor))
}

func TestClientIsCompatible(t *testing.T) {

// No server version to parse properly
malformedOut := []byte(`{
"clientVersion": {
"major": "1",
"minor": "20",
"gitVersion": "v1.20.14",
"gitCommit": "57a3aa3f1369xcf3db9c52d228c18db94fa81876",
"gitTreeState": "clean",
"buildDate": "2021-12-15T14:52:33Z",
"goVersion": "go1.15.15",
"compiler": "gc",
"platform": "darwin/amd64"
}
}
The connection to the server localhost:8080 was refused - did you specify the right host or port?`)
tc := testCase{
kubectlStdout: malformedOut,
expected: fmt.Errorf("Error: kube versions output is not of valid format\n%s\n", malformedOut),
}
createAndAssert(t, tc)

// Bad json
malformedOut = []byte(`lorem ipsum`)
tc = testCase{
kubectlStdout: malformedOut,
expected: fmt.Errorf("Error: kube versions output is not of valid format\n%s\n", malformedOut),
}
createAndAssert(t, tc)

// Bad clientMinor string
tc := testCase{"1", "abcd", "1", "0", fmt.Errorf("Error checking kubectl version: unable to parse client minor release from string \"abcd\"")}
tc = testCase{stdoutGenerator("1", "abcd", "1", "0"), fmt.Errorf("Error checking kubectl version: unable to parse client minor release from string \"abcd\"")}
createAndAssert(t, tc)

// Bad serverMinor string
tc = testCase{"1", "0", "1", "defg", fmt.Errorf("Error checking kubectl version: unable to parse server minor release from string \"defg\"")}
tc = testCase{stdoutGenerator("1", "0", "1", "defg"), fmt.Errorf("Error checking kubectl version: unable to parse server minor release from string \"defg\"")}
createAndAssert(t, tc)

// Client 1.0, Server 1.0
tc = testCase{"1", "0", "1", "0", nil}
tc = testCase{stdoutGenerator("1", "0", "1", "0"), nil}
createAndAssert(t, tc)

// Client 1.0, Server 1.1
tc = testCase{"1", "0", "1", "1", nil}
tc = testCase{stdoutGenerator("1", "0", "1", "1"), nil}
createAndAssert(t, tc)

// Client 1.0, Server 1.2
tc = testCase{"1", "0", "1", "2", fmt.Errorf("Error: kubectl client and server versions are incompatible. Client is 1.0; server is 1.2. Client must be same minor release as server or one minor release behind server.")}
tc = testCase{stdoutGenerator("1", "0", "1", "2"), fmt.Errorf("Error: kubectl client and server versions are incompatible. Client is 1.0; server is 1.2. Client must be same minor release as server or one minor release behind server")}
createAndAssert(t, tc)

// Client 1.2, Server 1.0
tc = testCase{"1", "2", "1", "0", fmt.Errorf("Error: kubectl client and server versions are incompatible. Client is 1.2; server is 1.0. Client must be same minor release as server or one minor release behind server.")}
tc = testCase{stdoutGenerator("1", "2", "1", "0"), fmt.Errorf("Error: kubectl client and server versions are incompatible. Client is 1.2; server is 1.0. Client must be same minor release as server or one minor release behind server")}
createAndAssert(t, tc)

// Client 1.1, Server 1.0
tc = testCase{"1", "1", "1", "0", nil}
tc = testCase{stdoutGenerator("1", "1", "1", "0"), nil}
createAndAssert(t, tc)

// Client 1.1, Server 1.1+
tc = testCase{"1", "1", "1", "1+", nil}
tc = testCase{stdoutGenerator("1", "1", "1", "1+"), nil}
createAndAssert(t, tc)

// Client 1.1+, Server 1.1
tc = testCase{"1", "1+", "1", "1", nil}
tc = testCase{stdoutGenerator("1", "1+", "1", "1"), nil}
createAndAssert(t, tc)

// Client 1.1, Server 1.2+
tc = testCase{"1", "1", "1", "2+", nil}
tc = testCase{stdoutGenerator("1", "1", "1", "2+"), nil}
createAndAssert(t, tc)

// Client 1.2, Server 1.1+
tc = testCase{"1", "2", "1", "1+", nil}
tc = testCase{stdoutGenerator("1", "2", "1", "1+"), nil}
createAndAssert(t, tc)
}

func createAndAssert(t *testing.T, tc testCase) {
assert := assert.New(t)
err := isCompatible(tc.clientMajor, tc.clientMinor, tc.serverMajor, tc.serverMinor)
err := isCompatible(tc.kubectlStdout)
assert.Equal(tc.expected, err)
}

0 comments on commit f26b2d8

Please sign in to comment.