diff --git a/src/operator/config/rbac/role.yaml b/src/operator/config/rbac/role.yaml index f3ca999..2fe3dd1 100644 --- a/src/operator/config/rbac/role.yaml +++ b/src/operator/config/rbac/role.yaml @@ -16,6 +16,17 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - get + - list + - patch + - update + - watch - apiGroups: - apps resources: diff --git a/src/operator/controllers/metadata/annotations.go b/src/operator/controllers/metadata/annotations.go index 8ebecbd..8561427 100644 --- a/src/operator/controllers/metadata/annotations.go +++ b/src/operator/controllers/metadata/annotations.go @@ -6,6 +6,9 @@ const ( TLSSecretNameAnnotation = "credentials-operator.otterize.com/tls-secret-name" TLSSecretNameAnnotationDeprecated = "spire-integration.otterize.com/tls-secret-name" + // ServiceAccountNameAnnotation is the name of the k8s service account that the operator will create + ServiceAccountNameAnnotation = "credentials-operator.otterize.com/service-account-name" + // DNSNamesAnnotation is a comma-separated list of additional dns names to be registered as part of the // SPIRE-server entry and encoded into the certificate data DNSNamesAnnotation = "credentials-operator.otterize.com/dns-names" diff --git a/src/operator/controllers/metadata/labels.go b/src/operator/controllers/metadata/labels.go index 9a33c2a..e744126 100644 --- a/src/operator/controllers/metadata/labels.go +++ b/src/operator/controllers/metadata/labels.go @@ -7,7 +7,10 @@ const ( // See documentation for intents.otterize.com/service-name annotation for information re service name resolution. RegisteredServiceNameLabel = "credentials-operator.otterize.com/registered-service-name" - // SecretTypeLabel is used to label secrets generated by the spire-integration-operator with their secret type. + // SecretTypeLabel is used to label secrets generated by the credentials-operator with their secret type. // This is used to detect which secrets are managed by this operator. 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" ) diff --git a/src/operator/controllers/pod_controller.go b/src/operator/controllers/pod_controller.go index dd3bf59..e39d719 100644 --- a/src/operator/controllers/pod_controller.go +++ b/src/operator/controllers/pod_controller.go @@ -47,6 +47,10 @@ type SecretsManager interface { RefreshTLSSecrets(ctx context.Context) error } +type ServiceAccountEnsurer interface { + EnsureServiceAccount(ctx context.Context, pod *corev1.Pod) error +} + // PodReconciler reconciles a Pod object type PodReconciler struct { client.Client @@ -56,16 +60,18 @@ type PodReconciler struct { serviceIdResolver *serviceidresolver.Resolver eventRecorder record.EventRecorder registerOnlyPodsWithSecretAnnotation bool + serviceAccountEnsurer ServiceAccountEnsurer } // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=core,resources=secrets,verbs=create;get;list;update;patch;watch // +kubebuilder:rbac:groups=apps,resources=replicasets;daemonsets;statefulsets;deployments,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups="",resources=events,verbs=get;update;patch;list;watch;create +// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;update;patch;list;watch;create func NewPodReconciler(client client.Client, scheme *runtime.Scheme, workloadRegistry WorkloadRegistry, secretsManager SecretsManager, serviceIdResolver *serviceidresolver.Resolver, eventRecorder record.EventRecorder, - registerOnlyPodsWithSecretAnnotation bool) *PodReconciler { + ServiceAccountEnsurer ServiceAccountEnsurer, registerOnlyPodsWithSecretAnnotation bool) *PodReconciler { return &PodReconciler{ Client: client, scheme: scheme, @@ -74,6 +80,7 @@ func NewPodReconciler(client client.Client, scheme *runtime.Scheme, workloadRegi serviceIdResolver: serviceIdResolver, eventRecorder: eventRecorder, registerOnlyPodsWithSecretAnnotation: registerOnlyPodsWithSecretAnnotation, + serviceAccountEnsurer: ServiceAccountEnsurer, } } @@ -177,6 +184,11 @@ func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R return ctrl.Result{}, nil } + err := r.serviceAccountEnsurer.EnsureServiceAccount(ctx, pod) + if err != nil { + return ctrl.Result{}, err + } + if !r.shouldRegisterEntryForPod(pod) { return ctrl.Result{}, nil } diff --git a/src/operator/controllers/pod_controller_test.go b/src/operator/controllers/pod_controller_test.go index 247678b..1a7f89f 100644 --- a/src/operator/controllers/pod_controller_test.go +++ b/src/operator/controllers/pod_controller_test.go @@ -8,6 +8,7 @@ import ( "github.com/otterize/credentials-operator/src/controllers/secrets/types" "github.com/otterize/credentials-operator/src/mocks/controller-runtime/client" mock_secrets "github.com/otterize/credentials-operator/src/mocks/controllers/secrets" + mockserviceaccounts "github.com/otterize/credentials-operator/src/mocks/controllers/serviceaccounts" mock_entries "github.com/otterize/credentials-operator/src/mocks/entries" mock_record "github.com/otterize/credentials-operator/src/mocks/eventrecorder" mock_spireclient "github.com/otterize/credentials-operator/src/mocks/spireclient" @@ -29,12 +30,13 @@ import ( type PodControllerSuite struct { suite.Suite - controller *gomock.Controller - client *mock_client.MockClient - spireClient *mock_spireclient.MockServerClient - entriesRegistry *mock_entries.MockWorkloadRegistry - secretsManager *mock_secrets.MockSecretsManager - podReconciler *PodReconciler + controller *gomock.Controller + client *mock_client.MockClient + spireClient *mock_spireclient.MockServerClient + entriesRegistry *mock_entries.MockWorkloadRegistry + secretsManager *mock_secrets.MockSecretsManager + podReconciler *PodReconciler + ServiceAccountEnsurer *mockserviceaccounts.MockServiceAccountEnsurer } type PodControllerSuiteWithoutEventRecorder struct { @@ -51,12 +53,14 @@ func (s *PodControllerSuiteWithoutEventRecorder) SetupTest() { eventRecorder := mock_record.NewMockEventRecorder(s.controller) eventRecorder.EXPECT().Event(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() eventRecorder.EXPECT().Eventf(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + s.ServiceAccountEnsurer = mockserviceaccounts.NewMockServiceAccountEnsurer(s.controller) + s.ServiceAccountEnsurer.EXPECT().EnsureServiceAccount(gomock.Any(), gomock.Any()).AnyTimes() scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) s.client.EXPECT().Scheme().Return(scheme).AnyTimes() s.podReconciler = NewPodReconciler(s.client, nil, s.entriesRegistry, s.secretsManager, - serviceIdResolver, eventRecorder, false) + serviceIdResolver, eventRecorder, s.ServiceAccountEnsurer, false) } type ObjectNameMatcher struct { @@ -256,8 +260,10 @@ func (s *PodControllerSuiteWithEventRecorder) SetupTest() { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) s.client.EXPECT().Scheme().Return(scheme).AnyTimes() + s.ServiceAccountEnsurer = mockserviceaccounts.NewMockServiceAccountEnsurer(s.controller) + s.ServiceAccountEnsurer.EXPECT().EnsureServiceAccount(gomock.Any(), gomock.Any()).AnyTimes() s.podReconciler = NewPodReconciler(s.client, nil, s.entriesRegistry, s.secretsManager, - serviceIdResolver, s.eventRecorder, false) + serviceIdResolver, s.eventRecorder, s.ServiceAccountEnsurer, false) } func (s *PodControllerSuiteWithEventRecorder) TestController_Reconcile_DeprecatedAnnotations() { diff --git a/src/operator/controllers/serviceaccount/ensurer.go b/src/operator/controllers/serviceaccount/ensurer.go new file mode 100644 index 0000000..2edb4ac --- /dev/null +++ b/src/operator/controllers/serviceaccount/ensurer.go @@ -0,0 +1,82 @@ +package serviceaccount + +import ( + "context" + "fmt" + "github.com/otterize/credentials-operator/src/controllers/metadata" + "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + ReasonCreateServiceAccount = "CreateServiceAccount" + ReasonCreatingServiceAccountFailed = "CreatingServiceAccountFailed" + ReasonCreateServiceAccountSkipped = "CreatingServiceAccountSkipped" +) + +type Ensurer struct { + client.Client + recorder record.EventRecorder +} + +func NewServiceAccountEnsurer(client client.Client, eventRecorder record.EventRecorder) *Ensurer { + return &Ensurer{Client: client, recorder: eventRecorder} +} + +func isServiceAccountNameValid(name string) bool { + return len(validation.IsDNS1123Subdomain(name)) == 0 +} + +func (e *Ensurer) EnsureServiceAccount(ctx context.Context, pod *v1.Pod) error { + if pod.Annotations == nil { + return nil + } + serviceAccountName, annotationExists := pod.Annotations[metadata.ServiceAccountNameAnnotation] + if !annotationExists { + logrus.Debugf("pod %s does'nt have service account annotation, skipping ensure service account", pod) + return nil + } + + if !isServiceAccountNameValid(serviceAccountName) { + err := fmt.Errorf("service account name %s is invalid according to 'RFC 1123 subdomain'. skipping service account ensure for pod %s", serviceAccountName, pod) + logrus.Warningf(err.Error()) + e.recorder.Eventf(pod, v1.EventTypeWarning, ReasonCreatingServiceAccountFailed, err.Error()) + return err + } + + serviceAccount := v1.ServiceAccount{} + err := e.Client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: serviceAccountName}, &serviceAccount) + if err != nil && !apierrors.IsNotFound(err) { + logrus.Errorf("failed get service accounts: %s", err.Error()) + e.recorder.Eventf(pod, v1.EventTypeWarning, ReasonCreatingServiceAccountFailed, "Failed creating service account: %s", err.Error()) + return err + } else if err == nil { + logrus.Debugf("service account %s already exists, skipping service account creation", serviceAccountName) + e.recorder.Eventf(pod, v1.EventTypeNormal, ReasonCreateServiceAccountSkipped, "service account %s already exists, skipping service account creation", serviceAccountName) + return nil + } + + logrus.Infof("creating service account named %s for pod %s", serviceAccountName, pod) + if err := e.createServiceAccount(ctx, serviceAccountName, pod); err != nil { + logrus.Errorf("failed creating service account for pod %s: %s", pod, err.Error()) + e.recorder.Eventf(pod, v1.EventTypeWarning, ReasonCreatingServiceAccountFailed, "Failed creating service account: %s", err.Error()) + return err + } + e.recorder.Eventf(pod, v1.EventTypeNormal, ReasonCreateServiceAccount, "Successfully created service account: %s", serviceAccountName) + logrus.Infof("successfuly created service account named %s for pod %s", serviceAccountName, pod) + + return nil +} + +func (e *Ensurer) createServiceAccount(ctx context.Context, serviceAccountName string, pod *v1.Pod) error { + serviceAccount := v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Name: serviceAccountName, Namespace: pod.Namespace, Labels: map[string]string{metadata.OtterizeServiceAccountLabel: serviceAccountName}}, + } + return e.Client.Create(ctx, &serviceAccount) +} diff --git a/src/operator/controllers/serviceaccount/ensurer_test.go b/src/operator/controllers/serviceaccount/ensurer_test.go new file mode 100644 index 0000000..d38e096 --- /dev/null +++ b/src/operator/controllers/serviceaccount/ensurer_test.go @@ -0,0 +1,144 @@ +package serviceaccount + +import ( + "context" + "errors" + "fmt" + "github.com/otterize/credentials-operator/src/controllers/metadata" + mock_client "github.com/otterize/credentials-operator/src/mocks/controller-runtime/client" + mock_record "github.com/otterize/credentials-operator/src/mocks/eventrecorder" + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" + v1 "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" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "net/http" + "testing" +) + +type serviceAccountMatcher struct { + Name string + Namespace string +} + +func (m *serviceAccountMatcher) String() string { + return fmt.Sprintf("expected Name: %s Namespace: %s", m.Name, m.Namespace) +} + +func (m *serviceAccountMatcher) Matches(x interface{}) bool { + sa := x.(*v1.ServiceAccount) + return sa.Name == m.Name && sa.Namespace == m.Namespace +} + +type PodServiceAccountEnsurerSuite struct { + suite.Suite + controller *gomock.Controller + client *mock_client.MockClient + ServiceAccountEnsurer *Ensurer + mockEventRecorder *mock_record.MockEventRecorder +} + +func (s *PodServiceAccountEnsurerSuite) SetupTest() { + s.controller = gomock.NewController(s.T()) + s.client = mock_client.NewMockClient(s.controller) + + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + s.client.EXPECT().Scheme().Return(scheme).AnyTimes() + s.mockEventRecorder = mock_record.NewMockEventRecorder(s.controller) + s.ServiceAccountEnsurer = NewServiceAccountEnsurer(s.client, s.mockEventRecorder) +} + +func (s *PodServiceAccountEnsurerSuite) TestCreate() { + serviceAccountName := "cool.name" + annotations := map[string]string{metadata.ServiceAccountNameAnnotation: serviceAccountName} + namespace := "namespace" + s.client.EXPECT().Get(gomock.Any(), gomock.Eq(types.NamespacedName{Name: serviceAccountName, Namespace: namespace}), gomock.AssignableToTypeOf(&v1.ServiceAccount{})). + Return( + &k8serrors.StatusError{ + ErrStatus: metav1.Status{Status: metav1.StatusFailure, Code: http.StatusNotFound, Reason: metav1.StatusReasonNotFound}, + }) + + s.client.EXPECT().Create(gomock.Any(), &serviceAccountMatcher{Name: serviceAccountName, Namespace: namespace}) + s.mockEventRecorder.EXPECT().Eventf(gomock.Any(), gomock.Eq(v1.EventTypeNormal), gomock.Eq(ReasonCreateServiceAccount), gomock.Any(), gomock.Any()) + err := s.ServiceAccountEnsurer.EnsureServiceAccount(context.Background(), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "Pod", Namespace: namespace, Annotations: annotations}}) + s.Require().NoError(err) + +} + +func (s *PodServiceAccountEnsurerSuite) TestDoesntCreateWhenFound() { + serviceAccountName := "cool.name" + annotations := map[string]string{metadata.ServiceAccountNameAnnotation: serviceAccountName} + namespace := "namespace" + s.client.EXPECT().Get(gomock.Any(), gomock.Eq(types.NamespacedName{Name: serviceAccountName, Namespace: namespace}), gomock.AssignableToTypeOf(&v1.ServiceAccount{})). + Return(nil) + + s.mockEventRecorder.EXPECT().Eventf(gomock.Any(), gomock.Eq(v1.EventTypeNormal), gomock.Eq(ReasonCreateServiceAccountSkipped), gomock.Any(), gomock.Any()) + + err := s.ServiceAccountEnsurer.EnsureServiceAccount(context.Background(), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "Pod", Namespace: namespace, Annotations: annotations}}) + s.Require().NoError(err) + +} + +func (s *PodServiceAccountEnsurerSuite) TestDoesntCreateWhenInvalidName() { + // Name with caps RFC 1123 subdomain + annotations := map[string]string{metadata.ServiceAccountNameAnnotation: "NameWithCapitalLetters"} + s.mockEventRecorder.EXPECT().Eventf(gomock.Any(), gomock.Eq(v1.EventTypeWarning), gomock.Eq(ReasonCreatingServiceAccountFailed), gomock.Any()) + err := s.ServiceAccountEnsurer.EnsureServiceAccount(context.Background(), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "Pod", Namespace: "namespace", Annotations: annotations}}) + s.Require().Error(err) + + // Very long Name (>253) + annotations = map[string]string{metadata.ServiceAccountNameAnnotation: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} + s.mockEventRecorder.EXPECT().Eventf(gomock.Any(), gomock.Eq(v1.EventTypeWarning), gomock.Eq(ReasonCreatingServiceAccountFailed), gomock.Any()) + err = s.ServiceAccountEnsurer.EnsureServiceAccount(context.Background(), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "Pod", Namespace: "namespace", Annotations: annotations}}) + s.Require().Error(err) + + // Name with / + annotations = map[string]string{metadata.ServiceAccountNameAnnotation: "name/asd"} + s.mockEventRecorder.EXPECT().Eventf(gomock.Any(), gomock.Eq(v1.EventTypeWarning), gomock.Eq(ReasonCreatingServiceAccountFailed), gomock.Any()) + err = s.ServiceAccountEnsurer.EnsureServiceAccount(context.Background(), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "Pod", Namespace: "namespace", Annotations: annotations}}) + s.Require().Error(err) + +} + +func (s *PodServiceAccountEnsurerSuite) TestDoesntCreateWhenNoAnnotation() { + err := s.ServiceAccountEnsurer.EnsureServiceAccount(context.Background(), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "Pod", Namespace: "namespace"}}) + s.Require().NoError(err) +} + +func (s *PodServiceAccountEnsurerSuite) TestEventOnErrorListing() { + serviceAccountName := "cool.name" + annotations := map[string]string{metadata.ServiceAccountNameAnnotation: serviceAccountName} + namespace := "namespace" + s.client.EXPECT().Get(gomock.Any(), gomock.Eq(types.NamespacedName{Name: serviceAccountName, Namespace: namespace}), gomock.AssignableToTypeOf(&v1.ServiceAccount{})). + Return(errors.New("unexpected error")) + + s.mockEventRecorder.EXPECT().Eventf(gomock.Any(), gomock.Eq(v1.EventTypeWarning), gomock.Eq(ReasonCreatingServiceAccountFailed), gomock.Any(), gomock.Any()) + err := s.ServiceAccountEnsurer.EnsureServiceAccount(context.Background(), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "Pod", Namespace: namespace, Annotations: annotations}}) + s.Require().Error(err) + +} + +func (s *PodServiceAccountEnsurerSuite) TestEventOnErrorCreating() { + serviceAccountName := "cool.name" + annotations := map[string]string{metadata.ServiceAccountNameAnnotation: serviceAccountName} + namespace := "namespace" + s.client.EXPECT().Get(gomock.Any(), gomock.Eq(types.NamespacedName{Name: serviceAccountName, Namespace: namespace}), gomock.AssignableToTypeOf(&v1.ServiceAccount{})). + Return( + &k8serrors.StatusError{ + ErrStatus: metav1.Status{Status: metav1.StatusFailure, Code: http.StatusNotFound, Reason: metav1.StatusReasonNotFound}, + }) + + s.client.EXPECT().Create(gomock.Any(), &serviceAccountMatcher{Name: serviceAccountName, Namespace: namespace}).Return(errors.New("unexpected error")) + s.mockEventRecorder.EXPECT().Eventf(gomock.Any(), gomock.Eq(v1.EventTypeWarning), gomock.Eq(ReasonCreatingServiceAccountFailed), gomock.Any(), gomock.Any()) + err := s.ServiceAccountEnsurer.EnsureServiceAccount(context.Background(), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "Pod", Namespace: namespace, Annotations: annotations}}) + s.Require().Error(err) +} + +func TestPodServiceAccountEnsurerSuite(t *testing.T) { + suite.Run(t, new(PodServiceAccountEnsurerSuite)) +} diff --git a/src/operator/generate.go b/src/operator/generate.go index a1eba56..4e74cfd 100644 --- a/src/operator/generate.go +++ b/src/operator/generate.go @@ -9,5 +9,6 @@ package main //go:generate go run go.uber.org/mock/mockgen@v0.2.0 -package mock_entries -destination=mocks/entries/mock.go github.com/otterize/credentials-operator/src/controllers WorkloadRegistry //go:generate go run go.uber.org/mock/mockgen@v0.2.0 -destination=mocks/spireclient/svids/mock.go github.com/otterize/credentials-operator/src/controllers/spireclient/svids Store //go:generate go run go.uber.org/mock/mockgen@v0.2.0 -package mock_secrets -destination=mocks/controllers/secrets/mock.go github.com/otterize/credentials-operator/src/controllers SecretsManager +//go:generate go run go.uber.org/mock/mockgen@v0.2.0 -package mockserviceaccounts -destination=mocks/controllers/serviceaccounts/mock.go github.com/otterize/credentials-operator/src/controllers ServiceAccountEnsurer //go:generate go run go.uber.org/mock/mockgen@v0.2.0 -destination=mocks/eventrecorder/mock.go k8s.io/client-go/tools/record EventRecorder //go:generate go run go.uber.org/mock/mockgen@v0.2.0 -destination=mocks/serviceidresolver/mock.go github.com/otterize/credentials-operator/src/controllers/secrets/types ServiceIdResolver diff --git a/src/operator/go.mod b/src/operator/go.mod index d8e7229..e95f464 100644 --- a/src/operator/go.mod +++ b/src/operator/go.mod @@ -8,7 +8,6 @@ require ( github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a github.com/bombsimon/logrusr/v3 v3.0.0 github.com/cert-manager/cert-manager v1.12.3 - github.com/go-logr/logr v1.2.4 github.com/otterize/intents-operator/src v0.0.0-20230907073856-90e27c4e09a0 github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.1 github.com/samber/lo v1.33.0 @@ -32,6 +31,9 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/DataDog/datadog-go v3.2.0+incompatible // indirect github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/alexflint/go-arg v1.4.2 // indirect + github.com/alexflint/go-scalar v1.0.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -43,6 +45,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-ldap/ldap/v3 v3.4.4 // indirect + github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect diff --git a/src/operator/go.sum b/src/operator/go.sum index fcb1ee6..3db7ed1 100644 --- a/src/operator/go.sum +++ b/src/operator/go.sum @@ -49,12 +49,17 @@ github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2y github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexflint/go-arg v1.4.2 h1:lDWZAXxpAnZUq4qwb86p/3rIJJ2Li81EoMbTMujhVa0= +github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= +github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70= +github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= github.com/amit7itz/goset v1.2.1 h1:usFphDJfZgwnqfbKT8zI+2juuOgsZ6O8UA7NMRUVG7s= github.com/amit7itz/goset v1.2.1/go.mod h1:i8ni2YcxUMAwLBOkHWpy3glFviYdTcWqCvFgp91EMGI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= diff --git a/src/operator/main.go b/src/operator/main.go index 1acd371..c2e34a6 100644 --- a/src/operator/main.go +++ b/src/operator/main.go @@ -28,6 +28,7 @@ import ( "github.com/otterize/credentials-operator/src/controllers/certmanageradapter" "github.com/otterize/credentials-operator/src/controllers/otterizeclient" "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" @@ -187,8 +188,9 @@ func main() { eventRecorder, certManagerIssuer, certManagerUseClusterIssuer) } - podReconciler := controllers.NewPodReconciler(mgr.GetClient(), mgr.GetScheme(), workloadRegistry, secretsManager, - serviceIdResolver, eventRecorder, provider == ProviderCloud) + client := mgr.GetClient() + podReconciler := controllers.NewPodReconciler(client, mgr.GetScheme(), workloadRegistry, secretsManager, + serviceIdResolver, eventRecorder, serviceaccount.NewServiceAccountEnsurer(client, eventRecorder), provider == ProviderCloud) if err = podReconciler.SetupWithManager(mgr); err != nil { logrus.WithField("controller", "Pod").WithError(err).Error("unable to create controller") diff --git a/src/operator/mocks/controllers/serviceaccounts/mock.go b/src/operator/mocks/controllers/serviceaccounts/mock.go new file mode 100644 index 0000000..2d08aa3 --- /dev/null +++ b/src/operator/mocks/controllers/serviceaccounts/mock.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/otterize/credentials-operator/src/controllers (interfaces: ServiceAccountEnsurer) + +// Package mockserviceaccounts is a generated GoMock package. +package mockserviceaccounts + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + v1 "k8s.io/api/core/v1" +) + +// MockServiceAccountEnsurer is a mock of ServiceAccountEnsurer interface. +type MockServiceAccountEnsurer struct { + ctrl *gomock.Controller + recorder *MockServiceAccountEnsurerMockRecorder +} + +// MockServiceAccountEnsurerMockRecorder is the mock recorder for MockServiceAccountEnsurer. +type MockServiceAccountEnsurerMockRecorder struct { + mock *MockServiceAccountEnsurer +} + +// NewMockServiceAccountEnsurer creates a new mock instance. +func NewMockServiceAccountEnsurer(ctrl *gomock.Controller) *MockServiceAccountEnsurer { + mock := &MockServiceAccountEnsurer{ctrl: ctrl} + mock.recorder = &MockServiceAccountEnsurerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockServiceAccountEnsurer) EXPECT() *MockServiceAccountEnsurerMockRecorder { + return m.recorder +} + +// EnsureServiceAccount mocks base method. +func (m *MockServiceAccountEnsurer) EnsureServiceAccount(arg0 context.Context, arg1 *v1.Pod) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnsureServiceAccount", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// EnsureServiceAccount indicates an expected call of EnsureServiceAccount. +func (mr *MockServiceAccountEnsurerMockRecorder) EnsureServiceAccount(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureServiceAccount", reflect.TypeOf((*MockServiceAccountEnsurer)(nil).EnsureServiceAccount), arg0, arg1) +}