Skip to content

Commit

Permalink
Add credit allocation api and update functional test (#8)
Browse files Browse the repository at this point in the history
* test refactor

* add creditallocation api

* new creditallocation unit test

* add ResourceProviderAccount API

* remove auth for now

* hyperlinked creditallocation

* functional test updated
  • Loading branch information
scrungus committed Jul 24, 2024
1 parent 3ae3fba commit a6e09a7
Show file tree
Hide file tree
Showing 14 changed files with 554 additions and 183 deletions.
4 changes: 4 additions & 0 deletions coral_credits/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from coral_credits.api import models

# Admin views are decorated with @csrf_protect so
# CSRF protection is enabled even without a middleware.

# Register your models here.
admin.site.register(models.CreditAccount)
admin.site.register(models.CreditAllocation)
Expand All @@ -10,3 +13,4 @@
admin.site.register(models.ResourceClass)
admin.site.register(models.ResourceConsumptionRecord)
admin.site.register(models.ResourceProvider)
admin.site.register(models.ResourceProviderAccount)
6 changes: 6 additions & 0 deletions coral_credits/api/db_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ class NoCreditAllocation(Exception):
"""Raised when an account has no credit allocated for a given resource"""

pass


class NoResourceClass(Exception):
"""Raised when there is no resource class matching the query"""

pass
73 changes: 72 additions & 1 deletion coral_credits/api/db_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@ def get_resource_provider_account(project_id):
return resource_provider_account


def get_credit_allocations(resource_provider_account):
def get_credit_allocation(id):
now = timezone.now()
try:
credit_allocation = models.CreditAllocation.objects.filter(
id=id, start__lte=now, end__gte=now
).first()
except models.CreditAllocation.DoesNotExist:
raise db_exceptions.NoCreditAllocation("Invalid allocation_id")
return credit_allocation


def get_all_credit_allocations(resource_provider_account):
# Find all associated active CreditAllocations
# Make sure we only look for CreditAllocations valid for the current time
now = timezone.now()
Expand Down Expand Up @@ -53,6 +64,39 @@ def get_credit_allocation_resources(credit_allocations, resource_classes):
return resource_allocations


def get_resource_class(resource_class_name):
try:
resource_class = models.ResourceClass.objects.get(name=resource_class_name)
except models.ResourceClass.DoesNotExist:
raise db_exceptions.NoResourceClass(
f"Resource class '{resource_class_name}' does not exist."
)
return resource_class


def get_valid_allocations(inventories):
"""Validates a dictionary of resource allocations.
Returns a list of dictionaries of the form:
[
{"VCPU": "resource_hours"},
{"MEMORY_MB": "resource_hours"},
...
]
"""
try:
allocations = {}
for resource_class_name, resource_hours in inventories.data.items():
resource_class = get_resource_class(resource_class_name)
allocations[resource_class] = float(resource_hours)
except ValueError:
raise db_exceptions.ResourceRequestFormatError(
f"Invalid value for resource hours: '{resource_hours}'"
)
return allocations


def get_resource_requests(lease, current_resource_requests=None):
"""Returns a dictionary of the form:
Expand Down Expand Up @@ -177,3 +221,30 @@ def spend_credits(
- resource_requests[resource_class]
)
credit_allocations[resource_class].save()


def create_credit_resource_allocations(credit_allocation, resource_allocations):
"""Allocates resource credits to a given credit allocation.
Returns a list of new/updated CreditAllocationResources:
[
CreditAllocationResource
]
"""
cars = []
for resource_class, resource_hours in resource_allocations.items():
# TODO(tyler) logging create or update?
car, created = models.CreditAllocationResource.objects.get_or_create(
allocation=credit_allocation,
resource_class=resource_class,
defaults={"resource_hours": resource_hours},
)
# If exists, update:
if not created:
car.resource_hours += resource_hours
car.save()

# Refresh from db to get the updated resource_hours
car.refresh_from_db()
cars.append(car)
return cars
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.6 on 2024-07-24 17:44

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("api", "0005_consumer_consumer_uuid_consumer_user_ref"),
]

operations = [
migrations.AlterUniqueTogether(
name="consumer",
unique_together={("consumer_uuid", "resource_provider_account")},
),
]
2 changes: 1 addition & 1 deletion coral_credits/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class Meta:
# )
# ]
unique_together = (
"consumer_ref",
"consumer_uuid",
"resource_provider_account",
)

Expand Down
36 changes: 22 additions & 14 deletions coral_credits/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,53 @@
class ResourceClassSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = models.ResourceClass
fields = ["url", "name", "created"]
fields = ["id", "url", "name", "created"]


class ResourceProviderSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = models.ResourceProvider
fields = ["url", "name", "created", "email", "info_url"]
fields = ["id", "url", "name", "created", "email", "info_url"]


class CreditAccountSerializer(serializers.ModelSerializer):
class ResourceProviderAccountSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = models.CreditAccount
fields = ["url", "name", "email", "created"]
model = models.ResourceProviderAccount
fields = ["id", "url", "account", "provider", "project_id"]


class ResourceClass(serializers.ModelSerializer):
class CreditAccountSerializer(serializers.ModelSerializer):
class Meta:
model = models.ResourceClass
fields = ["name"]
model = models.CreditAccount
fields = ["id", "url", "name", "email", "created"]


class CreditAllocationResource(serializers.ModelSerializer):
resource_class = ResourceClass()
class CreditAllocationResourceSerializer(serializers.ModelSerializer):
resource_class = ResourceClassSerializer()
resource_hours = serializers.FloatField()

class Meta:
model = models.CreditAllocationResource
fields = ["resource_class", "resource_hours"]

def to_representation(self, instance):
"""Pass the context to the ResourceClassSerializer"""
representation = super().to_representation(instance)
resource_class_serializer = ResourceClassSerializer(
instance.resource_class, context=self.context
)
representation["resource_class"] = resource_class_serializer.data
return representation

class CreditAllocation(serializers.ModelSerializer):
resources = CreditAllocationResource(many=True)

class CreditAllocationSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = models.CreditAllocation
fields = ["name", "start", "end", "resources"]
fields = ["id", "name", "created", "account", "start", "end"]


class ResourceConsumptionRecord(serializers.ModelSerializer):
resource_class = ResourceClass()
resource_class = ResourceClassSerializer()

class Meta:
model = models.ResourceConsumptionRecord
Expand Down
43 changes: 43 additions & 0 deletions coral_credits/api/tests/allocation_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from django.urls import reverse
import pytest
from rest_framework import status

import coral_credits.api.models as models


@pytest.fixture
def request_data():
return {
"inventories": {"VCPU": 50, "MEMORY_MB": 2000, "DISK_GB": 1000},
}


@pytest.mark.django_db
def test_credit_allocation_resource_create_success(
credit_allocation,
resource_classes,
api_client,
request_data,
):

# Prepare data for the API call
url = reverse(
"allocation-resource-list", kwargs={"allocation_pk": credit_allocation.id}
)

# Make the API call
response = api_client.post(url, request_data, format="json")

# Check that the request was successful
assert response.status_code == status.HTTP_200_OK, (
f"Expected {status.HTTP_200_OK}. "
f"Actual status {response.status_code}. "
f"Response text {response.content}"
)

# Check database entries are as expected
# First check we have the number that we expect
total_cars = models.CreditAllocationResource.objects.filter(
allocation=credit_allocation
)
assert len(total_cars) == len(request_data["inventories"])
86 changes: 86 additions & 0 deletions coral_credits/api/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from datetime import datetime, timedelta

from django.utils.timezone import make_aware
import pytest
from rest_framework.test import APIClient

import coral_credits.api.models as models


def pytest_configure(config):
config.PROJECT_ID = "20354d7a-e4fe-47af-8ff6-187bca92f3f9"
config.USER_REF = "caa8b54a-eb5e-4134-8ae2-a3946a428ec7"
config.START_DATE = make_aware(datetime.now())
config.END_DATE = config.START_DATE + timedelta(days=1)


# Fixtures defining all the necessary database entries for testing
@pytest.fixture
def resource_classes():
vcpu = models.ResourceClass.objects.create(name="VCPU")
memory = models.ResourceClass.objects.create(name="MEMORY_MB")
disk = models.ResourceClass.objects.create(name="DISK_GB")
return vcpu, memory, disk


@pytest.fixture
def provider():
return models.ResourceProvider.objects.create(
name="Test Provider",
email="[email protected]",
info_url="https://testprovider.com",
)


@pytest.fixture
def account():
return models.CreditAccount.objects.create(email="[email protected]", name="test")


@pytest.fixture
def resource_provider_account(request, account, provider):
return models.ResourceProviderAccount.objects.create(
account=account, provider=provider, project_id=request.config.PROJECT_ID
)


@pytest.fixture
def credit_allocation(account, request):
return models.CreditAllocation.objects.create(
account=account,
name="test",
start=request.config.START_DATE,
end=request.config.END_DATE,
)


@pytest.fixture
def api_client():
return APIClient()


@pytest.fixture
def create_credit_allocation_resources():
# Factory fixture
def _create_credit_allocation_resources(
credit_allocation, resource_classes, allocation_hours
):
vcpu, memory, disk = resource_classes
vcpu_allocation = models.CreditAllocationResource.objects.create(
allocation=credit_allocation,
resource_class=vcpu,
resource_hours=allocation_hours["vcpu"],
)
memory_allocation = models.CreditAllocationResource.objects.create(
allocation=credit_allocation,
resource_class=memory,
resource_hours=allocation_hours["memory"],
)
disk_allocation = models.CreditAllocationResource.objects.create(
allocation=credit_allocation,
resource_class=disk,
resource_hours=allocation_hours["disk"],
)
return (vcpu_allocation, memory_allocation, disk_allocation)

return _create_credit_allocation_resources
Loading

0 comments on commit a6e09a7

Please sign in to comment.