Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support service account creation #82

Merged
merged 4 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/operator/config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ rules:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- serviceaccounts
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- apps
resources:
Expand Down
3 changes: 3 additions & 0 deletions src/operator/controllers/metadata/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 13 additions & 1 deletion src/operator/controllers/pod_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -74,6 +80,7 @@ func NewPodReconciler(client client.Client, scheme *runtime.Scheme, workloadRegi
serviceIdResolver: serviceIdResolver,
eventRecorder: eventRecorder,
registerOnlyPodsWithSecretAnnotation: registerOnlyPodsWithSecretAnnotation,
serviceAccountEnsurer: ServiceAccountEnsurer,
}
}

Expand Down Expand Up @@ -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
}
Expand Down
22 changes: 14 additions & 8 deletions src/operator/controllers/pod_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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() {
Expand Down
57 changes: 57 additions & 0 deletions src/operator/controllers/serviceaccount/ensurer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package serviceaccount

import (
"context"
"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"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type Ensurer struct {
client.Client
}

func NewServiceAccountEnsurer(client client.Client) *Ensurer {
return &Ensurer{client}
}

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("skiping ensure service account for pod %s", pod)
return nil
}

if !isServiceAccountNameValid(serviceAccountName) {
logrus.Debugf("service account name %s is invalid according to 'RFC 1123 subdomain'. skipping service account ensure for pod %s", serviceAccountName, pod)
omris94 marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

serviceAccount := v1.ServiceAccount{}
err := e.Client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: serviceAccountName}, &serviceAccount)
omris94 marked this conversation as resolved.
Show resolved Hide resolved
if apierrors.IsNotFound(err) {
return e.createServiceAccount(ctx, serviceAccountName, pod)
} else if err != nil {
return err
}
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},
}
return e.Client.Create(ctx, &serviceAccount)
}
105 changes: 105 additions & 0 deletions src/operator/controllers/serviceaccount/ensurer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package serviceaccount

import (
"context"
"fmt"
"github.com/otterize/credentials-operator/src/controllers/metadata"
mock_client "github.com/otterize/credentials-operator/src/mocks/controller-runtime/client"
"github.com/stretchr/testify/suite"
"go.uber.org/mock/gomock"
v1 "k8s.io/api/core/v1"
"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
}

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.ServiceAccountEnsurer = NewServiceAccountEnsurer(s.client)
}

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(
&errors.StatusError{
ErrStatus: metav1.Status{Status: metav1.StatusFailure, Code: http.StatusNotFound, Reason: metav1.StatusReasonNotFound},
})

s.client.EXPECT().Create(gomock.Any(), &serviceAccountMatcher{Name: serviceAccountName, Namespace: namespace})
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)

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
annotations := map[string]string{metadata.ServiceAccountNameAnnotation: "NameWithCapitalLetters"}
err := s.ServiceAccountEnsurer.EnsureServiceAccount(context.Background(), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "Pod", Namespace: "namespace", Annotations: annotations}})
s.Require().NoError(err)

// Very long Name (>253)
annotations = map[string]string{metadata.ServiceAccountNameAnnotation: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
err = s.ServiceAccountEnsurer.EnsureServiceAccount(context.Background(), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "Pod", Namespace: "namespace", Annotations: annotations}})
s.Require().NoError(err)

// Name with /
annotations = map[string]string{metadata.ServiceAccountNameAnnotation: "name/asd"}
err = s.ServiceAccountEnsurer.EnsureServiceAccount(context.Background(), &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "Pod", Namespace: "namespace", Annotations: annotations}})
s.Require().NoError(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 TestPodServiceAccountEnsurerSuite(t *testing.T) {
suite.Run(t, new(PodServiceAccountEnsurerSuite))
}
1 change: 1 addition & 0 deletions src/operator/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion src/operator/go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/operator/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions src/operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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), provider == ProviderCloud)

if err = podReconciler.SetupWithManager(mgr); err != nil {
logrus.WithField("controller", "Pod").WithError(err).Error("unable to create controller")
Expand Down
Loading