Skip to content

Commit 4a39e80

Browse files
committed
Support role annotation
1 parent 20e1c0d commit 4a39e80

File tree

10 files changed

+317
-66
lines changed

10 files changed

+317
-66
lines changed

apis/core/v1alpha1/annotations.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ const (
3535
// TODO(jaypipes): Link to documentation on cross-account resource
3636
// management
3737
AnnotationOwnerAccountID = AnnotationPrefix + "owner-account-id"
38+
// AnnotationTeamID is an annotation whose value is the identifier
39+
// for the AWS team ID to manage the resources. If this annotation
40+
// is set on a CR, the Kubernetes user is indicating that the ACK service
41+
// controller should create/patch/delete the resource in the specified AWS
42+
// role for this team ID. In order for this cross-account resource management
43+
// to succeed, the AWS IAM Role that the ACK service controller runs as needs
44+
// to have the ability to call the AWS STS::AssumeRole API call and assume an
45+
// IAM Role in the target AWS Account.
46+
AnnotationTeamID = AnnotationPrefix + "team-id"
3847
// AnnotationRegion is an annotation whose value is the identifier for the
3948
// the AWS region in which the resources should be created. If this annotation
4049
// is set on a CR metadata, that means the user is indicating to the ACK service

apis/core/v1alpha1/common.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ type AWSRegion string
1919
// AWSAccountID represents an AWS account identifier
2020
type AWSAccountID string
2121

22+
// TeamID represents a team ID identifier.
23+
type TeamID string
24+
2225
// AWSResourceName represents an AWS Resource Name (ARN)
2326
type AWSResourceName string
2427

mocks/pkg/types/aws_resource_identifiers.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/runtime/adoption_reconciler.go

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"context"
1818
"fmt"
1919

20+
"github.com/aws/aws-sdk-go/aws/arn"
2021
"github.com/go-logr/logr"
2122
"github.com/pkg/errors"
2223
corev1 "k8s.io/api/core/v1"
@@ -108,27 +109,49 @@ func (r *adoptionReconciler) reconcile(ctx context.Context, req ctrlrt.Request)
108109
return ackerr.NotAdoptable
109110
}
110111

112+
// If a user specified a namespace with role ARN annotation,
113+
// we need to get the role and set the accout ID to that role.
114+
teamID := r.getTeamID(res)
115+
111116
// If a user has specified a namespace that is annotated with the
112117
// an owner account ID, we need an appropriate role ARN to assume
113118
// in order to perform the reconciliation. The roles ARN are typically
114119
// stored in a ConfigMap in the ACK system namespace.
115120
// If the ConfigMap is not created, or not populated with an
116121
// accountID to roleARN mapping, we need to properly requeue with a
117122
// helpful message to the user.
118-
var roleARN ackv1alpha1.AWSResourceName
119123
acctID, needCARMLookup := r.getOwnerAccountID(res)
124+
125+
var CARMLookupKey string
126+
if teamID != "" {
127+
CARMLookupKey = string(teamID)
128+
needCARMLookup = true
129+
} else {
130+
CARMLookupKey = string(acctID)
131+
}
132+
133+
var roleARN ackv1alpha1.AWSResourceName
120134
if needCARMLookup {
121135
// This means that the user is specifying a namespace that is
122-
// annotated with an owner account ID. We need to retrieve the
136+
// annotated with an owner account ID or team ID. We need to retrieve the
123137
// roleARN from the ConfigMap and properly requeue if the roleARN
124138
// is not available.
125-
roleARN, err = r.getRoleARN(acctID)
139+
roleARN, err = r.getRoleARN(CARMLookupKey)
126140
if err != nil {
127141
ackrtlog.InfoAdoptedResource(r.log, res, fmt.Sprintf("Unable to start adoption reconcilliation %s: %v", acctID, err))
128142
// r.getRoleARN errors are not terminal, we should requeue.
129143
return requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
130144
}
131145
}
146+
147+
if teamID != "" {
148+
parsedARN, err := arn.Parse(string(roleARN))
149+
if err != nil {
150+
return fmt.Errorf("failed to parsed role ARN %q from namespace annotation: %v", teamID, err)
151+
}
152+
acctID = ackv1alpha1.AWSAccountID(parsedARN.AccountID)
153+
}
154+
132155
region := r.getRegion(res)
133156
targetDescriptor := rmf.ResourceDescriptor()
134157
endpointURL := r.getEndpointURL(res)
@@ -460,6 +483,20 @@ func (r *adoptionReconciler) getOwnerAccountID(
460483
return ackv1alpha1.AWSAccountID(r.cfg.AccountID), false
461484
}
462485

