Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/deprecate tool #1404

Merged
merged 21 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9575103
Add is_deprecated flag to Tool model
michaeljcollinsuk Nov 29, 2024
eb49afb
Add field to mark release retired and add deprecation message
michaeljcollinsuk Dec 3, 2024
eccd8b8
Disable open tool button
michaeljcollinsuk Dec 4, 2024
79923c6
Handle retired and deprecated tools in the frontend
michaeljcollinsuk Dec 9, 2024
cda0c94
Display deprecation message as a warning
michaeljcollinsuk Dec 9, 2024
d341d0e
Show retired message as a warning
michaeljcollinsuk Dec 9, 2024
0570ae9
Another change to use is_retired
michaeljcollinsuk Dec 9, 2024
9566bdc
Disable buttons based on selected release
michaeljcollinsuk Dec 9, 2024
1d0bf5a
Replace image_tag property with model field
michaeljcollinsuk Dec 10, 2024
1ec6793
Rebuild migrations after rebase
michaeljcollinsuk Dec 10, 2024
21a73dc
Remove DEPRECATED message from options
michaeljcollinsuk Dec 10, 2024
3e00b28
Placate super-linter
michaeljcollinsuk Dec 10, 2024
8f5a7c6
Add image_tag to the release detail, create pages
michaeljcollinsuk Dec 11, 2024
fbfb8a5
Make tool description a required field
michaeljcollinsuk Dec 11, 2024
da0200b
Fix queryset when looking form related tool
michaeljcollinsuk Dec 11, 2024
32e1493
Fix tests
michaeljcollinsuk Dec 12, 2024
97a2fbf
Refactoring and add notes for further changes
michaeljcollinsuk Dec 12, 2024
8caf312
Add further tests
michaeljcollinsuk Dec 12, 2024
6874e3d
Fix bug finding tools using rc chart version
michaeljcollinsuk Dec 12, 2024
1e9164f
Filter tool releases by status in superuser view
michaeljcollinsuk Dec 13, 2024
63d646c
Add status tag in release admin, and allow filtering by status
michaeljcollinsuk Dec 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions controlpanel/api/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,8 @@ def _set_values(self, **kwargs):

values.update(self.tool.values)
values.update(kwargs)
# override the tool image tag with the value stored in the DB
values.update({self.tool.image_tag_key: self.tool.image_tag})
set_values = []
for key, val in values.items():
if val:
Expand Down Expand Up @@ -1067,6 +1069,22 @@ def get_deployments(cls, user, id_token, search_name=None, search_version=None):
deployments.append(deployment)
return deployments

@classmethod
def get_chart_details(cls, chart: str) -> tuple[str, str]:
"""
This is a bit of a hack to safely extract the chart version when it includes an 'rc' tag.
This wont be necessary anymore when we track deployed tools in the database.
See https://github.com/ministryofjustice/analytical-platform/issues/6266
"""
chart_name, chart_version = chart.rsplit("-", 1)
if "rc" not in chart_version:
return chart_name, chart_version

rc_tag = chart_version
chart_name, chart_version = chart_name.rsplit("-", 1)
chart_version = f"{chart_version}-{rc_tag}"
return chart_name, chart_version

def get_deployment(self, id_token):
deployments = self.__class__.get_deployments(
self.user,
Expand Down
3 changes: 3 additions & 0 deletions controlpanel/api/helm.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ def update_helm_repository(force=False):
_execute("repo", "update", timeout=None) # timeout = infinity.


# TODO no longer used, remove
def get_default_image_tag_from_helm_chart(chart_url, chart_name):
proc = _execute("show", "values", chart_url)
if proc:
Expand All @@ -171,6 +172,8 @@ def get_default_image_tag_from_helm_chart(chart_url, chart_name):
return None


# TODO this is no longer called from the Your Tools page
# consider removing as part of further refactoring
def get_helm_entries():
# Update repository metadata.
update_helm_repository()
Expand Down
31 changes: 31 additions & 0 deletions controlpanel/api/migrations/0050_tool_is_deprecated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 5.1.2 on 2024-11-29 16:25

# Third-party
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0049_alter_feedback_suggestions"),
]

operations = [
migrations.AddField(
model_name="tool",
name="is_deprecated",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="tool",
name="deprecated_message",
field=models.TextField(
blank=True, help_text="If no message is provided, a default message will be used."
),
),
migrations.AddField(
model_name="tool",
name="is_retired",
field=models.BooleanField(default=False),
),
]
18 changes: 18 additions & 0 deletions controlpanel/api/migrations/0051_tool_image_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-12-10 09:01

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0050_tool_is_deprecated"),
]

operations = [
migrations.AddField(
model_name="tool",
name="image_tag",
field=models.CharField(blank=True, max_length=100, null=True),
),
]
26 changes: 26 additions & 0 deletions controlpanel/api/migrations/0052_add_image_tag_value.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 5.1.2 on 2024-12-10 09:01

from django.db import migrations


def add_image_tag(apps, schema_editor):
Tool = apps.get_model("api", "Tool")
for tool in Tool.objects.all():
chart_image_key_name = tool.chart_name.split("-")[0]
values = tool.values or {}
image_tag = values.get("{}.tag".format(chart_image_key_name)) or values.get(
"{}.image.tag".format(chart_image_key_name)
)
tool.image_tag = image_tag
tool.save()


class Migration(migrations.Migration):

dependencies = [
("api", "0051_tool_image_tag"),
]

operations = [
migrations.RunPython(code=add_image_tag, reverse_code=migrations.RunPython.noop),
]
19 changes: 19 additions & 0 deletions controlpanel/api/migrations/0053_alter_tool_image_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.1.2 on 2024-12-10 09:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0052_add_image_tag_value"),
]

operations = [
migrations.AlterField(
model_name="tool",
name="image_tag",
field=models.CharField(default="", max_length=100),
preserve_default=False,
),
]
18 changes: 18 additions & 0 deletions controlpanel/api/migrations/0054_alter_tool_description.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-12-11 14:44

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0053_alter_tool_image_tag"),
]

operations = [
migrations.AlterField(
model_name="tool",
name="description",
field=models.TextField(),
),
]
68 changes: 60 additions & 8 deletions controlpanel/api/models/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,18 @@ class Tool(TimeStampedModel):
"rstudio": "rstudio",
"vscode": "vscode",
}

description = models.TextField(blank=True)
DEFAULT_DEPRECATED_MESSAGE = "The selected release has been deprecated and will be retired soon. Please update to a more recent version." # noqa
JUPYTER_DATASCIENCE_CHART_NAME = "jupyter-lab-datascience-notebook"
JUPYTER_ALL_SPARK_CHART_NAME = "jupyter-lab-all-spark"
JUPYTER_LAB_CHART_NAME = "jupyter-lab"
RSTUDIO_CHART_NAME = "rstudio"
VSCODE_CHART_NAME = "vscode"
STATUS_RETIRED = "retired"
STATUS_DEPRECATED = "deprecated"
STATUS_ACTIVE = "active"
STATUS_RESTRICTED = "restricted"

description = models.TextField(blank=False)
chart_name = models.CharField(max_length=100, blank=False)
name = models.CharField(max_length=100, blank=False)
values = JSONField(default=dict)
Expand All @@ -51,6 +61,13 @@ class Tool(TimeStampedModel):
default=None,
)

is_deprecated = models.BooleanField(default=False)
deprecated_message = models.TextField(
blank=True, help_text="If no message is provided, a default message will be used."
)
is_retired = models.BooleanField(default=False)
image_tag = models.CharField(max_length=100)

class Meta(TimeStampedModel.Meta):
db_table = "control_panel_api_tool"
ordering = ("name",)
Expand All @@ -65,19 +82,54 @@ def url(self, user):
def save(self, *args, **kwargs):
helm.update_helm_repository(force=True)

# TODO description is now required when creating a release, so this is unlikely to be called
# Consider removing
if not self.description:
self.description = helm.get_chart_app_version(self.chart_name, self.version) or ""

super().save(*args, **kwargs)
return self

@property
def image_tag(self):
chart_image_key_name = self.chart_name.split("-")[0]
values = self.values or {}
return values.get("{}.tag".format(chart_image_key_name)) or values.get(
"{}.image.tag".format(chart_image_key_name)
)
def get_deprecated_message(self):
if not self.is_deprecated:
return ""

if self.is_retired:
return ""

return self.deprecated_message or self.DEFAULT_DEPRECATED_MESSAGE

