Skip to content

Commit

Permalink
Support role annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
zicongmei committed Mar 11, 2024
1 parent 20e1c0d commit f7a0fad
Show file tree
Hide file tree
Showing 11 changed files with 320 additions and 66 deletions.
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
16 changes: 16 additions & 0 deletions mocks/pkg/types/aws_resource_identifiers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 43 additions & 6 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,49 @@ 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)

var CARMLookupKey string
if teamID != "" {
CARMLookupKey = string(teamID)
needCARMLookup = true
} else {
CARMLookupKey = string(acctID)
}

var roleARN ackv1alpha1.AWSResourceName
if needCARMLookup {
// This means that the user is specifying a namespace that is
// annotated with an owner account ID. We need to retrieve the
// 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.getRoleARN(acctID)
roleARN, err = r.getRoleARN(CARMLookupKey)
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)
}
}

if teamID != "" {
parsedARN, err := arn.Parse(string(roleARN))
if err != nil {
return fmt.Errorf("failed to parsed role ARN %q from namespace annotation: %v", teamID, err)
}
acctID = ackv1alpha1.AWSAccountID(parsedARN.AccountID)
}

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

// getTeamID gets the team-id from the namespace
// annotation.
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 ackv1alpha1.TeamID("")
}

// 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 @@ -481,11 +518,11 @@ func (r *adoptionReconciler) getEndpointURL(
// getRoleARN return the Role ARN that should be assumed in order to manage
// the resources.
func (r *adoptionReconciler) getRoleARN(
acctID ackv1alpha1.AWSAccountID,
key string,
) (ackv1alpha1.AWSResourceName, error) {
roleARN, err := r.cache.Accounts.GetAccountRoleARN(string(acctID))
roleARN, err := r.cache.Accounts.GetRoleARN(key)
if err != nil {
return "", fmt.Errorf("unable to retrieve role ARN for account %s: %v", acctID, err)
return "", fmt.Errorf("unable to retrieve role ARN for annotation %q: %v", key, err)
}
return ackv1alpha1.AWSResourceName(roleARN), nil
}
Expand Down
69 changes: 39 additions & 30 deletions pkg/runtime/cache/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package cache

import (
"errors"
"fmt"
"sync"

"github.com/go-logr/logr"
Expand All @@ -28,49 +29,57 @@ 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 key
// is not found in the CARM configmap.
ErrAccountIDNotFound = errors.New("account ID not found in CARM configmap")
ErrKeyNotFound = errors.New("key not found in CARM configmap")
// ErrEmptyRoleARN 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")
)

type CARMName string

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"
ACKRoleAccountMap CARMName = "ack-role-account-map"

// ACKRoleTeamMap is the name of the configmap map object storing
// all the AWS Team IDs associated with their AWS Role ARNs.
ACKRoleTeamMap CARMName = "ack-role-team-map"
)

// AccountCache is responsible for caching the CARM configmap
// CARMCache 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 CARMCache struct {
sync.RWMutex
name CARMName
log logr.Logger
roleARNs map[string]string
configMapCreated bool
}

// NewAccountCache instanciate a new AccountCache.
func NewAccountCache(log logr.Logger) *AccountCache {
return &AccountCache{
log: log.WithName("cache.account"),
// NewCARMCache instanciate a new CARMCache.
func NewCARMCache(name CARMName, log logr.Logger) *CARMCache {
return &CARMCache{
log: log.WithName(fmt.Sprintf("cache.%s", name)),
name: name,
roleARNs: make(map[string]string),
configMapCreated: false,
}
}

// resourceMatchACKRoleAccountConfigMap verifies if a resource is
// resourceMatchACKRoleConfigMap verifies if a resource is
// the CARM configmap. It verifies the name, namespace and object type.
func resourceMatchACKRoleAccountsConfigMap(raw interface{}) bool {
func resourceMatchACKRoleConfigMap(name CARMName, raw interface{}) bool {
object, ok := raw.(*corev1.ConfigMap)
return ok && object.ObjectMeta.Name == ACKRoleAccountMap
return ok && object.ObjectMeta.Name == string(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{}) {
c.log.V(1).Info("Starting shared informer for accounts cache", "targetConfigMap", ACKRoleAccountMap)
func (c *CARMCache) Run(clientSet kubernetes.Interface, stopCh <-chan struct{}) {
c.log.V(1).Info("Starting shared informer for CARM cache", "targetConfigMap", c.name)
informer := informersv1.NewConfigMapInformer(
clientSet,
ackSystemNamespace,
Expand All @@ -79,65 +88,65 @@ func (c *AccountCache) Run(clientSet kubernetes.Interface, stopCh <-chan struct{
)
informer.AddEventHandler(k8scache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
if resourceMatchACKRoleAccountsConfigMap(obj) {
if resourceMatchACKRoleConfigMap(c.name, obj) {
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.log.V(1).Info("created account config map", "name", cm.ObjectMeta.Name)
c.updateRoleData(configMapCreated, object.Data)
c.log.V(1).Info("created config map", "name", cm.ObjectMeta.Name)
}
},
UpdateFunc: func(orig, desired interface{}) {
if resourceMatchACKRoleAccountsConfigMap(desired) {
if resourceMatchACKRoleConfigMap(c.name, desired) {
cm := desired.(*corev1.ConfigMap)
object := cm.DeepCopy()
//TODO(a-hilaly): compare data checksum before updating the cache
c.updateAccountRoleData(true, object.Data)
c.log.V(1).Info("updated account config map", "name", cm.ObjectMeta.Name)
c.updateRoleData(true, object.Data)
c.log.V(1).Info("updated config map", "name", cm.ObjectMeta.Name)
}
},
DeleteFunc: func(obj interface{}) {
if resourceMatchACKRoleAccountsConfigMap(obj) {
if resourceMatchACKRoleConfigMap(c.name, obj) {
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.log.V(1).Info("deleted account config map", "name", cm.ObjectMeta.Name)
c.updateRoleData(configMapCreated, newMap)
c.log.V(1).Info("deleted config map", "name", cm.ObjectMeta.Name)
}
},
})
go informer.Run(stopCh)
}

// GetAccountRoleARN queries the AWS accountID associated Role ARN
// GetRoleARN queries the associated Role ARN
// 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 role ARN
// is empty.
//
// This function is thread safe.
func (c *AccountCache) GetAccountRoleARN(accountID string) (string, error) {
func (c *CARMCache) GetRoleARN(key string) (string, error) {
c.RLock()
defer c.RUnlock()

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

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

0 comments on commit f7a0fad

Please sign in to comment.