Skip to content

Commit

Permalink
Add command to manipulate IAM identity mappings
Browse files Browse the repository at this point in the history
Adds the following commands to get/create/delete IAM role mappings to
Kubernetes username and groups.

    eksctl get iamidentitymapping [--role arn]
    eksctl create iamidentitymapping --role <arn> [--username=USER] --group=GROUP0 [--group=GROUP1]
    eksctl delete iamidentitymapping --role <arn> [--all]

`eksctl get iamidentitymapping`
Returns all mappings; if role filter is given it returns all matching
roles (can be more than one).

`eksctl create iamidentitymapping`
Allows creating duplicates. Will warn if duplicates exist.

`eksctl delete iamidentitymapping`
Deletes a single mapping FIFO unless `--all` is given in which case it
removes all matching. Will warn if more mappings matching this role are
found.
  • Loading branch information
rndstr committed May 24, 2019
1 parent 2fa2bc6 commit 4f24c08
Show file tree
Hide file tree
Showing 12 changed files with 526 additions and 41 deletions.
12 changes: 11 additions & 1 deletion integration/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,18 @@ func eksctl(args ...string) *gexec.Session {
default:
t *= 30
}

session.Wait(t)
return session
}

func eksctlSuccess(args ...string) *gexec.Session {
session := eksctl(args...)
Expect(session.ExitCode()).To(Equal(0))
return session
}

func eksctlFail(args ...string) *gexec.Session {
session := eksctl(args...)
Expect(session.ExitCode()).To(Not(Equal(0)))
return session
}
137 changes: 125 additions & 12 deletions integration/creategetdelete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ import (

awseks "github.com/aws/aws-sdk-go/service/eks"
harness "github.com/dlespiau/kube-test-harness"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/yaml"

api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
"github.com/weaveworks/eksctl/pkg/authconfigmap"
"github.com/weaveworks/eksctl/pkg/ctl/cmdutils"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"

"github.com/weaveworks/eksctl/pkg/iam"
"github.com/weaveworks/eksctl/pkg/testutils/aws"
. "github.com/weaveworks/eksctl/pkg/testutils/matchers"
)
Expand Down Expand Up @@ -63,7 +64,7 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() {
clusterName = cmdutils.ClusterName("", "")
}

eksctl("create", "cluster",
eksctlSuccess("create", "cluster",
"--verbose", "4",
"--name", clusterName,
"--tags", "alpha.eksctl.io/description=eksctl integration test",
Expand Down Expand Up @@ -101,14 +102,14 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() {

Context("and listing clusters", func() {
It("should return the previously created cluster", func() {
cmdSession := eksctl("get", "clusters", "--region", region)
cmdSession := eksctlSuccess("get", "clusters", "--region", region)
Expect(string(cmdSession.Buffer().Contents())).To(ContainSubstring(clusterName))
})
})

Context("and scale the initial nodegroup", func() {
It("should not return an error", func() {
eksctl("scale", "nodegroup",
eksctlSuccess("scale", "nodegroup",
"--verbose", "4",
"--cluster", clusterName,
"--region", region,
Expand All @@ -134,7 +135,7 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() {

Context("and add the second nodegroup", func() {
It("should not return an error", func() {
eksctl("create", "nodegroup",
eksctlSuccess("create", "nodegroup",
"--cluster", clusterName,
"--region", region,
"--nodes", "4",
Expand Down Expand Up @@ -220,9 +221,121 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() {
})
})

Context("and manipulating iam identity mappings", func() {
role := "arn:aws:iam::123456:role/eksctl-testing-XYZ"
role0 := authconfigmap.MapRole{
RoleARN: role,
Identity: iam.Identity{
Username: "admin",
Groups: []string{"system:masters", "system:nodes"},
},
}
role1 := authconfigmap.MapRole{
RoleARN: role,
Identity: iam.Identity{
Groups: []string{"system:something"},
},
}
var exp0, exp1 string

It("should marshal authconfigmap.MapRole", func() {
bs0, err := yaml.Marshal([]authconfigmap.MapRole{role0})
Expect(err).ShouldNot(HaveOccurred())
exp0 = string(bs0)

bs1, err := yaml.Marshal([]authconfigmap.MapRole{role1})
Expect(err).ShouldNot(HaveOccurred())
exp1 = string(bs1)
})
It("fails getting unknown mapping", func() {
eksctlFail("get", "iamidentitymapping",
"--cluster", clusterName,
"--role", "idontexist",
"-o", "yaml",
)
})
It("creates mapping", func() {
eksctlSuccess("create", "iamidentitymapping",
"--cluster", clusterName,
"--role", role0.RoleARN,
"--username", role0.Username,
"--group", role0.Groups[0],
"--group", role0.Groups[1],
)

get := eksctlSuccess("get", "iamidentitymapping",
"--cluster", clusterName,
"--role", role0.RoleARN,
"-o", "yaml",
)
Expect(string(get.Buffer().Contents())).To(MatchYAML(exp0))
})
It("creates a duplicate mapping", func() {
eksctlSuccess("create", "iamidentitymapping",
"--cluster", clusterName,
"--role", role0.RoleARN,
"--username", role0.Username,
"--group", role0.Groups[0],
"--group", role0.Groups[1],
)

get := eksctlSuccess("get", "iamidentitymapping",
"--cluster", clusterName,
"--role", role0.RoleARN,
"-o", "yaml",
)
Expect(string(get.Buffer().Contents())).To(MatchYAML(exp0 + exp0))
})
It("creates a duplicate mapping with different identity", func() {
eksctlSuccess("create", "iamidentitymapping",
"--cluster", clusterName,
"--role", role1.RoleARN,
"--group", role1.Groups[0],
)

get := eksctlSuccess("get", "iamidentitymapping",
"--cluster", clusterName,
"--role", role1.RoleARN,
"-o", "yaml",
)
Expect(string(get.Buffer().Contents())).To(MatchYAML(exp0 + exp0 + exp1))
})
It("deletes a single mapping fifo", func() {
eksctlSuccess("delete", "iamidentitymapping",
"--cluster", clusterName,
"--role", role,
)

get := eksctlSuccess("get", "iamidentitymapping",
"--cluster", clusterName,
"--role", role,
"-o", "yaml",
)
Expect(string(get.Buffer().Contents())).To(MatchYAML(exp0 + exp1))
})
It("fails when deleting unknown mapping", func() {
eksctlFail("delete", "iamidentitymapping",
"--cluster", clusterName,
"--role", "idontexist",
)
})
It("deletes duplicate mappings with --all", func() {
eksctlSuccess("delete", "iamidentitymapping",
"--cluster", clusterName,
"--role", role,
"--all",
)
eksctlFail("get", "iamidentitymapping",
"--cluster", clusterName,
"--role", role,
"-o", "yaml",
)
})
})

Context("and delete the second nodegroup", func() {
It("should not return an error", func() {
eksctl("delete", "nodegroup",
eksctlSuccess("delete", "nodegroup",
"--verbose", "4",
"--cluster", clusterName,
"--region", region,
Expand All @@ -249,7 +362,7 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() {

Context("and scale the initial nodegroup back to 1 node", func() {
It("should not return an error", func() {
eksctl("scale", "nodegroup",
eksctlSuccess("scale", "nodegroup",
"--verbose", "4",
"--cluster", clusterName,
"--region", region,
Expand Down Expand Up @@ -279,7 +392,7 @@ var _ = Describe("(Integration) Create, Get, Scale & Delete", func() {
Skip("will not delete cluster " + clusterName)
}

eksctl("delete", "cluster",
eksctlSuccess("delete", "cluster",
"--verbose", "4",
"--name", clusterName,
"--region", region,
Expand Down
83 changes: 58 additions & 25 deletions pkg/authconfigmap/authconfigmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ import (
"sigs.k8s.io/yaml"

api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5"
"github.com/weaveworks/eksctl/pkg/iam"
)

type mapRole map[string]interface{}
type mapRoles []mapRole

const (
// ObjectName is the Kubernetes resource name of the auth ConfigMap
ObjectName = "aws-auth"
Expand All @@ -48,6 +46,27 @@ const (
// with the cluster, required for the instance role ARNs of node groups.
var RoleNodeGroupGroups = []string{"system:bootstrappers", "system:nodes"}

// MapRole represents an IAM identity with role.
type MapRole struct {
iam.Identity `json:",inline"`
RoleARN string `json:"rolearn"`
}

// MapRoles is a list of IAM identities with roles.
type MapRoles []MapRole

// Get returns all matching role mappings. Note that at this moment
// aws-iam-authenticator only considers the last one!
func (rs MapRoles) Get(arn string) MapRoles {
var m MapRoles
for _, r := range rs {
if r.RoleARN == arn {
m = append(m, r)
}
}
return m
}

// AuthConfigMap allows modifying the auth ConfigMap.
type AuthConfigMap struct {
client v1.ConfigMapInterface
Expand Down Expand Up @@ -141,50 +160,60 @@ func (a *AuthConfigMap) setAccounts(accounts []string) error {
// a role with given groups. If you are calling
// this as part of node creation you should use DefaultNodeGroups.
func (a *AuthConfigMap) AddRole(arn string, username string, groups []string) error {
roles, err := a.roles()
roles, err := a.Roles()
if err != nil {
return err
}
roles = append(roles, mapRole{
"rolearn": arn,
"username": username,
"groups": groups,
roles = append(roles, MapRole{
RoleARN: arn,
Identity: iam.Identity{
Username: username,
Groups: groups,
},
})
logger.Info("adding role %q to auth ConfigMap", arn)
return a.setRoles(roles)
}

// RemoveRole removes exactly one entry, even if there are duplicates.
// If it cannot find the role it returns an error.
func (a *AuthConfigMap) RemoveRole(arn string) error {
if arn == "" {
return errors.New("nodegroup instance role ARN is not set")
}
roles, err := a.roles()
// RemoveRole removes a role. If `all` is false it will only
// remove the first it encounters and return an error if it cannot
// find it.
// If `all` is true it will remove all of them and not return an
// error if it cannot be found.
func (a *AuthConfigMap) RemoveRole(arn string, all bool) error {
roles, err := a.Roles()
if err != nil {
return err
}

newroles := MapRoles{}
for i, role := range roles {
if role["rolearn"] == arn {
logger.Info("removing role %q from auth ConfigMap", arn)
roles = append(roles[:i], roles[i+1:]...)
return a.setRoles(roles)
if role.RoleARN == arn {
logger.Info("removing role %q from auth ConfigMap (username = %q, groups = %q)", arn, role.Username, role.Groups)
if !all {
roles = append(roles[:i], roles[i+1:]...)
return a.setRoles(roles)
}
} else if all {
newroles = append(newroles, role)
}
}

return fmt.Errorf("instance role ARN %q not found in auth ConfigMap", arn)
if !all {
return fmt.Errorf("instance role ARN %q not found in auth ConfigMap", arn)
}
return a.setRoles(newroles)
}

func (a *AuthConfigMap) roles() (mapRoles, error) {
var roles mapRoles
// Roles returns a list of roles that are currently in the (cached) configmap.
func (a *AuthConfigMap) Roles() (MapRoles, error) {
var roles MapRoles
if err := yaml.Unmarshal([]byte(a.cm.Data[rolesData]), &roles); err != nil {
return nil, errors.Wrap(err, "unmarshalling mapRoles")
}
return roles, nil
}

func (a *AuthConfigMap) setRoles(r mapRoles) error {
func (a *AuthConfigMap) setRoles(r MapRoles) error {
bs, err := yaml.Marshal(r)
if err != nil {
return errors.Wrap(err, "marshalling mapRoles")
Expand Down Expand Up @@ -233,11 +262,15 @@ func AddNodeGroup(clientSet kubernetes.Interface, ng *api.NodeGroup) error {
// RemoveNodeGroup removes a nodegroup from the ConfigMap and
// does a client update.
func RemoveNodeGroup(clientSet kubernetes.Interface, ng *api.NodeGroup) error {
arn := ng.IAM.InstanceRoleARN
if arn == "" {
return errors.New("nodegroup instance role ARN is not set")
}
acm, err := NewFromClientSet(clientSet)
if err != nil {
return err
}
if err := acm.RemoveRole(ng.IAM.InstanceRoleARN); err != nil {
if err := acm.RemoveRole(arn, false); err != nil {
return errors.Wrap(err, "removing nodegroup from auth ConfigMap")
}
if err := acm.Save(); err != nil {
Expand Down
15 changes: 13 additions & 2 deletions pkg/authconfigmap/authconfigmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ var _ = Describe("AuthConfigMap{}", func() {

removeAndSave := func(arn string) *corev1.ConfigMap {
client.reset()
err := acm.RemoveRole(arn)
err := acm.RemoveRole(arn, false)
Expect(err).NotTo(HaveOccurred())

err = acm.Save()
Expand All @@ -187,9 +187,20 @@ var _ = Describe("AuthConfigMap{}", func() {
Expect(cm.Data["mapRoles"]).To(MatchYAML("[]"))
})
It("should fail if role not found", func() {
err := acm.RemoveRole(roleA)
err := acm.RemoveRole(roleA, false)
Expect(err).To(HaveOccurred())
})
It("should remove all if specified", func() {
err := acm.AddRole(roleA, RoleNodeGroupUsername, RoleNodeGroupGroups)
Expect(err).NotTo(HaveOccurred())
err = acm.AddRole(roleA, RoleNodeGroupUsername, RoleNodeGroupGroups)
Expect(err).NotTo(HaveOccurred())
Expect(client.updated.Data["mapRoles"]).To(Not(MatchYAML("[]")))

err = acm.RemoveRole(roleA, true)
Expect(err).NotTo(HaveOccurred())
Expect(client.updated.Data["mapRoles"]).To(MatchYAML("[]"))
})
})
Describe("AddAccount()", func() {
existing := &corev1.ConfigMap{
Expand Down
Loading

0 comments on commit 4f24c08

Please sign in to comment.