Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion apis/core/v1alpha1/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const (
// CR, that means the user expects the ACK service controller to create the
// backend AWS service API resource.
AnnotationARN = AnnotationPrefix + "arn"
// AnnotationOwnerAccountID is an annotation whoe value is the identifier
// AnnotationOwnerAccountID is an annotation whose value is the identifier
// for the AWS account to which the resource belongs. 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
Expand All @@ -36,4 +36,20 @@ const (
// TODO(jaypipes): Link to documentation on cross-account resource
// management
AnnotationOwnerAccountID = AnnotationPrefix + "owner-account-id"
// 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
// controller that the CR should be created on specific region. ACK service
// controller will not override the resource region if this annotation is set.
AnnotationRegion = AnnotationPrefix + "region"
// AnnotationDefaultRegion is an annotation whose value is the identifier
// for the default AWS region in which resources should be created. If this
// annotation is set on a namespace, the Kubernetes user is indicating that
// the ACK service controller should set the regions in which the resource
// should be created, if a region annotation is not set on the CR metadata.
// If this annotation - and AnnotationRegion - are not set, ACK service
// controllers look for controller binary flags and environment variables
// injected by POD IRSA, to decide in which region the resources should be
// created.
AnnotationDefaultRegion = AnnotationPrefix + "default-region"
)
115 changes: 115 additions & 0 deletions pkg/runtime/cache/account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package cache

import (
"sync"

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
informersv1 "k8s.io/client-go/informers/core/v1"
kubernetes "k8s.io/client-go/kubernetes"
k8scache "k8s.io/client-go/tools/cache"
)

const (
// ACKRoleAccountMap is the name of the configmap map object storing
// all the AWS Account IDs associated with their AWS Role ARNs.
ACKRoleAccountMap = "ack-role-account-map"
)

// AccountCache is responsible for caching the CARM configmap
// data. It is listening to all the events related to the CARM map and
// make the changes accordingly.
type AccountCache struct {
sync.RWMutex

log logr.Logger

// ConfigMap informer
informer k8scache.SharedInformer
roleARNs map[string]string
}

// NewAccountCache makes a new AccountCache from a client.Interface
// and a logr.Logger
func NewAccountCache(clientset kubernetes.Interface, log logr.Logger) *AccountCache {
sharedInformer := informersv1.NewConfigMapInformer(
clientset,
currentNamespace,
informerResyncPeriod,
k8scache.Indexers{},
)
return &AccountCache{
informer: sharedInformer,
log: log.WithName("AccountCache"),
roleARNs: make(map[string]string),
}
}

// resourceMatchACKRoleAccountConfigMap verifies if a resource is
// the CARM configmap. It verifies the name, namespace and object type.
func resourceMatchACKRoleAccountsConfigMap(raw interface{}) bool {
object, ok := raw.(*corev1.ConfigMap)
return ok && object.ObjectMeta.Name == ACKRoleAccountMap
}

// Run adds the default event handler functions to the SharedInformer and
// runs the informer to begin processing items.
func (c *AccountCache) Run(stopCh <-chan struct{}) {
c.informer.AddEventHandler(k8scache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
if resourceMatchACKRoleAccountsConfigMap(obj) {
object := obj.(*corev1.ConfigMap).DeepCopy()
c.log.V(1).Info("ack-role-account-map has been created")
c.updateAccountRoleData(object.Data)
c.log.V(1).Info("cached ack-role-account-map data")
}
},
UpdateFunc: func(orig, desired interface{}) {
if resourceMatchACKRoleAccountsConfigMap(desired) {
object := desired.(*corev1.ConfigMap).DeepCopy()
c.log.V(1).Info("ack-role-account-map has been updated")
//TODO(a-hilaly): compare data checksum before updating the cache
c.updateAccountRoleData(object.Data)
c.log.V(1).Info("cached ack-role-account-map data")
}
},
DeleteFunc: func(obj interface{}) {
if resourceMatchACKRoleAccountsConfigMap(obj) {
c.log.V(1).Info("ack-role-account-map has been deleted")
newMap := make(map[string]string)
c.updateAccountRoleData(newMap)
c.log.V(1).Info("cleaned up role account map")
}
},
})
go c.informer.Run(stopCh)
}

// GetAccountRoleARN queries the AWS accountID associated Role ARN
// from the cached CARM configmap. This function is thread safe.
func (c *AccountCache) GetAccountRoleARN(accountID string) (string, bool) {
c.RLock()
defer c.RUnlock()
roleARN, ok := c.roleARNs[accountID]
return roleARN, ok
}

// updateAccountRoleData updates the CARM map. This function is thread safe.
func (c *AccountCache) updateAccountRoleData(data map[string]string) {
c.Lock()
defer c.Unlock()
c.roleARNs = data
}
143 changes: 143 additions & 0 deletions pkg/runtime/cache/account_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package cache_test

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/require"
"go.uber.org/zap/zapcore"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
k8sfake "k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
ctrlrtzap "sigs.k8s.io/controller-runtime/pkg/log/zap"

ackrtcache "github.com/aws/aws-controllers-k8s/pkg/runtime/cache"
)

const (
testNamespace = "ack-system"

testAccount1 = "012345678912"
testAccountARN1 = "arn:aws:iam::012345678912:role/S3Access"
testAccount2 = "219876543210"
testAccountARN2 = "arn:aws:iam::012345678912:role/root"
)

func TestAccountCache(t *testing.T) {
accountsMap1 := map[string]string{
testAccount1: testAccountARN1,
}

accountsMap2 := map[string]string{
testAccount1: testAccountARN1,
testAccount2: testAccountARN2,
}

// create a fake k8s client and a fake watcher
k8sClient := k8sfake.NewSimpleClientset()
watcher := watch.NewFake()
k8sClient.PrependWatchReactor("configMaps", k8stesting.DefaultWatchReactor(watcher, nil))

zapOptions := ctrlrtzap.Options{
Development: true,
Level: zapcore.InfoLevel,
}
fakeLogger := ctrlrtzap.New(ctrlrtzap.UseFlagOptions(&zapOptions))

// initlizing account cache
accountCache := ackrtcache.NewAccountCache(k8sClient, fakeLogger)
stopCh := make(chan struct{})
accountCache.Run(stopCh)

// Test create events
k8sClient.CoreV1().ConfigMaps(testNamespace).Create(
context.Background(),
&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "random-map",
},
Data: accountsMap1,
},
metav1.CreateOptions{},
)

time.Sleep(time.Second)

_, ok := accountCache.GetAccountRoleARN("random-account")
require.False(t, ok)

k8sClient.CoreV1().ConfigMaps(testNamespace).Create(
context.Background(),
&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: ackrtcache.ACKRoleAccountMap,
Namespace: "ack-system",
},
Data: accountsMap1,
},
metav1.CreateOptions{},
)

time.Sleep(time.Second)

roleARN, ok := accountCache.GetAccountRoleARN(testAccount1)
require.True(t, ok)
require.Equal(t, roleARN, testAccountARN1)

_, ok = accountCache.GetAccountRoleARN(testAccount2)
require.False(t, ok)

// Test update events
k8sClient.CoreV1().ConfigMaps("ack-system").Update(
context.Background(),
&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: ackrtcache.ACKRoleAccountMap,
Namespace: "ack-system",
},
Data: accountsMap2,
},
metav1.UpdateOptions{},
)

time.Sleep(time.Second)

roleARN, ok = accountCache.GetAccountRoleARN(testAccount1)
require.True(t, ok)
require.Equal(t, roleARN, testAccountARN1)

roleARN, ok = accountCache.GetAccountRoleARN(testAccount2)
require.True(t, ok)
require.Equal(t, roleARN, testAccountARN2)

// Test delete events
k8sClient.CoreV1().ConfigMaps("ack-system").Delete(
context.Background(),
ackrtcache.ACKRoleAccountMap,
metav1.DeleteOptions{},
)

time.Sleep(time.Second)

_, ok = accountCache.GetAccountRoleARN(testAccount1)
require.False(t, ok)
_, ok = accountCache.GetAccountRoleARN(testAccount2)
require.False(t, ok)

}
84 changes: 84 additions & 0 deletions pkg/runtime/cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package cache

import (
"os"
"time"

"github.com/go-logr/logr"
kubernetes "k8s.io/client-go/kubernetes"
)

const (
// defaultNamespace is the default namespace to use if the environment
// variable NAMESPACE is not found. The NAMESPACE variable is injected
// using the kubernetes downward api.
defaultNamespace = "ack-system"

// informerDefaultResyncPeriod is the period at which ShouldResync
// is considered.
informerResyncPeriod = 0 * time.Second
)

// currentNamespace is the namespace in which the current service
// controller Pod is running
var currentNamespace string

func init() {
currentNamespace = os.Getenv("K8S_NAMESPACE")
if currentNamespace == "" {
currentNamespace = defaultNamespace
}
}

// Caches is used to interact with the different caches
type Caches struct {
// stopCh is a channel use to stop all the
// owned caches
stopCh chan struct{}

// Accounts cache
Accounts *AccountCache

// Namespaces cache
Namespaces *NamespaceCache
}

// New creates a new Caches object from a kubernetes.Interface and
// a logr.Logger
func New(clientset kubernetes.Interface, log logr.Logger) Caches {
return Caches{
Accounts: NewAccountCache(clientset, log),
Namespaces: NewNamespaceCache(clientset, log),
}
}

// Run runs all the owned caches
func (c Caches) Run() {
stopCh := make(chan struct{})
if c.Accounts != nil {
c.Accounts.Run(stopCh)
}
if c.Namespaces != nil {
c.Namespaces.Run(stopCh)
}
c.stopCh = stopCh
}

// Stop closes the stop channel and cause all the SharedInformers
// by caches to stop running
func (c Caches) Stop() {
close(c.stopCh)
}
Loading