Skip to content
This repository has been archived by the owner on Jun 13, 2024. It is now read-only.

Commit

Permalink
Improve k8s Deployment and Daemonset wait conditions
Browse files Browse the repository at this point in the history
Ensure that Deployments and Daemonsets properly await
all replicas to be available. Correctly handles the
subtle edge case when a service account no longer exists.

Note that this will dramatically slow Daemonset updates
  • Loading branch information
willthames committed May 14, 2020
1 parent 5f3d2fc commit 35ffd0e
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 17 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ After the version is published, verify it exists on the [Kubernetes Collection G

## More Information

For more information about Ansible's Kubernetes integration, join the `#ansible-community` channel on Freenode IRC, and browse the resources in the [Kubernetes Working Group](https://github.com/ansible/community/wiki/Kubernetes) Community wiki page.
For more information about Ansible's Kubernetes integration, join the `#ansible-kubernetes` channel on Freenode IRC, and browse the resources in the [Kubernetes Working Group](https://github.com/ansible/community/wiki/Kubernetes) Community wiki page.

## License

Expand Down
2 changes: 2 additions & 0 deletions molecule/default/molecule.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ provisioner:
ansible_python_interpreter: '{{ ansible_playbook_python }}'
env:
ANSIBLE_FORCE_COLOR: 'true'
options:
vvv: True
scenario:
name: default
test_sequence:
Expand Down
156 changes: 156 additions & 0 deletions molecule/default/tasks/apply.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,162 @@
- k8s_service_3.result.spec.ports | length == 1
- k8s_service_3.result.spec.ports[0].port == 8081

- name: Insert new service port
k8s:
definition:
apiVersion: v1
kind: Service
metadata:
name: apply-svc
namespace: "{{ apply_namespace }}"
spec:
selector:
app: whatever
ports:
- name: mesh
port: 8080
targetPort: 8080
- name: http
port: 8081
targetPort: 8081
apply: yes
register: k8s_service_4

- name: Check ports are correct
assert:
that:
- k8s_service_4 is changed
- k8s_service_4.result.spec.ports | length == 2
- k8s_service_4.result.spec.ports[0].port == 8080
- k8s_service_4.result.spec.ports[1].port == 8081

- name: Remove new service port (check mode)
k8s:
definition:
apiVersion: v1
kind: Service
metadata:
name: apply-svc
namespace: "{{ apply_namespace }}"
spec:
selector:
app: whatever
ports:
- name: http
port: 8081
targetPort: 8081
apply: yes
check_mode: yes
register: k8s_service_check

- name: Check ports are correct
assert:
that:
- k8s_service_check is changed
- k8s_service_check.result.spec.ports | length == 1
- k8s_service_check.result.spec.ports[0].port == 8081

- name: Remove new service port
k8s:
definition:
apiVersion: v1
kind: Service
metadata:
name: apply-svc
namespace: "{{ apply_namespace }}"
spec:
selector:
app: whatever
ports:
- name: http
port: 8081
targetPort: 8081
apply: yes
register: k8s_service_5

- name: Check ports are correct
assert:
that:
- k8s_service_5 is changed
- k8s_service_5.result.spec.ports | length == 1
- k8s_service_5.result.spec.ports[0].port == 8081

- name: Add a serviceaccount
k8s:
definition:
apiVersion: v1
kind: ServiceAccount
metadata:
name: apply-deploy
namespace: "{{ apply_namespace }}"

- name: Add a deployment
k8s:
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: apply-deploy
namespace: "{{ apply_namespace }}"
spec:
replicas: 1
selector:
matchLabels:
app: "{{ k8s_pod_name }}"
template: "{{ k8s_pod_template }}"
wait: yes
apply: yes
vars:
k8s_pod_name: apply-deploy
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:v0.10.0-green
k8s_pod_service_account: apply-deploy
k8s_pod_ports:
- containerPort: 8080
name: http
protocol: TCP

- name: Remove the serviceaccount
k8s:
state: absent
definition:
apiVersion: v1
kind: ServiceAccount
metadata:
name: apply-deploy
namespace: "{{ apply_namespace }}"

- name: Update the earlier deployment
k8s:
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: apply-deploy
namespace: "{{ apply_namespace }}"
spec:
replicas: 1
selector:
matchLabels:
app: "{{ k8s_pod_name }}"
template: "{{ k8s_pod_template }}"
wait: yes
apply: yes
vars:
k8s_pod_name: apply-deploy
k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:v0.10.0-purple
k8s_pod_service_account: apply-deploy
k8s_pod_ports:
- containerPort: 8080
name: http
protocol: TCP
register: deploy_after_serviceaccount_removal
ignore_errors: yes

- name: Ensure that updating deployment after service account removal failed
assert:
that:
- deploy_after_serviceaccount_removal is failed

always:
- name: Remove namespace
k8s:
Expand Down
3 changes: 3 additions & 0 deletions molecule/default/vars/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ k8s_pod_metadata:
app: "{{ k8s_pod_name }}"

k8s_pod_spec:
serviceAccount: "{{ k8s_pod_service_account }}"
containers:
- image: "{{ k8s_pod_image }}"
imagePullPolicy: Always
Expand All @@ -20,6 +21,8 @@ k8s_pod_spec:
memory: "100Mi"
ports: "{{ k8s_pod_ports }}"

k8s_pod_service_account: default

k8s_pod_command: []

k8s_pod_ports: []
Expand Down
6 changes: 4 additions & 2 deletions plugins/module_utils/raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,8 @@ def _deployment_ready(deployment):
# Furthermore deployment.status.availableReplicas == deployment.status.replicas == None if status is empty
return (deployment.status and deployment.status.replicas is not None and
deployment.status.availableReplicas == deployment.status.replicas and
deployment.status.observedGeneration == deployment.metadata.generation)
deployment.status.observedGeneration == deployment.metadata.generation and
not deployment.status.unavailableReplicas)