@property
def image_tag_key(self):
mapping = {
self.JUPYTER_DATASCIENCE_CHART_NAME: "jupyter.tag",
self.JUPYTER_ALL_SPARK_CHART_NAME: "jupyter.tag",
self.JUPYTER_LAB_CHART_NAME: "jupyterlab.image.tag",
self.RSTUDIO_CHART_NAME: "rstudio.image.tag",
self.VSCODE_CHART_NAME: "vscode.image.tag",
}
return mapping[self.chart_name]

@property
def status(self):
if self.is_retired:
return self.STATUS_RETIRED.capitalize()
if self.is_deprecated:
return self.STATUS_DEPRECATED.capitalize()
if self.is_restricted:
return self.STATUS_RESTRICTED.capitalize()
return self.STATUS_ACTIVE.capitalize()

@property
def status_colour(self):
mapping = {
self.STATUS_RETIRED: "red",
self.STATUS_DEPRECATED: "grey",
self.STATUS_RESTRICTED: "yellow",
self.STATUS_ACTIVE: "green",
}
return mapping[self.status.lower()]


class ToolDeploymentManager:
Expand Down
2 changes: 1 addition & 1 deletion controlpanel/frontend/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def tool_restart(self, message):
log.debug(f"Restarted {tool.name} for {user}")

def get_tool_and_user(self, message):
tool = Tool.objects.get(pk=message["tool_id"])
tool = Tool.objects.get(is_retired=False, pk=message["tool_id"])
if not tool:
raise Exception(f"no Tool record found for query {message['tool_id']}")
user = User.objects.get(auth0_id=message["user_id"])
Expand Down
66 changes: 55 additions & 11 deletions controlpanel/frontend/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,67 @@
from controlpanel.api.models.tool import Tool


class ReleaseFilter(django_filters.FilterSet):
class InitialFilterSetMixin(django_filters.FilterSet):

def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
# if filterset is bound, use initial values as defaults
if data is not None:
# get a mutable copy of the QueryDict
data = data.copy()

for name, f in self.base_filters.items():
initial = f.extra.get("initial")

# filter param is either missing or empty, use initial as default
if not data.get(name) and initial:
data[name] = initial

super().__init__(data, queryset, request=request, prefix=prefix)


class ReleaseFilter(InitialFilterSetMixin):
YES_NO_CHOICES = [("all", "---------"), ("true", "Yes"), ("false", "No")]
chart_name = django_filters.ChoiceFilter()
is_restricted = django_filters.BooleanFilter(label="Restricted release?")
# is_restricted = django_filters.BooleanFilter(label="Restricted release?")
status = django_filters.ChoiceFilter(
choices=[
(Tool.STATUS_ACTIVE, Tool.STATUS_ACTIVE.capitalize()),
(Tool.STATUS_RESTRICTED, Tool.STATUS_RESTRICTED.capitalize()),
(Tool.STATUS_DEPRECATED, Tool.STATUS_DEPRECATED.capitalize()),
(Tool.STATUS_RETIRED, Tool.STATUS_RETIRED.capitalize()),
("all", "All"),
],
method="filter_status",
label="Status",
empty_label=None,
initial="active",
)

class Meta:
model = Tool
fields = ["chart_name", "is_restricted"]
fields = [
"chart_name",
]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, data=None, *args, **kwargs):
super().__init__(data, *args, **kwargs)
self.filters["chart_name"].extra["choices"] = (
Tool.objects.values_list("chart_name", "chart_name").order_by().distinct()
)
self.filters["chart_name"].field.widget.attrs = {"class": "govuk-select"}
self.filters["is_restricted"].field.widget.choices = [
("all", "---------"),
("true", "Yes"),
("false", "No"),
]
self.filters["is_restricted"].field.widget.attrs = {"class": "govuk-select"}
self.filters["status"].field.widget.attrs = {"class": "govuk-select"}

def filter_status(self, queryset, name, value):
if value == "all":
return queryset
if value == "retired":
return queryset.filter(is_retired=True)
# remove retired tools from the list
queryset = queryset.filter(is_retired=False)
if value == "active":
return queryset.filter(is_restricted=False, is_deprecated=False)
return queryset.filter(
**{
f"is_{value}": True,
}
)
4 changes: 4 additions & 0 deletions controlpanel/frontend/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,10 +540,14 @@ class Meta:
"name",
"chart_name",
"version",
"image_tag",
"values",
"is_restricted",
"tool_domain",
"description",
"is_deprecated",
"deprecated_message",
"is_retired",
]


Expand Down
Loading
Loading