Repository: kubeless/kubeless
Pull request #1115 (2020-02-20 01:09:19)

Add soft pod anti affinity
**Issue Ref**: None
 
**Description**: 

Adds a soft pod anti affinity rule to function deployments so that, where possible, function pods are deployed to different nodes.

Being only a soft or "PreferredDuringSchedulingIgnoredDuringExecution" anti affinity rule it remains backwards compatible because if the rule can't be satisfied it'll fall back to the current behavior.

Some reading for those unfamiliar: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#inter-pod-affinity-and-anti-affinity

**TODOs**:
 - [X] Ready to review
 - [X] Automated Tests
 - [X] Docs

I'm unsure if this needs to be documented anywhere, point me in the right direction if it should be.

d0x2f: Thanks again for reviewing @andresmgot 
I've added the doc section and addressed your comments.

Patch:
diff --git a/docs/advanced-function-deployment.md b/docs/advanced-function-deployment.md
index 106a38b1..bb07a9c1 100644
--- a/docs/advanced-function-deployment.md
+++ b/docs/advanced-function-deployment.md
@@ -35,6 +35,10 @@ The fields that a Function specification can contain are:
 
 Apart from the basic parameters, it is possible to add the specification of a `Deployment`, a `Service` or an `Horizontal Pod Autoscaler` that Kubeless will use to generate them.
 
