From 1f6232439e3419ba489f66b463d4d723606b922f Mon Sep 17 00:00:00 2001
From: David Gubler <david.gubler@vshn.net>
Date: Thu, 21 Mar 2024 15:14:28 +0100
Subject: [PATCH] feat: Support multiple replicas for Deployments

---
 .github/workflows/release.yml                 |  2 +
 docs/conversion.md                            | 22 +++++++
 internal/output.go                            |  8 +++
 pkg/converter/converter.go                    | 57 ++++++++++++++++++-
 .../101/manifests/nginx-oasp-deployment.yaml  | 10 ++++
 .../nginx-oasp-poddisruptionbudget.yaml       | 19 +++++++
 .../manifests/nginx-oasp-deployment.yaml      | 10 ++++
 .../manifests/nginx-oasp-deployment.yaml      | 10 ++++
 .../demo/manifests/mongo-statefulset.yaml     | 10 ++++
 .../manifests/portal-oasp-deployment.yaml     | 10 ++++
 .../portal-oasp-poddisruptionbudget.yaml      | 19 +++++++
 .../manifests/pinger-oasp-deployment.yaml     | 10 ++++
 .../manifests/pinger-oasp-deployment.yaml     | 10 ++++
 .../manifests/fooBar-oasp-deployment.yaml     | 10 ++++
 .../manifests/nginx-oasp-deployment.yaml      | 10 ++++
 .../manifests/pinger-oasp-deployment.yaml     | 10 ++++
 .../nginx-frontend-oasp-deployment.yaml       | 10 ++++
 .../parts/manifests/mongo-statefulset.yaml    | 10 ++++
 .../nginx-frontend-oasp-deployment.yaml       | 10 ++++
 ...inx-frontend-oasp-poddisruptionbudget.yaml | 19 +++++++
 tests/golden/poddisruptionbudget.yml          |  3 +
 tests/golden/poddisruptionbudget/compose.yml  |  7 +++
 .../manifests/nginx-oasp-deployment.yaml      | 56 ++++++++++++++++++
 .../nginx-oasp-poddisruptionbudget.yaml       | 19 +++++++
 .../manifests/nginx-oasp-service.yaml         | 18 ++++++
 .../manifests/default-oasp-statefulset.yaml   | 10 ++++
 .../default-shared-oasp-deployment.yaml       | 10 ++++
 .../manifests/share-0-oasp-deployment.yaml    | 10 ++++
 .../manifests/share-1-oasp-deployment.yaml    | 10 ++++
 .../manifests/singleton-db-statefulset.yaml   | 10 ++++
 .../manifests/default-oasp-statefulset.yaml   | 10 ++++
 .../default-shared-oasp-deployment.yaml       | 10 ++++
 .../manifests/share-0-oasp-deployment.yaml    | 10 ++++
 .../manifests/share-1-oasp-deployment.yaml    | 10 ++++
 .../manifests/singleton-db-statefulset.yaml   | 10 ++++
 35 files changed, 478 insertions(+), 1 deletion(-)
 create mode 100644 tests/golden/101/manifests/nginx-oasp-poddisruptionbudget.yaml
 create mode 100644 tests/golden/demo/manifests/portal-oasp-poddisruptionbudget.yaml
 create mode 100644 tests/golden/parts/manifests/nginx-frontend-oasp-poddisruptionbudget.yaml
 create mode 100644 tests/golden/poddisruptionbudget.yml
 create mode 100644 tests/golden/poddisruptionbudget/compose.yml
 create mode 100644 tests/golden/poddisruptionbudget/manifests/nginx-oasp-deployment.yaml
 create mode 100644 tests/golden/poddisruptionbudget/manifests/nginx-oasp-poddisruptionbudget.yaml
 create mode 100644 tests/golden/poddisruptionbudget/manifests/nginx-oasp-service.yaml

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e6693b7..fe1154a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -4,6 +4,8 @@ on:
   push:
     tags:
       - "*"
+#    branches:
+#      - 'yourbranch' # useful for testing
 
 env:
   ghcr_latest_tag: "${{ github.ref_type == 'tag' && ',ghcr.io/vshn/k8ify:latest' || '' }}"
diff --git a/docs/conversion.md b/docs/conversion.md
index bf98334..2aa5ea3 100644
--- a/docs/conversion.md
+++ b/docs/conversion.md
@@ -116,6 +116,17 @@ spec:
         # `services.$name.labels["k8ify.annotations"]` merged with `services.$name.labels["k8ify.Pod.annotations"]` (latter take priority)
         foo: bar
     spec:
+      # Anti-affinity is always configured to avoid running multiple replicas (instances) of the same deployment on the same node
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - `services.$name` # `$refSlug` isn't relevant because we don't mind running different deployments on the same node
+            topologyKey: kubernetes.io/hostname
       containers:
           # If singleton or no ref given: `$name`, otherwise: `$name-$refSlug`
         - name: "myapp-feat-foo"  # or "myapp"
@@ -226,6 +237,17 @@ spec:
         # timestamp to ensure restarts of all pods
         k8ify.restart-trigger: "1675680748"
     spec:
+      # Anti-affinity is always configured to avoid running multiple replicas (instances) of the same deployment on the same node
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - `services.$name` # `$refSlug` isn't relevant because we don't mind running different deployments on the same node
+            topologyKey: kubernetes.io/hostname
       containers:
           # If singleton or no ref given: `$name`, otherwise: `$name-$refSlug`
         - name: "myapp-feat-foo"  # or "myapp"
diff --git a/internal/output.go b/internal/output.go
index 4819c6e..87792d5 100644
--- a/internal/output.go
+++ b/internal/output.go
@@ -104,6 +104,14 @@ func WriteManifests(outputDir string, objects converter.Objects) error {
 	}
 	logrus.Infof("wrote %d ingresses\n", len(objects.Ingresses))
 
