Skip to content

Commit

Permalink
Add support for k8s pod to node affinity and taint toleration (#917)
Browse files Browse the repository at this point in the history
Reviewed-on: https://git.vdb.to/cerc-io/stack-orchestrator/pulls/917
Reviewed-by: Thomas E Lackey <telackey@noreply.git.vdb.to>
Co-authored-by: David Boreham <david@bozemanpass.com>
Co-committed-by: David Boreham <david@bozemanpass.com>
  • Loading branch information
dboreham authored and David Boreham committed Aug 15, 2024
1 parent 60d3421 commit e56da7d
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 1 deletion.
69 changes: 69 additions & 0 deletions .gitea/workflows/test-k8s-deployment-control.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: K8s Deployment Control Test

on:
pull_request:
branches: '*'
push:
branches: '*'
paths:
- '!**'
- '.gitea/workflows/triggers/test-k8s-deployment-control'
- '.gitea/workflows/test-k8s-deployment-control.yml'
- 'tests/k8s-deployment-control/run-test.sh'
schedule: # Note: coordinate with other tests to not overload runners at the same time of day
- cron: '3 30 * * *'

jobs:
test:
name: "Run deployment control suite on kind/k8s"
runs-on: ubuntu-22.04
steps:
- name: "Clone project repository"
uses: actions/checkout@v3
# At present the stock setup-python action fails on Linux/aarch64
# Conditional steps below workaroud this by using deadsnakes for that case only
- name: "Install Python for ARM on Linux"
if: ${{ runner.arch == 'arm64' && runner.os == 'Linux' }}
uses: deadsnakes/action@v3.0.1
with:
python-version: '3.8'
- name: "Install Python cases other than ARM on Linux"
if: ${{ ! (runner.arch == 'arm64' && runner.os == 'Linux') }}
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: "Print Python version"
run: python3 --version
- name: "Install shiv"
run: pip install shiv
- name: "Generate build version file"
run: ./scripts/create_build_tag_file.sh
- name: "Build local shiv package"
run: ./scripts/build_shiv_package.sh
- name: "Check cgroups version"
run: mount | grep cgroup
- name: "Install kind"
run: ./tests/scripts/install-kind.sh
- name: "Install Kubectl"
run: ./tests/scripts/install-kubectl.sh
- name: "Run k8s deployment control test"
run: |
source /opt/bash-utils/cgroup-helper.sh
join_cgroup
./tests/k8s-deployment-control/run-test.sh
- name: Notify Vulcanize Slack on CI failure
if: ${{ always() && github.ref_name == 'main' }}
uses: ravsamhq/notify-slack-action@v2
with:
status: ${{ job.status }}
notify_when: 'failure'
env:
SLACK_WEBHOOK_URL: ${{ secrets.VULCANIZE_SLACK_CI_ALERTS }}
- name: Notify DeepStack Slack on CI failure
if: ${{ always() && github.ref_name == 'main' }}
uses: ravsamhq/notify-slack-action@v2
with:
status: ${{ job.status }}
notify_when: 'failure'
env:
SLACK_WEBHOOK_URL: ${{ secrets.DEEPSTACK_SLACK_CI_ALERTS }}
Empty file.
27 changes: 27 additions & 0 deletions docs/k8s-deployment-enhancements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# K8S Deployment Enhancements
## Controlling pod placement
The placement of pods created as part of a stack deployment can be controlled to either avoid certain nodes, or require certain nodes.
### Pod/Node Affinity
Node affinity rules applied to pods target node labels. The effect is that a pod can only be placed on a node having the specified label value. Note that other pods that do not have any node affinity rules can also be placed on those same nodes. Thus node affinity for a pod controls where that pod can be placed, but does not control where other pods are placed.

Node affinity for stack pods is specified in the deployment's `spec.yml` file as follows:
```
node-affinities:
- label: nodetype
value: typeb
```
This example denotes that the stack's pods should only be placed on nodes that have the label `nodetype` with value `typeb`.
### Node Taint Toleration
K8s nodes can be given one or more "taints". These are special fields (distinct from labels) with a name (key) and optional value.
When placing pods, the k8s scheduler will only assign a pod to a tainted node if the pod posesses a corresponding "toleration".
This is metadata associated with the pod that specifies that the pod "tolerates" a given taint.
Therefore taint toleration provides a mechanism by which only certain pods can be placed on specific nodes, and provides a complementary mechanism to node affinity.

Taint toleration for stack pods is specified in the deployment's `spec.yml` file as follows:
```
node-tolerations:
- key: nodetype
value: typeb
```
This example denotes that the stack's pods will tolerate a taint: `nodetype=typeb`

2 changes: 2 additions & 0 deletions stack_orchestrator/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,7 @@
annotations_key = "annotations"
labels_key = "labels"
replicas_key = "replicas"
node_affinities_key = "node-affinities"
node_tolerations_key = "node-tolerations"
kind_config_filename = "kind-config.yml"
kube_config_filename = "kubeconfig.yml"
44 changes: 43 additions & 1 deletion stack_orchestrator/deploy/k8s/cluster_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,8 @@ def get_deployment(self, image_pull_policy: str = None):

annotations = None
labels = {"app": self.app_name}
affinity = None
tolerations = None

if self.spec.get_annotations():
annotations = {}
Expand All @@ -377,12 +379,52 @@ def get_deployment(self, image_pull_policy: str = None):
for service_name in services:
labels[key.replace("{name}", service_name)] = value

if self.spec.get_node_affinities():
affinities = []
for rule in self.spec.get_node_affinities():
# TODO add some input validation here
label_name = rule['label']
label_value = rule['value']
affinities.append(client.V1NodeSelectorTerm(
match_expressions=[client.V1NodeSelectorRequirement(
key=label_name,
operator="In",
values=[label_value]
)]
)
)
affinity = client.V1Affinity(
node_affinity=client.V1NodeAffinity(
required_during_scheduling_ignored_during_execution=client.V1NodeSelector(
node_selector_terms=affinities
))
)

if self.spec.get_node_tolerations():
tolerations = []
for toleration in self.spec.get_node_tolerations():
# TODO add some input validation here
toleration_key = toleration['key']
toleration_value = toleration['value']
tolerations.append(client.V1Toleration(
effect="NoSchedule",
key=toleration_key,
operator="Equal",
value=toleration_value
))

template = client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(
annotations=annotations,
labels=labels
),
spec=client.V1PodSpec(containers=containers, image_pull_secrets=image_pull_secrets, volumes=volumes),
spec=client.V1PodSpec(
containers=containers,
image_pull_secrets=image_pull_secrets,
volumes=volumes,
affinity=affinity,
tolerations=tolerations
),
)
spec = client.V1DeploymentSpec(
replicas=self.spec.get_replicas(),
Expand Down
6 changes: 6 additions & 0 deletions stack_orchestrator/deploy/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ def get_annotations(self):
def get_replicas(self):
return self.obj.get(constants.replicas_key, 1)

def get_node_affinities(self):
return self.obj.get(constants.node_affinities_key, [])

def get_node_tolerations(self):
return self.obj.get(constants.node_tolerations_key, [])

def get_labels(self):
return self.obj.get(constants.labels_key, {})

Expand Down
Loading

0 comments on commit e56da7d

Please sign in to comment.