Skip to content

Commit 5d0d1e9

Browse files
authored
Merge pull request #205 from QuanMPhm/187/acct_quota
Allow direct communication to Openshift Quota API
2 parents fcebc93 + 87ad16a commit 5d0d1e9

File tree

9 files changed

+270
-62
lines changed

9 files changed

+270
-62
lines changed

ci/run_functional_tests_openshift.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ fi
1717
export DJANGO_SETTINGS_MODULE="local_settings"
1818
export FUNCTIONAL_TESTS="True"
1919
export OS_AUTH_URL="https://onboarding-onboarding.cluster.local"
20+
export OS_API_URL="https://onboarding-onboarding.cluster.local:6443"
21+
2022

2123
coverage run --source="." -m django test coldfront_plugin_cloud.tests.functional.openshift
2224
coverage report

src/coldfront_plugin_cloud/attributes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class CloudAllocationAttribute:
1919

2020

2121
RESOURCE_AUTH_URL = 'Identity Endpoint URL'
22+
RESOURCE_API_URL = 'OpenShift API Endpoint URL'
2223
RESOURCE_IDENTITY_NAME = 'OpenShift Identity Provider Name'
2324
RESOURCE_ROLE = 'Role for User in Project'
2425

@@ -33,6 +34,7 @@ class CloudAllocationAttribute:
3334

3435
RESOURCE_ATTRIBUTES = [
3536
CloudResourceAttribute(name=RESOURCE_AUTH_URL),
37+
CloudResourceAttribute(name=RESOURCE_API_URL),
3638
CloudResourceAttribute(name=RESOURCE_IDENTITY_NAME),
3739
CloudResourceAttribute(name=RESOURCE_FEDERATION_PROTOCOL),
3840
CloudResourceAttribute(name=RESOURCE_IDP),

src/coldfront_plugin_cloud/management/commands/add_openshift_resource.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ def add_arguments(self, parser):
1717
help='Name of OpenShift resource')
1818
parser.add_argument('--auth-url', type=str, required=True,
1919
help='URL of the openshift-acct-mgt endpoint')
20+
parser.add_argument('--api-url', type=str, required=True,
21+
help='API URL of the openshift cluster')
22+
parser.add_argument('--idp', type=str, required=True,
23+
help='Name of Openshift identity provider')
2024
parser.add_argument('--role', type=str, default='edit',
2125
help='Role for user when added to project (default: edit)')
2226

@@ -37,6 +41,18 @@ def handle(self, *args, **options):
3741
resource=openshift,
3842
value=options['auth_url']
3943
)
44+
ResourceAttribute.objects.get_or_create(
45+
resource_attribute_type=ResourceAttributeType.objects.get(
46+
name=attributes.RESOURCE_API_URL),
47+
resource=openshift,
48+
value=options['api_url']
49+
)
50+
ResourceAttribute.objects.get_or_create(
51+
resource_attribute_type=ResourceAttributeType.objects.get(
52+
name=attributes.RESOURCE_IDENTITY_NAME),
53+
resource=openshift,
54+
value=options['idp']
55+
)
4056
ResourceAttribute.objects.get_or_create(
4157
resource_attribute_type=ResourceAttributeType.objects.get(
4258
name=attributes.RESOURCE_ROLE),

src/coldfront_plugin_cloud/management/commands/validate_allocations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def handle(self, *args, **options):
183183
)
184184
continue
185185

186-
quota = allocator.get_quota(project_id)["Quota"]
186+
quota = allocator.get_quota(project_id)
187187

188188
failed_validation = Command.sync_users(project_id, allocation, allocator, options["apply"])
189189

src/coldfront_plugin_cloud/openshift.py

Lines changed: 102 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ def clean_openshift_metadata(obj):
3636
return obj
3737

3838
QUOTA_KEY_MAPPING = {
39-
attributes.QUOTA_LIMITS_CPU: lambda x: {":limits.cpu": f"{x * 1000}m"},
40-
attributes.QUOTA_LIMITS_MEMORY: lambda x: {":limits.memory": f"{x}Mi"},
41-
attributes.QUOTA_LIMITS_EPHEMERAL_STORAGE_GB: lambda x: {":limits.ephemeral-storage": f"{x}Gi"},
42-
attributes.QUOTA_REQUESTS_STORAGE: lambda x: {":requests.storage": f"{x}Gi"},
43-
attributes.QUOTA_REQUESTS_GPU: lambda x: {":requests.nvidia.com/gpu": f"{x}"},
44-
attributes.QUOTA_PVC: lambda x: {":persistentvolumeclaims": f"{x}"},
39+
attributes.QUOTA_LIMITS_CPU: lambda x: {"limits.cpu": f"{x * 1000}m"},
40+
attributes.QUOTA_LIMITS_MEMORY: lambda x: {"limits.memory": f"{x}Mi"},
41+
attributes.QUOTA_LIMITS_EPHEMERAL_STORAGE_GB: lambda x: {"limits.ephemeral-storage": f"{x}Gi"},
42+
attributes.QUOTA_REQUESTS_STORAGE: lambda x: {"requests.storage": f"{x}Gi"},
43+
attributes.QUOTA_REQUESTS_GPU: lambda x: {"requests.nvidia.com/gpu": f"{x}"},
44+
attributes.QUOTA_PVC: lambda x: {"persistentvolumeclaims": f"{x}"},
4545
}
4646

