Skip to content

Commit

Permalink
docker_compose_v2: move some code to module_utils (#747)
Browse files Browse the repository at this point in the history
* Move some code to module_utils.

* Add unit tests.

Test cases are auto-generated from integration test logs.

* Rename ResourceEvent → Event.
  • Loading branch information
felixfontein authored Jan 7, 2024
1 parent eed89f3 commit cb4dd2f
Show file tree
Hide file tree
Showing 4 changed files with 16,684 additions and 252 deletions.
271 changes: 271 additions & 0 deletions plugins/module_utils/compose_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
# Copyright (c) 2023, Felix Fontein <felix@fontein.de>
# Copyright (c) 2023, Léo El Amri (@lel-amri)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type


import re
from collections import namedtuple

from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six.moves import shlex_quote


DOCKER_STATUS_DONE = frozenset((
'Started',
'Healthy',
'Exited',
'Restarted',
'Running',
'Created',
'Stopped',
'Killed',
'Removed',
# An extra, specific to containers
'Recreated',
# Extras for pull events
'Pulled',
))
DOCKER_STATUS_WORKING = frozenset((
'Creating',
'Starting',
'Waiting',
'Restarting',
'Stopping',
'Killing',
'Removing',
# An extra, specific to containers
'Recreate',
# Extras for pull events
'Pulling',
))
DOCKER_STATUS_PULL = frozenset((
'Pulled',
'Pulling',
))
DOCKER_STATUS_ERROR = frozenset((
'Error',
))
DOCKER_STATUS = frozenset(DOCKER_STATUS_DONE | DOCKER_STATUS_WORKING | DOCKER_STATUS_PULL | DOCKER_STATUS_ERROR)


class ResourceType(object):
UNKNOWN = "unknown"
NETWORK = "network"
IMAGE = "image"
VOLUME = "volume"
CONTAINER = "container"
SERVICE = "service"

@classmethod
def from_docker_compose_event(cls, resource_type):
# type: (Type[ResourceType], Text) -> Any
return {
"Network": cls.NETWORK,
"Image": cls.IMAGE,
"Volume": cls.VOLUME,
"Container": cls.CONTAINER,
}[resource_type]


Event = namedtuple(
'Event',
['resource_type', 'resource_id', 'status', 'msg']
)


_DRY_RUN_MARKER = 'DRY-RUN MODE -'

_RE_RESOURCE_EVENT = re.compile(
r'^'
r'\s*'
r'(?P<resource_type>Network|Image|Volume|Container)'
r'\s+'
r'(?P<resource_id>\S+)'
r'\s+'
r'(?P<status>\S(?:|.*\S))'
r'\s*'
r'$'
)

_RE_PULL_EVENT = re.compile(
r'^'
r'\s*'
r'(?P<service>\S+)'
r'\s+'
r'(?P<status>%s)'
r'\s*'
r'$'
% '|'.join(re.escape(status) for status in DOCKER_STATUS_PULL)
)

_RE_ERROR_EVENT = re.compile(
r'^'
r'\s*'
r'(?P<resource_id>\S+)'
r'\s+'
r'(?P<status>%s)'
r'\s*'
r'$'
% '|'.join(re.escape(status) for status in DOCKER_STATUS_ERROR)
)


def parse_events(stderr, dry_run=False, warn_function=None):
events = []
error_event = None
for line in stderr.splitlines():
line = to_native(line.strip())
if not line:
continue
if dry_run:
if line.startswith(_DRY_RUN_MARKER):
line = line[len(_DRY_RUN_MARKER):].lstrip()
elif error_event is None and warn_function:
# This could be a bug, a change of docker compose's output format, ...
# Tell the user to report it to us :-)
warn_function(
'Event line is missing dry-run mode marker: {0!r}. Please report this at '
'https://github.com/ansible-collections/community.docker/issues/new?assignees=&labels=&projects=&template=bug_report.md'
.format(line)
)
match = _RE_RESOURCE_EVENT.match(line)
if match is not None:
status = match.group('status')
msg = None
if status not in DOCKER_STATUS:
status, msg = msg, status
event = Event(
ResourceType.from_docker_compose_event(match.group('resource_type')),
match.group('resource_id'),
status,
msg,
)
events.append(event)
if status in DOCKER_STATUS_ERROR:
error_event = event
else:
error_event = None
continue
match = _RE_PULL_EVENT.match(line)
if match:
events.append(
Event(
ResourceType.SERVICE,
match.group('service'),
match.group('status'),
None,
)
)
error_event = None
continue
match = _RE_ERROR_EVENT.match(line)
if match:
error_event = Event(
ResourceType.UNKNOWN,
match.group('resource_id'),
match.group('status'),
None,
)
events.append(error_event)
continue
if error_event is not None:
# Unparsable line that apparently belongs to the previous error event
error_event = Event(
error_event.resource_type,
error_event.resource_id,
error_event.status,
'\n'.join(msg for msg in [error_event.msg, line] if msg is not None),
)
events[-1] = error_event
continue
if line.startswith('Error '):
# Error message that is independent of an error event
error_event = Event(
ResourceType.UNKNOWN,
'',
'Error',
line,
)
events.append(error_event)
continue
# This could be a bug, a change of docker compose's output format, ...
# Tell the user to report it to us :-)
if warn_function:
warn_function(
'Cannot parse event from line: {0!r}. Please report this at '
'https://github.com/ansible-collections/community.docker/issues/new?assignees=&labels=&projects=&template=bug_report.md'
.format(line)
)
return events


def has_changes(events):
for event in events:
if event.status in DOCKER_STATUS_WORKING:
return True
return False


def extract_actions(events):
actions = []
for event in events:
if event.status in DOCKER_STATUS_WORKING:
actions.append({
'what': event.resource_type,
'id': event.resource_id,
'status': event.status,
})
return actions


def emit_warnings(events, warn_function):
for event in events:
# If a message is present, assume it is a warning
if event.status is None and event.msg is not None:
warn_function('Docker compose: {resource_type} {resource_id}: {msg}'.format(
resource_type=event.resource_type,
resource_id=event.resource_id,
msg=event.msg,
))


def is_failed(events, rc):
if rc:
return True
for event in events:
if event.status in DOCKER_STATUS_ERROR:
return True
return False


def update_failed(result, events, args, stdout, stderr, rc, cli):
errors = []
for event in events:
if event.status in DOCKER_STATUS_ERROR:
msg = 'Error when processing {resource_type} {resource_id}: '
if event.resource_type == 'unknown':
msg = 'Error when processing {resource_id}: '
if event.resource_id == '':
msg = 'General error: '
msg += '{status}' if event.msg is None else '{msg}'
errors.append(msg.format(
resource_type=event.resource_type,
resource_id=event.resource_id,
status=event.status,
msg=event.msg,
))
if not errors and not rc:
return False
if not errors:
errors.append('Return code {code} is non-zero'.format(code=rc))
result['failed'] = True
result['msg'] = '\n'.join(errors)
result['cmd'] = ' '.join(shlex_quote(arg) for arg in [cli] + args)
result['stdout'] = to_native(stdout)
result['stderr'] = to_native(stderr)
result['rc'] = rc
return True
Loading

0 comments on commit cb4dd2f

Please sign in to comment.