Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support team-id annotation #139

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apis/core/v1alpha1/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ const (
// TODO(jaypipes): Link to documentation on cross-account resource
// management
AnnotationOwnerAccountID = AnnotationPrefix + "owner-account-id"
// AnnotationTeamID is an annotation whose value is the identifier
// for the AWS team ID to manage the resources. If this annotation
// is set on a CR, the Kubernetes user is indicating that the ACK service
// controller should create/patch/delete the resource in the specified AWS
// role for this team ID. In order for this cross-account resource management
// to succeed, the AWS IAM Role that the ACK service controller runs as needs
// to have the ability to call the AWS STS::AssumeRole API call and assume an
// IAM Role in the target AWS Account.
AnnotationTeamID = AnnotationPrefix + "team-id"
// AnnotationRegion is an annotation whose value is the identifier for the
// the AWS region in which the resources should be created. If this annotation
// is set on a CR metadata, that means the user is indicating to the ACK service
Expand Down
3 changes: 3 additions & 0 deletions apis/core/v1alpha1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ type AWSRegion string
// AWSAccountID represents an AWS account identifier
type AWSAccountID string

// TeamID represents a team ID identifier.
type TeamID string

// AWSResourceName represents an AWS Resource Name (ARN)
type AWSResourceName string

Expand Down
79 changes: 66 additions & 13 deletions pkg/runtime/adoption_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"context"
"fmt"

"github.com/aws/aws-sdk-go/aws/arn"
"github.com/go-logr/logr"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -108,27 +109,47 @@ func (r *adoptionReconciler) reconcile(ctx context.Context, req ctrlrt.Request)
return ackerr.NotAdoptable
}

// If a user specified a namespace with role ARN annotation,
// we need to get the role and set the accout ID to that role.
teamID := r.getTeamID(res)