def _pod_ready(pod):
return (pod.status and pod.status.containerStatuses is not None and
Expand All @@ -478,7 +479,8 @@ def _pod_ready(pod):
def _daemonset_ready(daemonset):
return (daemonset.status and daemonset.status.desiredNumberScheduled is not None and
daemonset.status.numberReady == daemonset.status.desiredNumberScheduled and
daemonset.status.observedGeneration == daemonset.metadata.generation)
daemonset.status.observedGeneration == daemonset.metadata.generation and
not daemonset.status.unavailableReplicas)

def _custom_condition(resource):
if not resource.status or not resource.status.conditions:
Expand Down
30 changes: 16 additions & 14 deletions plugins/module_utils/scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@

try:
import yaml
from openshift import watch
from openshift.dynamic.client import ResourceInstance
from openshift.helper.exceptions import KubernetesException
except ImportError as exc:
class KubernetesException(Exception):
from openshift.dynamic.exceptions import NotFoundError
except ImportError:
pass
try:
from openshift import watch
except ImportError:
try:
from openshift.dynamic.client import watch
except ImportError:
pass


Expand Down Expand Up @@ -114,7 +119,7 @@ def execute_module(self):
try:
existing = resource.get(name=name, namespace=namespace)
return_attributes['result'] = existing.to_dict()
except KubernetesException as exc:
except NotFoundError as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc),
error=exc.value.get('status'))

Expand Down Expand Up @@ -189,15 +194,12 @@ def _create_stream(self, resource, namespace, wait_time):
""" Create a stream of events for the object """
w = None
stream = None
try:
w = watch.Watch()
w._api_client = self.client.client
if namespace:
stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
else:
stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
except KubernetesException:
pass
w = watch.Watch()
w._api_client = self.client.client
if namespace:
stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
else:
stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
return w, stream

def _read_stream(self, resource, watcher, stream, name, replicas):
Expand Down

0 comments on commit 35ffd0e

Please sign in to comment.