Skip to content

Commit

Permalink
Merge pull request #80 from weaveworks/issue-29-merge-kubeconfig
Browse files Browse the repository at this point in the history
Extend kubeconfig writer to support global path with merging
  • Loading branch information
errordeveloper authored Jul 5, 2018
2 parents 536fc62 + 7e6bf07 commit 1e2726a
Show file tree
Hide file tree
Showing 10 changed files with 413 additions and 84 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ RUN mkdir -p "$(dirname ${EKSCTL})"
COPY . $EKSCTL

WORKDIR $EKSCTL
RUN make \
RUN make test && make \
&& cp ./eksctl /out/usr/local/bin/eksctl

RUN go build ./vendor/github.com/heptio/authenticator/cmd/heptio-authenticator-aws \
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ EKSCTL_IMAGE ?= weaveworks/eksctl:latest
build:
go build -ldflags "-X main.gitCommit=$(git_commit) -X main.builtAt=$(built_at)" ./cmd/eksctl

.PHONY: test
test:
go test -v ./pkg/... ./cmd/...

.PHONY: update-bindata
update-bindata:
go generate ./pkg/eks
Expand Down
25 changes: 14 additions & 11 deletions cmd/eksctl/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/weaveworks/eksctl/pkg/eks"
"github.com/weaveworks/eksctl/pkg/utils"
"github.com/weaveworks/eksctl/pkg/utils/kubeconfig"
)

