Skip to content

Commit

Permalink
Merge branch 'develop' into SS-666-inform-notebooks-shut-down
Browse files Browse the repository at this point in the history
  • Loading branch information
sandstromviktor authored Nov 6, 2023
2 parents 5bdd5a2 + 3fc3473 commit 70083ac
Show file tree
Hide file tree
Showing 18 changed files with 351 additions and 48 deletions.
70 changes: 70 additions & 0 deletions api/openapi/common_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from django.http import JsonResponse
from rest_framework import generics, viewsets
from rest_framework.response import Response

from studio.system_version import SystemVersion


class OpenAPI(generics.GenericAPIView):
"""
The Open API basic API information endpoints.
"""

def get(self, request):
print(f"Entered OpenAPI.get. Requested API version {request.version}")
return Response({"hello": "world"})


class APIInfo(viewsets.GenericViewSet):
"""
The Open API basic API information endpoints, in accordance to the standard
https://dev.dataportal.se/rest-api-profil/versionhantering
:returns dict: A dictionary of app-info information.
"""

def get_api_info(self, request):
print(f"Entered OpenAPI.get_api_info. Requested API version {request.version}")

if request.version == "beta":
data = {
"apiName": "SciLifeLab Serve OpenAPI",
"apiVersion": "beta",
"apiReleased": "TODO",
"apiDocumentation": "TODO",
"apiStatus": "beta",
"latest-api-version": "v1",
}
return JsonResponse(data)

elif request.version == "v1":
data = {
"apiName": "SciLifeLab Serve OpenAPI",
"apiVersion": "1.0.0",
"apiReleased": "TODO",
"apiDocumentation": "TODO",
"apiStatus": "beta",
"latest-api-version": "v1",
}
return JsonResponse(data)


def are_you_there(request):
"""
Most simple API endpoint useful for testing
and verifications.
:returns bool: true
"""
return JsonResponse({"status": True})


def get_system_version(request):
"""
Gets the version of the deployed application.
:returns dict: A dictionary of system version information.
"""
data = {
"system-version": SystemVersion().get_gitref(),
"build-date": SystemVersion().get_build_date(),
"image-tag": SystemVersion().get_imagetag(),
}
return JsonResponse(data)
56 changes: 56 additions & 0 deletions api/openapi/public_apps_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from django.db.models import Q
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from rest_framework import viewsets
from rest_framework.exceptions import NotFound

from apps.models import AppInstance, Apps


class PublicAppsAPI(viewsets.ReadOnlyModelViewSet):
"""
The Public Apps API with read-only methods to get public apps information.
"""

def list(self, request):
"""
This endpoint gets a list of public apps.
:returns list: A list of app information.
"""
print("PublicAppsAPI. Entered list method.")
print(f"Requested API version {request.version}")

queryset = (
AppInstance.objects.filter(~Q(state="Deleted"), access="public")
.order_by("-updated_on")[:8]
.values("id", "name", "app_id", "table_field", "description", "updated_on")
)

list_apps = list(queryset)
for app in list_apps:
add_data = Apps.objects.get(id=app["app_id"])
app["app_type"] = add_data.name
data = {"data": list_apps}
print("LIST: ", data)
return JsonResponse(data)

def retrieve(self, request, pk=None):
"""
This endpoint retrieves a single public app instance.
:returns dict: A dict of app information.
"""
print(f"PublicAppsAPI. Entered retrieve method with pk = {pk}")
print(f"Requested API version {self.request.version}")
queryset = AppInstance.objects.all().values(
"id", "name", "app_id", "table_field", "description", "updated_on", "access", "state"
)
app = get_object_or_404(queryset, pk=pk)
if app["state"] == "Deleted":
raise NotFound("this app has been deleted")
if app["access"] != "public":
raise NotFound()

add_data = Apps.objects.get(id=app["app_id"])
app["app_type"] = add_data.name
data = {"app": app}
return JsonResponse(data)
24 changes: 24 additions & 0 deletions api/openapi/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import rest_framework.routers as drfrouters
from django.conf.urls import include
from django.urls import path
from rest_framework_nested import routers

from .common_api import APIInfo, are_you_there, get_system_version
from .public_apps_api import PublicAppsAPI

app_name = "openapi"

router_drf = drfrouters.DefaultRouter()
router = routers.SimpleRouter(trailing_slash=False)

urlpatterns = [
path("", include(router_drf.urls)),
path("", include(router.urls)),
# Generic API endpoints
path("are-you-there", are_you_there),
path("system-version", get_system_version),
path("api-info", APIInfo.as_view({"get": "get_api_info"})),
# The Apps API
path("public-apps", PublicAppsAPI.as_view({"get": "list"})),
path("public-apps/<int:pk>", PublicAppsAPI.as_view({"get": "retrieve"})),
]
47 changes: 47 additions & 0 deletions api/tests/test_openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import json
import os

