diff --git a/changelogs/fragments/52895-docker_swarm-labels.yaml b/changelogs/fragments/52895-docker_swarm-labels.yaml new file mode 100644 index 00000000000..edb0aabec40 --- /dev/null +++ b/changelogs/fragments/52895-docker_swarm-labels.yaml @@ -0,0 +1,2 @@ +bugfixes: +- "docker_swarm - do not crash with older docker daemons (https://github.com/ansible/ansible/issues/51175)." diff --git a/lib/ansible/modules/cloud/docker/docker_swarm.py b/lib/ansible/modules/cloud/docker/docker_swarm.py index 4e01a9692be..90b2bf13e0e 100644 --- a/lib/ansible/modules/cloud/docker/docker_swarm.py +++ b/lib/ansible/modules/cloud/docker/docker_swarm.py @@ -239,15 +239,15 @@ except ImportError: from ansible.module_utils.docker.common import ( AnsibleDockerClient, DockerBaseClass, + DifferenceTracker, ) from ansible.module_utils._text import to_native class TaskParameters(DockerBaseClass): - def __init__(self, client): + def __init__(self): super(TaskParameters, self).__init__() - self.state = None self.advertise_addr = None self.listen_addr = None self.force_new_cluster = None @@ -274,13 +274,60 @@ class TaskParameters(DockerBaseClass): self.rotate_worker_token = None self.rotate_manager_token = None + @staticmethod + def from_ansible_params(client): + result = TaskParameters() for key, value in client.module.params.items(): - setattr(self, key, value) + if key in result.__dict__: + setattr(result, key, value) - self.update_parameters(client) + result.labels = result.labels or {} + + result.update_parameters(client) + return result + + def update_from_swarm_info(self, swarm_info): + spec = swarm_info['Spec'] + + ca_config = spec.get('CAConfig') or dict() + if self.node_cert_expiry is None: + self.node_cert_expiry = ca_config.get('NodeCertExpiry') + + dispatcher = spec.get('Dispatcher') or dict() + if self.dispatcher_heartbeat_period is None: + self.dispatcher_heartbeat_period = dispatcher.get('HeartbeatPeriod') + + raft = spec.get('Raft') or dict() + if self.snapshot_interval is None: + self.snapshot_interval = raft.get('SnapshotInterval') + if self.keep_old_snapshots is None: + self.keep_old_snapshots = raft.get('KeepOldSnapshots') + if self.heartbeat_tick is None: + self.heartbeat_tick = raft.get('HeartbeatTick') + if self.log_entries_for_slow_followers is None: + self.log_entries_for_slow_followers = raft.get('LogEntriesForSlowFollowers') + if self.election_tick is None: + self.election_tick = raft.get('ElectionTick') + + orchestration = spec.get('Orchestration') or dict() + if self.task_history_retention_limit is None: + self.task_history_retention_limit = orchestration.get('TaskHistoryRetentionLimit') + + encryption_config = spec.get('EncryptionConfig') or dict() + if self.autolock_managers is None: + self.autolock_managers = encryption_config.get('AutoLockManagers') + + if self.name is None: + self.name = spec['Name'] + + if self.labels is None: + self.labels = spec.get('Labels') or {} + + if 'LogDriver' in spec['TaskDefaults']: + self.log_driver = spec['TaskDefaults']['LogDriver'] def update_parameters(self, client): - self.spec = client.create_swarm_spec( + params = dict( snapshot_interval=self.snapshot_interval, task_history_retention_limit=self.task_history_retention_limit, keep_old_snapshots=self.keep_old_snapshots, @@ -290,13 +337,27 @@ class TaskParameters(DockerBaseClass): dispatcher_heartbeat_period=self.dispatcher_heartbeat_period, node_cert_expiry=self.node_cert_expiry, name=self.name, - labels=self.labels, signing_ca_cert=self.signing_ca_cert, signing_ca_key=self.signing_ca_key, ca_force_rotate=self.ca_force_rotate, autolock_managers=self.autolock_managers, - log_driver=self.log_driver + log_driver=self.log_driver, ) + if self.labels: + params['labels'] = self.labels + self.spec = client.create_swarm_spec(**params) + + def compare_to_active(self, other, differences): + for k in self.__dict__: + if k in ('advertise_addr', 'listen_addr', 'rotate_worker_token', 'rotate_manager_token', 'spec'): + continue + if self.__dict__[k] is None: + continue + if self.__dict__[k] != other.__dict__[k]: + differences.add(k, parameter=self.__dict__[k], active=other.__dict__[k]) + import q + q.q(k, self.__dict__[k], other.__dict__[k]) + return differences class SwarmManager(DockerBaseClass): @@ -310,7 +371,11 @@ class SwarmManager(DockerBaseClass): self.check_mode = self.client.check_mode self.swarm_info = {} - self.parameters = TaskParameters(client) + self.state = client.module.params['state'] + self.force = client.module.params['force'] + + self.differences = DifferenceTracker() + self.parameters = TaskParameters.from_ansible_params(client) def __call__(self): choice_map = { @@ -321,7 +386,7 @@ class SwarmManager(DockerBaseClass): "inspect": self.inspect_swarm } - choice_map.get(self.parameters.state)() + choice_map.get(self.state)() def __isSwarmManager(self): try: @@ -357,60 +422,27 @@ class SwarmManager(DockerBaseClass): self.__isSwarmManager() self.results['actions'].append("New Swarm cluster created: %s" % (self.swarm_info.get('ID'))) + self.differences.add('state', parameter='absent', active='present') self.results['changed'] = True self.results['swarm_facts'] = {u'JoinTokens': self.swarm_info.get('JoinTokens')} - def __update_spec(self, spec): - if (self.parameters.node_cert_expiry is None): - self.parameters.node_cert_expiry = spec['CAConfig']['NodeCertExpiry'] - - if (self.parameters.dispatcher_heartbeat_period is None): - self.parameters.dispatcher_heartbeat_period = spec['Dispatcher']['HeartbeatPeriod'] - - if (self.parameters.snapshot_interval is None): - self.parameters.snapshot_interval = spec['Raft']['SnapshotInterval'] - if (self.parameters.keep_old_snapshots is None): - self.parameters.keep_old_snapshots = spec['Raft']['KeepOldSnapshots'] - if (self.parameters.heartbeat_tick is None): - self.parameters.heartbeat_tick = spec['Raft']['HeartbeatTick'] - if (self.parameters.log_entries_for_slow_followers is None): - self.parameters.log_entries_for_slow_followers = spec['Raft']['LogEntriesForSlowFollowers'] - if (self.parameters.election_tick is None): - self.parameters.election_tick = spec['Raft']['ElectionTick'] - - if (self.parameters.task_history_retention_limit is None): - self.parameters.task_history_retention_limit = spec['Orchestration']['TaskHistoryRetentionLimit'] - - if (self.parameters.autolock_managers is None): - self.parameters.autolock_managers = spec['EncryptionConfig']['AutoLockManagers'] - - if (self.parameters.name is None): - self.parameters.name = spec['Name'] - - if (self.parameters.labels is None): - self.parameters.labels = spec['Labels'] - - if 'LogDriver' in spec['TaskDefaults']: - self.parameters.log_driver = spec['TaskDefaults']['LogDriver'] - - self.parameters.update_parameters(self.client) - - return self.parameters.spec - def __update_swarm(self): try: self.inspect_swarm() version = self.swarm_info['Version']['Index'] - spec = self.swarm_info['Spec'] - new_spec = self.__update_spec(spec) - del spec['TaskDefaults'] - if spec == new_spec: + self.parameters.update_from_swarm_info(self.swarm_info) + old_parameters = TaskParameters() + old_parameters.update_from_swarm_info(self.swarm_info) + self.parameters.compare_to_active(old_parameters, self.differences) + if self.differences.empty: self.results['actions'].append("No modification") self.results['changed'] = False return + self.parameters.update_parameters(self.client) if not self.check_mode: self.client.update_swarm( - version=version, swarm_spec=new_spec, rotate_worker_token=self.parameters.rotate_worker_token, + version=version, swarm_spec=self.parameters.spec, + rotate_worker_token=self.parameters.rotate_worker_token, rotate_manager_token=self.parameters.rotate_manager_token) except APIError as exc: self.client.fail("Can not update a Swarm Cluster: %s" % to_native(exc)) @@ -443,6 +475,7 @@ class SwarmManager(DockerBaseClass): except APIError as exc: self.client.fail("Can not join the Swarm Cluster: %s" % to_native(exc)) self.results['actions'].append("New node is added to swarm cluster") + self.differences.add('joined', parameter=True, active=False) self.results['changed'] = True def leave(self): @@ -451,10 +484,11 @@ class SwarmManager(DockerBaseClass): return if not self.check_mode: try: - self.client.leave_swarm(force=self.parameters.force) + self.client.leave_swarm(force=self.force) except APIError as exc: self.client.fail("This node can not leave the Swarm Cluster: %s" % to_native(exc)) self.results['actions'].append("Node has left the swarm cluster") + self.differences.add('joined', parameter='absent', active='present') self.results['changed'] = True def __get_node_info(self): @@ -488,10 +522,11 @@ class SwarmManager(DockerBaseClass): if not self.check_mode: try: - self.client.remove_node(node_id=self.parameters.node_id, force=self.parameters.force) + self.client.remove_node(node_id=self.parameters.node_id, force=self.force) except APIError as exc: self.client.fail("Can not remove the node from the Swarm Cluster: %s" % to_native(exc)) self.results['actions'].append("Node is removed from swarm cluster.") + self.differences.add('joined', parameter=False, active=True) self.results['changed'] = True @@ -528,6 +563,7 @@ def main(): ] option_minimal_versions = dict( + labels=dict(docker_api_version='1.32'), signing_ca_cert=dict(docker_api_version='1.30'), signing_ca_key=dict(docker_api_version='1.30'), ca_force_rotate=dict(docker_api_version='1.30'), diff --git a/test/integration/targets/docker_swarm/tasks/test_swarm.yml b/test/integration/targets/docker_swarm/tasks/test_swarm.yml index 73d6737997d..28cf74c826f 100644 --- a/test/integration/targets/docker_swarm/tasks/test_swarm.yml +++ b/test/integration/targets/docker_swarm/tasks/test_swarm.yml @@ -32,22 +32,26 @@ - name: Create a Swarm cluster (check mode) docker_swarm: state: present + advertise_addr: "{{ansible_default_ipv4.address}}" check_mode: yes register: output_1 - name: Create a Swarm cluster docker_swarm: state: present + advertise_addr: "{{ansible_default_ipv4.address}}" register: output_2 - name: Create a Swarm cluster (idempotent) docker_swarm: state: present + advertise_addr: "{{ansible_default_ipv4.address}}" register: output_3 - name: Create a Swarm cluster (idempotent, check mode) docker_swarm: state: present + advertise_addr: "{{ansible_default_ipv4.address}}" check_mode: yes register: output_4