pyceo/ceod/model/KubernetesService.py

116 lines
4.6 KiB
Python
Raw Normal View History

2021-12-12 23:57:22 -05:00
import base64
2021-12-18 01:51:06 -05:00
import json
2021-12-12 23:57:22 -05:00
import os
import subprocess
import tempfile
import time
from typing import List
import jinja2
from zope import component
from zope.interface import implementer
from ceo_common.interfaces import IConfig, IKubernetesService
@implementer(IKubernetesService)
class KubernetesService:
2021-12-18 01:51:06 -05:00
namespace_prefix = 'csc-'
2021-12-12 23:57:22 -05:00
def __init__(self):
cfg = component.getUtility(IConfig)
self.members_clusterrole = cfg.get('k8s_members_clusterrole')
self.members_group = cfg.get('k8s_members_group')
self.authority_cert_path = cfg.get('k8s_authority_cert_path')
self.server_url = cfg.get('k8s_server_url')
self.jinja_env = jinja2.Environment(
loader=jinja2.PackageLoader('ceod.model'),
)
def _run(self, args: List[str], check=True, **kwargs) -> subprocess.CompletedProcess:
return subprocess.run(args, check=check, text=True, **kwargs)
def _apply_manifest(self, manifest: str):
self._run(['kubectl', 'apply', '-f', '-'], input=manifest)
2021-12-18 01:51:06 -05:00
@classmethod
def _get_namespace(cls, username: str) -> str:
return cls.namespace_prefix + username
@classmethod
def _get_username_from_namespace(cls, namespace: str) -> str:
assert namespace.startswith(cls.namespace_prefix)
return namespace[len(cls.namespace_prefix):]
2021-12-12 23:57:22 -05:00
def create_account(self, username: str) -> str:
with tempfile.TemporaryDirectory() as tempdir:
# Create a new CSR
csr_path = os.path.join(tempdir, username + '.csr')
key_path = os.path.join(tempdir, username + '.key')
self._run([
'openssl', 'req', '-new', '-newkey', 'rsa:2048', '-nodes',
'-keyout', key_path, '-subj', f'/CN={username}/O={self.members_group}',
'-out', csr_path,
], stdin=subprocess.DEVNULL)
# Upload the CSR
encoded_csr = base64.b64encode(open(csr_path, 'rb').read()).decode()
csr_name = 'csc-' + username + '-csr'
template = self.jinja_env.get_template('kubernetes_csr.yaml.j2')
body = template.render(csr_name=csr_name, encoded_csr=encoded_csr)
self._apply_manifest(body)
# Approve the CSR
self._run(['kubectl', 'certificate', 'approve', csr_name])
# Wait until the certificate is issued
2021-12-18 01:51:06 -05:00
encoded_cert = ''
2021-12-12 23:57:22 -05:00
max_tries = 5
for i in range(max_tries):
proc = self._run([
'kubectl', 'get', 'csr', csr_name,
2021-12-18 01:51:06 -05:00
'-o', 'jsonpath={.status.certificate}',
2021-12-12 23:57:22 -05:00
], capture_output=True)
2021-12-18 01:51:06 -05:00
encoded_cert = proc.stdout
if encoded_cert != '':
2021-12-12 23:57:22 -05:00
break
time.sleep(1)
2021-12-18 01:51:06 -05:00
if encoded_cert == '':
2021-12-12 23:57:22 -05:00
raise Exception('Timed out waiting for certificate to get issued')
# Delete the CSR
self._run(['kubectl', 'delete', 'csr', csr_name])
# Create a namespace
namespace = self._get_namespace(username)
template = self.jinja_env.get_template('kubernetes_user.yaml.j2')
body = template.render(
username=username, namespace=namespace,
members_clusterrole=self.members_clusterrole)
self._apply_manifest(body)
# Return the kubeconfig
encoded_key = base64.b64encode(open(key_path, 'rb').read()).decode()
encoded_authority_cert = base64.b64encode(
open(self.authority_cert_path, 'rb').read()
).decode()
template = self.jinja_env.get_template('kubeconfig.j2')
body = template.render(
username=username, namespace=namespace,
server_url=self.server_url,
encoded_cert=encoded_cert, encoded_key=encoded_key,
encoded_authority_cert=encoded_authority_cert)
return body
def delete_account(self, username: str):
namespace = self._get_namespace(username)
# don't check exit code because namespace might not exist
self._run(['kubectl', 'delete', 'namespace', namespace], check=False)
2021-12-18 01:51:06 -05:00
def get_accounts(self) -> List[str]:
proc = self._run(['kubectl', 'get', 'namespaces', '-o', 'json'],
capture_output=True)
items = json.loads(proc.stdout)['items']
namespaces = [item['metadata']['name'] for item in items]
return [
self._get_username_from_namespace(namespace)
for namespace in namespaces
if namespace.startswith(self.namespace_prefix)
]