Skip to content

Commit 73b751b

Browse files
committed
llow direct communication to Openshift API through get_federated_user
As the first step towards merging the account manager into our coldfront cloud plugin, the `get_federated_user` function in the Openshift allocator will now (through several functions) directly call the Openshift API. Much of the functions added are copied from the `moc_openshift` module in the account manager. Aside from copying some functions, implementation of this feature also involved: - A new resource attribute `Identity Name` for the Openshift idp - A new unit test for the `get_federated_user` function - Changes to the CI file to enable these new unit tests
1 parent ce7c8e1 commit 73b751b

File tree

6 files changed

+147
-6
lines changed

6 files changed

+147
-6
lines changed

ci/run_functional_tests_openshift.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ set -xe
55

66
export OPENSHIFT_MICROSHIFT_USERNAME="admin"
77
export OPENSHIFT_MICROSHIFT_PASSWORD="pass"
8+
export OPENSHIFT_MICROSHIFT_TOKEN="$(oc create token -n onboarding onboarding-serviceaccount)"
89

910
if [[ ! "${CI}" == "true" ]]; then
1011
source /tmp/coldfront_venv/bin/activate

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
git+https://github.com/CCI-MOC/nerc-rates@74eb4a7#egg=nerc_rates
22
boto3
3+
kubernetes
34
coldfront >= 1.0.4
45
python-cinderclient # TODO: Set version for OpenStack Clients
56
python-keystoneclient

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ python_requires = >=3.8
2222
install_requires =
2323
nerc_rates @ git+https://github.com/CCI-MOC/nerc-rates@74eb4a7
2424
boto3
25+
kubernetes
2526
coldfront >= 1.0.4
2627
python-cinderclient
2728
python-keystoneclient

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_IDENTITY_NAME = 'Identity Name'
2223
RESOURCE_ROLE = 'Role for User in Project'
2324

2425
RESOURCE_FEDERATION_PROTOCOL = 'OpenStack Federation Protocol'
@@ -32,6 +33,7 @@ class CloudAllocationAttribute:
3233

