-
Notifications
You must be signed in to change notification settings - Fork 6
feat: Add opt-in upgrade_drain_timeout to drain control nodes before an upgrade #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Aureliolo
wants to merge
1
commit into
ctrliq:devel
Choose a base branch
from
Aureliolo:feat/upgrade-drain-timeout
base: devel
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| --- | ||
| # The molecule AWX spec sets upgrade_drain_timeout, but the application image | ||
| # does not change between reconciles, so the drain must not run: an ordinary | ||
| # reconcile has to leave every control node enabled and accepting jobs. | ||
| - name: Validate upgrade_drain_timeout | ||
| tags: | ||
| - upgrade_drain | ||
| block: | ||
| - name: Get the AWX resource | ||
| k8s_info: | ||
| namespace: '{{ namespace }}' | ||
| api_version: awx.ansible.com/v1beta1 | ||
| kind: AWX | ||
| name: example-awx | ||
| register: drain_awx | ||
|
|
||
| - name: Assert the spec carries upgrade_drain_timeout | ||
| ansible.builtin.assert: | ||
| that: | ||
| - drain_awx.resources | length == 1 | ||
| - drain_awx.resources[0].spec.upgrade_drain_timeout | int > 0 | ||
| fail_msg: 'upgrade_drain_timeout is not set on the AWX resource' | ||
|
|
||
| - name: Get the web pod | ||
| k8s_info: | ||
| namespace: '{{ namespace }}' | ||
| api_version: v1 | ||
| kind: Pod | ||
| label_selectors: | ||
| - 'app.kubernetes.io/name=example-awx-web' | ||
| field_selectors: | ||
| - status.phase=Running | ||
| register: drain_web_pods | ||
|
|
||
| - name: Assert a web pod is running | ||
| ansible.builtin.assert: | ||
| that: | ||
| - drain_web_pods.resources | length | ||
| fail_msg: 'no running web pod to query the instances from' | ||
|
|
||
| - name: List the instances | ||
| k8s_exec: | ||
| namespace: '{{ namespace }}' | ||
| pod: '{{ drain_web_pods.resources[0].metadata.name }}' | ||
| container: example-awx-web | ||
| command: awx-manage list_instances | ||
| register: drain_instances | ||
| changed_when: false | ||
|
|
||
| # list_instances marks a disabled instance with a [DISABLED] prefix. | ||
| - name: Assert the reconcile left every control node enabled | ||
| ansible.builtin.assert: | ||
| that: | ||
| - "'[DISABLED]' not in drain_instances.stdout" | ||
| fail_msg: 'a reconcile without an image change disabled a control node: {{ drain_instances.stdout }}' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| --- | ||
| # Take the control nodes out of service and let their running jobs finish before | ||
| # the deployments roll to a new application version. | ||
| # | ||
| # termination_grace_period_seconds cannot cover an upgrade on its own. As soon as | ||
| # the first pod running the new version registers itself, every control node still | ||
| # on the old version stops its own services (the version check in AWX's | ||
| # cluster_node_heartbeat), which cuts short the preStop drain and fails the very | ||
| # jobs that drain was waiting for. Draining has to happen while the cluster is | ||
| # still wholly on the old version, which is here: the deployments have not been | ||
| # applied yet and the schema migration runs later still. | ||
|
|
||
| - name: Get the current task deployment | ||
| k8s_info: | ||
| api_version: apps/v1 | ||
| kind: Deployment | ||
| namespace: '{{ ansible_operator_meta.namespace }}' | ||
| name: '{{ ansible_operator_meta.name }}-task' | ||
| register: _task_deployment | ||
|
|
||
| - name: Set the currently deployed application image | ||
| set_fact: | ||
| _deployed_image: >- | ||
| {{ _task_deployment['resources'][0]['spec']['template']['spec']['containers'] | ||
| | selectattr('name', 'equalto', ansible_operator_meta.name ~ '-task') | ||
| | map(attribute='image') | first | default('') }} | ||
| when: _task_deployment['resources'] | length > 0 | ||
|
|
||
| # Nothing to drain on a fresh install, when the application image is unchanged | ||
| # (every other reconcile), or when no web pod is available to run awx-manage in. | ||
| - name: Drain the control nodes | ||
| when: | ||
| - _deployed_image | default('') | length > 0 | ||
| - _deployed_image != _image | ||
| - awx_web_pod_name | length > 0 | ||
| block: | ||
| - name: Get the running control node pods | ||
| k8s_info: | ||
| api_version: v1 | ||
| kind: Pod | ||
| namespace: '{{ ansible_operator_meta.namespace }}' | ||
| label_selectors: | ||
| - "app.kubernetes.io/name={{ ansible_operator_meta.name }}-task" | ||
| - "app.kubernetes.io/managed-by={{ deployment_type }}-operator" | ||
| - "app.kubernetes.io/component={{ deployment_type }}" | ||
| field_selectors: | ||
| - status.phase=Running | ||
| register: _control_pods | ||
|
|
||
| - name: Set the control node hostnames | ||
| set_fact: | ||
| _control_hostnames: >- | ||
| {{ _control_pods['resources'] | ||
| | rejectattr('metadata.deletionTimestamp', 'defined') | ||
| | map(attribute='metadata.name') | list }} | ||
| _drain_retries: >- | ||
| {{ [1, ((upgrade_drain_timeout | int) / 10) | round(0, 'ceil') | int] | max }} | ||
|
|
||
| # Disable all of them before waiting on any of them: an instance that is still | ||
| # enabled keeps accepting work, so draining them one at a time would not | ||
| # converge. Disabling sets the instance capacity to zero, so jobs submitted | ||
| # from here on stay pending and run once the new pods register. | ||
| - name: Stop the control nodes accepting new jobs | ||
| k8s_exec: | ||
| namespace: '{{ ansible_operator_meta.namespace }}' | ||
| pod: '{{ awx_web_pod_name }}' | ||
| container: '{{ ansible_operator_meta.name }}-web' | ||
| command: awx-manage disable_instance --hostname {{ item }} | ||
| loop: '{{ _control_hostnames }}' | ||
| failed_when: false | ||
|
|
||
| # --retry=1 turns the command into a probe: it exits non-zero while the | ||
| # instance still has running or waiting jobs. Retrying from here keeps the | ||
| # k8s_exec calls short instead of holding one exec stream open for the whole | ||
| # drain. | ||
| - name: Wait for the control nodes' running jobs to finish | ||
| k8s_exec: | ||
| namespace: '{{ ansible_operator_meta.namespace }}' | ||
| pod: '{{ awx_web_pod_name }}' | ||
| container: '{{ ansible_operator_meta.name }}-web' | ||
| command: awx-manage disable_instance --hostname {{ item }} --wait --retry=1 --retry_sleep=1 | ||
| loop: '{{ _control_hostnames }}' | ||
| register: _drain_result | ||
| until: _drain_result.return_code | default(1) == 0 | ||
| retries: '{{ _drain_retries | int }}' | ||
| delay: 10 | ||
| changed_when: false | ||
| ignore_errors: true | ||
|
|
||
| # A drain that never finishes must not block the upgrade, so this is a | ||
| # warning rather than a failure: the jobs still running on those nodes will | ||
| # fail as they would without this feature. | ||
| - name: Report control nodes that were still running jobs | ||
| debug: | ||
| msg: >- | ||
| {{ item.item }} still had running jobs after upgrade_drain_timeout | ||
| ({{ upgrade_drain_timeout }}s); continuing with the upgrade. | ||
| loop: '{{ _drain_result.results | default([]) }}' | ||
| loop_control: | ||
| label: '{{ item.item }}' | ||
| when: item.return_code | default(1) != 0 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
untilretries per loop item, and during those retries the registered variable holdsthat item's own result. The
.resultsaggregation only exists once the loop hasfinished, which is after
untilhas been evaluated, soreturn_codeis the rightreference here.
Checked on ansible-core 2.18.18 (the version in the operator image) and on 2.21.1,
using the same condition shape as this task, including the
| default(1)fallbackthat would have masked the problem if it were real:
Both items satisfy the condition on the first attempt. If it were reading the
aggregate, r.rc would be undefined, default(1) would evaluate it to false, and
each item would show attempts=5 followed by a "Failed until condition" failure.
Leaving the task as is.