implement KubernetesService
This commit is contained in:
parent
ee0dd61793
commit
e14b261805
|
@ -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
|
|
@ -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.
|
||||
|
|
11
etc/ceod.ini
11
etc/ceod.ini
|
@ -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…
Reference in New Issue