3334
RESOURCE_ATTRIBUTES = [
3435
CloudResourceAttribute(name=RESOURCE_AUTH_URL),
36+
CloudResourceAttribute(name=RESOURCE_IDENTITY_NAME),
3537
CloudResourceAttribute(name=RESOURCE_FEDERATION_PROTOCOL),
3638
CloudResourceAttribute(name=RESOURCE_IDP),
3739
CloudResourceAttribute(name=RESOURCE_PROJECT_DOMAIN),

src/coldfront_plugin_cloud/openshift.py

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,29 @@
77
import time
88
from simplejson.errors import JSONDecodeError
99

10+
import kubernetes
11+
import kubernetes.dynamic.exceptions as kexc
12+
1013
from coldfront_plugin_cloud import attributes, base, utils
1114

15+
API_PROJECT = "project.openshift.io/v1"
16+
API_USER = "user.openshift.io/v1"
17+
API_RBAC = "rbac.authorization.k8s.io/v1"
18+
API_CORE = "v1"
19+
IGNORED_ATTRIBUTES = [
20+
"resourceVersion",
21+
"creationTimestamp",
22+
"uid",
23+
]
24+
25+
def clean_openshift_metadata(obj):
26+
if "metadata" in obj:
27+
for attr in IGNORED_ATTRIBUTES:
28+
if attr in obj["metadata"]:
29+
del obj["metadata"][attr]
30+
31+
return obj
32+
1233
QUOTA_KEY_MAPPING = {
1334
attributes.QUOTA_LIMITS_CPU: lambda x: {":limits.cpu": f"{x * 1000}m"},
1435
attributes.QUOTA_LIMITS_MEMORY: lambda x: {":limits.memory": f"{x}Mi"},
@@ -38,6 +59,41 @@ class OpenShiftResourceAllocator(base.ResourceAllocator):
3859

3960
project_name_max_length = 63
4061

62+
logger = logging.getLogger()
63+
64+
def __init__(self, resource, allocation):
65+
super().__init__(resource, allocation)
66+
self.safe_resource_name = utils.env_safe_name(resource.name)
67+
self.id_provider = resource.get_attribute(attributes.RESOURCE_IDENTITY_NAME)
68+
self.apis = {}
69+
70+
self.functional_tests = os.environ.get("FUNCTIONAL_TESTS", "").lower()
71+
self.verify = os.getenv(f"OPENSHIFT_{self.safe_resource_name}_VERIFY", "").lower()
72+
73+
if self.functional_tests == "true" or self.verify == "false":
74+
self.logger = logging.getLogger()
75+
else:
76+
self.logger = logging.getLogger("django")
77+
78+
@functools.cached_property
79+
def k8_client(self):
80+
# Load Endpoint URL and Auth token for new k8 client
81+
openshift_token = os.getenv(f"OPENSHIFT_{self.safe_resource_name}_TOKEN")
82+
openshift_url = self.resource.get_attribute(attributes.RESOURCE_AUTH_URL)
83+
84+
k8_config = kubernetes.client.Configuration()
85+
k8_config.api_key["authorization"] = openshift_token
86+
k8_config.api_key_prefix["authorization"] = "Bearer"
87+
k8_config.host = openshift_url
88+
89+
if self.functional_tests == "true" or self.verify == "false":
90+
k8_config.verify_ssl = False
91+
else:
92+
k8_config.verify_ssl = True
93+
94+
k8s_client = kubernetes.client.ApiClient(configuration=k8_config)
95+
return kubernetes.dynamic.DynamicClient(k8s_client)
96+
4197
@functools.cached_property
4298
def session(self):
4399
var_name = utils.env_safe_name(self.resource.name)
@@ -71,6 +127,18 @@ def check_response(response: requests.Response):
71127
raise Conflict(f"{response.status_code}: {response.text}")
72128
else:
73129
raise ApiException(f"{response.status_code}: {response.text}")
130+
131+
def qualified_id_user(self, id_user):
132+
return f"{self.id_provider}:{id_user}"
133+
134+
def get_resource_api(self, api_version: str, kind: str):
135+
"""Either return the cached resource api from self.apis, or fetch a
136+
new one, store it in self.apis, and return it."""
137+
k = f"{api_version}:{kind}"
138+
api = self.apis.setdefault(
139+
k, self.k8_client.resources.get(api_version=api_version, kind=kind)
140+
)
141+
return api
74142

75143
def create_project(self, suggested_project_name):
76144
sanitized_project_name = utils.get_sanitized_project_name(suggested_project_name)
@@ -113,13 +181,14 @@ def reactivate_project(self, project_id):
113181
pass
114182

115183
def get_federated_user(self, username):
116-
url = f"{self.auth_url}/users/{username}"
117-
try:
118-
r = self.session.get(url)
119-
self.check_response(r)
184+
if (
185+
self._openshift_user_exists(username)
186+
and self._openshift_get_identity(username)
187+
and self._openshift_useridentitymapping_exists(username, username)
188+
):
120189
return {'username': username}
121-
except NotFound:
122-
pass
190+
191+
self.logger.info("404: " + f"user ({username}) does not exist")
123192

124193
def create_federated_user(self, unique_id):
125194
url = f"{self.auth_url}/users/{unique_id}"
@@ -183,3 +252,39 @@ def get_users(self, project_id):
183252
url = f"{self.auth_url}/projects/{project_id}/users"
184253
r = self.session.get(url)
185254
return set(self.check_response(r))
255+
256+
def _openshift_get_user(self, username):
257+
api = self.get_resource_api(API_USER, "User")
258+
return clean_openshift_metadata(api.get(name=username).to_dict())
259+
260+
def _openshift_get_identity(self, id_user):
261+
api = self.get_resource_api(API_USER, "Identity")
262+
return clean_openshift_metadata(
263+
api.get(name=self.qualified_id_user(id_user)).to_dict()
264+
)
265+
266+
def _openshift_user_exists(self, user_name):
267+
try:
268+
self._openshift_get_user(user_name)
269+
except kexc.NotFoundError:
270+
return False
271+
return True
272+
273+
def _openshift_identity_exists(self, id_user):
274+
try:
275+
self._openshift_get_identity(id_user)
276+
except kexc.NotFoundError:
277+
return False
278+
return True
279+
280+
def _openshift_useridentitymapping_exists(self, user_name, id_user):
281+
try:
282+
user = self._openshift_get_user(user_name)
283+
except kexc.NotFoundError:
284+
return False
285+
286+
return any(
287+
identity == self.qualified_id_user(id_user)
288+
for identity in user.get("identities", [])
289+
)
290+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from unittest import mock
2+
3+
from coldfront_plugin_cloud.tests import base
4+
from coldfront_plugin_cloud.openshift import OpenShiftResourceAllocator
5+
6+
7+
class TestOpenshiftUser(base.TestBase):
8+
def setUp(self) -> None:
9+
mock_resource = mock.Mock()
10+
mock_allocation = mock.Mock()
11+
self.mock_openshift_allocator = OpenShiftResourceAllocator(mock_resource, mock_allocation)
12+
self.mock_openshift_allocator.id_provider = "fake_idp"
13+
self.mock_openshift_allocator.logger = mock.Mock()
14+
self.mock_openshift_allocator.k8_client = mock.Mock()
15+
16+
def test_get_federated_user(self):
17+
fake_user = mock.Mock(spec=["to_dict"])
18+
fake_user.to_dict.return_value = {"identities": ["fake_idp:fake_user"]}
19+
self.mock_openshift_allocator.k8_client.resources.get.return_value.get.return_value = fake_user
20+
21+
output = self.mock_openshift_allocator.get_federated_user("fake_user")
22+
self.assertEqual(output, {"username": "fake_user"})
23+
24+
def test_get_federated_user(self):
25+
fake_user = mock.Mock(spec=["to_dict"])
26+
fake_user.to_dict.return_value = {"identities": ["fake_idp:fake_user"]}
27+
self.mock_openshift_allocator.k8_client.resources.get.return_value.get.return_value = fake_user
28+
29+
output = self.mock_openshift_allocator.get_federated_user("fake_user_2")
30+
self.assertEqual(output, None)
31+
self.mock_openshift_allocator.logger.info.assert_called_with("404: user (fake_user_2) does not exist")

0 commit comments

Comments
 (0)