Skip to content

Commit

Permalink
Call kubectl version command with --output=json flag (#7032)
Browse files Browse the repository at this point in the history
* refactor kubectl client to use --output=json

* add unit tests for CheckAllCommands

* add unit tests for kubectl client

* rename interface to fix lint

* refactor kube client interface to interact only with version data

* remove unused vars

* fix get info test
  • Loading branch information
TiberiuGC authored Sep 14, 2023
1 parent edb7fb2 commit 90fb1ae
Show file tree
Hide file tree
Showing 11 changed files with 815 additions and 171 deletions.
3 changes: 1 addition & 2 deletions pkg/ctl/create/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import (
"github.com/weaveworks/eksctl/pkg/outposts"
"github.com/weaveworks/eksctl/pkg/printers"
"github.com/weaveworks/eksctl/pkg/utils/kubeconfig"
"github.com/weaveworks/eksctl/pkg/utils/kubectl"
"github.com/weaveworks/eksctl/pkg/utils/names"
"github.com/weaveworks/eksctl/pkg/utils/nodes"
"github.com/weaveworks/eksctl/pkg/utils/tasks"
Expand Down Expand Up @@ -467,7 +466,7 @@ func doCreateCluster(cmd *cmdutils.Cmd, ngFilter *filter.NodeGroupFilter, params
if err != nil {
return err
}
if err := kubectl.CheckAllCommands(params.KubeconfigPath, params.SetContext, kubeconfigContextName, env); err != nil {
if err := kubeconfig.CheckAllCommands(params.KubeconfigPath, params.SetContext, kubeconfigContextName, env); err != nil {
logger.Critical("%s\n", err.Error())
logger.Info("cluster should be functional despite missing (or misconfigured) client binaries")
}
Expand Down
31 changes: 4 additions & 27 deletions pkg/info/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package info
import (
"encoding/json"
"fmt"
"os/exec"
"runtime"

"github.com/weaveworks/eksctl/pkg/utils/kubectl"
"github.com/weaveworks/eksctl/pkg/version"
)

Expand All @@ -18,16 +18,6 @@ type Info struct {
OS string
}

// clientVersion holds git version info of kubectl client
type clientVersion struct {
GitVersion string `json:"gitVersion"`
}

// kubectlInfo holds version info of kubectl client
type kubectlInfo struct {
ClientVersion clientVersion `json:"clientVersion"`
}

// GetInfo returns versions info
func GetInfo() Info {
return Info{
Expand All @@ -42,25 +32,12 @@ func getEksctlVersion() string {
return version.GetVersion()
}

// getKubectlVersion returns the kubectl version
func getKubectlVersion() string {
cmd := exec.Command("kubectl", "version", "--client", "--output", "json")
out, err := cmd.CombinedOutput()
clientVersion, err := kubectl.NewVersionManager().ClientVersion()
if err != nil {
return fmt.Sprintf("error : %v", err)
}

var info kubectlInfo

if err := json.Unmarshal(out, &info); err != nil {
return fmt.Sprintf("error parsing `kubectl version` output: %v", err)
return err.Error()
}

if info.ClientVersion.GitVersion == "" {
return "unknown version"
}

return info.ClientVersion.GitVersion
return clientVersion
}

// String return info as JSON
Expand Down
20 changes: 19 additions & 1 deletion pkg/utils/kubeconfig/export_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
package kubeconfig

func SetExecCommand(f ExecCommandFunc) {
import (
"os/exec"

"github.com/weaveworks/eksctl/pkg/utils/kubectl"
)

func SetExecCommand(f func(name string, arg ...string) *exec.Cmd) {
execCommand = f
}

func SetExecLookPath(f func(file string) (string, error)) {
execLookPath = f
}

func SetNewVersionManager(f func() kubectl.KubernetesVersionManager) {
newVersionManager = f
}

func SetLookupAuthenticator(f func() (string, bool)) {
lookupAuthenticator = f
}
132 changes: 79 additions & 53 deletions pkg/utils/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

"github.com/gofrs/flock"
"github.com/kballard/go-shellquote"
"github.com/kris-nova/logger"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd"
Expand All @@ -19,6 +20,7 @@ import (
api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
"github.com/weaveworks/eksctl/pkg/utils"
"github.com/weaveworks/eksctl/pkg/utils/file"
"github.com/weaveworks/eksctl/pkg/utils/kubectl"
)

const (
Expand All @@ -37,9 +39,20 @@ const (
betaAPIVersion = "client.authentication.k8s.io/v1beta1"
)

type ExecCommandFunc func(name string, arg ...string) *exec.Cmd

var execCommand = exec.Command
var (
execCommand = exec.Command
execLookPath = exec.LookPath
newVersionManager = kubectl.NewVersionManager
lookupAuthenticator = func() (string, bool) {
for _, cmd := range authenticatorCommands() {
_, err := exec.LookPath(cmd)
if err == nil {
return cmd, true
}
}
return "", false
}
)

// DefaultPath defines the default path
func DefaultPath() string {
Expand All @@ -49,8 +62,8 @@ func DefaultPath() string {
return clientcmd.RecommendedHomeFile
}

// AuthenticatorCommands returns all authenticator commands.
func AuthenticatorCommands() []string {
// authenticatorCommands returns all authenticator commands.
func authenticatorCommands() []string {
return []string{
AWSIAMAuthenticator,
AWSEKSAuthenticator,
Expand Down Expand Up @@ -138,7 +151,7 @@ func NewForUser(cluster ClusterInfo, username string) *clientcmdapi.Config {
// a suitable authenticator and respecting provider settings
func NewForKubectl(cluster ClusterInfo, username, roleARN, profile string) *clientcmdapi.Config {
config := NewForUser(cluster, username)
authenticator, found := LookupAuthenticator()
authenticator, found := lookupAuthenticator()
if !found {
// fall back to aws-iam-authenticator
authenticator = AWSIAMAuthenticator
Expand Down Expand Up @@ -205,9 +218,9 @@ func AppendAuthenticator(config *clientcmdapi.Config, cluster ClusterInfo, authe
// kubectl 1.24.0 removes the alpha API version, so it will never work
// Therefore as a best effort try the beta version even if it might not work
if execConfig.APIVersion == alphaAPIVersion {
if kubectlVersion := getKubectlVersion(); kubectlVersion != "" {
if clientVersion, err := newVersionManager().ClientVersion(); err == nil {
// Silently ignore errors because kubectl is not required to run eksctl
compareVersions, err := utils.CompareVersions(kubectlVersion, "1.24.0")
compareVersions, err := utils.CompareVersions(strings.TrimLeft(clientVersion, "v"), "1.24.0")
if err == nil && compareVersions >= 0 {
execConfig.APIVersion = betaAPIVersion
}
Expand Down Expand Up @@ -297,44 +310,6 @@ func getAWSIAMAuthenticatorVersion() (string, error) {
return parsedVersion.Version, nil
}

/*
KubectlVersionFormat is the format used by kubectl version --format=json, example output:
{
"clientVersion": {
"major": "1",
"minor": "23",
"gitVersion": "v1.23.6",
"gitCommit": "ad3338546da947756e8a88aa6822e9c11e7eac22",
"gitTreeState": "archive",
"buildDate": "2022-04-29T06:39:16Z",
"goVersion": "go1.18.1",
"compiler": "gc",
"platform": "linux/amd64"
}
}
*/
type KubectlVersionData struct {
Version string `json:"gitVersion"`
}

type KubectlVersionFormat struct {
ClientVersion KubectlVersionData `json:"clientVersion"`
}

func getKubectlVersion() string {
cmd := execCommand("kubectl", "version", "--client", "--output=json")
output, err := cmd.Output()
if err != nil {
return ""
}
var parsedVersion KubectlVersionFormat
if err := json.Unmarshal(output, &parsedVersion); err != nil {
return ""
}
return strings.TrimLeft(parsedVersion.ClientVersion.Version, "v")
}

func lockFileName(filePath string) string {
return filepath.Join(os.TempDir(), fmt.Sprintf("%x.eksctl.lock", utils.FnvHash(filePath)))
}
Expand Down Expand Up @@ -569,13 +544,64 @@ func deleteClusterInfo(existing *clientcmdapi.Config, meta *api.ClusterMeta) boo
return isChanged
}

// LookupAuthenticator looks up an available authenticator
func LookupAuthenticator() (string, bool) {
for _, cmd := range AuthenticatorCommands() {
_, err := exec.LookPath(cmd)
if err == nil {
return cmd, true
// CheckAllCommands check version of kubectl, and if it can be used with either
// of the authenticator commands; most importantly it validates if kubectl can
// use kubeconfig we've created for it
func CheckAllCommands(kubeconfigPath string, isContextSet bool, contextName string, env []string) error {
authenticator, found := lookupAuthenticator()
if !found {
return fmt.Errorf("could not find any of the authenticator commands: %s", strings.Join(authenticatorCommands(), ", "))
}
logger.Debug("found authenticator: %s", authenticator)

kubectlPath, err := execLookPath(kubectl.Command)
if err != nil {
return fmt.Errorf("kubectl not found, v1.10.0 or newer is required")
}
logger.Debug("kubectl: %q", kubectlPath)

vm := newVersionManager()
clientVersion, err := vm.ClientVersion()
if err != nil {
return fmt.Errorf("getting kubectl version: %w", err)
}
logger.Debug("kubectl version: %s", clientVersion)

err = vm.ValidateVersion(clientVersion, kubectl.Client)
if err != nil {
return fmt.Errorf("validating kubectl version: %w", err)
}

if kubeconfigPath != "" {
var args []string
if kubeconfigPath != clientcmd.RecommendedHomeFile {
args = append(args, fmt.Sprintf("--kubeconfig=%s", kubeconfigPath))
}
if !isContextSet {
args = append(args, fmt.Sprintf("--context=%s", contextName))
}

suggestion := fmt.Sprintf("(check '%s')", fmtCmd(append(args, "version")))

serverVersion, err := vm.ServerVersion(env, args)
if err != nil {
return fmt.Errorf("getting Kubernetes version on EKS cluster: %w %s", err, suggestion)
}
err = vm.ValidateVersion(serverVersion, kubectl.Server)
if err != nil {
return fmt.Errorf("validating Kubernetes version returned by EKS API: %w", err)
}

logger.Info("kubectl command should work with %q, try '%s'", kubeconfigPath, fmtCmd(append(args, "get", "nodes")))
} else {
logger.Debug("skipping kubectl integration checks, as writing kubeconfig file is disabled")
}
return "", false

return nil
}

func fmtCmd(args []string) string {
cmd := []string{kubectl.Command}
cmd = append(cmd, args...)
return shellquote.Join(cmd...)
}
Loading

0 comments on commit 90fb1ae

Please sign in to comment.