parent
ee0dd61793
commit
e14b261805
@ -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. |
||||
""" |
@ -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) |
@ -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 |
@ -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" |
||||
|
Loading…
Reference in new issue