Feature: pod affinity changes for OVN

Deploy OVN northd using the same pod affinity rules
used with the NB and SB statefulsets. Also change
default anti-affinity for NB and SB statefulsets
to requiredDuringSchedulingIgnoredDuringExecution
over preferredDuringSchedulingIgnoredDuringExecution

Change-Id: I844bbdc53fe10a8b8d9f017b9a3c68d93b695b98

Add unit tests to for OVN Anti-Affinity Rules

These unit tests go over all the statefulset and
deployment  pods using the and make sure they are
all using the right anti-affinity rules.

Change-Id: I2c8b5bf7515040e715bdeed4410acf6656578133
diff --git a/charts/ovn/templates/deployment-northd.yaml b/charts/ovn/templates/deployment-northd.yaml
index 2dbbb68..baf5a0c 100644
--- a/charts/ovn/templates/deployment-northd.yaml
+++ b/charts/ovn/templates/deployment-northd.yaml
@@ -49,6 +49,8 @@
         configmap-bin-hash: {{ tuple "configmap-bin.yaml" . | include "helm-toolkit.utils.hash" }}
     spec:
       serviceAccountName: {{ $serviceAccountName }}
+      affinity:
+{{- tuple $envAll "ovn" "ovn_northd" | include "helm-toolkit.snippets.kubernetes_pod_anti_affinity" | indent 8 }}
       nodeSelector:
         {{ .Values.labels.ovn_northd.node_selector_key }}: {{ .Values.labels.ovn_northd.node_selector_value }}
       initContainers:
diff --git a/charts/patches/ovn/0003-add-ovn-northd-pod-affinity.patch b/charts/patches/ovn/0003-add-ovn-northd-pod-affinity.patch
new file mode 100644
index 0000000..37acdae
--- /dev/null
+++ b/charts/patches/ovn/0003-add-ovn-northd-pod-affinity.patch
@@ -0,0 +1,13 @@
+diff --git a/charts/ovn/templates/deployment-northd.yaml b/charts/ovn/templates/deployment-northd.yaml
+index 2dbbb689..baf5a0c7 100644
+--- a/ovn/templates/deployment-northd.yaml
++++ b/ovn/templates/deployment-northd.yaml
+@@ -49,6 +49,8 @@ spec:
+         configmap-bin-hash: {{ tuple "configmap-bin.yaml" . | include "helm-toolkit.utils.hash" }}
+     spec:
+       serviceAccountName: {{ $serviceAccountName }}
++      affinity:
++{{- tuple $envAll "ovn" "ovn_northd" | include "helm-toolkit.snippets.kubernetes_pod_anti_affinity" | indent 8 }}
+       nodeSelector:
+         {{ .Values.labels.ovn_northd.node_selector_key }}: {{ .Values.labels.ovn_northd.node_selector_value }}
+       initContainers:
diff --git a/internal/openstack_helm/openstack_helm.go b/internal/openstack_helm/openstack_helm.go
index c8dd65e..e7a509d 100644
--- a/internal/openstack_helm/openstack_helm.go
+++ b/internal/openstack_helm/openstack_helm.go
@@ -20,8 +20,11 @@
 	PriorityClass PodPriorityClassConfig `yaml:"priorityClassName,omitempty"`
 	RuntimeClass  PodRuntimeClassConfig  `yaml:"runtimeClassName,omitempty"`
 	Mounts        map[string]PodMount    `yaml:"mounts,omitempty"`
+	AntiAffinityType PodAntiAffinityTypeConfig `yaml:"affinity.anti.type,omitempty"`
 }
 
+type PodAntiAffinityTypeConfig map[string]interface{}
+
 type Conf struct {
 	Barbican  *BarbicanConf  `yaml:"barbican,omitempty"`
 	Cinder    *CinderConf    `yaml:"cinder,omitempty"`
diff --git a/internal/testutils/oslo_db.go b/internal/testutils/oslo_db.go
index 0b02304..3a9159d 100644
--- a/internal/testutils/oslo_db.go
+++ b/internal/testutils/oslo_db.go
@@ -5,6 +5,7 @@
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"github.com/vexxhost/atmosphere/internal/openstack_helm"
 )
@@ -43,3 +44,20 @@
 		assert.Contains(t, vals.Pod.PriorityClass, podName)
 	}
 }
+
+func TestAllPodsHaveAntiAffinityType(t *testing.T, vals *openstack_helm.HelmValues) {
+	for pod := range vals.Pod.AntiAffinityType {
+		podName := podNameForClass(pod)
+
+		expected := "requiredDuringSchedulingIgnoredDuringExecution"
+
+		defaultRaw, ok := vals.Pod.AntiAffinityType["default"]
+		require.True(t, ok, "default key not found in affinity.anti.type block")
+
+		actual, ok := defaultRaw.(string)
+		require.True(t, ok, "default anti affinity type is not a string")
+
+		assert.Equal(t, expected, actual, "anti affinity type does not match expected value")
+		assert.Contains(t, vals.Pod.AntiAffinityType, podName)
+	}
+}
diff --git a/releasenotes/notes/enable-ovn-affinity-rules-54efa650be79426c.yaml b/releasenotes/notes/enable-ovn-affinity-rules-54efa650be79426c.yaml
new file mode 100644
index 0000000..601f254
--- /dev/null
+++ b/releasenotes/notes/enable-ovn-affinity-rules-54efa650be79426c.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Applied the same pod affinity rules used for OVN NB/SB sts's to northd deployment and
+    changed the default pod affinity rules from preferred during scheduling to required
+    during scheduling.
diff --git a/roles/ovn/vars/main.yml b/roles/ovn/vars/main.yml
index b879919..a24b7e9 100644
--- a/roles/ovn/vars/main.yml
+++ b/roles/ovn/vars/main.yml
@@ -43,6 +43,14 @@
               initialDelaySeconds: 30
               timeoutSeconds: 30
               periodSeconds: 60
+    affinity:
+      anti:
+        type:
+          default: requiredDuringSchedulingIgnoredDuringExecution
+        topologyKey:
+          default: kubernetes.io/hostname
+        weight:
+          default: 10
     replicas:
       ovn_ovsdb_nb: 3
       ovn_ovsdb_sb: 3
diff --git a/roles/ovn/vars_test.go b/roles/ovn/vars_test.go
new file mode 100644
index 0000000..ee4d95b
--- /dev/null
+++ b/roles/ovn/vars_test.go
@@ -0,0 +1,39 @@
+package ovn
+
+import (
+	_ "embed"
+	"os"
+	"testing"
+
+	"github.com/goccy/go-yaml"
+	"github.com/stretchr/testify/require"
+
+	"github.com/vexxhost/atmosphere/internal/openstack_helm"
+	"github.com/vexxhost/atmosphere/internal/testutils"
+)
+
+var (
+	//go:embed vars/main.yml
+	varsFile []byte
+	vars     Vars
+)
+
+type Vars struct {
+	openstack_helm.HelmValues `yaml:"_ovn_helm_values"`
+}
+
+func TestMain(m *testing.M) {
+	t := &testing.T{}
+	err := yaml.UnmarshalWithOptions(varsFile, &vars)
+	require.NoError(t, err)
+
+	code := m.Run()
+	os.Exit(code)
+}
+
+func TestHelmValues(t *testing.T) {
+	vals, err := openstack_helm.CoalescedHelmValues("../../charts/ovn", &vars.HelmValues)
+	require.NoError(t, err)
+
+	testutils.TestAllPodsHaveAntiAffinityType(t, vals)
+}