Skip to content

Commit

Permalink
SS-1288 Add mlflow support (#274)
Browse files Browse the repository at this point in the history
Signed-off-by: Nikita Churikov <nikita@chur.ru>
  • Loading branch information
churnikov authored Feb 21, 2025
1 parent 8b66da4 commit 5338585
Show file tree
Hide file tree
Showing 17 changed files with 1,181 additions and 990 deletions.
8 changes: 8 additions & 0 deletions apps/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
FilemanagerInstance,
GradioInstance,
JupyterInstance,
MLFlowInstance,
NetpolicyInstance,
RStudioInstance,
ShinyInstance,
Expand Down Expand Up @@ -44,6 +45,7 @@ class AppsAdmin(admin.ModelAdmin):
"user_can_create",
"user_can_edit",
"user_can_delete",
"user_can_see_secrets",
"slug",
)
list_filter = ("user_can_create",)
Expand Down Expand Up @@ -200,6 +202,12 @@ class DashInstanceAdmin(BaseAppAdmin):
list_display = BaseAppAdmin.list_display + ("image",)


@admin.register(MLFlowInstance)
class MLFlowAppInstanceAdmin(BaseAppAdmin):
# list any fields that you want be listed in the admin pannel.
list_display = BaseAppAdmin.list_display


@admin.register(CustomAppInstance)
class CustomAppInstanceAdmin(BaseAppAdmin):
list_display = BaseAppAdmin.list_display + (
Expand Down
3 changes: 3 additions & 0 deletions apps/app_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
VolumeForm,
VSCodeForm,
)
from apps.forms.mlflow import MLFlowAppForm
from apps.models import (
CustomAppInstance,
DashInstance,
FilemanagerInstance,
GradioInstance,
JupyterInstance,
MLFlowInstance,
NetpolicyInstance,
RStudioInstance,
ShinyInstance,
Expand All @@ -43,3 +45,4 @@
APP_REGISTRY.register("filemanager", ModelFormTuple(FilemanagerInstance, FilemanagerForm))
APP_REGISTRY.register("gradio", ModelFormTuple(GradioInstance, GradioForm))
APP_REGISTRY.register("streamlit", ModelFormTuple(StreamlitInstance, StreamlitForm))
APP_REGISTRY.register("mlflow", ModelFormTuple(MLFlowInstance, MLFlowAppForm))
24 changes: 24 additions & 0 deletions apps/forms/mlflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from crispy_forms.layout import Div, Field, Layout

from apps.forms.base import BaseForm
from apps.models import MLFlowInstance

__all__ = [
"MLFlowAppForm",
]


class MLFlowAppForm(BaseForm):
def _setup_form_helper(self):
super()._setup_form_helper()
body = Div(
Field("name", placeholder="Name your app"),
Field("subdomain", placeholder="Enter a subdomain or leave blank for a random one"),
css_class="card-body",
)

self.helper.layout = Layout(body, self.footer)

class Meta:
model = MLFlowInstance
fields = ["name"]
2 changes: 2 additions & 0 deletions apps/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ def save_instance_and_related_data(instance, form):
form.save_m2m()
instance.set_k8s_values()
instance.url = get_URI(instance)
# For MLFLOW, we need to set the k8s_values again to update the URL
instance.set_k8s_values()
instance.save(update_fields=["k8s_values", "url"])


Expand Down
41 changes: 41 additions & 0 deletions apps/migrations/0022_mlflowinstance_apps_user_can_see_secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 5.1.1 on 2025-02-21 10:44

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("apps", "0021_dashinstance_default_url_subpath"),
]

operations = [
migrations.CreateModel(
name="MLFlowInstance",
fields=[
(
"baseappinstance_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="apps.baseappinstance",
),
),
("access", models.CharField(choices=[("project", "Project")], default="project", max_length=20)),
],
options={
"verbose_name": "MLFlow App Instance",
"verbose_name_plural": "MLFlow App Instances",
"permissions": [("can_access_app", "Can access app service")],
},
bases=("apps.baseappinstance",),
),
migrations.AddField(
model_name="apps",
name="user_can_see_secrets",
field=models.BooleanField(default=False),
),
]
1 change: 1 addition & 0 deletions apps/models/app_types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .dash import DashInstance, DashInstanceManager
from .filemanager import FilemanagerInstance, FilemanagerInstanceManager
from .jupyter import JupyterInstance, JupyterInstanceManager
from .mlflow import MlflowAppManager, MLFlowInstance
from .netpolicy import NetpolicyInstance, NetpolicyInstanceManager
from .rstudio import RStudioInstance, RStudioInstanceManager
from .shiny import ShinyInstance, ShinyInstanceManager
Expand Down
62 changes: 62 additions & 0 deletions apps/models/app_types/mlflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from django.db import models
from django.utils.crypto import get_random_string

from apps.models import AppInstanceManager, BaseAppInstance


class MlflowAppManager(AppInstanceManager):
model_type = "mlflow"


