From 29661aa7cd195f03a01c24ce080796519b80b9f6 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 2 Dec 2025 16:09:21 +0100 Subject: [PATCH] Implement plan logic in create/update --- src/servala/core/models/service.py | 99 +++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 67ec4f7..3465d54 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -686,6 +686,60 @@ class ServiceInstance(ServalaModelMixin, models.Model): spec_data = prune_empty_data(spec_data) return spec_data + @staticmethod + def _apply_compute_plan_to_spec(spec_data, compute_plan_assignment): + """ + Apply compute plan resource allocations and SLA to spec. + """ + if not compute_plan_assignment: + return spec_data + + compute_plan = compute_plan_assignment.compute_plan + + if "parameters" not in spec_data: + spec_data["parameters"] = {} + if "size" not in spec_data["parameters"]: + spec_data["parameters"]["size"] = {} + if "requests" not in spec_data["parameters"]["size"]: + spec_data["parameters"]["size"]["requests"] = {} + if "service" not in spec_data["parameters"]: + spec_data["parameters"]["service"] = {} + + spec_data["parameters"]["size"]["memory"] = compute_plan.memory_limits + spec_data["parameters"]["size"]["cpu"] = compute_plan.cpu_limits + spec_data["parameters"]["size"]["requests"][ + "memory" + ] = compute_plan.memory_requests + spec_data["parameters"]["size"]["requests"]["cpu"] = compute_plan.cpu_requests + spec_data["parameters"]["service"]["serviceLevel"] = compute_plan_assignment.sla + return spec_data + + @staticmethod + def _build_billing_annotations(compute_plan_assignment, control_plane): + """ + Build Kubernetes annotations for billing integration. + """ + annotations = {} + + if compute_plan_assignment: + annotations["servala.com/erp_product_id_resource"] = str( + compute_plan_assignment.odoo_product_id + ) + annotations["servala.com/erp_unit_id_resource"] = str( + compute_plan_assignment.odoo_unit_id + ) + + if control_plane.storage_plan_odoo_product_id: + annotations["servala.com/erp_product_id_storage"] = str( + control_plane.storage_plan_odoo_product_id + ) + if control_plane.storage_plan_odoo_unit_id: + annotations["servala.com/erp_unit_id_storage"] = str( + control_plane.storage_plan_odoo_unit_id + ) + + return annotations + @classmethod def _format_kubernetes_error(cls, error_message): if not error_message: @@ -740,7 +794,15 @@ class ServiceInstance(ServalaModelMixin, models.Model): @classmethod @transaction.atomic - def create_instance(cls, name, organization, context, created_by, spec_data): + def create_instance( + cls, + name, + organization, + context, + created_by, + spec_data, + compute_plan_assignment=None, + ): # Ensure the namespace exists context.control_plane.get_or_create_namespace(organization) try: @@ -749,6 +811,7 @@ class ServiceInstance(ServalaModelMixin, models.Model): organization=organization, created_by=created_by, context=context, + compute_plan_assignment=compute_plan_assignment, ) except IntegrityError: message = _( @@ -759,6 +822,11 @@ class ServiceInstance(ServalaModelMixin, models.Model): try: spec_data = cls._prepare_spec_data(spec_data) + if compute_plan_assignment: + spec_data = cls._apply_compute_plan_to_spec( + spec_data, compute_plan_assignment + ) + if "writeConnectionSecretToRef" not in spec_data: spec_data["writeConnectionSecretToRef"] = {} @@ -776,6 +844,13 @@ class ServiceInstance(ServalaModelMixin, models.Model): }, "spec": spec_data, } + + annotations = cls._build_billing_annotations( + compute_plan_assignment, context.control_plane + ) + if annotations: + create_data["metadata"]["annotations"] = annotations + if label := context.control_plane.required_label: create_data["metadata"]["labels"] = {settings.DEFAULT_LABEL_KEY: label} api_instance = context.control_plane.custom_objects_api @@ -813,12 +888,23 @@ class ServiceInstance(ServalaModelMixin, models.Model): raise ValidationError(organization.add_support_message(message)) return instance - def update_spec(self, spec_data, updated_by): + def update_spec(self, spec_data, updated_by, compute_plan_assignment=None): try: spec_data = self._prepare_spec_data(spec_data) + + plan_to_use = compute_plan_assignment or self.compute_plan_assignment + if plan_to_use: + spec_data = self._apply_compute_plan_to_spec(spec_data, plan_to_use) + api_instance = self.context.control_plane.custom_objects_api patch_body = {"spec": spec_data} + annotations = self._build_billing_annotations( + plan_to_use, self.context.control_plane + ) + if annotations: + patch_body["metadata"] = {"annotations": annotations} + api_instance.patch_namespaced_custom_object( group=self.context.group, version=self.context.version, @@ -828,7 +914,14 @@ class ServiceInstance(ServalaModelMixin, models.Model): body=patch_body, ) self._clear_kubernetes_caches() - self.save() # Updates updated_at timestamp + + if ( + compute_plan_assignment + and compute_plan_assignment != self.compute_plan_assignment + ): + self.compute_plan_assignment = compute_plan_assignment + # Saving to update updated_at timestamp even if nothing was visibly changed + self.save() except ApiException as e: if e.status == 404: message = _(