Skip to content

Commit

Permalink
Create app when posting a new container, as part of #751.
Browse files Browse the repository at this point in the history
  • Loading branch information
donkirkby committed Feb 13, 2019
1 parent 73397aa commit 8bc6297
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 20 deletions.
1 change: 1 addition & 0 deletions kive/container/management/commands/purge.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
# error - summary of unregistered files, can't meet purge target, or can't purge
# warning - list each unregistered file
# info - summary of regular purge
# debug - list each purged file, summary even when nothing purged
logger = logging.getLogger(__name__)


Expand Down
34 changes: 29 additions & 5 deletions kive/container/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,16 +262,28 @@ def clean(self):

if self.file_type == Container.ZIP:
try:
with ZipFile(self.file):
pass
was_closed = self.file.closed
self.file.open()
try:
with ZipFile(self.file):
pass
finally:
if was_closed:
self.file.close()
except BadZipfile:
raise ValidationError(self.DEFAULT_ERROR_MESSAGES["invalid_archive"],
code="invalid_archive")

else: # this is either a tarfile or a gzipped tar file
try:
with tarfile.open(fileobj=self.file, mode="r"):
pass
was_closed = self.file.closed
self.file.open()
try:
with tarfile.open(fileobj=self.file, mode="r"):
pass
finally:
if was_closed:
self.file.close()
except tarfile.ReadError:
raise ValidationError(self.DEFAULT_ERROR_MESSAGES["invalid_archive"],
code="invalid_archive")
Expand Down Expand Up @@ -345,6 +357,7 @@ def open_content(self, mode='r'):
file_mode = 'rb+'
else:
raise ValueError('Unsupported mode for archive content: {!r}.'.format(mode))
was_closed = self.file.closed
self.file.open(file_mode)
try:
if self.file_type == Container.ZIP:
Expand All @@ -358,7 +371,8 @@ def open_content(self, mode='r'):
yield archive
archive.close()
finally:
self.file.close()
if was_closed:
self.file.close()

def get_content(self):
with self.open_content() as archive:
Expand Down Expand Up @@ -392,6 +406,16 @@ def write_content(self, content):
if file_name not in file_names:
archive.write(file_name, pipeline_json)
break
self.create_app_from_content(content)

def create_app_from_content(self, content=None):
""" Creat an app based on the content configuration.
:raises ValueError: if this is not an archive container
"""
if content is None:
content = self.get_content()
pipeline = content['pipeline']
if self.pipeline_valid(pipeline):
self.apps.all().delete()
default_config = pipeline.get('default_config',
Expand Down
6 changes: 6 additions & 0 deletions kive/container/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ class Meta:
'content',
'removal_plan')

def create(self, validated_data):
container = super(ContainerSerializer, self).create(validated_data)
if container.file_type != Container.SIMG:
container.create_app_from_content()
return container


class ContainerAppSerializer(serializers.ModelSerializer):
absolute_url = URLField(source='get_absolute_url', read_only=True)
Expand Down
126 changes: 114 additions & 12 deletions kive/container/tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import json
import logging
import os
import re
Expand Down Expand Up @@ -38,6 +39,50 @@
from librarian.models import Dataset, ExternalFileDirectory, get_upload_path


def create_tar_content(container=None, content=None):
""" Create a tar file for an archive container.
:param container: the container to attach this file to (not saved)
:param content: the JSON content to write into the container
:returns: a BytesIO object with the tar file contents
"""
bytes_file = BytesIO()
with TarFile(fileobj=bytes_file, mode="w") as f:
foo = BytesIO(b"The first file.")
foo_info = TarInfo('foo.txt')
foo_info.size = len(foo.getvalue())
f.addfile(foo_info, foo)
bar = BytesIO(b"The second file.")
bar_info = TarInfo('bar.txt')
bar_info.size = len(bar.getvalue())
f.addfile(bar_info, bar)
if content is not None:
pipeline = BytesIO(json.dumps(content['pipeline']).encode('utf8'))
pipeline_info = TarInfo('kive/pipeline1.json')
pipeline_info.size = len(pipeline.getvalue())
f.addfile(pipeline_info, pipeline)
if container is not None:
container.file = ContentFile(bytes_file.getvalue(), "container.tar")
container.file_type = Container.TAR
return bytes_file


def create_valid_tar_content():
return create_tar_content(content=dict(
files=["bar.txt", "foo.txt"],
pipeline=dict(default_config=dict(memory=200,
threads=2),
inputs=[dict(dataset_name='in1')],
steps=[dict(driver='foo.txt',
inputs=[dict(dataset_name="in1",
source_step=0,
source_dataset_name="in1")],
outputs=["out1"])],
outputs=[dict(dataset_name="out1",
source_step=1,
source_dataset_name="out1")])))


