Skip to content

Commit

Permalink
Merge pull request #124 from vshn/APPX-24-multiple-replicas
Browse files Browse the repository at this point in the history
feat: Create PodDisruptionBudgets and set Anti-Affinity
  • Loading branch information
davidgubler authored Apr 9, 2024
2 parents 1e2580e + 1f62324 commit e90a449
Show file tree
Hide file tree
Showing 35 changed files with 478 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' || '' }}"
Expand Down
22 changes: 22 additions & 0 deletions docs/conversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions internal/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
57 changes: 56 additions & 1 deletion pkg/converter/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package converter

import (
"fmt"
v1 "k8s.io/api/policy/v1"
"log"
"maps"
"os"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -260,6 +260,7 @@ func composeServiceToPodTemplate(
RestartPolicy: core.RestartPolicyAlways,
Volumes: volumesArray,
ServiceAccountName: serviceAccountName,
Affinity: composeServiceToAffinity(workload),
}

return core.PodTemplateSpec{
Expand All @@ -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,
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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
Expand All @@ -778,6 +831,7 @@ type Objects struct {
PersistentVolumeClaims []core.PersistentVolumeClaim
Secrets []core.Secret
Ingresses []networking.Ingress
PodDisruptionBudgets []v1.PodDisruptionBudget
Others []unstructured.Unstructured
}

Expand All @@ -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...),
}
}
10 changes: 10 additions & 0 deletions tests/golden/101/manifests/nginx-oasp-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions tests/golden/101/manifests/nginx-oasp-poddisruptionbudget.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions tests/golden/defaults/manifests/nginx-oasp-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions tests/golden/demo/manifests/mongo-statefulset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions tests/golden/demo/manifests/portal-oasp-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions tests/golden/demo/manifests/portal-oasp-poddisruptionbudget.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions tests/golden/env-vars/manifests/fooBar-oasp-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions tests/golden/expose-plain/manifests/nginx-oasp-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions tests/golden/noports/manifests/pinger-oasp-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit e90a449

Please sign in to comment.