4747

@@ -77,7 +77,7 @@ def __init__(self, resource, allocation):
7777
def k8_client(self):
7878
# Load Endpoint URL and Auth token for new k8 client
7979
openshift_token = os.getenv(f"OPENSHIFT_{self.safe_resource_name}_TOKEN")
80-
openshift_url = self.resource.get_attribute(attributes.RESOURCE_AUTH_URL)
80+
openshift_url = self.resource.get_attribute(attributes.RESOURCE_API_URL)
8181

8282
k8_config = kubernetes.client.Configuration()
8383
k8_config.api_key["authorization"] = openshift_token
@@ -146,20 +146,41 @@ def create_project(self, suggested_project_name):
146146
project_name = project_id
147147
self._create_project(project_name, project_id)
148148
return self.Project(project_name, project_id)
149+
150+
def delete_moc_quotas(self, project_id):
151+
"""deletes all resourcequotas from an openshift project"""
152+
resourcequotas = self._openshift_get_resourcequotas(project_id)
153+
for resourcequota in resourcequotas:
154+
self._openshift_delete_resourcequota(project_id, resourcequota["metadata"]["name"])
155+
156+
logger.info(f"All quotas for {project_id} successfully deleted")
149157

150158
def set_quota(self, project_id):
151-
url = f"{self.auth_url}/projects/{project_id}/quota"
152-
payload = dict()
159+
"""Sets the quota for a project, creating a minimal resourcequota
160+
object in the project namespace with no extra scopes"""
161+
162+
quota_spec = {}
153163
for key, func in QUOTA_KEY_MAPPING.items():
154164
if (x := self.allocation.get_attribute(key)) is not None:
155-
payload.update(func(x))
156-
r = self.session.put(url, data=json.dumps({'Quota': payload}))
157-
self.check_response(r)
165+
quota_spec.update(func(x))
166+
167+
quota_def = {
168+
"metadata": {"name": f"{project_id}-project"},
169+
"spec": {"hard": quota_spec},
170+
}
171+
172+
self.delete_moc_quotas(project_id)
173+
self._openshift_create_resourcequota(project_id, quota_def)
174+
175+
logger.info(f"Quota for {project_id} successfully created")
158176

159177
def get_quota(self, project_id):
160-
url = f"{self.auth_url}/projects/{project_id}/quota"
161-
r = self.session.get(url)
162-
return self.check_response(r)
178+
cloud_quotas = self._openshift_get_resourcequotas(project_id)
179+
combined_quota = {}
180+
for cloud_quota in cloud_quotas:
181+
combined_quota.update(cloud_quota["spec"]["hard"])
182+
183+
return combined_quota
163184

164185
def create_project_defaults(self, project_id):
165186
pass
@@ -181,7 +202,7 @@ def reactivate_project(self, project_id):
181202
def get_federated_user(self, username):
182203
if (
183204
self._openshift_user_exists(username)
184-
and self._openshift_get_identity(username)
205+
and self._openshift_identity_exists(username)
185206
and self._openshift_useridentitymapping_exists(username, username)
186207
):
187208
return {'username': username}
@@ -264,25 +285,84 @@ def _openshift_get_identity(self, id_user):
264285
def _openshift_user_exists(self, user_name):
265286
try:
266287
self._openshift_get_user(user_name)
267-
except kexc.NotFoundError:
268-
return False
288+
except kexc.NotFoundError as e:
289+
# Ensures error raise because resource not found,
290+
# not because of other reasons, like incorrect url
291+
e_info = json.loads(e.body)
292+
if (
293+
e_info["reason"] == "NotFound"
294+
and e_info["details"]["name"] == user_name
295+
):
296+
return False
297+
raise e
269298
return True
270299

271300
def _openshift_identity_exists(self, id_user):
272301
try:
273302
self._openshift_get_identity(id_user)
274-
except kexc.NotFoundError:
275-
return False
303+
except kexc.NotFoundError as e:
304+
e_info = json.loads(e.body)
305+
if e_info.get("reason") == "NotFound":
306+
return False
307+
raise e
276308
return True
277309

