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

Add support for dockerfiles #81

Merged
merged 13 commits into from
Apr 16, 2018
Merged
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
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