-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support service account creation for pods with "credentials-operator.…
…otterize.com/service-account-name" annotation (#82)
- Loading branch information
Showing
12 changed files
with
335 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
144 changes: 144 additions & 0 deletions
144
src/operator/controllers/serviceaccount/ensurer_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.