278310
def _openshift_useridentitymapping_exists(self, user_name, id_user):
279311
try:
280312
user = self._openshift_get_user(user_name)
281-
except kexc.NotFoundError:
282-
return False
313+
except kexc.NotFoundError as e:
314+
e_info = json.loads(e.body)
315+
if e_info.get("reason") == "NotFound":
316+
return False
317+
raise e
283318

284319
return any(
285320
identity == self.qualified_id_user(id_user)
286321
for identity in user.get("identities", [])
287322
)
288323

324+
def _openshift_get_project(self, project_name):
325+
api = self.get_resource_api(API_PROJECT, "Project")
326+
return clean_openshift_metadata(api.get(name=project_name).to_dict())
327+
328+
def _openshift_get_resourcequotas(self, project_id):
329+
"""Returns a list of resourcequota objects in namespace with name `project_id`"""
330+
# Raise a NotFound error if the project doesn't exist
331+
self._openshift_get_project(project_id)
332+
api = self.get_resource_api(API_CORE, "ResourceQuota")
333+
res = clean_openshift_metadata(api.get(namespace=project_id).to_dict())
334+
335+
return res["items"]
336+
337+
def _wait_for_quota_to_settle(self, project_id, resource_quota):
338+
"""Wait for quota on resourcequotas to settle.
339+
340+
When creating a new resourcequota that sets a quota on resourcequota objects, we need to
341+
wait for OpenShift to calculate the quota usage before we attempt to create any new
342+
resourcequota objects.
343+
"""
344+
345+
if "resourcequotas" in resource_quota["spec"]["hard"]:
346+
logger.info("waiting for resourcequota quota")
347+
348+
api = self.get_resource_api(API_CORE, "ResourceQuota")
349+
while True:
350+
resp = clean_openshift_metadata(
351+
api.get(
352+
namespace=project_id, name=resource_quota["metadata"]["name"]
353+
).to_dict()
354+
)
355+
if "resourcequotas" in resp["status"].get("used", {}):
356+
break
357+
time.sleep(0.1)
358+
359+
def _openshift_create_resourcequota(self, project_id, quota_def):
360+
api = self.get_resource_api(API_CORE, "ResourceQuota")
361+
res = api.create(namespace=project_id, body=quota_def).to_dict()
362+
self._wait_for_quota_to_settle(project_id, res)
363+
364+
def _openshift_delete_resourcequota(self, project_id, resourcequota_name):
365+
"""In an openshift namespace {project_id) delete a specified resourcequota"""
366+
api = self.get_resource_api(API_CORE, "ResourceQuota")
367+
return api.delete(namespace=project_id, name=resourcequota_name).to_dict()
368+

src/coldfront_plugin_cloud/tests/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,15 @@ def new_openstack_resource(name=None, auth_url=None) -> Resource:
8181
return Resource.objects.get(name=resource_name)
8282

8383
@staticmethod
84-
def new_openshift_resource(name=None, auth_url=None) -> Resource:
84+
def new_openshift_resource(name=None, auth_url=None, api_url=None, idp=None) -> Resource:
8585
resource_name = name or uuid.uuid4().hex
8686

8787
call_command(
8888
'add_openshift_resource',
8989
name=resource_name,
9090
auth_url=auth_url or 'https://onboarding-onboarding.cluster.local',
91+
api_url=api_url or 'https://onboarding-onboarding.cluster.local:6443',
92+
idp=idp or 'developer',
9193
)
9294
return Resource.objects.get(name=resource_name)
9395

src/coldfront_plugin_cloud/tests/functional/openshift/test_allocation.py

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ def setUp(self) -> None:
1515
super().setUp()
1616
self.resource = self.new_openshift_resource(
1717
name='Microshift',
18-
auth_url=os.getenv('OS_AUTH_URL')
18+
auth_url=os.getenv('OS_AUTH_URL'),
19+
api_url=os.getenv('OS_API_URL'),
1920
)
2021

2122
def test_new_allocation(self):
@@ -36,7 +37,8 @@ def test_new_allocation(self):
3637
allocator._get_project(project_id)
3738

3839
# Check user and roles
39-
allocator.get_federated_user(user.username)
40+
user_info = allocator.get_federated_user(user.username)
41+
self.assertEqual(user_info, {'username': user.username})
4042

4143
allocator._get_role(user.username, project_id)
4244

@@ -73,7 +75,8 @@ def test_add_remove_user(self):
7375
tasks.add_user_to_allocation(allocation_user2.pk)
7476
allocator._get_role(user.username, project_id)
7577

76-
allocator.get_federated_user(user2.username)
78+
user_info = allocator.get_federated_user(user.username)
79+
self.assertEqual(user_info, {'username': user.username})
7780

