301 lines
11 KiB
Python
301 lines
11 KiB
Python
# hobo - portal to configure and deploy applications
|
|
# Copyright (C) 2015-2022 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import io
|
|
import json
|
|
import tarfile
|
|
import urllib.parse
|
|
|
|
from django.conf import settings
|
|
from django.core.files.base import ContentFile
|
|
from django.http import HttpResponse, HttpResponseRedirect
|
|
from django.urls import reverse, reverse_lazy
|
|
from django.views.generic import FormView, ListView, TemplateView
|
|
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
|
|
|
from hobo.environment.utils import get_installed_services
|
|
|
|
from .forms import InstallForm
|
|
from .models import Application, Element, Relation, Version
|
|
from .utils import Requests
|
|
|
|
requests = Requests()
|
|
|
|
|
|
class HomeView(ListView):
|
|
template_name = 'hobo/applications/home.html'
|
|
model = Application
|
|
|
|
def get_queryset(self):
|
|
return super().get_queryset().order_by('name')
|
|
|
|
|
|
home = HomeView.as_view()
|
|
|
|
|
|
class InitView(CreateView):
|
|
template_name = 'hobo/applications/create.html'
|
|
model = Application
|
|
fields = ['name']
|
|
|
|
def get_success_url(self):
|
|
return reverse('application-manifest', kwargs={'app_slug': self.object.slug})
|
|
|
|
|
|
init = InitView.as_view()
|
|
|
|
|
|
def get_object_types():
|
|
object_types = []
|
|
for service_id, services in getattr(settings, 'KNOWN_SERVICES', {}).items():
|
|
if service_id not in Application.SUPPORTED_MODULES:
|
|
continue
|
|
service_objects = {x.get_base_url_path(): x for x in get_installed_services(types=[service_id])}
|
|
for service in services.values():
|
|
if service_objects[service['url']].secondary:
|
|
continue
|
|
url = urllib.parse.urljoin(service['url'], 'api/export-import/')
|
|
response = requests.get(url)
|
|
if not response.ok:
|
|
continue
|
|
for object_type in response.json()['data']:
|
|
object_type['service'] = service
|
|
object_types.append(object_type)
|
|
return object_types
|
|
|
|
|
|
class ManifestView(TemplateView):
|
|
template_name = 'hobo/applications/manifest.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
context['app'] = Application.objects.get(slug=self.kwargs['app_slug'])
|
|
|
|
context['relations'] = context['app'].relation_set.all().select_related('element')
|
|
context['versions'] = context['app'].version_set.exists()
|
|
context['types_by_service'] = {}
|
|
|
|
type_labels = {}
|
|
for object_type in get_object_types():
|
|
type_labels[object_type['id']] = object_type['singular']
|
|
if object_type.get('minor'):
|
|
continue
|
|
service = object_type['service']['title']
|
|
if service not in context['types_by_service']:
|
|
context['types_by_service'][service] = []
|
|
context['types_by_service'][service].append(object_type)
|
|
|
|
for relation in context['relations']:
|
|
relation.element.type_label = type_labels.get(relation.element.type)
|
|
|
|
return context
|
|
|
|
|
|
manifest = ManifestView.as_view()
|
|
|
|
|
|
class MetadataView(UpdateView):
|
|
template_name = 'hobo/applications/edit-metadata.html'
|
|
model = Application
|
|
slug_url_kwarg = 'app_slug'
|
|
fields = ['name', 'slug', 'description']
|
|
|
|
def get_success_url(self):
|
|
return reverse('application-manifest', kwargs={'app_slug': self.object.slug})
|
|
|
|
|
|
metadata = MetadataView.as_view()
|
|
|
|
|
|
class AppAddElementView(TemplateView):
|
|
template_name = 'hobo/applications/add-element.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['app'] = Application.objects.get(slug=self.kwargs['app_slug'])
|
|
for object_type in get_object_types():
|
|
if object_type.get('id') == self.kwargs['type']:
|
|
context['type'] = object_type
|
|
url = object_type['urls']['list']
|
|
response = requests.get(url)
|
|
context['elements'] = response.json()['data']
|
|
break
|
|
return context
|
|
|
|
def post(self, request, app_slug, type):
|
|
context = self.get_context_data()
|
|
app = context['app']
|
|
element_infos = {x['id']: x for x in context['elements']}
|
|
for element_slug in request.POST.getlist('elements'):
|
|
element, created = Element.objects.get_or_create(
|
|
type=type, slug=element_slug, defaults={'name': element_infos[element_slug]['text']}
|
|
)
|
|
element.name = element_infos[element_slug]['text']
|
|
element.cache = element_infos[element_slug]
|
|
element.save()
|
|
relation, created = Relation.objects.get_or_create(application=app, element=element)
|
|
relation.auto_dependency = False
|
|
relation.save()
|
|
return HttpResponseRedirect(reverse('application-manifest', kwargs={'app_slug': app_slug}))
|
|
|
|
|
|
add_element = AppAddElementView.as_view()
|
|
|
|
|
|
class AppDeleteElementView(DeleteView):
|
|
model = Relation
|
|
template_name = 'hobo/applications/element_confirm_delete.html'
|
|
|
|
def get_success_url(self):
|
|
return reverse('application-manifest', kwargs={'app_slug': self.kwargs['app_slug']})
|
|
|
|
|
|
delete_element = AppDeleteElementView.as_view()
|
|
|
|
|
|
def scandeps(request, app_slug):
|
|
app = Application.objects.get(slug=app_slug)
|
|
app.relation_set.filter(auto_dependency=True).delete()
|
|
relations = app.relation_set.select_related('element')
|
|
elements = {(x.element.type, x.element.slug): x.element for x in relations}
|
|
finished = False
|
|
while not finished:
|
|
finished = True
|
|
for (type, slug), element in list(elements.items()):
|
|
dependencies_url = element.cache['urls'].get('dependencies')
|
|
element.done = True
|
|
if not dependencies_url:
|
|
continue
|
|
response = requests.get(dependencies_url)
|
|
for dependency in response.json()['data']:
|
|
if (dependency['type'], dependency['id']) in elements:
|
|
continue
|
|
finished = False
|
|
element, created = Element.objects.get_or_create(
|
|
type=dependency['type'], slug=dependency['id'], defaults={'name': dependency['text']}
|
|
)
|
|
element.name = dependency['text']
|
|
element.cache = dependency
|
|
element.save()
|
|
relation, created = Relation.objects.get_or_create(application=app, element=element)
|
|
if created:
|
|
relation.auto_dependency = True
|
|
relation.save()
|
|
elements[(element.type, element.slug)] = element
|
|
|
|
return HttpResponseRedirect(reverse('application-manifest', kwargs={'app_slug': app_slug}))
|
|
|
|
|
|
def generate(request, app_slug):
|
|
app = Application.objects.get(slug=app_slug)
|
|
|
|
version = Version(application=app)
|
|
version.save()
|
|
|
|
tar_io = io.BytesIO()
|
|
with tarfile.open(mode='w', fileobj=tar_io) as tar:
|
|
manifest_json = {
|
|
'application': app.name,
|
|
'slug': app.slug,
|
|
'description': app.description,
|
|
'elements': [],
|
|
}
|
|
|
|
for relation in app.relation_set.all().select_related('element'):
|
|
element = relation.element
|
|
manifest_json['elements'].append(
|
|
{
|
|
'type': element.type,
|
|
'slug': element.slug,
|
|
'name': element.name,
|
|
'auto-dependency': relation.auto_dependency,
|
|
}
|
|
)
|
|
|
|
response = requests.get(element.cache['urls']['export'])
|
|
tarinfo = tarfile.TarInfo('%s/%s' % (element.type, element.slug))
|
|
tarinfo.mtime = version.last_update_timestamp.timestamp()
|
|
tarinfo.size = int(response.headers['content-length'])
|
|
tar.addfile(tarinfo, fileobj=io.BytesIO(response.content))
|
|
|
|
manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
|
|
tarinfo = tarfile.TarInfo('manifest.json')
|
|
tarinfo.size = len(manifest_fd.getvalue())
|
|
tarinfo.mtime = version.last_update_timestamp.timestamp()
|
|
tar.addfile(tarinfo, fileobj=manifest_fd)
|
|
|
|
version.bundle.save('%s.tar' % app_slug, content=ContentFile(tar_io.getvalue()))
|
|
version.save()
|
|
|
|
return HttpResponseRedirect(reverse('application-manifest', kwargs={'app_slug': app_slug}))
|
|
|
|
|
|
def download(request, app_slug):
|
|
app = Application.objects.get(slug=app_slug)
|
|
version = app.version_set.order_by('-last_update_timestamp')[0]
|
|
response = HttpResponse(version.bundle, content_type='application/x-tar')
|
|
response['Content-Disposition'] = 'attachment; filename="%s"' % '%s.tar' % app_slug
|
|
return response
|
|
|
|
|
|
class Install(FormView):
|
|
form_class = InstallForm
|
|
template_name = 'hobo/applications/install.html'
|
|
success_url = reverse_lazy('applications-home')
|
|
|
|
def form_valid(self, form):
|
|
tar_io = io.BytesIO(self.request.FILES['bundle'].read())
|
|
tar = tarfile.open(fileobj=tar_io)
|
|
manifest = json.loads(tar.extractfile('manifest.json').read().decode())
|
|
app, created = Application.objects.get_or_create(
|
|
slug=manifest.get('slug'), defaults={'name': manifest.get('application')}
|
|
)
|
|
app.name = manifest.get('application')
|
|
app.description = manifest.get('description')
|
|
if created:
|
|
# mark as non-editable only newly deployed applications, this allows
|
|
# overwriting a local application and keep on developing it.
|
|
app.editable = False
|
|
else:
|
|
app.relation_set.all().delete()
|
|
app.save()
|
|
|
|
for element_dict in manifest.get('elements'):
|
|
element, created = Element.objects.get_or_create(
|
|
type=element_dict['type'], slug=element_dict['slug'], defaults={'name': element_dict['name']}
|
|
)
|
|
element.name = element_dict['name']
|
|
element.save()
|
|
|
|
relation = Relation(
|
|
application=app,
|
|
element=element,
|
|
auto_dependency=element_dict['auto-dependency'],
|
|
)
|
|
relation.save()
|
|
|
|
version = Version(application=app)
|
|
version.bundle.save('%s.tar' % app.slug, content=ContentFile(tar_io.getvalue()))
|
|
version.save()
|
|
|
|
version.deploy()
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
install = Install.as_view()
|