Commit b7b30d3e authored by BAIRE Anthony's avatar BAIRE Anthony
Browse files

derive docker tags names from WebappVersion.id

With this change docker images are no longer
named as: <Webapp.docker_name>:<WebappVersion.number>
but       <Webapp.docker_name>:id</WebappVersion.id>

This is only for storage, for the user we still present the image as
<Webapp.docker_name>:<WebappVersion.number>

There are multiple reasons to do that:
- this simplifies the controller design, because docker images are no
  longer replaced (once an image is committed with tag, 'id<SOMETHING>'
  it won't be modified anymore) -> thus it is no longer necessary to
  track the image state carefully (when pushing/pulling from/to the
  registry)
- this prevent reusing dangling images from a removed webapp (because we
  now have a strong guarantee that the image tags are unique)
- this will avoid nasty race conditions when we implement direct 'push'
  to the registry (because we then assign the new image id before the
  manifest is actually pushed, if a push and commit are done in the same
  time we will keep the latest one, i.e. with the highest id)
- this will make easy to implement image recovery: we can keep removed
  images in the registry for some time (eg: 1 month) before they are
  really deleted

Note: the REPLACED state is no longer transient (since we now keep the
replaced images in the db and since we may still have remaining
job/sandboxes using them). Maybe we can rename it as DELETED when we
implement #265.
parent cfa7268c
......@@ -537,7 +537,7 @@ class SandboxManager(Manager):
state = int(VersionState.SANDBOX))
)
def _start(self, webapp, version):
def _start(self, webapp, image_tag):
"""Start a webapp sandbox
(to be executed in a thread pool)
......@@ -547,14 +547,7 @@ class SandboxManager(Manager):
ses = ctrl.session
# prepare sandbox parameters
# docker image
if version is None:
image = "%s:%s" % (ctrl.gen_factory_name(webapp.docker_os),
webapp.docker_os.version)
else:
image = "%s:%s" % (webapp.image_name, version.number)
image = ":".join(image_tag)
log.debug("sandbox %r: using image %r", webapp.docker_name, image)
# safety checks
......@@ -698,7 +691,7 @@ EOF
ctrl.check_host_path("isdir", etc_dir)
ctrl.check_host_path("isdir", run_dir)
if version is None and webapp.entrypoint:
if webapp.sandbox_version_id is None and webapp.entrypoint:
# prepend instructions to initialise a dummy entrypoint
dn, bn = os.path.split(webapp.entrypoint)
# FIXME: do nothing if entrypoint already exists
......@@ -802,7 +795,6 @@ exec /.toolbox/bin/sshd -D
", ".join(map(repr, sorted(v.number for v in versions))))
recover = tuple(v.id for v in versions)
# TODO: make 'sandbox' a reserved name
if error:
description = "pre-commit error: " + error
......@@ -824,15 +816,19 @@ exec /.toolbox/bin/sshd -D
published = False,
state = int(VersionState.SANDBOX))
ses.add(version)
version.webapp
ses.refresh(version)
ses.expunge(version)
ses.expunge_all()
assert version is not None
# commit the docker image
image, tag = ctrl.gen_image_tag(version, webapp)
log.debug("dicts %r %r", webapp.__dict__, version.__dict__)
log.info("commit sandbox %r version %r", webapp.docker_name, version.number)
log.info("commit sandbox %r version %r (%s)",
webapp.docker_name, version.number, tag)
container = webapp.sandbox_name
next_state = image_size = None
......@@ -843,7 +839,7 @@ exec /.toolbox/bin/sshd -D
ctrl.sandbox.wait(container)
# commit
cid = ctrl.sandbox.commit(container, webapp.image_name, version.number)
cid = ctrl.sandbox.commit(container, image, tag)
next_state = VersionState.COMMITTED
image_size = ctrl.sandbox.inspect_image(cid)["Size"]
......@@ -931,7 +927,7 @@ exec /.toolbox/bin/sshd -D
ses = ctrl.session
with ses.begin():
# current state of the sandbox + load docker os
# current state of the sandbox + load docker_os
webapp = ses.query(Webapp).filter_by(id=webapp_id).one()
webapp.docker_os
......@@ -945,7 +941,6 @@ exec /.toolbox/bin/sshd -D
# docker name of the sandbox & image
webapp.sandbox_name = ctrl.gen_sandbox_name(webapp)
webapp.image_name = ctrl.gen_image_name(webapp)
phase = "inspect"
next_state = fail_state = None
......@@ -965,16 +960,19 @@ exec /.toolbox/bin/sshd -D
raise Error("invalid version id %d (belongs to webapp %d)" % (
sandbox_version.id, sandbox_version.webapp_id))
image_tag = ctrl.gen_image_tag(sandbox_version, webapp)
# pull requested image
yield from ctrl.image_manager.pull(sandbox_version.id)
else:
image_tag = (ctrl.gen_factory_name(webapp.docker_os),
webapp.docker_os.version)
# pull image
yield from ctrl.image_manager.sandbox_pull_manager.process((
ctrl.gen_factory_name(webapp.docker_os),
webapp.docker_os.version))
yield from ctrl.image_manager.sandbox_pull_manager.process(image_tag)
# start sandbox
yield from self.run_in_executor(self._start, webapp, sandbox_version)
yield from self.run_in_executor(self._start, webapp, image_tag)
elif webapp.sandbox_state == SandboxState.STOPPING:
# stop the sandbox
......@@ -1056,18 +1054,24 @@ class JobManager(Manager):
job = ses.query(Job).filter_by(id=info.job_id).one()
webapp = job.webapp
log.info("start job %d (%s:%s)",
info.job_id, webapp.docker_name, info.version)
repo = ctrl.gen_image_name(webapp)
image = "%s:%s" % (repo, info.version)
if info.ver_id is None:
image = None
image_desc = ""
else:
version = ses.query(WebappVersion).filter_by(id=info.ver_id).one()
repo, tag = ctrl.gen_image_tag(version)
image = "%s:%s" % (repo, tag)
image_desc = ", image=" + tag
log.info("start job %d (%s:%s%s)", info.job_id,
webapp.docker_name, info.version, image_desc)
job_path = ctrl.gen_job_path(job)
log.debug("job.path: %r", job_path)
if info.ver_id is None:
assert info.version == "sandbox"
image = tmp_img = info.client.commit(ctrl.gen_sandbox_name(webapp), repo, info.version)["Id"]
image = tmp_img = info.client.commit(ctrl.gen_sandbox_name(webapp))["Id"]
# TODO use another workdir
# TODO use another uid
......@@ -1572,18 +1576,8 @@ class PushManager(Manager):
raise Error("unable to push (image not yet committed)")
raise Error("unable to push (invalid state: %s)" % version.state)
# ensure that there is no other version with the same number in the pipeline
# (to avoid a race condition)
others = (ses.query(WebappVersion.id)
.filter_by(webapp_id=version.webapp_id, number=version.number)
.filter(WebappVersion.id != version.id)
.filter(WebappVersion.state.in_((int(VersionState.SANDBOX), int(VersionState.COMMITTED)))))
if others.count():
raise Error("unable to push (there are other pushable versions with the same number: %s)" % (
" ".join(map(str, itertools.chain(*others)))))
image = self.ctrl.gen_image_name(version.webapp)
tag = version.number
image, tag = self.ctrl.gen_image_tag(version)
ses.expunge(version)
log.info("push from the %-8s %s:%s", "sandbox", image, tag)
# FIXME: docker-py folks are morons in 1.9.0 they added the
......@@ -1607,24 +1601,33 @@ class PushManager(Manager):
reset()
with ses.begin():
version = ses.query(WebappVersion).filter_by(id=version_id).one()
prev = ses.query(WebappVersion).filter_by(
webapp_id = version.webapp_id,
number = version.number,
state = int(VersionState.READY)).scalar()
log.debug("prev version id %r", (prev.id if prev else None))
if prev is None:
# this is a new version
version.state = int(VersionState.READY)
else:
# overwrite an existing version
for key in "updated_at", "description", "published":
setattr(prev, key, getattr(version, key))
# mark this version as replaced
version.state = int(VersionState.REPLACED)
ses.add(version)
# Now that the push is completed, we need to switch the state to READY
#
# However since push is performed asynchronously and since we
# may be replacing a version that already exists, when may have
# multiple versions competing for the READY state.
#
# We set the READY state to the version with the highest id
# (the one which was committed/pushed last) and put all other
# versions in the REPLACED state.
# select and lock all candidate versions
versions = ses.query(WebappVersion).filter_by(id=version.id).union(
ses.query(WebappVersion).filter_by(
webapp_id = version.webapp_id,
number = version.number,
state = int(VersionState.READY))
).with_for_update().all()
# set the latest one to READY and the others to REPLACED
latest_id = max(v.id for v in versions)
for v in versions:
new_state = (VersionState.READY if v.id == latest_id else
VersionState.REPLACED)
log.debug("version id %d: %s -> %s", v.id,
VersionState(v.state).name, new_state)
v.state = int(new_state)
class ImageManager:
......@@ -1655,8 +1658,7 @@ class ImageManager:
# get the version object and check its state
version = ses.query(WebappVersion).filter_by(id=version_id).one()
image = self.ctrl.gen_image_name(version.webapp)
tag = version.number
image, tag = self.ctrl.gen_image_tag(version)
if swarm:
......@@ -1676,7 +1678,7 @@ class ImageManager:
else:
# pull to the sandbox
if version.state == VersionState.COMMITTED:
# do not pull!
# nothing to do
return
if version.state not in (VersionState.READY, VersionState.REPLACED):
......@@ -1771,8 +1773,17 @@ class DockerController:
def gen_sandbox_name(self, webapp):
return "%s-sandbox-%s" % (self.env, webapp.docker_name)
def gen_image_name(self, webapp):
return "%s/%s" % (self.registry, webapp.docker_name)
def gen_image_tag(self, webapp_version, webapp=None):
"""Get the docker image for a given WebappVersion
return a tuple (REPO, TAG)
"""
if webapp is None:
webapp = webapp_version.webapp
else:
assert webapp.id == webapp_version.webapp_id
return ("%s/%s" % (self.registry, webapp.docker_name),
"id%d" % webapp_version.id)
def gen_job_name(self, job):
return "%s-job-%s-%d-%s" % (self.env, job.queue.name, job.id, job.webapp.docker_name)
......@@ -1886,8 +1897,6 @@ class DockerController:
ses = self.session
with ses.begin():
if startup:
ses.execute("DELETE FROM dj_webapp_versions WHERE state=%d"
% VersionState.REPLACED)
for version_id, in ses.execute(
"SELECT id FROM dj_webapp_versions WHERE state=%d"
% VersionState.COMMITTED).fetchall():
......
......@@ -356,14 +356,20 @@ class WebappSandboxPanel(LoginRequiredMixin, TemplateView):
"unable to commit sandbox %r because it is not running"
% webapp.name)
else:
# query previous active versions of this webapp
previous = WebappVersion.objects.filter(webapp=webapp,
state__in = (WebappVersion.READY, WebappVersion.COMMITTED))
extra = {}
if request.POST["version-action"] == "replace-version":
number = request.POST["version-select"]
# keep the previous 'created_at' timestamp when replacing an image
extra["created_at"] = getattr(previous.filter(number=number).first(), "created_at")
else:
number = request.POST["version-new"]
# ensure that the version does not already exist
if WebappVersion.objects.filter(webapp=webapp, number=number,
state__in = (WebappVersion.READY, WebappVersion.COMMITTED)
).exists():
# ensure that this version number does not already exist
if previous.filter(number=number).exists():
messages.error(request, "unable to commit because version %r already exists"
" (if you want to overwrite this version, then use"
" 'replace version' instead)" % number)
......@@ -375,7 +381,8 @@ class WebappSandboxPanel(LoginRequiredMixin, TemplateView):
state=WebappVersion.SANDBOX,
published=True,
description=request.POST["description"],
url="http://WTF")
url="http://WTF",
**extra)
webapp.sandbox_state = Webapp.STOPPING;
webapp.save()
......
......@@ -69,7 +69,7 @@ fi
if [ -n "$seed_registry" ]
then
IMAGE="$REGISTRY/sleep:latest"
IMAGE="$REGISTRY/sleep:id1"
echo "build image $IMAGE"
docker build -t "$IMAGE" -- "`dirname "$0"`/sleep"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment