',
+ 'icon': """
+
+
+
+ """ },
+
+ 'info':
+ { 'alert': '
',
+ 'icon': """
+
"""}
+ }
+
+ def __init__ (self, git_url, git_path=None, git_commit=None):
+ self._git_url = git_url
+ self._git_path = git_path
+ self._git_commit = git_commit
+ self._temp_dir = None
+
+ def __del__ (self):
+ if self._temp_dir:
+ try:
+ shutil.rmtree(self._temp_dir)
+ except (IOError, OSError, AttributeError) as exc:
+ pass
+
+ def _get_dockerfile (self):
+ self._temp_dir = tempfile.mkdtemp ()
+ git.Repo.clone_from (self._git_url, self._temp_dir)
+ if self._git_path:
+ if self._git_path.endswith('Dockerfile'):
+ git_df_dir = os.path.dirname(self._git_path)
+ df_path = os.path.abspath(os.path.join(self._temp_dir,
+ git_df_dir))
+ else:
+ df_path = os.path.abspath(os.path.join(self._temp_dir,
+ self._git_path))
+ else:
+ df_path = self._temp_dir
+
+ self._Dockerfile = os.path.join(df_path, "Dockerfile")
+
+ def _run_dockerfile_lint (self):
+ with open ("/dev/null", "rw") as devnull:
+ dfl = subprocess.Popen (["dockerfile_lint",
+ "-j",
+ "-r", self.rules,
+ "-f", self._Dockerfile],
+ stdin=devnull,
+ stdout=subprocess.PIPE,
+ stderr=devnull,
+ close_fds=True)
+
+ (stdout, stderr) = dfl.communicate ()
+ jsonstr = stdout.decode ()
+ self._lint = json.loads (jsonstr)
+
+ def _mark_dockerfile (self):
+ out = ""
+ with open(self._Dockerfile, "r") as df:
+ dflines = df.readlines ()
+ dflines.append ("\n") # Extra line to bind 'absent' messages to
+ lastline = len (dflines)
+ msgs_by_linenum = {}
+ for severity in ["error", "warn", "info"]:
+ for msg in self._lint[severity]["data"]:
+ if "line" in msg:
+ linenum = msg["line"]
+ else:
+ linenum = lastline
+
+ msgs = msgs_by_linenum.get (linenum, [])
+ msgs.append (msg)
+ msgs_by_linenum[linenum] = msgs
+
+ linenum = 1
+ for line in dflines:
+ msgs = msgs_by_linenum.get (linenum, [])
+ linenum += 1
+ if not msgs:
+ continue
+
+ display_line = False
+ msgout = ""
+ for msg in msgs:
+ if "line" in msg:
+ display_line = True
+
+ level = msg["level"]
+ classes = self.PF_CLASSES.get (level)
+ if not classes:
+ continue
+
+ msgout += classes["alert"] + classes["icon"] + "\n"
+ msgout += ("
" +
+ html_escape (msg["message"]) +
+ " ")
+ description = msg.get ("description", "None")
+ if description != "None":
+ msgout += html_escape (description)
+ url = msg.get ("reference_url", "None")
+ if url != "None":
+ if type (url) == list:
+ url = reduce (lambda a, b: a + b, url)
+
+ msgout += ('
'
+ '(more info)' % url)
+ msgout += "\n
\n"
+
+ if display_line:
+ out += (("
%d:" % linenum) +
+ html_escape (line).rstrip () + "
\n")
+
+ out += msgout
+
+ if out == "":
+ out = """
+
+
+ Looks good! This Dockerfile has no output from dockerfile_lint.
+
+"""
+
+ self._html_markup = out
+
+ def run (self):
+ self._get_dockerfile ()
+ self._run_dockerfile_lint ()
+ self._mark_dockerfile ()
+
+ if self._temp_dir:
+ try:
+ shutil.rmtree(self._temp_dir)
+ self._temp_dir = None
+ except (IOError, OSError) as exc:
+ pass
+
+ return self._html_markup
+
+ def get_json (self):
+ return self._lint
+
+if __name__ == "__main__":
+ git_url = "https://github.com/TomasTomecek/docker-hello-world.git"
+ lint = DockerfileLint (git_url)
+ print (lint.run ())
diff --git a/dbs/models.py b/dbs/models.py
index cb8a473..38b0050 100644
--- a/dbs/models.py
+++ b/dbs/models.py
@@ -20,6 +20,12 @@ def __unicode__(self):
return json.dumps(json.loads(self.json), indent=4)
+class TaskLint(models.Model):
+ lint = models.TextField()
+
+ def __unicode__(self):
+ return self.lint
+
class Task(models.Model):
STATUS_PENDING = 1
@@ -48,6 +54,7 @@ class Task(models.Model):
type = models.IntegerField(choices=_TYPE_NAMES.items())
owner = models.CharField(max_length=38)
task_data = models.ForeignKey(TaskData)
+ task_lint = models.ForeignKey(TaskLint, null=True, blank=True)
log = models.TextField(blank=True, null=True)
class Meta:
diff --git a/dbs/task_api.py b/dbs/task_api.py
index b68cc76..f13827e 100644
--- a/dbs/task_api.py
+++ b/dbs/task_api.py
@@ -21,7 +21,11 @@ def watch_task(task, callback, kwargs=None):
:return: None
"""
- response = task.wait()
+ try:
+ response = task.wait()
+ except Exception as exc:
+ response = exc
+
if kwargs:
callback(response, **kwargs)
else:
@@ -33,7 +37,7 @@ class TaskApi(object):
def build_docker_image(self, build_image, git_url, local_tag, git_dockerfile_path=None, git_commit=None,
parent_registry=None, target_registries=None, tag=None, repos=None,
- callback=None, kwargs=None):
+ callback=None, lint_callback=None, kwargs=None):
"""
build docker image from supplied git repo
@@ -63,14 +67,43 @@ def build_docker_image(self, build_image, git_url, local_tag, git_dockerfile_pat
'git_commit': git_commit,
'git_dockerfile_path': git_dockerfile_path,
'repos': repos}
- task_info = tasks.build_image.apply_async(args=args, kwargs=task_kwargs,
- link=tasks.submit_results.s())
- task_id = task_info.task_id
- if callback:
- t = Thread(target=watch_task, args=(task_info, callback, kwargs))
- #w.daemon = True
- t.start()
- return task_id
+
+ # The linter task, which runs dockerfile_lint
+ linter_task = tasks.linter.s(git_url,
+ git_dockerfile_path,
+ git_commit)
+
+ # This task builds the image
+ build_image_task = tasks.build_image.subtask((build_image,
+ git_url,
+ local_tag),
+ **task_kwargs)
+
+ # This task submits the results
+ submit_results_task = tasks.submit_results.s()
+
+ # Chain the tasks together in the right order and start them
+ task_chain = (linter_task |
+ build_image_task |
+ submit_results_task).apply_async()
+
+ # Call lint_callback when the linter task is done
+ linter = task_chain.parent.parent # 3rd from last task
+ lint_watcher = Thread(target=watch_task,
+ args=(linter,
+ lint_callback,
+ kwargs))
+ lint_watcher.start()
+
+ # Call callback when the entire chain is done
+ chain_watcher = Thread(target=watch_task,
+ args=(task_chain,
+ callback,
+ kwargs))
+ chain_watcher.start()
+
+ # Return the celery task ID of the chain
+ return task_chain.task_id
def find_dockerfiles_in_git(self):
raise NotImplemented()
diff --git a/dbs/tasks.py b/dbs/tasks.py
index e565c31..9d20c8c 100644
--- a/dbs/tasks.py
+++ b/dbs/tasks.py
@@ -3,7 +3,38 @@
from celery import shared_task
from dock.core import DockerBuilder, DockerTasker
from dock.outer import PrivilegedDockerBuilder
+from dbs.lint import DockerfileLint
+import time
+class LintErrors(Exception):
+ """
+ This exception indicates the build was not attempted due to
+ lint errors.
+ """
+
+@shared_task
+def linter(git_url, git_path=None, git_commit=None):
+ """
+ run dockerfile_lint on the Dockerfile we want to build
+
+ :param git_url: url to git repo
+ :param git_path: path to dockerfile within git repo (default is ./Dockerfile)
+ :param git_commit: which commit to checkout (master by default)
+ :return: HTML markup of Dockerfile with dockerfile_lint messages
+ """
+ json = None
+ try:
+ lint = DockerfileLint (git_url, git_path, git_commit)
+ html_markup = lint.run ()
+ json = lint.get_json ()
+ except OSError as exc:
+ # Perhaps dockerfile_lint is not installed
+ html_markup = "Executing dockerfile_lint: %s" % exc.strerror
+ except ValueError as exc:
+ # Perhaps there was a problem parsing the JSON output
+ html_markup = "Internal error: %s" % exc.message
+ finally:
+ return { "json": json, "html_markup": html_markup }
@shared_task
def build_image_hostdocker(
@@ -43,13 +74,14 @@ def build_image_hostdocker(
# TODO: postbuild_data = run_postbuild_plugins(d, private_tag)
return inspect_data
-@shared_task
-def build_image(build_image, git_url, local_tag, git_dockerfile_path=None,
+@shared_task(throws=(LintErrors,))
+def build_image(lint, build_image, git_url, local_tag, git_dockerfile_path=None,
git_commit=None, parent_registry=None, target_registries=None,
tag=None, repos=None, store_results=True):
"""
build docker image from provided arguments inside privileged container
+ :param lint: output from linter task
:param build_image: name of the build image (supplied docker image is built inside this image)
:param git_url: url to git repo
:param local_tag: image is known within the service with this tag
@@ -63,6 +95,13 @@ def build_image(build_image, git_url, local_tag, git_dockerfile_path=None,
in local docker registry
:return: dict with data from docker inspect
"""
+ if lint and lint["json"]:
+ count = lint["json"]["error"]["count"]
+ if count > 0:
+ time.sleep (1) # Shouldn't be needed but seems to be
+ raise LintErrors("Build aborted: %d dockerfile_lint errors" %
+ count)
+
db = PrivilegedDockerBuilder(build_image, {
"git_url": git_url,
"local_tag": local_tag,
@@ -108,4 +147,4 @@ def submit_results(result):
"""
# 2 requests, one for 'finished', other for data
print(result)
-
+ return result
diff --git a/dbs/web/templates/dbs/task_detail.html b/dbs/web/templates/dbs/task_detail.html
index 6b5f2af..5ccf892 100644
--- a/dbs/web/templates/dbs/task_detail.html
+++ b/dbs/web/templates/dbs/task_detail.html
@@ -49,6 +49,10 @@
Task Detail
Logs
{{ task.log|linebreaks }}
{% endif %}
+ {% if task.task_lint != nil %}
+
Dockerfile lint
+ {{ task.task_lint|safe }}
+ {% endif %}
{% endblock %}