diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index cd8773b..9fb91c9 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,7 +21,7 @@ jobs: uses: golangci/golangci-lint-action@v3 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v1.51.0 + version: v1.55.2 # Optional: working directory, useful for monorepos working-directory: src/operator diff --git a/src/operator/controllers/aws_iam/pods/pods_aws_role_cleanup_controller.go b/src/operator/controllers/aws_iam/pods/pods_aws_role_cleanup_controller.go new file mode 100644 index 0000000..e2243c7 --- /dev/null +++ b/src/operator/controllers/aws_iam/pods/pods_aws_role_cleanup_controller.go @@ -0,0 +1,132 @@ +package pods + +import ( + "context" + "github.com/otterize/credentials-operator/src/controllers/metadata" + "github.com/samber/lo" + "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +type PodAWSRoleCleanupReconciler struct { + client.Client +} + +func NewPodAWSRoleCleanupReconciler(client client.Client) *PodAWSRoleCleanupReconciler { + return &PodAWSRoleCleanupReconciler{ + Client: client, + } +} + +const podServiceAccountIndexField = "spec.serviceAccountName" + +func initPodServiceAccountIndexField(mgr ctrl.Manager) error { + err := mgr.GetCache().IndexField( + context.Background(), + &corev1.Pod{}, + podServiceAccountIndexField, + func(object client.Object) []string { + pod := object.(*corev1.Pod) + return []string{pod.Spec.ServiceAccountName} + }) + if err != nil { + return err + } + + return nil +} + +func (r *PodAWSRoleCleanupReconciler) SetupWithManager(mgr ctrl.Manager) error { + err := initPodServiceAccountIndexField(mgr) + if err != nil { + return err + } + + return ctrl.NewControllerManagedBy(mgr). + WithOptions(controller.Options{RecoverPanic: lo.ToPtr(true)}). + For(&corev1.Pod{}). + Complete(r) +} + +func (r *PodAWSRoleCleanupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + pod := corev1.Pod{} + + err := r.Get(ctx, req.NamespacedName, &pod) + if err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + if pod.DeletionTimestamp == nil { + return ctrl.Result{}, nil + } + + if !controllerutil.ContainsFinalizer(&pod, metadata.AWSRoleFinalizer) { + logrus.Debugf("pod %v does not have the Otterize finalizer, skipping", pod.Name) + return ctrl.Result{}, nil + } + + var pods corev1.PodList + err = r.List(ctx, &pods, + client.MatchingFields{podServiceAccountIndexField: pod.Spec.ServiceAccountName}, + &client.ListOptions{Namespace: pod.Namespace}) + if err != nil { + return ctrl.Result{}, err + } + // check if this is the last pod linked to this SA. + if len(pods.Items) == 1 && pods.Items[0].UID == pod.UID { + var serviceAccount corev1.ServiceAccount + err := r.Get(ctx, types.NamespacedName{Name: pod.Spec.ServiceAccountName, Namespace: pod.Namespace}, &serviceAccount) + if err != nil { + // service account can be deleted before the pods go down, in which case cleanup has already occurred, so just let the pod terminate. + if apierrors.IsNotFound(err) { + return r.removeFinalizerFromPod(ctx, pod) + } + return ctrl.Result{}, err + } + + updatedServiceAccount := serviceAccount.DeepCopy() + if updatedServiceAccount.Labels == nil { + updatedServiceAccount.Labels = make(map[string]string) + } + // Normally we would call the other reconciler, but because this is blocking the removal of a pod finalizer, + // we instead update the ServiceAccount and let it do the hard work, so we can remove the pod finalizer ASAP. + updatedServiceAccount.Labels[metadata.OtterizeServiceAccountLabel] = metadata.OtterizeServiceAccountHasNoPodsValue + err = r.Client.Patch(ctx, updatedServiceAccount, client.MergeFrom(&serviceAccount)) + if err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + // service account can be deleted before the pods go down, in which case cleanup has already occurred, so just let the pod terminate. + if apierrors.IsNotFound(err) { + return r.removeFinalizerFromPod(ctx, pod) + } + return ctrl.Result{}, err + } + } + // in case there's more than 1 pod, this is not the last pod so we can just let the pod terminate. + return r.removeFinalizerFromPod(ctx, pod) +} + +func (r *PodAWSRoleCleanupReconciler) removeFinalizerFromPod(ctx context.Context, pod corev1.Pod) (ctrl.Result, error) { + updatedPod := pod.DeepCopy() + if controllerutil.RemoveFinalizer(updatedPod, metadata.AWSRoleFinalizer) { + err := r.Client.Patch(ctx, updatedPod, client.MergeFrom(&pod)) + if err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, err + } + } + + return ctrl.Result{}, nil +} diff --git a/src/operator/controllers/aws_iam/pods/pods_aws_role_cleanup_controller_test.go b/src/operator/controllers/aws_iam/pods/pods_aws_role_cleanup_controller_test.go new file mode 100644 index 0000000..a6278fa --- /dev/null +++ b/src/operator/controllers/aws_iam/pods/pods_aws_role_cleanup_controller_test.go @@ -0,0 +1,431 @@ +package pods + +import ( + "context" + "errors" + "github.com/otterize/credentials-operator/src/controllers/metadata" + mock_client "github.com/otterize/credentials-operator/src/mocks/controller-runtime/client" + "github.com/samber/lo" + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "testing" +) + +type TestPodsRoleCleanupControllerSuite struct { + suite.Suite + controller *gomock.Controller + client *mock_client.MockClient + reconciler *PodAWSRoleCleanupReconciler +} + +func (s *TestPodsRoleCleanupControllerSuite) SetupTest() { + s.controller = gomock.NewController(s.T()) + s.client = mock_client.NewMockClient(s.controller) + s.reconciler = NewPodAWSRoleCleanupReconciler(s.client) +} + +const ( + testPodName = "pod" + testNamespace = "namespace" + testServiceAccountName = "serviceaccount" + testPodUID = "pod-uid" +) + +func (s *TestPodsRoleCleanupControllerSuite) TestPodsRoleCleanupController_PodNotTerminatingNotAffected() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testPodName}, + } + + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespace, + UID: testPodUID, + DeletionTimestamp: nil, + }, + Spec: corev1.PodSpec{ServiceAccountName: testServiceAccountName}, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&pod)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.Pod, arg3 ...client.GetOption) error { + pod.DeepCopyInto(arg2) + return nil + }, + ) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().NoError(err) + s.Require().Empty(res) +} + +func (s *TestPodsRoleCleanupControllerSuite) TestPodsRoleCleanupController_PodTerminatingWithNoFinalizerIsNotAffected() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testPodName}, + } + + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespace, + UID: testPodUID, + DeletionTimestamp: lo.ToPtr(metav1.Now()), + }, + Spec: corev1.PodSpec{ServiceAccountName: testServiceAccountName}, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&pod)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.Pod, arg3 ...client.GetOption) error { + pod.DeepCopyInto(arg2) + return nil + }, + ) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().NoError(err) + s.Require().Empty(res) +} + +func (s *TestPodsRoleCleanupControllerSuite) TestPodsRoleCleanupController_LastPodTerminatingButDifferentPodUIDDoesNotLabelServiceAccountAndRemovesFinalizer() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testPodName}, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceAccountName, + Namespace: testNamespace, + Labels: map[string]string{metadata.OtterizeServiceAccountLabel: metadata.OtterizeServiceAccountHasPodsValue}, + }, + } + + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespace, + UID: testPodUID, + DeletionTimestamp: lo.ToPtr(metav1.Now()), + Finalizers: []string{ + metadata.AWSRoleFinalizer, + }, + }, + Spec: corev1.PodSpec{ServiceAccountName: serviceAccount.Name}, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&pod)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.Pod, arg3 ...client.GetOption) error { + pod.DeepCopyInto(arg2) + return nil + }, + ) + + s.client.EXPECT().List( + gomock.Any(), + gomock.AssignableToTypeOf(&corev1.PodList{}), + client.MatchingFields{podServiceAccountIndexField: serviceAccount.Name}, + gomock.Any(), + ).DoAndReturn( + func(arg0 context.Context, arg1 *corev1.PodList, arg2 ...client.ListOption) error { + podList := corev1.PodList{Items: []corev1.Pod{pod}} + podList.Items[0].UID += "somestring" + + podList.DeepCopyInto(arg1) + return nil + }, + ) + + // should not update serviceaccount because UID was different + + updatedPod := pod.DeepCopy() + s.Require().True(controllerutil.RemoveFinalizer(updatedPod, metadata.AWSRoleFinalizer)) + + s.client.EXPECT().Patch(gomock.Any(), updatedPod, gomock.Any()) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().NoError(err) + s.Require().Empty(res) +} + +func (s *TestPodsRoleCleanupControllerSuite) TestPodsRoleCleanupController_LastPodTerminatingWithFinalizerLabelsServiceAccountAndRemovesFinalizer() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testPodName}, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceAccountName, + Namespace: testNamespace, + Labels: map[string]string{metadata.OtterizeServiceAccountLabel: metadata.OtterizeServiceAccountHasPodsValue}, + }, + } + + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespace, + UID: testPodUID, + DeletionTimestamp: lo.ToPtr(metav1.Now()), + Finalizers: []string{ + metadata.AWSRoleFinalizer, + }, + }, + Spec: corev1.PodSpec{ServiceAccountName: serviceAccount.Name}, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&pod)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.Pod, arg3 ...client.GetOption) error { + pod.DeepCopyInto(arg2) + return nil + }, + ) + + s.client.EXPECT().List( + gomock.Any(), + gomock.AssignableToTypeOf(&corev1.PodList{}), + client.MatchingFields{podServiceAccountIndexField: serviceAccount.Name}, + gomock.Any(), + ).DoAndReturn( + func(arg0 context.Context, arg1 *corev1.PodList, arg2 ...client.ListOption) error { + podList := corev1.PodList{Items: []corev1.Pod{pod}} + + podList.DeepCopyInto(arg1) + return nil + }, + ) + + s.client.EXPECT().Get(gomock.Any(), types.NamespacedName{ + Namespace: serviceAccount.Namespace, + Name: serviceAccount.Name, + }, gomock.AssignableToTypeOf(&serviceAccount)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.ServiceAccount, arg3 ...client.GetOption) error { + serviceAccount.DeepCopyInto(arg2) + return nil + }, + ) + + updatedServiceAccount := serviceAccount.DeepCopy() + updatedServiceAccount.Labels = map[string]string{metadata.OtterizeServiceAccountLabel: metadata.OtterizeServiceAccountHasNoPodsValue} + + s.client.EXPECT().Patch(gomock.Any(), updatedServiceAccount, gomock.Any()) + + updatedPod := pod.DeepCopy() + s.Require().True(controllerutil.RemoveFinalizer(updatedPod, metadata.AWSRoleFinalizer)) + + s.client.EXPECT().Patch(gomock.Any(), updatedPod, gomock.Any()) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().NoError(err) + s.Require().Empty(res) +} + +func (s *TestPodsRoleCleanupControllerSuite) TestPodsRoleCleanupController_NonLastPodTerminatingDoesNotLabelServiceAccountAndRemovesFinalizer() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testPodName}, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceAccountName, + Namespace: testNamespace, + Labels: map[string]string{metadata.OtterizeServiceAccountLabel: metadata.OtterizeServiceAccountHasPodsValue}, + }, + } + + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespace, + UID: testPodUID, + DeletionTimestamp: lo.ToPtr(metav1.Now()), + Finalizers: []string{ + metadata.AWSRoleFinalizer, + }, + }, + Spec: corev1.PodSpec{ServiceAccountName: serviceAccount.Name}, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&pod)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.Pod, arg3 ...client.GetOption) error { + pod.DeepCopyInto(arg2) + return nil + }, + ) + + s.client.EXPECT().List( + gomock.Any(), + gomock.AssignableToTypeOf(&corev1.PodList{}), + client.MatchingFields{podServiceAccountIndexField: serviceAccount.Name}, + gomock.Any(), + ).DoAndReturn( + func(arg0 context.Context, arg1 *corev1.PodList, arg2 ...client.ListOption) error { + podList := corev1.PodList{Items: []corev1.Pod{pod}} + pod2 := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName + "2", + Namespace: testNamespace, + UID: testPodUID + "2", + Finalizers: []string{ + metadata.AWSRoleFinalizer, + }, + }, + Spec: corev1.PodSpec{ServiceAccountName: serviceAccount.Name}, + } + podList.Items = append(podList.Items, pod2) + + podList.DeepCopyInto(arg1) + return nil + }, + ) + + // should not update serviceaccount because it's not the last pod + + updatedPod := pod.DeepCopy() + s.Require().True(controllerutil.RemoveFinalizer(updatedPod, metadata.AWSRoleFinalizer)) + + s.client.EXPECT().Patch(gomock.Any(), updatedPod, gomock.Any()) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().NoError(err) + s.Require().Empty(res) +} + +func (s *TestPodsRoleCleanupControllerSuite) TestPodsRoleCleanupController_LastPodTerminatingWithFinalizerServiceAccountGoneAndRemovesFinalizerAnyway() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testPodName}, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceAccountName, + Namespace: testNamespace, + Labels: map[string]string{metadata.OtterizeServiceAccountLabel: metadata.OtterizeServiceAccountHasPodsValue}, + }, + } + + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespace, + UID: testPodUID, + DeletionTimestamp: lo.ToPtr(metav1.Now()), + Finalizers: []string{ + metadata.AWSRoleFinalizer, + }, + }, + Spec: corev1.PodSpec{ServiceAccountName: serviceAccount.Name}, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&pod)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.Pod, arg3 ...client.GetOption) error { + pod.DeepCopyInto(arg2) + return nil + }, + ) + + s.client.EXPECT().List( + gomock.Any(), + gomock.AssignableToTypeOf(&corev1.PodList{}), + client.MatchingFields{podServiceAccountIndexField: serviceAccount.Name}, + gomock.Any(), + ).DoAndReturn( + func(arg0 context.Context, arg1 *corev1.PodList, arg2 ...client.ListOption) error { + podList := corev1.PodList{Items: []corev1.Pod{pod}} + + podList.DeepCopyInto(arg1) + return nil + }, + ) + + s.client.EXPECT().Get(gomock.Any(), types.NamespacedName{ + Namespace: serviceAccount.Namespace, + Name: serviceAccount.Name, + }, gomock.AssignableToTypeOf(&serviceAccount)).Return(k8serrors.NewNotFound(schema.GroupResource{}, serviceAccount.Name)) + + updatedPod := pod.DeepCopy() + s.Require().True(controllerutil.RemoveFinalizer(updatedPod, metadata.AWSRoleFinalizer)) + + s.client.EXPECT().Patch(gomock.Any(), updatedPod, gomock.Any()) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().NoError(err) + s.Require().Empty(res) +} + +func (s *TestPodsRoleCleanupControllerSuite) TestPodsRoleCleanupController_LastPodTerminatingWithFinalizerLabelsServiceAccountButIsConflictSoRequeues() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testPodName}, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceAccountName, + Namespace: testNamespace, + Labels: map[string]string{metadata.OtterizeServiceAccountLabel: metadata.OtterizeServiceAccountHasPodsValue}, + }, + } + + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPodName, + Namespace: testNamespace, + UID: testPodUID, + DeletionTimestamp: lo.ToPtr(metav1.Now()), + Finalizers: []string{ + metadata.AWSRoleFinalizer, + }, + }, + Spec: corev1.PodSpec{ServiceAccountName: serviceAccount.Name}, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&pod)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.Pod, arg3 ...client.GetOption) error { + pod.DeepCopyInto(arg2) + return nil + }, + ) + + s.client.EXPECT().List( + gomock.Any(), + gomock.AssignableToTypeOf(&corev1.PodList{}), + client.MatchingFields{podServiceAccountIndexField: serviceAccount.Name}, + gomock.Any(), + ).DoAndReturn( + func(arg0 context.Context, arg1 *corev1.PodList, arg2 ...client.ListOption) error { + podList := corev1.PodList{Items: []corev1.Pod{pod}} + + podList.DeepCopyInto(arg1) + return nil + }, + ) + + s.client.EXPECT().Get(gomock.Any(), types.NamespacedName{ + Namespace: serviceAccount.Namespace, + Name: serviceAccount.Name, + }, gomock.AssignableToTypeOf(&serviceAccount)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.ServiceAccount, arg3 ...client.GetOption) error { + serviceAccount.DeepCopyInto(arg2) + return nil + }, + ) + + updatedServiceAccount := serviceAccount.DeepCopy() + updatedServiceAccount.Labels = map[string]string{metadata.OtterizeServiceAccountLabel: metadata.OtterizeServiceAccountHasNoPodsValue} + + s.client.EXPECT().Patch(gomock.Any(), updatedServiceAccount, gomock.Any()).Return(k8serrors.NewConflict(schema.GroupResource{}, serviceAccount.Name, errors.New("conflict"))) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().NoError(err) + s.Require().Equal(reconcile.Result{Requeue: true}, res) +} + +func TestRunServiceAccountControllerSuite(t *testing.T) { + suite.Run(t, new(TestPodsRoleCleanupControllerSuite)) +} diff --git a/src/operator/controllers/aws_iam/serviceaccount/generate.go b/src/operator/controllers/aws_iam/serviceaccount/generate.go new file mode 100644 index 0000000..2c1e4cd --- /dev/null +++ b/src/operator/controllers/aws_iam/serviceaccount/generate.go @@ -0,0 +1,3 @@ +package serviceaccount + +//go:generate go run go.uber.org/mock/mockgen@v0.2.0 -destination mocks/mocks.go -source=serviceaccount_controller.go diff --git a/src/operator/controllers/aws_iam/serviceaccount/mocks/mocks.go b/src/operator/controllers/aws_iam/serviceaccount/mocks/mocks.go new file mode 100644 index 0000000..d3c8da5 --- /dev/null +++ b/src/operator/controllers/aws_iam/serviceaccount/mocks/mocks.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: serviceaccount_controller.go + +// Package mock_serviceaccount is a generated GoMock package. +package mock_serviceaccount + +import ( + context "context" + reflect "reflect" + + types "github.com/aws/aws-sdk-go-v2/service/iam/types" + gomock "go.uber.org/mock/gomock" +) + +// MockAWSRolePolicyManager is a mock of AWSRolePolicyManager interface. +type MockAWSRolePolicyManager struct { + ctrl *gomock.Controller + recorder *MockAWSRolePolicyManagerMockRecorder +} + +// MockAWSRolePolicyManagerMockRecorder is the mock recorder for MockAWSRolePolicyManager. +type MockAWSRolePolicyManagerMockRecorder struct { + mock *MockAWSRolePolicyManager +} + +// NewMockAWSRolePolicyManager creates a new mock instance. +func NewMockAWSRolePolicyManager(ctrl *gomock.Controller) *MockAWSRolePolicyManager { + mock := &MockAWSRolePolicyManager{ctrl: ctrl} + mock.recorder = &MockAWSRolePolicyManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAWSRolePolicyManager) EXPECT() *MockAWSRolePolicyManagerMockRecorder { + return m.recorder +} + +// CreateOtterizeIAMRole mocks base method. +func (m *MockAWSRolePolicyManager) CreateOtterizeIAMRole(ctx context.Context, namespace, name string) (*types.Role, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOtterizeIAMRole", ctx, namespace, name) + ret0, _ := ret[0].(*types.Role) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateOtterizeIAMRole indicates an expected call of CreateOtterizeIAMRole. +func (mr *MockAWSRolePolicyManagerMockRecorder) CreateOtterizeIAMRole(ctx, namespace, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOtterizeIAMRole", reflect.TypeOf((*MockAWSRolePolicyManager)(nil).CreateOtterizeIAMRole), ctx, namespace, name) +} + +// DeleteOtterizeIAMRole mocks base method. +func (m *MockAWSRolePolicyManager) DeleteOtterizeIAMRole(ctx context.Context, namespace, name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteOtterizeIAMRole", ctx, namespace, name) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteOtterizeIAMRole indicates an expected call of DeleteOtterizeIAMRole. +func (mr *MockAWSRolePolicyManagerMockRecorder) DeleteOtterizeIAMRole(ctx, namespace, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOtterizeIAMRole", reflect.TypeOf((*MockAWSRolePolicyManager)(nil).DeleteOtterizeIAMRole), ctx, namespace, name) +} + +// GenerateRoleARN mocks base method. +func (m *MockAWSRolePolicyManager) GenerateRoleARN(namespace, name string) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateRoleARN", namespace, name) + ret0, _ := ret[0].(string) + return ret0 +} + +// GenerateRoleARN indicates an expected call of GenerateRoleARN. +func (mr *MockAWSRolePolicyManagerMockRecorder) GenerateRoleARN(namespace, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateRoleARN", reflect.TypeOf((*MockAWSRolePolicyManager)(nil).GenerateRoleARN), namespace, name) +} + +// GetOtterizeRole mocks base method. +func (m *MockAWSRolePolicyManager) GetOtterizeRole(ctx context.Context, namespaceName, accountName string) (bool, *types.Role, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOtterizeRole", ctx, namespaceName, accountName) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(*types.Role) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetOtterizeRole indicates an expected call of GetOtterizeRole. +func (mr *MockAWSRolePolicyManagerMockRecorder) GetOtterizeRole(ctx, namespaceName, accountName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOtterizeRole", reflect.TypeOf((*MockAWSRolePolicyManager)(nil).GetOtterizeRole), ctx, namespaceName, accountName) +} diff --git a/src/operator/controllers/serviceaccount/serviceaccount_controller.go b/src/operator/controllers/aws_iam/serviceaccount/serviceaccount_controller.go similarity index 63% rename from src/operator/controllers/serviceaccount/serviceaccount_controller.go rename to src/operator/controllers/aws_iam/serviceaccount/serviceaccount_controller.go index 8f7f818..80efdb5 100644 --- a/src/operator/controllers/serviceaccount/serviceaccount_controller.go +++ b/src/operator/controllers/aws_iam/serviceaccount/serviceaccount_controller.go @@ -5,27 +5,31 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/otterize/credentials-operator/src/controllers/metadata" - "github.com/otterize/intents-operator/src/shared/awsagent" "github.com/samber/lo" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +type AWSRolePolicyManager interface { + DeleteOtterizeIAMRole(ctx context.Context, namespace string, name string) error + GenerateRoleARN(namespace string, name string) string + GetOtterizeRole(ctx context.Context, namespaceName, accountName string) (bool, *types.Role, error) + CreateOtterizeIAMRole(ctx context.Context, namespace string, name string) (*types.Role, error) +} + type ServiceAccountReconciler struct { client.Client - scheme *runtime.Scheme - awsAgent *awsagent.Agent + awsAgent AWSRolePolicyManager } -func NewServiceAccountReconciler(client client.Client, scheme *runtime.Scheme, awsAgent *awsagent.Agent) *ServiceAccountReconciler { +func NewServiceAccountReconciler(client client.Client, awsAgent AWSRolePolicyManager) *ServiceAccountReconciler { return &ServiceAccountReconciler{ Client: client, - scheme: scheme, awsAgent: awsAgent, } } @@ -42,21 +46,55 @@ func (r *ServiceAccountReconciler) Reconcile(ctx context.Context, req ctrl.Reque serviceAccount := corev1.ServiceAccount{} - if err := r.Get(ctx, req.NamespacedName, &serviceAccount); err != nil { + err := r.Get(ctx, req.NamespacedName, &serviceAccount) + if err != nil { if apierrors.IsNotFound(err) { - err = r.awsAgent.DeleteOtterizeIAMRole(ctx, req.Namespace, req.Name) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } - if err != nil { - logger.WithError(err).Errorf("failed to remove service account") - } + isReferencedByPods, exists := getServiceAccountLabelValue(&serviceAccount) + if !exists { + logger.Debug("serviceAccount not labeled with credentials-operator.otterize.com/service-account, skipping") + return ctrl.Result{}, nil + } - return ctrl.Result{}, err + isNoLongerReferencedByPodsOrIsBeingDeleted := serviceAccount.DeletionTimestamp != nil || !isReferencedByPods + + if isNoLongerReferencedByPodsOrIsBeingDeleted { + err = r.awsAgent.DeleteOtterizeIAMRole(ctx, req.Namespace, req.Name) + + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to remove service account: %w", err) } - logger.WithError(err).Error("unable to fetch ServiceAccount") - return ctrl.Result{}, err + if serviceAccount.DeletionTimestamp != nil { + updatedServiceAccount := serviceAccount.DeepCopy() + if controllerutil.RemoveFinalizer(updatedServiceAccount, metadata.AWSRoleFinalizer) { + err := r.Client.Patch(ctx, updatedServiceAccount, client.MergeFrom(&serviceAccount)) + if err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, err + } + } + + } + return ctrl.Result{}, nil } + updatedServiceAccount := serviceAccount.DeepCopy() + if controllerutil.AddFinalizer(updatedServiceAccount, metadata.AWSRoleFinalizer) { + err := r.Client.Patch(ctx, updatedServiceAccount, client.MergeFrom(&serviceAccount)) + if err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, err + } + } shouldUpdateAnnotation, role, err := r.reconcileAWSRole(ctx, &serviceAccount) if err != nil { @@ -64,7 +102,6 @@ func (r *ServiceAccountReconciler) Reconcile(ctx context.Context, req ctrl.Reque } if shouldUpdateAnnotation { - updatedServiceAccount := serviceAccount.DeepCopy() if updatedServiceAccount.Annotations == nil { updatedServiceAccount.Annotations = make(map[string]string) } @@ -90,11 +127,6 @@ func (r *ServiceAccountReconciler) Reconcile(ctx context.Context, req ctrl.Reque func (r *ServiceAccountReconciler) reconcileAWSRole(ctx context.Context, serviceAccount *corev1.ServiceAccount) (updateAnnotation bool, role *types.Role, err error) { logger := logrus.WithFields(logrus.Fields{"serviceAccount": serviceAccount.Name, "namespace": serviceAccount.Namespace}) - if !hasOtterizeServiceAccountLabel(serviceAccount) { - logger.Debug("serviceAccount not labeled with credentials-operator.otterize.com/service-account, skipping") - return false, nil, nil - } - if roleARN, ok := hasAWSAnnotation(serviceAccount); ok { generatedRoleARN := r.awsAgent.GenerateRoleARN(serviceAccount.Namespace, serviceAccount.Name) found, role, err := r.awsAgent.GetOtterizeRole(ctx, serviceAccount.Namespace, serviceAccount.Name) @@ -131,11 +163,11 @@ func hasAWSAnnotation(serviceAccount *corev1.ServiceAccount) (string, bool) { return roleARN, ok } -func hasOtterizeServiceAccountLabel(serviceAccount *corev1.ServiceAccount) bool { +func getServiceAccountLabelValue(serviceAccount *corev1.ServiceAccount) (hasPods bool, exists bool) { if serviceAccount.Labels == nil { - return false + return false, false } - _, ok := serviceAccount.Labels[metadata.OtterizeServiceAccountLabel] - return ok + value, ok := serviceAccount.Labels[metadata.OtterizeServiceAccountLabel] + return value == "true", ok } diff --git a/src/operator/controllers/aws_iam/serviceaccount/serviceaccount_controller_test.go b/src/operator/controllers/aws_iam/serviceaccount/serviceaccount_controller_test.go new file mode 100644 index 0000000..31a0baf --- /dev/null +++ b/src/operator/controllers/aws_iam/serviceaccount/serviceaccount_controller_test.go @@ -0,0 +1,208 @@ +package serviceaccount + +import ( + "context" + "errors" + awstypes "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/otterize/credentials-operator/src/controllers/aws_iam/serviceaccount/mocks" + "github.com/otterize/credentials-operator/src/controllers/metadata" + mock_client "github.com/otterize/credentials-operator/src/mocks/controller-runtime/client" + "github.com/samber/lo" + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "testing" +) + +type TestServiceAccountSuite struct { + suite.Suite + controller *gomock.Controller + client *mock_client.MockClient + mockAWS *mock_serviceaccount.MockAWSRolePolicyManager + reconciler *ServiceAccountReconciler +} + +func (s *TestServiceAccountSuite) SetupTest() { + s.controller = gomock.NewController(s.T()) + s.client = mock_client.NewMockClient(s.controller) + s.mockAWS = mock_serviceaccount.NewMockAWSRolePolicyManager(s.controller) + s.reconciler = NewServiceAccountReconciler(s.client, s.mockAWS) +} + +const ( + testPodName = "pod" + testNamespace = "namespace" + testServiceAccountName = "serviceaccount" + testPodUID = "pod-uid" + testRoleARN = "role-arn" + testRoleName = "role-name" +) + +// Tests: +// 1. SA not being deleted and is not modified. +// 2. SA deleted but no finalizer and is not modified. +// 3. SA with finalizer causes role delete. +// 4. SA with finalizer causes deletion to role but role is 404 so sa is terminated successfully. +// 5. SA with finalizer causes update to role but role update returns error so is retried, and terminates successfully on second attempt. + +func (s *TestServiceAccountSuite) TestServiceAccountSuite_ServiceAccountNotTerminatingAndHasPodsNotAffected() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testServiceAccountName}, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceAccountName, + Namespace: testNamespace, + Annotations: map[string]string{metadata.ServiceAccountAWSRoleARNAnnotation: testRoleARN}, + Labels: map[string]string{metadata.OtterizeServiceAccountLabel: metadata.OtterizeServiceAccountHasPodsValue}, + Finalizers: []string{metadata.AWSRoleFinalizer}, + }, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&serviceAccount)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.ServiceAccount, arg3 ...client.GetOption) error { + serviceAccount.DeepCopyInto(arg2) + return nil + }, + ) + + s.mockAWS.EXPECT().GenerateRoleARN(testNamespace, testServiceAccountName).Return(testRoleARN) + s.mockAWS.EXPECT().GetOtterizeRole(gomock.Any(), testNamespace, testServiceAccountName).Return(true, &awstypes.Role{ + Arn: lo.ToPtr(testRoleARN), + RoleName: lo.ToPtr(testRoleName), + }, nil) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().NoError(err) + s.Require().Empty(res) +} + +func (s *TestServiceAccountSuite) TestServiceAccountSuite_ServiceAccountTerminatingWithNoLabelIsNotAffected() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testServiceAccountName}, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceAccountName, + Namespace: testNamespace, + Annotations: map[string]string{metadata.ServiceAccountAWSRoleARNAnnotation: testRoleARN}, + DeletionTimestamp: lo.ToPtr(metav1.Now()), + Finalizers: []string{metadata.AWSRoleFinalizer}, + }, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&serviceAccount)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.ServiceAccount, arg3 ...client.GetOption) error { + serviceAccount.DeepCopyInto(arg2) + return nil + }, + ) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().NoError(err) + s.Require().Empty(res) +} + +func (s *TestServiceAccountSuite) TestServiceAccountSuite_ServiceAccountTerminatingWithLabelAndFinalizerRemovesRoleAndFinalizer() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testServiceAccountName}, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceAccountName, + Namespace: testNamespace, + Annotations: map[string]string{metadata.ServiceAccountAWSRoleARNAnnotation: testRoleARN}, + Labels: map[string]string{metadata.OtterizeServiceAccountLabel: metadata.OtterizeServiceAccountHasPodsValue}, + DeletionTimestamp: lo.ToPtr(metav1.Now()), + Finalizers: []string{metadata.AWSRoleFinalizer}, + }, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&serviceAccount)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.ServiceAccount, arg3 ...client.GetOption) error { + serviceAccount.DeepCopyInto(arg2) + return nil + }, + ) + + s.mockAWS.EXPECT().DeleteOtterizeIAMRole(context.Background(), testNamespace, testServiceAccountName).Return(nil) + + updatedServiceAccount := serviceAccount.DeepCopy() + s.Require().True(controllerutil.RemoveFinalizer(updatedServiceAccount, metadata.AWSRoleFinalizer)) + s.client.EXPECT().Patch(gomock.Any(), updatedServiceAccount, gomock.Any()) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().NoError(err) + s.Require().Empty(res) +} + +func (s *TestServiceAccountSuite) TestServiceAccountSuite_ServiceAccountServiceAccountLabeledNoPodsDeletesRoleAndDoesntRemoveFinalizer() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testServiceAccountName}, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceAccountName, + Namespace: testNamespace, + Annotations: map[string]string{metadata.ServiceAccountAWSRoleARNAnnotation: testRoleARN}, + Labels: map[string]string{metadata.OtterizeServiceAccountLabel: metadata.OtterizeServiceAccountHasNoPodsValue}, + Finalizers: []string{metadata.AWSRoleFinalizer}, + }, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&serviceAccount)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.ServiceAccount, arg3 ...client.GetOption) error { + serviceAccount.DeepCopyInto(arg2) + return nil + }, + ) + + s.mockAWS.EXPECT().DeleteOtterizeIAMRole(context.Background(), testNamespace, testServiceAccountName).Return(nil) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().NoError(err) + s.Require().Empty(res) +} + +func (s *TestServiceAccountSuite) TestServiceAccountSuite_ServiceAccountServiceAccountTerminatingButRoleDeletionFailsSoDoesntRemoveFinalizer() { + req := ctrl.Request{ + NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testServiceAccountName}, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceAccountName, + Namespace: testNamespace, + Annotations: map[string]string{metadata.ServiceAccountAWSRoleARNAnnotation: testRoleARN}, + Labels: map[string]string{metadata.OtterizeServiceAccountLabel: metadata.OtterizeServiceAccountHasPodsValue}, + DeletionTimestamp: lo.ToPtr(metav1.Now()), + Finalizers: []string{metadata.AWSRoleFinalizer}, + }, + } + + s.client.EXPECT().Get(gomock.Any(), req.NamespacedName, gomock.AssignableToTypeOf(&serviceAccount)).DoAndReturn( + func(arg0 context.Context, arg1 types.NamespacedName, arg2 *corev1.ServiceAccount, arg3 ...client.GetOption) error { + serviceAccount.DeepCopyInto(arg2) + return nil + }, + ) + + s.mockAWS.EXPECT().DeleteOtterizeIAMRole(context.Background(), testNamespace, testServiceAccountName).Return(errors.New("role deletion failed")) + + res, err := s.reconciler.Reconcile(context.Background(), req) + s.Require().ErrorContains(err, "role deletion failed") + s.Require().Empty(res) +} + +func TestRunServiceAccountControllerSuite(t *testing.T) { + suite.Run(t, new(TestServiceAccountSuite)) +} diff --git a/src/operator/controllers/webhooks/pod_webhook.go b/src/operator/controllers/aws_iam/webhooks/pod_webhook.go similarity index 96% rename from src/operator/controllers/webhooks/pod_webhook.go rename to src/operator/controllers/aws_iam/webhooks/pod_webhook.go index 3b43e6e..23a73ca 100644 --- a/src/operator/controllers/webhooks/pod_webhook.go +++ b/src/operator/controllers/aws_iam/webhooks/pod_webhook.go @@ -12,6 +12,7 @@ import ( "k8s.io/apimachinery/pkg/types" "net/http" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "time" @@ -67,7 +68,7 @@ func (a *ServiceAccountAnnotatingPodWebhook) handleOnce(ctx context.Context, pod // we don't actually create the role here, so that the webhook returns quickly - a ServiceAccount reconciler takes care of it for us. updatedServiceAccount.Annotations[metadata.ServiceAccountAWSRoleARNAnnotation] = roleArn - updatedServiceAccount.Labels[metadata.OtterizeServiceAccountLabel] = "true" + updatedServiceAccount.Labels[metadata.OtterizeServiceAccountLabel] = metadata.OtterizeServiceAccountHasPodsValue if !dryRun { err = a.client.Patch(ctx, updatedServiceAccount, client.MergeFrom(&serviceAccount)) if err != nil { @@ -81,6 +82,7 @@ func (a *ServiceAccountAnnotatingPodWebhook) handleOnce(ctx context.Context, pod } pod.Annotations[metadata.OtterizeServiceAccountAWSRoleARNAnnotation] = roleArn + controllerutil.AddFinalizer(&pod, metadata.AWSRoleFinalizer) return pod, true, "pod and service account updated to create AWS role", nil } diff --git a/src/operator/controllers/metadata/annotations.go b/src/operator/controllers/metadata/annotations.go index 6488ffa..0e1993d 100644 --- a/src/operator/controllers/metadata/annotations.go +++ b/src/operator/controllers/metadata/annotations.go @@ -14,6 +14,9 @@ const ( // and IAM roles ServiceAccountAWSRoleARNAnnotation = "eks.amazonaws.com/role-arn" + // AWSRoleFinalizer indicates that cleanup on AWS is needed upon termination. + AWSRoleFinalizer = "credentials-operator.otterize.com/aws-role" + // OtterizeServiceAccountAWSRoleARNAnnotation is used to update a Pod in the mutating webhook with the role ARN // so that reinvocation is triggered for the EKS pod identity mutating webhook. OtterizeServiceAccountAWSRoleARNAnnotation = "credentials-operator.otterize.com/eks-role-arn" diff --git a/src/operator/controllers/metadata/labels.go b/src/operator/controllers/metadata/labels.go index 3314e29..2e636b4 100644 --- a/src/operator/controllers/metadata/labels.go +++ b/src/operator/controllers/metadata/labels.go @@ -12,7 +12,9 @@ const ( SecretTypeLabel = "credentials-operator.otterize.com/secret-type" // OtterizeServiceAccountLabel is used to label service accounts generated by the credentials-operator - OtterizeServiceAccountLabel = "credentials-operator.otterize.com/service-account-managed" + OtterizeServiceAccountLabel = "credentials-operator.otterize.com/service-account-managed" + OtterizeServiceAccountHasPodsValue = "true" + OtterizeServiceAccountHasNoPodsValue = "no-pods" // CreateAWSRoleLabel by using this annotation a pod marks that the operator should create an AWS IAM role for its service account CreateAWSRoleLabel = "credentials-operator.otterize.com/create-aws-role" ) diff --git a/src/operator/go.mod b/src/operator/go.mod index 74f812a..72983ea 100644 --- a/src/operator/go.mod +++ b/src/operator/go.mod @@ -9,7 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/iam v1.27.2 github.com/bombsimon/logrusr/v3 v3.0.0 github.com/cert-manager/cert-manager v1.12.3 - github.com/otterize/intents-operator/src v0.0.0-20231121123451-d892801be004 + github.com/otterize/intents-operator/src v0.0.0-20231207171817-9ec35db3bcb6 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.1 github.com/samber/lo v1.33.0 github.com/sirupsen/logrus v1.9.0 @@ -17,7 +17,7 @@ require ( github.com/spiffe/go-spiffe/v2 v2.1.1 github.com/spiffe/spire v1.4.3 github.com/spiffe/spire-api-sdk v1.3.1 - github.com/stretchr/testify v1.8.3 + github.com/stretchr/testify v1.8.4 github.com/suessflorian/gqlfetch v0.6.0 go.uber.org/mock v0.2.0 golang.org/x/exp v0.0.0-20230124195608-d38c7dcee874 @@ -112,7 +112,7 @@ require ( go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/mod v0.13.0 // indirect - golang.org/x/net v0.16.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.6.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect diff --git a/src/operator/go.sum b/src/operator/go.sum index 1fe8cd4..2d93653 100644 --- a/src/operator/go.sum +++ b/src/operator/go.sum @@ -347,14 +347,10 @@ github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= -github.com/otterize/intents-operator/src v0.0.0-20231119211508-00d52205eecc h1:nwUzhSfjuKJrH4O9b840FP/n48Vdf3AT2zbiDr4x56E= -github.com/otterize/intents-operator/src v0.0.0-20231119211508-00d52205eecc/go.mod h1:1V5AvkQodX1eaeZRGCCCSYrG6bN5qRVnUP3UaWAQV3Y= -github.com/otterize/intents-operator/src v0.0.0-20231121105340-b2baab573e58 h1:ASGDx7oqq09SVnB97dWbYOWPuPiTXo0d1+ewQc4wY+c= -github.com/otterize/intents-operator/src v0.0.0-20231121105340-b2baab573e58/go.mod h1:1V5AvkQodX1eaeZRGCCCSYrG6bN5qRVnUP3UaWAQV3Y= -github.com/otterize/intents-operator/src v0.0.0-20231121110025-8a9e88b1453c h1:cvXgq1ralsSYfjHa+D+TKM4ucsXwonDv9+goIj8g338= -github.com/otterize/intents-operator/src v0.0.0-20231121110025-8a9e88b1453c/go.mod h1:1V5AvkQodX1eaeZRGCCCSYrG6bN5qRVnUP3UaWAQV3Y= -github.com/otterize/intents-operator/src v0.0.0-20231121123451-d892801be004 h1:4Pn6poMytSoUxAUCgJAMHYrdZ00zZTZ77QkOFcQgL24= -github.com/otterize/intents-operator/src v0.0.0-20231121123451-d892801be004/go.mod h1:1V5AvkQodX1eaeZRGCCCSYrG6bN5qRVnUP3UaWAQV3Y= +github.com/otterize/intents-operator/src v0.0.0-20231206201014-048265d1db20 h1:hZxI7VvkXwZGSP1cTxyU+P2sIldm7C6RtGwHDglci64= +github.com/otterize/intents-operator/src v0.0.0-20231206201014-048265d1db20/go.mod h1:J8C5ur2cSznEaN811zNnVXrNzBM5L7JvFr1z16hOIS0= +github.com/otterize/intents-operator/src v0.0.0-20231207171817-9ec35db3bcb6 h1:btGWTxeTy0gF46vmgc9l/ezkSKqlo03t5X7Dc58VF2E= +github.com/otterize/intents-operator/src v0.0.0-20231207171817-9ec35db3bcb6/go.mod h1:J8C5ur2cSznEaN811zNnVXrNzBM5L7JvFr1z16hOIS0= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.1 h1:FyBdsRqqHH4LctMLL+BL2oGO+ONcIPwn96ctofCVtNE= @@ -446,8 +442,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/suessflorian/gqlfetch v0.6.0 h1:6e+Oe9mWbbjSmJez+6I4tyskQMy6lQlFFQYj64gaCQU= @@ -580,8 +577,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= -golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/src/operator/main.go b/src/operator/main.go index a6f862d..1468c1d 100644 --- a/src/operator/main.go +++ b/src/operator/main.go @@ -22,19 +22,20 @@ import ( "fmt" "github.com/bombsimon/logrusr/v3" certmanager "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + "github.com/otterize/credentials-operator/src/controllers/aws_iam/pods" + "github.com/otterize/credentials-operator/src/controllers/aws_iam/serviceaccount" + "github.com/otterize/credentials-operator/src/controllers/aws_iam/webhooks" "github.com/otterize/credentials-operator/src/controllers/certificates/otterizecertgen" "github.com/otterize/credentials-operator/src/controllers/certificates/spirecertgen" "github.com/otterize/credentials-operator/src/controllers/certmanageradapter" "github.com/otterize/credentials-operator/src/controllers/otterizeclient" "github.com/otterize/credentials-operator/src/controllers/poduserpassword" "github.com/otterize/credentials-operator/src/controllers/secrets" - "github.com/otterize/credentials-operator/src/controllers/serviceaccount" "github.com/otterize/credentials-operator/src/controllers/spireclient" "github.com/otterize/credentials-operator/src/controllers/spireclient/bundles" "github.com/otterize/credentials-operator/src/controllers/spireclient/entries" "github.com/otterize/credentials-operator/src/controllers/spireclient/svids" "github.com/otterize/credentials-operator/src/controllers/tls_pod" - "github.com/otterize/credentials-operator/src/controllers/webhooks" operatorwebhooks "github.com/otterize/intents-operator/src/operator/webhooks" "github.com/otterize/intents-operator/src/shared/awsagent" "github.com/otterize/intents-operator/src/shared/serviceidresolver" @@ -224,12 +225,18 @@ func main() { logrus.WithError(err).Error("failed to initialize AWS agent") os.Exit(1) } - serviceAccountReconciler := serviceaccount.NewServiceAccountReconciler(client, mgr.GetScheme(), awsAgent) + serviceAccountReconciler := serviceaccount.NewServiceAccountReconciler(client, awsAgent) if err = serviceAccountReconciler.SetupWithManager(mgr); err != nil { logrus.WithField("controller", "ServiceAccount").WithError(err).Error("unable to create controller") os.Exit(1) } + podCleanupReconciler := pods.NewPodAWSRoleCleanupReconciler(client) + if err = podCleanupReconciler.SetupWithManager(mgr); err != nil { + logrus.WithField("controller", "ServiceAccount").WithError(err).Error("unable to create controller") + os.Exit(1) + } + if selfSignedCert { logrus.Infoln("Creating self signing certs") certBundle, err := @@ -247,11 +254,10 @@ func main() { if err != nil { logrus.WithError(err).Fatal("updating validation webhook certificate failed") } + podAnnotatorWebhook := webhooks.NewServiceAccountAnnotatingPodWebhook(mgr, awsAgent) + mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{Handler: podAnnotatorWebhook}) } - podAnnotatorWebhook := webhooks.NewServiceAccountAnnotatingPodWebhook(mgr, awsAgent) - mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{Handler: podAnnotatorWebhook}) - } if certProvider != CertProviderNone {