+## Pod Anti Affinity
+
+By default, a kubless generated `Deployment` will include a soft pod anti-affinity rule that will signal to kubernetes that it should try to deploy pods to different nodes. This behaviour can be overridden using a deployment template.
+
 ## Deploying large functions
 
 As any Kubernetes object, function objects have a maximum size of 1.5MiB (due to the [maximum size](https://github.com/etcd-io/etcd/blob/master/Documentation/dev-guide/limit.md#request-size-limit) of an etcd entry). Because of that, it's not possible to specify in the `function` field of the YAML content that surpasses that size. To workaround this issue it's possible to specify an URL in the `function` field. This file will be downloaded at build time (extracted if necessary) and the checksum will be checked. Doing this we avoid any limitation regarding the file size. It's also possible to include the function dependencies in this file and skip the dependency installation step. Note that since the file will be downloaded in a pod the URL should be accessible from within the cluster:
@@ -91,7 +95,7 @@ spec:
 ```
 
 Would create a function with the environment variable `FOO`, using CPU and memory limits and mounting the secret `my-secret` as a volume. Note that you can also specify a default template for a Deployment spec in the [controller configuration](/docs/function-controller-configuration).
-The resource configuration in `initContainers` will be applied to all of the initial containers in the target deployment (like `provision`, `compile` etc.)   
+The resource configuration in `initContainers` will be applied to all of the initial containers in the target deployment (like `provision`, `compile` etc.)
 
 
 ## Custom Service
diff --git a/pkg/utils/kubelessutil.go b/pkg/utils/kubelessutil.go
index dbaf2e64..a51f123c 100644
--- a/pkg/utils/kubelessutil.go
+++ b/pkg/utils/kubelessutil.go
@@ -684,6 +684,28 @@ func EnsureFuncDeployment(client kubernetes.Interface, funcObj *kubelessApi.Func
 		}
 	}
 
+	// Add soft pod anti affinity
+	if dpm.Spec.Template.Spec.Affinity == nil {
+		dpm.Spec.Template.Spec.Affinity = &v1.Affinity{
+			PodAntiAffinity: &v1.PodAntiAffinity{
+				PreferredDuringSchedulingIgnoredDuringExecution: []v1.WeightedPodAffinityTerm{
+					{
+						Weight: 100,
+						PodAffinityTerm: v1.PodAffinityTerm{
+							LabelSelector: &metav1.LabelSelector{
+								MatchLabels: map[string]string{
+									"created-by": "kubeless",
+									"function":   funcObj.ObjectMeta.Name,
+								},
+							},
+							TopologyKey: "kubernetes.io/hostname",
+						},
+					},
+				},
+			},
+		}
+	}
+
 	_, err = client.AppsV1().Deployments(funcObj.ObjectMeta.Namespace).Create(dpm)
 	if err != nil && k8sErrors.IsAlreadyExists(err) {
 		// In case the Deployment already exists we should update


Test patch:
diff --git a/pkg/utils/kubelessutil_test.go b/pkg/utils/kubelessutil_test.go
index 44ccaacd..0cc46208 100644
--- a/pkg/utils/kubelessutil_test.go
+++ b/pkg/utils/kubelessutil_test.go
@@ -636,10 +636,34 @@ func TestEnsureDeployment(t *testing.T) {
 			},
 		},
 	}
+
 	if !reflect.DeepEqual(dpm.Spec.Template.Spec.Containers[0], expectedContainer) {
 		t.Errorf("Unexpected container definition. Received:\n %+v\nExpecting:\n %+v", dpm.Spec.Template.Spec.Containers[0], expectedContainer)
 	}
 
+	expectedAffinity := &v1.Affinity{
+		PodAntiAffinity: &v1.PodAntiAffinity{
+			PreferredDuringSchedulingIgnoredDuringExecution: []v1.WeightedPodAffinityTerm{
+				{
+					Weight: 100,
+					PodAffinityTerm: v1.PodAffinityTerm{
+						LabelSelector: &metav1.LabelSelector{
+							MatchLabels: map[string]string{
+								"created-by": "kubeless",
+								"function":   f1Name,
+							},
+						},
+						TopologyKey: "kubernetes.io/hostname",
+					},
+				},
+			},
+		},
+	}
+
+	if !reflect.DeepEqual(dpm.Spec.Template.Spec.Affinity, expectedAffinity) {
+		t.Errorf("Unexpected pod affinity definition. Received:\n %+v\nExpecting:\n %+v", dpm.Spec.Template.Spec.Affinity, expectedAffinity)
+	}
+
 	secrets := dpm.Spec.Template.Spec.ImagePullSecrets
 	if secrets[0].Name != "creds" && secrets[1].Name != "p1" && secrets[2].Name != "p2" {
 		t.Errorf("Expected first secret to be 'p1' but found %v and second secret to be 'p2' and found %v", secrets[0], secrets[1])
@@ -847,6 +871,30 @@ func TestDeploymentWithVolumes(t *testing.T) {
 	}
 }
 
+func TestEnsureDeploymentWithAffinityOverridden(t *testing.T) {
+	funcName := "func"
+	clientset, or, ns, lr := prepareDeploymentTest(funcName)
+	// If the Image has been already provided it should not resolve it
+	f3 := getDefaultFunc(funcName, ns)
+	f3.Spec.Deployment.Spec.Template.Spec.Affinity = &v1.Affinity{}
+	err := EnsureFuncDeployment(clientset, f3, or, lr, "", "unzip", []v1.LocalObjectReference{})
+	if err != nil {
+		t.Errorf("Unexpected error: %s", err)
+	}
+	dpm, err := clientset.AppsV1().Deployments(ns).Get(funcName, metav1.GetOptions{})
+	if err != nil {
+		t.Errorf("Unexpected error: %s", err)
+	}
+	expectedAffinity := &v1.Affinity{NodeAffinity: nil, PodAffinity: nil, PodAntiAffinity: nil}
+	if *dpm.Spec.Template.Spec.Affinity != *expectedAffinity {
+		t.Errorf(
+			"Unexpected Affinity Definition:\nExpecting: %+v\nReceived: %+v",
+			expectedAffinity,
+			dpm.Spec.Template.Spec.Affinity,
+		)
+	}
+}
+
 func doesNotContain(envs []v1.EnvVar, env v1.EnvVar) bool {
 	for _, e := range envs {
 		if e == env {