diff --git a/docs/advanced-function-deployment.md b/docs/advanced-function-deployment.md index 106a38b1c..bb07a9c12 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 dbaf2e64e..a51f123c3 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 diff --git a/pkg/utils/kubelessutil_test.go b/pkg/utils/kubelessutil_test.go index 44ccaacde..0cc462084 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 {