Skip to content

Commit

Permalink
Merge pull request #993 from weaveworks/authenticator-role-arn-788-749
Browse files Browse the repository at this point in the history
 Add support for `aws eks get-token` authenticator and IAM role ARN
  • Loading branch information
errordeveloper authored Jul 18, 2019
2 parents eefb3cf + 95bfb55 commit 079c5b7
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 98 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ or [environment variables][awsenv]. For more information read [AWS documentation
[awsenv]: https://docs.aws.amazon.com/cli/latest/userguide/cli-environment.html
[awsconfig]: https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html

You will also need [AWS IAM Authenticator for Kubernetes](https://github.com/kubernetes-sigs/aws-iam-authenticator) command (either `aws-iam-authenticator` or `heptio-authenticator-aws`) in your `PATH`.
You will also need [AWS IAM Authenticator for Kubernetes](https://github.com/kubernetes-sigs/aws-iam-authenticator) command (either `aws-iam-authenticator` or `aws eks get-token` (available in version 1.16.156 or greater of AWS CLI) in your `PATH`.

## Basic usage

Expand Down
3 changes: 2 additions & 1 deletion pkg/ctl/cmdutils/cmdutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ func AddUpdateAuthConfigMap(fs *pflag.FlagSet, updateAuthConfigMap *bool, descri
}

// AddCommonFlagsForKubeconfig adds common flags for controlling how output kubeconfig is written
func AddCommonFlagsForKubeconfig(fs *pflag.FlagSet, outputPath *string, setContext, autoPath *bool, exampleName string) {
func AddCommonFlagsForKubeconfig(fs *pflag.FlagSet, outputPath, authenticatorRoleARN *string, setContext, autoPath *bool, exampleName string) {
fs.StringVar(outputPath, "kubeconfig", kubeconfig.DefaultPath, "path to write kubeconfig (incompatible with --auto-kubeconfig)")
fs.StringVar(authenticatorRoleARN, "authenticator-role-arn", "", "AWS IAM role to assume for authenticator")
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.BoolVar(autoPath, "auto-kubeconfig", false, fmt.Sprintf("save kubeconfig file by cluster name, e.g. %q", kubeconfig.AutoPath(exampleName)))
}
Expand Down
22 changes: 10 additions & 12 deletions pkg/ctl/create/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ import (
)

type createClusterCmdParams struct {
writeKubeconfig bool
kubeconfigPath string
autoKubeconfigPath bool
setContext bool
availabilityZones []string
writeKubeconfig bool
kubeconfigPath string
autoKubeconfigPath bool
authenticatorRoleARN string
setContext bool
availabilityZones []string

kopsClusterNameForVPC string
subnets map[api.SubnetTopology]*[]string
Expand Down Expand Up @@ -81,7 +82,7 @@ func createClusterCmd(rc *cmdutils.ResourceCmd) {
cmdutils.AddCommonFlagsForAWS(rc.FlagSetGroup, rc.ProviderConfig, true)

rc.FlagSetGroup.InFlagSet("Output kubeconfig", func(fs *pflag.FlagSet) {
cmdutils.AddCommonFlagsForKubeconfig(fs, &params.kubeconfigPath, &params.setContext, &params.autoKubeconfigPath, exampleClusterName)
cmdutils.AddCommonFlagsForKubeconfig(fs, &params.kubeconfigPath, &params.authenticatorRoleARN, &params.setContext, &params.autoKubeconfigPath, exampleClusterName)
fs.BoolVar(&params.writeKubeconfig, "write-kubeconfig", true, "toggle writing of kubeconfig")
})
}
Expand Down Expand Up @@ -304,13 +305,10 @@ func doCreateCluster(rc *cmdutils.ResourceCmd, params *createClusterCmdParams) e
var kubeconfigContextName string

if params.writeKubeconfig {
client, err := ctl.NewClient(cfg, false)
if err != nil {
return err
}
kubeconfigContextName = client.ContextName
kubectlConfig := kubeconfig.NewForKubectl(cfg, ctl.GetUsername(), params.authenticatorRoleARN, ctl.Provider.Profile())
kubeconfigContextName = kubectlConfig.CurrentContext

params.kubeconfigPath, err = kubeconfig.Write(params.kubeconfigPath, *client.Config, params.setContext)
params.kubeconfigPath, err = kubeconfig.Write(params.kubeconfigPath, *kubectlConfig, params.setContext)
if err != nil {
return errors.Wrap(err, "writing kubeconfig")
}
Expand Down
15 changes: 6 additions & 9 deletions pkg/ctl/utils/write_kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ func writeKubeconfigCmd(rc *cmdutils.ResourceCmd) {

var (
outputPath string
authenticatorRoleARN string
setContext, autoPath bool
)

rc.SetDescription("write-kubeconfig", "Write kubeconfig file for a given cluster", "")

rc.SetRunFuncWithNameArg(func() error {
return doWriteKubeconfigCmd(rc, outputPath, setContext, autoPath)
return doWriteKubeconfigCmd(rc, outputPath, authenticatorRoleARN, setContext, autoPath)
})

rc.FlagSetGroup.InFlagSet("General", func(fs *pflag.FlagSet) {
Expand All @@ -34,13 +35,13 @@ func writeKubeconfigCmd(rc *cmdutils.ResourceCmd) {
})

rc.FlagSetGroup.InFlagSet("Output kubeconfig", func(fs *pflag.FlagSet) {
cmdutils.AddCommonFlagsForKubeconfig(fs, &outputPath, &setContext, &autoPath, "<name>")
cmdutils.AddCommonFlagsForKubeconfig(fs, &outputPath, &authenticatorRoleARN, &setContext, &autoPath, "<name>")
})

cmdutils.AddCommonFlagsForAWS(rc.FlagSetGroup, rc.ProviderConfig, false)
}

func doWriteKubeconfigCmd(rc *cmdutils.ResourceCmd, outputPath string, setContext, autoPath bool) error {
func doWriteKubeconfigCmd(rc *cmdutils.ResourceCmd, outputPath, roleARN string, setContext, autoPath bool) error {
cfg := rc.ClusterConfig

ctl := eks.New(rc.ProviderConfig, cfg)
Expand Down Expand Up @@ -72,12 +73,8 @@ func doWriteKubeconfigCmd(rc *cmdutils.ResourceCmd, outputPath string, setContex
return err
}

client, err := ctl.NewClient(cfg, false)
if err != nil {
return err
}

filename, err := kubeconfig.Write(outputPath, *client.Config, setContext)
kubectlConfig := kubeconfig.NewForKubectl(cfg, ctl.GetUsername(), roleARN, ctl.Provider.Profile())
filename, err := kubeconfig.Write(outputPath, *kubectlConfig, setContext)
if err != nil {
return errors.Wrap(err, "writing kubeconfig")
}
Expand Down
29 changes: 12 additions & 17 deletions pkg/eks/client.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package eks

import (
"github.com/pkg/errors"
"strings"

"github.com/pkg/errors"

"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
Expand All @@ -15,7 +16,6 @@ import (

api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
kubewrapper "github.com/weaveworks/eksctl/pkg/kubernetes"
"github.com/weaveworks/eksctl/pkg/utils"
"github.com/weaveworks/eksctl/pkg/utils/kubeconfig"
)

Expand All @@ -27,35 +27,30 @@ type Client struct {
rawConfig *restclient.Config
}

// NewClient creates a new client config, if withEmbeddedToken is true
// it will embed the STS token, otherwise it will use authenticator exec plugin
// and ensures that AWS_PROFILE environment variable gets set also
func (c *ClusterProvider) NewClient(spec *api.ClusterConfig, withEmbeddedToken bool) (*Client, error) {
clientConfig, _, contextName := kubeconfig.New(spec, c.getUsername(), "")
// NewClient creates a new client config by embedding the STS token
func (c *ClusterProvider) NewClient(spec *api.ClusterConfig) (*Client, error) {
clientConfig, _, contextName := kubeconfig.New(spec, c.GetUsername(), "")

config := &Client{
Config: clientConfig,
ContextName: contextName,
}

return config.new(spec, withEmbeddedToken, c.Provider.STS(), c.Provider.Profile())
return config.new(spec, c.Provider.STS())
}

func (c *ClusterProvider) getUsername() string {
// GetUsername extracts the username part from the IAM role ARN
func (c *ClusterProvider) GetUsername() string {
usernameParts := strings.Split(c.Status.iamRoleARN, "/")
if len(usernameParts) > 1 {
return usernameParts[len(usernameParts)-1]
}
return "iam-root-account"
}

func (c *Client) new(spec *api.ClusterConfig, withEmbeddedToken bool, stsClient stsiface.STSAPI, profile string) (*Client, error) {
if withEmbeddedToken {
if err := c.useEmbeddedToken(spec, stsClient); err != nil {
return nil, err
}
} else {
kubeconfig.AppendAuthenticator(c.Config, spec, utils.DetectAuthenticator(), profile)
func (c *Client) new(spec *api.ClusterConfig, stsClient stsiface.STSAPI) (*Client, error) {
if err := c.useEmbeddedToken(spec, stsClient); err != nil {
return nil, err
}

rawConfig, err := clientcmd.NewDefaultClientConfig(*c.Config, &clientcmd.ConfigOverrides{}).ClientConfig()
Expand Down Expand Up @@ -102,7 +97,7 @@ func (c *ClusterProvider) NewStdClientSet(spec *api.ClusterConfig) (*kubernetes.
}

func (c *ClusterProvider) newClientSetWithEmbeddedToken(spec *api.ClusterConfig) (*Client, *kubernetes.Clientset, error) {
client, err := c.NewClient(spec, true)
client, err := c.NewClient(spec)
if err != nil {
return nil, nil, errors.Wrap(err, "creating Kubernetes client config with embedded token")
}
Expand Down
36 changes: 16 additions & 20 deletions pkg/eks/client_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package eks_test

import (
"fmt"
"strings"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
. "github.com/weaveworks/eksctl/pkg/eks"
"github.com/weaveworks/eksctl/pkg/testutils/mockprovider"
"github.com/weaveworks/eksctl/pkg/utils/kubeconfig"
)

var _ = Describe("eks auth helpers", func() {
Expand Down Expand Up @@ -40,17 +42,14 @@ var _ = Describe("eks auth helpers", func() {
},
}

It("should create config with authenticator", func() {
clientConfig, err := ctl.NewClient(cfg, false)

Expect(err).To(Not(HaveOccurred()))

testAuthenticatorConfig := func(roleARN string) {
clientConfig := kubeconfig.NewForKubectl(cfg, ctl.GetUsername(), roleARN, ctl.Provider.Profile())
Expect(clientConfig).To(Not(BeNil()))
ctx := clientConfig.ContextName
ctx := clientConfig.CurrentContext
cluster := strings.Split(ctx, "@")[1]
Expect(ctx).To(Equal("iam-root-account@auth-test-cluster.eu-west-3.eksctl.io"))

k := clientConfig.Config
k := clientConfig

Expect(k.CurrentContext).To(Equal(ctx))

Expand All @@ -72,32 +71,29 @@ var _ = Describe("eks auth helpers", func() {

Expect(k.AuthInfos[ctx].Exec.Command).To(MatchRegexp("(heptio-authenticator-aws|aws-iam-authenticator)"))

Expect(strings.Join(k.AuthInfos[ctx].Exec.Args, " ")).To(Equal("token -i auth-test-cluster"))
expectedArgs := "token -i auth-test-cluster"
if roleARN != "" {
expectedArgs += fmt.Sprintf(" -r %s", roleARN)
}
Expect(strings.Join(k.AuthInfos[ctx].Exec.Args, " ")).To(Equal(expectedArgs))

Expect(k.Clusters).To(HaveKey(cluster))
Expect(k.Clusters).To(HaveLen(1))

Expect(k.Clusters[cluster].InsecureSkipTLSVerify).To(BeFalse())
Expect(k.Clusters[cluster].Server).To(Equal(cfg.Status.Endpoint))
Expect(k.Clusters[cluster].CertificateAuthorityData).To(Equal(cfg.Status.CertificateAuthorityData))
}

It("should create config with authenticator", func() {
testAuthenticatorConfig("")
testAuthenticatorConfig("arn:aws:iam::111111111111:role/eksctl")
})

It("should create config with embedded token", func() {
// TODO: cannot test this, as token generator uses STS directly, we cannot pass the interface
// we can probably fix the package itself
})

It("should create clientset", func() {
clientConfig, err := ctl.NewClient(cfg, false)

Expect(err).To(Not(HaveOccurred()))
Expect(clientConfig).To(Not(BeNil()))

clientSet, err := clientConfig.NewClientSet()

Expect(err).To(Not(HaveOccurred()))
Expect(clientSet).To(Not(BeNil()))
})
})
})
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/nodebootstrap/userdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func makeClientConfigData(spec *api.ClusterConfig, ng *api.NodeGroup) ([]byte, e
if ng.AMIFamily == ami.ImageFamilyUbuntu1804 {
authenticator = kubeconfig.HeptioAuthenticatorAWS
}
kubeconfig.AppendAuthenticator(clientConfig, spec, authenticator, "")
kubeconfig.AppendAuthenticator(clientConfig, spec, authenticator, "", "")
clientConfigData, err := clientcmd.Write(*clientConfig)
if err != nil {
return nil, errors.Wrap(err, "serialising kubeconfig for nodegroup")
Expand Down
63 changes: 56 additions & 7 deletions pkg/utils/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package kubeconfig

import (
"fmt"
"github.com/weaveworks/eksctl/pkg/utils/file"
"os"
"path"
"strings"

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

"os/exec"

"github.com/kris-nova/logger"
"github.com/pkg/errors"
api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
Expand All @@ -22,18 +25,21 @@ const (
AWSIAMAuthenticator = "aws-iam-authenticator"
// HeptioAuthenticatorAWS defines the old name of AWS IAM authenticator
HeptioAuthenticatorAWS = "heptio-authenticator-aws"
// AWSEKSAuthenticator defines the recently added `aws eks get-token` command
AWSEKSAuthenticator = "aws"
)

// AuthenticatorCommands returns all of authenticator commands
func AuthenticatorCommands() []string {
return []string{
AWSIAMAuthenticator,
HeptioAuthenticatorAWS,
AWSEKSAuthenticator,
}
}

// New creates Kubernetes client configuration for a given username
// if certificateAuthorityPath is no empty, it is used instead of
// if certificateAuthorityPath is not empty, it is used instead of
// embedded certificate-authority-data
func New(spec *api.ClusterConfig, username, certificateAuthorityPath string) (*clientcmdapi.Config, string, string) {
clusterName := spec.Metadata.String()
Expand Down Expand Up @@ -66,26 +72,58 @@ func New(spec *api.ClusterConfig, username, certificateAuthorityPath string) (*c
return c, clusterName, contextName
}

// NewForKubectl creates configuration for kubectl using a suitable authenticator
func NewForKubectl(spec *api.ClusterConfig, username, roleARN, profile string) *clientcmdapi.Config {
config, _, _ := New(spec, username, "")
authenticator, found := LookupAuthenticator()
if !found {
// fall back to aws-iam-authenticator
authenticator = AWSIAMAuthenticator
}
AppendAuthenticator(config, spec, authenticator, roleARN, profile)
return config
}

// AppendAuthenticator appends the AWS IAM authenticator, and
// if profile is non-empty string it sets AWS_PROFILE environment
// variable also
func AppendAuthenticator(c *clientcmdapi.Config, spec *api.ClusterConfig, command, profile string) {
func AppendAuthenticator(config *clientcmdapi.Config, spec *api.ClusterConfig, authenticatorCMD, roleARN, profile string) {
var (
args []string
roleARNFlag string
)

switch authenticatorCMD {
case AWSIAMAuthenticator, HeptioAuthenticatorAWS:
args = []string{"token", "-i", spec.Metadata.Name}
roleARNFlag = "-r"
case AWSEKSAuthenticator:
args = []string{"eks", "get-token", "--cluster-name", spec.Metadata.Name}
roleARNFlag = "--role-arn"
if spec.Metadata.Region != "" {
args = append(args, "--region", spec.Metadata.Region)
}
}
if roleARN != "" {
args = append(args, roleARNFlag, roleARN)
}

execConfig := &clientcmdapi.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1alpha1",
Command: command,
Args: []string{"token", "-i", spec.Metadata.Name},
Command: authenticatorCMD,
Args: args,
}

if profile != "" {
execConfig.Env = []clientcmdapi.ExecEnvVar{
clientcmdapi.ExecEnvVar{
{
Name: "AWS_PROFILE",
Value: profile,
},
}
}

c.AuthInfos[c.CurrentContext] = &clientcmdapi.AuthInfo{
config.AuthInfos[config.CurrentContext] = &clientcmdapi.AuthInfo{
Exec: execConfig,
}
}
Expand Down Expand Up @@ -248,3 +286,14 @@ func deleteClusterInfo(existing *clientcmdapi.Config, cl *api.ClusterMeta) bool

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
}
}
return "", false
}
Loading

0 comments on commit 079c5b7

Please sign in to comment.