Add billing annotations and tests
This commit is contained in:
parent
3e6623278c
commit
84c0220af0
2 changed files with 258 additions and 7 deletions
|
|
@ -715,10 +715,18 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
return spec_data
|
return spec_data
|
||||||
|
|
||||||
@staticmethod
|
@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.
|
Build Kubernetes annotations for billing integration.
|
||||||
"""
|
"""
|
||||||
|
from servala.core.models.organization import InvoiceGroupingChoice
|
||||||
|
|
||||||
annotations = {}
|
annotations = {}
|
||||||
|
|
||||||
if compute_plan_assignment:
|
if compute_plan_assignment:
|
||||||
|
|
@ -738,6 +746,30 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
control_plane.storage_plan_odoo_unit_id
|
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
|
return annotations
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -846,7 +878,11 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
}
|
}
|
||||||
|
|
||||||
annotations = cls._build_billing_annotations(
|
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:
|
if annotations:
|
||||||
create_data["metadata"]["annotations"] = annotations
|
create_data["metadata"]["annotations"] = annotations
|
||||||
|
|
@ -900,7 +936,11 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
patch_body = {"spec": spec_data}
|
patch_body = {"spec": spec_data}
|
||||||
|
|
||||||
annotations = self._build_billing_annotations(
|
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:
|
if annotations:
|
||||||
patch_body["metadata"] = {"annotations": annotations}
|
patch_body["metadata"] = {"annotations": annotations}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import pytest
|
||||||
from servala.core.models import (
|
from servala.core.models import (
|
||||||
ComputePlan,
|
ComputePlan,
|
||||||
ComputePlanAssignment,
|
ComputePlanAssignment,
|
||||||
|
InvoiceGroupingChoice,
|
||||||
ServiceInstance,
|
ServiceInstance,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -125,7 +126,8 @@ def test_build_billing_annotations_complete():
|
||||||
control_plane.storage_plan_odoo_unit_id = "storage-unit-id"
|
control_plane.storage_plan_odoo_unit_id = "storage-unit-id"
|
||||||
|
|
||||||
annotations = ServiceInstance._build_billing_annotations(
|
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"
|
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_product_id = "storage-product-id"
|
||||||
control_plane.storage_plan_odoo_unit_id = "storage-unit-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_product_id_resource" not in annotations
|
||||||
assert "servala.com/erp_unit_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
|
control_plane.storage_plan_odoo_unit_id = None
|
||||||
|
|
||||||
annotations = ServiceInstance._build_billing_annotations(
|
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"
|
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_product_id = None
|
||||||
control_plane.storage_plan_odoo_unit_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 == {}
|
assert annotations == {}
|
||||||
|
|
||||||
|
|
@ -197,3 +206,205 @@ def test_all_billing_units():
|
||||||
assert str(choices["day"]) == "Day"
|
assert str(choices["day"]) == "Day"
|
||||||
assert "Month" in str(choices["month"])
|
assert "Month" in str(choices["month"])
|
||||||
assert str(choices["year"]) == "Year"
|
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)"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue