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
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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
-
-
-
-
-
-
-
-
-
-
-
-
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 }}
-
-
-
-
-
- Password copied successfully!
-
-
- 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.
-
-
-
-
-
-
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 %}
+
+
+
+
+ {% 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 %}
+
+
+
+
+ {% 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 }}
+
+
+
+
+
+ Password copied successfully!
+
+
+ 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.
+
+
+
+{% 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 = {}
+}