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

Test volume idempotency with unittests #556

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 211 additions & 31 deletions plugins/module_utils/podman/podman_container_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1347,40 +1347,220 @@ def diffparam_uts(self):
after = before
return self._diff_update_and_compare('uts', before, after)

def diffparam_volume(self):
def clean_volume(x):
'''Remove trailing and double slashes from volumes.'''
def diffparam_volumes_all(self):
'''check for difference between old and new uses of --mount, --tmpfs, --volume and
Containerfile's VOLUME.'''

def clean_path(x):
'''Remove trailing and multiple consecutive slashes from path. Collapse '/./' to '/'.'''
if not x.rstrip("/"):
return "/"
return x.replace("//", "/").rstrip("/")

before = self.info['mounts']
before_local_vols = []
if before:
volumes = []
local_vols = []
for m in before:
if m['type'] != 'volume':
volumes.append(
[
clean_volume(m['source']),
clean_volume(m['destination'])
])
elif m['type'] == 'volume':
local_vols.append(
[m['name'], clean_volume(m['destination'])])
before = [":".join(v) for v in volumes]
before_local_vols = [":".join(v) for v in local_vols]
if self.params['volume'] is not None:
after = [":".join(
[clean_volume(i) for i in v.split(":")[:2]]
) for v in self.params['volume']]
else:
after = []
if before_local_vols:
after = list(set(after).difference(before_local_vols))
b = x.rstrip("/")
while True:
a = b.replace("//", "/")
a = a.replace("/./", "/")
if a == b:
break
b = a
return a

def prep_mount_args_for_comp(mounts, mtype=None):
'''Parse a mount string, using clean_path() on any known path
values. Sort the resulting mount string as a defense against
order changing as the return is likely used for comparisons.'''

# check the code where defaults below are used if changing. unfortunately things
# are not cleanly separated and their handling requires some encoding. :(
# the 'fake' mount options are there to allow for matching with tmpfs/volume/etc
# options whilst keeping them separate from real options.
# all args are assumed to be item[=val] format and comma separated in-code.
default_mount_args = {
'volume': 'readonly=false,chown=false,idmap=false',
'bind': 'readonly=false,chown=false,idmap=false,bind-propagation=rprivate',
'tmpfs': 'readonly=false,chown=false,tmpfs-mode=1777',
'devpts': 'uid=0,gid=0,mode=600,max=1048576',
'image': 'readwrite=false',
}
noargs_tmpfs_args = 'rw,noexec,nosuid,nodev'
default_tmpfs_args = 'rw,suid,dev,exec,size=50%'
default_volume_args = 'rw,rprivate,nosuid,nodev' # manpage says "private" but podman inspect says "rprivate"

mfin = []
wants_str = isinstance(mounts, str)
for mstr in [mounts] if wants_str else mounts:
mstype = mtype
msparsed = {
'type': mtype,
}
for mitem in sorted(mstr.split(',')):
nv = mitem.split('=', maxsplit=1)
miname = nv[0]
mival = nv[1] if len(nv) > 1 else None
if miname == 'type':
mstype = mival
msparsed['type'] = mstype
break
if mstype == 'tmpfs':
if not mstr:
mstr = noargs_tmpfs_args
mstr = ','.join([default_mount_args[mstype], default_tmpfs_args, mstr]).strip(',')
elif mstype == 'volume':
mstr = ','.join([default_mount_args[mstype], default_volume_args, mstr]).strip(',')
else:
mstr = ','.join([default_mount_args[mstype], mstr]).strip(',')
if not mstype:
raise ValueError("mount type not set/found for '{0}'.".format(mstr))
for mitem in mstr.split(','):
nv = mitem.split('=', maxsplit=1)
miname = nv[0]
mival = nv[1] if len(nv) > 1 else None
if miname in ['source', 'src']:
miname = 'src'
if mival:
mival = clean_path(mival)
elif miname in ['destination', 'dest', 'target', 'dst']:
miname = 'dst'
if mival:
mival = clean_path(mival)
elif miname in ['ro', 'readonly']:
miname = 'readonly'
if not mival:
mival = 'true'
elif miname in ['rw', 'readwrite']:
miname = 'readonly'
if not mival:
mival = 'false'
elif mival == 'true':
mival = 'false'
elif mival == 'false':
mival = 'true'
elif miname in ['U', 'chown']:
miname = 'chown'
if not mival:
mival = 'true'
elif miname in ['z']:
miname = 'relabel'
mival = 'shared'
elif miname in ['Z']:
miname = 'relabel'
mival = 'private'
elif miname in ['exec', 'dev', 'suid', 'sync']:
if not mival:
mival = 'true'
elif miname in ['noexec', 'nodev', 'nosuid']:
miname = miname[2:]
mival = 'false'
elif miname in ['shared', 'slave', 'private', 'unbindable', 'rshared', 'rslave', 'runbindable', 'rprivate']:
mival = miname
miname = 'bind-propagation'
elif miname in ['idmap']:
if not mival:
mival = 'true'
elif mstype == 'tmpfs' and miname in ['size', 'mode'] and mival:
miname = 'tmpfs-{0}'.format(miname)
elif miname in ['type', 'tmpfs-size', 'tmpfs-mode', 'bind-propagation', 'relabel']:
pass
elif miname.startswith('_unparsed_'):
pass
else:
miname = '_unparsed_{0}'.format(miname)
# source can be optional and can be specified as empty. If it is empty
# remove it altogether so that comparisons can be made simply.
if miname == 'src' and not mival:
miname = None
if miname:
msparsed[miname] = mival
msfin = []
for miname, mival in msparsed.items():
if mival is None:
msfin.append(miname)
else:
msfin.append("{0}={1}".format(miname, mival))
mfin.append(','.join(sorted(list(set(msfin)))))
return mfin[0] if wants_str else mfin