func createCmd() *cobra.Command {
Expand All @@ -35,14 +36,13 @@ const (
DEFAULT_NODE_COUNT = 2
DEFAULT_NODE_TYPE = "m5.large"
DEFAULT_SSH_PUBLIC_KEY = "~/.ssh/id_rsa.pub"

DEFAULT_KUBECONFIG_PATH = "kubeconfig"
)

var (
writeKubeconfig bool
kubeconfigPath string
autoKubeconfigPath bool
setContext bool
)

func createClusterCmd() *cobra.Command {
Expand Down Expand Up @@ -78,8 +78,9 @@ func createClusterCmd() *cobra.Command {
fs.StringVar(&cfg.SSHPublicKeyPath, "ssh-public-key", DEFAULT_SSH_PUBLIC_KEY, "SSH public key to use for nodes (import from local path, or use existing EC2 key pair)")

fs.BoolVar(&writeKubeconfig, "write-kubeconfig", true, "toggle writing of kubeconfig")
fs.BoolVar(&autoKubeconfigPath, "auto-kubeconfig", false, fmt.Sprintf("save kubconfig file by cluster name, e.g. %q", utils.ConfigPath(exampleClusterName)))
fs.StringVar(&kubeconfigPath, "kubeconfig", DEFAULT_KUBECONFIG_PATH, "path to write kubeconfig (incompatible with --auto-kubeconfig)")
fs.BoolVar(&autoKubeconfigPath, "auto-kubeconfig", false, fmt.Sprintf("save kubconfig file by cluster name, e.g. %q", kubeconfig.AutoPath(exampleClusterName)))
fs.StringVar(&kubeconfigPath, "kubeconfig", kubeconfig.DefaultPath, "path to write kubeconfig (incompatible with --auto-kubeconfig)")
fs.BoolVar(&setContext, "set-kubeconfig-context", true, "if true then current-context will be set in kubeconfig; if a context is already set then it will be overwritten")

fs.DurationVar(&cfg.AWSOperationTimeout, "aws-api-timeout", 20*time.Minute, "number of seconds after which to timeout AWS API operations")

Expand All @@ -101,10 +102,10 @@ func doCreateCluster(cfg *eks.ClusterConfig, name string) error {
cfg.ClusterName = utils.ClusterName(cfg.ClusterName, name)

if autoKubeconfigPath {
if kubeconfigPath != DEFAULT_KUBECONFIG_PATH {
if kubeconfigPath != kubeconfig.DefaultPath {
return fmt.Errorf("--kubeconfig and --auto-kubeconfig cannot be used at the same time")
}
kubeconfigPath = utils.ConfigPath(cfg.ClusterName)
kubeconfigPath = kubeconfig.AutoPath(cfg.ClusterName)
}

if cfg.SSHPublicKeyPath == "" {
Expand Down Expand Up @@ -143,12 +144,14 @@ func doCreateCluster(cfg *eks.ClusterConfig, name string) error {
return err
}

// TODO: https://github.com/weaveworks/eksctl/issues/29
if writeKubeconfig {
if err := clientConfigBase.WithExecHeptioAuthenticator().WriteToFile(kubeconfigPath); err != nil {
config := clientConfigBase.WithExecHeptioAuthenticator()
kubeconfigPath, err = kubeconfig.Write(kubeconfigPath, config.Client, setContext)
if err != nil {
return errors.Wrap(err, "writing kubeconfig")
}
logger.Info("wrote %q", kubeconfigPath)

logger.Success("saved kubeconfig as %q", kubeconfigPath)
} else {
kubeconfigPath = ""
}
Expand All @@ -172,9 +175,9 @@ func doCreateCluster(cfg *eks.ClusterConfig, name string) error {
// check kubectl version, and offer install instructions if missing or old
// also check heptio-authenticator
// TODO: https://github.com/weaveworks/eksctl/issues/30
if err := utils.CheckAllCommands(kubeconfigPath); err != nil {
if err := utils.CheckAllCommands(kubeconfigPath, setContext, clientConfigBase.ContextName); err != nil {
logger.Critical(err.Error())
logger.Info("cluster should be functional despite missing client binaries that need to be installed in the PATH")
logger.Info("cluster should be functional despite missing (or misconfigured) client binaries")
}
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/eksctl/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/kubicorn/kubicorn/pkg/logger"

"github.com/weaveworks/eksctl/pkg/eks"
"github.com/weaveworks/eksctl/pkg/utils"
"github.com/weaveworks/eksctl/pkg/utils/kubeconfig"
)

func deleteCmd() *cobra.Command {
Expand Down Expand Up @@ -89,7 +89,7 @@ func doDeleteCluster(cfg *eks.ClusterConfig, name string) error {

ctl.MaybeDeletePublicSSHKey()

utils.MaybeDeleteConfig(cfg.ClusterName)
kubeconfig.MaybeDeleteConfig(cfg.ClusterName)

logger.Success("all EKS cluster %q resource will be deleted (if in doubt, check CloudFormation console)", cfg.ClusterName)

Expand Down
14 changes: 9 additions & 5 deletions cmd/eksctl/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import (

"github.com/kubicorn/kubicorn/pkg/logger"
"github.com/weaveworks/eksctl/pkg/eks"
"github.com/weaveworks/eksctl/pkg/utils"
"github.com/weaveworks/eksctl/pkg/utils/kubeconfig"
)

var (
utilsKubeconfigInputPath string
utilsKubeconfigOutputPath string
utilsSetContext bool
)

func utilsCmd() *cobra.Command {
Expand Down Expand Up @@ -100,7 +101,8 @@ func writeKubeconfigCmd() *cobra.Command {
fs.StringVarP(&cfg.Region, "region", "r", DEFAULT_EKS_REGION, "AWS region")
fs.StringVarP(&cfg.Profile, "profile", "p", "", "AWS profile to use. If provided, this overrides the AWS_PROFILE environment variable")

fs.StringVar(&utilsKubeconfigOutputPath, "kubeconfig", "", "path to write kubeconfig")
fs.StringVar(&utilsKubeconfigOutputPath, "kubeconfig", kubeconfig.DefaultPath, "path to write kubeconfig")
fs.BoolVar(&utilsSetContext, "set-kubeconfig-context", true, "if true then current-context will be set in kubeconfig; if a context is already set then it will be overwritten")

return cmd
}
Expand All @@ -125,7 +127,7 @@ func doWriteKubeconfigCmd(cfg *eks.ClusterConfig, name string) error {
}

if utilsKubeconfigOutputPath == "" {
utilsKubeconfigOutputPath = utils.ConfigPath(cfg.ClusterName)
utilsKubeconfigOutputPath = kubeconfig.AutoPath(cfg.ClusterName)
}

cluster, err := ctl.DescribeControlPlane()
Expand All @@ -144,11 +146,13 @@ func doWriteKubeconfigCmd(cfg *eks.ClusterConfig, name string) error {
return err
}

if err := clientConfigBase.WithExecHeptioAuthenticator().WriteToFile(utilsKubeconfigOutputPath); err != nil {
config := clientConfigBase.WithExecHeptioAuthenticator()
filename, err := kubeconfig.Write(utilsKubeconfigOutputPath, config.Client, utilsSetContext)
if err != nil {
return errors.Wrap(err, "writing kubeconfig")
}

logger.Info("wrote kubeconfig file %q", utilsKubeconfigOutputPath)
logger.Success("saved kubeconfig as %q", filename)

return nil
}
33 changes: 16 additions & 17 deletions pkg/eks/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,19 @@ func (c *ClusterProvider) MaybeDeletePublicSSHKey() {

func (c *ClusterProvider) getUsername() string {
usernameParts := strings.Split(c.svc.arn, "/")
username := usernameParts[len(usernameParts)-1]
return username
if len(usernameParts) > 1 {
return usernameParts[len(usernameParts)-1]
}
return "iam-root-account"
}

type ClientConfig struct {
Client *clientcmdapi.Config
Cluster *ClusterConfig
roleARN string
sts stsiface.STSAPI
Client *clientcmdapi.Config
Cluster *ClusterConfig
ClusterName string
ContextName string
roleARN string
sts stsiface.STSAPI
}

// based on "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig"
Expand Down Expand Up @@ -109,8 +113,10 @@ func (c *ClusterProvider) NewClientConfig() (*ClientConfig, error) {
},
CurrentContext: contextName,
},
roleARN: c.svc.arn,
sts: c.svc.sts,
ClusterName: clusterName,
ContextName: contextName,
roleARN: c.svc.arn,
sts: c.svc.sts,
}

return clientConfig, nil
Expand All @@ -119,7 +125,7 @@ func (c *ClusterProvider) NewClientConfig() (*ClientConfig, error) {
func (c *ClientConfig) WithExecHeptioAuthenticator() *ClientConfig {
clientConfigCopy := *c

x := clientConfigCopy.Client.AuthInfos[c.Client.CurrentContext]
x := clientConfigCopy.Client.AuthInfos[c.ContextName]
x.Exec = &clientcmdapi.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
Command: "heptio-authenticator-aws",
Expand Down Expand Up @@ -150,19 +156,12 @@ func (c *ClientConfig) WithEmbeddedToken() (*ClientConfig, error) {
return nil, errors.Wrap(err, "could not get token")
}

x := c.Client.AuthInfos[c.Client.CurrentContext]
x := c.Client.AuthInfos[c.ContextName]
x.Token = tok

return &clientConfigCopy, nil
}

func (c *ClientConfig) WriteToFile(filename string) error {
if err := clientcmd.WriteToFile(*c.Client, filename); err != nil {
return errors.Wrapf(err, "couldn't write client config file %q", filename)
}
return nil
}

func (c *ClientConfig) NewClientSet() (*clientset.Clientset, error) {
clientConfig, err := clientcmd.NewDefaultClientConfig(*c.Client, &clientcmd.ConfigOverrides{}).ClientConfig()
if err != nil {
Expand Down
126 changes: 126 additions & 0 deletions pkg/utils/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package kubeconfig

import (
"fmt"
"os"
"path"
"strings"

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

"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"

"github.com/kubicorn/kubicorn/pkg/logger"
)

var DefaultPath = clientcmd.RecommendedHomeFile

// 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 *api.Config, setContext bool) (string, error) {
configAccess := getConfigAccess(path)

config, err := configAccess.GetStartingConfig()

logger.Debug("merging kubeconfig files")
merged, err := merge(config, newConfig)
if err != nil {
return "", errors.Wrapf(err, "unable to merge configuration with existing kubeconfig file %q", path)
}

if setContext && newConfig.CurrentContext != "" {
logger.Debug("setting current-context to %s", newConfig.CurrentContext)
merged.CurrentContext = newConfig.CurrentContext
}

if err := clientcmd.ModifyConfig(configAccess, *merged, true); err != nil {
return "", nil
}

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

func merge(existing *api.Config, tomerge *api.Config) (*api.Config, error) {
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, nil
}

func AutoPath(name string) string {
return path.Join(clientcmd.RecommendedConfigDir, "eksctl", "clusters", name)
}

func isValidConfig(p, name string) error {
clientConfig, err := clientcmd.LoadFromFile(p)
if err != nil {
return errors.Wrapf(err, "unable to load config %q", p)
}

if err := clientcmd.ConfirmUsable(*clientConfig, ""); err != nil {
return errors.Wrapf(err, "unable to parse config %q", p)
}

// we want to make sure we only delete config files that haven't be modified by the user
// checking context name is a good start, we might want ot do deeper checks later, e.g. checksum,
// as we don't want to delete any files by accident that didn't belong to us
ctxFmtErr := fmt.Errorf("unable to verify ownership of config %q, unexpected contex name %q", p, clientConfig.CurrentContext)

ctx := strings.Split(clientConfig.CurrentContext, "@")
if len(ctx) != 2 {
return ctxFmtErr
}
if strings.HasPrefix(ctx[1], name+".") && strings.HasSuffix(ctx[1], ".eksctl.io") {
return nil
}
return ctxFmtErr
}

func tryDeleteConfig(p, name string) {
if err := isValidConfig(p, name); err != nil {
logger.Debug("ignoring error while checking config file %q: %s", p, err.Error())
return
}
if err := os.Remove(p); err != nil {
logger.Debug("ignoring error while removing config file %q: %s", p, err.Error())
}
}

func MaybeDeleteConfig(name string) {
p := AutoPath(name)

autoConfExists, err := utils.FileExists(p)
if err != nil {
logger.Debug("error checking if auto-generated kubeconfig file exists %q: %s", p, err.Error())
return
}
if autoConfExists {
if err := os.Remove(p); err != nil {
logger.Debug("ignoring error while removing auto-generated config file %q: %s", p, err.Error())
}
return
}

// Print message to manually remove from config file
logger.Warning("as you are not using the auto-generated kubeconfig file you will need to remove the details of cluster %s manually", name)
}
Loading

0 comments on commit 1e2726a

Please sign in to comment.