Skip to content
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

docker_swarm_service: Extend env and add env_files support #51762

Merged
merged 7 commits into from
Feb 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
minor_changes:
- "docker_swarm_service - ``env`` parameter now supports setting values as a dict."
- "docker_swarm_service - Added support for ``env_files`` parameter."
72 changes: 67 additions & 5 deletions lib/ansible/modules/cloud/docker/docker_swarm_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,22 @@
- vip
- dnsrr
env:
type: list
type: raw
description:
- List of the service environment variables.
- List or dictionary of the service environment variables.
- If passed a list each items need to be in the format of C(KEY=VALUE).
- If passed a dictionary values which might be parsed as numbers,
booleans or other types by the YAML parser must be quoted (e.g. C("true"))
in order to avoid data loss.
- Corresponds to the C(--env) option of C(docker service create).
env_files:
type: list
description:
- List of paths to files, present on the target, containing environment variables C(FOO=BAR).
- The order of the list is significant in determining the value assigned to a
variable that shows up more than once.
- If variable also present in I(env), then I(env) value will override.
version_added: "2.8"
log_driver:
type: str
description:
Expand Down Expand Up @@ -568,13 +580,61 @@

try:
from docker import types
from docker.utils import parse_repository_tag
from docker.utils import (
parse_repository_tag,
parse_env_file,
format_environment
)
from docker.errors import APIError, DockerException
except ImportError:
# missing docker-py handled in ansible.module_utils.docker.common
pass


def get_docker_environment(env, env_files):
"""
Will return a list of "KEY=VALUE" items. Supplied env variable can
be either a list or a dictionary.

If environment files are combined with explicit environment variables,
the explicit environment variables take precedence.
"""
env_dict = {}
if env_files:
for env_file in env_files:
parsed_env_file = parse_env_file(env_file)
for name, value in parsed_env_file.items():
env_dict[name] = str(value)
if env is not None and isinstance(env, string_types):
env = env.split(',')
if env is not None and isinstance(env, dict):
for name, value in env.items():
if not isinstance(value, string_types):
raise ValueError(
'Non-string value found for env option. '
'Ambiguous env options must be wrapped in quotes to avoid YAML parsing. Key: %s' % name
)
env_dict[name] = str(value)
elif env is not None and isinstance(env, list):
for item in env:
try:
name, value = item.split('=', 1)
except ValueError:
raise ValueError('Invalid environment variable found in list, needs to be in format KEY=VALUE.')
env_dict[name] = value
elif env is not None:
raise ValueError(
'Invalid type for env %s (%s). Only list or dict allowed.' % (env, type(env))
)
env_list = format_environment(env_dict)
if not env_list:
if env is not None or env_files is not None:
return []
else:
return None
return sorted(env_list)


class DockerService(DockerBaseClass):
def __init__(self):
super(DockerService, self).__init__()
Expand Down Expand Up @@ -674,7 +734,6 @@ def from_ansible_params(ap, old_service, image_digest):
s.dns_options = ap['dns_options']
s.hostname = ap['hostname']
s.tty = ap['tty']
s.env = ap['env']
s.log_driver = ap['log_driver']
s.log_driver_options = ap['log_driver_options']
s.labels = ap['labels']
Expand Down Expand Up @@ -724,6 +783,8 @@ def from_ansible_params(ap, old_service, image_digest):
% (s.command, type(s.command))
)

s.env = get_docker_environment(ap['env'], ap['env_files'])

if ap['force_update']:
s.force_update = int(str(time.time()).replace('.', ''))

Expand Down Expand Up @@ -1487,7 +1548,8 @@ def main():
networks=dict(type='list'),
command=dict(type='raw'),
args=dict(type='list'),
env=dict(type='list'),
env=dict(type='raw'),
env_files=dict(type='list', elements='path'),
force_update=dict(default=False, type='bool'),
log_driver=dict(type='str'),
log_driver_options=dict(type='dict'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TEST3=val3
TEST4=val4
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TEST3=val5
TEST5=val5
102 changes: 100 additions & 2 deletions test/integration/targets/docker_swarm_service/tasks/tests/options.yml
Original file line number Diff line number Diff line change
Expand Up @@ -704,8 +704,8 @@
image: alpine:3.8
command: '/bin/sh -v -c "sleep 10m"'
env:
- "TEST1=val1"
- "TEST2=val2"
TEST1: val1
TEST2: val2
register: env_2

- name: env (changes)
Expand Down Expand Up @@ -734,6 +734,25 @@
env: []
register: env_5

- name: env (fail unwrapped values)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
env:
TEST1: true
register: env_6
ignore_errors: yes

- name: env (fail invalid formatted string)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
env:
- "TEST1=val3"
- "TEST2"
register: env_7
ignore_errors: yes

- name: cleanup
docker_swarm_service:
name: "{{ service_name }}"
Expand All @@ -747,6 +766,85 @@
- env_3 is changed
- env_4 is changed
- env_5 is not changed
- env_6 is failed
- env_7 is failed

####################################################################
## env_files #######################################################
####################################################################

- name: env_files
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
env_files:
- "{{ role_path }}/files/env-file-1"
register: env_file_1

- name: env_files (idempotency)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
env_files:
- "{{ role_path }}/files/env-file-1"
register: env_file_2

- name: env_files (more items)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
env_files:
- "{{ role_path }}/files/env-file-1"
- "{{ role_path }}/files/env-file-2"
register: env_file_3

- name: env_files (order)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
env_files:
- "{{ role_path }}/files/env-file-2"
- "{{ role_path }}/files/env-file-1"
register: env_file_4

- name: env_files (multiple idempotency)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
env_files:
- "{{ role_path }}/files/env-file-2"
- "{{ role_path }}/files/env-file-1"
register: env_file_5

- name: env_files (empty)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
env_files: []
register: env_file_6

- name: env_files (empty idempotency)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
env_files: []
register: env_file_7

- name: cleanup
docker_swarm_service:
name: "{{ service_name }}"
state: absent
diff: no

- assert:
that:
- env_file_1 is changed
- env_file_2 is not changed
- env_file_3 is changed
- env_file_4 is changed
- env_file_5 is not changed
- env_file_6 is changed
- env_file_7 is not changed

###################################################################
## force_update ###################################################
Expand Down
53 changes: 47 additions & 6 deletions test/units/modules/cloud/docker/test_docker_swarm_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@


class APIErrorMock(Exception):

def __init__(self, message, response=None, explanation=None):
self.message = message
self.response = response
Expand All @@ -26,6 +25,7 @@ def docker_module_mock(mocker):
@pytest.fixture(autouse=True)
def docker_swarm_service():
from ansible.modules.cloud.docker import docker_swarm_service

return docker_swarm_service


Expand All @@ -46,14 +46,55 @@ def test_retry_on_out_of_sequence_error(mocker, docker_swarm_service):

def test_no_retry_on_general_api_error(mocker, docker_swarm_service):
run_mock = mocker.MagicMock(
side_effect=APIErrorMock(
message='',
response=None,
explanation='some error',
)
side_effect=APIErrorMock(message='', response=None, explanation='some error')
)
manager = docker_swarm_service.DockerServiceManager(client=None)
manager.run = run_mock
with pytest.raises(APIErrorMock):
manager.run_safe()
assert run_mock.call_count == 1


def test_get_docker_environment(mocker, docker_swarm_service):
env_file_result = {'TEST1': 'A', 'TEST2': 'B', 'TEST3': 'C'}
env_dict = {'TEST3': 'CC', 'TEST4': 'D'}
env_string = "TEST3=CC,TEST4=D"

env_list = ['TEST3=CC', 'TEST4=D']
expected_result = sorted(['TEST1=A', 'TEST2=B', 'TEST3=CC', 'TEST4=D'])
mocker.patch.object(
docker_swarm_service, 'parse_env_file', return_value=env_file_result
)
mocker.patch.object(
docker_swarm_service,
'format_environment',
side_effect=lambda d: ['{0}={1}'.format(key, value) for key, value in d.items()],
)
# Test with env dict and file
result = docker_swarm_service.get_docker_environment(
env_dict, env_files=['dummypath']
)
assert result == expected_result
# Test with env list and file
result = docker_swarm_service.get_docker_environment(
env_list,
env_files=['dummypath']
)
assert result == expected_result
# Test with env string and file
result = docker_swarm_service.get_docker_environment(
env_string, env_files=['dummypath']
)
assert result == expected_result

assert result == expected_result
# Test with empty env
result = docker_swarm_service.get_docker_environment(
[], env_files=None
)
assert result == []
# Test with empty env_files
result = docker_swarm_service.get_docker_environment(
None, env_files=[]
)
assert result == []