Skip to content

Commit

Permalink
Support role annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
zicongmei committed Feb 22, 2024
1 parent 24070d9 commit 78569ee
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 0 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"
// AnnotationRoleARN is an annotation whose value is the identifier
// for the AWS role ARN 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. 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.
AnnotationRoleARN = AnnotationPrefix + "role-arn"
// 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
28 changes: 28 additions & 0 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 @@ -128,6 +129,19 @@ func (r *adoptionReconciler) reconcile(ctx context.Context, req ctrlrt.Request)
return requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
}
}

// If a user specified a namespace with role ARN annotation,
// we need to get the role and set the accout ID to that role.
roleARNFromAnnotation := r.getRoleARNFromAnnotation(res)
if roleARNFromAnnotation != "" {
roleARN = roleARNFromAnnotation
parsedARN, err := arn.Parse(string(roleARNFromAnnotation))
if err != nil {
return fmt.Errorf("failed to parsed role ARN %q from namespace annotation: %v", roleARNFromAnnotation, err)
}
acctID = ackv1alpha1.AWSAccountID(parsedARN.AccountID)
}

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

// getRoleARNFromAnnotation gets the role ARN from the namespace
// annotation.
func (r *adoptionReconciler) getRoleARNFromAnnotation(
res *ackv1alpha1.AdoptedResource,
) ackv1alpha1.AWSResourceName {
// look for role ARN in the namespace annotations
namespace := res.GetNamespace()
roleARN, ok := r.cache.Namespaces.GetRoleARN(namespace)
if ok {
return ackv1alpha1.AWSResourceName(roleARN)
}
return ""
}

// 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 Down
24 changes: 24 additions & 0 deletions pkg/runtime/cache/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type namespaceInfo struct {
endpointURL string
// {service}.services.k8s.aws/deletion-policy Annotations (keyed by service)
deletionPolicies map[string]string
// services.k8s.aws/role-arn Annotation
roleARN string
}

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

// getRoleARN returns the namespace role ARN
func (n *namespaceInfo) getRoleARN() string {
if n == nil {
return ""
}
return n.roleARN
}

