Repository: vmware/tern
Pull request #617 (2020-03-30 07:10:27)

Add ‘checksums’ properties to Image and ImageLayer
This PR does following,

1. It includes checksum_type and checksum properties
 to Image and ImageLayer classes.
2.It includes appropriate tests to validate the checksum property

Resolved: #271

Signed-off-by: mukultaneja <mtaneja@vmware.com>

nishakm: @mukultaneja This looks good. Can you modify the `Work towards:` to `Resolves` in your commit message? This will automatically close the referenced issue. Thanks!
mukultaneja: @nishakm Done!

Patch:
diff --git a/tern/classes/image.py b/tern/classes/image.py
index b757662..2dd2e4f 100644
--- a/tern/classes/image.py
+++ b/tern/classes/image.py
@@ -16,9 +16,14 @@ class Image:
         manifest: the json object representing the image manifest
         config: the image config metadata
         layers: list of layer objects in the image
+        checksum_type: the digest algorithm used to create the image checksum
+        checksum: the checksum
+        checksums: a list of tuples of the form (checksum_type, checksum)
     methods:
         load_image: this method is to be implemented in the derived classes
         get_layer_diff_ids: returns a list of layer diff ids only
+        set_checksum: set the checksum of the image given the checksum type
+        add_checksums: add new checksums in the existing list of the checksums
         to_dict: return a python dictionary representation of the image
     '''
     def __init__(self, image_id=None):
@@ -29,6 +34,9 @@ class Image:
         self._manifest = {}
         self._config = {}
         self._layers = []
+        self.__checksum_type = ''
+        self.__checksum = ''
+        self.__checksums = []
         self._origins = Origins()
 
     @property
@@ -55,6 +63,22 @@ class Image:
     def name(self):
         return self._name
 
+    @property
+    def checksum_type(self):
+        return self.__checksum_type
+
+    @property
+    def checksum(self):
+        return self.__checksum
+
+    @property
+    def checksums(self):
+        return self.__checksums
+
+    @checksums.setter
+    def checksums(self, checksums):
+        self.__checksums = checksums
+
     @name.setter
     def name(self, name):
         self._name = name
@@ -67,6 +91,17 @@ class Image:
     def tag(self, tag):
         self._tag = tag
 
+    def set_checksum(self, checksum_type='', checksum=''):
+        '''Set the checksum type and checksum of the image'''
+        # TODO: calculate the checksum if not given
+        self.__checksum_type = checksum_type
+        self.__checksum = checksum
+
+    def add_checksums(self, checksums):
+        '''Add checksum tuples to checksums property'''
+        for checksum in checksums:
+            self.__checksums.append(checksum)
+
     def get_layer_diff_ids(self):
         '''Get a list of layer diff ids'''
         return [layer.diff_id for layer in self.layers]
diff --git a/tern/classes/image_layer.py b/tern/classes/image_layer.py
index c44a928..1bb912a 100644
--- a/tern/classes/image_layer.py
+++ b/tern/classes/image_layer.py
@@ -33,6 +33,10 @@ class ImageLayer:
         files_analyzed: whether the files in this layer are analyzed or not
         analyzed_output: the result of the file analysis
         files: a list of files included in the image layer
+        checksum_type: the digest algorithm used to create the image layer
+        checksum
+        checksum: the checksum
+        checksums: a list of tuples of the form (checksum_type, checksum)
     methods:
         add_package: adds a package to the layer
         remove_package: removes a package from the layer
@@ -42,7 +46,11 @@ class ImageLayer:
         add_file: adds a file to the layer
         remove_file: given the file path, remove a file object from the
         list of files in this layer
-        get_file_paths: Get a list of file paths in the image layer'''
+        get_file_paths: Get a list of file paths in the image layer
+        set_checksum: set the checksum of the image layer given the checksum
+        type
+        add_checksums: add new checksums in the existing list of the checksums
+        '''
 
     def __init__(self, diff_id, tar_file=None, created_by=None):
         self.__diff_id = diff_id
@@ -58,6 +66,9 @@ class ImageLayer:
         self.__os_guess = ''
         self.__files_analyzed = False
         self.__analyzed_output = ''
+        self.__checksum_type = ''
+        self.__checksum = ''
+        self.__checksums = []
 
     @property
     def diff_id(self):
@@ -87,6 +98,22 @@ class ImageLayer:
     def created_by(self):
         return self.__created_by
 
+    @property
+    def checksum_type(self):
+        return self.__checksum_type
+
+    @property
+    def checksum(self):
+        return self.__checksum
+
+    @property
+    def checksums(self):
+        return self.__checksums
+
+    @checksums.setter
+    def checksums(self, checksums):
+        self.__checksums = checksums
+
     @created_by.setter
     def created_by(self, create_string):
         self.__created_by = create_string
@@ -145,6 +172,17 @@ class ImageLayer:
         else:
             raise ValueError('files_analyzed should be boolean')
 
+    def set_checksum(self, checksum_type='', checksum=''):
+        '''Set the checksum type and checksum of the image layer'''
+        # TODO: calculate the checksum if not given
+        self.__checksum_type = checksum_type
+        self.__checksum = checksum
+
+    def add_checksums(self, checksums):
+        '''Add checksum tuples to checksums property'''
+        for checksum in checksums:
+            self.__checksums.append(checksum)
+
     def add_package(self, package):
         if isinstance(package, Package):
             if package.name not in self.get_package_names():


Test patch:
diff --git a/tests/test_analyze_common.py b/tests/test_analyze_common.py
index 7176d16..10ea86c 100644
--- a/tests/test_analyze_common.py
+++ b/tests/test_analyze_common.py
@@ -4,9 +4,13 @@
 # SPDX-License-Identifier: BSD-2-Clause
 
 import unittest
+from unittest.mock import Mock
+
 from tern.analyze import common
 from test_fixtures import TestImage
 from tern.classes.command import Command
+from tern.classes.file_data import FileData
+from tern.utils import cache
 
 
 class TestAnalyzeCommon(unittest.TestCase):
@@ -15,11 +19,14 @@ class TestAnalyzeCommon(unittest.TestCase):
         self.command1 = Command("yum install nfs-utils")
         self.command2 = Command("yum remove nfs-utils")
         self.image = TestImage('5678efgh')
+        self.file = FileData('README.txt', '/home/test')
+        cache.cache = {}
 
     def tearDown(self):
         del self.image
         del self.command1
         del self.command2
+        cache.cache = {}
 
     def testGetShellCommands(self):
         command = common.get_shell_commands("yum install nfs-utils")
@@ -27,6 +34,132 @@ class TestAnalyzeCommon(unittest.TestCase):
         self.assertEqual(len(command), 1)
         self.assertEqual(command[0].options, self.command1.options)
 
+    def testLoadFromCache(self):
+        '''Given a layer object, populate the given layer in case the cache isn't empty'''
+        self.image.load_image()
+        # No cache
+        self.assertFalse(common.load_from_cache(self.image.layers[0], redo=True))
+
+        # Cache populated
+        layer = self.image.layers[0]
+        cache.cache = {
+            layer.fs_hash: {
+                'files_analyzed': False,
+                'os_guess': 'Ubuntu',
+                'pkg_format': 'tar',
+                'files': [{'name': 'README.md', 'path': '/home/test/projectsX'}],
+                'packages': [{'name': 'README'}]
+            }}
+        self.assertTrue(common.load_from_cache(self.image.layers[0], redo=False))
+
+    def testLoadFilesFromCache(self):
+        '''Given a layer object, populate the files of given layer in case the cache isn't empty'''
+        self.image.load_image()
+
+        # not loaded in cache
+        layer = self.image.layers[0]
+        layer.add_files = Mock(return_value=None)
+        self.assertFalse(common.load_files_from_cache(layer))
+        self.assertTrue(layer.add_files.called)
+
+        # Empty files
+        layer = self.image.layers[0]
+        cache.cache = {
+            layer.fs_hash: {
+                'files_analyzed': False,
+                'files': [],
+                'packages': [{'name': 'README.md', 'path': '/home/test/projectsX'}]
+            }}
+        layer.add_files = Mock(return_value=None)
+        self.assertFalse(common.load_files_from_cache(layer))
+        self.assertTrue(layer.add_files.called)
+
+        # With no empty files
+        layer.add_files = Mock(return_value=None)
+        cache.cache[layer.fs_hash]['files'].append({'name': 'README.md', 'path': '/home/test/projectsX', 'origins': [
+            {'origin_str': 'security issue', 'notices': [{
+                'message': 'Missing security policy',
+                'level': 'info'
+            }]}
+        ]})
+
+        self.assertEqual(common.load_files_from_cache(layer), None)
+        self.assertFalse(layer.add_files.called)
+
+    def testLoadPackagesFromCache(self):
+        '''Given a layer object, populate the packages of given layer in case the cache isn't empty'''
+        self.image.load_image()
+
+        # not loaded in cache
+        layer = self.image.layers[0]
+        layer.add_package = Mock(return_value=None)
+        self.assertFalse(common.load_packages_from_cache(layer))
+        self.assertFalse(layer.add_package.called)
+
+        # Empty files
+        layer = self.image.layers[0]
+        cache.cache = {
+            layer.fs_hash: {
+                'files_analyzed': False,
+                'files': [],
+                'packages': []
+            }}
+        layer.add_package = Mock(return_value=None)
+        self.assertFalse(common.load_packages_from_cache(layer))
+        self.assertFalse(layer.add_package.called)
+
+        # With no empty files
+        layer.add_package = Mock(return_value=None)
+        cache.cache[layer.fs_hash]['packages'].append({'name': 'README.md', 'origins': [
+            {'origin_str': 'security issue', 'notices': [{
+                'message': 'Missing security policy',
+                'level': 'info'
+            }]}
+        ]})
+
+        self.assertTrue(common.load_packages_from_cache(layer))
+        self.assertTrue(layer.add_package.called)
+
+    def testLoadNoticesFromCache(self):
+        '''Given a layer object, populate the notices messages of given layer in case the cache isn't empty'''
+        origin_str = 'security issue'
+
+        self.image.load_image()
+
+        # not loaded in cache
+        layer = self.image.layers[0]
+        cache.cache = {
+            layer.fs_hash: {
+                'origins': []
+            }}
+        layer.origins.add_notice_origin = Mock(return_value=None)
+        layer.origins.add_notice_to_origins = Mock(return_value=None)
+        common.load_notices_from_cache(layer)
+        self.assertFalse(layer.origins.add_notice_origin.called)
+        self.assertFalse(layer.origins.add_notice_to_origins.called)
+
+        # With no empty
+        layer.origins.add_notice_origin = Mock(return_value=None)
+        layer.origins.add_notice_to_origins = Mock(return_value=None)
+        cache.cache[layer.fs_hash]['origins'].append({
+            'origin_str': origin_str, 'notices': [{
+                'message': 'Missing security policy',
+                'level': 'info'
+            }]
+        })
+        common.load_notices_from_cache(layer)
+        layer.origins.add_notice_origin.assert_called_with(origin_str)
+        self.assertTrue(layer.origins.add_notice_to_origins.called)
+
+    def testSaveToCache(self):
+        '''Given a image object, check the image layers are cached'''
+        self.image.load_image()
+        layer = self.image.layers[0]
+        cache.add_layer = Mock()
+
+        common.save_to_cache(self.image)
+        cache.add_layer.assert_called_once_with(layer)
+
     def testUpdateMasterListWithoutPackages(self):
         self.image.load_image()
         layer = self.image.layers[0]
