Skip to content

Commit 717e740

Browse files
committed
Block deletions of resources with active consumers + tests
1 parent da0ff94 commit 717e740

File tree

5 files changed

+158
-19
lines changed

5 files changed

+158
-19
lines changed

coral_credits/api/views.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
1515

1616
LOG = logging.getLogger(__name__)
1717

18+
def destroy_if_no_active_consumers(linked_consumers_queryset, request, destroy_super):
19+
20+
current_time = make_aware(datetime.now())
21+
for consumer in linked_consumers_queryset:
22+
if current_time < consumer.end:
23+
return _http_403_forbidden(repr(db_exceptions.ActiveConsumersInAllocation))
24+
else:
25+
return destroy_super.destroy(request)
1826

1927
class CreditAllocationViewSet(viewsets.ModelViewSet):
2028
queryset = models.CreditAllocation.objects.all()
@@ -23,15 +31,10 @@ class CreditAllocationViewSet(viewsets.ModelViewSet):
2331

2432
def destroy(self, request, pk=None):
2533
allocation = get_object_or_404(self.queryset, pk=pk)
26-
active_consumers = models.Consumer.objects.filter(
34+
linked_consumers = models.Consumer.objects.filter(
2735
resource_provider_account__account__pk=allocation.account.pk
2836
)
29-
current_time = make_aware(datetime.now())
30-
for consumer in active_consumers:
31-
if current_time < consumer.end:
32-
return _http_403_forbidden(repr(db_exceptions.ActiveConsumersInAllocation))
33-
else:
34-
return super().destroy(request)
37+
return destroy_if_no_active_consumers(linked_consumers, request, super())
3538

3639

3740
class CreditAllocationResourceViewSet(viewsets.ModelViewSet):
@@ -88,6 +91,13 @@ def create(self, request, allocation_pk=None):
8891

8992
def update(self, request, allocation_pk=None, pk=None):
9093
return self._create_update_credit_allocations(request, allocation_pk)
94+
95+
def destroy(self, request, allocation_pk=None, pk=None):
96+
resource = get_object_or_404(self.get_queryset(), pk=pk)
97+
linked_consumers = models.Consumer.objects.filter(
98+
resource_provider_account__account__pk=resource.allocation.account.pk
99+
)
100+
return destroy_if_no_active_consumers(linked_consumers, request, super())
91101

92102
def _validate_request(self, request):
93103

@@ -111,12 +121,25 @@ class ResourceProviderViewSet(viewsets.ModelViewSet):
111121
serializer_class = serializers.ResourceProviderSerializer
112122
permission_classes = [permissions.IsAuthenticated]
113123

124+
def destroy(self, request, pk=None):
125+
rpa = get_object_or_404(self.queryset, pk=pk)
126+
linked_consumers = models.Consumer.objects.filter(
127+
resource_provider_account__pk=rpa.pk
128+
)
129+
return destroy_if_no_active_consumers(linked_consumers, request, super())
114130

115131
class ResourceProviderAccountViewSet(viewsets.ModelViewSet):
116132
queryset = models.ResourceProviderAccount.objects.all()
117133
serializer_class = serializers.ResourceProviderAccountSerializer
118134
permission_classes = [permissions.IsAuthenticated]
119135

136+
def destroy(self, request, pk=None):
137+
provider = get_object_or_404(self.queryset, pk=pk)
138+
linked_consumers = models.Consumer.objects.filter(
139+
resource_provider_account__provider__pk=provider.pk
140+
)
141+
return destroy_if_no_active_consumers(linked_consumers, request, super())
142+
120143

121144
class AccountViewSet(viewsets.ModelViewSet):
122145
queryset = models.CreditAccount.objects.all()
@@ -183,6 +206,13 @@ def retrieve(self, request, pk=None):
183206
)
184207

185208
return Response(account_summary)
209+
210+
def destroy(self, request, pk=None):
211+
account = get_object_or_404(self.queryset, pk=pk)
212+
linked_consumers = models.Consumer.objects.filter(
213+
resource_provider_account__account__pk=account.pk
214+
)
215+
return destroy_if_no_active_consumers(linked_consumers, request, super())
186216

187217

188218
class ConsumerViewSet(viewsets.ModelViewSet):

