Skip to content

Commit d79f772

Browse files
alfredeenj-awadaanondo1969churnikovhamzaimran08
authored
Refactor App Status (#269)
Signed-off-by: Nikita Churikov <[email protected]> Co-authored-by: Jana Awada <[email protected]> Co-authored-by: Mahbub Ul Alam <[email protected]> Co-authored-by: Nikita Churikov <[email protected]> Co-authored-by: Hamza <[email protected]>
1 parent e667b87 commit d79f772

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+2695
-356
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ on:
3232
jobs:
3333
CI:
3434
if: github.repository == 'scilifelabdatacentre/serve'
35-
runs-on: ubuntu-20.04
35+
runs-on: ubuntu-22.04
3636
env:
3737
working-directory: .
3838

.github/workflows/e2e-tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ on:
2727
jobs:
2828
e2e:
2929
if: github.repository == 'scilifelabdatacentre/serve'
30-
runs-on: ubuntu-20.04
30+
runs-on: ubuntu-22.04
3131
env:
3232
working-directory: .
3333

.github/workflows/pre-commit.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ on:
1212

1313
jobs:
1414
pre_commit:
15-
runs-on: ubuntu-20.04
15+
runs-on: ubuntu-22.04
1616
steps:
1717
- name: Checkout
1818
uses: actions/checkout@v4

.github/workflows/publish.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
||
3535
(github.event_name == 'workflow_dispatch')
3636
)
37-
runs-on: ubuntu-20.04
37+
runs-on: ubuntu-22.04
3838
strategy:
3939
fail-fast: false
4040
matrix:
@@ -100,7 +100,7 @@ jobs:
100100
file: ${{ matrix.dockerfile }}
101101

102102
on-failure:
103-
runs-on: ubuntu-latest
103+
runs-on: ubuntu-22.04
104104
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
105105
steps:
106106
- run: echo 'Not publishing. Triggering workflow failed' && exit 1

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.12.3-alpine3.19 as builder
1+
FROM python:3.12.3-alpine3.19 AS builder
22

33
LABEL maintainer="[email protected]"
44
WORKDIR /app

api/openapi/public_apps_api.py

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from rest_framework.exceptions import NotFound
77

88
from apps.app_registry import APP_REGISTRY
9-
from apps.models import Apps, AppStatus
9+
from apps.models import Apps, BaseAppInstance
1010
from studio.utils import get_logger
1111

