Skip to content

Commit

Permalink
Merge pull request #81 from lachmanfrantisek/dockerfile-support
Browse files Browse the repository at this point in the history
Add support for dockerfiles
  • Loading branch information
jpopelka committed Apr 16, 2018
2 parents 1589944 + 2e1a08d commit 5a33b88
Show file tree
Hide file tree
Showing 20 changed files with 291 additions and 142 deletions.
92 changes: 86 additions & 6 deletions colin/checks/abstract/dockerfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,64 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import logging
import re

from ..check_utils import check_label
from ..result import CheckResult
from .abstract_check import AbstractCheck

logger = logging.getLogger(__name__)


def get_instructions_from_dockerfile_parse(dfp, instruction):
"""
Get the list of instruction dictionary for given instruction name.
(Subset of DockerfileParser.structure only for given instruction.)
:param dfp: DockerfileParser
:param instruction: str
:return: list
"""
return [inst for inst in dfp.structure if inst["instruction"] == instruction]


class DockerfileCheck(AbstractCheck):
pass


class InstructionCheck(AbstractCheck):
class InstructionCheck(DockerfileCheck):

def __init__(self, name, message, description, reference_url, tags, instruction, regex, required):
def __init__(self, name, message, description, reference_url, tags, instruction, value_regex, required):
super().__init__(name, message, description, reference_url, tags)
self.instruction = instruction
self.regex = regex
self.value_regex = value_regex
self.required = required

def check(self, target):
pass
instructions = get_instructions_from_dockerfile_parse(target.instance, self.instruction)
pattern = re.compile(self.value_regex)
logs = []
passed = True
for inst in instructions:
match = bool(pattern.match(inst["value"]))
passed = match == self.required
log = "Value for instruction {} {}mach regex: '{}'.".format(inst["content"],
"" if match else "does not ",
self.value_regex)
logs.append(log)
logger.debug(log)

return CheckResult(ok=passed,
severity=self.severity,
description=self.description,
message=self.message,
reference_url=self.reference_url,
check_name=self.name,
logs=logs)

class InstructionCountCheck(AbstractCheck):

class InstructionCountCheck(DockerfileCheck):