@skipIfDBFeature('is_mocked')
class ContainerTests(TestCase):
def create_zip_content(self, container):
Expand All @@ -53,18 +98,7 @@ def add_zip_content(self, container, filename, content):
f.writestr(filename, content)

def create_tar_content(self, container):
bytes_file = BytesIO()
with TarFile(fileobj=bytes_file, mode="w") as f:
foo = BytesIO(b"The first file.")
foo_info = TarInfo('foo.txt')
foo_info.size = foo.tell()
f.addfile(foo_info, foo)
bar = BytesIO(b"The second file.")
bar_info = TarInfo('bar.txt')
bar_info.size = bar.tell()
f.addfile(bar_info, bar)
container.file = ContentFile(bytes_file.getvalue(), "container.tar")
container.file_type = Container.TAR
create_tar_content(container)

def add_tar_content(self, container, filename, content):
with TarFile(container.file_path, 'a') as f:
Expand Down Expand Up @@ -277,6 +311,55 @@ def test_write_content_and_app(self):
self.assertEqual(expected_inputs, app.inputs)
self.assertEqual(expected_outputs, app.outputs)

def test_create_content_and_app(self):
user = User.objects.first()
family = ContainerFamily.objects.create(user=user)
parent = Container.objects.create(family=family, user=user)
tar_bytes = create_valid_tar_content()

client = Client()
client.force_login(user)
expected_inputs = 'in1'
expected_outputs = 'out1'

response = client.post(reverse('container_add',
kwargs=dict(family_id=family.id)),
dict(tag='test',
parent=parent.id,
file=ContentFile(tar_bytes.getvalue(),
"container.tar")))
if response.status_code != 302:
self.assertEqual({}, response.context['form'].errors)
new_container = Container.objects.first()
app, = new_container.apps.all()
self.assertEqual(expected_inputs, app.inputs)
self.assertEqual(expected_outputs, app.outputs)

def test_create_singularity_no_app(self):
user = User.objects.first()
family = ContainerFamily.objects.create(user=user)
image_path = os.path.abspath(os.path.join(__file__,
'..',
'..',
'..',
'samplecode',
'singularity',
'python2-alpine-trimmed.simg'))
expected_app_count = 0

client = Client()
client.force_login(user)

with open(image_path, 'rb') as f:
response = client.post(reverse('container_add',
kwargs=dict(family_id=family.id)),
dict(tag='test',
file=File(f)))
if response.status_code != 302:
self.assertEqual({}, response.context['form'].errors)
new_container = Container.objects.first()
self.assertEqual(expected_app_count, new_container.apps.count())

def test_extract_zip(self):
run = ContainerRun()
run.create_sandbox(prefix='test_extract_zip')
Expand Down Expand Up @@ -460,6 +543,25 @@ def test_put_bad_content(self):
self.assertEqual(expected_content, content)
self.assertEqual(400, response.status_code)

def test_create_tar_with_app(self):
tar_bytes = create_valid_tar_content()
request2 = self.factory.post(self.list_path,
dict(tag='v1.0',
family=self.family_path,
parent=self.detail_path,
description='A really cool container',
file_type=Container.TAR,
file=ContentFile(tar_bytes.getvalue(),
"container.tar")))

force_authenticate(request2, user=self.kive_user)
resp = self.list_view(request2).data

self.assertIn('id', resp)
container_id = resp['id']
container = Container.objects.get(id=container_id)
self.assertEqual(1, container.apps.count())


@skipIfDBFeature('is_mocked')
class ContainerAppTests(TestCase):
Expand Down
6 changes: 3 additions & 3 deletions kive/container/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import json
import os
from io import BytesIO

import errno
from django.conf import settings
Expand All @@ -21,8 +20,7 @@
from container.forms import ContainerFamilyForm, ContainerForm, \
ContainerUpdateForm, ContainerAppForm, ContainerRunForm, BatchForm
from container.models import ContainerFamily, Container, ContainerApp, \
ContainerRun, ContainerArgument, ContainerLog, Batch, ChildNotConfigured
from file_access_utils import compute_md5
ContainerRun, ContainerArgument, ContainerLog, Batch
from portal.views import developer_check, AdminViewMixin

dev_decorators = [login_required, user_passes_test(developer_check)]
Expand Down Expand Up @@ -90,6 +88,8 @@ def form_valid(self, form):
with transaction.atomic():
self.object.grant_from_json(form.cleaned_data["permissions"])
self.object.validate_restrict_access([self.object.family])
if self.object.file_type != Container.SIMG:
self.object.create_app_from_content()
return response

def get_success_url(self):
Expand Down

0 comments on commit 8bc6297

Please sign in to comment.