1212
logger = get_logger(__name__)
@@ -30,20 +30,38 @@ def list_apps(self, request):
3030
# TODO: MAKE SURE THAT THIS IS FILTERED BASED ON ACCESS
3131
for model_class in APP_REGISTRY.iter_orm_models():
3232
# Loop over all models, and check if they have the access and description field
33+
# Note: It is not possible to use BaseAppInstance.objects.get_app_instances_not_deleted here
34+
# This is the reason for looping over model_class instead
3335
if hasattr(model_class, "description") and hasattr(model_class, "access"):
34-
queryset = model_class.objects.filter(~Q(app_status__status="Deleted"), access="public").values(
35-
"id", "name", "app_id", "url", "description", "created_on", "updated_on", "app_status"
36+
queryset = model_class.objects.filter(access="public").values(
37+
"id",
38+
"name",
39+
"app_id",
40+
"url",
41+
"description",
42+
"created_on",
43+
"updated_on",
44+
"latest_user_action",
45+
"k8s_user_app_status",
3646
)
37-
# using a dictionary to avoid duplicates for shiny apps
47+
# Using a dictionary to avoid duplicates for shiny apps
3848
for item in queryset:
39-
list_apps_dict[item["id"]] = item
49+
# Do not include deleted apps
50+
app_status = BaseAppInstance.convert_to_app_status(
51+
item.get("latest_user_action"),
52+
item.get("k8s_user_app_status"),
53+
)
54+
if app_status != "Deleted":
55+
item["app_status"] = app_status
56+
list_apps_dict[item["id"]] = item
4057

4158
# Order the combined list by "created_on"
4259
list_apps = sorted(list_apps_dict.values(), key=lambda x: x["created_on"], reverse=True)
4360

4461
for app in list_apps:
62+
assert app["app_status"] != "Deleted"
63+
4564
app["app_type"] = Apps.objects.get(id=app["app_id"]).name
46-
app["app_status"] = AppStatus.objects.get(pk=app["app_status"]).status
4765

4866
# Add the previous url key located at app.table_field.url to support clients using the previous schema
4967
app["table_field"] = {"url": app["url"]}
@@ -71,8 +89,18 @@ def retrieve(self, request, app_slug=None, pk=None):
7189

7290
try:
7391
queryset = model_class.objects.all().values(
74-
"id", "name", "app_id", "url", "description", "created_on", "updated_on", "access", "app_status"
92+
"id",
93+
"name",
94+
"app_id",
95+
"url",
96+
"description",
97+
"created_on",
98+
"updated_on",
99+
"access",
100+
"latest_user_action",
101+
"k8s_user_app_status",
75102
)
103+
76104
logger.info("Queryset: %s", queryset)
77105
except FieldError as e:
78106
message = f"Error in field: {e}"
@@ -84,24 +112,26 @@ def retrieve(self, request, app_slug=None, pk=None):
84112
logger.error("App instance is not available")
85113
raise NotFound("App instance is not available")
86114

87-
app_status_pk = app_instance.get("app_status", None)
88-
logger.info("DID WE GET HERE?!")
89-
if app_status_pk is None:
90-
raise NotFound("App status is not available")
115+
app_status = BaseAppInstance.convert_to_app_status(
116+
app_instance.get("latest_user_action"),
117+
app_instance.get("k8s_user_app_status"),
118+
)
91119

92-
app_status = AppStatus.objects.get(pk=app_status_pk)
93-
if app_status.status == "Deleted":
94-
logger.error("This app has been deleted")
120+
if app_status == "Deleted":
121+
logger.info("This app has been deleted")
95122
raise NotFound("This app has been deleted")
96123

124+
app_instance["app_status"] = app_status
125+
97126
if app_instance.get("access", False) != "public":
98-
logger.error("This app is non-existent or not public")
99127
raise NotFound("This app is non-existent or not public")
100128

101-
app_instance["app_status"] = app_status.status
129+
app_type_info = Apps.objects.get(id=app_instance["app_id"])
130+
app_instance["app_type"] = app_type_info.name
131+
132+
# Remove misleading app_id from the final output because it only refers to the app type
133+
del app_instance["app_id"]
102134

103-
add_data = Apps.objects.get(id=app_instance["app_id"])
104-
app_instance["app_type"] = add_data.name
105135
data = {"app": app_instance}
106136
logger.info("API call successful")
107137
return JsonResponse(data)

api/tests/test_openapi_public_apps.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from rest_framework import status
77
from rest_framework.test import APITestCase
88

9-
from apps.models import Apps, AppStatus, CustomAppInstance, Subdomain
9+
from apps.models import Apps, CustomAppInstance, K8sUserAppStatus, Subdomain
1010
from projects.models import Project
1111
from studio.utils import get_logger
1212

@@ -25,12 +25,13 @@ class PublicAppsApiTests(APITestCase):
2525

2626
@classmethod
2727
def setUpTestData(cls):
28+
# Create a public app with status Running
2829
cls.user = User.objects.create_user(test_user["username"], test_user["email"], test_user["password"])
2930
cls.project = Project.objects.create_project(name="test-perm", owner=cls.user, description="")
3031
cls.app = Apps.objects.create(name="Some App", slug="customapp")
3132

3233
subdomain = Subdomain.objects.create(subdomain="test_internal")
33-
app_status = AppStatus.objects.create(status="Running")
34+
k8s_user_app_status = K8sUserAppStatus.objects.create(status="Running")
3435
cls.app_instance = CustomAppInstance.objects.create(
3536
access="public",
3637
owner=cls.user,
@@ -42,7 +43,7 @@ def setUpTestData(cls):
4243
"environment": {"pk": ""},
4344
},
4445
subdomain=subdomain,
45-
app_status=app_status,
46+
k8s_user_app_status=k8s_user_app_status,
4647
)
4748

4849
def test_public_apps_list(self):
@@ -80,7 +81,6 @@ def test_public_apps_single_app(self):
8081
self.assertEqual(actual["id"], self.app_instance.id)
8182
self.assertIsNotNone(actual["name"])
8283
self.assertEqual(actual["name"], self.app_instance.name)
83-
self.assertTrue(actual["app_id"] > 0)
8484
self.assertEqual(actual["description"], self.app_instance.description)
8585
updated_on = datetime.fromisoformat(actual["updated_on"][:-1])
8686
self.assertEqual(datetime.date(updated_on), datetime.date(self.app_instance.updated_on))