486+
// getTeamID gets the team-id from the namespace
487+
// annotation.
488+
func (r *adoptionReconciler) getTeamID(
489+
res *ackv1alpha1.AdoptedResource,
490+
) ackv1alpha1.TeamID {
491+
// look for team-id in the namespace annotations
492+
namespace := res.GetNamespace()
493+
teamID, ok := r.cache.Namespaces.GetTeamID(namespace)
494+
if ok {
495+
return ackv1alpha1.TeamID(teamID)
496+
}
497+
return ackv1alpha1.TeamID("")
498+
}
499+
463500
// getEndpointURL returns the AWS account that owns the supplied resource.
464501
// We look for the namespace associated endpoint url, if that is set we use it.
465502
// Otherwise if none of these annotations are set we use the endpoint url specified
@@ -481,11 +518,11 @@ func (r *adoptionReconciler) getEndpointURL(
481518
// getRoleARN return the Role ARN that should be assumed in order to manage
482519
// the resources.
483520
func (r *adoptionReconciler) getRoleARN(
484-
acctID ackv1alpha1.AWSAccountID,
521+
key string,
485522
) (ackv1alpha1.AWSResourceName, error) {
486-
roleARN, err := r.cache.Accounts.GetAccountRoleARN(string(acctID))
523+
roleARN, err := r.cache.Accounts.GetRoleARN(key)
487524
if err != nil {
488-
return "", fmt.Errorf("unable to retrieve role ARN for account %s: %v", acctID, err)
525+
return "", fmt.Errorf("unable to retrieve role ARN for annotation %q: %v", key, err)
489526
}
490527
return ackv1alpha1.AWSResourceName(roleARN), nil
491528
}

pkg/runtime/cache/account.go

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package cache
1515

1616
import (
1717
"errors"
18+
"fmt"
1819
"sync"
1920

2021
"github.com/go-logr/logr"
@@ -28,49 +29,57 @@ var (
2829
// ErrCARMConfigMapNotFound is an error that is returned when the CARM
2930
// configmap is not found.
3031
ErrCARMConfigMapNotFound = errors.New("CARM configmap not found")
31-
// ErrAccountIDNotFound is an error that is returned when the account ID
32+
// ErrKeyNotFound is an error that is returned when the key
3233
// is not found in the CARM configmap.
33-
ErrAccountIDNotFound = errors.New("account ID not found in CARM configmap")
34+
ErrKeyNotFound = errors.New("key not found in CARM configmap")
3435
// ErrEmptyRoleARN is an error that is returned when the role ARN is empty
3536
// in the CARM configmap.
3637
ErrEmptyRoleARN = errors.New("role ARN is empty in CARM configmap")
3738
)
3839

40+
type CARMName string
41+
3942
const (
4043
// ACKRoleAccountMap is the name of the configmap map object storing
4144
// all the AWS Account IDs associated with their AWS Role ARNs.
42-
ACKRoleAccountMap = "ack-role-account-map"
45+
ACKRoleAccountMap CARMName = "ack-role-account-map"
46+
47+
// ACKRoleTeamMap is the name of the configmap map object storing
48+
// all the AWS Team IDs associated with their AWS Role ARNs.
49+
ACKRoleTeamMap CARMName = "ack-role-team-map"
4350
)
4451

45-
// AccountCache is responsible for caching the CARM configmap
52+
// CARMCache is responsible for caching the CARM configmap
4653
// data. It is listening to all the events related to the CARM map and
4754
// make the changes accordingly.
48-
type AccountCache struct {
55+
type CARMCache struct {
4956
sync.RWMutex
57+
name CARMName
5058
log logr.Logger
5159
roleARNs map[string]string
5260
configMapCreated bool
5361
}
5462

55-
// NewAccountCache instanciate a new AccountCache.
56-
func NewAccountCache(log logr.Logger) *AccountCache {
57-
return &AccountCache{
58-
log: log.WithName("cache.account"),
63+
// NewCARMCache instanciate a new CARMCache.
64+
func NewCARMCache(name CARMName, log logr.Logger) *CARMCache {
65+
return &CARMCache{
66+
log: log.WithName(fmt.Sprintf("cache.%s", name)),
67+
name: name,
5968
roleARNs: make(map[string]string),
6069
configMapCreated: false,
6170
}
6271
}
6372

64-
// resourceMatchACKRoleAccountConfigMap verifies if a resource is
73+
// resourceMatchACKRoleConfigMap verifies if a resource is
6574
// the CARM configmap. It verifies the name, namespace and object type.
66-
func resourceMatchACKRoleAccountsConfigMap(raw interface{}) bool {
75+
func resourceMatchACKRoleConfigMap(name CARMName, raw interface{}) bool {
6776
object, ok := raw.(*corev1.ConfigMap)
68-
return ok && object.ObjectMeta.Name == ACKRoleAccountMap
77+
return ok && object.ObjectMeta.Name == string(name)
6978
}
7079

7180
// Run instantiate a new SharedInformer for ConfigMaps and runs it to begin processing items.
72-
func (c *AccountCache) Run(clientSet kubernetes.Interface, stopCh <-chan struct{}) {
73-
c.log.V(1).Info("Starting shared informer for accounts cache", "targetConfigMap", ACKRoleAccountMap)
81+
func (c *CARMCache) Run(clientSet kubernetes.Interface, stopCh <-chan struct{}) {
82+
c.log.V(1).Info("Starting shared informer for CARM cache", "targetConfigMap", c.name)
7483
informer := informersv1.NewConfigMapInformer(
7584
clientSet,
7685
ackSystemNamespace,
@@ -79,65 +88,65 @@ func (c *AccountCache) Run(clientSet kubernetes.Interface, stopCh <-chan struct{
7988
)
8089
informer.AddEventHandler(k8scache.ResourceEventHandlerFuncs{
8190
AddFunc: func(obj interface{}) {
82-
if resourceMatchACKRoleAccountsConfigMap(obj) {
91+
if resourceMatchACKRoleConfigMap(c.name, obj) {
8392
cm := obj.(*corev1.ConfigMap)
8493
object := cm.DeepCopy()
8594
// To avoid multiple mutex locks, we are updating the cache
8695
// and the configmap existence flag in the same function.
8796
configMapCreated := true
88-
c.updateAccountRoleData(configMapCreated, object.Data)
89-
c.log.V(1).Info("created account config map", "name", cm.ObjectMeta.Name)
97+
c.updateRoleData(configMapCreated, object.Data)
98+
c.log.V(1).Info("created config map", "name", cm.ObjectMeta.Name)
9099
}
91100
},
92101
UpdateFunc: func(orig, desired interface{}) {
93-
if resourceMatchACKRoleAccountsConfigMap(desired) {
102+
if resourceMatchACKRoleConfigMap(c.name, desired) {
94103
cm := desired.(*corev1.ConfigMap)
95104
object := cm.DeepCopy()
96105
//TODO(a-hilaly): compare data checksum before updating the cache
97-
c.updateAccountRoleData(true, object.Data)
98-
c.log.V(1).Info("updated account config map", "name", cm.ObjectMeta.Name)
106+
c.updateRoleData(true, object.Data)
107+
c.log.V(1).Info("updated config map", "name", cm.ObjectMeta.Name)
99108
}
100109
},
101110
DeleteFunc: func(obj interface{}) {
102-
if resourceMatchACKRoleAccountsConfigMap(obj) {
111+
if resourceMatchACKRoleConfigMap(c.name, obj) {
103112
cm := obj.(*corev1.ConfigMap)
104113
newMap := make(map[string]string)
105114
// To avoid multiple mutex locks, we are updating the cache
106115
// and the configmap existence flag in the same function.
107116
configMapCreated := false
108-
c.updateAccountRoleData(configMapCreated, newMap)
109-
c.log.V(1).Info("deleted account config map", "name", cm.ObjectMeta.Name)
117+
c.updateRoleData(configMapCreated, newMap)
118+
c.log.V(1).Info("deleted config map", "name", cm.ObjectMeta.Name)
110119
}
111120
},
112121
})
113122
go informer.Run(stopCh)
114123
}
115124

116-
// GetAccountRoleARN queries the AWS accountID associated Role ARN
125+
// GetRoleARN queries the associated Role ARN
117126
// from the cached CARM configmap. It will return an error if the
118-
// configmap is not found, the accountID is not found or the role ARN
127+
// configmap is not found, the key is not found or the role ARN
119128
// is empty.
120129
//
121130
// This function is thread safe.
122-
func (c *AccountCache) GetAccountRoleARN(accountID string) (string, error) {
131+
func (c *CARMCache) GetRoleARN(key string) (string, error) {
123132
c.RLock()
124133
defer c.RUnlock()
125134

126135
if !c.configMapCreated {
127136
return "", ErrCARMConfigMapNotFound
128137
}
129-
roleARN, ok := c.roleARNs[accountID]
138+
roleARN, ok := c.roleARNs[key]
130139
if !ok {
131-
return "", ErrAccountIDNotFound
140+
return "", ErrKeyNotFound
132141
}
133142
if roleARN == "" {
134143
return "", ErrEmptyRoleARN
135144
}
136145
return roleARN, nil
137146
}
138147

139-
// updateAccountRoleData updates the CARM map. This function is thread safe.
140-
func (c *AccountCache) updateAccountRoleData(exist bool, data map[string]string) {
148+
// updateRoleData updates the CARM map. This function is thread safe.
149+
func (c *CARMCache) updateRoleData(exist bool, data map[string]string) {
141150
c.Lock()
142151
defer c.Unlock()
143152
c.roleARNs = data

0 commit comments

Comments
 (0)