#67 General dockerlinter for Dockerfile.
Merged 6 years ago by jscotka. Opened 6 years ago by phracek.

@@ -1,1 +1,1 @@ 

- 0.4.43-1 distro/

+ 0.4.52-1 distro/

file modified
+6 -5
@@ -24,11 +24,11 @@ 

  Vagrant.configure(2) do |config|

  

      config.vm.box = "fedora/25-cloud-base"

-     config.vm.synced_folder ".", "/vagrant"

+     config.vm.synced_folder ".", "/home/vagrant"

      config.vm.network "private_network", ip: "192.168.50.10"

      config.vm.network "forwarded_port", guest: 80, host: 8888

      config.vm.hostname = "moduletesting"

-     config.vm.post_up_message = "Results: http://localhost:8888/job-results"

+     config.vm.post_up_message = "Results: http://localhost:8888/avocado/job-results/latest/html/results.html"

  

      config.vm.provider "libvirt" do |libvirt|

          libvirt.memory = 1024
@@ -42,9 +42,10 @@ 

  

      config.vm.provision "shell", inline: <<-SHELL

          set -x

-         dnf install -y python-pip make docker httpd git python2-avocado python2-avocado-plugins-output-html

-         cd /vagrant

-         make all

+         dnf install -y make docker httpd git python2-avocado python2-avocado-plugins-output-html python-netifaces

+         cd /home/vagrant

+         make install

+         make check

          cp -r /root/avocado /var/www/html/

          chmod -R a+x /var/www/html/

          restorecon -r /var/www/html/

@@ -1,7 +1,7 @@ 

  %global framework_name moduleframework

  

  Name:           modularity-testing-framework

- Version:        0.4.43

+ Version:        0.4.52

  Release:        1%{?dist}

  Summary:        Framework for writing tests for modules and containers

  
@@ -16,6 +16,7 @@ 

  Requires:       python2-avocado-plugins-output-html

  Requires:       python-netifaces

  Requires:       docker

+ Requires:       python2-dockerfile-parse

  

  %description

  %{summary}.
@@ -44,6 +45,33 @@ 

  %{_datadir}/moduleframework/

  

  %changelog

+ * Fri Jun 02 2017 Jan Scotka <jscotka@redhat.com> 0.4.52-1

+ - 

+ 

+ * Thu Jun 01 2017 Jan Scotka <jscotka@redhat.com> 0.4.51-1

+ - 

+ 

+ * Thu Jun 01 2017 Jan Scotka <jscotka@redhat.com> 0.4.50-1

+ - 

+ 

+ * Wed May 31 2017 Jan Scotka <jscotka@redhat.com> 0.4.49-1

+ - 

+ 

+ * Wed May 31 2017 Jan Scotka <jscotka@redhat.com> 0.4.48-1

+ - 

+ 

+ * Wed May 31 2017 Jan Scotka <jscotka@redhat.com> 0.4.47-1

+ - 

+ 

+ * Wed May 31 2017 Jan Scotka <jscotka@redhat.com> 0.4.46-1

+ - 

+ 

+ * Wed May 31 2017 Jan Scotka <jscotka@redhat.com> 0.4.45-1

+ - 

+ 

+ * Wed May 31 2017 Jan Scotka <jscotka@redhat.com> 0.4.44-1

+ - 

+ 

  * Tue May 30 2017 Jan Scotka <jscotka@redhat.com> 0.4.43-1

  - 

  