def __init__(self, name, message, description, reference_url, tags, instruction, min_count=None, max_count=None):
super().__init__(name, message, description, reference_url, tags)
Expand All @@ -42,4 +79,47 @@ def __init__(self, name, message, description, reference_url, tags, instruction,
self.max_count = max_count

def check(self, target):
pass
count = len(get_instructions_from_dockerfile_parse(target.instance, self.instruction))

log = "Found {} occurrences of the {} instruction. Needed: min {} | max {}".format(count,
self.instruction,
self.min_count,
self.max_count)
logger.debug(log)
passed = True
if self.min_count:
passed = passed and self.min_count <= count
if self.max_count:
passed = passed and count <= self.max_count

return CheckResult(ok=passed,
severity=self.severity,
description=self.description,
message=self.message,
reference_url=self.reference_url,
check_name=self.name,
logs=[log])


class DockerfileLabelCheck(DockerfileCheck):

def __init__(self, name, message, description, reference_url, tags, label, required, value_regex=None):
super().__init__(name, message, description, reference_url, tags)
self.label = label
self.required = required
self.value_regex = value_regex

def check(self, target):
labels = target.instance.labels
passed = check_label(label=self.label,
required=self.required,
value_regex=self.value_regex,
labels=labels)

return CheckResult(ok=passed,
severity=self.severity,
description=self.description,
message=self.message,
reference_url=self.reference_url,
check_name=self.name,
logs=[])
2 changes: 1 addition & 1 deletion colin/checks/abstract/envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

import re

from ..result import CheckResult
from .containers import ContainerCheck
from .images import ImageCheck
from ..result import CheckResult


class EnvCheck(ContainerCheck, ImageCheck):
Expand Down
4 changes: 2 additions & 2 deletions colin/checks/abstract/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

from ...core.exceptions import ColinException
from ..result import CheckResult
from .containers import ContainerCheck
from .images import ImageCheck
from ..result import CheckResult
from ...core.exceptions import ColinException


class FileSystemCheck(ContainerCheck, ImageCheck):
Expand Down
31 changes: 10 additions & 21 deletions colin/checks/abstract/labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

import re

from ..check_utils import check_label
from ..result import CheckResult
from .containers import ContainerCheck
from .dockerfile import DockerfileCheck
from .images import ImageCheck
from ..result import CheckResult


class LabelCheck(ContainerCheck, ImageCheck):
class LabelCheck(ContainerCheck, ImageCheck, DockerfileCheck):

def __init__(self, name, message, description, reference_url, tags, label, required, value_regex=None):
super().__init__(name, message, description, reference_url, tags)
Expand All @@ -30,20 +29,10 @@ def __init__(self, name, message, description, reference_url, tags, label, requi
self.value_regex = value_regex

def check(self, target):
labels = target.instance.get_metadata()["Config"]["Labels"]
present = labels is not None and self.label in labels

if present:
if self.required and not self.value_regex:
passed = True
elif self.value_regex:
pattern = re.compile(self.value_regex)
passed = bool(pattern.match(labels[self.label]))
else:
passed = False

else:
passed = not self.required
passed = check_label(label=self.label,
required=self.required,
value_regex=self.value_regex,
labels=target.labels)

return CheckResult(ok=passed,
severity=self.severity,
Expand All @@ -54,15 +43,15 @@ def check(self, target):
logs=[])


class DeprecatedLabelCheck(ContainerCheck, ImageCheck):
class DeprecatedLabelCheck(ContainerCheck, ImageCheck, DockerfileCheck):

def __init__(self, name, message, description, reference_url, tags, old_label, new_label):
super().__init__(name, message, description, reference_url, tags)
self.old_label = old_label
self.new_label = new_label

def check(self, target):
labels = target.instance.get_metadata()["Config"]["Labels"]
labels = target.labels
old_present = labels is not None and self.old_label in labels

passed = (not old_present) or (self.new_label in labels)
Expand Down
27 changes: 27 additions & 0 deletions colin/checks/check_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import re


def check_label(label, required, value_regex, labels):
"""
Check if the label is required and match the regex
:param label: str
:param required: bool (if the presence means pass or not)
:param value_regex: str
:param labels: [str]
:return: bool (required==True: True if the label is present and match the regex if specified)
(required==False: True if the label is not present)
"""
present = labels is not None and label in labels

if present:
if required and not value_regex:
return True
elif value_regex:
pattern = re.compile(value_regex)
return bool(pattern.match(labels[label]))
else:
return False

else:
return not required
31 changes: 21 additions & 10 deletions colin/checks/dockerfile/from_tag.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
from colin.checks.abstract.dockerfile import InstructionCheck
from colin.checks.abstract.dockerfile import DockerfileCheck
from colin.checks.result import CheckResult
from colin.core.target import ImageName


class FromTagCheck(InstructionCheck):
class FromTagCheck(DockerfileCheck):

def __init__(self):
super().__init__(name="is_tag_latest",
message="",
description="",
reference_url="https://docs.docker.com/engine/reference/builder/#from",
tags=["from", "dockerfile", "latest"],
instruction="FROM",
regex=".*/latest$",
required=False)
super().__init__(name="from_tag_not_latest",
message="In FROM, tag has to be specified and not 'latest'.",
description="Using the 'latest' tag may cause unpredictable builds."
"It is recommended that a specific tag is used in the FROM.",
reference_url="https://fedoraproject.org/wiki/Container:Guidelines#FROM",
tags=["from", "dockerfile", "baseimage", "latest"])

def check(self, target):
im = ImageName.parse(target.instance.baseimage)
passed = im.tag and im.tag != "latest"
return CheckResult(ok=passed,
severity=self.severity,
description=self.description,
message=self.message,
reference_url=self.reference_url,
check_name=self.name,
logs=[])
15 changes: 0 additions & 15 deletions colin/checks/dockerfile/layered_run.py

This file was deleted.

2 changes: 1 addition & 1 deletion colin/checks/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from six import iteritems

from ..core.constant import REQUIRED, PASSED, FAILED, WARNING, OPTIONAL
from ..core.constant import FAILED, OPTIONAL, PASSED, REQUIRED, WARNING


class CheckResult(object):
Expand Down
8 changes: 4 additions & 4 deletions colin/cli/colin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
from six import iteritems

from ..checks.abstract.abstract_check import AbstractCheck
from ..core.ruleset.ruleset import get_rulesets
from .default_group import DefaultGroup
from ..core.colin import get_checks, run
from ..core.constant import COLOURS, OUTPUT_CHARS
from ..core.exceptions import ColinException
from ..core.colin import run, get_checks
from ..core.ruleset.ruleset import get_rulesets
from ..version import __version__
from .default_group import DefaultGroup

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -58,7 +58,7 @@ def cli():
help="Verbose mode.")
def check(target, ruleset, ruleset_file, debug, json, stat, verbose):
"""
Check the image/container (default).
Check the image/container/dockerfile (default).
"""
if ruleset and ruleset_file:
raise click.BadOptionUsage("Options '--ruleset' and '--file-ruleset' cannot be used together.")
Expand Down
5 changes: 3 additions & 2 deletions colin/core/colin.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ def run(target, group=None, severity=None, tags=None, ruleset_name=None, ruleset
"""
Runs the sanity checks for the target.
:param target: str or Image/Container (name of the container or image or Image/Container instance from conu,
dockerfile will be added in the future)
:param target: str
or Image/Container (name of the container/image or Image/Container instance from conu)
or path or file-like object for dockerfile
:param group: str (name of the folder with group of checks, if None, all of them will be checked.)
:param severity: str (if not None, only those checks will be run -- optional x required x warn ...)
:param tags: list of str (if not None, the checks will be filtered by tags.)
Expand Down
1 change: 1 addition & 0 deletions colin/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#


class ColinException(Exception):
""" Generic exception when something goes wrong with colin. """

Expand Down
Loading

0 comments on commit 5a33b88

Please sign in to comment.