Repository: numaproj/numaflow (go)

Issue:
ServiceAccountName incorrectly configured in case vertex template exist
**Describe the bug**

In case the vertex template is configured, the `serviceAccountName` isn't respected and is set to `default`.


**To Reproduce**

A sample pipeline - in this case the `cat` vertice should be configured with serviceAccountName as `test-two` instead of `default`.

```yaml
apiVersion: numaflow.numaproj.io/v1alpha1
kind: Pipeline
metadata:
  name: simple-pipeline
spec:
  templates:
    vertex:
      metadata:
        annotations:
          cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
      tolerations:
        - key: "kubernetes.azure.com/scalesetpriority"
          operator: "Equal"
          value: "spot"
          effect: "NoSchedule"
  vertices:
    - name: in
      source:
        # A self data generating source
        generator:
          rpu: 5
          duration: 1s
    - name: cat
      serviceAccountName: "test-two"
      udf:
        builtin:
          name: cat # A built-in UDF which simply cats the message
    - name: out
      sink:
        # A simple log printing sink
        log: {}
  edges:
    - from: in
      to: cat
    - from: cat
      to: out
```

**Expected behavior**
The serviceAccountName at the vertice level should be adhered

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Environment (please complete the following information):**

- Kubernetes: v1.29.2
- Numaflow: v1.1.5

**Additional context**
Add any other context about the problem here.

---

