Skip to content
This repository has been archived by the owner on Mar 31, 2023. It is now read-only.

Commit

Permalink
Adds eksctl functions to write and merge kubeconfig files
Browse files Browse the repository at this point in the history
* Switches context to newly created cluster by default, except if
  if --use-context=false is passed.

* If --artifact-directory is not passed, and ~/.kube/config file
  exists, the context is merged in the existing file. If ~/.kube/config
  doesn't exist, it creates it.

* Sets SilenceUsage to true in kubeconfig root command, to avoid showing
  the usage message in the case of error.
  Relevant issue: spf13/cobra#340

* Removes printing of error in Execute() of kubeconfig command
  to avoid printing twice an error message.
  Relevant issue: spf13/cobra#304

* Updates docs in `wksctl kubeconfig` part

Signed-off-by: Dinos Kousidis <dinos@weave.works>
  • Loading branch information
dinosk committed Mar 13, 2020
1 parent 7912852 commit 3c73f94
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 38 deletions.
56 changes: 40 additions & 16 deletions cmd/wksctl/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package kubeconfig

import (
"fmt"
"io/ioutil"
"path/filepath"

"github.com/pkg/errors"
Expand All @@ -12,16 +11,18 @@ import (
"github.com/weaveworks/wksctl/pkg/specs"
"github.com/weaveworks/wksctl/pkg/utilities/manifest"
"github.com/weaveworks/wksctl/pkg/utilities/path"
"k8s.io/client-go/tools/clientcmd"
)

// A new version of the kubeconfig command that retrieves the config from
// /etc/kubernetes/admin.conf on a cluster master node

// Cmd represents the kubeconfig command
var Cmd = &cobra.Command{
Use: "kubeconfig",
Short: "Generate a kubeconfig file for the cluster",
RunE: kubeconfigRun,
Use: "kubeconfig",
Short: "Generate a kubeconfig file for the cluster",
RunE: kubeconfigRun,
SilenceUsage: true,
}

var kubeconfigOptions struct {
Expand All @@ -34,6 +35,7 @@ var kubeconfigOptions struct {
artifactDirectory string
namespace string
sshKeyPath string
useContext bool
skipTLSVerify bool
useLocalhost bool
usePublicAddress bool
Expand All @@ -56,6 +58,9 @@ func init() {
&kubeconfigOptions.artifactDirectory, "artifact-directory", "", "Write output files in the specified directory")
Cmd.Flags().StringVar(
&kubeconfigOptions.namespace, "namespace", manifest.DefaultNamespace, "namespace portion of kubeconfig path")
Cmd.Flags().BoolVar(
&kubeconfigOptions.useContext, "use-context", true,
"Set current context to the newly created one")
Cmd.Flags().BoolVar(
&kubeconfigOptions.skipTLSVerify, "insecure-skip-tls-verify", false,
"Enables kubectl to communicate with the API w/o verifying the certificate")
Expand Down Expand Up @@ -93,29 +98,48 @@ func kubeconfigRun(cmd *cobra.Command, args []string) error {
}

func writeKubeconfig(cpath, mpath string) error {
wksHome, err := path.CreateDirectory(
path.WKSHome(kubeconfigOptions.artifactDirectory))
if err != nil {
return errors.Wrapf(err, "failed to create WKS home directory")
}
var wksHome string
var err error
var configPath string

sp := specs.NewFromPaths(cpath, mpath)

if kubeconfigOptions.artifactDirectory != "" {
wksHome, err = path.CreateDirectory(
path.WKSHome(kubeconfigOptions.artifactDirectory))
if err != nil {
return errors.Wrapf(err, "failed to create WKS home directory")
}

_, err = path.CreateDirectory(filepath.Dir(configPath))
if err != nil {
return errors.Wrapf(err, "failed to create configuration directory")
}
configPath = path.Kubeconfig(wksHome, kubeconfigOptions.namespace, sp.GetClusterName())
} else {
configPath = clientcmd.RecommendedHomeFile
}

configStr, err := config.GetRemoteKubeconfig(sp, kubeconfigOptions.sshKeyPath, kubeconfigOptions.verbose, kubeconfigOptions.skipTLSVerify)
if err != nil {
return errors.Wrapf(err, "GetRemoteKubeconfig")
return errors.Wrapf(err, "failed to get remote kubeconfig")
}

configPath := path.Kubeconfig(wksHome, kubeconfigOptions.namespace, sp.GetClusterName())

_, err = path.CreateDirectory(filepath.Dir(configPath))
remoteConfig, err := clientcmd.Load([]byte(configStr))
if err != nil {
return errors.Wrapf(err, "failed to create configuration directory")
return errors.Wrapf(err, "failed to load kubeconfig")
}
config.RenameConfig(sp, remoteConfig)

err = ioutil.WriteFile(configPath, []byte(configStr), 0644)
configPath, err = config.Write(configPath, *remoteConfig, kubeconfigOptions.useContext)
if err != nil {
return errors.Wrapf(err, "failed to write Kubernetes configuration locally")
}
fmt.Printf("To use kubectl with the %s cluster, enter:\n$ export KUBECONFIG=%s\n", sp.GetClusterName(), configPath)
if kubeconfigOptions.artifactDirectory != "" {
fmt.Printf("To use kubectl with the %s cluster, enter:\n$ export KUBECONFIG=%s\n", sp.GetClusterName(), configPath)
} else {
fmt.Printf("The kubeconfig file at %q has been updated\n", configPath)
}

return nil
}
2 changes: 0 additions & 2 deletions cmd/wksctl/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"fmt"
"os"

log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -66,7 +65,6 @@ func main() {
}

if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}

Expand Down
4 changes: 1 addition & 3 deletions docs/wks-and-footloose.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ These commands assume you are in the `examples/footloose` directory.

```console
$ wksctl kubeconfig --cluster=cluster.yaml
To use kubectl with the example cluster, enter:
export KUBECONFIG=$HOME/.wks/weavek8sops/example/kubeconfig
$ export KUBECONFIG=/home/lucas/.wks/weavek8sops/example/kubeconfig
The kubeconfig file at "/home/dinos/.kube/config" has been updated
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
b4fdde36eb122804 Ready master 77s v1.14.1
Expand Down
77 changes: 77 additions & 0 deletions pkg/kubernetes/config/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,24 @@ import (

yaml "github.com/ghodss/yaml"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/weaveworks/wksctl/pkg/cluster/machine"
"github.com/weaveworks/wksctl/pkg/plan/runners/ssh"
"github.com/weaveworks/wksctl/pkg/plan/runners/sudo"
"github.com/weaveworks/wksctl/pkg/specs"
"github.com/weaveworks/wksctl/pkg/utilities/path"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
clusterv1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1"
)

// DefaultPath defines the default path
var DefaultPath = clientcmd.RecommendedHomeFile
var DefaultClusterName = "kubernetes"
var DefaultClusterAdminName = "kubernetes-admin"
var DefaultContextName = fmt.Sprintf("%s@%s", DefaultClusterAdminName, DefaultClusterName)

// NewKubeConfig generates a Kubernetes configuration (e.g. for kubectl to use)
// from the provided machines, and places it in the provided directory.
func NewKubeConfig(artifactDirectory string, machines []*clusterv1.Machine) (string, error) {
Expand Down Expand Up @@ -76,6 +85,7 @@ func Sanitize(configStr string, params Params) (string, error) {
return configStr, nil
}

// GetRemoteKubeconfig retrieves Kubernetes configuration from a master node of the cluster
func GetRemoteKubeconfig(sp *specs.Specs, sshKeyPath string, verbose, skipTLSVerify bool) (string, error) {
sshClient, err := ssh.NewClientForMachine(sp.MasterSpec, sp.ClusterSpec.User, sshKeyPath, verbose)
if err != nil {
Expand All @@ -99,3 +109,70 @@ func GetRemoteKubeconfig(sp *specs.Specs, sshKeyPath string, verbose, skipTLSVer
SkipTLSVerify: skipTLSVerify,
})
}

// Write will write Kubernetes client configuration to a file.
// If path isn't specified then the path will be determined by client-go.
// If file pointed to by path doesn't exist it will be created.
// If the file already exists then the configuration will be merged with the existing file.
func Write(path string, newConfig clientcmdapi.Config, setContext bool) (string, error) {
configAccess := GetConfigAccess(path)

existingConfig, err := configAccess.GetStartingConfig()
if err != nil {
return "", errors.Wrapf(err, "Unable to read existing kubeconfig file %q", path)
}

log.Debug("Merging kubeconfig files")
mergedConfig := Merge(existingConfig, &newConfig)

if setContext && newConfig.CurrentContext != "" {
log.Debugf("setting current-context to %s", newConfig.CurrentContext)
mergedConfig.CurrentContext = newConfig.CurrentContext
}

if err := clientcmd.ModifyConfig(configAccess, *mergedConfig, true); err != nil {
return "", errors.Wrapf(err, "unable to modify kubeconfig %s", path)
}

return configAccess.GetDefaultFilename(), nil
}

func GetConfigAccess(explicitPath string) clientcmd.ConfigAccess {
pathOptions := clientcmd.NewDefaultPathOptions()
if explicitPath != "" && explicitPath != DefaultPath {
pathOptions.LoadingRules.ExplicitPath = explicitPath
}

return interface{}(pathOptions).(clientcmd.ConfigAccess)
}

// Merge two kubeconfig objects
func Merge(existing *clientcmdapi.Config, tomerge *clientcmdapi.Config) *clientcmdapi.Config {
for k, v := range tomerge.Clusters {
existing.Clusters[k] = v
}
for k, v := range tomerge.AuthInfos {
existing.AuthInfos[k] = v
}
for k, v := range tomerge.Contexts {
existing.Contexts[k] = v
}

return existing
}

// RenameConfig renames the default cluster and context names to the values from cluster.yaml
func RenameConfig(sp *specs.Specs, newConfig *clientcmdapi.Config) {
log.Debug("Renaming cluster")
newConfig.Clusters[sp.GetClusterName()] = newConfig.Clusters[DefaultClusterName]
delete(newConfig.Clusters, DefaultClusterName)

log.Debug("Renaming context")
newContextName := fmt.Sprintf("%s@%s", DefaultClusterAdminName, sp.GetClusterName())
newConfig.Contexts[newContextName] = newConfig.Contexts[DefaultContextName]
newConfig.Contexts[newContextName].Cluster = sp.GetClusterName()
delete(newConfig.Contexts, DefaultContextName)

log.Debug("Renaming current context")
newConfig.CurrentContext = newContextName
}
46 changes: 46 additions & 0 deletions pkg/kubernetes/config/kubeconfig_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package config_test

import (
"io/ioutil"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/weaveworks/wksctl/pkg/kubernetes/config"
clientcmd "k8s.io/client-go/tools/clientcmd"
)

const validConfig = `apiVersion: v1
Expand Down Expand Up @@ -86,3 +89,46 @@ func TestSanitizeWithInvalidConfigContainingSSHBannerAndWithPublicIPChange(t *te
assert.NoError(t, err)
assert.Equal(t, validConfigWithPublicIP, actualConfig)
}

func TestWrite(t *testing.T) {
testDataDir := "./testdata/"
testDataPath := "./testdata/test_kubeconfig"
defer os.RemoveAll(testDataDir)
validConfigObject, err := clientcmd.Load([]byte(validConfig))
assert.NoError(t, err)
_, err = config.Write(testDataPath, *validConfigObject, true)
assert.NoError(t, err)
loadedConfig, err := clientcmd.LoadFromFile(testDataPath)
assert.NoError(t, err)
err = clientcmd.Validate(*loadedConfig)
assert.NoError(t, err)
}

func TestMerge(t *testing.T) {
// Create 2 test kubeconfig objects
validConfigA, err := clientcmd.Load([]byte(validConfig))
assert.NoError(t, err)
validConfigB, err := clientcmd.Load([]byte(validConfigWithPublicIP))
assert.NoError(t, err)

mergedConfig := config.Merge(validConfigA, validConfigB)
err = clientcmd.Validate(*mergedConfig)
assert.NoError(t, err)
}

func TestInvalidExistingConfig(t *testing.T) {
// Fail if the current kubeconfig, which will be merged with the newly created one, is invalid
testDataDir := "./testdata/"
testDataPath := "./testdata/test_kubeconfig"
defer os.RemoveAll(testDataDir)
os.Mkdir(testDataDir, 0777)

err := ioutil.WriteFile(testDataPath, []byte(invalidConfigWithSSHBanner), 0777)
assert.NoError(t, err)

validConfig, err := clientcmd.Load([]byte(validConfig))
assert.NoError(t, err)

_, err = config.Write(testDataPath, *validConfig, true)
assert.Errorf(t, err, "Unable to read existing kubeconfig file")
}
11 changes: 3 additions & 8 deletions pkg/utilities/path/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"os/user"
"path/filepath"
"strings"

"k8s.io/client-go/tools/clientcmd"
)

// UserHomeDirectory returns the user directory.
Expand Down Expand Up @@ -44,14 +46,7 @@ func WKSHome(artifactDirectory string) string {
if artifactDirectory != "" {
return expandHome(artifactDirectory)
}

userHome, err := UserHomeDirectory()
if err == nil {
return filepath.Join(userHome, ".wks")
}

wd, _ := os.Getwd()
return wd
return clientcmd.RecommendedHomeFile
}

// WKSResourcePath joins the provided (optional) artifact directory and the
Expand Down
14 changes: 5 additions & 9 deletions test/integration/test/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import (
"io/ioutil"
"log"
"os"
"os/user"
"path"
"path/filepath"
"testing"
"time"

"k8s.io/client-go/tools/clientcmd"

"github.com/weaveworks/wksctl/pkg/cluster/machine"
"github.com/weaveworks/wksctl/pkg/kubernetes"
"github.com/weaveworks/wksctl/pkg/plan/runners/ssh"
Expand All @@ -21,12 +21,11 @@ import (
baremetalspecv1 "github.com/weaveworks/wksctl/pkg/baremetalproviderspec/v1alpha1"
spawn "github.com/weaveworks/wksctl/test/integration/spawn"

"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
clusterv1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1"

"github.com/stretchr/testify/assert"
)

// Runs a basic set of tests for apply.
Expand Down Expand Up @@ -240,13 +239,10 @@ func getClusterNamespaceAndName(t *testing.T) (string, string) {
// The installer names the kubeconfig file from the cluster namespace and name
// ~/.wks
func wksKubeconfig(t *testing.T, l *clusterv1.MachineList) string {
currentUser, err := user.Current()
assert.NoError(t, err)
master := machine.FirstMasterInArray(l.Items)
assert.NotNil(t, master)
namespace, name := getClusterNamespaceAndName(t)
kubeconfig := path.Join(currentUser.HomeDir, ".wks", namespace, name, "kubeconfig")
_, err = os.Stat(kubeconfig)
kubeconfig := clientcmd.RecommendedHomeFile
_, err := os.Stat(kubeconfig)
assert.NoError(t, err)

return kubeconfig
Expand Down

0 comments on commit 3c73f94

Please sign in to comment.