7881
allocator._get_role(user.username, project_id)
7982
allocator._get_role(user2.username, project_id)
@@ -123,17 +126,16 @@ def test_new_allocation_quota(self):
123126
self.assertEqual(allocation.get_attribute(attributes.QUOTA_REQUESTS_GPU), 2 * 0)
124127
self.assertEqual(allocation.get_attribute(attributes.QUOTA_PVC), 2 * 2)
125128

126-
quota = allocator.get_quota(project_id)['Quota']
127-
quota = {k: v for k, v in quota.items() if v is not None}
129+
quota = allocator.get_quota(project_id)
128130
# The return value will update to the most relevant unit, so
129131
# 2000m cores becomes 2 and 8192Mi becomes 8Gi
130132
self.assertEqual(quota, {
131-
":limits.cpu": "2",
132-
":limits.memory": "8Gi",
133-
":limits.ephemeral-storage": "10Gi",
134-
":requests.storage": "40Gi",
135-
":requests.nvidia.com/gpu": "0",
136-
":persistentvolumeclaims": "4",
133+
"limits.cpu": "2",
134+
"limits.memory": "8Gi",
135+
"limits.ephemeral-storage": "10Gi",
136+
"requests.storage": "40Gi",
137+
"requests.nvidia.com/gpu": "0",
138+
"persistentvolumeclaims": "4",
137139
})
138140

139141
# change a bunch of attributes
@@ -154,16 +156,16 @@ def test_new_allocation_quota(self):
154156
# This call should update the openshift quota to match the current attributes
155157
call_command('validate_allocations', apply=True)
156158

157-
quota = allocator.get_quota(project_id)['Quota']
159+
quota = allocator.get_quota(project_id)
158160
quota = {k: v for k, v in quota.items() if v is not None}
159161

160162
self.assertEqual(quota, {
161-
":limits.cpu": "6",
162-
":limits.memory": "8Gi",
163-
":limits.ephemeral-storage": "50Gi",
164-
":requests.storage": "100Gi",
165-
":requests.nvidia.com/gpu": "1",
166-
":persistentvolumeclaims": "10",
163+
"limits.cpu": "6",
164+
"limits.memory": "8Gi",
165+
"limits.ephemeral-storage": "50Gi",
166+
"requests.storage": "100Gi",
167+
"requests.nvidia.com/gpu": "1",
168+
"persistentvolumeclaims": "10",
167169
})
168170

169171
def test_reactivate_allocation(self):
@@ -180,19 +182,17 @@ def test_reactivate_allocation(self):
180182

181183
self.assertEqual(allocation.get_attribute(attributes.QUOTA_LIMITS_CPU), 2)
182184

183-
quota = allocator.get_quota(project_id)['Quota']
185+
quota = allocator.get_quota(project_id)
184186

185-
# https://github.com/CCI-MOC/openshift-acct-mgt
186-
quota = {k: v for k, v in quota.items() if v is not None}
187187
# The return value will update to the most relevant unit, so
188188
# 2000m cores becomes 2 and 8192Mi becomes 8Gi
189189
self.assertEqual(quota, {
190-
":limits.cpu": "2",
191-
":limits.memory": "8Gi",
192-
":limits.ephemeral-storage": "10Gi",
193-
":requests.storage": "40Gi",
194-
":requests.nvidia.com/gpu": "0",
195-
":persistentvolumeclaims": "4",
190+
"limits.cpu": "2",
191+
"limits.memory": "8Gi",
192+
"limits.ephemeral-storage": "10Gi",
193+
"requests.storage": "40Gi",
194+
"requests.nvidia.com/gpu": "0",
195+
"persistentvolumeclaims": "4",
196196
})
197197

198198
# Simulate an attribute change request and subsequent approval which
@@ -201,17 +201,16 @@ def test_reactivate_allocation(self):
201201
tasks.activate_allocation(allocation.pk)
202202
allocation.refresh_from_db()
203203

204-
quota = allocator.get_quota(project_id)['Quota']
205-
quota = {k: v for k, v in quota.items() if v is not None}
204+
quota = allocator.get_quota(project_id)
206205
# The return value will update to the most relevant unit, so
207206
# 3000m cores becomes 3 and 8192Mi becomes 8Gi
208207
self.assertEqual(quota, {
209-
":limits.cpu": "3",
210-
":limits.memory": "8Gi",
211-
":limits.ephemeral-storage": "10Gi",
212-
":requests.storage": "40Gi",
213-
":requests.nvidia.com/gpu": "0",
214-
":persistentvolumeclaims": "4",
208+
"limits.cpu": "3",
209+
"limits.memory": "8Gi",
210+
"limits.ephemeral-storage": "10Gi",
211+
"requests.storage": "40Gi",
212+
"requests.nvidia.com/gpu": "0",
213+
"persistentvolumeclaims": "4",
215214
})
216215

217216
allocator._get_role(user.username, project_id)

0 commit comments

Comments
 (0)