@@ -1,21 +1,21 @@ 

  CMD=python -m avocado run --filter-by-tags=-WIP

  TESTS=$(shell ls *.py *.sh modulelint/*.py)

  export MTF_REMOTE_REPOS=yes

- export MTF_DO_NOT_CLEANUP=yes

  export DEBUG=yes

  

  check-docker:

  	MODULE=docker $(CMD) $(TESTS)

  	@true

  

- check-rpm:

- 	MODULE=nspawn $(CMD) $(TESTS)

- 	@true

- 

  check-minimal-config-docker:

  	MODULE=docker CONFIG=./minimal.yaml $(CMD) $(TESTS)

  	@true

  

+ 

+ check-rpm:

+ 	MODULE=nspawn $(CMD) $(TESTS)

+ 	@true

+ 

  check-minimal-config-rpm:

  	MODULE=nspawn CONFIG=./minimal.yaml $(CMD) $(TESTS)

  	@true
@@ -25,19 +25,23 @@ 

  	@true

  

  check-multihost-testing:

- 	cd ../multios_testing; MTF_DO_NOT_CLEANUP= MTF_DISABLE_MODULE=yes $(CMD) *.py

+ 	cd ../multios_testing; MTF_DISABLE_MODULE=yes $(CMD) *.py

  	@true

  

  check-run-them-pdc-baseruntime:

- 	MTF_REMOTE_REPOS= MTF_DO_NOT_CLEANUP= ../../tools/run-them.sh base-runtime base-runtime-f26-20170504201340 pdc

+ 	../../tools/run-them.sh base-runtime base-runtime-f26-20170504201340 pdc

  	@true

  

  check-run-them-pdc-testmodule:

- 	MTF_REMOTE_REPOS= ../../tools/run-them.sh testmodule testmodule-master-20170413142055 pdc

+ 	../../tools/run-them.sh testmodule testmodule-master-20170413142055 pdc

  	@true

  

  check-run-them-fedmsg-testmodule:

- 	MTF_REMOTE_REPOS= ../../tools/run-them.sh testmodule ../../tools/example_message_module.yaml fedmsg

+ 	../../tools/run-them.sh testmodule ../../tools/example_message_module.yaml fedmsg

+ 	@true

+ 

+ check-exceptions:

+ 	MODULEMDURL=XXX  MODULE=nspawn $(CMD) --show-job-log simpleTest.py  | grep 'raise ConfigExc' &>/dev/null

  	@true

  

  

@@ -31,6 +31,10 @@ 

  

      def testCopyThereAndBack(self):

          self.start()

+         #cleanup of all files, because there is bug in nspawn copying of files causing that it hang in case of existing file (F-25)

+         self.runHost("rm a b", ignore_status=True)

+         self.run("rm /a.test", ignore_status=True)

+ 

          self.runHost("echo x > a", shell=True)

          self.copyTo("a", "/a.test")

          self.assertIn("x", self.run("cat /a.test").stdout)

file modified
+36 -1
@@ -32,6 +32,37 @@ 

  import socket

  import os

  

+ 

+ class ModuleFrameworkException(Exception):

+     def __init__(self,*args,**kwargs):

+         super(ModuleFrameworkException, self).__init__(*args,**kwargs)

+         print_info('EXCEPTION nspawn', *args)

+ 

+ class NspawnExc(ModuleFrameworkException):

+     def __init__(self,*args,**kwargs):

+         super(NspawnExc, self).__init__('EXCEPTION nspawn', *args,**kwargs)

+ 

+ class RpmExc(ModuleFrameworkException):

+     def __init__(self,*args,**kwargs):

+         super(RpmExc, self).__init__('EXCEPTION rpm dnf yum', *args,**kwargs)

+ 

+ class ContainerExc(ModuleFrameworkException):

+     def __init__(self,*args,**kwargs):

+         super(ContainerExc, self).__init__('EXCEPTION container', *args,**kwargs)

+ 

+ class ConfigExc(ModuleFrameworkException):

+     def __init__(self,*args,**kwargs):

+         super(ConfigExc, self).__init__('EXCEPTION config', *args,**kwargs)

+ 

+ class PDCExc(ModuleFrameworkException):

+     def __init__(self,*args,**kwargs):

+         super(PDCExc, self).__init__('EXCEPTION PDC', *args,**kwargs)

+ 

+ class KojiExc(ModuleFrameworkException):

+     def __init__(self,*args,**kwargs):

+         super(KojiExc, self).__init__('EXCEPTION Koji', *args,**kwargs)

+ 

+ 

  defroutedev = netifaces.gateways().get('default').values(

  )[0][1] if netifaces.gateways().get('default') else "lo"

  hostipaddr = netifaces.ifaddresses(defroutedev)[2][0]['addr']
@@ -46,6 +77,7 @@ 

  

  # translation table for config.yaml files syntax is {VARIABLE} in config file

  trans_dict = {"HOSTIPADDR": hostipaddr,

+               "GUESTIPADDR": hostipaddr,

                "DEFROUTE": defroutedev,

                "HOSTNAME": hostname,

                "ROOT": "/",
@@ -70,6 +102,9 @@ 

  def is_debug():

      return bool(os.environ.get("DEBUG"))

  

+ def is_not_silent():

+     return not is_debug()

+ 

  def print_info(*args):

      """

      Print data to selected output in case you are not in testing class, there is self.log
@@ -83,7 +118,7 @@ 

              try:

                  out = arg.format(**trans_dict)

              except KeyError:

-                 raise BaseException(

+                 raise ModuleFrameworkException(

                      "String is formatted by using trans_dict, if you want to use brackets { } in your code please use {{ or }}, possible values in trans_dict are:",

                      trans_dict)

          print >> sys.stderr, out

@@ -0,0 +1,185 @@ 

+ from __future__ import absolute_import, print_function

+ 

+ import os

+ import re

+ import ast

+ 

+ from dockerfile_parse import DockerfileParser

+ 

+ # Dockerfile path

+ DOCKERFILE = "Dockerfile"

+ 

+ EXPOSE = "EXPOSE"

+ VOLUME = "VOLUME"

+ LABEL = "LABEL"

+ ENV = "ENV"

+ PORTS = "PORTS"

+ FROM = "FROM"

+ RUN = "RUN"

+ 

+ 

+ def get_string(value):

+     return ast.literal_eval(value)

+ 

+ 

+ class DockerfileLinter(object):

+     """

+     Class checks a Dockerfile

+     It requires only directory with Dockerfile.

+     """

+ 

+     dockerfile = None

+     oc_template = None

+     dfp = {}

+     docker_dict = {}

+ 

+     def __init__(self, dir_name=None):

+         self.dockerfile = os.path.join(dir_name, DOCKERFILE)

+         if not self._exist_docker_file():

+             self.dfp = None

+         else:

+             self.dfp = DockerfileParser(path=dir_name)

+             self._get_structure_as_dict()

+ 

+     def _exist_docker_file(self):

+         """

+         Function checks if docker file exists

+         :return: True if exists

+         """

+         if not os.path.exists(self.dockerfile):

+             print("Dockerfile has to exists in the %s directory." % self.dir)

+             return False

+         return True

+ 

+     def _get_general(self, value):

+         """

+         Function returns exposes as field.

+         It is used for RUN, EXPOSE and FROM

+         :param value:

+         :return:

+         """

+         return value.split()

+ 

+     def _get_env(self, value):

+         """Function gets env as field"""

+         return value.split(" ")

+ 

+     def _get_volume(self, value):

+         """Function evaluates a value and returns as string."""

+         return get_string(value)

+ 

+     def _get_label(self, val):

+         """

+         Function returns label from Docker file

+         except INSTALL, UNINSTALL and RUN label used by atomic.

+         :param value: row from Dockerfile

+         :return: label_dict

+         """

+         untracked_values = ['INSTALL', 'UNINSTALL', 'RUN']

+         if [f for f in untracked_values if val.startswith(f)]:

+             return None

+         labels = re.sub('\s\s+', ';', val).split(';')

+         labels = [l.replace('"', '') for l in labels]

+         try:

+             label_dict = {l.split(' ')[0]: l.split(' ')[1] for l in labels}

+         except IndexError:

+             label_dict = {l.split('=')[0]: l.split('=')[1] for l in labels}

+         return label_dict

+ 

+     def _get_structure_as_dict(self):

+         functions = {ENV: self._get_env,

+                      EXPOSE: self._get_general,

+                      VOLUME: self._get_volume,

+                      LABEL: self._get_label,

+                      FROM: self._get_general,

+                      RUN: self._get_general}

+ 

+         for struct in self.dfp.structure:

+             key = struct["instruction"]

+             val = struct["value"]

+             if key == LABEL:

+                 if key not in self.docker_dict:

+                     self.docker_dict[key] = {}

+                 value = functions[key](val)

+                 if value is not None:

+                     self.docker_dict[key].update(value)

+             else:

+                 if key not in self.docker_dict:

+                     self.docker_dict[key] = []

+                 try:

+                     ret_val = functions[key](val)

+                     for v in ret_val:

+                         if v not in self.docker_dict[key]:

+                             self.docker_dict[key].append(v)

+                 except KeyError:

+                     print("Dockerfile tag %s is not parsed by MTF" % key)

+ 

+     def get_docker_env(self):

+         if ENV in self.docker_dict and self.docker_dict[ENV]:

+             return self.docker_dict[ENV]

+ 

+     def get_docker_specific_env(self, env_name=None):

+         """

+         Function returns list of specific env_names or empty list

+         :param env_name: Specify env_name for check

+         :return: List of env or empty list

+         """

+         if env_name is None:

+             return []

+         env_list = self.get_docker_env()

+         return [env_name in env_list]

+ 

+     def get_docker_expose(self):

+         """

+         Function return docker EXPOSE directives

+         :return: list of PORTS

+         """

+         ports_list = []

+         if EXPOSE in self.docker_dict and self.docker_dict[EXPOSE]:

+             for p in self.docker_dict[EXPOSE]:

+                 ports_list.append(int(p))

+         return ports_list

+ 

+     def get_docker_labels(self):

+         """

+         Function returns docker labels

+         :return: label dictionary

+         """

+         if LABEL in self.docker_dict and self.docker_dict[LABEL]:

+             return self.docker_dict[LABEL]

+         return None

+ 

+     def get_specific_label(self, label_name=None):

+         """

+         Function returns list of specific label names or empty list

+         :param label_name: Specify label_name for check

+         :return: List of labels or empty list.

+         """

+         if label_name is None:

+             return []

+         label_list = self.get_docker_labels()

+         return [label_name in label_list]

+ 

+     def check_baseruntime(self):

+         """

+         Function returns docker labels

+         :return: label dictionary

+         """

+         if FROM in self.docker_dict:

+             return [x for x in self.docker_dict[FROM] if "baseruntime/baseruntime" in x]

+ 

+     def check_microdnf(self):

+         """

+         Function returns docker labels

+         :return: label dictionary

+         """

+         if RUN in self.docker_dict:

+             for val in self.docker_dict[RUN]:

+                 if val.startswith("yum") or " yum " in val:

+                     return False

+                 if val.startswith("dnf") or " dnf " in val:

+                     return False

+                 else:

+                     return True

+ 

+ 

@@ -28,16 +28,15 @@ 

  what you should use for your tests (inherited)

  """

  

- import os

  import re

  import shutil

  import yaml

  import json

- import time

  import urllib

  import glob

  from avocado import Test

  from avocado import utils

+ from avocado.core import exceptions

  from avocado.utils import service

  from compose_info import ComposeParser

  import pdc_data
@@ -45,9 +44,9 @@ 

  from timeoutlib import Retry

  import time

  import warnings

- 

  PROFILE = None

  

+ 

  def skipTestIf(value, text="Test not intended for this module profile"):

      """

      function what solves troubles that it is not possible to call SKIP inside code
@@ -58,7 +57,7 @@ 

      :return: None

      """

      if value:

-         raise BaseException("DEPRECATED, don't use this skip, use self.cancel() inside test function, or self.skip() in setUp()")

+         raise ModuleFrameworkException("DEPRECATED, don't use this skip, use self.cancel() inside test function, or self.skip() in setUp()")

  

  

  class CommonFunctions(object):
@@ -87,7 +86,7 @@ 

          try:

              formattedcommand = command.format(**trans_dict)

          except KeyError:

-             raise BaseException("Command is formatted by using trans_dict, if you want to use brackets { } in your code please use {{ or }}, possible values in trans_dict are:", trans_dict)

+             raise ModuleFrameworkException("Command is formatted by using trans_dict, if you want to use brackets { } in your code please use {{ or }}, possible values in trans_dict are:", trans_dict)

          return utils.process.run("%s" % formattedcommand, **kwargs)

  

      def installTestDependencies(self, packages=None):
@@ -116,7 +115,7 @@ 

              self.runHost(

                  "{HOSTPACKAGER} install " +

                  " ".join(packages),

-                 ignore_status=True)

+                 ignore_status=True, verbose=is_not_silent())

  

      def loadconfig(self):

          """
@@ -170,22 +169,26 @@ 

          :param urllink: load this url instead of default one defined in config, or redefined by vaiable CONFIG

          :return: dict

          """

-         if urllink:

-             ymlfile = urllib.urlopen(urllink)

-             cconfig = yaml.load(ymlfile)

-             return cconfig

-         elif not get_if_module():

-             trans_dict["GUESTPACKAGER"] = "yum -y"

-             return {"data":{}}

-         else:

-             if self.config is None:

-                 self.loadconfig()

-             if not self.modulemdConf:

-                 modulemd = get_correct_modulemd()

-                 if modulemd:

-                     ymlfile = urllib.urlopen(modulemd)

-                     self.modulemdConf = yaml.load(ymlfile)

-             return self.modulemdConf

+         try:

+             if urllink:

+                 ymlfile = urllib.urlopen(urllink)

+                 cconfig = yaml.load(ymlfile)

+                 link = cconfig

+             elif not get_if_module():

+                 trans_dict["GUESTPACKAGER"] = "yum -y"

+                 link = {"data": {}}

+             else:

+                 if self.config is None:

+                     self.loadconfig()

+                 if not self.modulemdConf:

+                     modulemd = get_correct_modulemd()

+                     if modulemd:

+                         ymlfile = urllib.urlopen(modulemd)

+                         self.modulemdConf = yaml.load(ymlfile)

+                 link = self.modulemdConf

+             return link

+         except IOError as e:

+             raise ConfigExc("Cannot load file")

  

      def getIPaddr(self):

          """
@@ -279,7 +282,7 @@ 

          :return: None

          """

          if not os.path.isfile('/usr/bin/docker-current'):

-             self.runHost("{HOSTPACKAGER} install docker")

+             self.runHost("{HOSTPACKAGER} install docker",verbose=is_not_silent())

  

      def __prepareContainer(self):

          """
@@ -307,16 +310,16 @@ 

          if self.tarbased:

              self.runHost(

                  "docker import %s %s" %

-                 (self.icontainer, self.jmeno))

+                 (self.icontainer, self.jmeno), verbose=is_not_silent())

          elif "docker=" in self.icontainer:

              pass

          else:

-             self.runHost("docker pull %s" % self.jmeno)

+             self.runHost("docker pull %s" % self.jmeno, verbose=is_not_silent())

  

          self.containerInfo = json.loads(

              self.runHost(

                  "docker inspect %s" %

-                 self.jmeno).stdout)[0]["Config"]

+                 self.jmeno, verbose=is_not_silent()).stdout)[0]["Config"]

  

      def start(self, args="-it -d", command="/bin/bash"):

          """
@@ -330,11 +333,11 @@ 

              if 'start' in self.info and self.info['start']:

                  self.docker_id = self.runHost(

                      "%s -d %s" %

-                     (self.info['start'], self.jmeno), shell=True, ignore_bg_processes=True).stdout

+                     (self.info['start'], self.jmeno), shell=True, ignore_bg_processes=True, verbose=is_not_silent()).stdout

              else:

                  self.docker_id = self.runHost(

                      "docker run %s %s %s" %

-                     (args, self.jmeno, command), shell=True, ignore_bg_processes=True).stdout

+                     (args, self.jmeno, command), shell=True, ignore_bg_processes=True, verbose=is_not_silent()).stdout

              self.docker_id = self.docker_id.strip()

              if self.getPackageList():

                  a = self.run(
@@ -354,7 +357,7 @@ 

                  else:

                      print_info("Nothing installed (nor via {HOSTPACKAGER} nor {GUESTPACKAGER}), but package list is not empty", self.getPackageList())

          if self.status() is False:

-             raise BaseException("Container %s (for module %s) is not running, probably DEAD immediately after start (ID: %s)" % (self.jmeno, self.moduleName, self.docker_id))

+             raise ContainerExc("Container %s (for module %s) is not running, probably DEAD immediately after start (ID: %s)" % (self.jmeno, self.moduleName, self.docker_id))

  

      def stop(self):

          """
@@ -364,8 +367,8 @@ 

          """

          if self.status():

              try:

-                 self.runHost("docker stop %s" % self.docker_id)

-                 self.runHost("docker rm %s" % self.docker_id)

+                 self.runHost("docker stop %s" % self.docker_id, verbose=is_not_silent())

+                 self.runHost("docker rm %s" % self.docker_id, verbose=is_not_silent())

              except Exception as e:

                  print_debug(e, "docker already removed")

                  pass
@@ -378,7 +381,7 @@ 

          """

          if self.docker_id and self.docker_id[

                  : 12] in self.runHost(

-                 "docker ps", shell=True).stdout:

+                 "docker ps", shell=True, verbose=is_not_silent()).stdout:

              return True

          else:

              return False
@@ -406,7 +409,7 @@ 

          :return: None

          """

          self.start()

-         self.runHost("docker cp %s %s:%s" % (src, self.docker_id, dest))

+         self.runHost("docker cp %s %s:%s" % (src, self.docker_id, dest), verbose=is_not_silent())

  

      def copyFrom(self, src, dest):

          """
@@ -417,7 +420,7 @@ 

          :return: None

          """

          self.start()

-         self.runHost("docker cp %s:%s %s" % (self.docker_id, src, dest))

+         self.runHost("docker cp %s:%s %s" % (self.docker_id, src, dest), verbose=is_not_silent())

  

      def __callSetupFromConfig(self):

          """
@@ -426,7 +429,7 @@ 

          :return: None

          """

          if self.info.get("setup"):

-             self.runHost(self.info.get("setup"), shell=True, ignore_bg_processes=True)

+             self.runHost(self.info.get("setup"), shell=True, ignore_bg_processes=True, verbose=is_not_silent())

  

      def __callCleanupFromConfig(self):

          """
@@ -435,7 +438,7 @@ 

          :return: None

          """

          if self.info.get("cleanup"):

-             self.runHost(self.info.get("cleanup"), shell=True, ignore_bg_processes=True)

+             self.runHost(self.info.get("cleanup"), shell=True, ignore_bg_processes=True, verbose=is_not_silent())

  

  

  class RpmHelper(CommonFunctions):
@@ -521,7 +524,7 @@ 

                  elif self.info.get('repos'):

                      self.repos = self.info.get('repos')

                  else:

-                     raise ValueError("no RPM given in file or via URL")

+                     raise RpmExc("no RPM given in file or via URL")

          if whattooinstall:

              self.whattoinstallrpm = " ".join(set(whattooinstall))

          else:
@@ -564,21 +567,20 @@ 

  

          :return: None

          """

-         try:

-             self.runHost(

-                 "%s --disablerepo=* --enablerepo=%s* --allowerasing install %s" %

-                 (trans_dict["HOSTPACKAGER"],self.moduleName, self.whattoinstallrpm))

-             self.runHost(

-                 "%s --disablerepo=* --enablerepo=%s* --allowerasing distro-sync" %

-                 (trans_dict["HOSTPACKAGER"], self.moduleName), ignore_status=True)

-         except Exception as e:

-             raise Exception(

-                 "ERROR: Unable to install packages %s from repositories \n%s\n original exeption:\n%s\n" %

-                 (self.whattoinstallrpm,

-                  self.runHost(

-                      "cat %s" %

-                      self.yumrepo).stdout,

-                     e))

+ 

+         a = self.runHost(

+             "%s --disablerepo=* --enablerepo=%s* --allowerasing install %s" %

+             (trans_dict["HOSTPACKAGER"],self.moduleName, self.whattoinstallrpm), ignore_status=True, verbose=is_not_silent())

+         b =self.runHost(

+             "%s --disablerepo=* --enablerepo=%s* --allowerasing distro-sync" %

+             (trans_dict["HOSTPACKAGER"], self.moduleName), ignore_status=True, verbose=is_not_silent())

+ 

+         if a.exit_status != 0 and b.exit_status != 0:

+             raise RpmExc("ERROR: Unable to install packages %s" % self.whattoinstallrpm,

+                          "repositories are: ",

+                          self.runHost("cat %s" % self.yumrepo, verbose=is_not_silent()).stdout)

+ 

+         self.ipaddr = trans_dict["GUESTIPADDR"]

  

      def status(self, command="/bin/true"):

          """
@@ -589,9 +591,9 @@ 

          """

          try:

              if 'status' in self.info and self.info['status']:

-                 a = self.runHost(self.info['status'], shell=True, verbose=False, ignore_bg_processes=True)

+                 a = self.runHost(self.info['status'], shell=True, ignore_bg_processes=True, verbose=is_not_silent())

              else:

-                 a = self.runHost("%s" % command, shell=True, verbose=False, ignore_bg_processes=True)

+                 a = self.runHost("%s" % command, shell=True, ignore_bg_processes=True, verbose=is_not_silent())

              print_debug("command:",a.command ,"stdout:",a.stdout, "stderr:", a.stderr)

              return True

          except BaseException:
@@ -606,9 +608,9 @@ 

          :return: None

          """

          if 'start' in self.info and self.info['start']:

-             self.runHost(self.info['start'], shell=True, ignore_bg_processes=True)

+             self.runHost(self.info['start'], shell=True, ignore_bg_processes=True, verbose=is_not_silent())

          else:

-             self.runHost("%s" % command, shell=True, ignore_bg_processes=True)

+             self.runHost("%s" % command, shell=True, ignore_bg_processes=True, verbose=is_not_silent())

  

      def stop(self, command="/bin/true"):

          """
@@ -619,9 +621,9 @@ 

          :return: None

          """

          if 'stop' in self.info and self.info['stop']:

-             self.runHost(self.info['stop'], shell=True, ignore_bg_processes=True)

+             self.runHost(self.info['stop'], shell=True, ignore_bg_processes=True, verbose=is_not_silent())

          else:

-             self.runHost("%s" % command, shell=True, ignore_bg_processes=True)

+             self.runHost("%s" % command, shell=True, ignore_bg_processes=True, verbose=is_not_silent())

  

      def run(self, command="ls /", **kwargs):

          """
@@ -642,7 +644,7 @@ 

          :param dest: str

          :return: None

          """

-         self.runHost("cp -r %s %s" % (src, dest))

+         self.runHost("cp -r %s %s" % (src, dest), verbose=is_not_silent())

  

      def copyFrom(self, src, dest):

          """
@@ -652,7 +654,7 @@ 

          :param dest: str

          :return: None

          """

-         self.runHost("cp -r %s %s" % (src, dest))

+         self.runHost("cp -r %s %s" % (src, dest), verbose=is_not_silent())

  

      def __callSetupFromConfig(self):

          """
@@ -661,7 +663,7 @@ 

          :return: None

          """

          if self.info.get("setup"):

-             self.runHost(self.info.get("setup"), shell=True, ignore_bg_processes=True)

+             self.runHost(self.info.get("setup"), shell=True, ignore_bg_processes=True, verbose=is_not_silent())

  

      def __callCleanupFromConfig(self):

          """
@@ -670,7 +672,7 @@ 

          :return: None

          """

          if self.info.get("cleanup"):

-             self.runHost(self.info.get("cleanup"), shell=True, ignore_bg_processes=True)

+             self.runHost(self.info.get("cleanup"), shell=True, ignore_bg_processes=True, verbose=is_not_silent())

  

  

  class NspawnHelper(RpmHelper):
@@ -715,12 +717,13 @@ 

              # (failing because of selinux)

              self.__selinuxState = self.runHost(

                  "getenforce", ignore_status=True).stdout.strip()

-             self.runHost("setenforce Permissive", ignore_status=True)

+             self.runHost("setenforce Permissive", ignore_status=True, verbose=is_not_silent())

          self.setModuleDependencies()

          self.setRepositoriesAndWhatToInstall()

          self.installTestDependencies()

          self.__prepareSetup()

          self.__callSetupFromConfig()

+         self.__bootMachine()

  

      def __is_killed(self):

          for foo in range(DEFAULTRETRYTIMEOUT):
@@ -729,7 +732,7 @@ 

              if out.exit_status != 0:

                  print_debug("NSPAWN machine %s stopped" % self.jmeno)

                  return True

-         raise BaseException("Unable to stop machine %s within %d" % (self.jmeno,DEFAULTRETRYTIMEOUT))

+         raise NspawnExc("Unable to stop machine %s within %d" % (self.jmeno,DEFAULTRETRYTIMEOUT))

  

      def __is_booted(self):

          for foo in range(DEFAULTRETRYTIMEOUT):
@@ -739,7 +742,7 @@ 

                  time.sleep(2)

                  print_debug("NSPAWN machine %s booted" % self.jmeno)

                  return True

-         raise BaseException("Unable to start machine %s within %d" % (self.jmeno,DEFAULTRETRYTIMEOUT))

+         raise NspawnExc("Unable to start machine %s within %d" % (self.jmeno,DEFAULTRETRYTIMEOUT))

  

      def __prepareSetup(self):

          """
@@ -751,12 +754,12 @@ 

              shutil.rmtree(self.chrootpath, ignore_errors=True)

              os.mkdir(self.chrootpath)

          try:

-             self.runHost("machinectl terminate %s" % self.jmeno)

+             self.runHost("machinectl terminate %s" % self.jmeno, verbose=is_debug())

              self.__is_killed()

          except BaseException:

              pass

          if not os.path.exists(os.path.join(self.chrootpath, "usr")):

-             self.runHost("{HOSTPACKAGER} install systemd-container")

+             self.runHost("{HOSTPACKAGER} install systemd-container", verbose=is_not_silent())

              repos_to_use = ""

              counter = 0

              for repo in self.repos:
@@ -764,19 +767,21 @@ 

                  repos_to_use += " --repofrompath %s%d,%s" % (

                      self.moduleName, counter, repo)

              try:

-                 self.runHost(

-                     "%s --nogpgcheck install --installroot %s --allowerasing --disablerepo=* --enablerepo=%s* %s %s" %

-                     (trans_dict["HOSTPACKAGER"], self.chrootpath, self.moduleName, repos_to_use, self.whattoinstallrpm))

+                 @Retry(attempts=DEFAULTRETRYCOUNT, timeout=DEFAULTRETRYTIMEOUT*60, delay=2*60, error=NspawnExc("RETRY: Unable to install packages"))

+                 def tmpfunc():

+                     self.runHost(

+                         "%s install --nogpgcheck --setopt=install_weak_deps=False --installroot %s --allowerasing --disablerepo=* --enablerepo=%s* %s %s" %

+                         (trans_dict["HOSTPACKAGER"], self.chrootpath, self.moduleName, repos_to_use, self.whattoinstallrpm), verbose=is_not_silent())

+                 tmpfunc()

              except Exception as e:

-                 raise Exception(

+                 raise NspawnExc(

                      "ERROR: Unable to install packages %s\n original exeption:\n%s\n" %

                      (self.whattoinstallrpm, str(e)))

              # COPY yum repository inside NSPAW, to be able to do installations

              insiderepopath = os.path.join(self.chrootpath, self.yumrepo[1:])

              try:

                  os.makedirs(os.path.dirname(insiderepopath))

-             except Exception as e:

-                 print_debug(e)

+             except:

                  pass

              counter = 0

              f = open(insiderepopath, 'w')
@@ -818,17 +823,25 @@ 

                  shutil.copy(filename, pkipath_ch)

              print_info("repo prepared for microdnf:", insiderepopath, open(insiderepopath, 'r').read())

  

-         @Retry(attempts=DEFAULTRETRYCOUNT, timeout=DEFAULTRETRYTIMEOUT, delay=21, error=Exception("Timeout: Unable to start nspawn machine"))

+     def __bootMachine(self):

+ 

+         @Retry(attempts=DEFAULTRETRYCOUNT, timeout=DEFAULTRETRYTIMEOUT, delay=21,

+                error=NspawnExc("RETRY: Unable to start nspawn machine"))

          def tempfnc():

-             print_debug("starting container via command:", "systemd-nspawn --machine=%s -bD %s" % (self.jmeno, self.chrootpath))

+             print_debug("starting container via command:",

+                         "systemd-nspawn --machine=%s -bD %s" % (self.jmeno, self.chrootpath))

              nspawncont = utils.process.SubProcess(

                  "systemd-nspawn --machine=%s -bD %s" %

-                 (self.jmeno, self.chrootpath))

+                 (self.jmeno, self.chrootpath), verbose=is_debug())

              nspawncont.start()

              self.__is_booted()

+ 

          tempfnc()

          print_info("machine: %s started" % self.jmeno)

  

+         trans_dict["GUESTIPADDR"] = trans_dict["HOSTIPADDR"]

+         self.ipaddr = trans_dict["GUESTIPADDR"]

+ 

      def status(self, command="/bin/true"):

          """

          Return status of module
@@ -896,7 +909,7 @@ 

          try:

              if not kwargs:

                  kwargs = {}

-             kwargs["verbose"]=False

+             kwargs["verbose"]=is_not_silent()

              should_ignore=kwargs.get("ignore_status")

              kwargs["ignore_status"]=True

              b = self.runHost(
@@ -914,7 +927,7 @@ 

              if comout.exit_status == 0 or should_ignore:

                  return comout

              else:

-                 utils.process.CmdError(comout.command, comout)

+                 raise utils.process.CmdError(comout.command, comout)

  

      def selfcheck(self):

          """
@@ -935,7 +948,7 @@ 

          """

          self.runHost(

              " machinectl copy-to  %s %s %s" %

-             (self.jmeno, src, dest), timeout = DEFAULTPROCESSTIMEOUT, ignore_bg_processes=True)

+             (self.jmeno, src, dest), timeout = DEFAULTPROCESSTIMEOUT, ignore_bg_processes=True, verbose=is_not_silent())

  

      def copyFrom(self, src, dest):

          """
@@ -947,7 +960,7 @@ 

          """

          self.runHost(

              " machinectl copy-from  %s %s %s" %

-             (self.jmeno, src, dest), timeout = DEFAULTPROCESSTIMEOUT, ignore_bg_processes=True)

+             (self.jmeno, src, dest), timeout = DEFAULTPROCESSTIMEOUT, ignore_bg_processes=True, verbose=is_not_silent())

  

      def tearDown(self):

          """
@@ -956,7 +969,7 @@ 

          :return: None

          """

          self.stop()

-         self.runHost("machinectl poweroff %s" % self.jmeno)

+         self.runHost("machinectl poweroff %s" % self.jmeno, verbose=is_not_silent())

          # self.nspawncont.stop()

          self.__is_killed()

          if not os.environ.get('MTF_SKIP_DISABLING_SELINUX'):
@@ -965,7 +978,7 @@ 

              self.runHost(

                  "setenforce %s" %

                  self.__selinuxState,

-                 ignore_status=True)

+                 ignore_status=True, verbose=is_not_silent())

          if get_if_do_cleanup() and os.path.exists(self.chrootpath):

              shutil.rmtree(self.chrootpath, ignore_errors=True)

          self.__callCleanupFromConfig()
@@ -978,7 +991,7 @@ 

          :return: None

          """

          if self.info.get("setup"):

-             self.runHost(self.info.get("setup"), shell=True, ignore_bg_processes=True)

+             self.runHost(self.info.get("setup"), shell=True, ignore_bg_processes=True, verbose=is_not_silent())

  

      def __callCleanupFromConfig(self):

          """
@@ -987,7 +1000,7 @@ 

          :return: None

          """

          if self.info.get("cleanup"):

-             self.runHost(self.info.get("cleanup"), shell=True, ignore_bg_processes=True)

+             self.runHost(self.info.get("cleanup"), shell=True, ignore_bg_processes=True, verbose=is_not_silent())

  

  

  # INTERFACE CLASS FOR GENERAL TESTS OF MODULES
@@ -1005,16 +1018,19 @@ 

      :avocado: disable

      """

      def __init__(self,*args, **kwargs):

-         @Retry(attempts=1,timeout=55)

-         def tmpfunc():

-             super(AvocadoTest,self).__init__(*args, **kwargs)

-             (self.backend, self.moduleType) = get_correct_backend()

-             self.moduleProfile = get_correct_profile()

-             print_info(

-                 "Module Type: %s; Profile: %s" %

-                 (self.moduleType, self.moduleProfile))

-         tmpfunc()

+         super(AvocadoTest,self).__init__(*args, **kwargs)

+ 

+         (self.backend, self.moduleType) = get_correct_backend()

+         self.moduleProfile = get_correct_profile()

+         print_info(

+             "Module Type: %s; Profile: %s" %

+             (self.moduleType, self.moduleProfile))

  

+     def cancel(self, *args, **kwargs):

+         try:

+             super(AvocadoTest, self).cancel(*args, **kwargs)

+         except AttributeError:

+             raise exceptions.TestDecoratorSkip(*args, **kwargs)

  

      def setUp(self):

          """
@@ -1139,7 +1155,7 @@ 

          :return: str

          """

          self.start()

-         allpackages = self.run(r'rpm -qa --qf="%{{name}}\n"').stdout.split('\n')

+         allpackages = self.run(r'rpm -qa --qf="%{{name}}\n"', verbose=is_not_silent()).stdout.split('\n')

          return allpackages

  

      def copyTo(self, *args, **kwargs):
@@ -1228,7 +1244,6 @@ 

          super(NspawnAvocadoTest, self).setUp()

  

  

- 

  def get_correct_backend():

      """

      Return proper module type, set by config by default_module section, or defined via
@@ -1249,7 +1264,7 @@ 

      elif amodule == 'nspawn':

          return NspawnHelper(), amodule

      else:

-         raise ValueError("Unsupported MODULE={0}".format(amodule))

+         raise ModuleFrameworkException("Unsupported MODULE={0}".format(amodule), "supproted are: docker, rpm, nspawn")

  

  

  def get_correct_profile():
@@ -1289,13 +1304,13 @@ 

      if not cfgfile:

          cfgfile = "config.yaml"

      if not os.path.exists(cfgfile):

-         raise ValueError(

+         raise ConfigExc(

              "Config file (%s) does not exist or is inaccesible (you can also redefine own by CONFIG=path/to/configfile.yaml env variable)" %

              cfgfile)

      with open(cfgfile, 'r') as ymlfile:

          xcfg = yaml.load(ymlfile.read())

          if xcfg['document'] != 'modularity-testing':

-             raise ValueError(

+             raise ConfigExc(

                  "Bad Config file, not yaml or does not contain proper document type" %

                  cfgfile)

      return xcfg

file modified
+8 -7
@@ -80,7 +80,7 @@ 

      Class for parsing PDC data via some setters line setFullVersion, setViaFedMsg, setLatestPDC

      """

  

-     @Retry(attempts=DEFAULTRETRYCOUNT*5, timeout=DEFAULTRETRYTIMEOUT, delay=20)

+     @Retry(attempts=DEFAULTRETRYCOUNT*5, timeout=DEFAULTRETRYTIMEOUT, delay=20, error=PDCExc("RETRY: Unable to get data from PDC"))

      def __getDataFromPdc(self):

          """

          Internal method, do not use it
@@ -89,12 +89,12 @@ 

          """

          PDC = "%s/?variant_name=%s&variant_version=%s&variant_release=%s&active=True" % (

              PDCURL, self.name, self.stream, self.version)

-         print_debug("attemt to contact PDC with:", PDC)

+         print_info("Attemt to contact PDC (may take longer time) with query:", PDC)

          out=json.load(urllib.urlopen(PDC))["results"]

          if out:

              self.pdcdata = out[-1]

          else:

-             raise BaseException("Unable to get data from PDC URL: %s" % PDC)

+             raise PDCExc("Unable to get data from PDC URL: %s" % PDC)

  

      def setFullVersion(self, nvr):

          """
@@ -142,8 +142,9 @@ 

  

          :return: str

          """

-         rpmrepo = "http://kojipkgs.fedoraproject.org/repos/%s/latest/%s" % (

-             self.pdcdata["koji_tag"] + "-build", ARCH)

+         #rpmrepo = "http://kojipkgs.fedoraproject.org/repos/%s/latest/%s" % (

+         #    self.pdcdata["koji_tag"] + "-build", ARCH)

+         rpmrepo = "https://kojipkgs.stg.fedoraproject.org/compose/branched/jkaluza/latest-Fedora-Modular-26/compose/Server/%s/os/" % ARCH

          return rpmrepo

  

      def generateGitHash(self):
@@ -220,7 +221,7 @@ 

                  if len(pkgbouid) > 4:

                      print_info("DOWNLOADING: %s" % foo)

  

-                     @Retry(attempts=DEFAULTRETRYCOUNT*10, timeout=DEFAULTRETRYTIMEOUT*60, delay=DEFAULTRETRYTIMEOUT, error=Exception("Unbale to fetch package from koji after %d attempts" % (DEFAULTRETRYCOUNT*10)))

+                     @Retry(attempts=DEFAULTRETRYCOUNT*10, timeout=DEFAULTRETRYTIMEOUT*60, delay=DEFAULTRETRYTIMEOUT, error=KojiExc("RETRY: Unbale to fetch package from koji after %d attempts" % (DEFAULTRETRYCOUNT*10)))

                      def tmpfunc():

                          a = utils.process.run(

                              "cd %s; koji download-build %s  -a %s -a noarch" %
@@ -229,7 +230,7 @@ 

                              if "packages available for" in a.stdout.strip():

                                  print_info('UNABLE TO DOWNLOAD package (intended for other architectures, GOOD):', a.command)

                              else:

-                                 raise BaseException('UNABLE TO DOWNLOAD package (KOJI issue, BAD):', a.command)

+                                 raise KojiExc('UNABLE TO DOWNLOAD package (KOJI issue, BAD):', a.command)

                      tmpfunc()

              utils.process.run(

                  "cd %s; createrepo -v %s" %

file modified
+2 -1
@@ -97,6 +97,7 @@ 

      install_requires=['avocado-framework',

                        'netifaces',

                        'behave',

-                       'PyYAML'

+                       'PyYAML',

+                       'dockerfile-parse'

                        ]

  )

file modified
+66
@@ -21,8 +21,66 @@ 

  # Authors: Jan Scotka <jscotka@redhat.com>

  #

  

+ import os

+ 

  

  from moduleframework import module_framework

+ from moduleframework import dockerlinter

+ 

+ 

+ class DockerfileLinter(module_framework.ContainerAvocadoTest):

+     """

+     :avocado: enable

+ 

+     """

+ 

+     dp = None

+ 

+     def setUp(self):

+         # it is not intended just for docker, but just docker packages are

+         # actually properly signed

+         super(self.__class__, self).setUp()

+         self.dp = dockerlinter.DockerLinter(os.path.join(os.getcwd(), ".."))

+         if self.dp is None:

+             self.skip()

+ 

+     def testDockerFromBaseruntime(self):

+         self.assertTrue(self.dp.check_baseruntime())

+ 

+     def testDockerRunMicrodnf(self):

+         self.assertTrue(self.dp.check_microdnf())

+ 

+     def testArchitectureInEnvAndLabelExists(self):

+         self.assertTrue(self.dp.get_docker_specific_env("ARCH="))

+         self.assertTrue(self.dp.get_specific_label("architecture"))

+ 

+     def testNameInEnvAndLabelExists(self):

+         self.assertTrue(self.dp.get_docker_specific_env("NAME="))

+         self.assertTrue(self.dp.get_specific_label("name"))

+ 

+     def testReleaseLabelExists(self):

+         self.assertTrue(self.dp.get_specific_label("release"))

+ 

+     def testVersionLabelExists(self):

+         self.assertTrue(self.dp.get_specific_label("version"))

+ 

+     def testComRedHatComponentLabelExists(self):

+         self.assertTrue(self.dp.get_specific_label("com.redhat.component"))

+ 

+     def testIok8sDescriptionExists(self):

+         self.assertTrue(self.dp.get_specific_label("io.k8s.description"))

+ 

+     def testIoOpenshiftExposeServicesExists(self):

+         label_io_openshift = "io.openshift.expose-services"

+         exposes = self.dp.get_docker_expose()

+         label_list = self.dp.get_docker_labels()

+         self.assertTrue(label_list[label_io_openshift])

+         for exp in exposes:

+             self.assertTrue("%s" % exp in label_list[label_io_openshift])

+ 

+     def testIoOpenShiftTagsExists(self):

+         label_list = self.dp.get_docker_labels()

+         self.assertTrue("io.openshift.tags" in label_list)

  

  

  class DockerLint(module_framework.ContainerAvocadoTest):
@@ -35,10 +93,18 @@ 

          self.assertTrue("bin" in self.run("ls /").stdout)

  

      def testContainerIsRunning(self):

+         """

+         Function tests whether container is running

+         :return:

+         """

          self.start()

          self.assertIn(self.backend.jmeno, self.runHost("docker ps").stdout)

  

      def testLabels(self):

+         """

+         Function tests whether labels are set in modulemd YAML file properly.

+         :return:

+         """

          llabels = self.getConfigModule().get('labels')

          if llabels is None or len(llabels) == 0:

              print "No labels defined in config to check"

@@ -42,12 +42,15 @@ 

          """

          self.log.info("Checking availability of component and installation and remove them")

          for profile in self.getModulemdYamlconfig()["data"].get("profiles"):

-             actualpackagelist = " ".join(

-                 set(self.getModulemdYamlconfig()["data"]["profiles"].get(profile))

-                 -set(self.backend.bootstrappackages)

-             )

+             actualpackagelist = set(self.getModulemdYamlconfig()["data"]["profiles"][profile]["rpms"]) - set(self.backend.bootstrappackages)

              packager = common.trans_dict["GUESTPACKAGER"]

              if actualpackagelist:

-                 self.run("%s install %s" % (packager, actualpackagelist))

-                 self.run("rpm -q %s" % actualpackagelist)

-                 self.run("%s remove %s" % (packager, actualpackagelist))

+                 checkpackage = self.run("rpm -q --qf='%{{name}}\\n' " + " ".join(actualpackagelist), ignore_status=True).stdout.split()

+                 installed = [x for x in checkpackage if "not installed" not in x]

+                 self.log.info("Already installed packages:", installed)

+ 

+                 actualpackages = " ".join(list(set(actualpackagelist)-set(installed)))

+                 if len(actualpackages)>2:

+                     self.run("%s install %s" % (packager, actualpackages))

+                     self.run("rpm -q %s" % actualpackagelist)

+                     self.run("%s remove %s" % (packager, actualpackages))

This Pull Request covers dockerlinter.
It checks these things:
- FROM baseruntime/baseruntime
- Dockerfile uses microdnf and not dnf
- ARCH exists in ENV directive and LABEL container architecture too.
- NAME exists in ENV directive and LABEL container name too.
- LABEL contains release.
- com.redhat.component exists in LABEL
- io.k8s.description exists in LABEL
- io.openshift.expose-services exists in LABEL
- io.openshift.tags exists in LABEL

1 new commit added

  • Add dependency into python2-dockerfile-parse
6 years ago

@jscotka @ttomecek I would be glad for any feedback.

probably this change also need to have same change in setup.py file for pip, to install this dependency

I don't understand why functions _get_from, _get_run, _get_expose are same, probably would be better to have one, just doing something like normalize, or whatever how to name it

in case of negative test, would be better to check all possibilities, also check that "yum" is not used too.

Please reconsider to merge it with DockerLint: https://pagure.io/modularity-testing-framework/blob/dockerlinter/f/tools/modulelint.py#_102
probably some of tests are same, or could be on this level. There is also check for labels against config. Could be here.

maybe there can be removed os.path.join(os.getcwd() and then there can be removed import os,
dockerlinter.DockerLinter("..")
should work well too I think

All functions *LabelExists seems to be very similar, reconsider to create one function in dockerlinter.DockerLinter class, and these function will be just wrapper like:

def testArchitectureInEnvAndLabelExists(self):
self.assertTrue(dp.label_check(label=architecture, env='ARCH='))

For me this line in every test does not make sense: if self.dp is not None
in case tests does not have sense, just add it into setUp like:

if not self.dp:
     self.skip()

2 new commits added

  • Fixes according to comments.
  • Add dockerfile-parse into deps.
6 years ago

Seems well now.
If you want I can merge it, after you rebase it, to be able to merge it.
Actually merging is blocked by some conflicts.

1 new commit added

  • Merge branch 'master' into dockerlinter
6 years ago

Pull-Request has been merged by jscotka

6 years ago