+	for _, podDisruptionBudget := range objects.PodDisruptionBudgets {
+		err := writeManifest(&podDisruptionBudget, outputDir+"/"+podDisruptionBudget.Name+"-poddisruptionbudget.yaml")
+		if err != nil {
+			return err
+		}
+	}
+	logrus.Infof("wrote %d podDisruptionBudgets\n", len(objects.PodDisruptionBudgets))
+
 	for _, other := range objects.Others {
 		err := writeManifest(&other, outputDir+"/"+other.GetName()+"-"+strings.ToLower(other.GetObjectKind().GroupVersionKind().Kind)+".yaml")
 		if err != nil {
diff --git a/pkg/converter/converter.go b/pkg/converter/converter.go
index 7eca812..013daa0 100644
--- a/pkg/converter/converter.go
+++ b/pkg/converter/converter.go
@@ -2,6 +2,7 @@ package converter
 
 import (
 	"fmt"
+	v1 "k8s.io/api/policy/v1"
 	"log"
 	"maps"
 	"os"
@@ -183,7 +184,6 @@ func composeServiceToStatefulSet(
 	volumeClaims []core.PersistentVolumeClaim,
 	labels map[string]string,
 ) (apps.StatefulSet, []core.Secret) {
-
 	statefulset := apps.StatefulSet{}
 	statefulset.APIVersion = "apps/v1"
 	statefulset.Kind = "StatefulSet"
@@ -260,6 +260,7 @@ func composeServiceToPodTemplate(
 		RestartPolicy:      core.RestartPolicyAlways,
 		Volumes:            volumesArray,
 		ServiceAccountName: serviceAccountName,
+		Affinity:           composeServiceToAffinity(workload),
 	}
 
 	return core.PodTemplateSpec{
@@ -271,6 +272,29 @@ func composeServiceToPodTemplate(
 	}, secrets
 }
 
+func composeServiceToAffinity(workload *ir.Service) *core.Affinity {
+	podAntiAffinity := core.PodAntiAffinity{
+		RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{
+			{
+				LabelSelector: &metav1.LabelSelector{
+					MatchExpressions: []metav1.LabelSelectorRequirement{
+						{
+							Key:      "k8ify.service",
+							Operator: "In",
+							Values:   []string{workload.Name},
+						},
+					},
+				},
+				TopologyKey: "kubernetes.io/hostname", // should be available on pretty much any k8s setup
+			},
+		},
+	}
+	affinity := core.Affinity{
+		PodAntiAffinity: &podAntiAffinity,
+	}
+	return &affinity
+}
+
 func composeServiceToContainer(
 	workload *ir.Service,
 	refSlug string,
@@ -717,6 +741,13 @@ func ComposeServiceToK8s(ref string, workload *ir.Service, projectVolumes map[st
 		objects.Secrets = secrets
 	}
 
+	podDisruptionBudget := composeServiceToPodDisruptionBudget(workload, refSlug, labels)
+	if podDisruptionBudget == nil {
+		objects.PodDisruptionBudgets = []v1.PodDisruptionBudget{}
+	} else {
+		objects.PodDisruptionBudgets = []v1.PodDisruptionBudget{*podDisruptionBudget}
+	}
+
 	ingress := composeServiceToIngress(workload, refSlug, objects.Services, labels, targetCfg)
 	if ingress == nil {
 		objects.Ingresses = []networking.Ingress{}
@@ -769,6 +800,28 @@ func composeVolumeToPvc(name string, labels map[string]string, volume *ir.Volume
 	}
 }
 
+func composeServiceToPodDisruptionBudget(workload *ir.Service, refSlug string, labels map[string]string) *v1.PodDisruptionBudget {
+	replicas := composeServiceToReplicas(workload.AsCompose())
+	if replicas == nil || *replicas <= 1 {
+		return nil
+	}
+
+	podDisruptionBudget := v1.PodDisruptionBudget{}
+	podDisruptionBudget.APIVersion = "policy/v1"
+	podDisruptionBudget.Kind = "PodDisruptionBudget"
+	podDisruptionBudget.Name = workload.Name + refSlug
+	podDisruptionBudget.Labels = labels
+	podDisruptionBudget.Annotations = util.Annotations(workload.Labels(), podDisruptionBudget.Kind)
+	maxUnavailable := intstr.FromString("50%")
+	podDisruptionBudget.Spec = v1.PodDisruptionBudgetSpec{
+		MaxUnavailable: &maxUnavailable,
+		Selector: &metav1.LabelSelector{
+			MatchLabels: labels,
+		},
+	}
+	return &podDisruptionBudget
+}
+
 // Objects combines all possible resources the conversion process could produce
 type Objects struct {
 	// Deployments
@@ -778,6 +831,7 @@ type Objects struct {
 	PersistentVolumeClaims []core.PersistentVolumeClaim
 	Secrets                []core.Secret
 	Ingresses              []networking.Ingress
+	PodDisruptionBudgets   []v1.PodDisruptionBudget
 	Others                 []unstructured.Unstructured
 }
 
@@ -802,6 +856,7 @@ func (this Objects) Append(other Objects) Objects {
 		PersistentVolumeClaims: pvcs,
 		Secrets:                append(this.Secrets, other.Secrets...),
 		Ingresses:              append(this.Ingresses, other.Ingresses...),
+		PodDisruptionBudgets:   append(this.PodDisruptionBudgets, other.PodDisruptionBudgets...),
 		Others:                 append(this.Others, other.Others...),
 	}
 }
diff --git a/tests/golden/101/manifests/nginx-oasp-deployment.yaml b/tests/golden/101/manifests/nginx-oasp-deployment.yaml
index 92c49eb..82aa995 100644
--- a/tests/golden/101/manifests/nginx-oasp-deployment.yaml
+++ b/tests/golden/101/manifests/nginx-oasp-deployment.yaml
@@ -21,6 +21,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: nginx
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - nginx
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: docker.io/library/nginx
         imagePullPolicy: Always
diff --git a/tests/golden/101/manifests/nginx-oasp-poddisruptionbudget.yaml b/tests/golden/101/manifests/nginx-oasp-poddisruptionbudget.yaml
new file mode 100644
index 0000000..87f7bac
--- /dev/null
+++ b/tests/golden/101/manifests/nginx-oasp-poddisruptionbudget.yaml
@@ -0,0 +1,19 @@
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+  creationTimestamp: null
+  labels:
+    k8ify.ref-slug: oasp
+    k8ify.service: nginx
+  name: nginx-oasp
+spec:
+  maxUnavailable: 50%
+  selector:
+    matchLabels:
+      k8ify.ref-slug: oasp
+      k8ify.service: nginx
+status:
+  currentHealthy: 0
+  desiredHealthy: 0
+  disruptionsAllowed: 0
+  expectedPods: 0
diff --git a/tests/golden/cluster-apps-domain/manifests/nginx-oasp-deployment.yaml b/tests/golden/cluster-apps-domain/manifests/nginx-oasp-deployment.yaml
index ac27539..e648d9c 100644
--- a/tests/golden/cluster-apps-domain/manifests/nginx-oasp-deployment.yaml
+++ b/tests/golden/cluster-apps-domain/manifests/nginx-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: nginx
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - nginx
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: docker.io/library/nginx
         imagePullPolicy: Always
diff --git a/tests/golden/defaults/manifests/nginx-oasp-deployment.yaml b/tests/golden/defaults/manifests/nginx-oasp-deployment.yaml
index ac27539..e648d9c 100644
--- a/tests/golden/defaults/manifests/nginx-oasp-deployment.yaml
+++ b/tests/golden/defaults/manifests/nginx-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: nginx
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - nginx
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: docker.io/library/nginx
         imagePullPolicy: Always
diff --git a/tests/golden/demo/manifests/mongo-statefulset.yaml b/tests/golden/demo/manifests/mongo-statefulset.yaml
index 39d8e1d..bdcc049 100644
--- a/tests/golden/demo/manifests/mongo-statefulset.yaml
+++ b/tests/golden/demo/manifests/mongo-statefulset.yaml
@@ -24,6 +24,16 @@ spec:
       labels:
         k8ify.service: mongo
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - mongo
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: mongo:4.0
         imagePullPolicy: Always
diff --git a/tests/golden/demo/manifests/portal-oasp-deployment.yaml b/tests/golden/demo/manifests/portal-oasp-deployment.yaml
index 7c0ded3..c518516 100644
--- a/tests/golden/demo/manifests/portal-oasp-deployment.yaml
+++ b/tests/golden/demo/manifests/portal-oasp-deployment.yaml
@@ -21,6 +21,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: portal
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - portal
+            topologyKey: kubernetes.io/hostname
       containers:
       - args:
         - Hello World
diff --git a/tests/golden/demo/manifests/portal-oasp-poddisruptionbudget.yaml b/tests/golden/demo/manifests/portal-oasp-poddisruptionbudget.yaml
new file mode 100644
index 0000000..9b8145a
--- /dev/null
+++ b/tests/golden/demo/manifests/portal-oasp-poddisruptionbudget.yaml
@@ -0,0 +1,19 @@
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+  creationTimestamp: null
+  labels:
+    k8ify.ref-slug: oasp
+    k8ify.service: portal
+  name: portal-oasp
+spec:
+  maxUnavailable: 50%
+  selector:
+    matchLabels:
+      k8ify.ref-slug: oasp
+      k8ify.service: portal
+status:
+  currentHealthy: 0
+  desiredHealthy: 0
+  disruptionsAllowed: 0
+  expectedPods: 0
diff --git a/tests/golden/empty-env-vars-list/manifests/pinger-oasp-deployment.yaml b/tests/golden/empty-env-vars-list/manifests/pinger-oasp-deployment.yaml
index d1e7974..6b57fac 100644
--- a/tests/golden/empty-env-vars-list/manifests/pinger-oasp-deployment.yaml
+++ b/tests/golden/empty-env-vars-list/manifests/pinger-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: pinger
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - pinger
+            topologyKey: kubernetes.io/hostname
       containers:
       - envFrom:
         - secretRef:
diff --git a/tests/golden/empty-env-vars-map/manifests/pinger-oasp-deployment.yaml b/tests/golden/empty-env-vars-map/manifests/pinger-oasp-deployment.yaml
index d1e7974..6b57fac 100644
--- a/tests/golden/empty-env-vars-map/manifests/pinger-oasp-deployment.yaml
+++ b/tests/golden/empty-env-vars-map/manifests/pinger-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: pinger
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - pinger
+            topologyKey: kubernetes.io/hostname
       containers:
       - envFrom:
         - secretRef:
diff --git a/tests/golden/env-vars/manifests/fooBar-oasp-deployment.yaml b/tests/golden/env-vars/manifests/fooBar-oasp-deployment.yaml
index b299bf8..53f5db4 100644
--- a/tests/golden/env-vars/manifests/fooBar-oasp-deployment.yaml
+++ b/tests/golden/env-vars/manifests/fooBar-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: fooBar
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - fooBar
+            topologyKey: kubernetes.io/hostname
       containers:
       - env:
         - name: PASSWORD
diff --git a/tests/golden/expose-plain/manifests/nginx-oasp-deployment.yaml b/tests/golden/expose-plain/manifests/nginx-oasp-deployment.yaml
index 7d64824..cd8df82 100644
--- a/tests/golden/expose-plain/manifests/nginx-oasp-deployment.yaml
+++ b/tests/golden/expose-plain/manifests/nginx-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: nginx
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - nginx
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: docker.io/library/nginx
         imagePullPolicy: Always
diff --git a/tests/golden/noports/manifests/pinger-oasp-deployment.yaml b/tests/golden/noports/manifests/pinger-oasp-deployment.yaml
index 2007814..2053498 100644
--- a/tests/golden/noports/manifests/pinger-oasp-deployment.yaml
+++ b/tests/golden/noports/manifests/pinger-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: pinger
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - pinger
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: pinger:4.0
         imagePullPolicy: Always
diff --git a/tests/golden/parts-ingress/manifests/nginx-frontend-oasp-deployment.yaml b/tests/golden/parts-ingress/manifests/nginx-frontend-oasp-deployment.yaml
index 525bd09..f970d7b 100644
--- a/tests/golden/parts-ingress/manifests/nginx-frontend-oasp-deployment.yaml
+++ b/tests/golden/parts-ingress/manifests/nginx-frontend-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: nginx-frontend
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - nginx-frontend
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: nginx-frontend:prod
         imagePullPolicy: Always
diff --git a/tests/golden/parts/manifests/mongo-statefulset.yaml b/tests/golden/parts/manifests/mongo-statefulset.yaml
index f5ac915..fece827 100644
--- a/tests/golden/parts/manifests/mongo-statefulset.yaml
+++ b/tests/golden/parts/manifests/mongo-statefulset.yaml
@@ -16,6 +16,16 @@ spec:
       labels:
         k8ify.service: mongo
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - mongo
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: mongo:4.0
         imagePullPolicy: Always
diff --git a/tests/golden/parts/manifests/nginx-frontend-oasp-deployment.yaml b/tests/golden/parts/manifests/nginx-frontend-oasp-deployment.yaml
index 1aa3eb2..858d74a 100644
--- a/tests/golden/parts/manifests/nginx-frontend-oasp-deployment.yaml
+++ b/tests/golden/parts/manifests/nginx-frontend-oasp-deployment.yaml
@@ -21,6 +21,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: nginx-frontend
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - nginx-frontend
+            topologyKey: kubernetes.io/hostname
       containers:
       - envFrom:
         - secretRef:
diff --git a/tests/golden/parts/manifests/nginx-frontend-oasp-poddisruptionbudget.yaml b/tests/golden/parts/manifests/nginx-frontend-oasp-poddisruptionbudget.yaml
new file mode 100644
index 0000000..ca20d82
--- /dev/null
+++ b/tests/golden/parts/manifests/nginx-frontend-oasp-poddisruptionbudget.yaml
@@ -0,0 +1,19 @@
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+  creationTimestamp: null
+  labels:
+    k8ify.ref-slug: oasp
+    k8ify.service: nginx-frontend
+  name: nginx-frontend-oasp
+spec:
+  maxUnavailable: 50%
+  selector:
+    matchLabels:
+      k8ify.ref-slug: oasp
+      k8ify.service: nginx-frontend
+status:
+  currentHealthy: 0
+  desiredHealthy: 0
+  disruptionsAllowed: 0
+  expectedPods: 0
diff --git a/tests/golden/poddisruptionbudget.yml b/tests/golden/poddisruptionbudget.yml
new file mode 100644
index 0000000..508ae83
--- /dev/null
+++ b/tests/golden/poddisruptionbudget.yml
@@ -0,0 +1,3 @@
+---
+environments:
+  prod: {}
diff --git a/tests/golden/poddisruptionbudget/compose.yml b/tests/golden/poddisruptionbudget/compose.yml
new file mode 100644
index 0000000..5870814
--- /dev/null
+++ b/tests/golden/poddisruptionbudget/compose.yml
@@ -0,0 +1,7 @@
+services:
+  nginx:
+    deploy:
+      replicas: 2
+    image: docker.io/library/nginx
+    ports:
+      - '8080:80'
diff --git a/tests/golden/poddisruptionbudget/manifests/nginx-oasp-deployment.yaml b/tests/golden/poddisruptionbudget/manifests/nginx-oasp-deployment.yaml
new file mode 100644
index 0000000..2caf595
--- /dev/null
+++ b/tests/golden/poddisruptionbudget/manifests/nginx-oasp-deployment.yaml
@@ -0,0 +1,56 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  creationTimestamp: null
+  labels:
+    k8ify.ref-slug: oasp
+    k8ify.service: nginx
+  name: nginx-oasp
+spec:
+  replicas: 2
+  selector:
+    matchLabels:
+      k8ify.ref-slug: oasp
+      k8ify.service: nginx
+  strategy:
+    type: Recreate
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        k8ify.ref-slug: oasp
+        k8ify.service: nginx
+    spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - nginx
+            topologyKey: kubernetes.io/hostname
+      containers:
+      - image: docker.io/library/nginx
+        imagePullPolicy: Always
+        livenessProbe:
+          failureThreshold: 3
+          periodSeconds: 30
+          successThreshold: 1
+          tcpSocket:
+            port: 80
+          timeoutSeconds: 60
+        name: nginx-oasp
+        ports:
+        - containerPort: 80
+        resources: {}
+        startupProbe:
+          failureThreshold: 30
+          periodSeconds: 10
+          successThreshold: 1
+          tcpSocket:
+            port: 80
+          timeoutSeconds: 60
+      restartPolicy: Always
+status: {}
diff --git a/tests/golden/poddisruptionbudget/manifests/nginx-oasp-poddisruptionbudget.yaml b/tests/golden/poddisruptionbudget/manifests/nginx-oasp-poddisruptionbudget.yaml
new file mode 100644
index 0000000..87f7bac
--- /dev/null
+++ b/tests/golden/poddisruptionbudget/manifests/nginx-oasp-poddisruptionbudget.yaml
@@ -0,0 +1,19 @@
+apiVersion: policy/v1
+kind: PodDisruptionBudget
+metadata:
+  creationTimestamp: null
+  labels:
+    k8ify.ref-slug: oasp
+    k8ify.service: nginx
+  name: nginx-oasp
+spec:
+  maxUnavailable: 50%
+  selector:
+    matchLabels:
+      k8ify.ref-slug: oasp
+      k8ify.service: nginx
+status:
+  currentHealthy: 0
+  desiredHealthy: 0
+  disruptionsAllowed: 0
+  expectedPods: 0
diff --git a/tests/golden/poddisruptionbudget/manifests/nginx-oasp-service.yaml b/tests/golden/poddisruptionbudget/manifests/nginx-oasp-service.yaml
new file mode 100644
index 0000000..aeb65dd
--- /dev/null
+++ b/tests/golden/poddisruptionbudget/manifests/nginx-oasp-service.yaml
@@ -0,0 +1,18 @@
+apiVersion: v1
+kind: Service
+metadata:
+  creationTimestamp: null
+  labels:
+    k8ify.ref-slug: oasp
+    k8ify.service: nginx
+  name: nginx-oasp
+spec:
+  ports:
+  - name: "8080"
+    port: 8080
+    targetPort: 80
+  selector:
+    k8ify.ref-slug: oasp
+    k8ify.service: nginx
+status:
+  loadBalancer: {}
diff --git a/tests/golden/storage-encrypted/manifests/default-oasp-statefulset.yaml b/tests/golden/storage-encrypted/manifests/default-oasp-statefulset.yaml
index 0f78e95..2846fd2 100644
--- a/tests/golden/storage-encrypted/manifests/default-oasp-statefulset.yaml
+++ b/tests/golden/storage-encrypted/manifests/default-oasp-statefulset.yaml
@@ -19,6 +19,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: default
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - default
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: nginx
         imagePullPolicy: Always
diff --git a/tests/golden/storage-encrypted/manifests/default-shared-oasp-deployment.yaml b/tests/golden/storage-encrypted/manifests/default-shared-oasp-deployment.yaml
index d6a822f..d3a9993 100644
--- a/tests/golden/storage-encrypted/manifests/default-shared-oasp-deployment.yaml
+++ b/tests/golden/storage-encrypted/manifests/default-shared-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: default-shared
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - default-shared
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: nginx
         imagePullPolicy: Always
diff --git a/tests/golden/storage-encrypted/manifests/share-0-oasp-deployment.yaml b/tests/golden/storage-encrypted/manifests/share-0-oasp-deployment.yaml
index 0008c3b..1da7479 100644
--- a/tests/golden/storage-encrypted/manifests/share-0-oasp-deployment.yaml
+++ b/tests/golden/storage-encrypted/manifests/share-0-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: share-0
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - share-0
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: nginx
         imagePullPolicy: Always
diff --git a/tests/golden/storage-encrypted/manifests/share-1-oasp-deployment.yaml b/tests/golden/storage-encrypted/manifests/share-1-oasp-deployment.yaml
index e1a8c25..a8a6d95 100644
--- a/tests/golden/storage-encrypted/manifests/share-1-oasp-deployment.yaml
+++ b/tests/golden/storage-encrypted/manifests/share-1-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: share-1
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - share-1
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: nginx
         imagePullPolicy: Always
diff --git a/tests/golden/storage-encrypted/manifests/singleton-db-statefulset.yaml b/tests/golden/storage-encrypted/manifests/singleton-db-statefulset.yaml
index c5ea693..83cdd7f 100644
--- a/tests/golden/storage-encrypted/manifests/singleton-db-statefulset.yaml
+++ b/tests/golden/storage-encrypted/manifests/singleton-db-statefulset.yaml
@@ -16,6 +16,16 @@ spec:
       labels:
         k8ify.service: singleton-db
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - singleton-db
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: nginx
         imagePullPolicy: Always
diff --git a/tests/golden/storage/manifests/default-oasp-statefulset.yaml b/tests/golden/storage/manifests/default-oasp-statefulset.yaml
index b151384..80a7f9d 100644
--- a/tests/golden/storage/manifests/default-oasp-statefulset.yaml
+++ b/tests/golden/storage/manifests/default-oasp-statefulset.yaml
@@ -19,6 +19,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: default
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - default
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: nginx
         imagePullPolicy: Always
diff --git a/tests/golden/storage/manifests/default-shared-oasp-deployment.yaml b/tests/golden/storage/manifests/default-shared-oasp-deployment.yaml
index d6a822f..d3a9993 100644
--- a/tests/golden/storage/manifests/default-shared-oasp-deployment.yaml
+++ b/tests/golden/storage/manifests/default-shared-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: default-shared
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - default-shared
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: nginx
         imagePullPolicy: Always
diff --git a/tests/golden/storage/manifests/share-0-oasp-deployment.yaml b/tests/golden/storage/manifests/share-0-oasp-deployment.yaml
index 0008c3b..1da7479 100644
--- a/tests/golden/storage/manifests/share-0-oasp-deployment.yaml
+++ b/tests/golden/storage/manifests/share-0-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: share-0
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - share-0
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: nginx
         imagePullPolicy: Always
diff --git a/tests/golden/storage/manifests/share-1-oasp-deployment.yaml b/tests/golden/storage/manifests/share-1-oasp-deployment.yaml
index e1a8c25..a8a6d95 100644
--- a/tests/golden/storage/manifests/share-1-oasp-deployment.yaml
+++ b/tests/golden/storage/manifests/share-1-oasp-deployment.yaml
@@ -20,6 +20,16 @@ spec:
         k8ify.ref-slug: oasp
         k8ify.service: share-1
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - share-1
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: nginx
         imagePullPolicy: Always
diff --git a/tests/golden/storage/manifests/singleton-db-statefulset.yaml b/tests/golden/storage/manifests/singleton-db-statefulset.yaml
index d06b328..5e8cef1 100644
--- a/tests/golden/storage/manifests/singleton-db-statefulset.yaml
+++ b/tests/golden/storage/manifests/singleton-db-statefulset.yaml
@@ -16,6 +16,16 @@ spec:
       labels:
         k8ify.service: singleton-db
     spec:
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: k8ify.service
+                operator: In
+                values:
+                - singleton-db
+            topologyKey: kubernetes.io/hostname
       containers:
       - image: nginx
         imagePullPolicy: Always