from rest_framework import status
from rest_framework.test import APITestCase


class OpenApiTests(APITestCase):
"""Tests for the Open API generic base endpoints"""

BASE_API_URL = "/openapi/v1/"

def test_get_are_you_there(self):
"""Tests the API endpoint /are-you-there"""
url = os.path.join(self.BASE_API_URL, "are-you-there")
response = self.client.get(url, format="json")

self.assertEqual(response.status_code, status.HTTP_200_OK)
actual = json.loads(response.content)
self.assertEqual(actual, {"status": True})

def test_get_system_version(self):
"""Tests the API endpoint /system-version"""
url = os.path.join(self.BASE_API_URL, "system-version")
response = self.client.get(url, format="json")

self.assertEqual(response.status_code, status.HTTP_200_OK)
print(response.content)
actual = json.loads(response.content)
assert actual["system-version"] == ""
assert actual["build-date"] == ""
assert actual["image-tag"] == ""

def test_app_info(self):
"""Tests the API endpoint /api-info"""
url = os.path.join(self.BASE_API_URL, "api-info")
response = self.client.get(url, format="json")

self.assertEqual(response.status_code, status.HTTP_200_OK)
print(response.content)
actual = json.loads(response.content)
self.assertEqual(actual["apiName"], "SciLifeLab Serve OpenAPI")
self.assertEqual(actual["apiVersion"], "1.0.0")
self.assertEqual(actual["apiReleased"], "TODO")
self.assertEqual(actual["apiDocumentation"], "TODO")
self.assertEqual(actual["apiStatus"], "beta")
self.assertEqual(actual["latest-api-version"], "v1")
85 changes: 85 additions & 0 deletions api/tests/test_openapi_public_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import json
import os
from datetime import date, datetime

from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.test import APITestCase

from apps.models import AppInstance, Apps
from projects.models import Project

User = get_user_model()

test_user = {"username": "[email protected]", "email": "[email protected]", "password": "bar"}


class PublicAppsApiTests(APITestCase):
"""Tests for the public apps API resource endpoints"""

BASE_API_URL = "/openapi/v1/"

@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(test_user["username"], test_user["email"], test_user["password"])
cls.project = Project.objects.create_project(name="test-perm", owner=cls.user, description="", repository="")
cls.app = Apps.objects.create(name="Some App", slug="some-app")

cls.app_instance = AppInstance.objects.create(
access="public",
owner=cls.user,
name="test_app_instance_public",
description="My app description",
app=cls.app,
project=cls.project,
parameters={
"environment": {"pk": ""},
},
)

def test_public_apps_list(self):
"""Tests the API resource public-apps default endpoint (action list)"""
url = os.path.join(self.BASE_API_URL, "public-apps")
response = self.client.get(url, format="json")

self.assertEqual(response.status_code, status.HTTP_200_OK)
print(response.content)
actual = json.loads(response.content)["data"]
self.assertEqual(len(actual), 1)

app = actual[0]
self.assertEqual(app["id"], self.app_instance.id)
self.assertIsNotNone(app["name"])
self.assertEqual(app["name"], self.app_instance.name)
self.assertTrue(app["app_id"] > 0)
self.assertEqual(app["description"], self.app_instance.description)
updated_on = datetime.fromisoformat(app["updated_on"][:-1])
self.assertEqual(datetime.date(updated_on), datetime.date(self.app_instance.updated_on))
self.assertEqual(app["app_type"], self.app_instance.app.name)

def test_public_apps_single_app(self):
"""Tests the API resource public-apps get single object"""
id = str(self.app_instance.id)
url = os.path.join(self.BASE_API_URL, "public-apps/", id)
response = self.client.get(url, format="json")

self.assertEqual(response.status_code, status.HTTP_200_OK)
print(response.content)
actual = json.loads(response.content)["app"]
print(type(actual))

self.assertEqual(actual["id"], self.app_instance.id)
self.assertIsNotNone(actual["name"])
self.assertEqual(actual["name"], self.app_instance.name)
self.assertTrue(actual["app_id"] > 0)
self.assertEqual(actual["description"], self.app_instance.description)
updated_on = datetime.fromisoformat(actual["updated_on"][:-1])
self.assertEqual(datetime.date(updated_on), datetime.date(self.app_instance.updated_on))
self.assertEqual(actual["app_type"], self.app_instance.app.name)

