diff --git a/.github/workflows/publish-docker-image.yaml b/.github/workflows/publish-docker-image.yaml index 94b134f..f454825 100644 --- a/.github/workflows/publish-docker-image.yaml +++ b/.github/workflows/publish-docker-image.yaml @@ -2,12 +2,12 @@ name: Publish Docker Image on: push: - branches: ['main'] + branches: ["main"] paths: - - docker/** + - self-registration/** pull_request: paths: - - docker/** + - self-registration/** env: REGISTRY: ghcr.io @@ -26,7 +26,7 @@ jobs: - name: Set the timestamp for version run: echo "VERSION=$(date +'%Y%m%d-%H%M')" >> ${GITHUB_ENV} - name: Build image - run: docker build docker --file docker/Dockerfile --tag $IMAGE_LC --label "runnumber=${GITHUB_RUN_ID}" + run: docker build self-registration --file self-registration/Dockerfile --tag $IMAGE_LC --label "runnumber=${GITHUB_RUN_ID}" - name: Log in to registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login $REGISTRY -u $ --password-stdin - name: Push image @@ -40,4 +40,4 @@ jobs: echo IMAGE_ID=$IMAGE_ID echo VERSION=$VERSION docker tag $IMAGE_LC $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION \ No newline at end of file + docker push $IMAGE_ID:$VERSION diff --git a/README.md b/README.md index e684e33..f84d2db 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI - Version](https://img.shields.io/pypi/v/nebari-plugin-self-registration.svg)](https://pypi.org/project/nebari-plugin-self-registration) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/nebari-plugin-self-registration.svg)](https://pypi.org/project/nebari-plugin-self-registration) ------ +--- **Table of Contents** @@ -16,7 +16,27 @@ pip install nebari-plugin-self-registration ``` +## Running locally with Docker + +_Note_: running locally requires a `config.yaml` file to be present within the `self-registration` directory. Please create a copy of the `sample.config.yaml`, rename, and update as needed before proceeding: + +1. Navigate to the `self-registration` directory +2. To build the docker image, run the following: + +``` +docker build . --file Dockerfile.local -t self-registration +``` + +3. To run the app, run the following: + +``` +docker run -p 8000:8000 --name self-registration self-registration +``` + +4. Navigate to http://0.0.0.0:8000/registration + ## User Registration via this extension + Steps for self registration: - Navigate to your Nebari domain. @@ -34,7 +54,7 @@ Steps for self registration: - After clicking "Submit" follow the instructions to login with your temporary password. By clicking the "Login" button, it will take you to a Welcome page where you can sign in with Keycloak. -- After you have entered a new password, you will receive a verification email. +- After you have entered a new password, you will receive a verification email.

diff --git a/docker/app/static/logo.png b/docker/app/static/logo.png deleted file mode 100644 index 6ed7f8d..0000000 Binary files a/docker/app/static/logo.png and /dev/null differ diff --git a/docker/app/templates/index.html b/docker/app/templates/index.html deleted file mode 100644 index 236e953..0000000 --- a/docker/app/templates/index.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - Self Registration - - - - - -

- logo -
-
-
-
-

Account Registration

-
-
-

- Submit your email address and a valid coupon code to receive a trial - account for our AI platform. -

- {% if error_message %} -

{{ error_message }}

- {% endif %} -
-
- - -
-
- - -
-
- -
-
-
-
-
- - diff --git a/docker/app/templates/success.html b/docker/app/templates/success.html deleted file mode 100644 index 0c9821e..0000000 --- a/docker/app/templates/success.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - Self Registration - - - - - - -
- logo -
-
-
-

Account Confirmation

-
- Congratulations! Your account has been successfully created! Your - account will expire automatically on - {{ expiration_date }}. -
-
- Your temporary password for - {{ email }} is: -
-
- {{ temporary_password }} - -
- -
- Please copy the password and log in, after which you will be asked to - set a new password. Please note, this temporary password cannot be - recovered. -
-
- Login -
-
-
- - diff --git a/docker/.dockerignore b/self-registration/.dockerignore similarity index 100% rename from docker/.dockerignore rename to self-registration/.dockerignore diff --git a/docker/Dockerfile b/self-registration/Dockerfile similarity index 100% rename from docker/Dockerfile rename to self-registration/Dockerfile diff --git a/self-registration/Dockerfile.local b/self-registration/Dockerfile.local new file mode 100644 index 0000000..fa3d9c4 --- /dev/null +++ b/self-registration/Dockerfile.local @@ -0,0 +1,19 @@ +FROM python:3.12 + +WORKDIR /app + +COPY ./requirements.txt /tmp/requirements.txt +COPY ./config.yaml . + +# need to pre-install cython < 3 to avoid pyyaml >= 5.4.1 incompatibility https://github.com/yaml/pyyaml/issues/601 +RUN echo "cython<3" > /tmp/constraint.txt + +RUN PIP_CONSTRAINT=/tmp/constraint.txt pip install --no-cache-dir --upgrade -r /tmp/requirements.txt + +COPY ./app /app + +# Run web app as non-root +RUN useradd -m www +USER www + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/docker/app/job.py b/self-registration/app/job.py similarity index 100% rename from docker/app/job.py rename to self-registration/app/job.py diff --git a/docker/app/main.py b/self-registration/app/main.py similarity index 52% rename from docker/app/main.py rename to self-registration/app/main.py index bc618f4..26a20c3 100644 --- a/docker/app/main.py +++ b/self-registration/app/main.py @@ -1,42 +1,56 @@ -from datetime import datetime, timedelta -from fastapi import FastAPI, Form, Request, APIRouter -from fastapi.templating import Jinja2Templates -from fastapi.staticfiles import StaticFiles -import string -import re +import os import random +import re +import string +from datetime import datetime, timedelta + import yaml +from fastapi import APIRouter, FastAPI, Form, Request +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates from keycloak import KeycloakAdmin, KeycloakConnectionError, KeycloakGetError +from theme import DEFAULT_THEME + class UserExistsException(Exception): pass + # Manual context URL. Must be prepended to all paths in app and templates. url_prefix = "/registration" app = FastAPI() -app.mount(url_prefix+"/static", StaticFiles(directory="static"), name="static") +app.mount(url_prefix + "/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") -file_path = "/mnt/config.yaml" -with open(file_path, "r") as file: - config = yaml.safe_load(file) +mount_path = "/mnt/config.yaml" +app_path = "/app/config.yaml" +if os.path.exists(mount_path): + with open(mount_path) as file: + config = yaml.safe_load(file) +elif os.path.exists(app_path): + with open(app_path) as file: + config = yaml.safe_load(file) +else: + config = {} + def check_email_domain(email): approved_domains = config.get("approved_domains", []) for domain in approved_domains: # Replace wildcard with its regex equivalent - pattern = domain.replace('*', '.*') + pattern = domain.replace("*", ".*") if re.search(f"@{pattern}$", email): return True return False + def create_keycloak_user(email, expiration_days=7): # Random password generator def generate_random_password(length=12): characters = string.ascii_letters + string.digits + string.punctuation - return ''.join(random.choice(characters) for i in range(length)) + return "".join(random.choice(characters) for i in range(length)) try: keycloak_admin = KeycloakAdmin( @@ -50,17 +64,18 @@ def generate_random_password(length=12): except KeycloakConnectionError: return email, False, None - # Check if the user already exists user_id = keycloak_admin.get_user_id(email) if user_id: - raise UserExistsException("A user with this email address already exists. Contact the administrator if you need to recover your account.") + raise UserExistsException( + "A user with this email address already exists. Contact the administrator if you need to recover your account." + ) # Calculate account expiration as Unix timestamp expiration_date = datetime.utcnow() + timedelta(days=expiration_days) # Format expiration timestamp as a human-readable string - date_format="%Y-%m-%d %H:%M:%S" + date_format = "%Y-%m-%d %H:%M:%S" formatted_expiration_date = expiration_date.strftime(date_format) # Create a new user @@ -79,6 +94,7 @@ def generate_random_password(length=12): keycloak_admin.set_user_password(user_id, temporary_password, temporary=True) return keycloak_admin.get_user(user_id), temporary_password, expiration_date + # Function to assign a user to a group def assign_user_to_group(user, group_name): try: @@ -97,42 +113,101 @@ def assign_user_to_group(user, group_name): try: group = keycloak_admin.get_group_by_path(group_name) except KeycloakGetError: - return False # Fail if Keycloak group throws exception finding group + return False # Fail if Keycloak group throws exception finding group if not group: return False # Also fail if Keycloak admin doesn't throw exception but group is still missing # Assign the user to the group keycloak_admin.group_user_add(user["id"], group["id"]) - + return True + +def get_theme(): + theme = config.get("theme", {}) + if theme: + return {**DEFAULT_THEME, **theme} + else: + return DEFAULT_THEME + + +def get_template_context(request: Request, error_message: str = None): + if (error_message is None) or (error_message == ""): + return { + "url_prefix": url_prefix, + "request": request, + "registration_message": config.get("registration_message", None), + **get_theme(), + } + else: + return { + "url_prefix": url_prefix, + "request": request, + "registration_message": config.get("registration_message", None), + "error_message": error_message, + **get_theme(), + } + + @app.get(url_prefix) def read_root(request: Request): - return templates.TemplateResponse("index.html", {"url_prefix": url_prefix, "request": request}) + return templates.TemplateResponse("index.html", get_template_context(request)) + @app.post(url_prefix + "/validate/") async def validate_submission(request: Request, email: str = Form(...), coupon_code: str = Form(...)): if coupon_code in config.get("coupons", []): if check_email_domain(email): - + # Create the user in Keycloak try: - user, temporary_password, expiration_date = create_keycloak_user(email, config.get("account_expiration_days", None)) + user, temporary_password, expiration_date = create_keycloak_user( + email, config.get("account_expiration_days", None) + ) except UserExistsException as e: - return templates.TemplateResponse("index.html", {"url_prefix": url_prefix, "request": request, "error_message": str(e)}) - + return templates.TemplateResponse("index.html", get_template_context(request, str(e))) + # Assign user to group if user: success = assign_user_to_group(user, config.get("registration_group", None)) if success: - return templates.TemplateResponse("success.html", {"url_prefix": url_prefix, "request": request, "email": email, "temporary_password": temporary_password, "user_id": user["id"], "expiration_date": expiration_date.strftime("%m-%d-%Y")}) + return templates.TemplateResponse( + "success.html", + { + "url_prefix": url_prefix, + "request": request, + "email": email, + "temporary_password": temporary_password, + "user_id": user["id"], + "expiration_date": expiration_date.strftime("%m-%d-%Y"), + **get_theme(), + }, + ) else: - return templates.TemplateResponse("index.html", {"url_prefix": url_prefix, "request": request, "error_message": "Your user was registered but could not be granted access to JupyterLab environments. Please contact support for assistance."}) + return templates.TemplateResponse( + "index.html", + get_template_context( + request, + "User created but could not be assigned to JupyterLab group. Please contact support for assistance.", + ), + ) else: - return templates.TemplateResponse("index.html", {"url_prefix": url_prefix, "request": request, "error_message": "Unable to create user. Please try again later."}) + return templates.TemplateResponse( + "index.html", + get_template_context(request, "Unable to create user. Please try again later."), + ) else: - return templates.TemplateResponse("index.html", {"url_prefix": url_prefix, "request": request, "error_message": "Access to the platform is limited to accounts created with pre-approved email domains. The email address you provided when registering your account uses a domain that's not currently approved. Please contact the system administrator to request access."}) + return templates.TemplateResponse( + "index.html", + get_template_context( + request, + "Access to the platform is limited to accounts created with pre-approved email domains. The email address you provided when registering your account uses a domain that's not currently approved. Please contact the system administrator to request access.", + ), + ) else: - return templates.TemplateResponse("index.html", {"url_prefix": url_prefix, "request": request, "error_message": "Invalid coupon code. Please try again."}) \ No newline at end of file + return templates.TemplateResponse( + "index.html", + get_template_context(request, "Invalid coupon code. Please try again."), + ) diff --git a/self-registration/app/static/favicon.ico b/self-registration/app/static/favicon.ico new file mode 100644 index 0000000..29acb56 Binary files /dev/null and b/self-registration/app/static/favicon.ico differ diff --git a/self-registration/app/static/logo.svg b/self-registration/app/static/logo.svg new file mode 100644 index 0000000..d1b5f73 --- /dev/null +++ b/self-registration/app/static/logo.svg @@ -0,0 +1 @@ + diff --git a/self-registration/app/templates/base.html b/self-registration/app/templates/base.html new file mode 100644 index 0000000..fc7e37c --- /dev/null +++ b/self-registration/app/templates/base.html @@ -0,0 +1,30 @@ + + + + Self Registration + + {% if favicon.startswith('/') %} + + {% else %} + + {% endif %} + + {% block head %}{% endblock %} + + +
+ + {% if logo.startswith('/') %} + logo + {% else %} + logo + {% endif %} + +
+
+ {% block content %}{% endblock %} +
+ + diff --git a/self-registration/app/templates/index.html b/self-registration/app/templates/index.html new file mode 100644 index 0000000..e647c2a --- /dev/null +++ b/self-registration/app/templates/index.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} {% block content %} +
+
+

Account Registration

+
+
+

+ {% if registration_message %} {{ (registration_message| safe) }} {% else + %} Submit your email address and a valid coupon code to receive a trial + account for our AI platform. {% endif %} +

+ {% if error_message %} +

{{ error_message }}

+ {% endif %} +
+
+ + +
+
+ + +
+
+ +
+
+
+
+{% endblock %} diff --git a/docker/app/static/styles.css b/self-registration/app/templates/styles.css similarity index 54% rename from docker/app/static/styles.css rename to self-registration/app/templates/styles.css index dee6135..de607d2 100644 --- a/docker/app/static/styles.css +++ b/self-registration/app/templates/styles.css @@ -1,25 +1,52 @@ +:root { + --heading-color: {{ h1_color | default("#0f1015") }}; + --text-color: {{ text_color | default("#1c1d26") }}; + --link-text-color: {{ text_color | default("#1c1d26") }}; + --primary-color: {{ primary_color | default("#ba18da") }}; + --primary-color-dark: {{ primary_color_dark | default("#9b00ce") }}; + --secondary-color: {{ secondary_color | default("#18817a") }}; + --secondary-color-dark: {{ secondary_color_dark | default("#12635e") }}; + --accent-color: {{ accent_color | default("#eda61d") }}; + --accent-color-dark: {{ accent_color_dark | default("#a16d14") }}; + --navbar-background-color: {{ navbar_color | default("#1c1d26") }}; + --navbar-text-color: {{ navbar_text_color | default("#ffffff") }}; + --navbar-hover-color: {{ navbar_hover_color | default("#20b1a8") }}; + --link-hover-color: {{ accent_color | default("#20b1a8") }}; + --light-text-color: #f1f1f6; + --danger-color: #e60f66; + --danger-color-dark: #b81a53; + --gray-color: #e6e6e6; + --gray-color-dark: #e1e3e4; + --white-color: #fff; + --blue-link-color: #276be9; +} + header { height: 41px; - background-color: #000; - color: #fff; + background-color: var(--navbar-background-color); + color: var(--navbar-text-color); font-size: 36px; } -header > img { + +header > a > img { height: 28px; margin: 6px 24px; } + .container { max-width: 100% !important; height: 80vh; padding: 48px; } + .card { width: 400px; margin: auto auto; vertical-align: middle; - border: 1px solid #e6e6e6; + border: 1px solid var(--gray-color); border-radius: 4px; } + .card-header { display: flex; padding: 16px; @@ -27,17 +54,19 @@ header > img { align-items: flex-start; gap: 8px; align-self: stretch; - border: 1px solid #e1e3e4; - background: #e1e3e4; + border: 1px solid var(--accent-color); + background: var(--accent-color); } + .card-header > h1 { - color: #000; + color: var(--text-color); text-align: center; font-size: 24px; font-style: normal; font-weight: 700; line-height: 32px; } + .card-body { display: flex; padding: 24px; @@ -46,6 +75,7 @@ header > img { gap: 24px; align-self: stretch; } + .card-body > p { color: #000; font-size: 16px; @@ -53,9 +83,15 @@ header > img { font-weight: 400; line-height: 24px; } + +.card-body > p.error { + color: var(--danger-color); +} + form { width: 100%; } + .form-group { display: flex; flex-direction: column; @@ -64,6 +100,7 @@ form { align-self: stretch; padding-bottom: 20px; } + label { display: flex; flex-direction: column; @@ -76,6 +113,7 @@ label { font-weight: 400; line-height: 16px; } + input:not(.btn) { width: 100%; display: flex; @@ -84,13 +122,15 @@ input:not(.btn) { gap: 8px; align-self: stretch; border-radius: 6px; - border: 1px solid #e1e1e6; - background: #fff; + border: 1px solid var(--gray-color-dark); + background: var(--white-color); } + input:focus, .btn.btn-primary:focus { - outline: 0.2rem solid #2e5084; + outline: 0.2rem solid var(--primary-color); } + .btn { width: 100%; display: flex; @@ -101,13 +141,16 @@ input:focus, border-radius: 4px; cursor: pointer; } + .btn-primary { - background-color: #2e5084 !important; - color: #fff; + background-color: var(--primary-color) !important; + color: var(--white-color) !important; } + .btn-primary:focus { outline-offset: 0.2rem; } + .confirmation > h1 { color: #000; font-size: 36px; @@ -115,6 +158,7 @@ input:focus, font-weight: 600; line-height: 32px; } + .password-box { width: 300px; display: flex; @@ -122,12 +166,12 @@ input:focus, align-items: center; gap: 8px; border-radius: 6px; - border: 1px solid #e1e1e6; - background: #fff; + border: 1px solid var(--gray-color-dark); + background: var(--white-color); } .password-content { - color: #000; + color: var(--text-color); font-size: 16px; font-style: normal; font-weight: 400; diff --git a/self-registration/app/templates/success.html b/self-registration/app/templates/success.html new file mode 100644 index 0000000..71c9123 --- /dev/null +++ b/self-registration/app/templates/success.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} {% block head %} + + +{% endblock %} {% block content %} +
+

Account Confirmation

+
+ Congratulations! Your account has been successfully created! Your account + will expire automatically on + {{ expiration_date }}. +
+
+ Your temporary password for + {{ email }} is: +
+
+ {{ temporary_password }} + +
+ +
+ Please copy the password and log in, after which you will be asked to set a + new password. Please note, this temporary password cannot be recovered. +
+
+ Login +
+
+{% endblock %} diff --git a/self-registration/app/theme.py b/self-registration/app/theme.py new file mode 100644 index 0000000..79e3eb4 --- /dev/null +++ b/self-registration/app/theme.py @@ -0,0 +1,19 @@ +LOGO = "/static/logo.svg" +FAVICON = "/static/favicon.ico" + +DEFAULT_THEME = { + "logo": LOGO, + "favicon": FAVICON, + "primary_color": "#ba18da", + "primary_color_dark": "#9b00ce", + "secondary_color": "#18817a", + "secondary_color_dark": "#12635e", + "accent_color": "#eda61d", + "accent_color_dark": "#a16d14", + "text_color": "#1c1d26", + "h1_color": "#0f1015", + "h2_color": "#0f1015", + "navbar_text_color": "#ffffff", + "navbar_hover_color": "#20b1a8", + "navbar_color": "#1c1d26", +} diff --git a/self-registration/config.sample.yaml b/self-registration/config.sample.yaml new file mode 100644 index 0000000..71c4eb7 --- /dev/null +++ b/self-registration/config.sample.yaml @@ -0,0 +1,7 @@ +namespace: self-registration +coupons: + - abcdefg +approved_domains: + - metrostar.com +account_expiration_days: 30 +registration_group: test-group diff --git a/docker/requirements.txt b/self-registration/requirements.txt similarity index 100% rename from docker/requirements.txt rename to self-registration/requirements.txt diff --git a/self-registration/run.sh b/self-registration/run.sh new file mode 100755 index 0000000..b532326 --- /dev/null +++ b/self-registration/run.sh @@ -0,0 +1,7 @@ +# Cleanup Old Container +docker container stop self-registration +docker container rm self-registration + +# Build and Run New Container +docker build . --file Dockerfile.local -t self-registration +docker run -d -p 8000:8000 --name self-registration self-registration \ No newline at end of file diff --git a/src/nebari_plugin_self_registration/__about__.py b/src/nebari_plugin_self_registration/__about__.py index 6526deb..a73339b 100644 --- a/src/nebari_plugin_self_registration/__about__.py +++ b/src/nebari_plugin_self_registration/__about__.py @@ -1 +1 @@ -__version__ = "0.0.7" +__version__ = "0.0.8" diff --git a/src/nebari_plugin_self_registration/__init__.py b/src/nebari_plugin_self_registration/__init__.py index f51e077..4686f60 100644 --- a/src/nebari_plugin_self_registration/__init__.py +++ b/src/nebari_plugin_self_registration/__init__.py @@ -1,31 +1,31 @@ import inspect import sys import time +from pathlib import Path +from typing import Any, Dict, List, Optional, Union -from nebari.schema import Base from _nebari.stages.base import NebariTerraformStage +from _nebari.stages.tf_objects import NebariKubernetesProvider, NebariTerraformState from nebari.hookspecs import NebariStage, hookimpl -from pathlib import Path -from typing import Any, Dict, List, Optional, Union -from _nebari.stages.tf_objects import ( - NebariKubernetesProvider, - NebariTerraformState, -) +from nebari.schema import Base NUM_ATTEMPTS = 10 TIMEOUT = 10 CLIENT_NAME = "self-registration" + class SelfRegistrationAffinitySelectorConfig(Base): default: str app: Optional[str] = "" job: Optional[str] = "" -class SelfRegistrationConfig(Base): + +class SelfRegistrationAffinityConfig(Base): enabled: Optional[bool] = True selector: Union[SelfRegistrationAffinitySelectorConfig, str] = "general" + class SelfRegistrationConfig(Base): name: Optional[str] = "self-registration" namespace: Optional[str] = None @@ -33,17 +33,19 @@ class SelfRegistrationConfig(Base): account_expiration_days: Optional[int] = 7 approved_domains: Optional[List[str]] = [] coupons: Optional[List[str]] = [] - registration_group: Optional[str] = "" - affinity: SelfRegistrationConfig = SelfRegistrationConfig() + registration_group: Optional[str] = "" + registration_message: Optional[str] = "" + affinity: SelfRegistrationAffinityConfig = SelfRegistrationAffinityConfig() class InputSchema(Base): self_registration: SelfRegistrationConfig = SelfRegistrationConfig() + class SelfRegistrationStage(NebariTerraformStage): name = "self-registration" priority = 103 - wait = True # wait for install to complete on nebari deploy + wait = True # wait for install to complete on nebari deploy input_schema = InputSchema def tf_objects(self) -> List[Dict]: @@ -52,12 +54,10 @@ def tf_objects(self) -> List[Dict]: NebariKubernetesProvider(self.config), ] - - @property def template_directory(self): return Path(inspect.getfile(self.__class__)).parent / "terraform" - + def _attempt_keycloak_connection( self, keycloak_url, @@ -84,12 +84,10 @@ def _attempt_keycloak_connection( client_id=client_id, verify=verify, ) - c = realm_admin.get_client_id(CLIENT_NAME) # lookup client guid - existing_client = realm_admin.get_client(c) # query client info + c = realm_admin.get_client_id(CLIENT_NAME) # lookup client guid + existing_client = realm_admin.get_client(c) # query client info if existing_client != None and existing_client["name"] == CLIENT_NAME: - print( - f"Attempt {i+1} succeeded connecting to keycloak and nebari client={CLIENT_NAME} exists" - ) + print(f"Attempt {i+1} succeeded connecting to keycloak and nebari client={CLIENT_NAME} exists") return True else: print( @@ -99,28 +97,26 @@ def _attempt_keycloak_connection( print(f"Attempt {i+1} failed connecting to keycloak {client_realm_name} realm -- {e}") time.sleep(timeout) return False - + def check(self, stage_outputs: Dict[str, Dict[str, Any]], disable_prompt=False) -> bool: - + try: _ = self.config.escaped_project_name _ = self.config.provider - + except KeyError: - print( - "\nBase config values not found: escaped_project_name, provider" - ) + print("\nBase config values not found: escaped_project_name, provider") return False keycloak_config = self.get_keycloak_config(stage_outputs) if not self._attempt_keycloak_connection( - keycloak_url = keycloak_config["keycloak_url"], - username = keycloak_config["username"], - password = keycloak_config["password"], - master_realm_name = keycloak_config["master_realm_id"], - client_id = keycloak_config["master_client_id"], - client_realm_name = keycloak_config["realm_id"], + keycloak_url=keycloak_config["keycloak_url"], + username=keycloak_config["username"], + password=keycloak_config["password"], + master_realm_name=keycloak_config["master_realm_id"], + client_id=keycloak_config["master_client_id"], + client_realm_name=keycloak_config["realm_id"], verify=False, ): print( @@ -133,10 +129,10 @@ def check(self, stage_outputs: Dict[str, Dict[str, Any]], disable_prompt=False) def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): keycloak_config = self.get_keycloak_config(stage_outputs) - + try: domain = stage_outputs["stages/04-kubernetes-ingress"]["domain"] - + except KeyError: raise Exception("Prerequisite stage output(s) not found: stages/04-kubernetes-ingress") @@ -152,6 +148,7 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): "approved_domains": self.config.self_registration.approved_domains, "coupons": self.config.self_registration.coupons, "registration_group": self.config.self_registration.registration_group, + "registration_message": self.config.self_registration.registration_message, "project_name": self.config.escaped_project_name, "realm_id": keycloak_config["realm_id"], "client_id": CLIENT_NAME, @@ -163,11 +160,15 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): "overrides": self.config.self_registration.values, "affinity": { "enabled": self.config.self_registration.affinity.enabled, - "selector": self.config.self_registration.affinity.selector.__dict__ - if isinstance(self.config.self_registration.affinity.selector, SelfRegistrationAffinitySelectorConfig) - else self.config.self_registration.affinity.selector, + "selector": ( + self.config.self_registration.affinity.selector.__dict__ + if isinstance( + self.config.self_registration.affinity.selector, SelfRegistrationAffinitySelectorConfig + ) + else self.config.self_registration.affinity.selector + ), }, - + "theme": self.config.theme.jupyterhub.dict(), } def get_keycloak_config(self, stage_outputs: Dict[str, Dict[str, Any]]): @@ -182,7 +183,8 @@ def get_keycloak_config(self, stage_outputs: Dict[str, Dict[str, Any]]): "master_client_id": stage_outputs[directory]["keycloak_credentials"]["value"]["client_id"], "realm_id": stage_outputs["stages/06-kubernetes-keycloak-configuration"]["realm_id"]["value"], } - + + @hookimpl def nebari_stage() -> List[NebariStage]: - return [ SelfRegistrationStage ] + return [SelfRegistrationStage] diff --git a/src/nebari_plugin_self_registration/terraform/main.tf b/src/nebari_plugin_self_registration/terraform/main.tf index a81d8aa..834a133 100644 --- a/src/nebari_plugin_self_registration/terraform/main.tf +++ b/src/nebari_plugin_self_registration/terraform/main.tf @@ -21,10 +21,12 @@ module "self-registration" { ingress_host = var.ingress_host self_registration_sa_name = local.self_registration_sa_name registration_group = var.registration_group + registration_message = var.registration_message namespace = var.namespace keycloak_base_url = var.external_url keycloak_config = module.keycloak.config overrides = var.overrides realm_id = var.realm_id affinity = var.affinity + theme = var.theme } \ No newline at end of file diff --git a/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/Chart.yaml b/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/Chart.yaml index 83086ad..0230794 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/Chart.yaml +++ b/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.0.15 +version: 0.0.16 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.0.6" +appVersion: "0.0.8" diff --git a/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml b/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml index 719ea92..318c2a6 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml +++ b/src/nebari_plugin_self_registration/terraform/modules/self-registration/chart/values.yaml @@ -7,7 +7,7 @@ image: repository: ghcr.io/metrostar/nebari-self-registration pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "20240213-1718" + tag: "20240515-1204" imagePullSecrets: [] nameOverride: "" diff --git a/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf b/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf index b06e052..31e384d 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf +++ b/src/nebari_plugin_self_registration/terraform/modules/self-registration/main.tf @@ -79,12 +79,14 @@ resource "helm_release" "self_registration" { approved_domains = var.approved_domains account_expiration_days = var.account_expiration_days registration_group = var.registration_group + registration_message = var.registration_message keycloak = { server_url = var.keycloak_base_url realm_name = var.realm_id client_id = var.keycloak_config["client_id"] client_secret = var.keycloak_config["client_secret"] } + theme = var.theme } env = [ ] diff --git a/src/nebari_plugin_self_registration/terraform/modules/self-registration/variables.tf b/src/nebari_plugin_self_registration/terraform/modules/self-registration/variables.tf index b47a08e..02a7905 100644 --- a/src/nebari_plugin_self_registration/terraform/modules/self-registration/variables.tf +++ b/src/nebari_plugin_self_registration/terraform/modules/self-registration/variables.tf @@ -51,6 +51,12 @@ variable "registration_group" { type = string } +variable "registration_message" { + description = "Custom message to display to registering users" + type = string + default = "" +} + variable "self_registration_sa_name" { description = "Name of K8S service account for Self Registration app workloads" type = string @@ -77,3 +83,9 @@ variable "affinity" { error_message = "\"affinity.selector\" argument must be a string or object { default, app, job }" } } + +variable "theme" { + description = "Theme configured in theme.jupyterhub" + type = map(any) + default = {} +} \ No newline at end of file diff --git a/src/nebari_plugin_self_registration/terraform/variables.tf b/src/nebari_plugin_self_registration/terraform/variables.tf index 1e20dd7..a4b7a79 100644 --- a/src/nebari_plugin_self_registration/terraform/variables.tf +++ b/src/nebari_plugin_self_registration/terraform/variables.tf @@ -74,6 +74,12 @@ variable "registration_group" { default = "" } +variable "registration_message" { + description = "Custom message to display to registering users" + type = string + default = "" +} + variable "affinity" { type = object({ enabled = optional(bool, true) @@ -89,4 +95,10 @@ variable "affinity" { condition = can(tostring(var.affinity.selector)) || (can(var.affinity.selector.default) && length(try(var.affinity.selector.default, "")) > 0) error_message = "\"affinity.selector\" argument must be a string or object { default, app, job }" } -} \ No newline at end of file +} + +variable "theme" { + description = "Theme configured in theme.jupyterhub" + type = map(any) + default = {} +}