Add model field, migrations, and backend methods
This commit is contained in:
parent
ffc5b0285a
commit
46a2826ff5
2 changed files with 103 additions and 4 deletions
46
src/servala/core/migrations/0019_add_display_name.py
Normal file
46
src/servala/core/migrations/0019_add_display_name.py
Normal 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),
|
||||||
|
]
|
||||||
|
|
@ -18,6 +18,10 @@ from encrypted_fields.fields import EncryptedJSONField
|
||||||
from kubernetes import client, config
|
from kubernetes import client, config
|
||||||
from kubernetes.client.rest import ApiException
|
from kubernetes.client.rest import ApiException
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from servala.core import rules as perms
|
from servala.core import rules as perms
|
||||||
from servala.core.models.mixins import ServalaModelMixin
|
from servala.core.models.mixins import ServalaModelMixin
|
||||||
from servala.core.validators import kubernetes_name_validator
|
from servala.core.validators import kubernetes_name_validator
|
||||||
|
|
@ -615,8 +619,16 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
on the fly.
|
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(
|
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(
|
organization = models.ForeignKey(
|
||||||
to="core.Organization",
|
to="core.Organization",
|
||||||
|
|
@ -686,6 +698,24 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
spec_data = prune_empty_data(spec_data)
|
spec_data = prune_empty_data(spec_data)
|
||||||
return 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
|
@staticmethod
|
||||||
def _apply_compute_plan_to_spec(spec_data, compute_plan_assignment):
|
def _apply_compute_plan_to_spec(spec_data, compute_plan_assignment):
|
||||||
"""
|
"""
|
||||||
|
|
@ -719,16 +749,20 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
compute_plan_assignment,
|
compute_plan_assignment,
|
||||||
control_plane,
|
control_plane,
|
||||||
instance_name=None,
|
instance_name=None,
|
||||||
|
display_name=None,
|
||||||
organization=None,
|
organization=None,
|
||||||
service=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
|
from servala.core.models.organization import InvoiceGroupingChoice
|
||||||
|
|
||||||
annotations = {}
|
annotations = {}
|
||||||
|
|
||||||
|
if display_name:
|
||||||
|
annotations["servala.com/displayName"] = display_name
|
||||||
|
|
||||||
if compute_plan_assignment:
|
if compute_plan_assignment:
|
||||||
annotations["servala.com/erp_product_id_resource"] = str(
|
annotations["servala.com/erp_product_id_resource"] = str(
|
||||||
compute_plan_assignment.odoo_product_id
|
compute_plan_assignment.odoo_product_id
|
||||||
|
|
@ -830,18 +864,35 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create_instance(
|
def create_instance(
|
||||||
cls,
|
cls,
|
||||||
name,
|
display_name,
|
||||||
organization,
|
organization,
|
||||||
context,
|
context,
|
||||||
created_by,
|
created_by,
|
||||||
spec_data,
|
spec_data,
|
||||||
compute_plan_assignment=None,
|
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
|
# Ensure the namespace exists
|
||||||
context.control_plane.get_or_create_namespace(organization)
|
context.control_plane.get_or_create_namespace(organization)
|
||||||
try:
|
try:
|
||||||
instance = cls.objects.create(
|
instance = cls.objects.create(
|
||||||
name=name,
|
name=name,
|
||||||
|
display_name=display_name,
|
||||||
organization=organization,
|
organization=organization,
|
||||||
created_by=created_by,
|
created_by=created_by,
|
||||||
context=context,
|
context=context,
|
||||||
|
|
@ -883,8 +934,9 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
compute_plan_assignment=compute_plan_assignment,
|
compute_plan_assignment=compute_plan_assignment,
|
||||||
control_plane=context.control_plane,
|
control_plane=context.control_plane,
|
||||||
instance_name=name,
|
instance_name=name,
|
||||||
|
display_name=display_name,
|
||||||
organization=organization,
|
organization=organization,
|
||||||
service=context.service_offering.service,
|
service=service,
|
||||||
)
|
)
|
||||||
if annotations:
|
if annotations:
|
||||||
create_data["metadata"]["annotations"] = annotations
|
create_data["metadata"]["annotations"] = annotations
|
||||||
|
|
@ -941,6 +993,7 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
compute_plan_assignment=plan_to_use,
|
compute_plan_assignment=plan_to_use,
|
||||||
control_plane=self.context.control_plane,
|
control_plane=self.context.control_plane,
|
||||||
instance_name=self.name,
|
instance_name=self.name,
|
||||||
|
display_name=self.display_name,
|
||||||
organization=self.organization,
|
organization=self.organization,
|
||||||
service=self.context.service_offering.service,
|
service=self.context.service_offering.service,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue