Organization Namespace Generation #46
7 changed files with 43 additions and 33 deletions
|
@ -65,8 +65,7 @@ class OrganizationAdmin(admin.ModelAdmin):
|
|||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
readonly_fields = list(super().get_readonly_fields(request, obj) or [])
|
||||
if obj: # If this is an edit (not a new organization)
|
||||
readonly_fields.append("namespace")
|
||||
readonly_fields.append("namespace") # Always read-only
|
||||
return readonly_fields
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 5.2b1 on 2025-04-09 14:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0002_alter_controlplanecrd_options_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="organization",
|
||||
name="namespace",
|
||||
field=models.CharField(
|
||||
help_text="This namespace will be used for all Kubernetes resources.",
|
||||
max_length=20,
|
||||
null=True,
|
||||
unique=True,
|
||||
verbose_name="Kubernetes Namespace",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -9,19 +9,19 @@ from django_scopes import ScopedManager, scopes_disabled
|
|||
|
||||
from servala.core import rules as perms
|
||||
from servala.core.models.mixins import ServalaModelMixin
|
||||
from servala.core.validators import kubernetes_name_validator
|
||||
|
||||
|
||||
class Organization(ServalaModelMixin, models.Model):
|
||||
name = models.CharField(max_length=100, verbose_name=_("Name"))
|
||||
# The namespace is generated as "org-{id}" in accordance with RFC 1035 Label Names.
|
||||
# It is nullable as we need to write to the database in order to read the ID, but should
|
||||
# not be null in practical use.
|
||||
namespace = models.CharField(
|
||||
max_length=63,
|
||||
max_length=20,
|
||||
verbose_name=_("Kubernetes Namespace"),
|
||||
null=True,
|
||||
unique=True,
|
||||
help_text=_(
|
||||
"This namespace will be used for all Kubernetes resources. Cannot be changed after creation."
|
||||
),
|
||||
validators=[kubernetes_name_validator],
|
||||
help_text=_("This namespace will be used for all Kubernetes resources."),
|
||||
)
|
||||
|
||||
billing_entity = models.ForeignKey(
|
||||
|
@ -90,6 +90,13 @@ class Organization(ServalaModelMixin, models.Model):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if not self.namespace:
|
||||
# Set namespace after initial save to ensure we have an ID
|
||||
self.namespace = f"org-{self.pk}"
|
||||
self.save(update_fields=["namespace"])
|
||||
|
||||
|
||||
class BillingEntity(ServalaModelMixin, models.Model):
|
||||
"""
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
from django import forms
|
||||
from django.forms import ModelForm
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from servala.core.models import Organization
|
||||
from servala.frontend.forms.mixins import HtmxMixin
|
||||
|
@ -9,19 +7,4 @@ from servala.frontend.forms.mixins import HtmxMixin
|
|||
class OrganizationForm(HtmxMixin, ModelForm):
|
||||
class Meta:
|
||||
model = Organization
|
||||
fields = ("name", "namespace")
|
||||
widgets = {
|
||||
"namespace": forms.TextInput(
|
||||
attrs={
|
||||
"pattern": "[a-z0-9]([-a-z0-9]*[a-z0-9])?",
|
||||
"title": _(
|
||||
'Lowercase alphanumeric characters or "-", must start and end with alphanumeric'
|
||||
),
|
||||
}
|
||||
)
|
||||
}
|
||||
help_texts = {
|
||||
"namespace": _(
|
||||
"This namespace will be used for all resources and cannot be changed later."
|
||||
)
|
||||
}
|
||||
fields = ("name",)
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
</th>
|
||||
<td>
|
||||
<div>{{ form.instance.namespace }}</div>
|
||||
<small class="text-muted">{% translate "The namespace cannot be changed after creation." %}</small>
|
||||
<small class="text-muted">{% translate "System-generated namespace for Kubernetes resources." %}</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -15,16 +15,12 @@ def origin():
|
|||
|
||||
@pytest.fixture
|
||||
def organization(origin):
|
||||
return Organization.objects.create(
|
||||
name="Test Org", namespace="test-org", origin=origin
|
||||
)
|
||||
return Organization.objects.create(name="Test Org", origin=origin)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def other_organization(origin):
|
||||
return Organization.objects.create(
|
||||
name="Test Org Alternate", namespace="test-org-alternate", origin=origin
|
||||
)
|
||||
return Organization.objects.create(name="Test Org Alternate", origin=origin)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
@ -23,6 +23,7 @@ def test_root_view_invalid_urls_404(client):
|
|||
def test_owner_can_access_dashboard(client, org_owner, organization):
|
||||
client.force_login(org_owner)
|
||||
response = client.get(organization.urls.base)
|
||||
assert organization.namespace == f"org-{organization.pk}"
|
||||
assert response.status_code == 200
|
||||
assert organization.name in response.content.decode()
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue