|
|
@@ -0,0 +1,311 @@
|
|
|
+package aws
|
|
|
+
|
|
|
+// Copy of eksctl that uses authconfigmap
|
|
|
+// https://github.com/weaveworks/eksctl/blob/35d0d6dc169e13b1fb655aea19d3aa2e7691dc81/pkg/authconfigmap/authconfigmap.go#L1
|
|
|
+//
|
|
|
+// eksctl is still pinned on v0.16.8 of Kubernetes, while we use v0.18.8
|
|
|
+
|
|
|
+import (
|
|
|
+ "context"
|
|
|
+ "fmt"
|
|
|
+ "strings"
|
|
|
+
|
|
|
+ "github.com/kris-nova/logger"
|
|
|
+ "github.com/pkg/errors"
|
|
|
+ "k8s.io/client-go/kubernetes"
|
|
|
+ "sigs.k8s.io/yaml"
|
|
|
+
|
|
|
+ corev1 "k8s.io/api/core/v1"
|
|
|
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
|
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
+ v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
|
+
|
|
|
+ "github.com/aws/aws-sdk-go/aws/arn"
|
|
|
+ "github.com/aws/aws-sdk-go/aws/awsutil"
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ // ObjectName is the Kubernetes resource name of the auth ConfigMap
|
|
|
+ ObjectName = "aws-auth"
|
|
|
+ // ObjectNamespace is the namespace the object can be found
|
|
|
+ ObjectNamespace = metav1.NamespaceSystem
|
|
|
+
|
|
|
+ rolesData = "mapRoles"
|
|
|
+
|
|
|
+ usersData = "mapUsers"
|
|
|
+
|
|
|
+ accountsData = "mapAccounts"
|
|
|
+
|
|
|
+ // GroupMasters is the admin group which is also automatically
|
|
|
+ // granted to the IAM role that creates the cluster.
|
|
|
+ GroupMasters = "system:masters"
|
|
|
+
|
|
|
+ // RoleNodeGroupUsername is the default username for a nodegroup
|
|
|
+ // role mapping.
|
|
|
+ RoleNodeGroupUsername = "system:node:{{EC2PrivateDNSName}}"
|
|
|
+)
|
|
|
+
|
|
|
+// AuthConfigMap allows modifying the auth ConfigMap.
|
|
|
+type AuthConfigMap struct {
|
|
|
+ client v1.ConfigMapInterface
|
|
|
+ cm *corev1.ConfigMap
|
|
|
+}
|
|
|
+
|
|
|
+// New creates an AuthConfigMap instance that manipulates
|
|
|
+// a ConfigMap. If it is nil, one is created.
|
|
|
+func New(client v1.ConfigMapInterface, cm *corev1.ConfigMap) *AuthConfigMap {
|
|
|
+ if cm == nil {
|
|
|
+ cm = &corev1.ConfigMap{
|
|
|
+ ObjectMeta: ObjectMeta(),
|
|
|
+ Data: map[string]string{},
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if cm.Data == nil {
|
|
|
+ cm.ObjectMeta = ObjectMeta()
|
|
|
+ cm.Data = map[string]string{}
|
|
|
+ }
|
|
|
+ return &AuthConfigMap{client: client, cm: cm}
|
|
|
+}
|
|
|
+
|
|
|
+// ObjectMeta constructs metadata for the ConfigMap.
|
|
|
+func ObjectMeta() metav1.ObjectMeta {
|
|
|
+ return metav1.ObjectMeta{
|
|
|
+ Name: ObjectName,
|
|
|
+ Namespace: ObjectNamespace,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// NewFromClientSet fetches the auth ConfigMap.
|
|
|
+func NewFromClientSet(clientSet kubernetes.Interface) (*AuthConfigMap, error) {
|
|
|
+ client := clientSet.CoreV1().ConfigMaps(ObjectNamespace)
|
|
|
+
|
|
|
+ cm, err := client.Get(context.TODO(), ObjectName, metav1.GetOptions{})
|
|
|
+ // It is fine for the configmap not to exist. Any other error is fatal.
|
|
|
+ if err != nil && !apierrors.IsNotFound(err) {
|
|
|
+ return nil, errors.Wrapf(err, "getting auth ConfigMap")
|
|
|
+ }
|
|
|
+ logger.Debug("aws-auth = %s", awsutil.Prettify(cm))
|
|
|
+ return New(client, cm), nil
|
|
|
+}
|
|
|
+
|
|
|
+// AddIdentity maps an IAM role or user ARN to a k8s group dynamically. It modifies the
|
|
|
+// role or user with given groups. If you are calling
|
|
|
+// this as part of node creation you should use DefaultNodeGroups.
|
|
|
+func (a *AuthConfigMap) AddIdentity(identity Identity) error {
|
|
|
+ identities, err := a.Identities()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ identities = append(identities, identity)
|
|
|
+
|
|
|
+ logger.Info("adding identity %q to auth ConfigMap", identity.ARN())
|
|
|
+ return a.setIdentities(identities)
|
|
|
+}
|
|
|
+
|
|
|
+// Identities returns a list of iam users and roles that are currently in the (cached) configmap.
|
|
|
+func (a *AuthConfigMap) Identities() ([]Identity, error) {
|
|
|
+ var roles []RoleIdentity
|
|
|
+ if err := yaml.Unmarshal([]byte(a.cm.Data[rolesData]), &roles); err != nil {
|
|
|
+ return nil, errors.Wrapf(err, "unmarshalling %q", rolesData)
|
|
|
+ }
|
|
|
+
|
|
|
+ var users []UserIdentity
|
|
|
+ if err := yaml.Unmarshal([]byte(a.cm.Data[usersData]), &users); err != nil {
|
|
|
+ return nil, errors.Wrapf(err, "unmarshalling %q", usersData)
|
|
|
+ }
|
|
|
+
|
|
|
+ all := make([]Identity, len(users)+len(roles))
|
|
|
+ for i, r := range roles {
|
|
|
+ all[i] = r
|
|
|
+ }
|
|
|
+ for i, u := range users {
|
|
|
+ all[i+len(roles)] = u
|
|
|
+ }
|
|
|
+ return all, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (a *AuthConfigMap) setIdentities(identities []Identity) error {
|
|
|
+ // Split identities into list of roles and list of users
|
|
|
+ users, roles := []Identity{}, []Identity{}
|
|
|
+ for _, identity := range identities {
|
|
|
+ switch identity.Type() {
|
|
|
+ case ResourceTypeRole:
|
|
|
+ roles = append(roles, identity)
|
|
|
+ case ResourceTypeUser:
|
|
|
+ users = append(users, identity)
|
|
|
+ default:
|
|
|
+ return errors.Errorf("cannot determine if %q refers to a user or role during setIdentities preprocessing", identity.ARN())
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update the corresponding keys
|
|
|
+ _roles, err := yaml.Marshal(roles)
|
|
|
+ if err != nil {
|
|
|
+ return errors.Wrapf(err, "marshalling %q", rolesData)
|
|
|
+ }
|
|
|
+ a.cm.Data[rolesData] = string(_roles)
|
|
|
+
|
|
|
+ _users, err := yaml.Marshal(users)
|
|
|
+ if err != nil {
|
|
|
+ return errors.Wrapf(err, "marshalling %q", usersData)
|
|
|
+ }
|
|
|
+ a.cm.Data[usersData] = string(_users)
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// Save persists the ConfigMap to the cluster. It determines
|
|
|
+// whether to create or update by looking at the ConfigMap's UID.
|
|
|
+func (a *AuthConfigMap) Save() (err error) {
|
|
|
+ if a.cm.UID == "" {
|
|
|
+ a.cm, err = a.client.Create(context.TODO(), a.cm, metav1.CreateOptions{})
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ a.cm, err = a.client.Update(context.TODO(), a.cm, metav1.UpdateOptions{})
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
+var (
|
|
|
+ // ErrNeitherUserNorRole is the error returned when an identity is missing both UserARN
|
|
|
+ // and RoleARN.
|
|
|
+ ErrNeitherUserNorRole = errors.New("arn is neither user nor role")
|
|
|
+
|
|
|
+ // ErrNoKubernetesIdentity is the error returned when an identity has neither a Kubernetes
|
|
|
+ // username nor a list of groups.
|
|
|
+ ErrNoKubernetesIdentity = errors.New("neither username nor group are set for iam identity")
|
|
|
+)
|
|
|
+
|
|
|
+// Identity represents an IAM identity and its corresponding Kubernetes identity
|
|
|
+type Identity interface {
|
|
|
+ ARN() string
|
|
|
+ Type() string
|
|
|
+ Username() string
|
|
|
+ Groups() []string
|
|
|
+}
|
|
|
+
|
|
|
+// KubernetesIdentity represents a kubernetes identity to be used in iam mappings
|
|
|
+type KubernetesIdentity struct {
|
|
|
+ KubernetesUsername string `json:"username,omitempty"`
|
|
|
+ KubernetesGroups []string `json:"groups,omitempty"`
|
|
|
+}
|
|
|
+
|
|
|
+// UserIdentity represents a mapping from an IAM user to a kubernetes identity
|
|
|
+type UserIdentity struct {
|
|
|
+ UserARN string `json:"userarn,omitempty"`
|
|
|
+ KubernetesIdentity
|
|
|
+}
|
|
|
+
|
|
|
+// RoleIdentity represents a mapping from an IAM role to a kubernetes identity
|
|
|
+type RoleIdentity struct {
|
|
|
+ RoleARN string `json:"rolearn,omitempty"`
|
|
|
+ KubernetesIdentity
|
|
|
+}
|
|
|
+
|
|
|
+// Username returns the Kubernetes username
|
|
|
+func (k KubernetesIdentity) Username() string {
|
|
|
+ return k.KubernetesUsername
|
|
|
+}
|
|
|
+
|
|
|
+// Groups returns the Kubernetes groups
|
|
|
+func (k KubernetesIdentity) Groups() []string {
|
|
|
+ return k.KubernetesGroups
|
|
|
+}
|
|
|
+
|
|
|
+// ARN returns the ARN of the iam mapping
|
|
|
+func (u UserIdentity) ARN() string {
|
|
|
+ return u.UserARN
|
|
|
+}
|
|
|
+
|
|
|
+// Type returns the resource type of the iam mapping
|
|
|
+func (u UserIdentity) Type() string {
|
|
|
+ return ResourceTypeUser
|
|
|
+}
|
|
|
+
|
|
|
+// ARN returns the ARN of the iam mapping
|
|
|
+func (r RoleIdentity) ARN() string {
|
|
|
+ return r.RoleARN
|
|
|
+}
|
|
|
+
|
|
|
+// Type returns the resource type of the iam mapping
|
|
|
+func (r RoleIdentity) Type() string {
|
|
|
+ return ResourceTypeRole
|
|
|
+}
|
|
|
+
|
|
|
+// NewIdentity determines into which field the given arn goes and returns the new identity
|
|
|
+// alongside any error resulting for checking its validity.
|
|
|
+func NewIdentity(arn string, username string, groups []string) (Identity, error) {
|
|
|
+ if arn == "" {
|
|
|
+ return nil, fmt.Errorf("expected a valid arn but got empty string")
|
|
|
+ }
|
|
|
+ if username == "" && len(groups) == 0 {
|
|
|
+ return nil, ErrNoKubernetesIdentity
|
|
|
+ }
|
|
|
+
|
|
|
+ parsedARN, err := Parse(arn)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ switch {
|
|
|
+ case parsedARN.IsUser():
|
|
|
+ return &UserIdentity{
|
|
|
+ UserARN: arn,
|
|
|
+ KubernetesIdentity: KubernetesIdentity{
|
|
|
+ KubernetesUsername: username,
|
|
|
+ KubernetesGroups: groups,
|
|
|
+ },
|
|
|
+ }, nil
|
|
|
+ case parsedARN.IsRole():
|
|
|
+ return &RoleIdentity{
|
|
|
+ RoleARN: arn,
|
|
|
+ KubernetesIdentity: KubernetesIdentity{
|
|
|
+ KubernetesUsername: username,
|
|
|
+ KubernetesGroups: groups,
|
|
|
+ },
|
|
|
+ }, nil
|
|
|
+ default:
|
|
|
+ return nil, ErrNeitherUserNorRole
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const (
|
|
|
+ // ResourceTypeRole is the resource type of the role ARN
|
|
|
+ ResourceTypeRole = "role"
|
|
|
+ // ResourceTypeUser is the resource type of the user ARN
|
|
|
+ ResourceTypeUser = "user"
|
|
|
+)
|
|
|
+
|
|
|
+// ARN implements the pflag.Value interface for aws-sdk-go/aws/arn.ARN
|
|
|
+type ARN struct {
|
|
|
+ arn.ARN
|
|
|
+}
|
|
|
+
|
|
|
+// Parse wraps the aws-sdk-go/aws/arn.Parse function and instead returns a
|
|
|
+// iam.ARN
|
|
|
+func Parse(s string) (ARN, error) {
|
|
|
+ a, err := arn.Parse(s)
|
|
|
+ return ARN{a}, err
|
|
|
+}
|
|
|
+
|
|
|
+// ResourceType returns the type of the resource specified in the ARN.
|
|
|
+// Typically, in the case of IAM, it is a role or a user
|
|
|
+func (a *ARN) ResourceType() string {
|
|
|
+ t := a.Resource
|
|
|
+ if idx := strings.Index(t, "/"); idx >= 0 {
|
|
|
+ t = t[:idx] // remove everything following the forward slash
|
|
|
+ }
|
|
|
+
|
|
|
+ return t
|
|
|
+}
|
|
|
+
|
|
|
+// IsUser returns whether the arn represents a IAM user or not
|
|
|
+func (a *ARN) IsUser() bool {
|
|
|
+ return a.ResourceType() == ResourceTypeUser
|
|
|
+}
|
|
|
+
|
|
|
+// IsRole returns whether the arn represents a IAM role or not
|
|
|
+func (a *ARN) IsRole() bool {
|
|
|
+ return a.ResourceType() == ResourceTypeRole
|
|
|
+}
|