Add billing annotations and tests

This commit is contained in:
Tobias Kunze 2025-12-04 16:43:59 +01:00 committed by Tobias Brunner
parent 3e6623278c
commit 84c0220af0
Signed by: tobru
SSH key fingerprint: SHA256:kOXg1R6c11XW3/Pt9dbLdQvOJGFAy+B2K6v6PtRWBGQ
2 changed files with 258 additions and 7 deletions

View file

@ -715,10 +715,18 @@ class ServiceInstance(ServalaModelMixin, models.Model):
return spec_data
@staticmethod
def _build_billing_annotations(compute_plan_assignment, control_plane):
def _build_billing_annotations(
compute_plan_assignment,
control_plane,
instance_name=None,
organization=None,
service=None,
):
"""
Build Kubernetes annotations for billing integration.
"""
from servala.core.models.organization import InvoiceGroupingChoice
annotations = {}
if compute_plan_assignment:
@ -738,6 +746,30 @@ class ServiceInstance(ServalaModelMixin, models.Model):
control_plane.storage_plan_odoo_unit_id
)
if organization and instance_name:
invoice_grouping = organization.origin.invoice_grouping
cloud_provider_name = control_plane.cloud_provider.name
control_plane_name = control_plane.name
if invoice_grouping == InvoiceGroupingChoice.BY_SERVICE:
if service:
annotations["servala.com/erp_item_group_description"] = (
f"Servala Service: {service.name}"
)
annotations["servala.com/erp_item_description"] = (
f"{instance_name} on {cloud_provider_name} {control_plane_name}"
)
else:
group_description = f"Organization: {organization.name}"
item_description = f"{instance_name} on {control_plane_name}"
if organization.osb_guid:
group_description += f" ({organization.osb_guid})"
item_description += f" [Org: {organization.osb_guid}]"
annotations["servala.com/erp_item_group_description"] = group_description
annotations["servala.com/erp_item_description"] = item_description
return annotations
@classmethod
@ -846,7 +878,11 @@ class ServiceInstance(ServalaModelMixin, models.Model):
}
annotations = cls._build_billing_annotations(
compute_plan_assignment, context.control_plane
compute_plan_assignment=compute_plan_assignment,
control_plane=context.control_plane,
instance_name=name,
organization=organization,
service=context.service_offering.service,
)
if annotations:
create_data["metadata"]["annotations"] = annotations
@ -900,7 +936,11 @@ class ServiceInstance(ServalaModelMixin, models.Model):
patch_body = {"spec": spec_data}
annotations = self._build_billing_annotations(
plan_to_use, self.context.control_plane
compute_plan_assignment=plan_to_use,
control_plane=self.context.control_plane,
instance_name=self.name,
organization=self.organization,
service=self.context.service_offering.service,
)
if annotations:
patch_body["metadata"] = {"annotations": annotations}

View file

@ -5,6 +5,7 @@ import pytest
from servala.core.models import (
ComputePlan,
ComputePlanAssignment,
InvoiceGroupingChoice,
ServiceInstance,
)
@ -125,7 +126,8 @@ def test_build_billing_annotations_complete():
control_plane.storage_plan_odoo_unit_id = "storage-unit-id"
annotations = ServiceInstance._build_billing_annotations(
compute_plan_assignment, control_plane
compute_plan_assignment=compute_plan_assignment,
control_plane=control_plane,
)
assert annotations["servala.com/erp_product_id_resource"] == "test-product-123"
@ -140,7 +142,10 @@ def test_build_billing_annotations_no_compute_plan():
control_plane.storage_plan_odoo_product_id = "storage-product-id"
control_plane.storage_plan_odoo_unit_id = "storage-unit-id"
annotations = ServiceInstance._build_billing_annotations(None, control_plane)
annotations = ServiceInstance._build_billing_annotations(
compute_plan_assignment=None,
control_plane=control_plane,
)
assert "servala.com/erp_product_id_resource" not in annotations
assert "servala.com/erp_unit_id_resource" not in annotations
@ -158,7 +163,8 @@ def test_build_billing_annotations_no_storage_plan():
control_plane.storage_plan_odoo_unit_id = None
annotations = ServiceInstance._build_billing_annotations(
compute_plan_assignment, control_plane
compute_plan_assignment=compute_plan_assignment,
control_plane=control_plane,
)
assert annotations["servala.com/erp_product_id_resource"] == "product-id"
@ -172,7 +178,10 @@ def test_build_billing_annotations_empty():
control_plane.storage_plan_odoo_product_id = None
control_plane.storage_plan_odoo_unit_id = None
annotations = ServiceInstance._build_billing_annotations(None, control_plane)
annotations = ServiceInstance._build_billing_annotations(
compute_plan_assignment=None,
control_plane=control_plane,
)
assert annotations == {}
@ -197,3 +206,205 @@ def test_all_billing_units():
assert str(choices["day"]) == "Day"
assert "Month" in str(choices["month"])
assert str(choices["year"]) == "Year"
def test_build_billing_annotations_by_service_grouping():
control_plane = Mock()
control_plane.storage_plan_odoo_product_id = None
control_plane.storage_plan_odoo_unit_id = None
control_plane.cloud_provider = Mock()
control_plane.cloud_provider.name = "Exoscale"
control_plane.name = "Geneva (CH-GVA-2)"
organization = Mock()
organization.name = "ACME Corp"
organization.osb_guid = "01998651-dc86-7d43-9e49-cdb790fcc4f0"
organization.origin = Mock()
organization.origin.invoice_grouping = InvoiceGroupingChoice.BY_SERVICE
service = Mock()
service.name = "Redis"
annotations = ServiceInstance._build_billing_annotations(
compute_plan_assignment=None,
control_plane=control_plane,
instance_name="MyProdRedis",
organization=organization,
service=service,
)
assert (
annotations["servala.com/erp_item_group_description"]
== "Servala Service: Redis"
)
assert (
annotations["servala.com/erp_item_description"]
== "MyProdRedis on Exoscale Geneva (CH-GVA-2)"
)
def test_build_billing_annotations_by_organization_grouping_with_osb_guid():
control_plane = Mock()
control_plane.storage_plan_odoo_product_id = None
control_plane.storage_plan_odoo_unit_id = None
control_plane.cloud_provider = Mock()
control_plane.cloud_provider.name = "Exoscale"
control_plane.name = "Geneva (CH-GVA-2)"
organization = Mock()
organization.name = "ACME"
organization.osb_guid = "01998651-dc86-7d43-9e49-cdb790fcc4f0"
organization.origin = Mock()
organization.origin.invoice_grouping = InvoiceGroupingChoice.BY_ORGANIZATION
service = Mock()
service.name = "Redis"
annotations = ServiceInstance._build_billing_annotations(
compute_plan_assignment=None,
control_plane=control_plane,
instance_name="MyProdRedis",
organization=organization,
service=service,
)
assert (
annotations["servala.com/erp_item_group_description"]
== "Organization: ACME (01998651-dc86-7d43-9e49-cdb790fcc4f0)"
)
assert (
annotations["servala.com/erp_item_description"]
== "MyProdRedis on Geneva (CH-GVA-2) [Org: 01998651-dc86-7d43-9e49-cdb790fcc4f0]"
)
def test_build_billing_annotations_by_organization_grouping_without_osb_guid():
control_plane = Mock()
control_plane.storage_plan_odoo_product_id = None
control_plane.storage_plan_odoo_unit_id = None
control_plane.cloud_provider = Mock()
control_plane.cloud_provider.name = "Exoscale"
control_plane.name = "Geneva (CH-GVA-2)"
organization = Mock()
organization.name = "ACME Corp"
organization.osb_guid = None # No OSB GUID
organization.origin = Mock()
organization.origin.invoice_grouping = InvoiceGroupingChoice.BY_ORGANIZATION
service = Mock()
service.name = "Redis"
annotations = ServiceInstance._build_billing_annotations(
compute_plan_assignment=None,
control_plane=control_plane,
instance_name="MyProdRedis",
organization=organization,
service=service,
)
assert (
annotations["servala.com/erp_item_group_description"]
== "Organization: ACME Corp"
)
assert (
annotations["servala.com/erp_item_description"]
== "MyProdRedis on Geneva (CH-GVA-2)"
)
def test_build_billing_annotations_by_organization_grouping_with_empty_osb_guid():
control_plane = Mock()
control_plane.storage_plan_odoo_product_id = None
control_plane.storage_plan_odoo_unit_id = None
control_plane.cloud_provider = Mock()
control_plane.cloud_provider.name = "Exoscale"
control_plane.name = "Geneva (CH-GVA-2)"
organization = Mock()
organization.name = "ACME Corp"
organization.osb_guid = "" # Empty string OSB GUID
organization.origin = Mock()
organization.origin.invoice_grouping = InvoiceGroupingChoice.BY_ORGANIZATION
service = Mock()
service.name = "Redis"
annotations = ServiceInstance._build_billing_annotations(
compute_plan_assignment=None,
control_plane=control_plane,
instance_name="MyProdRedis",
organization=organization,
service=service,
)
assert (
annotations["servala.com/erp_item_group_description"]
== "Organization: ACME Corp"
)
assert (
annotations["servala.com/erp_item_description"]
== "MyProdRedis on Geneva (CH-GVA-2)"
)
def test_build_billing_annotations_no_item_description_without_organization():
control_plane = Mock()
control_plane.storage_plan_odoo_product_id = "storage-product"
control_plane.storage_plan_odoo_unit_id = "storage-unit"
annotations = ServiceInstance._build_billing_annotations(
compute_plan_assignment=None,
control_plane=control_plane,
instance_name="MyProdRedis",
organization=None, # No organization
service=None,
)
assert "servala.com/erp_item_group_description" not in annotations
assert "servala.com/erp_item_description" not in annotations
# Storage annotations should still be there
assert annotations["servala.com/erp_product_id_storage"] == "storage-product"
def test_build_billing_annotations_combined():
compute_plan_assignment = Mock()
compute_plan_assignment.odoo_product_id = "compute-product"
compute_plan_assignment.odoo_unit_id = "compute-unit"
control_plane = Mock()
control_plane.storage_plan_odoo_product_id = "storage-product"
control_plane.storage_plan_odoo_unit_id = "storage-unit"
control_plane.cloud_provider = Mock()
control_plane.cloud_provider.name = "Exoscale"
control_plane.name = "Geneva (CH-GVA-2)"
organization = Mock()
organization.name = "ACME"
organization.osb_guid = "test-guid"
organization.origin = Mock()
organization.origin.invoice_grouping = InvoiceGroupingChoice.BY_SERVICE
service = Mock()
service.name = "PostgreSQL"
annotations = ServiceInstance._build_billing_annotations(
compute_plan_assignment=compute_plan_assignment,
control_plane=control_plane,
instance_name="ProdDB",
organization=organization,
service=service,
)
assert annotations["servala.com/erp_product_id_resource"] == "compute-product"
assert annotations["servala.com/erp_unit_id_resource"] == "compute-unit"
assert annotations["servala.com/erp_product_id_storage"] == "storage-product"
assert annotations["servala.com/erp_unit_id_storage"] == "storage-unit"
assert (
annotations["servala.com/erp_item_group_description"]
== "Servala Service: PostgreSQL"
)
assert (
annotations["servala.com/erp_item_description"]
== "ProdDB on Exoscale Geneva (CH-GVA-2)"
)