Skip to content

Commit 78569ee

Browse files
committed
Support role annotation
1 parent 24070d9 commit 78569ee

File tree

5 files changed

+180
-0
lines changed

5 files changed

+180
-0
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+
// AnnotationRoleARN is an annotation whose value is the identifier
39+
// for the AWS role ARN 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. In order for this cross-account resource management to succeed,
43+
// the AWS IAM Role that the ACK service controller runs as needs to have
44+
// the ability to call the AWS STS::AssumeRole API call and assume an IAM
45+
// Role in the target AWS Account.
46+
AnnotationRoleARN = AnnotationPrefix + "role-arn"
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

pkg/runtime/adoption_reconciler.go

Lines changed: 28 additions & 0 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"
@@ -128,6 +129,19 @@ func (r *adoptionReconciler) reconcile(ctx context.Context, req ctrlrt.Request)
128129
return requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
129130
}
130131
}
132+
133+
// If a user specified a namespace with role ARN annotation,
134+
// we need to get the role and set the accout ID to that role.
135+
roleARNFromAnnotation := r.getRoleARNFromAnnotation(res)
136+
if roleARNFromAnnotation != "" {
137+
roleARN = roleARNFromAnnotation
138+
parsedARN, err := arn.Parse(string(roleARNFromAnnotation))
139+
if err != nil {
140+
return fmt.Errorf("failed to parsed role ARN %q from namespace annotation: %v", roleARNFromAnnotation, err)
141+
}
142+
acctID = ackv1alpha1.AWSAccountID(parsedARN.AccountID)
143+
}
144+
131145
region := r.getRegion(res)
132146
targetDescriptor := rmf.ResourceDescriptor()
133147
endpointURL := r.getEndpointURL(res)
@@ -459,6 +473,20 @@ func (r *adoptionReconciler) getOwnerAccountID(
459473
return ackv1alpha1.AWSAccountID(r.cfg.AccountID), false
460474
}
461475

476+
// getRoleARNFromAnnotation gets the role ARN from the namespace
477+
// annotation.
478+
func (r *adoptionReconciler) getRoleARNFromAnnotation(
479+
res *ackv1alpha1.AdoptedResource,
480+
) ackv1alpha1.AWSResourceName {
481+
// look for role ARN in the namespace annotations
482+
namespace := res.GetNamespace()
483+
roleARN, ok := r.cache.Namespaces.GetRoleARN(namespace)
484+
if ok {
485+
return ackv1alpha1.AWSResourceName(roleARN)
486+
}
487+
return ""
488+
}
489+
462490
// getEndpointURL returns the AWS account that owns the supplied resource.
463491
// We look for the namespace associated endpoint url, if that is set we use it.
464492
// Otherwise if none of these annotations are set we use the endpoint url specified

pkg/runtime/cache/namespace.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ type namespaceInfo struct {
3636
endpointURL string
3737
// {service}.services.k8s.aws/deletion-policy Annotations (keyed by service)
3838
deletionPolicies map[string]string
39+
// services.k8s.aws/role-arn Annotation
40+
roleARN string
3941
}
4042

4143
// getDefaultRegion returns the default region value
@@ -54,6 +56,14 @@ func (n *namespaceInfo) getOwnerAccountID() string {
5456
return n.ownerAccountID
5557
}
5658

59+
// getRoleARN returns the namespace role ARN
60+
func (n *namespaceInfo) getRoleARN() string {
61+
if n == nil {
62+
return ""
63+
}
64+
return n.roleARN
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+
// GetRoleARN returns the role ARN if it exists
196+
func (c *NamespaceCache) GetRoleARN(namespace string) (string, bool) {
197+
info, ok := c.getNamespaceInfo(namespace)
198+
if ok {
199+
a := info.getRoleARN()
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)
@@ -229,6 +249,10 @@ func (c *NamespaceCache) setNamespaceInfoFromK8sObject(ns *corev1.Namespace) {
229249
if ok {
230250
nsInfo.endpointURL = EndpointURL
231251
}
252+
RoleARN, ok := nsa[ackv1alpha1.AnnotationRoleARN]
253+
if ok {
254+
nsInfo.roleARN = RoleARN
255+
}
232256

233257
nsInfo.deletionPolicies = map[string]string{}
234258
nsDeletionPolicySuffix := "." + ackv1alpha1.AnnotationDeletionPolicy

pkg/runtime/cache/namespace_test.go

Lines changed: 95 additions & 0 deletions
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.AnnotationRoleARN: "arn:aws:iam::123456789012:role/some-role",
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+
roleARN, ok := namespaceCache.GetRoleARN("production")
177+
require.True(t, ok)
178+
require.Equal(t, "arn:aws:iam::123456789012:role/some-role", roleARN)
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.AnnotationRoleARN: "arn:aws:iam::223456789012:role/some-role",
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+
roleARN, ok = namespaceCache.GetRoleARN("production")
208+
require.True(t, ok)
209+
require.Equal(t, "arn:aws:iam::223456789012:role/some-role", roleARN)
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"},

pkg/runtime/reconciler.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"strings"
2121
"time"
2222

23+
"github.com/aws/aws-sdk-go/aws/arn"
2324
backoff "github.com/cenkalti/backoff/v4"
2425
"github.com/go-logr/logr"
2526
"github.com/pkg/errors"
@@ -178,6 +179,17 @@ func (r *resourceReconciler) Reconcile(ctx context.Context, req ctrlrt.Request)
178179
return ctrlrt.Result{}, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
179180
}
180181
}
182+
183+
roleARNFromAnnotation := r.getRoleARNFromAnnotation(desired)
184+
if roleARNFromAnnotation != "" {
185+
roleARN = roleARNFromAnnotation
186+
parsedARN, err := arn.Parse(string(roleARNFromAnnotation))
187+
if err != nil {
188+
return ctrlrt.Result{}, fmt.Errorf("failed to parsed role ARN %q from namespace annotation: %v", roleARNFromAnnotation, err)
189+
}
190+
acctID = ackv1alpha1.AWSAccountID(parsedARN.AccountID)
191+
}
192+
181193
region := r.getRegion(desired)
182194

183195
endpointURL := r.getEndpointURL(desired)
@@ -1025,6 +1037,18 @@ func (r *resourceReconciler) getOwnerAccountID(
10251037
return controllerAccountID, false
10261038
}
10271039

1040+
func (r *resourceReconciler) getRoleARNFromAnnotation(
1041+
res acktypes.AWSResource,
1042+
) ackv1alpha1.AWSResourceName {
1043+
// look for role ARN in the namespace annotations
1044+
namespace := res.MetaObject().GetNamespace()
1045+
roleARN, ok := r.cache.Namespaces.GetRoleARN(namespace)
1046+
if ok {
1047+
return ackv1alpha1.AWSResourceName(roleARN)
1048+
}
1049+
return ""
1050+
}
1051+
10281052
// getRoleARN return the Role ARN that should be assumed in order to manage
10291053
// the resources.
10301054
func (r *resourceReconciler) getRoleARN(

0 commit comments

Comments
 (0)