Skip to content

Commit

Permalink
Fix package pinning when package is also pkg mngr
Browse files Browse the repository at this point in the history
Since the addition of language package managers, the Dockerfile lock
functionality was getting confused when trying to pin packages that also
double as language package managers.

This commit does the following to resolve the issue:
 1) In analyze.py, instead of comparing each of the packages in the
    layer to see if they are installed in the RUN line, first gather
    the list of packages being installed in the dockerfile RUN line
    and then compare those against the packages installed in the layer.
    This prevents unnecessary comparisons and duplicate pinnings.

 2) Add a 'should_pin' method to dockerfile.py that checks whether the
    package in the RUN line should be pinned or not. Use this function
    in the existing 'expand_package' function in dockerfile.py to
    determine if we should expand/pin the package or not.

 3) Modify the way 'expand_package' in dockerfile.py actually pins the
    package and version in the dockerfile object. Instead of using the
    replace method, which was expanding non-exact matches of a package
    name, we go word by word in the RUN line to determine if a package
    should be pinned.

  4) Modify the 'package_in_dockerfile' function to return the list of
     packages being installed in a given RUN line instead of returning
     true or false. Also change the name of the function to more
     accurately reflect its purpose. The function is now called
     'get_install_packages'.

Resolves #702

Signed-off-by: Rose Judge <rjudge@vmware.com>
  • Loading branch information
rnjudge authored and Nisha K committed May 28, 2020
1 parent 6ed1f0c commit 202c4c8
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 18 deletions.
22 changes: 13 additions & 9 deletions tern/analyze/docker/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,19 @@ def analyze_subsequent_layers(image_obj, shell, master_list, redo, dfobj=None,
if dfile_lock:
# collect list of RUN commands that could install pkgs
run_dict = d_file.get_run_layers(dfobj)
for package in image_obj.layers[curr_layer].packages:
# check that package is in current dfobj RUN line
if d_file.package_in_dockerfile(
run_dict[curr_layer - 1], package.name):
d_file.expand_package(
run_dict[curr_layer - 1], package.name,
package.version,
command_lib.check_pinning_separator(
pkg_listing))
# use the run_dict to get list of packages being installed
install_list = d_file.get_install_packages(
run_dict[curr_layer - 1])
for install_pkg in install_list:
for layer_pkg in image_obj.layers[curr_layer].packages:
if install_pkg == layer_pkg.name:
# dockerfile package in layer, let's pin it
d_file.expand_package(
run_dict[curr_layer - 1],
install_pkg,
layer_pkg.version,
command_lib.check_pinning_separator(
pkg_listing))
if command_list:
rootfs.undo_mount()
rootfs.unmount_rootfs()
Expand Down
45 changes: 36 additions & 9 deletions tern/analyze/docker/dockerfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from tern.utils import constants
from tern.analyze.docker import container
from tern.analyze import common
from tern.command_lib import command_lib

# global logger
logger = logging.getLogger(constants.logger_name)
Expand Down Expand Up @@ -188,12 +189,37 @@ def expand_from_images(dfobj):
counter = counter + 1


def should_pin(run_line, package, index):
'''This check is necessary to make sure that commands that double as
packages (i.e. language package managers like pip, gem) are pinned properly
in a locked dockerfile. For example, in the RUN line "pip install pkg"
we would not pin 'pip' to its version, but if the line was
"apt install pip" then we would want to pin the version.
Return True if the package should be pinned, return False if it
should not.'''
if package not in command_lib.command_lib['snippets'].keys():
return True
if index < 1:
return False
if index < len(run_line) + 1:
if run_line[index + 1] == \
command_lib.command_lib['snippets'][package]['install']:
return False
return True


def expand_package(command_dict, package, version, pinning_separator):
'''Update the given dockerfile object with the pinned package
and version information. '''
command_dict['value'] = command_dict['value'].replace(package, package +
pinning_separator +
version, 1)
sub_string = ''
for i, word in enumerate(command_dict['value'].split()):
# only pin if the package word is not the install directive
if word == package and should_pin(command_dict['value'].split(),
word, i):
sub_string += word + pinning_separator + version + ' '
else:
sub_string += word + ' '
command_dict['value'] = sub_string
# Update 'content' to match 'value' in dfobj
command_dict['content'] = command_dict['instruction'] + ' ' + \
command_dict['value'] + '\n'
Expand All @@ -208,14 +234,15 @@ def get_run_layers(dfobj):
return run_list


def package_in_dockerfile(command_dict, pkg_name):
'''Return True if pkg_name is a package specified in the command_dict
RUN line provided, otherwise return False.'''
def get_install_packages(command_dict):
'''Given a dockerfile RUN line, return a list of packages to be
installed from that line.'''
command_words, _ = common.filter_install_commands(command_dict['value'])
install_packages = []
for command in command_words:
if pkg_name in command.words:
return True
return False
for word in command.words:
install_packages.append(word)
return install_packages


def get_command_list(dfobj_structure):
Expand Down

0 comments on commit 202c4c8

Please sign in to comment.