diff --git a/backend/app.py b/backend/app.py index 76b3247bc1b6a841eb5d1111a2a4d2e889d125b8..9bc3812072ee48c795b29d1392e36c33f6ec8e34 100644 --- a/backend/app.py +++ b/backend/app.py @@ -19,6 +19,7 @@ from areas import resources from areas import roles from areas import tags from cliapp import cliapp +import helpers.kubernetes import helpers.provision from web import login @@ -104,16 +105,25 @@ def init_routines(): app.logger.info(f"upgrade failed: {type(e)}: {e}") sys.exit(2) - # We need this app context in order to talk the database, which is managed by - # flask-sqlalchemy, which assumes a flask app context. + def reload(): + # We need this app context in order to talk the database, which is managed by + # flask-sqlalchemy, which assumes a flask app context. + with app.app_context(): + app.logger.info("Reloading dashboard config from cluster resources.") + # Load the list of apps from a configmap and store any missing ones in the + # database. + app_slugs = cluster_config.populate_apps() + # Same for the list of oauthclients. + cluster_config.populate_oauthclients() + # Load per-app scim config if present. + cluster_config.populate_scim_config(app_slugs) + reload() with app.app_context(): - # Load the list of apps from a configmap and store any missing ones in the - # database. - app_slugs = cluster_config.populate_apps() - # Same for the list of oauthclients. - cluster_config.populate_oauthclients() - # Load per-app scim config if present. - cluster_config.populate_scim_config(app_slugs) + app.logger.info("Setting watch for dashboard config.") + try: + helpers.kubernetes.watch_dashboard_config(app, reload) + except Exception as e: + app.logger.error(f"Error watching: {e}") # if provisioner.enabled: # We define this wrapper because the SCIM provisioning code needs to access the diff --git a/backend/cluster_config.py b/backend/cluster_config.py index cc15b24658b82c9da0c40a3bb2d4320ce5c0e342..f5bf967c60c9e2b3f978f53267f9e9023bdacbef 100644 --- a/backend/cluster_config.py +++ b/backend/cluster_config.py @@ -98,17 +98,18 @@ def populate_scim_config(apps): if scim_config is None: logging.info(f"Could not find secret '{secret_name}' in namespace 'flux-system'; ignoring.") continue + logging.info(f"Processing secret stackspin-scim-{app}") app = App.query.filter_by(slug=app).first() if not app: logging.error(f" could not find app with slug {app}") continue scim_url = scim_config.get("scim_url") if scim_url is None: - logging.error(f" 'url' is not set") + logging.error(f" 'scim_url' is not set") continue scim_token = scim_config.get("scim_token") if scim_token is None: - logging.error(f" 'token' is not set") + logging.error(f" 'scim_token' is not set") continue scim_url = base64.b64decode(scim_url).decode() # We substitute the string `$BASE` or `${BASE}` in the `scim_url` by diff --git a/backend/helpers/kubernetes.py b/backend/helpers/kubernetes.py index cfdcbf7d53145558fb5d636b969b698c8cf28aa1..4e8ad96405a2e67c96ce302cefc15014af11fe1d 100644 --- a/backend/helpers/kubernetes.py +++ b/backend/helpers/kubernetes.py @@ -2,12 +2,14 @@ List of functions to get data from Flux Kustomizations and Helmreleases """ import crypt +import functools import secrets import string +import threading import jinja2 import yaml -from kubernetes import client, config +from kubernetes import client, config, watch from kubernetes.config.incluster_config import InClusterConfigLoader from kubernetes.client import api_client from kubernetes.client.exceptions import ApiException @@ -421,3 +423,36 @@ def get_gitrepo(name, namespace='flux-system'): # Raise all non-404 errors raise error return resource + +def debounce(timeout: float): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + wrapper.func.cancel() + wrapper.func = threading.Timer(timeout, func, args, kwargs) + wrapper.func.start() + wrapper.func = threading.Timer(timeout, lambda: None) + return wrapper + return decorator + +def watch_dashboard_config(app, reload): + current_app.logger.info("Creating watch.") + w = watch.Watch() + current_app.logger.info("Creating api instance.") + api_client_instance = api_client.ApiClient() + api_instance = client.CoreV1Api(api_client_instance) + def p(): + with app.app_context(): + for event in w.stream( + api_instance.list_namespaced_secret, + 'flux-system', + label_selector="stackspin.net/scim-config=1", + watch=True + ): + current_app.logger.info(f"{event['type']}: {event['object'].metadata.name}") + debounce(1)(reload)() + current_app.logger.info("Creating thread.") + thread = threading.Thread(target=p) + current_app.logger.info("Starting thread.") + thread.start() + current_app.logger.info("watch_dashboard-config finished") diff --git a/backend/helpers/provision.py b/backend/helpers/provision.py index d14c014f614e474b57ea8c3d514e4fe0de1e53fc..200c11cceebfde38878df0b0d91d874c8979deec 100644 --- a/backend/helpers/provision.py +++ b/backend/helpers/provision.py @@ -221,7 +221,7 @@ class Provision: if kratos_user.name is None or kratos_user.name == '': data['name']['formatted'] = " " # Zulip doesn't support SCIM user groups, but we can set the user - # role directly. + # role as a field on the user object. if app_role.role_id == Role.ADMIN_ROLE_ID: data['role'] = 'owner' diff --git a/deployment/helmchart/templates/rbac/clusterrole.yaml b/deployment/helmchart/templates/rbac/clusterrole.yaml index b4b8874817640a3eb0b45c8b4672bae13df31944..834c6c980c38a7174115a1e7a1eb42492f6715a3 100644 --- a/deployment/helmchart/templates/rbac/clusterrole.yaml +++ b/deployment/helmchart/templates/rbac/clusterrole.yaml @@ -46,6 +46,7 @@ rules: - configmaps verbs: - list + - watch - get - patch - delete