// If a user has specified a namespace that is annotated with the
// an owner account ID, we need an appropriate role ARN to assume
// in order to perform the reconciliation. The roles ARN are typically
// stored in a ConfigMap in the ACK system namespace.
// If the ConfigMap is not created, or not populated with an
// accountID to roleARN mapping, we need to properly requeue with a
// helpful message to the user.
var roleARN ackv1alpha1.AWSResourceName
acctID, needCARMLookup := r.getOwnerAccountID(res)
if needCARMLookup {
// This means that the user is specifying a namespace that is
// annotated with an owner account ID. We need to retrieve the
// roleARN from the ConfigMap and properly requeue if the roleARN
// is not available.
roleARN, err = r.getRoleARN(acctID)

var roleARN ackv1alpha1.AWSResourceName
if teamID != "" {
roleARN, err = r.getTeamRoleARN(teamID)
if err != nil {
ackrtlog.InfoAdoptedResource(r.log, res, fmt.Sprintf("Unable to start adoption reconcilliation %s: %v", acctID, err))
// r.getRoleARN errors are not terminal, we should requeue.
return requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
}
parsedARN, err := arn.Parse(string(roleARN))
if err != nil {
return fmt.Errorf("failed to parsed role ARN %q from namespace annotation: %v", roleARN, err)
}
acctID = ackv1alpha1.AWSAccountID(parsedARN.AccountID)
} else {
if needCARMLookup {
// This means that the user is specifying a namespace that is
// annotated with an owner account ID or team ID. We need to retrieve the
// roleARN from the ConfigMap and properly requeue if the roleARN
// is not available.
roleARN, err = r.getOwnerAccountRoleARN(acctID)
if err != nil {
ackrtlog.InfoAdoptedResource(r.log, res, fmt.Sprintf("Unable to start adoption reconcilliation %s: %v", acctID, err))
// r.getRoleARN errors are not terminal, we should requeue.
return requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
}
}
}

region := r.getRegion(res)
targetDescriptor := rmf.ResourceDescriptor()
endpointURL := r.getEndpointURL(res)
Expand Down Expand Up @@ -460,6 +481,19 @@ func (r *adoptionReconciler) getOwnerAccountID(
return ackv1alpha1.AWSAccountID(r.cfg.AccountID), false
}

// getTeamID returns the team ID that owns the supplied resource.
func (r *adoptionReconciler) getTeamID(
res *ackv1alpha1.AdoptedResource,
) ackv1alpha1.TeamID {
// look for team id in the namespace annotations
namespace := res.GetNamespace()
teamID, ok := r.cache.Namespaces.GetTeamID(namespace)
if ok {
return ackv1alpha1.TeamID(teamID)
}
return ""
}

// getEndpointURL returns the AWS account that owns the supplied resource.
// We look for the namespace associated endpoint url, if that is set we use it.
// Otherwise if none of these annotations are set we use the endpoint url specified
Expand All @@ -478,18 +512,37 @@ func (r *adoptionReconciler) getEndpointURL(
return r.cfg.EndpointURL
}

// getRoleARN return the Role ARN that should be assumed in order to manage
// the resources.
func (r *adoptionReconciler) getRoleARN(
// getRoleARN return the Role ARN that should be assumed for accoutn ID
// in order to manage the resources.
func (r *adoptionReconciler) getOwnerAccountRoleARN(
acctID ackv1alpha1.AWSAccountID,
) (ackv1alpha1.AWSResourceName, error) {
roleARN, err := r.cache.Accounts.GetAccountRoleARN(string(acctID))
if err != nil {
return "", fmt.Errorf("unable to retrieve role ARN for account %s: %v", acctID, err)
roleARN, err := r.cache.CARMMaps.GetValue(ackrtcache.OwnerAccountIDPrefix + string(acctID))
if err == ackrtcache.ErrCARMConfigMapNotFound || err == ackrtcache.ErrKeyNotFound {
// CARM map v2 not defined. Check v1 map.
roleARN, err = r.cache.Accounts.GetValue(string(acctID))
if err != nil {
return "", fmt.Errorf("unable to retrieve role ARN for account %s: %v", acctID, err)
}
} else if err != nil {
return "", fmt.Errorf("unable to retrieve role ARN from CARM v2 for account %s: %v", acctID, err)
}
return ackv1alpha1.AWSResourceName(roleARN), nil
}

// getTeamRoleARN return the Role ARN that should be assumed for a team ID
// in order to manage the resources.
func (r *adoptionReconciler) getTeamRoleARN(
teamID ackv1alpha1.TeamID,
) (ackv1alpha1.AWSResourceName, error) {
roleARN, err := r.cache.CARMMaps.GetValue(ackrtcache.TeamIDPrefix + string(teamID))
if err == ackrtcache.ErrCARMConfigMapNotFound || err == ackrtcache.ErrKeyNotFound {
return "", fmt.Errorf("unable to retrieve role ARN from CARM v2 for account %s: %v", teamID, err)
}

return ackv1alpha1.AWSResourceName(roleARN), nil
}

// getRegion returns the AWS region that the given resource is in or should be
// created in. If the CR have a region associated with it, it is used. Otherwise
// we look for the namespace associated region, if that is set we use it. Finally
Expand Down
65 changes: 35 additions & 30 deletions pkg/runtime/cache/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,48 +28,53 @@ var (
// ErrCARMConfigMapNotFound is an error that is returned when the CARM
// configmap is not found.
ErrCARMConfigMapNotFound = errors.New("CARM configmap not found")
// ErrAccountIDNotFound is an error that is returned when the account ID
// ErrKeyNotFound is an error that is returned when the account ID
// is not found in the CARM configmap.
ErrAccountIDNotFound = errors.New("account ID not found in CARM configmap")
// ErrEmptyRoleARN is an error that is returned when the role ARN is empty
ErrKeyNotFound = errors.New("key not found in CARM configmap")
// ErrEmptyValue is an error that is returned when the role ARN is empty
// in the CARM configmap.
ErrEmptyRoleARN = errors.New("role ARN is empty in CARM configmap")
ErrEmptyValue = errors.New("role value is empty in CARM configmap")
)

const (
// ACKRoleAccountMap is the name of the configmap map object storing
// all the AWS Account IDs associated with their AWS Role ARNs.
ACKRoleAccountMap = "ack-role-account-map"

// ACKCARMMapV2 is the name of the v2 CARM map.
// It stores the mapping for:
// - Account ID to the AWS role ARNs.
ACKCARMMapV2 = "ack-carm-map"
)

// AccountCache is responsible for caching the CARM configmap
// CARMMap is responsible for caching the CARM configmap
// data. It is listening to all the events related to the CARM map and
// make the changes accordingly.
type AccountCache struct {
type CARMMap struct {
sync.RWMutex
log logr.Logger
roleARNs map[string]string
data map[string]string
configMapCreated bool
}

// NewAccountCache instanciate a new AccountCache.
func NewAccountCache(log logr.Logger) *AccountCache {
return &AccountCache{
// NewCARMMapCache instanciate a new CARMMap.
func NewCARMMapCache(log logr.Logger) *CARMMap {
return &CARMMap{
log: log.WithName("cache.account"),
roleARNs: make(map[string]string),
data: make(map[string]string),
configMapCreated: false,
}
}

// resourceMatchACKRoleAccountConfigMap verifies if a resource is
// resourceMatchCARMConfigMap verifies if a resource is
// the CARM configmap. It verifies the name, namespace and object type.
func resourceMatchACKRoleAccountsConfigMap(raw interface{}) bool {
func resourceMatchCARMConfigMap(raw interface{}, name string) bool {
object, ok := raw.(*corev1.ConfigMap)
return ok && object.ObjectMeta.Name == ACKRoleAccountMap
return ok && object.ObjectMeta.Name == name
}

// Run instantiate a new SharedInformer for ConfigMaps and runs it to begin processing items.
func (c *AccountCache) Run(clientSet kubernetes.Interface, stopCh <-chan struct{}) {
func (c *CARMMap) Run(name string, clientSet kubernetes.Interface, stopCh <-chan struct{}) {
c.log.V(1).Info("Starting shared informer for accounts cache", "targetConfigMap", ACKRoleAccountMap)
informer := informersv1.NewConfigMapInformer(
clientSet,
Expand All @@ -79,67 +84,67 @@ func (c *AccountCache) Run(clientSet kubernetes.Interface, stopCh <-chan struct{
)
informer.AddEventHandler(k8scache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
if resourceMatchACKRoleAccountsConfigMap(obj) {
if resourceMatchCARMConfigMap(obj, name) {
cm := obj.(*corev1.ConfigMap)
object := cm.DeepCopy()
// To avoid multiple mutex locks, we are updating the cache
// and the configmap existence flag in the same function.
configMapCreated := true
c.updateAccountRoleData(configMapCreated, object.Data)
c.updateData(configMapCreated, object.Data)
c.log.V(1).Info("created account config map", "name", cm.ObjectMeta.Name)
}
},
UpdateFunc: func(orig, desired interface{}) {
if resourceMatchACKRoleAccountsConfigMap(desired) {
if resourceMatchCARMConfigMap(desired, name) {
cm := desired.(*corev1.ConfigMap)
object := cm.DeepCopy()
//TODO(a-hilaly): compare data checksum before updating the cache
c.updateAccountRoleData(true, object.Data)
c.updateData(true, object.Data)
c.log.V(1).Info("updated account config map", "name", cm.ObjectMeta.Name)
}
},
DeleteFunc: func(obj interface{}) {
if resourceMatchACKRoleAccountsConfigMap(obj) {
if resourceMatchCARMConfigMap(obj, name) {
cm := obj.(*corev1.ConfigMap)
newMap := make(map[string]string)
// To avoid multiple mutex locks, we are updating the cache
// and the configmap existence flag in the same function.
configMapCreated := false
c.updateAccountRoleData(configMapCreated, newMap)
c.updateData(configMapCreated, newMap)
c.log.V(1).Info("deleted account config map", "name", cm.ObjectMeta.Name)
}
},
})
go informer.Run(stopCh)
}

// GetAccountRoleARN queries the AWS accountID associated Role ARN
// GetValue queries the value
// from the cached CARM configmap. It will return an error if the
// configmap is not found, the accountID is not found or the role ARN
// configmap is not found, the key is not found or the value
// is empty.
//
// This function is thread safe.
func (c *AccountCache) GetAccountRoleARN(accountID string) (string, error) {
func (c *CARMMap) GetValue(key string) (string, error) {
c.RLock()
defer c.RUnlock()

if !c.configMapCreated {
return "", ErrCARMConfigMapNotFound
}
roleARN, ok := c.roleARNs[accountID]
roleARN, ok := c.data[key]
if !ok {
return "", ErrAccountIDNotFound
return "", ErrKeyNotFound
}
if roleARN == "" {
return "", ErrEmptyRoleARN
return "", ErrEmptyValue
}
return roleARN, nil
}

// updateAccountRoleData updates the CARM map. This function is thread safe.
func (c *AccountCache) updateAccountRoleData(exist bool, data map[string]string) {
// updateData updates the CARM map. This function is thread safe.
func (c *CARMMap) updateData(exist bool, data map[string]string) {
c.Lock()
defer c.Unlock()
c.roleARNs = data
c.data = data
c.configMapCreated = exist
}
Loading