Skip to content

Commit 6752900

Browse files
committed
Allow 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 - A top-level logger for the Openshift allocator - New additions to the package requirements
1 parent c61fd6e commit 6752900

File tree

7 files changed

+146
-6
lines changed

7 files changed

+146
-6
lines changed

ci/run_functional_tests_openshift.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ set -xe
77

88
export OPENSHIFT_MICROSHIFT_USERNAME="admin"
99
export OPENSHIFT_MICROSHIFT_PASSWORD="pass"
10+
export OPENSHIFT_MICROSHIFT_TOKEN="$(oc create token -n onboarding onboarding-serviceaccount)"
11+
export OPENSHIFT_MICROSHIFT_VERIFY="false"
1012

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

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
git+https://github.com/CCI-MOC/nerc-rates@74eb4a7#egg=nerc_rates
22
boto3
3+
kubernetes
4+
openshift
35
coldfront >= 1.1.0
46
python-cinderclient # TODO: Set version for OpenStack Clients
57
python-keystoneclient

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ python_requires = >=3.8
2222
install_requires =
2323
nerc_rates @ git+https://github.com/CCI-MOC/nerc-rates@74eb4a7
2424
boto3
25+
kubernetes
26+
openshift
2527
coldfront >= 1.1.0
2628
python-cinderclient
2729
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 = 'OpenShift Identity Provider 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: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,34 @@
77
import time
88
from simplejson.errors import JSONDecodeError
99

10+
import kubernetes
11+
import kubernetes.dynamic.exceptions as kexc
12+
from openshift.dynamic import DynamicClient
13+
1014
from coldfront_plugin_cloud import attributes, base, utils
1115

16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
API_PROJECT = "project.openshift.io/v1"
21+
API_USER = "user.openshift.io/v1"
22+
API_RBAC = "rbac.authorization.k8s.io/v1"
23+
API_CORE = "v1"
24+
IGNORED_ATTRIBUTES = [
25+
"resourceVersion",
26+
"creationTimestamp",
27+
"uid",
28+
]
29+
30+
def clean_openshift_metadata(obj):
31+
if "metadata" in obj:
32+
for attr in IGNORED_ATTRIBUTES:
33+
if attr in obj["metadata"]:
34+
del obj["metadata"][attr]
35+
36+
return obj
37+
1238
QUOTA_KEY_MAPPING = {
1339
attributes.QUOTA_LIMITS_CPU: lambda x: {":limits.cpu": f"{x * 1000}m"},
1440
attributes.QUOTA_LIMITS_MEMORY: lambda x: {":limits.memory": f"{x}Mi"},
@@ -38,6 +64,34 @@ class OpenShiftResourceAllocator(base.ResourceAllocator):
3864

3965
project_name_max_length = 63
4066

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

75141
def create_project(self, suggested_project_name):
76142
sanitized_project_name = utils.get_sanitized_project_name(suggested_project_name)
@@ -113,13 +179,14 @@ def reactivate_project(self, project_id):
113179
pass
114180

115181
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)
182+
if (
183+
self._openshift_user_exists(username)
184+
and self._openshift_get_identity(username)
185+
and self._openshift_useridentitymapping_exists(username, username)
186+
):
120187
return {'username': username}
121-
except NotFound:
122-
pass
188+
189+
logger.info(f"User ({username}) does not exist")
123190

124191
def create_federated_user(self, unique_id):
125192
url = f"{self.auth_url}/users/{unique_id}"
@@ -183,3 +250,39 @@ def get_users(self, project_id):
183250
url = f"{self.auth_url}/projects/{project_id}/users"
184251
r = self.session.get(url)
185252
return set(self.check_response(r))
253+
254+
def _openshift_get_user(self, username):
255+
api = self.get_resource_api(API_USER, "User")
256+
return clean_openshift_metadata(api.get(name=username).to_dict())
257+
258+
def _openshift_get_identity(self, id_user):
259+
api = self.get_resource_api(API_USER, "Identity")
260+
return clean_openshift_metadata(
261+
api.get(name=self.qualified_id_user(id_user)).to_dict()
262+
)
263+
264+
def _openshift_user_exists(self, user_name):
265+
try:
266+
self._openshift_get_user(user_name)
267+
except kexc.NotFoundError:
268+
return False
269+
return True
270+
271+
def _openshift_identity_exists(self, id_user):
272+
try:
273+
self._openshift_get_identity(id_user)
274+
except kexc.NotFoundError:
275+
return False
276+
return True
277+
278+
def _openshift_useridentitymapping_exists(self, user_name, id_user):
279+
try:
280+
user = self._openshift_get_user(user_name)
281+
except kexc.NotFoundError:
282+
return False
283+
284+
return any(
285+
identity == self.qualified_id_user(id_user)
286+
for identity in user.get("identities", [])
287+
)
288+

src/coldfront_plugin_cloud/tests/unit/openshift/__init__.py

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.allocator = OpenShiftResourceAllocator(mock_resource, mock_allocation)
12+
self.allocator.id_provider = "fake_idp"
13+
self.allocator.k8_client = mock.Mock()
14+
15+
def test_get_federated_user(self):
16+
fake_user = mock.Mock(spec=["to_dict"])
17+
fake_user.to_dict.return_value = {"identities": ["fake_idp:fake_user"]}
18+
self.allocator.k8_client.resources.get.return_value.get.return_value = fake_user
19+
20+
output = self.allocator.get_federated_user("fake_user")
21+
self.assertEqual(output, {"username": "fake_user"})
22+
23+
def test_get_federated_user_not_exist(self):
24+
fake_user = mock.Mock(spec=["to_dict"])
25+
fake_user.to_dict.return_value = {"identities": ["fake_idp:fake_user"]}
26+
self.allocator.k8_client.resources.get.return_value.get.return_value = fake_user
27+
28+
output = self.allocator.get_federated_user("fake_user_2")
29+
self.assertEqual(output, None)

0 commit comments

Comments
 (0)