class MLFlowInstance(BaseAppInstance):
objects = MlflowAppManager()
ACCESS_TYPES = (("project", "Project"),)
access = models.CharField(max_length=20, default="project", choices=ACCESS_TYPES)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def get_k8s_values(self):
k8s_values = super().get_k8s_values()
k8s_values["commonLabels"] = {
"release": self.subdomain.subdomain,
"app": "mlflow",
"project": self.project.slug,
}
k8s_values["tracking"] = {
"auth": {"enabled": True, "username": get_random_string(10), "password": get_random_string(20)},
"ingress": {
"enabled": True,
"ingressClassName": "nginx",
"hostname": self.url.split("://")[1] if self.url is not None else self.url,
},
"podLabels": {
"type": "app",
},
"resources": {
"requests": {"cpu": "1", "memory": "512Mi", "ephemeral-storage": "512Mi"},
"limits": {"cpu": "2", "memory": "1Gi", "ephemeral-storage": "1Gi"},
},
"pdb": {"create": False},
}
k8s_values["run"] = {
"resources": {
"requests": {"cpu": "1", "memory": "512Mi", "ephemeral-storage": "512Mi"},
"limits": {"cpu": "2", "memory": "1Gi", "ephemeral-storage": "1Gi"},
}
}
k8s_values["minio"] = {"pdb": {"create": False}}
k8s_values["postgresql"] = {
"primary": {
"pdb": {"create": False},
},
"readReplicas": {
"pdb": {"create": False},
},
}
return k8s_values

class Meta:
verbose_name = "MLFlow App Instance"
verbose_name_plural = "MLFlow App Instances"
permissions = [("can_access_app", "Can access app service")]
1 change: 1 addition & 0 deletions apps/models/base/app_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Apps(models.Model):
user_can_create = models.BooleanField(default=True)
user_can_edit = models.BooleanField(default=True)
user_can_delete = models.BooleanField(default=True)
user_can_see_secrets = models.BooleanField(default=False)
access = models.CharField(max_length=20, blank=True, null=True, default="public")
category = models.ForeignKey(
"AppCategories",
Expand Down
10 changes: 8 additions & 2 deletions apps/signals.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from django.db.models.signals import post_delete, post_save, pre_delete
from django.db.models.signals import post_delete, post_save, pre_delete, pre_save
from django.dispatch import receiver
from guardian.shortcuts import assign_perm, remove_perm

from apps.app_registry import APP_REGISTRY
from apps.models import AppStatus, BaseAppInstance
from apps.models import AppStatus, BaseAppInstance, MLFlowInstance
from studio.utils import get_logger

from .tasks import helm_delete
Expand Down Expand Up @@ -37,6 +37,12 @@ def post_delete_subdomain_remove(sender, instance, using, **kwargs):
baseapp_instance.save()


@receiver(post_save, sender=MLFlowInstance)
def set_mlflow_user_can_see_secrets(sender, instance, **kwargs):
instance.app.user_can_see_secrets = True
instance.app.save()


def update_permission(sender, instance, created, **kwargs):
owner = instance.owner

Expand Down
1 change: 1 addition & 0 deletions apps/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
path("create/<app_slug>", views.CreateApp.as_view(), name="create"),
path("settings/<app_slug>/<app_id>", views.CreateApp.as_view(), name="appsettings"),
path("delete/<app_slug>/<app_id>", views.delete, name="delete"),
path("secrets/<app_slug>/<app_id>", views.SecretsView.as_view(), name="secrets"),
]
44 changes: 44 additions & 0 deletions apps/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import base64
import subprocess
from datetime import datetime

import requests
Expand Down Expand Up @@ -295,3 +297,45 @@ def get_form(self, request, project, app_slug, app_id):
# Maybe this makes typing hard.
else:
return None


@method_decorator(
permission_required_or_403("can_view_project", (Project, "slug", "project")),
name="dispatch",
)
class SecretsView(View):
"""This view is used to display the secrets only of an MLFlow instance for now"""

template = "apps/secrets_view.html"

def get(self, request, project, app_slug, app_id):
instance = APP_REGISTRY.get_orm_model(app_slug).objects.get(pk=app_id)

username, password = None, None
if instance.app_status.status == "Running":
subdomain = instance.subdomain
username = subprocess.run(
(
"kubectl get secret "
f"--namespace default {subdomain.subdomain}-mlflow-tracking "
'-o jsonpath="{.data.admin-user}"'
).split(),
check=True,
text=True,
capture_output=True,
).stdout
username = base64.b64decode(username).decode()
password = subprocess.run(
(
"kubectl get secret "
f"--namespace default {subdomain.subdomain}-mlflow-tracking "
'-o jsonpath="{.data.admin-password}"'
).split(),
check=True,
text=True,
capture_output=True,
).stdout
password = base64.b64decode(password).decode()

context = {"mlflow_username": username, "mlflow_password": password, "mlflow_url": instance.url}
return render(request, self.template, context)
Loading

0 comments on commit 5338585

Please sign in to comment.