Skip to content

Commit

Permalink
Merge pull request #162 from LBGarber/fix/nb-config-update
Browse files Browse the repository at this point in the history
fix: Resolve issue when attempting to update existing NodeBalancer configs
  • Loading branch information
LBGarber authored Jun 9, 2022
2 parents 240a7e5 + e7c6aa8 commit d0c9309
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 8 deletions.
1 change: 1 addition & 0 deletions docs/modules/nodebalancer.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Manage a Linode NodeBalancer.
| `port` | `int` | Optional | The port this Config is for. |
| `protocol` | `str` | Optional | The protocol this port is configured to serve. (Choices: `http` `https` `tcp`) |
| `proxy_protocol` | `str` | Optional | ProxyProtocol is a TCP extension that sends initial TCP connection information such as source/destination IPs and ports to backend devices. (Choices: `none` `v1` `v2`) |
| `recreate` | `bool` | Optional | If true, the config will be forcibly recreated on every run. This is useful for updates to redacted fields (`ssl_cert`, `ssl_key`) |
| `ssl_cert` | `str` | Optional | The PEM-formatted public SSL certificate (or the combined PEM-formatted SSL certificate and Certificate Authority chain) that should be served on this NodeBalancerConfig’s port. |
| `ssl_key` | `str` | Optional | The PEM-formatted private key for the SSL certificate set in the ssl_cert field. |
| `stickiness` | `str` | Optional | Controls how session stickiness is handled on this port. (Choices: `none` `table` `http_cookie`) |
Expand Down
43 changes: 35 additions & 8 deletions plugins/modules/nodebalancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from __future__ import absolute_import, division, print_function

import copy
from typing import Optional, cast, Any, List, Set, Tuple

import linode_api4
Expand Down Expand Up @@ -111,6 +112,12 @@
'information such as source/destination IPs and ports to backend devices.',
choices=['none', 'v1', 'v2']),

recreate=dict(
type='bool', required=False, default=False,
description='If true, the config will be forcibly recreated on every run. '
'This is useful for updates to redacted fields (`ssl_cert`, `ssl_key`)'
),

ssl_cert=dict(
type='str', required=False,
description='The PEM-formatted public SSL certificate (or the combined '
Expand Down Expand Up @@ -333,9 +340,15 @@ def _check_config_exists(target: Set[NodeBalancerConfig], config: dict) \
-> Tuple[bool, Optional[NodeBalancerConfig]]:
"""Returns whether a config exists in the target set"""

tmp_config = copy.deepcopy(config)

# These fields will return as <REDACTED> so we should not diff on them
tmp_config.pop('ssl_cert')
tmp_config.pop('ssl_key')

for remote_config in target:
config_match, remote_config_match = \
dict_select_matching(filter_null_values(config), remote_config._raw_json)
dict_select_matching(filter_null_values(tmp_config), remote_config._raw_json)

if config_match == remote_config_match:
return True, remote_config
Expand All @@ -348,22 +361,36 @@ def _handle_configs(self) -> None:
new_configs = self.module.params.get('configs') or []
remote_configs = set(self._node_balancer.configs)

to_create = []
to_update = []
to_delete = remote_configs

for config in new_configs:
config_exists, remote_config = self._check_config_exists(remote_configs, config)

if config_exists:
if config.get('nodes') is not None:
self._handle_config_nodes(remote_config, config.get('nodes'))
remote_configs.remove(remote_config)
if not config_exists:
to_create.append((config, remote_config))
continue

if config.get('recreate'):
to_create.append((config, remote_config))
continue

to_update.append((config, remote_config))
to_delete.remove(remote_config)

# Remove remaining configs
for config in to_delete:
self._delete_config_register(config)

for config, remote_config in to_create:
new_config = self._create_config_register(self._node_balancer, config)
if config.get('nodes') is not None:
self._handle_config_nodes(new_config, config.get('nodes'))

# Remove remaining configs
for config in remote_configs:
self._delete_config_register(config)
for config, remote_config in to_update:
if config.get('nodes') is not None:
self._handle_config_nodes(remote_config, config.get('nodes'))

cast(list, self.results['configs']) \
.extend(paginated_list_to_json(self._node_balancer.configs))
Expand Down
59 changes: 59 additions & 0 deletions tests/integration/targets/nodebalancer_basic/tasks/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,65 @@
- create_empty_nodebalancer.changed
- create_empty_nodebalancer.configs|length == 0

- name: Add NodeBalancer config
linode.cloud.nodebalancer:
api_token: '{{ api_token }}'
label: '{{ create_empty_nodebalancer.node_balancer.label }}'
region: us-east
client_conn_throttle: 6
state: present
configs:
- port: 80
protocol: http
algorithm: roundrobin
register: create_config

- assert:
that:
- create_config.configs|length == 1
- create_config.configs[0].port == 80

- name: Update NodeBalancer config
linode.cloud.nodebalancer:
api_token: '{{ api_token }}'
label: '{{ create_empty_nodebalancer.node_balancer.label }}'
region: us-east
client_conn_throttle: 6
state: present
configs:
- port: 80
protocol: http
algorithm: roundrobin
check_timeout: 1
register: update_config

- assert:
that:
- update_config.configs|length == 1
- update_config.configs[0].check_timeout == 1

- update_config.changed

- name: Recreate NodeBalancer config
linode.cloud.nodebalancer:
api_token: '{{ api_token }}'
label: '{{ create_empty_nodebalancer.node_balancer.label }}'
region: us-east
client_conn_throttle: 6
state: present
configs:
- port: 80
protocol: http
algorithm: roundrobin
check_timeout: 1
recreate: true
register: recreate_config

- assert:
that:
- recreate_config.changed
- update_config.configs|length == 1

always:
- ignore_errors: yes
block:
Expand Down

0 comments on commit d0c9309

Please sign in to comment.