diff --git a/api/v1alpha1/spinapp_types.go b/api/v1alpha1/spinapp_types.go index 404c294..8eca94e 100644 --- a/api/v1alpha1/spinapp_types.go +++ b/api/v1alpha1/spinapp_types.go @@ -86,6 +86,11 @@ type SpinAppSpec struct { // If this is not provided all components are executed. // +kubebuilder:validation:MinItems:=1 Components []string `json:"components,omitempty"` + + // ServiceAccountName is the name of the Kubernetes service account to use for the pod. + // If not specified, the default service account will be used. + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` } // SpinAppStatus defines the observed state of SpinApp diff --git a/config/crd/bases/core.spinkube.dev_spinapps.yaml b/config/crd/bases/core.spinkube.dev_spinapps.yaml index 5f74967..02695a7 100644 --- a/config/crd/bases/core.spinkube.dev_spinapps.yaml +++ b/config/crd/bases/core.spinkube.dev_spinapps.yaml @@ -532,6 +532,11 @@ spec: type: object type: array type: object + serviceAccountName: + description: |- + ServiceAccountName is the name of the Kubernetes service account to use for the pod. + If not specified, the default service account will be used. + type: string serviceAnnotations: additionalProperties: type: string diff --git a/go.sum b/go.sum index 96381a2..910e346 100644 --- a/go.sum +++ b/go.sum @@ -68,7 +68,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= @@ -214,7 +213,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= diff --git a/internal/controller/spinapp_controller.go b/internal/controller/spinapp_controller.go index 755a5ca..afcf38b 100644 --- a/internal/controller/spinapp_controller.go +++ b/internal/controller/spinapp_controller.go @@ -444,6 +444,8 @@ func constructDeployment(ctx context.Context, app *spinv1alpha1.SpinApp, config labels := constructAppLabels(app) + serviceAccountName := getServiceAccountName(ctx, app) + var container corev1.Container if config.RuntimeClassName != nil { container = corev1.Container{ @@ -508,10 +510,11 @@ func constructDeployment(ctx context.Context, app *spinv1alpha1.SpinApp, config Annotations: templateAnnotations, }, Spec: corev1.PodSpec{ - RuntimeClassName: config.RuntimeClassName, - Containers: []corev1.Container{container}, - ImagePullSecrets: app.Spec.ImagePullSecrets, - Volumes: volumes, + RuntimeClassName: config.RuntimeClassName, + ServiceAccountName: serviceAccountName, + Containers: []corev1.Container{container}, + ImagePullSecrets: app.Spec.ImagePullSecrets, + Volumes: volumes, }, }, }, @@ -530,6 +533,25 @@ func constructDeployment(ctx context.Context, app *spinv1alpha1.SpinApp, config return dep, nil } +// getServiceAccountName returns the service account name to use for the deployment. +// If serviceAccountName is specified on the SpinApp, it returns that value. +// Otherwise, it returns "default" which is the Kubernetes default. +func getServiceAccountName(ctx context.Context, app *spinv1alpha1.SpinApp) string { + log := logging.FromContext(ctx).WithValues("component", "getServiceAccountName") + + log.Debug("Determining service account name", + "app", app.Name, + "namespace", app.Namespace, + "serviceAccountNameInSpec", app.Spec.ServiceAccountName) + + if app.Spec.ServiceAccountName != "" { + log.Debug("Using service account from SpinApp", "serviceAccountName", app.Spec.ServiceAccountName) + return app.Spec.ServiceAccountName + } + + return "default" +} + // findDeploymentForApp finds the deployment for a SpinApp. func (r *SpinAppReconciler) findDeploymentForApp(ctx context.Context, app *spinv1alpha1.SpinApp) (*appsv1.Deployment, error) { var deployment appsv1.Deployment diff --git a/internal/controller/spinapp_controller_test.go b/internal/controller/spinapp_controller_test.go index 54d2eb5..b2cfa4a 100644 --- a/internal/controller/spinapp_controller_test.go +++ b/internal/controller/spinapp_controller_test.go @@ -630,3 +630,64 @@ func TestReconcile_Integration_Deployment_SpinCAInjection(t *testing.T) { cancelFunc() wg.Wait() } + +func TestReconcile_Integration_Deployment_ServiceAccountName(t *testing.T) { + t.Parallel() + + envTest, mgr, _ := setupController(t) + + ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFunc() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + require.NoError(t, mgr.Start(ctx)) + wg.Done() + }() + + executor := &spinv1alpha1.SpinAppExecutor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "executor", + Namespace: "default", + }, + Spec: spinv1alpha1.SpinAppExecutorSpec{ + CreateDeployment: true, + DeploymentConfig: &spinv1alpha1.ExecutorDeploymentConfig{ + RuntimeClassName: generics.Ptr("foobar"), + }, + }, + } + + require.NoError(t, envTest.k8sClient.Create(ctx, executor)) + + spinApp := &spinv1alpha1.SpinApp{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app", + Namespace: "default", + }, + Spec: spinv1alpha1.SpinAppSpec{ + Executor: "executor", + Image: "ghcr.io/radu-matei/perftest:v1", + ServiceAccountName: "my-service-account", + }, + } + + require.NoError(t, envTest.k8sClient.Create(ctx, spinApp)) + + var deployment appsv1.Deployment + require.Eventually(t, func() bool { + err := envTest.k8sClient.Get(ctx, + types.NamespacedName{ + Namespace: "default", + Name: spinApp.Name}, + &deployment) + return err == nil + }, 3*time.Second, 100*time.Millisecond) + + require.Equal(t, "my-service-account", deployment.Spec.Template.Spec.ServiceAccountName) + + // Terminate the context to force the manager to shut down. + cancelFunc() + wg.Wait() +} diff --git a/internal/webhook/spinapp_validating.go b/internal/webhook/spinapp_validating.go index 4f19f60..3947ea8 100644 --- a/internal/webhook/spinapp_validating.go +++ b/internal/webhook/spinapp_validating.go @@ -64,6 +64,7 @@ func (v *SpinAppValidator) validateSpinApp(ctx context.Context, spinApp *spinv1a if err := validateAnnotations(spinApp.Spec, executor); err != nil { allErrs = append(allErrs, err) } + if len(allErrs) == 0 { return nil }