Skip to content

Commit a1359c9

Browse files
authored
Merge pull request #155 from TiberiuGC/feature/CARMv2
Introduce a new CARM config map with support for `teamIDs` and service level isolation
2 parents dda2022 + 2d1b192 commit a1359c9

File tree

10 files changed

+349
-88
lines changed

10 files changed

+349
-88
lines changed

apis/core/v1alpha1/annotations.go

+9
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

+3
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

pkg/featuregate/features.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616
// optionally overridden.
1717
package featuregate
1818

19+
const (
20+
// CARMv2 is the name of the CARMv2 feature.
21+
CARMv2 = "CARMv2"
22+
)
23+
1924
// defaultACKFeatureGates is a map of feature names to Feature structs
2025
// representing the default feature gates for ACK controllers.
2126
var defaultACKFeatureGates = FeatureGates{
2227
// Set feature gates here
23-
// "feature1": {Stage: Alpha, Enabled: false},
28+
CARMv2: {Stage: Alpha, Enabled: false},
2429
}
2530

2631
// FeatureStage represents the development stage of a feature.

pkg/runtime/adoption_reconciler.go

+67-14
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"
@@ -32,6 +33,7 @@ import (
3233
ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1"
3334
ackcfg "github.com/aws-controllers-k8s/runtime/pkg/config"
3435
ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors"
36+
"github.com/aws-controllers-k8s/runtime/pkg/featuregate"
3537
ackmetrics "github.com/aws-controllers-k8s/runtime/pkg/metrics"
3638
"github.com/aws-controllers-k8s/runtime/pkg/requeue"
3739
ackrtcache "github.com/aws-controllers-k8s/runtime/pkg/runtime/cache"
@@ -115,20 +117,44 @@ func (r *adoptionReconciler) reconcile(ctx context.Context, req ctrlrt.Request)
115117
// If the ConfigMap is not created, or not populated with an
116118
// accountID to roleARN mapping, we need to properly requeue with a
117119
// helpful message to the user.
118-
var roleARN ackv1alpha1.AWSResourceName
119120
acctID, needCARMLookup := r.getOwnerAccountID(res)
120-
if needCARMLookup {
121-
// This means that the user is specifying a namespace that is
122-
// annotated with an owner account ID. We need to retrieve the
123-
// roleARN from the ConfigMap and properly requeue if the roleARN
124-
// is not available.
121+
122+
var roleARN ackv1alpha1.AWSResourceName
123+
if r.cfg.FeatureGates.IsEnabled(featuregate.CARMv2) {
124+
teamID := r.getTeamID(res)
125+
if teamID != "" {
126+
// The user is specifying a namespace that is annotated with a team ID.
127+
// Requeue if the corresponding roleARN is not available in the CARMv2 configmap.
128+
// Additionally, set the account ID to the role's account ID.
129+
roleARN, err = r.getRoleARNv2(string(teamID))
130+
if err != nil {
131+
ackrtlog.InfoAdoptedResource(r.log, res, fmt.Sprintf("Unable to start adoption reconcilliation %s: %v", acctID, err))
132+
return requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
133+
}
134+
parsedARN, err := arn.Parse(string(roleARN))
135+
if err != nil {
136+
return fmt.Errorf("parsing role ARN %q from namespace annotation: %v", roleARN, err)
137+
}
138+
acctID = ackv1alpha1.AWSAccountID(parsedARN.AccountID)
139+
} else if needCARMLookup {
140+
// The user is specifying a namespace that is annotated with an owner account ID.
141+
// Requeue if the corresponding roleARN is not available in the CARMv2 configmap.
142+
roleARN, err = r.getRoleARNv2(string(acctID))
143+
if err != nil {
144+
ackrtlog.InfoAdoptedResource(r.log, res, fmt.Sprintf("Unable to start adoption reconcilliation %s: %v", acctID, err))
145+
return requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
146+
}
147+
}
148+
} else if needCARMLookup {
149+
// The user is specifying a namespace that is annotated with an owner account ID.
150+
// Requeue if the corresponding roleARN is not available in the Accounts (CARMv1) configmap.
125151
roleARN, err = r.getRoleARN(acctID)
126152
if err != nil {
127153
ackrtlog.InfoAdoptedResource(r.log, res, fmt.Sprintf("Unable to start adoption reconcilliation %s: %v", acctID, err))
128-
// r.getRoleARN errors are not terminal, we should requeue.
129154
return requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
130155
}
131156
}
157+
132158
region := r.getRegion(res)
133159
targetDescriptor := rmf.ResourceDescriptor()
134160
endpointURL := r.getEndpointURL(res)
@@ -460,6 +486,19 @@ func (r *adoptionReconciler) getOwnerAccountID(
460486
return ackv1alpha1.AWSAccountID(r.cfg.AccountID), false
461487
}
462488

489+
// getTeamID returns the team ID that owns the supplied resource.
490+
func (r *adoptionReconciler) getTeamID(
491+
res *ackv1alpha1.AdoptedResource,
492+
) ackv1alpha1.TeamID {
493+
// look for team id in the namespace annotations
494+
namespace := res.GetNamespace()
495+
teamID, ok := r.cache.Namespaces.GetTeamID(namespace)
496+
if ok {
497+
return ackv1alpha1.TeamID(teamID)
498+
}
499+
return ""
500+
}
501+
463502
// getEndpointURL returns the AWS account that owns the supplied resource.
464503
// We look for the namespace associated endpoint url, if that is set we use it.
465504
// Otherwise if none of these annotations are set we use the endpoint url specified
@@ -478,14 +517,28 @@ func (r *adoptionReconciler) getEndpointURL(
478517
return r.cfg.EndpointURL
479518
}
480519

481-
// getRoleARN return the Role ARN that should be assumed in order to manage
482-
// the resources.
483-
func (r *adoptionReconciler) getRoleARN(
484-
acctID ackv1alpha1.AWSAccountID,
485-
) (ackv1alpha1.AWSResourceName, error) {
486-
roleARN, err := r.cache.Accounts.GetAccountRoleARN(string(acctID))
520+
// getRoleARNv2 returns the Role ARN that should be assumed for the given account/team ID,
521+
// from the CARMv2 configmap, in order to manage the resources.
522+
func (r *adoptionReconciler) getRoleARNv2(id string) (ackv1alpha1.AWSResourceName, error) {
523+
// use service level roleARN if present
524+
serviceID := r.sc.GetMetadata().ServiceAlias + "." + id
525+
if roleARN, err := r.cache.CARMMaps.GetValue(serviceID); err == nil {
526+
return ackv1alpha1.AWSResourceName(roleARN), nil
527+
}
528+
// otherwise use account/team level roleARN
529+
roleARN, err := r.cache.CARMMaps.GetValue(id)
530+
if err != nil {
531+
return "", fmt.Errorf("retrieving role ARN for account/team ID %q from %q configmap: %v", id, ackrtcache.ACKCARMMapV2, err)
532+
}
533+
return ackv1alpha1.AWSResourceName(roleARN), nil
534+
}
535+
536+
// getRoleARN returns the Role ARN that should be assumed for the given account ID,
537+
// from the CARMv1 configmap, in order to manage the resources.
538+
func (r *adoptionReconciler) getRoleARN(acctID ackv1alpha1.AWSAccountID) (ackv1alpha1.AWSResourceName, error) {
539+
roleARN, err := r.cache.Accounts.GetValue(string(acctID))
487540
if err != nil {
488-
return "", fmt.Errorf("unable to retrieve role ARN for account %s: %v", acctID, err)
541+
return "", fmt.Errorf("retrieving role ARN for account ID %q from %q configMap: %v", acctID, ackrtcache.ACKRoleAccountMap, err)
489542
}
490543
return ackv1alpha1.AWSResourceName(roleARN), nil
491544
}

pkg/runtime/cache/account.go

+36-31
Original file line numberDiff line numberDiff line change
@@ -28,49 +28,54 @@ var (
2828
// ErrCARMConfigMapNotFound is an error that is returned when the CARM
2929
// configmap is not found.
3030
ErrCARMConfigMapNotFound = errors.New("CARM configmap not found")
31-
// ErrAccountIDNotFound is an error that is returned when the account ID
31+
// ErrKeyNotFound is an error that is returned when the account ID
3232
// is not found in the CARM configmap.
33-
ErrAccountIDNotFound = errors.New("account ID not found in CARM configmap")
34-
// ErrEmptyRoleARN is an error that is returned when the role ARN is empty
33+
ErrKeyNotFound = errors.New("key not found in CARM configmap")
34+
// ErrEmptyValue is an error that is returned when the role ARN is empty
3535
// in the CARM configmap.
36-
ErrEmptyRoleARN = errors.New("role ARN is empty in CARM configmap")
36+
ErrEmptyValue = errors.New("role value is empty in CARM configmap")
3737
)
3838

3939
const (
4040
// ACKRoleAccountMap is the name of the configmap map object storing
4141
// all the AWS Account IDs associated with their AWS Role ARNs.
4242
ACKRoleAccountMap = "ack-role-account-map"
43+
44+
// ACKCARMMapV2 is the name of the v2 CARM map.
45+
// It stores the mapping for:
46+
// - Account ID to the AWS role ARNs.
47+
ACKCARMMapV2 = "ack-carm-map"
4348
)
4449

45-
// AccountCache is responsible for caching the CARM configmap
50+
// CARMMap is responsible for caching the CARM configmap
4651
// data. It is listening to all the events related to the CARM map and
4752
// make the changes accordingly.
48-
type AccountCache struct {
53+
type CARMMap struct {
4954
sync.RWMutex
5055
log logr.Logger
51-
roleARNs map[string]string
56+
data map[string]string
5257
configMapCreated bool
5358
hasSynced func() bool
5459
}
5560

56-
// NewAccountCache instanciate a new AccountCache.
57-
func NewAccountCache(log logr.Logger) *AccountCache {
58-
return &AccountCache{
59-
log: log.WithName("cache.account"),
60-
roleARNs: make(map[string]string),
61+
// NewCARMMapCache instanciate a new CARMMap.
62+
func NewCARMMapCache(log logr.Logger) *CARMMap {
63+
return &CARMMap{
64+
log: log.WithName("cache.carm"),
65+
data: make(map[string]string),
6166
configMapCreated: false,
6267
}
6368
}
6469

65-
// resourceMatchACKRoleAccountConfigMap verifies if a resource is
70+
// resourceMatchCARMConfigMap verifies if a resource is
6671
// the CARM configmap. It verifies the name, namespace and object type.
67-
func resourceMatchACKRoleAccountsConfigMap(raw interface{}) bool {
72+
func resourceMatchCARMConfigMap(raw interface{}, name string) bool {
6873
object, ok := raw.(*corev1.ConfigMap)
69-
return ok && object.ObjectMeta.Name == ACKRoleAccountMap
74+
return ok && object.ObjectMeta.Name == name
7075
}
7176

7277
// Run instantiate a new SharedInformer for ConfigMaps and runs it to begin processing items.
73-
func (c *AccountCache) Run(clientSet kubernetes.Interface, stopCh <-chan struct{}) {
78+
func (c *CARMMap) Run(name string, clientSet kubernetes.Interface, stopCh <-chan struct{}) {
7479
c.log.V(1).Info("Starting shared informer for accounts cache", "targetConfigMap", ACKRoleAccountMap)
7580
informer := informersv1.NewConfigMapInformer(
7681
clientSet,
@@ -80,33 +85,33 @@ func (c *AccountCache) Run(clientSet kubernetes.Interface, stopCh <-chan struct{
8085
)
8186
informer.AddEventHandler(k8scache.ResourceEventHandlerFuncs{
8287
AddFunc: func(obj interface{}) {
83-
if resourceMatchACKRoleAccountsConfigMap(obj) {
88+
if resourceMatchCARMConfigMap(obj, name) {
8489
cm := obj.(*corev1.ConfigMap)
8590
object := cm.DeepCopy()
8691
// To avoid multiple mutex locks, we are updating the cache
8792
// and the configmap existence flag in the same function.
8893
configMapCreated := true
89-
c.updateAccountRoleData(configMapCreated, object.Data)
94+
c.updateData(configMapCreated, object.Data)
9095
c.log.V(1).Info("created account config map", "name", cm.ObjectMeta.Name)
9196
}
9297
},
9398
UpdateFunc: func(orig, desired interface{}) {
94-
if resourceMatchACKRoleAccountsConfigMap(desired) {
99+
if resourceMatchCARMConfigMap(desired, name) {
95100
cm := desired.(*corev1.ConfigMap)
96101
object := cm.DeepCopy()
97102
//TODO(a-hilaly): compare data checksum before updating the cache
98-
c.updateAccountRoleData(true, object.Data)
103+
c.updateData(true, object.Data)
99104
c.log.V(1).Info("updated account config map", "name", cm.ObjectMeta.Name)
100105
}
101106
},
102107
DeleteFunc: func(obj interface{}) {
103-
if resourceMatchACKRoleAccountsConfigMap(obj) {
108+
if resourceMatchCARMConfigMap(obj, name) {
104109
cm := obj.(*corev1.ConfigMap)
105110
newMap := make(map[string]string)
106111
// To avoid multiple mutex locks, we are updating the cache
107112
// and the configmap existence flag in the same function.
108113
configMapCreated := false
109-
c.updateAccountRoleData(configMapCreated, newMap)
114+
c.updateData(configMapCreated, newMap)
110115
c.log.V(1).Info("deleted account config map", "name", cm.ObjectMeta.Name)
111116
}
112117
},
@@ -115,33 +120,33 @@ func (c *AccountCache) Run(clientSet kubernetes.Interface, stopCh <-chan struct{
115120
c.hasSynced = informer.HasSynced
116121
}
117122

118-
// GetAccountRoleARN queries the AWS accountID associated Role ARN
123+
// GetValue queries the value
119124
// from the cached CARM configmap. It will return an error if the
120-
// configmap is not found, the accountID is not found or the role ARN
125+
// configmap is not found, the key is not found or the value
121126
// is empty.
122127
//
123128
// This function is thread safe.
124-
func (c *AccountCache) GetAccountRoleARN(accountID string) (string, error) {
129+
func (c *CARMMap) GetValue(key string) (string, error) {
125130
c.RLock()
126131
defer c.RUnlock()
127132

128133
if !c.configMapCreated {
129134
return "", ErrCARMConfigMapNotFound
130135
}
131-
roleARN, ok := c.roleARNs[accountID]
136+
roleARN, ok := c.data[key]
132137
if !ok {
133-
return "", ErrAccountIDNotFound
138+
return "", ErrKeyNotFound
134139
}
135140
if roleARN == "" {
136-
return "", ErrEmptyRoleARN
141+
return "", ErrEmptyValue
137142
}
138143
return roleARN, nil
139144
}
140145

141-
// updateAccountRoleData updates the CARM map. This function is thread safe.
142-
func (c *AccountCache) updateAccountRoleData(exist bool, data map[string]string) {
146+
// updateData updates the CARM map. This function is thread safe.
147+
func (c *CARMMap) updateData(exist bool, data map[string]string) {
143148
c.Lock()
144149
defer c.Unlock()
145-
c.roleARNs = data
150+
c.data = data
146151
c.configMapCreated = exist
147152
}

0 commit comments

Comments
 (0)