api/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
# Internal API endpoints
5555
path("token-auth/", CustomAuthToken.as_view(), name="api_token_auth"),
5656
path("settings/", get_studio_settings),
57+
# TODO: Consider renaming this endpoint to update-k8s-app-status
5758
path("app-status/", update_app_status),
5859
path("app-subdomain/validate/", get_subdomain_is_valid),
5960
path("app-subdomain/is-available/", get_subdomain_is_available),

api/views.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,9 @@ def create(self, request, *args, **kwargs):
519519
return HttpResponse("App created.", status=200)
520520

521521
def destroy(self, request, *args, **kwargs):
522+
# TODO: Revisit
523+
raise RuntimeError("api/AppInstanceList destroy. Is this function used?")
524+
522525
appinstance = self.get_object()
523526
# Check that user is allowed to delete app:
524527
# Either user owns the app, or is a member of the project
@@ -892,9 +895,9 @@ def update_app_status(request: HttpRequest) -> HttpResponse:
892895

893896
new_status = request.data["new-status"]
894897

895-
if len(new_status) > 15:
896-
logger.debug(f"Status code is longer than 15 chars so shortening: {new_status}")
897-
new_status = new_status[:15]
898+
if len(new_status) > 20:
899+
logger.debug(f"Status code is longer than 20 chars so shortening: {new_status}")
900+
new_status = new_status[:20]
898901

899902
event_ts = datetime.strptime(request.data["event-ts"], "%Y-%m-%dT%H:%M:%S.%fZ")
900903
event_ts = utc.localize(event_ts)
@@ -947,8 +950,8 @@ def update_app_status(request: HttpRequest) -> HttpResponse:
947950
return Response(f"Unknown return code from handle_update_status_request() = {result}", 500)
948951

949952
except ObjectDoesNotExist:
950-
# This is often not a problem. It typically happens during app re-deployemnts.
951-
logger.warning(f"The specified app instance was not found release={release}")
953+
# This is often not a problem. It typically happens during app re-deployments.
954+
logger.info(f"The specified app instance was not found release={release}")
952955
return Response(f"The specified app instance was not found {release=}", 404)
953956

954957
except Exception as err:

apps/admin.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
FilemanagerInstance,
1717
GradioInstance,
1818
JupyterInstance,
19+
K8sUserAppStatus,
1920
MLFlowInstance,
2021
NetpolicyInstance,
2122
RStudioInstance,
@@ -54,18 +55,34 @@ class AppsAdmin(admin.ModelAdmin):
5455
admin.site.register(Apps, AppsAdmin)
5556

5657

58+
class K8sUserAppStatusAdmin(admin.ModelAdmin):
59+
list_display = (
60+
"status",
61+
"time",
62+
)
63+
list_filter = ["status", "time"]
64+
65+
5766
class BaseAppAdmin(admin.ModelAdmin):
58-
list_display = ("name", "display_owner", "display_project", "display_status", "display_subdomain", "chart")
67+
list_display = (
68+
"name",
69+
"display_owner",
70+
"display_project",
71+
"display_status",
72+
"display_subdomain",
73+
"chart",
74+
"upload_size",
75+
)
5976
readonly_fields = ("id", "created_on")
60-
list_filter = ["owner", "project", "app_status__status", "chart"]
77+
list_filter = ["owner", "project", "k8s_user_app_status__status", "chart"]
6178
actions = ["redeploy_apps", "deploy_resources", "delete_resources"]
6279

6380
def display_status(self, obj):
64-
status_object = obj.app_status
65-
if status_object:
66-
return status_object.status
67-
else:
68-
"No status"
81+
try:
82+
return obj.get_app_status()
83+
except Exception as err:
84+
logger.warn("Error getting app status: %s", err)
85+
return "No status"
6986

7087
display_status.short_description = "Status"
7188

@@ -263,3 +280,4 @@ class StreamlitInstanceAdmin(BaseAppAdmin):
263280
admin.site.register(Subdomain)
264281
admin.site.register(AppCategories)
265282
admin.site.register(AppStatus, AppStatusAdmin)
283+
admin.site.register(K8sUserAppStatus, K8sUserAppStatusAdmin)

0 commit comments

Comments
 (0)