diff --git a/tests/test_class_image.py b/tests/test_class_image.py
index 3090997..536f98a 100644
--- a/tests/test_class_image.py
+++ b/tests/test_class_image.py
@@ -102,6 +102,19 @@ class TestClassImage(unittest.TestCase):
         self.assertTrue(self.image2.get_layer_object("123abc") is not None)
         self.assertTrue(self.image2.get_layer_object("1234abcd") is None)
 
+    def testSetChecksum(self):
+        self.image1.set_checksum('sha256', '12345abcde')
+        self.assertEqual(self.image1.checksum_type, 'sha256')
+        self.assertEqual(self.image1.checksum, '12345abcde')
+
+    def testAddChecksums(self):
+        self.image1.add_checksums([
+            ('SHA1', '12345abcde'),
+            ('MD5', '1ff38cc592c4c5d0c8e3ca38be8f1eb1')])
+        self.assertEqual(self.image1.checksums,
+                         [('SHA1', '12345abcde'),
+                          ('MD5', '1ff38cc592c4c5d0c8e3ca38be8f1eb1')])
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_class_image_layer.py b/tests/test_class_image_layer.py
index 40b172a..5b5d288 100644
--- a/tests/test_class_image_layer.py
+++ b/tests/test_class_image_layer.py
@@ -124,6 +124,18 @@ class TestClassImageLayer(unittest.TestCase):
         file_paths = self.layer.get_file_paths()
         self.assertEqual(file_paths, ['path/to/file1', 'path/to/file2'])
 
+    def testSetChecksum(self):
+        self.layer.set_checksum('sha256', '12345abcde')
+        self.assertEqual(self.layer.checksum_type, 'sha256')
+        self.assertEqual(self.layer.checksum, '12345abcde')
+
+    def testAddChecksums(self):
+        self.layer.add_checksums([('SHA1', '12345abcde'),
+                                  ('MD5', '1ff38cc592c4c5d0c8e3ca38be8f1eb1')])
+        self.assertEqual(self.layer.checksums,
+                         [('SHA1', '12345abcde'),
+                          ('MD5', '1ff38cc592c4c5d0c8e3ca38be8f1eb1')])
+
 
 if __name__ == '__main__':
     unittest.main()