>For quick help and support, join our [slack](https://numaproj.slack.com/join/shared_invite/zt-19svuv47m-YKHhsQ~~KK9mBv1E7pNzfg#/shared-invite/email) channel.

<!-- Issue Author: Don't delete this message to encourage other users to support your issue! -->

**Message from the maintainers**:

Impacted by this bug? Give it a 👍. We often sort issues this way to know what to prioritize.


Pull request:
fix: podSpec incorrectly configured in case template exist
closes #1514

With this change PodSpec fields are assigned with values from template only if they are null/empty.

<!--

Before you push your changes:

* Run `make pre-push -B` to fix codegen and lint problems.

Then, you MUST:

* Sign-off your commit (otherwise the DCO check will fail).
* Use [a conventional commit message](https://www.conventionalcommits.org/en/v1.0.0/) (otherwise the commit message check will fail).

If you did not do this, reset all your commit and replace them with a single commit:

```
git reset HEAD~1 ;# change 1 to how many commits you made
git commit --signoff -m 'feat: my feat. Fixes #1234'
```

When creating your PR: 

* Make sure that "Fixes #" or "Closes #" is in both the PR title (for release notes) and description (to automatically link and close the issue).
* Say how you tested your changes. If you changed the UI, attach screenshots.
* Set your PR as a draft initially.
* Your PR needs to pass the required checks before it can be approved. 
* Once required tests have passed, mark your PR "Ready for review".

If changes were requested, once you've made them, you MUST dismiss the review to get it reviewed again.

-->


Patch:
diff --git a/pkg/apis/numaflow/v1alpha1/pod_template.go b/pkg/apis/numaflow/v1alpha1/pod_template.go
index 7e325752..442ede94 100644
--- a/pkg/apis/numaflow/v1alpha1/pod_template.go
+++ b/pkg/apis/numaflow/v1alpha1/pod_template.go
@@ -94,18 +94,42 @@ type AbstractPodTemplate struct {
 
 // ApplyToPodSpec updates the PodSpec with the values in the AbstractPodTemplate
 func (apt *AbstractPodTemplate) ApplyToPodSpec(ps *corev1.PodSpec) {
-	ps.NodeSelector = apt.NodeSelector
-	ps.Tolerations = apt.Tolerations
-	ps.SecurityContext = apt.SecurityContext
-	ps.ImagePullSecrets = apt.ImagePullSecrets
-	ps.PriorityClassName = apt.PriorityClassName
-	ps.Priority = apt.Priority
-	ps.Affinity = apt.Affinity
-	ps.ServiceAccountName = apt.ServiceAccountName
-	ps.RuntimeClassName = apt.RuntimeClassName
-	ps.AutomountServiceAccountToken = apt.AutomountServiceAccountToken
-	ps.DNSPolicy = apt.DNSPolicy
-	ps.DNSConfig = apt.DNSConfig
+	if len(ps.NodeSelector) == 0 {
+		ps.NodeSelector = apt.NodeSelector
+	}
+	if len(ps.Tolerations) == 0 {
+		ps.Tolerations = apt.Tolerations
+	}
+	if ps.SecurityContext == nil {
+		ps.SecurityContext = apt.SecurityContext
+	}
+	if len(ps.ImagePullSecrets) == 0 {
+		ps.ImagePullSecrets = apt.ImagePullSecrets
+	}
+	if ps.PriorityClassName == "" {
+		ps.PriorityClassName = apt.PriorityClassName
+	}
+	if ps.Priority == nil {
+		ps.Priority = apt.Priority
+	}
+	if ps.Affinity == nil {
+		ps.Affinity = apt.Affinity
+	}
+	if ps.ServiceAccountName == "" {
+		ps.ServiceAccountName = apt.ServiceAccountName
+	}
+	if ps.RuntimeClassName == nil {
+		ps.RuntimeClassName = apt.RuntimeClassName
+	}
+	if ps.AutomountServiceAccountToken == nil {
+		ps.AutomountServiceAccountToken = apt.AutomountServiceAccountToken
+	}
+	if ps.DNSPolicy == "" {
+		ps.DNSPolicy = apt.DNSPolicy
+	}
+	if ps.DNSConfig == nil {
+		ps.DNSConfig = apt.DNSConfig
+	}
 }
 
 // ApplyToPodTemplateSpec updates the PodTemplateSpec with the values in the AbstractPodTemplate


Test patch:
diff --git a/pkg/apis/numaflow/v1alpha1/pod_template_test.go b/pkg/apis/numaflow/v1alpha1/pod_template_test.go
new file mode 100644
index 00000000..0fb44b19
--- /dev/null
+++ b/pkg/apis/numaflow/v1alpha1/pod_template_test.go
@@ -0,0 +1,79 @@
+package v1alpha1
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+)
+
+func TestApplyToPodSpec(t *testing.T) {
+	runAsUser := int64(1001)
+	runAsUser0 := int64(0)
+	priority := int32(100)
+
+	abstractPodTemplate := &AbstractPodTemplate{
+		Metadata: &Metadata{
+			Annotations: map[string]string{"my-annotation-name": "my-annotation-value"},
+			Labels:      map[string]string{"my-label-name": "my-label-value"},
+		},
+		NodeSelector: map[string]string{"my-node-selector-name": "my-node-selector-value"},
+		SecurityContext: &corev1.PodSecurityContext{
+			RunAsUser:  &runAsUser,
+			RunAsGroup: &runAsUser0,
+		},
+		Tolerations: []corev1.Toleration{
+			{
+				Key:      "my-toleration-key",
+				Operator: "Equal",
+				Value:    "my-toleration-value",
+				Effect:   "NoSchedule",
+			},
+		},
+		ImagePullSecrets: []corev1.LocalObjectReference{
+			{
+				Name: "template-image-pull-secret",
+			},
+		},
+		PriorityClassName: "my-priority-class-name",
+		Priority:          &priority,
+		Affinity: &corev1.Affinity{
+			NodeAffinity: &corev1.NodeAffinity{
+				RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+					NodeSelectorTerms: []corev1.NodeSelectorTerm{
+						{
+							MatchExpressions: []corev1.NodeSelectorRequirement{
+								{
+									Key:      "m",
+									Operator: "n",
+									Values:   []string{"o"},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		ServiceAccountName: "template-sa",
+	}
+
+	podSpec := &corev1.PodSpec{
+		ServiceAccountName: "spec-sa",
+		ImagePullSecrets: []corev1.LocalObjectReference{
+			{
+				Name: "spec-image-pull-secret",
+			},
+		},
+	}
+
+	abstractPodTemplate.ApplyToPodSpec(podSpec)
+
+	assert.Equal(t, podSpec.NodeSelector, abstractPodTemplate.NodeSelector)
+	assert.Equal(t, podSpec.Tolerations, abstractPodTemplate.Tolerations)
+	assert.Equal(t, podSpec.SecurityContext, abstractPodTemplate.SecurityContext)
+	assert.Equal(t, podSpec.ImagePullSecrets[0].Name, "spec-image-pull-secret")
+	assert.Equal(t, podSpec.PriorityClassName, abstractPodTemplate.PriorityClassName)
+	assert.Equal(t, podSpec.Priority, abstractPodTemplate.Priority)
+	assert.Equal(t, podSpec.Affinity, abstractPodTemplate.Affinity)
+	assert.Equal(t, podSpec.ServiceAccountName, "spec-sa")
+}