implement KubernetesService

pull/38/head
Max Erenberg 1 year ago
parent ee0dd61793
commit e14b261805
  1. 8
      .drone/common.sh
  2. 19
      .drone/k8s-authority.crt
  3. 21
      .drone/mock_kubectl
  4. 17
      ceo_common/interfaces/IKubernetesService.py
  5. 12
      ceo_common/interfaces/IVHostManager.py
  6. 1
      ceo_common/interfaces/__init__.py
  7. 9
      ceod/api/app_factory.py
  8. 14
      ceod/api/cloud.py
  9. 37
      ceod/model/CloudService.py
  10. 100
      ceod/model/KubernetesService.py
  11. 65
      ceod/model/VHostManager.py
  12. 1
      ceod/model/__init__.py
  13. 20
      ceod/model/templates/kubeconfig.j2
  14. 10
      ceod/model/templates/kubernetes_csr.yaml.j2
  15. 60
      ceod/model/templates/kubernetes_user.yaml.j2
  16. 4
      debian/control
  17. 11
      etc/ceod.ini
  18. 1
      etc/default/ceod
  19. 9
      tests/ceod_dev.ini
  20. 9
      tests/ceod_test_local.ini
  21. 12
      tests/conftest.py

@ -11,6 +11,14 @@ ln -s /bin/true /usr/local/bin/systemctl
# mock out acme.sh
mkdir -p /root/.acme.sh
ln -s /bin/true /root/.acme.sh/acme.sh
# mock out kubectl
cp .drone/mock_kubectl /usr/local/bin/kubectl
chmod +x /usr/local/bin/kubectl
# add k8s authority certificate
mkdir -p /etc/csc
cp .drone/k8s-authority.crt /etc/csc/k8s-authority.crt
# openssl is actually already present in the python Docker image,
# so we don't need to mock it out
get_ip_addr() {
getent hosts $1 | cut -d' ' -f1

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIC/jCCAeagAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
cm5ldGVzMB4XDTIxMTIwNDIxNDcxOVoXDTMxMTIwMjIxNDcxOVowFTETMBEGA1UE
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN50
H4RcrV5ZDDqT5XMfN1ml8MalyMDAG8mE+lNT1rsUGBUp2jhNfG0OpFUm55yGarI9
2BrNGXLyFGm3yy6MWJorSUqaSBzt9+JHtBDVQwCgTX9PYSX1X/kFNQFLZkNrMtO4
417WELlkl9miCWWmTPOZAMYZWbnRKrndd3MsrhOcuDwqT5rX+LLl6VktWx5+qmuc
49sd3fWJ1MxLZ+Q6/Eo5jPuPVOPl8wLcwf9MD0rgRMVU+XycwDKr/3vmBbs22hiw
PcWIPHugAy4PRbiWfHOymO+c4WSCCS7nre3mIAyXuT0EEPDnEnrkbYoSuwIJ0tLp
N8/6vaLbBfO5ckAU2VUCAwEAAaNZMFcwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
/wQFMAMBAf8wHQYDVR0OBBYEFNqlikMIHwY+A1/PHzwPB0CtSLX+MBUGA1UdEQQO
MAyCCmt1YmVybmV0ZXMwDQYJKoZIhvcNAQELBQADggEBAJ2j87US8VTVTFoayNSk
mzip60VzgKxawi/lP1F0/JqCHtdcaA/JmlN8FggzaSxS6AA/gxNTriLNLedhqgNF
f5F5Lq0bQAebzbijsEMr+wGE6zYBgg2L0u55jqSSU1Quhay83eCD0b0O3XHGdzg0
29jC+r8pOYWuwCBaIU8NN8EouHbQ25jqJAPLCIjuqPSEPfxjZla9f2ZO7Zpx+Yud
jDYHz9ZwBYmeR7Z74/oStJ+eIFfwlJKIQL0QFzKgw2KUHmmzHVxpx60rajiGNAb8
7FNPWTjIYX11Hy56jZAUirfwCak1IxfI8O0/X1LzVPCs7uaE1SG8TCsJgjrD2Nwm
2w4=
-----END CERTIFICATE-----

@ -0,0 +1,21 @@
#!/bin/bash
if [ "$1" = apply ]; then
exit
elif [ "$1" = delete ]; then
exit
elif [ "$1" = certificate ]; then
exit
elif [ "$1" = get ]; then
if [ "$2" = csr -a "$4" = "-o" ]; then
if [ "$5" = 'jsonpath={.status.conditions[0].type}' ]; then
echo -n Approved
exit
elif [ "$5" = 'jsonpath={.status.certificate}' ]; then
echo -n 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMxekNDQWI4Q0ZHeVk0ZVpVMnAvTjMzU0pCTlptMm1vSlE5TXFNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1DZ3gKRURBT0JnTlZCQU1NQjJOMFpHRnNaV3N4RkRBU0JnTlZCQW9NQzJOell5MXRaVzFpWlhKek1CNFhEVEl4TVRJeApNekEwTWpJek4xb1hEVEl5TURFeE1qQTBNakl6TjFvd0tERVFNQTRHQTFVRUF3d0hZM1JrWVd4bGF6RVVNQklHCkExVUVDZ3dMWTNOakxXMWxiV0psY25Nd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUIKQVFEV09vaTd6ejE0c3VBZ0V2QkgrSHFHSzlCUUlQTm5QQ0llVkxXenlFRTNxUWZRV2YvcWNzeGNST2pSKzVCTgpKSXBaQlNZdjRmNE52WFZqaHlQendoWUd0bXJRYksyT3RCTDlqMDJMWjhMVHp2TnE0MW9CYVdXUFhhaVdIVys2CjkzQnlBdXFPMmdnSEt0elNkV09TcTZpeFBXMVNGUzJRMkFWaXdZUEg3b1pQYnZacUZvMzdhbVdwd1pWUHVuVi8KV2tFRUttNUVqV05DSVUzVWpPdS9HeEJOT1g0WEpqWld4bFcwQUVROVp3K2ZSazBkdU5ScVVyUDQxbDZvcG4rKwpLRkE5NFg2NUlzcUMvMlJ4OWgrNkZFRHhIcjJPcjhOcGFuMXRjZEZHQlFyMGMxV1JxRkNHTytIM0VTeUNya1BjCmdnRDlVN3c0TmdGYkQyaVU0QXc3ZkhwakFnTUJBQUV3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUY3VWUwc3YKcFhSUzN1TFl1Y0k3UkRNRGpOZnFpZ0R3NnorbzZxVmxTdGpZTGpDNjFXRyswZ0g4TDJIbm5jZVYyelhjNDkrQQp6TjFna0lWT3JlRUQvRitKbGRPUGgySUpOY1pGYTBsckFFV0dNNWRRR3pDSUM0cEtmSGxOMTZ0c0w2bGdqWTYzCmUvZlhMTFdLdktDR2lRMUlBUTh4KzYyaTVvSmU3aDBlQ1Q0aEEyM0JTRnRNelo2aEdGUURNNGxxaWhHQjEyT2UKZE5yYStsNVdLemNFR21aVFBYTXNudEZVVndPejhaNld2eGo0UW1zL1dQUElKWDdLM2NiRUo4L1RQWG1tUzJrQwowNUtueUxVQzltYnR2TGZoYldhbFZVVlJVUkYwT1RaVk5mNkt6MDJWYlRqQjRJQXdyWGZKZC9lMkMvNFpGWlJTCjVWMnlJSnBJeVJGWTdQST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo='
exit
fi
fi
fi
echo 'Unrecognized command'
exit 1

@ -0,0 +1,17 @@
from zope.interface import Interface
class IKubernetesService(Interface):
"""A client for the syscom-managed k8s cluster on CloudStack."""
def create_account(username: str) -> str:
"""
Create a new k8s namespace for the given user and create new
k8s credentials for them.
The contents of a kubeconfig file are returned.
"""
def delete_account(username: str) -> str:
"""
Delete the k8s namespace for the given user.
"""

@ -34,3 +34,15 @@ class IVHostManager(Interface):
"ip_address": "172.19.134.12"
}
"""
def is_valid_domain(username: str, domain: str) -> bool:
"""
Returns true iff the user with the given username is allowed
to create a vhost for the given domain.
"""
def is_valid_ip_address(ip_address: str) -> bool:
"""
Returns true iff ip_address is a valid proxy_pass target
in NGINX and falls within the acceptable IP range.
"""

@ -11,3 +11,4 @@ from .IMailmanService import IMailmanService
from .IHTTPClient import IHTTPClient
from .IDatabaseService import IDatabaseService
from .IVHostManager import IVHostManager
from .IKubernetesService import IKubernetesService

@ -8,11 +8,11 @@ from zope import component
from .error_handlers import register_error_handlers
from ceo_common.interfaces import IConfig, IKerberosService, ILDAPService, IFileService, \
IMailmanService, IMailService, IUWLDAPService, IHTTPClient, IDatabaseService, \
ICloudService
ICloudService, IKubernetesService
from ceo_common.model import Config, HTTPClient, RemoteMailmanService
from ceod.api.spnego import init_spnego
from ceod.model import KerberosService, LDAPService, FileService, \
MailmanService, MailService, UWLDAPService, CloudService
MailmanService, MailService, UWLDAPService, CloudService, KubernetesService
from ceod.db import MySQLService, PostgreSQLService
@ -124,7 +124,10 @@ def register_services(app):
psql_srv = PostgreSQLService()
component.provideUtility(psql_srv, IDatabaseService, 'postgresql')
# CloudService
# CloudService, KubernetesService
if hostname == cfg.get('ceod_cloud_host'):
cloud_srv = CloudService()
component.provideUtility(cloud_srv, ICloudService)
k8s_srv = KubernetesService()
component.provideUtility(k8s_srv, IKubernetesService)

@ -3,7 +3,7 @@ from zope import component
from .utils import requires_authentication_no_realm, authz_restrict_to_syscom, \
get_valid_member_or_throw
from ceo_common.interfaces import ICloudService
from ceo_common.interfaces import ICloudService, IKubernetesService
bp = Blueprint('cloud', __name__)
@ -49,3 +49,15 @@ def get_vhosts(auth_user: str):
cloud_srv = component.getUtility(ICloudService)
vhosts = cloud_srv.get_vhosts(auth_user)
return {'vhosts': vhosts}
@bp.route('/k8s/accounts/create', methods=['POST'])
@requires_authentication_no_realm
def create_k8s_account(auth_user: str):
get_valid_member_or_throw(auth_user)
k8s_srv = component.getUtility(IKubernetesService)
kubeconfig = k8s_srv.create_account(auth_user)
return {
'status': 'OK',
'kubeconfig': kubeconfig,
}

@ -2,10 +2,8 @@ from base64 import b64encode
import datetime
import hashlib
import hmac
import ipaddress
import json
import os
import re
from typing import Dict, List
from urllib.parse import quote
@ -18,7 +16,7 @@ from ceo_common.errors import CloudStackAPIError, InvalidDomainError, \
InvalidIPError, RateLimitError
from ceo_common.logger_factory import logger_factory
from ceo_common.interfaces import ICloudService, IConfig, IUser, ILDAPService, \
IMailService
IMailService, IKubernetesService
from ceo_common.model import Term
import ceo_common.utils as utils
@ -27,8 +25,6 @@ logger = logger_factory(__name__)
@implementer(ICloudService)
class CloudService:
VALID_DOMAIN_RE = re.compile(r'^(?:[0-9a-z-]+\.)+[a-z]+$')
def __init__(self):
cfg = component.getUtility(IConfig)
self.api_key = cfg.get('cloudstack_api_key')
@ -39,9 +35,6 @@ class CloudService:
self.vhost_mgr = VHostManager()
self.max_vhosts_per_account = cfg.get('cloud vhosts_max_vhosts_per_account')
self.vhost_rate_limit_secs = cfg.get('cloud vhosts_rate_limit_seconds')
self.vhost_domain = cfg.get('cloud vhosts_members_domain')
self.vhost_ip_min = ipaddress.ip_address(cfg.get('cloud vhosts_ip_range_min'))
self.vhost_ip_max = ipaddress.ip_address(cfg.get('cloud vhosts_ip_range_max'))
state_dir = '/run/ceod'
if not os.path.isdir(state_dir):
@ -133,6 +126,7 @@ class CloudService:
ldap_srv = component.getUtility(ILDAPService)
mail_srv = component.getUtility(IMailService)
k8s_srv = component.getUtility(IKubernetesService)
domain_id = self._get_domain_id(self.members_domain)
accounts = self._get_all_accounts(domain_id)
@ -159,8 +153,12 @@ class CloudService:
continue
account_id = username_to_account_id[username]
# Delete CloudStack resources
self._delete_account(account_id)
# Delete NGINX resources
self.vhost_mgr.delete_all_vhosts_for_user(username)
# Delete k8s resources
k8s_srv.delete_account(username)
accounts_deleted.append(username)
mail_srv.send_cloud_account_has_been_deleted_message(user)
@ -188,23 +186,6 @@ class CloudService:
json.dump(state, open(self.pending_deletions_file, 'w'))
return result
def _is_valid_domain(self, username: str, domain: str) -> bool:
subdomain = username + '.' + self.vhost_domain
if not (domain == subdomain or domain.endswith('.' + subdomain)):
return False
if self.VALID_DOMAIN_RE.match(domain) is None:
return False
if len(domain) > 80:
return False
return True
def _is_valid_ip_address(self, ip_address: str) -> bool:
try:
addr = ipaddress.ip_address(ip_address)
except ValueError:
return False
return self.vhost_ip_min <= addr <= self.vhost_ip_max
def _check_rate_limit(self, username: str):
if os.path.exists(self.vhost_rate_limit_file):
d = json.load(open(self.vhost_rate_limit_file))
@ -231,9 +212,9 @@ class CloudService:
if self.vhost_mgr.get_num_vhosts(username) >= self.max_vhosts_per_account:
raise Exception(f'Only {self.max_vhosts_per_account} vhosts '
'allowed per account')
if not self._is_valid_domain(username, domain):
if not self.vhost_mgr.is_valid_domain(username, domain):
raise InvalidDomainError()
if not self._is_valid_ip_address(ip_address):
if not self.vhost_mgr.is_valid_ip_address(ip_address):
raise InvalidIPError()
self._check_rate_limit(username)
# Wait for the vhost creation to succeed before updating the timestamp;
@ -243,7 +224,7 @@ class CloudService:
self._update_rate_limit_timestamp(username)
def delete_vhost(self, username: str, domain: str):
if not self._is_valid_domain(username, domain):
if not self.vhost_mgr.is_valid_domain(username, domain):
raise InvalidDomainError()
self.vhost_mgr.delete_vhost(username, domain)

@ -0,0 +1,100 @@
import base64
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:
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)
@staticmethod
def _get_namespace(username: str) -> str:
return 'csc-' + username
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
max_tries = 5
for i in range(max_tries):
proc = self._run([
'kubectl', 'get', 'csr', csr_name,
'-o', 'jsonpath={.status.conditions[0].type}',
], capture_output=True)
if proc.stdout == 'Approved':
break
time.sleep(1)
if i == max_tries:
raise Exception('Timed out waiting for certificate to get issued')
# Retrieve the certificate
proc = self._run([
'kubectl', 'get', 'csr', csr_name,
'-o', 'jsonpath={.status.certificate}',
], capture_output=True)
encoded_cert = proc.stdout
# 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)

@ -1,4 +1,5 @@
import glob
import ipaddress
import os
import re
import shutil
@ -12,8 +13,12 @@ from zope.interface import implementer
from ceo_common.logger_factory import logger_factory
from ceo_common.interfaces import IVHostManager, IConfig
PROXY_PASS_IP_RE = re.compile(r'^\s+proxy_pass\s+http://(?P<ip_address>[\d.]+);$')
PROXY_PASS_IP_RE = re.compile(
r'^\s+proxy_pass\s+http://(?P<ip_address>[\d.]+(:\d{2,5})?);$'
)
VHOST_FILENAME_RE = re.compile(r'^(?P<username>[0-9a-z-]+)_(?P<domain>[0-9a-z.-]+)$')
VALID_DOMAIN_RE = re.compile(r'^(?:[0-9a-z-]+\.)+[a-z]+$')
IP_WITH_PORT_RE = re.compile(r'^(?P<ip_address>[\d.]+)(:\d{2,5})?$')
logger = logger_factory(__name__)
@ -31,6 +36,20 @@ class VHostManager:
self.default_ssl_cert = cfg.get('cloud vhosts_default_ssl_cert')
self.default_ssl_key = cfg.get('cloud vhosts_default_ssl_key')
self.vhost_domain = cfg.get('cloud vhosts_members_domain')
self.vhost_domain_re = re.compile(
r'^[a-z0-9-]+\.' + self.vhost_domain.replace('.', r'\.') + '$'
)
self.k8s_vhost_domain = cfg.get('cloud vhosts_k8s_members_domain')
self.k8s_vhost_domain_re = re.compile(
r'^[a-z0-9-]+\.' + self.k8s_vhost_domain.replace('.', r'\.') + '$'
)
self.k8s_ssl_cert = cfg.get('cloud vhosts_k8s_ssl_cert')
self.k8s_ssl_key = cfg.get('cloud vhosts_k8s_ssl_key')
self.vhost_ip_min = ipaddress.ip_address(cfg.get('cloud vhosts_ip_range_min'))
self.vhost_ip_max = ipaddress.ip_address(cfg.get('cloud vhosts_ip_range_max'))
self.acme_challenge_dir = cfg.get('cloud vhosts_acme_challenge_dir')
self.acme_dir = '/root/.acme.sh'
@ -62,7 +81,47 @@ class VHostManager:
logger.debug('Reloading NGINX')
self._run(['systemctl', 'reload', 'nginx'])
def is_valid_domain(self, username: str, domain: str) -> bool:
subdomain = username + '.' + self.vhost_domain
if domain == subdomain:
return True
# Members may only create domains of the form *.username.csclub.cloud
# or *-username.csclub.cloud
if not domain.endswith('.' + subdomain) and \
not domain.endswith('-' + subdomain):
return False
# Make sure that the domain doesn't have invalid characters
if VALID_DOMAIN_RE.match(domain) is None:
return False
if len(domain) > 80:
return False
return True
def is_valid_ip_address(self, ip_address: str) -> bool:
if ip_address == 'k8s':
# special case - this is an NGINX upstream
return True
# strip off the port number, if there is one
match = IP_WITH_PORT_RE.match(ip_address)
if match is None:
return False
ip_address = match.group('ip_address')
# make sure the IP is in the allowed range
try:
addr = ipaddress.ip_address(ip_address)
except ValueError:
return False
return self.vhost_ip_min <= addr <= self.vhost_ip_max
def _get_cert_and_key_path(self, domain: str) -> Tuple[str, str]:
# Use the wildcard certs, if possible
if self.vhost_domain_re.match(domain) is not None:
return self.default_ssl_cert, self.default_ssl_key
elif self.k8s_vhost_domain_re.match(domain) is not None:
return self.k8s_ssl_cert, self.k8s_ssl_key
# Otherwise, obtain a new cert with acme.sh
cert_path = f'{self.ssl_dir}/{domain}.chain'
key_path = f'{self.ssl_dir}/{domain}.key'
return cert_path, key_path
@ -106,7 +165,9 @@ class VHostManager:
def delete_vhost(self, username: str, domain: str):
cert_path, key_path = self._get_cert_and_key_path(domain)
if os.path.exists(cert_path) and os.path.exists(key_path):
if cert_path not in [self.default_ssl_cert, self.k8s_ssl_cert] \
and cert_path.startswith(self.ssl_dir) \
and os.path.exists(cert_path) and os.path.exists(key_path):
self._delete_cert(domain, cert_path, key_path)
filepath = self._vhost_filepath(username, domain)

@ -10,3 +10,4 @@ from .SudoRole import SudoRole
from .MailService import MailService
from .MailmanService import MailmanService
from .VHostManager import VHostManager
from .KubernetesService import KubernetesService

@ -0,0 +1,20 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: {{ encoded_authority_cert }}
server: {{ server_url }}
name: kubernetes
contexts:
- context:
cluster: kubernetes
namespace: {{ namespace }}
user: {{ username }}
name: {{ namespace }}
current-context: {{ namespace }}
kind: Config
preferences: {}
users:
- name: {{ username }}
user:
client-certificate-data: {{ encoded_cert }}
client-key-data: {{ encoded_key }}

@ -0,0 +1,10 @@
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: {{ csr_name }}
spec:
request: {{ encoded_csr }}
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 315360000 # ten years
usages:
- client auth

@ -0,0 +1,60 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ namespace }}
labels:
pod-security.kubernetes.io/enforce: baseline
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: {{ username }}-resourcequota
namespace: {{ namespace }}
spec:
hard:
requests.storage: 25Gi
count/jobs.batch: 10
count/cronjobs.batch: 10
count/pods: 40
services.loadbalancers: 0
services.nodeports: 5
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ members_clusterrole }}
namespace: {{ namespace }}
subjects:
- kind: User
name: {{ username }}
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: {{ members_clusterrole }}
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ username }}-ns-role
namespace: {{ namespace }}
rules:
# Allow members to view their own namespace
- apiGroups: [""]
resources: ["namespaces"]
resourceNames: ["{{ namespace }}"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ username }}-ns-rolebinding
namespace: {{ namespace }}
subjects:
- kind: User
name: {{ username }}
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: {{ username }}-ns-role
apiGroup: rbac.authorization.k8s.io

4
debian/control vendored

@ -47,7 +47,7 @@ Package: ceo
Architecture: amd64
Replaces: ceo-python, ceo-clients
Conflicts: ceo-python, ceo-clients
Depends: ceo-common (>= 1.0.0), ${misc:Depends}
Depends: ceo-common (= ${source:Version}), ${misc:Depends}
Description: CSC Electronic Office client
This package contains the command line interface and text
user interface clients for the CSC Electronic Office.
@ -56,6 +56,6 @@ Package: ceod
Architecture: amd64
Replaces: ceo-daemon
Conflicts: ceo-daemon
Depends: ceo-common (>= 1.0.0), ${misc:Depends}
Depends: ceo-common (= ${source:Version}), openssl (>= 1.1.1), ${misc:Depends}
Description: CSC Electronic Office daemon
This package contains the daemon for the CSC Electronic Office.

@ -86,8 +86,17 @@ vhost_dir = /etc/nginx/ceod/member-vhosts
ssl_dir = /etc/nginx/ceod/member-ssl
default_ssl_cert = /etc/ssl/private/csclub.cloud.chain
default_ssl_key = /etc/ssl/private/csclub.cloud.key
rate_limit_seconds = 10
k8s_ssl_cert = /etc/nginx/ceod/syscom-ssl/k8s.csclub.cloud.chain
k8s_ssl_key = /etc/nginx/ceod/syscom-ssl/k8s.csclub.cloud.key
rate_limit_seconds = 60
max_vhosts_per_account = 10
members_domain = csclub.cloud
k8s_members_domain = k8s.csclub.cloud
ip_range_min = 172.19.134.10
ip_range_max = 172.19.134.160
[k8s]
members_clusterrole = csc-members-default
members_group = csc-members
authority_cert_path = /etc/csc/k8s-authority.crt
server_url = https://172.19.134.149:6443

@ -1 +1,2 @@
GUNICORN_ARGS="-w 2 -b 0.0.0.0:9987 --access-logfile - --certfile /etc/ssl/private/csclub-wildcard-chain.crt --keyfile /etc/ssl/private/csclub-wildcard.key"
LE_WORKING_DIR="/root/.acme.sh"

@ -80,8 +80,17 @@ vhost_dir = /run/ceod/member-vhosts
ssl_dir = /run/ceod/member-ssl
default_ssl_cert = /etc/ssl/private/csclub.cloud.chain
default_ssl_key = /etc/ssl/private/csclub.cloud.key
k8s_ssl_cert = /etc/nginx/ceod/syscom-ssl/k8s.csclub.cloud.chain
k8s_ssl_key = /etc/nginx/ceod/syscom-ssl/k8s.csclub.cloud.key
rate_limit_seconds = 30
max_vhosts_per_account = 10
members_domain = csclub.cloud
k8s_members_domain = k8s.csclub.cloud
ip_range_min = 172.19.134.10
ip_range_max = 172.19.134.160
[k8s]
members_clusterrole = csc-members-default
members_group = csc-members
authority_cert_path = /etc/csc/k8s-authority.crt
server_url = https://172.19.134.149:6443

@ -79,8 +79,17 @@ vhost_dir = /run/ceod/member-vhosts
ssl_dir = /run/ceod/member-ssl
default_ssl_cert = /etc/ssl/private/csclub.cloud.chain
default_ssl_key = /etc/ssl/private/csclub.cloud.key
k8s_ssl_cert = /etc/nginx/ceod/syscom-ssl/k8s.csclub.cloud.chain
k8s_ssl_key = /etc/nginx/ceod/syscom-ssl/k8s.csclub.cloud.key
rate_limit_seconds = 10
max_vhosts_per_account = 10
members_domain = csclub.cloud
k8s_members_domain = k8s.csclub.cloud
ip_range_min = 172.19.134.10
ip_range_max = 172.19.134.160
[k8s]
members_clusterrole = csc-members-default
members_group = csc-members
authority_cert_path = /etc/csc/k8s-authority.crt
server_url = https://172.19.134.149:6443

@ -25,13 +25,13 @@ from .utils import ( # noqa: F401
)
from ceo_common.interfaces import IConfig, IKerberosService, ILDAPService, \
IFileService, IMailmanService, IHTTPClient, IUWLDAPService, IMailService, \
IDatabaseService, ICloudService
IDatabaseService, ICloudService, IKubernetesService
from ceo_common.model import Config, HTTPClient, Term
from ceod.api import create_app
from ceod.db import MySQLService, PostgreSQLService
from ceod.model import KerberosService, LDAPService, FileService, User, \
MailmanService, Group, UWLDAPService, UWLDAPRecord, MailService, \
CloudService
CloudService, KubernetesService
from .MockSMTPServer import MockSMTPServer
from .MockMailmanServer import MockMailmanServer
from .MockCloudStackServer import MockCloudStackServer
@ -286,6 +286,13 @@ def cloud_srv(cfg, vhost_dir_setup):
return _cloud_srv
@pytest.fixture(scope='session')
def k8s_srv(cfg, vhost_dir_setup):
_k8s_srv = KubernetesService()
component.getGlobalSiteManager().registerUtility(_k8s_srv, IKubernetesService)
return _k8s_srv
@pytest.fixture(autouse=True, scope='session')
def app(
cfg,
@ -298,6 +305,7 @@ def app(
mysql_srv,
postgresql_srv,
cloud_srv,
k8s_srv,
):
app = create_app({'TESTING': True})
return app

Loading…
Cancel
Save