Add model field, migrations, and backend methods

This commit is contained in:
Tobias Kunze 2025-12-09 09:26:48 +01:00
parent 9cff1e85ac
commit 582c4ed564
2 changed files with 103 additions and 4 deletions

View file

@ -0,0 +1,46 @@
from django.db import migrations, models
import servala.core.validators
def populate_display_name(apps, schema_editor):
"""
For existing instances, copy name to display_name.
Existing instances already have their name matching the k8s resource,
so we cannot add a prefix.
"""
ServiceInstance = apps.get_model("core", "ServiceInstance")
for instance in ServiceInstance.objects.all():
instance.display_name = instance.name
instance.save(update_fields=["display_name"])
class Migration(migrations.Migration):
dependencies = [
("core", "0018_add_invoice_grouping_to_organization_origin"),
]
operations = [
migrations.AlterField(
model_name="serviceinstance",
name="name",
field=models.CharField(
max_length=63,
validators=[servala.core.validators.kubernetes_name_validator],
verbose_name="Resource Name",
),
),
# Add display_name field with a temporary default
migrations.AddField(
model_name="serviceinstance",
name="display_name",
field=models.CharField(
default="",
help_text="Display name for this instance",
max_length=100,
verbose_name="Name",
),
preserve_default=False,
),
migrations.RunPython(populate_display_name, migrations.RunPython.noop),
]

View file

@ -18,6 +18,10 @@ from encrypted_fields.fields import EncryptedJSONField
from kubernetes import client, config
from kubernetes.client.rest import ApiException
import hashlib
from django.conf import settings
from servala.core import rules as perms
from servala.core.models.mixins import ServalaModelMixin
from servala.core.validators import kubernetes_name_validator
@ -615,8 +619,16 @@ class ServiceInstance(ServalaModelMixin, models.Model):
on the fly.
"""
# The Kubernetes resource name (metadata.name). This field is immutable after
# creation and is auto-generated for new instances. Do not modify directly!
name = models.CharField(
max_length=63, verbose_name=_("Name"), validators=[kubernetes_name_validator]
max_length=63,
verbose_name=_("Resource Name"),
validators=[kubernetes_name_validator],
)
display_name = models.CharField(
max_length=100,
verbose_name=_("Name"),
)
organization = models.ForeignKey(
to="core.Organization",
@ -686,6 +698,24 @@ class ServiceInstance(ServalaModelMixin, models.Model):
spec_data = prune_empty_data(spec_data)
return spec_data
@staticmethod
def generate_resource_name(organization, display_name, service, attempt=0):
"""
Generate a unique Kubernetes-compatible resource name.
Format: {prefix}-{sha256[:8]}
The hash input is: org_slug:display_name:service_slug[:attempt if > 0]
On collision, we retry with an incremented attempt number included in hash.
"""
hash_input = (
f"{organization.slug}:{display_name.lower().strip()}:{service.slug}"
)
if attempt > 0:
hash_input += f":{attempt}"
hash_value = hashlib.sha256(hash_input.encode("utf-8")).hexdigest()[:8]
return f"{settings.SERVALA_INSTANCE_NAME_PREFIX}-{hash_value}"
@staticmethod
def _apply_compute_plan_to_spec(spec_data, compute_plan_assignment):
"""
@ -719,16 +749,20 @@ class ServiceInstance(ServalaModelMixin, models.Model):
compute_plan_assignment,
control_plane,
instance_name=None,
display_name=None,
organization=None,
service=None,
):
"""
Build Kubernetes annotations for billing integration.
Build Kubernetes annotations for billing integration and display name.
"""
from servala.core.models.organization import InvoiceGroupingChoice
annotations = {}
if display_name:
annotations["servala.com/displayName"] = display_name
if compute_plan_assignment:
annotations["servala.com/erp_product_id_resource"] = str(
compute_plan_assignment.odoo_product_id
@ -830,18 +864,35 @@ class ServiceInstance(ServalaModelMixin, models.Model):
@transaction.atomic
def create_instance(
cls,
name,
display_name,
organization,
context,
created_by,
spec_data,
compute_plan_assignment=None,
):
service = context.service_offering.service
name = None
for attempt in range(10):
name = cls.generate_resource_name(
organization, display_name, service, attempt
)
if not cls.objects.filter(
name=name, organization=organization, context=context
).exists():
break
else:
message = _(
"Could not generate a unique resource name. Please try a different display name."
)
raise ValidationError(organization.add_support_message(message))
# Ensure the namespace exists
context.control_plane.get_or_create_namespace(organization)
try:
instance = cls.objects.create(
name=name,
display_name=display_name,
organization=organization,
created_by=created_by,
context=context,
@ -883,8 +934,9 @@ class ServiceInstance(ServalaModelMixin, models.Model):
compute_plan_assignment=compute_plan_assignment,
control_plane=context.control_plane,
instance_name=name,
display_name=display_name,
organization=organization,
service=context.service_offering.service,
service=service,
)
if annotations:
create_data["metadata"]["annotations"] = annotations
@ -941,6 +993,7 @@ class ServiceInstance(ServalaModelMixin, models.Model):
compute_plan_assignment=plan_to_use,
control_plane=self.context.control_plane,
instance_name=self.name,
display_name=self.display_name,
organization=self.organization,
service=self.context.service_offering.service,
)