tofu/.gitignore

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@ tests/__pycache__/**
1111
crash.log
1212
crash.*.log
1313

14-
# Exclude all .tfvars files, which are likely to contain sensitive data, such as
15-
# password, private keys, and other secrets. These should not be part of version
16-
# control as they are data points which are potentially sensitive and subject
17-
# to change depending on the environment.
18-
*.tfvars
19-
*.tfvars.json
20-
2114
# Ignore override files as they are usually used to override resources locally and so
2215
# are not checked in
2316
override.tf
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
resource_provider_name = "Test Provider"
2+
resource_provider_email = "[email protected]"
3+
resource_provider_info_url = "https://www.google.com"
4+
5+
accounts = [
6+
{
7+
name = "TestAccount1"
8+
9+
openstack_project_id = "c2eced313b324cdb8e670e6e30bf387d"
10+
},
11+
{
12+
name = "TestAccount2"
13+
14+
openstack_project_id = "2fbf511968aa443e883a82283b0f0160"
15+
}
16+
]
17+
18+
allocations = {
19+
Q2 = {
20+
start_date = "2026-01-01-12:00:00"
21+
end_date = "2026-04-01-12:00:00"
22+
projects = [
23+
{
24+
account_email = "[email protected]"
25+
resources = {
26+
VCPU = 80000
27+
MEMORY_MB = 8000000
28+
DISK_GB = 300000
29+
}
30+
}
31+
]
32+
}
33+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# coral_uri = "http://credits.apps.<azimuth-domain>"
2+
# auth_token = Get bearer token with `curl -X POST -H "Content-Type: application/json" -d \
3+
# "{
4+
# \"username\": \"admin\",
5+
# \"password\": \"$TEST_PASSWORD\"
6+
# }" \ http://credits.apps.<azimuth-domain>/api_auth_token/
7+
8+
resource_provider_name = "Test Provider"
9+
resource_provider_email = "[email protected]"
10+
resource_provider_info_url = "https://www.google.com"
11+
12+
accounts = [
13+
{
14+
name = "TestAccount1"
15+
16+
openstack_project_id = "c2eced313b324cdb8e670e6e30bf387d"
17+
},
18+
{
19+
name = "TestAccount2"
20+
21+
openstack_project_id = "2fbf511968aa443e883a82283b0f0160"
22+
}
23+
]
24+
25+
allocations = {
26+
Q1 = {
27+
start_date = "2025-09-01-12:00:00"
28+
end_date = "2025-12-01-12:00:00"
29+
projects = [
30+
{
31+
account_email = "[email protected]"
32+
resources = {
33+
VCPU = 40000
34+
MEMORY_MB = 4423680
35+
DISK_GB = 108000
36+
}
37+
},
38+
{
39+
account_email = "[email protected]"
40+
resources = {
41+
VCPU = 20000
42+
MEMORY_MB = 2000000
43+
DISK_GB = 200000
44+
}
45+
}
46+
]
47+
}
48+
Q2 = {
49+
start_date = "2026-01-01-12:00:00"
50+
end_date = "2026-04-01-12:00:00"
51+
projects = [
52+
{
53+
account_email = "[email protected]"
54+
resources = {
55+
VCPU = 80000
56+
MEMORY_MB = 8000000
57+
DISK_GB = 300000
58+
}
59+
}
60+
]
61+
}
62+
}

tofu/tests/tofu_tests.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@ def get_lease_request_json():
4545
@pytest.fixture(scope="session")
4646
def terraform_rest_setup():
4747
working_dir = os.path.join(os.path.dirname(__file__), "..")
48-
var_file = os.path.join(working_dir, "example-config.tfvars")
48+
var_file = os.path.join(working_dir, "tests", "tofu_configs", "initial.tfvars")
4949

5050
tf = Tofu(cwd=working_dir)
5151
tf.init()
5252
tf.apply(extra_args=["--var-file="+var_file])
5353

54-
yield
54+
yield tf
5555

5656
destroy = tf.apply(extra_args=["--var-file="+var_file],destroy=True)
5757
assert len(destroy.errors) == 0
@@ -66,15 +66,20 @@ def add_consumer_request(terraform_rest_setup):
6666
},
6767
json=lease_request_json
6868
)
69-
yield consumer.status_code
69+
yield dict(status = consumer.status_code, tf_workspace = terraform_rest_setup)
7070
requests.post(coral_uri+"/consumer/on-end",headers={
7171
"Authorization": "Bearer "+os.environ.get("TF_VAR_auth_token"),
7272
"Content-Type": "application/json"
7373
},
7474
json=lease_request_json
7575
)
7676

77-
# def test_allocation_resources_still_consumed_by_deleted_consumers
77+
@pytest.fixture(scope="session")
78+
def try_delete_active_allocation(add_consumer_request):
79+
delete_file = os.path.join(os.path.dirname(__file__), "..", "tests", "tofu_configs", "delete-active.tfvars")
80+
try_delete = add_consumer_request["tf_workspace"].apply(extra_args=["--var-file="+delete_file])
81+
return len(try_delete.errors)
82+
7883

7984
def api_get_request(resource):
8085
return requests.get(coral_uri+"/"+resource,headers=headers).json()
@@ -156,8 +161,24 @@ def test_resource_allocations_created(terraform_rest_setup):
156161
assert allocation_resources["Q1-1"] == {"VCPU": 20000, "MEMORY_MB": 2000000, "DISK_GB": 200000}
157162
assert allocation_resources["Q2-0"] == {"VCPU": 80000, "MEMORY_MB": 8000000, "DISK_GB": 300000}
158163

164+
def test_resources_consumed_by_consumers(add_consumer_request):
165+
raise NotImplementedError()
166+
167+
def test_resources_still_consumed_after_consumer_delete():
168+
raise NotImplementedError()
169+
159170
def test_consumer_added_or_exists(add_consumer_request):
160-
assert add_consumer_request == 204
171+
assert add_consumer_request["status"] == 204
161172

162173
def test_can_query_consumer(add_consumer_request):
163174
assert len(api_get_request("consumer")) == 1
175+
176+
def test_delete_allocation_with_consumer_forbidden(try_delete_active_allocation):
177+
assert try_delete_active_allocation > 0
178+
179+
def test_delete_active_allocation_resources_fails(try_delete_active_allocation):
180+
allocations = api_get_request("allocation")
181+
q1_allocations = [a for a in allocations if a["name"][:2] == "Q1"]
182+
for alloc in q1_allocations:
183+
tst = api_get_request("allocation/"+str(alloc["id"])+"/resources")
184+
assert len(tst) == 3

0 commit comments

Comments
 (0)