// getEndpointURL returns the namespace Endpoint URL
func (n *namespaceInfo) getEndpointURL() string {
if n == nil {
Expand Down Expand Up @@ -182,6 +192,16 @@ func (c *NamespaceCache) GetOwnerAccountID(namespace string) (string, bool) {
return "", false
}

// GetRoleARN returns the role ARN if it exists
func (c *NamespaceCache) GetRoleARN(namespace string) (string, bool) {
info, ok := c.getNamespaceInfo(namespace)
if ok {
a := info.getRoleARN()
return a, a != ""
}
return "", false
}

// GetEndpointURL returns the endpoint URL if it exists
func (c *NamespaceCache) GetEndpointURL(namespace string) (string, bool) {
info, ok := c.getNamespaceInfo(namespace)
Expand Down Expand Up @@ -229,6 +249,10 @@ func (c *NamespaceCache) setNamespaceInfoFromK8sObject(ns *corev1.Namespace) {
if ok {
nsInfo.endpointURL = EndpointURL
}
RoleARN, ok := nsa[ackv1alpha1.AnnotationRoleARN]
if ok {
nsInfo.roleARN = RoleARN
}

nsInfo.deletionPolicies = map[string]string{}
nsDeletionPolicySuffix := "." + ackv1alpha1.AnnotationDeletionPolicy
Expand Down
95 changes: 95 additions & 0 deletions pkg/runtime/cache/namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,101 @@ func TestNamespaceCache(t *testing.T) {
require.False(t, ok)
}

func TestNamespaceCacheWithRoleARN(t *testing.T) {
// create a fake k8s client and fake watcher
k8sClient := k8sfake.NewSimpleClientset()
watcher := watch.NewFake()
k8sClient.PrependWatchReactor("production", k8stesting.DefaultWatchReactor(watcher, nil))

// New logger writing to specific buffer
zapOptions := ctrlrtzap.Options{
Development: true,
Level: zapcore.InfoLevel,
}
fakeLogger := ctrlrtzap.New(ctrlrtzap.UseFlagOptions(&zapOptions))

// initlizing account cache
namespaceCache := ackrtcache.NewNamespaceCache(fakeLogger, []string{}, []string{})
stopCh := make(chan struct{})

namespaceCache.Run(k8sClient, stopCh)

// Test create events
_, err := k8sClient.CoreV1().Namespaces().Create(
context.Background(),
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "production",
Annotations: map[string]string{
ackv1alpha1.AnnotationDefaultRegion: "us-west-2",
ackv1alpha1.AnnotationRoleARN: "arn:aws:iam::123456789012:role/some-role",
ackv1alpha1.AnnotationEndpointURL: "https://amazon-service.region.amazonaws.com",
},
},
},
metav1.CreateOptions{},
)
require.Nil(t, err)

time.Sleep(time.Second)

defaultRegion, ok := namespaceCache.GetDefaultRegion("production")
require.True(t, ok)
require.Equal(t, "us-west-2", defaultRegion)

roleARN, ok := namespaceCache.GetRoleARN("production")
require.True(t, ok)
require.Equal(t, "arn:aws:iam::123456789012:role/some-role", roleARN)

endpointURL, ok := namespaceCache.GetEndpointURL("production")
require.True(t, ok)
require.Equal(t, "https://amazon-service.region.amazonaws.com", endpointURL)

// Test update events
_, err = k8sClient.CoreV1().Namespaces().Update(
context.Background(),
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "production",
Annotations: map[string]string{
ackv1alpha1.AnnotationDefaultRegion: "us-est-1",
ackv1alpha1.AnnotationRoleARN: "arn:aws:iam::223456789012:role/some-role",
ackv1alpha1.AnnotationEndpointURL: "https://amazon-other-service.region.amazonaws.com",
},
},
},
metav1.UpdateOptions{},
)
require.Nil(t, err)

time.Sleep(time.Second)

defaultRegion, ok = namespaceCache.GetDefaultRegion("production")
require.True(t, ok)
require.Equal(t, "us-est-1", defaultRegion)

roleARN, ok = namespaceCache.GetRoleARN("production")
require.True(t, ok)
require.Equal(t, "arn:aws:iam::223456789012:role/some-role", roleARN)

endpointURL, ok = namespaceCache.GetEndpointURL("production")
require.True(t, ok)
require.Equal(t, "https://amazon-other-service.region.amazonaws.com", endpointURL)

// Test delete events
err = k8sClient.CoreV1().Namespaces().Delete(
context.Background(),
"production",
metav1.DeleteOptions{},
)
require.Nil(t, err)

time.Sleep(time.Second)

_, ok = namespaceCache.GetDefaultRegion(testNamespace1)
require.False(t, ok)
}

func TestScopedNamespaceCache(t *testing.T) {
defaultConfig := ackrtcache.Config{
WatchScope: []string{"watch-scope", "watch-scope-2"},
Expand Down
24 changes: 24 additions & 0 deletions pkg/runtime/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"strings"
"time"

"github.com/aws/aws-sdk-go/aws/arn"
backoff "github.com/cenkalti/backoff/v4"
"github.com/go-logr/logr"
"github.com/pkg/errors"
Expand Down Expand Up @@ -178,6 +179,17 @@ func (r *resourceReconciler) Reconcile(ctx context.Context, req ctrlrt.Request)
return ctrlrt.Result{}, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay)
}
}

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

region := r.getRegion(desired)

endpointURL := r.getEndpointURL(desired)
Expand Down Expand Up @@ -1025,6 +1037,18 @@ func (r *resourceReconciler) getOwnerAccountID(
return controllerAccountID, false
}

func (r *resourceReconciler) getRoleARNFromAnnotation(
res acktypes.AWSResource,
) ackv1alpha1.AWSResourceName {
// look for role ARN in the namespace annotations
namespace := res.MetaObject().GetNamespace()
roleARN, ok := r.cache.Namespaces.GetRoleARN(namespace)
if ok {
return ackv1alpha1.AWSResourceName(roleARN)
}
return ""
}

// getRoleARN return the Role ARN that should be assumed in order to manage
// the resources.
func (r *resourceReconciler) getRoleARN(
Expand Down

0 comments on commit 78569ee

Please sign in to comment.