def test_public_apps_single_app_notfound(self):
"""Tests the API resource public-apps get single object for a non-existing id"""
id = "-1"
url = os.path.join(self.BASE_API_URL, "public-apps/", id)
response = self.client.get(url, format="json")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
4 changes: 2 additions & 2 deletions apps/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ def serialize_flavor(form_selection, project):
"cpu": flavor.cpu_lim,
"memory": flavor.mem_lim,
"ephemeral-storage": flavor.ephmem_lim,
}
},
}
if flavor.gpu_req and int(flavor.gpu_req) > 0:
flavor_json["flavor"]["requests"]["nvidia.com/gpu"] = flavor.gpu_req,
flavor_json["flavor"]["requests"]["nvidia.com/gpu"] = (flavor.gpu_req,)
flavor_json["flavor"]["limits"]["nvidia.com/gpu"] = flavor.gpu_lim

return flavor_json
Expand Down
10 changes: 7 additions & 3 deletions apps/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

from . import controller
from .models import AppInstance, Apps, AppStatus, ResourceData

ReleaseName = apps.get_model(app_label=settings.RELEASENAME_MODEL)


def get_URI(parameters):
URI = "https://" + parameters["release"] + "." + parameters["global"]["domain"]

Expand Down Expand Up @@ -183,14 +185,17 @@ def delete_and_deploy_resource(instance_pk, new_release_name):
appinstance.save()
deploy_resource(instance_pk)
try:
rel_name_obj = ReleaseName.objects.get(name=new_release_name, project=appinstance.project, status="active")
rel_name_obj = ReleaseName.objects.get(
name=new_release_name, project=appinstance.project, status="active"
)
rel_name_obj.status = "in-use"
rel_name_obj.app = appinstance
rel_name_obj.save()
except Exception as e:
print("Error: Submitted release name not owned by project.")
print(e)


@shared_task
@transaction.atomic
def deploy_resource(instance_pk, action="create"):
Expand Down Expand Up @@ -392,8 +397,7 @@ def check_status():
# Find the app instance release name
app_release = instance.parameters["release"] # e.g 'rfc058c6f'
# Now check if there exists a pod with that release
cmd = f"kubectl -n {settings.NAMESPACE} get po -l release=\"{app_release}\""

cmd = f'kubectl -n {settings.NAMESPACE} get po -l release="{app_release}"'

try:
# returns a byte-like object
Expand Down
8 changes: 4 additions & 4 deletions apps/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import re

import requests
from django.apps import apps
from django.conf import settings
Expand Down Expand Up @@ -295,9 +294,10 @@ def create_releasename(request, user, project, app_slug):
def add_tag(request, user, project, ai_id):
appinstance = AppInstance.objects.get(pk=ai_id)
if request.method == "POST":
new_tag = request.POST.get("tag", "")
print("New Tag: ", new_tag)
appinstance.tags.add(new_tag)
new_tags = request.POST.get("tag", "")
for new_tag in new_tags.split(","):
print("New Tag: ", new_tag)
appinstance.tags.add(new_tag.strip().lower().replace("\"", ""))
appinstance.save()

return HttpResponseRedirect(
Expand Down
1 change: 0 additions & 1 deletion common/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ def input_form(
@given(form=input_form())
@settings(verbosity=Verbosity.verbose, max_examples=1, deadline=None)
def test_pass_validation(form):

UserProfile.objects.all().delete()
User.objects.all().delete()
is_val = form.is_valid()
Expand Down
4 changes: 2 additions & 2 deletions models/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,11 @@ def post(self, request, user, project):
# The minio sidecar does this.
# First find the minio release name
minio_set = Apps.objects.get(slug="minio")
minio = AppInstance.objects.filter(Q(app=minio_set),Q(project=model_project), Q(state="Running")).first()
minio = AppInstance.objects.filter(Q(app=minio_set), Q(project=model_project), Q(state="Running")).first()

minio_release = minio.parameters["release"] # e.g 'rfc058c6f'
# Now find the related pod
cmd = f"kubectl get po -n {settings.NAMESPACE} -l release=\"{minio_release}\" -o jsonpath=\"{{.items[0].metadata.name}}\""
cmd = f'kubectl get po -n {settings.NAMESPACE} -l release="{minio_release}" -o jsonpath="{{.items[0].metadata.name}}"'
try:
result = subprocess.check_output(cmd, shell=True)
# because the above subprocess run returns a byte-like object
Expand Down
8 changes: 5 additions & 3 deletions scripts/admin_token.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from django.contrib.auth import get_user_model
from rest_framework.authtoken.models import Token

import os
User = get_user_model()


def run(*args):
admin = User.objects.get(username="admin")
if not User.objects.filter(email='[email protected]').exists():
User.objects.create_superuser('admin', '[email protected]', os.getenv("DJANGO_SUPERUSER_PASSWORD"))

admin = User.objects.get(username="[email protected]")
try:
_ = Token.objects.get(user=admin)
except Token.DoesNotExist:
Expand Down
Loading

0 comments on commit 70083ac

Please sign in to comment.