def clean_image_vols(iv_type):
'''cleans a list or string of Containerfile VOLUME config(s).'''
if iv_type == 'bind':
return prep_volume_for_comp(image_volumes)
elif iv_type == 'tmpfs':
return prep_tmpfs_for_comp(image_volumes)
raise ValueError("invalid image volume type: {0}.".format(iv_type))

def prep_tmpfs_for_comp(all_ms):
res = []
wants_str = isinstance(all_ms, str)
is_dict = isinstance(all_ms, dict)
for ms in [all_ms] if wants_str else all_ms:
if is_dict:
dm = [ms, all_ms[ms]]
else:
dm = ms.split(':', maxsplit=1)
if len(dm) == 1:
res.append('dst={0}'.format(dm[0]))
else:
res.append('dst={0},{1}'.format(dm[0], dm[1]))
return prep_mount_args_for_comp(res[0] if wants_str else res, mtype='tmpfs')

def prep_volume_for_comp(all_ms):
res = []
wants_str = isinstance(all_ms, str)
for ms in [all_ms] if wants_str else all_ms:
dm = ms.split(':', maxsplit=2)
mtype = 'volume'
if len(dm) == 1:
# ms is <contdir>, mtype is volume
dm.append(dm[0]) # <contdir>
dm.append('') # <opt>
dm[0] = '' # <srcvol>
else:
# ms is <srcvol|hostdir>:<contdir>:<opt>
if dm[0].startswith('/'):
mtype = 'bind'
if len(dm) == 2:
# ms is <srcvol|hostdir>:<contdir>
dm.append('') # <opt>
dm[2] = prep_mount_args_for_comp(dm[2], mtype=mtype)
res.append('src={0},dst={1},{2}'.format(dm[0], dm[1], dm[2]).strip(','))
return prep_mount_args_for_comp(res[0] if wants_str else res, mtype='volume')

before = []
iv_type = 'bind' # documented default
image_volumes = list(map(clean_path, self.image_info.get('config', {}).get('volumes', {}).keys()))
if self.info['config'].get('createcommand'):
cr_com = self.info['config']['createcommand']
for i, v in enumerate(cr_com):
if v == '--image-volume':
if cr_com[i + 1]:
iv_type = cr_com[i + 1]
elif v == '--mount':
f = prep_mount_args_for_comp(cr_com[i + 1])
if f:
before.append(f)
elif v == '--tmpfs':
f = prep_tmpfs_for_comp(cr_com[i + 1])
if f:
before.append(f)
elif v in ('--volume', '-v'):
f = prep_volume_for_comp(cr_com[i + 1])
if f:
before.append(f)
before.extend(clean_image_vols(iv_type))

after = list(self.params.get('mount') or set())
after_iv_type = self.params.get('image_volume', 'bind') or 'bind'
after.extend(clean_image_vols(after_iv_type))

tvols = self.params.get('tmpfs')
if tvols:
after.extend(prep_tmpfs_for_comp(tvols))
tvols = self.params.get('volume')
if tvols:
after.extend(prep_volume_for_comp(tvols))
after = prep_mount_args_for_comp(after)

before, after = sorted(list(set(before))), sorted(list(set(after)))
return self._diff_update_and_compare('volume', before, after)
return self._diff_update_and_compare('volumes_all', before, after)

def diffparam_volumes_from(self):
# Possibly volumesfrom is not in config
Expand Down
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ ansible-core
pytest
pytest-forked
pytest-xdist
pytest-mock
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@

- name: check test13
assert:
that: test13 is not changed
that: test13 is changed

- containers.podman.podman_container:
executable: "{{ test_executable | default('podman') }}"
Expand Down
Loading
Loading