Skip to content

Commit 96a54b3

Browse files
zicongmeiTiberiuGC
authored andcommitted
Support role annotation
1 parent 2dd9ebb commit 96a54b3

File tree

7 files changed

+258
-25
lines changed

7 files changed

+258
-25
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/runtime/adoption_reconciler.go

+56-9
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,47 @@ 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)
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.
125-
roleARN, err = r.getOwnerAccountRoleARN(acctID)
124+
125+
var roleARN ackv1alpha1.AWSResourceName
126+
if teamID != "" {
127+
roleARN, err = r.getTeamRoleARN(teamID)
126128
if err != nil {
127129
ackrtlog.InfoAdoptedResource(r.log, res, fmt.Sprintf("Unable to start adoption reconcilliation %s: %v", acctID, err))
128130
// r.getRoleARN errors are not terminal, we should requeue.
129131
return requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
130132
}
133+
parsedARN, err := arn.Parse(string(roleARN))
134+
if err != nil {
135+
return fmt.Errorf("failed to parsed role ARN %q from namespace annotation: %v", roleARN, err)
136+
}
137+
acctID = ackv1alpha1.AWSAccountID(parsedARN.AccountID)
138+
} else {
139+
if needCARMLookup {
140+
// This means that the user is specifying a namespace that is
141+
// annotated with an owner account ID or team ID. We need to retrieve the
142+
// roleARN from the ConfigMap and properly requeue if the roleARN
143+
// is not available.
144+
roleARN, err = r.getOwnerAccountRoleARN(acctID)
145+
if err != nil {
146+
ackrtlog.InfoAdoptedResource(r.log, res, fmt.Sprintf("Unable to start adoption reconcilliation %s: %v", acctID, err))
147+
// r.getRoleARN errors are not terminal, we should requeue.
148+
return requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
149+
}
150+
}
131151
}
152+
132153
region := r.getRegion(res)
133154
targetDescriptor := rmf.ResourceDescriptor()
134155
endpointURL := r.getEndpointURL(res)
@@ -460,6 +481,19 @@ func (r *adoptionReconciler) getOwnerAccountID(
460481
return ackv1alpha1.AWSAccountID(r.cfg.AccountID), false
461482
}
462483

484+
// getTeamID returns the team ID that owns the supplied resource.
485+
func (r *adoptionReconciler) getTeamID(
486+
res *ackv1alpha1.AdoptedResource,
487+
) ackv1alpha1.TeamID {
488+
// look for team id in the namespace annotations
489+
namespace := res.GetNamespace()
490+
teamID, ok := r.cache.Namespaces.GetTeamID(namespace)
491+
if ok {
492+
return ackv1alpha1.TeamID(teamID)
493+
}
494+
return ""
495+
}
496+
463497
// getEndpointURL returns the AWS account that owns the supplied resource.
464498
// We look for the namespace associated endpoint url, if that is set we use it.
465499
// Otherwise if none of these annotations are set we use the endpoint url specified
@@ -478,8 +512,8 @@ func (r *adoptionReconciler) getEndpointURL(
478512
return r.cfg.EndpointURL
479513
}
480514

481-
// getOwnerAccountRoleARN return the Role ARN that should be assumed in order to manage
482-
// the resources.
515+
// getRoleARN return the Role ARN that should be assumed for accoutn ID
516+
// in order to manage the resources.
483517
func (r *adoptionReconciler) getOwnerAccountRoleARN(
484518
acctID ackv1alpha1.AWSAccountID,
485519
) (ackv1alpha1.AWSResourceName, error) {
@@ -496,6 +530,19 @@ func (r *adoptionReconciler) getOwnerAccountRoleARN(
496530
return ackv1alpha1.AWSResourceName(roleARN), nil
497531
}
498532

533+
// getTeamRoleARN return the Role ARN that should be assumed for a team ID
534+
// in order to manage the resources.
535+
func (r *adoptionReconciler) getTeamRoleARN(
536+
teamID ackv1alpha1.TeamID,
537+
) (ackv1alpha1.AWSResourceName, error) {
538+
roleARN, err := r.cache.CARMMaps.GetValue(ackrtcache.TeamIDPrefix + string(teamID))
539+
if err == ackrtcache.ErrCARMConfigMapNotFound || err == ackrtcache.ErrKeyNotFound {
540+
return "", fmt.Errorf("unable to retrieve role ARN from CARM v2 for account %s: %v", teamID, err)
541+
}
542+
543+
return ackv1alpha1.AWSResourceName(roleARN), nil
544+
}
545+
499546
// getRegion returns the AWS region that the given resource is in or should be
500547
// created in. If the CR have a region associated with it, it is used. Otherwise
501548
// we look for the namespace associated region, if that is set we use it. Finally

pkg/runtime/cache/cache.go

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ const (
4646

4747
// The prefix for owner account ID in the v2 CARM map.
4848
OwnerAccountIDPrefix = "owner-account-id/"
49+
50+
// The prefix for owner team ID in the v2 CARM map.
51+
TeamIDPrefix = "team-id/"
4952
)
5053

5154
// ackSystemNamespace is the namespace in which we look up ACK system

pkg/runtime/cache/namespace.go

+24
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ type namespaceInfo struct {
3232
defaultRegion string
3333
// services.k8s.aws/owner-account-id Annotation
3434
ownerAccountID string
35+
// services.k8s.aws/team-id Annotation
36+
teamID string
3537
// services.k8s.aws/endpoint-url Annotation
3638
endpointURL string
3739
// {service}.services.k8s.aws/deletion-policy Annotations (keyed by service)
@@ -54,6 +56,14 @@ func (n *namespaceInfo) getOwnerAccountID() string {
5456
return n.ownerAccountID
5557
}
5658

59+
// getTeamID returns the namespace team-id
60+
func (n *namespaceInfo) getTeamID() string {
61+
if n == nil {
62+
return ""
63+
}
64+
return n.teamID
65+
}
66+
5767
// getEndpointURL returns the namespace Endpoint URL
5868
func (n *namespaceInfo) getEndpointURL() string {
5969
if n == nil {
@@ -182,6 +192,16 @@ func (c *NamespaceCache) GetOwnerAccountID(namespace string) (string, bool) {
182192
return "", false
183193
}
184194

195+
// GetTeamID returns the team-id if it exists
196+
func (c *NamespaceCache) GetTeamID(namespace string) (string, bool) {
197+
info, ok := c.getNamespaceInfo(namespace)
198+
if ok {
199+
a := info.getTeamID()
200+
return a, a != ""
201+
}
202+
return "", false
203+
}
204+
185205
// GetEndpointURL returns the endpoint URL if it exists
186206
func (c *NamespaceCache) GetEndpointURL(namespace string) (string, bool) {
187207
info, ok := c.getNamespaceInfo(namespace)
@@ -225,6 +245,10 @@ func (c *NamespaceCache) setNamespaceInfoFromK8sObject(ns *corev1.Namespace) {
225245
if ok {
226246
nsInfo.ownerAccountID = OwnerAccountID
227247
}
248+
TeamID, ok := nsa[ackv1alpha1.AnnotationTeamID]
249+
if ok {
250+
nsInfo.teamID = TeamID
251+
}
228252
EndpointURL, ok := nsa[ackv1alpha1.AnnotationEndpointURL]
229253
if ok {
230254
nsInfo.endpointURL = EndpointURL

pkg/runtime/cache/namespace_test.go

+95
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,101 @@ func TestNamespaceCache(t *testing.T) {
131131
require.False(t, ok)
132132
}
133133

134+
func TestNamespaceCacheWithRoleARN(t *testing.T) {
135+
// create a fake k8s client and fake watcher
136+
k8sClient := k8sfake.NewSimpleClientset()
137+
watcher := watch.NewFake()
138+
k8sClient.PrependWatchReactor("production", k8stesting.DefaultWatchReactor(watcher, nil))
139+
140+
// New logger writing to specific buffer
141+
zapOptions := ctrlrtzap.Options{
142+
Development: true,
143+
Level: zapcore.InfoLevel,
144+
}
145+
fakeLogger := ctrlrtzap.New(ctrlrtzap.UseFlagOptions(&zapOptions))
146+
147+
// initlizing account cache
148+
namespaceCache := ackrtcache.NewNamespaceCache(fakeLogger, []string{}, []string{})
149+
stopCh := make(chan struct{})
150+
151+
namespaceCache.Run(k8sClient, stopCh)
152+
153+
// Test create events
154+
_, err := k8sClient.CoreV1().Namespaces().Create(
155+
context.Background(),
156+
&corev1.Namespace{
157+
ObjectMeta: metav1.ObjectMeta{
158+
Name: "production",
159+
Annotations: map[string]string{
160+
ackv1alpha1.AnnotationDefaultRegion: "us-west-2",
161+
ackv1alpha1.AnnotationTeamID: "team-a",
162+
ackv1alpha1.AnnotationEndpointURL: "https://amazon-service.region.amazonaws.com",
163+
},
164+
},
165+
},
166+
metav1.CreateOptions{},
167+
)
168+
require.Nil(t, err)
169+
170+
time.Sleep(time.Second)
171+
172+
defaultRegion, ok := namespaceCache.GetDefaultRegion("production")
173+
require.True(t, ok)
174+
require.Equal(t, "us-west-2", defaultRegion)
175+
176+
teamID, ok := namespaceCache.GetTeamID("production")
177+
require.True(t, ok)
178+
require.Equal(t, "team-a", teamID)
179+
180+
endpointURL, ok := namespaceCache.GetEndpointURL("production")
181+
require.True(t, ok)
182+
require.Equal(t, "https://amazon-service.region.amazonaws.com", endpointURL)
183+
184+
// Test update events
185+
_, err = k8sClient.CoreV1().Namespaces().Update(
186+
context.Background(),
187+
&corev1.Namespace{
188+
ObjectMeta: metav1.ObjectMeta{
189+
Name: "production",
190+
Annotations: map[string]string{
191+
ackv1alpha1.AnnotationDefaultRegion: "us-est-1",
192+
ackv1alpha1.AnnotationTeamID: "team-b",
193+
ackv1alpha1.AnnotationEndpointURL: "https://amazon-other-service.region.amazonaws.com",
194+
},
195+
},
196+
},
197+
metav1.UpdateOptions{},
198+
)
199+
require.Nil(t, err)
200+
201+
time.Sleep(time.Second)
202+
203+
defaultRegion, ok = namespaceCache.GetDefaultRegion("production")
204+
require.True(t, ok)
205+
require.Equal(t, "us-est-1", defaultRegion)
206+
207+
teamID, ok = namespaceCache.GetTeamID("production")
208+
require.True(t, ok)
209+
require.Equal(t, "team-b", teamID)
210+
211+
endpointURL, ok = namespaceCache.GetEndpointURL("production")
212+
require.True(t, ok)
213+
require.Equal(t, "https://amazon-other-service.region.amazonaws.com", endpointURL)
214+
215+
// Test delete events
216+
err = k8sClient.CoreV1().Namespaces().Delete(
217+
context.Background(),
218+
"production",
219+
metav1.DeleteOptions{},
220+
)
221+
require.Nil(t, err)
222+
223+
time.Sleep(time.Second)
224+
225+
_, ok = namespaceCache.GetDefaultRegion(testNamespace1)
226+
require.False(t, ok)
227+
}
228+
134229
func TestScopedNamespaceCache(t *testing.T) {
135230
defaultConfig := ackrtcache.Config{
136231
WatchScope: []string{"watch-scope", "watch-scope-2"},

0 commit comments

Comments
 (0)