diff --git a/.github/workflows/release_web_app.yml b/.github/workflows/release_web_app.yml new file mode 100644 index 0000000..7c52153 --- /dev/null +++ b/.github/workflows/release_web_app.yml @@ -0,0 +1,133 @@ +name: Build & Publish Web App (Docker + Helm) + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Release tag in the form appName@x.y.z" + required: true + type: string + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + parse-tag: + name: Parse app name and version + runs-on: ubuntu-latest + outputs: + app: ${{ steps.parse.outputs.app }} + version: ${{ steps.parse.outputs.version }} + steps: + - name: Determine tag + id: tag + run: | + if [ "${{ github.event_name }}" = "release" ]; then + echo "TAG=${{ github.event.release.tag_name }}" >> $GITHUB_ENV + else + echo "TAG=${{ inputs.tag }}" >> $GITHUB_ENV + fi + + - name: Parse tag + id: parse + run: | + TAG="${TAG}" + + if [[ ! "$TAG" =~ ^[^@]+@(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$ ]]; then + echo "Invalid tag format: $TAG" + exit 1 + fi + + APP="${TAG%@*}" # remove shortest match of @* from the end of $TAG + VERSION="${TAG#*@}" # remove the shortest match *@ from the start of $TAG + + echo "app=$APP" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + + docker: + name: Build & Push Docker image + runs-on: ubuntu-latest + needs: parse-tag + + steps: + - uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.web-app + push: true + build-args: | + APP_NAME=${{ needs.parse-tag.outputs.app }} + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ needs.parse-tag.outputs.app }}:${{ needs.parse-tag.outputs.version }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ needs.parse-tag.outputs.app }}:latest + + helm: + name: Package & Push Helm chart + runs-on: ubuntu-latest + needs: parse-tag + + steps: + - uses: actions/checkout@v4 + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: v3.14.0 + + - name: Log in to GHCR (Helm OCI) + run: | + echo "${{ secrets.GHCR_PAT }}" | \ + helm registry login ghcr.io \ + --username ${{ github.actor }} \ + --password-stdin + + - name: Sync versions in Chart and Values + run: | + CHART_DIR="apps/${{ needs.parse-tag.outputs.app }}/helm" + VERSION="${{ needs.parse-tag.outputs.version }}" + + # 1. Update Chart.yaml + yq -i ".version = \"$VERSION\" | .appVersion = \"$VERSION\"" "$CHART_DIR/Chart.yaml" + + # 2. Update the image tag in values.yaml + yq -i ".ui-base.image.tag = \"$VERSION\"" "$CHART_DIR/values.yaml" + + - name: Build base chart dependencies + run: | + helm dependency build ./helm + + - name: Build chart dependencies + run: | + CHART_DIR="apps/${{ needs.parse-tag.outputs.app }}/helm" + helm dependency build "$CHART_DIR" + + - name: Package chart + run: | + CHART_DIR="apps/${{ needs.parse-tag.outputs.app }}/helm" + helm package "$CHART_DIR" \ + --version "${{ needs.parse-tag.outputs.version }}" \ + --app-version "${{ needs.parse-tag.outputs.version }}" + + - name: Push chart + run: | + CHART_PACKAGE=$(ls *.tgz) + + helm push "$CHART_PACKAGE" \ + oci://${{ env.REGISTRY }}/${{ github.repository }}/charts diff --git a/.gitignore b/.gitignore index 64718f1..268c42b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,8 @@ dist-ssr # Turborepo .turbo + +# Helm dependency build artifacts +/helm/charts/ +/apps/*/helm/charts/ +*.tgz diff --git a/README.md b/README.md new file mode 100644 index 0000000..19ce616 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# Atlas - Diamond II Science UI Monorepo + +**Atlas** is the monorepo for Diamond II science-facing web user interfaces. + +It provides a shared home for multiple applications built with TypeScript + React, using the sci-react-ui component library for consistent look, feel, and interaction patterns across beamlines and services. + +The goal is to keep UI development: + +- Centralised – all science UIs live in one repository +- Visible – teams can see, learn from, and contribute to each other’s work +- Reusable – shared components, patterns, and infrastructure +- Consistent – common authentication, layout, and deployment model + +## Tech stack + +- Frontend: React + Typescript +- UI components: `sci-react-ui`/`mui` +- Deployment: Kubernetes via Helm +- Authentication: OIDC (Keycloak) via `oauth2-proxy` + +## Repository structure + +``` +apps/ # Individual UI applications +helm/ # Shared base Helm chart (ui-base) +packages/ # Shared libraries/utilities (if applicable) +``` + +Each application under `apps/` is independently buildable and releasable, but follows shared conventions for layout, auth, and deployment. + +## Testing + +Frontend testing across the monorepo is standardised via [packages/vitest-conf](packages/vitest-conf/README.md). This package centralises shared test setup, including, Vitest configuration, jsdom test environment, Testing Library matchers and common setup. + +Applications and shared packages extend this configuration rather than redefining their own, which keeps test setup minimal, consistent, and easy to maintain across the repository. + +## Common deployment model + +All UIs are deployed behind a shared authentication and routing layer provided by the `ui-base` Helm chart. + +Application charts depend on this base chart and only need to declare: + +1. Which container image to run +2. Which backend services (“upstreams”) the UI talks to + +The base chart handles: + +- Ingress +- Service wiring +- OAuth2/OIDC login via Keycloak +- Reverse proxying to backend APIs + +This keeps app charts small, declarative, and consistent across the platform. + +## Releasing applications + +Applications are released using Git tags. Each app is versioned and released independently. + +### Tag format + +``` +@ +``` + +### Examples + +``` +visr@0.1.8 +swift@1.2.0-beta.2 +``` + +### What happens on release + +When a tag matching this format is pushed (or a release workflow is manually triggered with the same inputs), CI will: + +1. Parse the tag into application name and version +2. Build and publish the application using repo-level Dockerfile and nginx configuration +3. Update the app’s Helm chart: + - `Chart.yaml`: `version` and `appVersion` + - `values.yaml`: container image tag +4. Build Helm dependencies +5. Package application chart +6. Push the chart to the GitHub Container Registry (OCI) + +This ensures a single Git tag produces a versioned **container image** and a matching **Helm chart**. + +## Authentication + +All production deployments use OIDC authentication via Diamond’s Keycloak realms, enforced at the ingress layer using `oauth2-proxy`. Applications themselves do not need to implement login flows directly. + +## Contributing + +When building a new UI: + +- Follow existing app structure under apps/ +- Use components from sci-react-ui wherever possible +- Keep app-specific logic in the app, and reusable logic in shared packages + +Consistency across UIs is a core goal of this repository. diff --git a/docker/Dockerfile.web-app b/docker/Dockerfile.web-app new file mode 100644 index 0000000..5719602 --- /dev/null +++ b/docker/Dockerfile.web-app @@ -0,0 +1,44 @@ +FROM node:22-alpine AS base + +# Use a consistent working directory across all stages +WORKDIR /app + +# 1) Install dependencies +FROM base AS deps + +ARG APP_NAME + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc* ./ +COPY packages ./packages +COPY apps/${APP_NAME}/package.json ./apps/${APP_NAME}/ + +# Install dependencies +RUN corepack enable pnpm && pnpm i --frozen-lockfile --filter=@atlas/${APP_NAME}... + +# 2) Run the build +FROM base AS builder + +ARG APP_NAME + +# Copy all node_modules from deps stage +COPY --from=deps /app ./ + +# Copy source files +COPY apps/${APP_NAME} ./apps/${APP_NAME} +COPY packages ./packages +COPY turbo.json package.json pnpm-workspace.yaml tsconfig.json ./ +RUN corepack enable pnpm && pnpm --filter=@atlas/${APP_NAME}... build + +# 3) Create minimal image to serve the app +FROM nginxinc/nginx-unprivileged:1.25-alpine AS runner + +ARG APP_NAME + +# Copy built files to nginx web root +COPY --from=builder /app/apps/${APP_NAME}/dist /usr/share/nginx/html + +# Copy your custom nginx config +COPY docker/nginx.conf /etc/nginx/nginx.conf + +EXPOSE 8080 +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..00ce775 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,61 @@ +worker_processes auto; # Let Nginx decide based on CPU cores +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging to stdout/stderr is standard for Docker + access_log /dev/stdout; + error_log /dev/stderr warn; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip Compression - Critical for web app performance + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml image/svg+xml; + + # Temp paths for unprivileged user + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + server { + listen 8080; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA routing: try file, then directory, then fall back to index.html + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-store" always; + } + + # Cache hashed assets (JS/CSS/Images) for a long time + # This assumes your build tool (Vite/Webpack) uses hashes in filenames + location ~* \.(?:js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +} \ No newline at end of file diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..a5869b6 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: ui-base +description: Web UI behind oauth2-proxy +type: application +version: 0.1.0 +appVersion: "0.1.0" + +dependencies: + - name: oauth2-proxy + repository: https://oauth2-proxy.github.io/manifests + version: 10.1.0 \ No newline at end of file diff --git a/helm/README.md b/helm/README.md new file mode 100644 index 0000000..a4a6eec --- /dev/null +++ b/helm/README.md @@ -0,0 +1,243 @@ +# ui-base Helm Chart + +`ui-base` is a foundational Helm chart for deploying authenticated web UIs inside the platform. It encapsulates all Kubernetes and OAuth plumbing so that application charts can focus on just two concerns: + +1. **What container image should run the UI** +2. **What upstream services the UI needs to talk to** + +Everything else — ingress, service wiring, and OAuth2/OIDC authentication via **oauth2-proxy** — is handled centrally. + +This chart is intended to be used **as a dependency** by individual UI application charts in this monorepo. + +--- + +## What This Chart Provides + +When included as a dependency, `ui-base` deploys: + +- A **Deployment** running your UI container +- A **Service** exposing the UI internally +- An **Ingress** configured to: + - Route external traffic to the UI + - Enforce authentication via **oauth2-proxy** + +- An **oauth2-proxy** instance configured for Keycloak-based OIDC login +- Reverse-proxy routing from the UI path space to declared backend **upstreams** + +This creates a consistent, secure pattern for all web UIs. + +--- + +## How Application Charts Use `ui-base` + +Application charts depend on `ui-base` and provide their configuration under the `ui-base:` key in their own `values.yaml`. + +### Example Application Chart + +```yaml +apiVersion: v2 +name: visr +description: ViSR UI +type: application +version: 0.1.8 +appVersion: "0.1.8" + +dependencies: + - name: ui-base + repository: "file://../../../helm/" + version: 0.1.0 +``` + +```yaml +ui-base: + name: visr + host: douglas.diamond.ac.uk + + image: + repository: ghcr.io/douglaswinter/atlas/visr + tag: 0.1.8 + + upstreams: + - id: blueapi + path: /api/ + rewriteTarget: / + target: + external: + uri: http://b01-1-blueapi.diamond.ac.uk + + - id: workflows + path: /api/workflows + rewriteTarget: /graphql + target: + external: + uri: https://workflows.diamond.ac.uk + + - id: data + path: /api/data/ + rewriteTarget: / + target: + service: + name: dataserver + port: 8000 + + - id: supergraph + path: /api/graphql + rewriteTarget: / + target: + external: + uri: https://graph-nightly.diamond.ac.uk + passHostHeader: false + + identityProvider: legacy +``` + +From the app chart’s perspective, this is all that is required to get a fully authenticated UI deployed. + +--- + +## Core Values (ui-base) + +These are the main values exposed by `ui-base` and typically set by the parent application chart. + +### Basic App Configuration + +| Key | Description | +| ------------------ | ------------------------------------------------------------ | +| `name` | Logical name of the UI application (used in resource naming) | +| `image.repository` | Container image repository | +| `image.tag` | Container image tag | +| `image.pullPolicy` | Kubernetes image pull policy | +| `replicaCount` | Number of UI pod replicas | +| `ui.port` | Port your UI container listens on (default `8080`) | +| `host` | External hostname used for ingress | + +--- + +### Upstreams + +The `upstreams` list defines backend services that are exposed behind the same authenticated domain. + +Each upstream defines a path on the public host that is proxied to either a Kubernetes Service or an external URL. + +```yaml +upstreams: + - id: data + path: /api/data/ + rewriteTarget: / + target: + service: + name: dataserver + port: 8000 +``` + +#### Fields + +| Field | Description | +| --------------------- | --------------------------------------------------- | +| `id` | Unique identifier used in generated config | +| `path` | Public path prefix | +| `rewriteTarget` | Path rewrite rule before forwarding upstream | +| `passHostHeader` | Whether to pass original Host header (default true) | +| `target.service` | Route to a Kubernetes Service | +| `target.external.uri` | Route to an external HTTP(S) endpoint | + +--- + +## Authentication & Identity Providers + +Authentication is handled by **oauth2-proxy** using OIDC against Keycloak realms. + +### Selecting an Identity Provider + +```yaml +identityProviders: + prod: https://identity.diamond.ac.uk/realms/dls + test: https://identity-test.diamond.ac.uk/realms/dls + dev: https://identity-dev.diamond.ac.uk/realms/dls + legacy: https://authn.diamond.ac.uk/realms/master + +identityProvider: prod +``` + +Application charts select the realm using: + +```yaml +ui-base: + identityProvider: legacy +``` + +--- + +## Required Secrets + +This chart expects certain secrets to already exist in the target namespace. + +### `ui-keycloak-secret` (Required) + +`oauth2-proxy` is configured to read its client credentials from a secret named: + +``` +ui-keycloak-secret +``` + +This secret must contain the Keycloak OIDC client configuration (client ID, client secret, [cookie secret](https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview/#generating-a-cookie-secret)). + +It must be created **before** installing the chart, unless it is supplied by another chart in the same release. + +--- + +### Other oauth2-proxy Config Secrets + +`ui-base` also references: + +- `oauth2-proxy-config` +- `ui-oauth2-alpha-secret` + +These are used for advanced oauth2-proxy configuration. + +--- + +## ⚠ Namespace Limitation + +Because several resources use **fixed names**, only **one `ui-base` deployment per namespace** is currently supported. + +Conflicting shared resource names include: + +- `ui-oauth2-alpha-secret` +- `oauth2-proxy-config` + +Installing multiple releases of charts that depend on `ui-base` into the **same namespace** will cause resource name collisions unless the chart is modified to parameterize these names. + +--- + +## oauth2-proxy Subchart + +`ui-base` depends on the upstream `oauth2-proxy` Helm chart: + +```yaml +dependencies: + - name: oauth2-proxy + repository: https://oauth2-proxy.github.io/manifests + version: 10.1.0 +``` + +It is enabled by default and preconfigured with: + +- Standard, request, and auth logging +- Provider button skipped +- Debug output on auth errors +- Service port `4180` + +Ingress for oauth2-proxy itself is disabled; traffic flows through the main UI ingress. + +--- + +## Summary + +`ui-base` standardizes how authenticated UIs are deployed: + +- Application charts declare **image + upstreams** +- `ui-base` handles **auth, ingress, routing, and services** +- Security and identity integration stay consistent across all UIs + +This keeps app charts small, declarative, and focused purely on application behavior rather than infrastructure. diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl new file mode 100644 index 0000000..5d63e4a --- /dev/null +++ b/helm/templates/_helpers.tpl @@ -0,0 +1,82 @@ +{{/* +Chart name +*/}} +{{- define "ui-base.name" -}} +{{- .Values.name | default .Chart.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Fully qualified release name +*/}} +{{- define "ui-base.fullname" -}} +{{- printf "%s-%s" .Release.Name (include "ui-base.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Return the container image reference. +Defaults tag to .Chart.AppVersion if empty. +*/}} +{{- define "ui-base.image" -}} +{{- $repo := .Values.image.repository -}} +{{- $tag := .Values.image.tag | default .Chart.AppVersion -}} +{{ printf "%s:%s" $repo $tag }} +{{- end -}} + + +{{/* +Return the number of replicas. +*/}} +{{- define "ui-base.replicas" -}} +{{- .Values.replicas | default 1 -}} +{{- end -}} + +{{/* +Common labels applied to all resources +*/}} +{{- define "ui-base.labels" -}} +app.kubernetes.io/name: {{ include "ui-base.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "ui-base.renderUpstreams" -}} + +# Base UI upstream (always present) +- id: ui + path: / + uri: http://{{ include "ui-base.name" . }}.{{ .Release.Namespace }}.svc.cluster.local:80 + passHostHeader: true + +{{- range (.Values.upstreams | default list) }} +- id: {{ .id }} + path: {{ .path }} + uri: {{ include "ui-base.upstreamUri" (dict "target" .target "Release" $.Release) }} + {{- if .rewriteTarget }} + rewriteTarget: {{ .rewriteTarget }} + {{- end }} + passHostHeader: {{ .passHostHeader | default false }} +{{- end }} + +{{- end }} + + +{{- define "ui-base.upstreamUri" -}} +{{- if .target.service -}} +http://{{ .target.service.name }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .target.service.port }} +{{- else if .target.external -}} +{{ .target.external.uri }} +{{- end -}} +{{- end }} + +{{- define "ui-base.identityIssuerURL" -}} +{{- $provider := .Values.identityProvider -}} +{{- $providers := .Values.identityProviders -}} + +{{- if not (hasKey $providers $provider) -}} +{{- fail (printf "Invalid provider '%s'. Must be one of: %s" + $provider (keys $providers | sortAlpha | join ", ")) -}} +{{- end -}} + +{{- index $providers $provider -}} +{{- end -}} \ No newline at end of file diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml new file mode 100644 index 0000000..a47fe49 --- /dev/null +++ b/helm/templates/configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: oauth2-proxy-config +data: + oauth2_proxy.cfg: | + email_domains = ["*"] + cookie_secure = true + cookie_samesite = "lax" + cookie_expire = "168h" + cookie_refresh = "45s" + cookie_name = "{{ include "oauth2-proxy.fullname" . }}-cookie" \ No newline at end of file diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 0000000..110255d --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment + +metadata: + name: {{ include "ui-base.name" . }} + namespace: {{ .Release.Namespace }} +spec: + replicas: {{ include "ui-base.replicas" . }} + selector: + matchLabels: + app: {{ include "ui-base.name" . }} + template: + metadata: + labels: + app: {{ include "ui-base.name" . }} + spec: + containers: + - name: {{ include "ui-base.name" . }} + image: {{ include "ui-base.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.ui.port }} \ No newline at end of file diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml new file mode 100644 index 0000000..d1ca20f --- /dev/null +++ b/helm/templates/ingress.yaml @@ -0,0 +1,24 @@ +{{ if .Values.host }} +apiVersion: {{ include "capabilities.ingress.apiVersion" . }} +kind: Ingress +metadata: + name: {{ include "ui-base.name" . }} + labels: + {{ include "ui-base.labels" . | nindent 4 }} + annotations: + nginx.ingress.kubernetes.io/proxy-buffer-size: "8k" +spec: + ingressClassName: nginx + rules: + - host: {{ .Values.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: +{{ include "ingress.backend" (dict + "serviceName" (printf "%s-oauth2-proxy" .Release.Name ) + "servicePort" (index .Values "oauth2-proxy" "service" "portNumber") + "context" $ +) | nindent 14 }} +{{ end }} diff --git a/helm/templates/oauth2-alpha-secret.yaml b/helm/templates/oauth2-alpha-secret.yaml new file mode 100644 index 0000000..787c9fb --- /dev/null +++ b/helm/templates/oauth2-alpha-secret.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: Secret +metadata: + name: ui-oauth2-alpha-secret + labels: + {{- include "ui-base.labels" . | nindent 4 }} +type: Opaque +stringData: + oauth2_proxy.yml: | + server: + BindAddress: "0.0.0.0:{{ index .Values "oauth2-proxy" "service" "portNumber" }}" + + metricsServer: + BindAddress: "0.0.0.0:44180" + + injectRequestHeaders: + - name: Authorization + values: + - claim: access_token + prefix: "Bearer " + - name: X-Auth-Request-User + values: + - claim: user + - name: X-Auth-Request-Email + values: + - claim: email + + providers: + - provider: oidc + id: {{ .Values.identityProvider }} + clientID: ${OAUTH2_PROXY_CLIENT_ID} + clientSecret: ${OAUTH2_PROXY_CLIENT_SECRET} + scope: openid email profile offline_access + oidcConfig: + issuerURL: {{ include "ui-base.identityIssuerURL" . | quote }} + insecureAllowUnverifiedEmail: true + insecureSkipNonce: true + emailClaim: sub + audienceClaims: ["aud"] + + upstreamConfig: + proxyRawPath: false + upstreams: +{{ include "ui-base.renderUpstreams" . | indent 8 }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 0000000..8e62008 --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ui-base.name" . }} + namespace: {{ .Release.Namespace }} +spec: + type: ClusterIP + selector: + app: {{ include "ui-base.name" . }} + ports: + - port: 80 + targetPort: {{ .Values.ui.port }} diff --git a/helm/values.schema.json b/helm/values.schema.json new file mode 100644 index 0000000..fb4b3c5 --- /dev/null +++ b/helm/values.schema.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UI Base Chart Values", + "type": "object", + "required": ["name", "image", "host", "identityProvider"], + "properties": { + "name": { + "type": "string", + "description": "The name of the application deployment." + }, + "host": { + "type": "string", + "description": "The FQDN for the application ingress (e.g., visr.diamond.ac.uk)." + }, + "replicaCount": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "image": { + "type": "object", + "required": ["repository", "tag"], + "properties": { + "repository": { "type": "string" }, + "tag": { "type": "string" }, + "pullPolicy": { + "type": "string", + "enum": ["Always", "Never", "IfNotPresent"], + "default": "IfNotPresent" + } + } + }, + "ui": { + "type": "object", + "properties": { + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "default": 8080 + } + } + }, + "identityProvider": { + "type": "string", + "description": "The key of the identity provider to use from the identityProviders list.", + "enum": ["prod", "test", "dev", "legacy"] + }, + "identityProviders": { + "type": "object", + "additionalProperties": { + "type": "string", + "format": "uri" + } + }, + "upstreams": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "path", "target"], + "properties": { + "id": { "type": "string" }, + "path": { "type": "string" }, + "rewriteTarget": { "type": "string" }, + "sse": { "type": "boolean" }, + "passHostHeader": { "type": "boolean" }, + "target": { + "type": "object", + "oneOf": [ + { + "required": ["external"], + "properties": { + "external": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { "type": "string", "format": "uri" } + } + } + } + }, + { + "required": ["service"], + "properties": { + "service": { + "type": "object", + "required": ["name", "port"], + "properties": { + "name": { "type": "string" }, + "port": { "type": "integer", "minimum": 1 } + } + } + } + } + ] + } + } + } + }, + "oauth2-proxy": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "config": { + "type": "object", + "properties": { + "existingSecret": { "type": "string" }, + "existingConfig": { "type": "string" } + } + } + } + } + } +} \ No newline at end of file diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000..1d151eb --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,47 @@ +name: ui-base + +image: + repository: "" + tag: "" + pullPolicy: IfNotPresent + +host: "" + +replicaCount: 1 + +ui: + port: 8080 + +upstreams: [] + +identityProviders: + prod: https://identity.diamond.ac.uk/realms/dls + test: https://identity-test.diamond.ac.uk/realms/dls + dev: https://identity-dev.diamond.ac.uk/realms/dls + legacy: https://authn.diamond.ac.uk/realms/master + +identityProvider: prod + +oauth2-proxy: + enabled: true + + extraArgs: + skip-provider-button: "true" + standard-logging: "true" + request-logging: "true" + auth-logging: "true" + show-debug-on-error: "true" + + ingress: + enabled: false + + config: + existingSecret: ui-keycloak-secret + existingConfig: oauth2-proxy-config + + alphaConfig: + enabled: true + existingSecret: ui-oauth2-alpha-secret + + service: + portNumber: 4180 \ No newline at end of file diff --git a/packages/vitest-conf/package.json b/packages/vitest-conf/package.json index a80d95b..446157e 100644 --- a/packages/vitest-conf/package.json +++ b/packages/vitest-conf/package.json @@ -34,6 +34,7 @@ "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.0.10", "jsdom": "^26.1.0" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c54bbb..1577570 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,13 +44,13 @@ importers: version: 8.44.1(eslint@9.36.0)(typescript@5.9.2) vitest: specifier: ^4.0.13 - version: 4.0.13(@types/node@24.5.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2)) + version: 4.0.13(@types/node@25.0.10)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.11.3(@types/node@25.0.10)(typescript@5.9.2)) packages/vitest-conf: dependencies: vitest: specifier: '*' - version: 3.2.4(@types/node@24.5.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2)) + version: 3.2.4(@types/node@25.0.10)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.11.3(@types/node@25.0.10)(typescript@5.9.2)) devDependencies: '@testing-library/dom': specifier: ^10.4.1 @@ -64,6 +64,9 @@ importers: '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) + '@types/node': + specifier: ^25.0.10 + version: 25.0.10 jsdom: specifier: ^26.1.0 version: 26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -555,8 +558,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@24.5.2': - resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} + '@types/node@25.0.10': + resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==} '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -1499,8 +1502,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@7.12.0: - resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} @@ -1891,34 +1894,34 @@ snapshots: '@inquirer/ansi@1.0.0': optional: true - '@inquirer/confirm@5.1.18(@types/node@24.5.2)': + '@inquirer/confirm@5.1.18(@types/node@25.0.10)': dependencies: - '@inquirer/core': 10.2.2(@types/node@24.5.2) - '@inquirer/type': 3.0.8(@types/node@24.5.2) + '@inquirer/core': 10.2.2(@types/node@25.0.10) + '@inquirer/type': 3.0.8(@types/node@25.0.10) optionalDependencies: - '@types/node': 24.5.2 + '@types/node': 25.0.10 optional: true - '@inquirer/core@10.2.2(@types/node@24.5.2)': + '@inquirer/core@10.2.2(@types/node@25.0.10)': dependencies: '@inquirer/ansi': 1.0.0 '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@24.5.2) + '@inquirer/type': 3.0.8(@types/node@25.0.10) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.5.2 + '@types/node': 25.0.10 optional: true '@inquirer/figures@1.0.13': optional: true - '@inquirer/type@3.0.8(@types/node@24.5.2)': + '@inquirer/type@3.0.8(@types/node@25.0.10)': optionalDependencies: - '@types/node': 24.5.2 + '@types/node': 25.0.10 optional: true '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2074,10 +2077,9 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@24.5.2': + '@types/node@25.0.10': dependencies: - undici-types: 7.12.0 - optional: true + undici-types: 7.16.0 '@types/prop-types@15.7.15': {} @@ -2203,23 +2205,23 @@ snapshots: chai: 6.2.1 tinyrainbow: 3.0.3 - '@vitest/mocker@3.2.4(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(vite@7.1.7(@types/node@24.5.2))': + '@vitest/mocker@3.2.4(msw@2.11.3(@types/node@25.0.10)(typescript@5.9.2))(vite@7.1.7(@types/node@25.0.10))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - msw: 2.11.3(@types/node@24.5.2)(typescript@5.9.2) - vite: 7.1.7(@types/node@24.5.2) + msw: 2.11.3(@types/node@25.0.10)(typescript@5.9.2) + vite: 7.1.7(@types/node@25.0.10) - '@vitest/mocker@4.0.13(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(vite@7.1.7(@types/node@24.5.2))': + '@vitest/mocker@4.0.13(msw@2.11.3(@types/node@25.0.10)(typescript@5.9.2))(vite@7.1.7(@types/node@25.0.10))': dependencies: '@vitest/spy': 4.0.13 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.11.3(@types/node@24.5.2)(typescript@5.9.2) - vite: 7.1.7(@types/node@24.5.2) + msw: 2.11.3(@types/node@25.0.10)(typescript@5.9.2) + vite: 7.1.7(@types/node@25.0.10) '@vitest/pretty-format@3.2.4': dependencies: @@ -2736,11 +2738,11 @@ snapshots: ms@2.1.3: {} - msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2): + msw@2.11.3(@types/node@25.0.10)(typescript@5.9.2): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 - '@inquirer/confirm': 5.1.18(@types/node@24.5.2) + '@inquirer/confirm': 5.1.18(@types/node@25.0.10) '@mswjs/interceptors': 0.39.6 '@open-draft/deferred-promise': 2.2.0 '@types/cookie': 0.6.0 @@ -3077,8 +3079,7 @@ snapshots: typescript@5.9.2: {} - undici-types@7.12.0: - optional: true + undici-types@7.16.0: {} until-async@3.0.2: optional: true @@ -3092,13 +3093,13 @@ snapshots: node-gyp-build: 4.8.4 optional: true - vite-node@3.2.4(@types/node@24.5.2): + vite-node@3.2.4(@types/node@25.0.10): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.7(@types/node@24.5.2) + vite: 7.1.7(@types/node@25.0.10) transitivePeerDependencies: - '@types/node' - jiti @@ -3113,7 +3114,7 @@ snapshots: - tsx - yaml - vite@7.1.7(@types/node@24.5.2): + vite@7.1.7(@types/node@25.0.10): dependencies: esbuild: 0.25.10 fdir: 6.5.0(picomatch@4.0.3) @@ -3122,14 +3123,14 @@ snapshots: rollup: 4.52.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.5.2 + '@types/node': 25.0.10 fsevents: 2.3.3 - vitest@3.2.4(@types/node@24.5.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2)): + vitest@3.2.4(@types/node@25.0.10)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.11.3(@types/node@25.0.10)(typescript@5.9.2)): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(vite@7.1.7(@types/node@24.5.2)) + '@vitest/mocker': 3.2.4(msw@2.11.3(@types/node@25.0.10)(typescript@5.9.2))(vite@7.1.7(@types/node@25.0.10)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -3147,11 +3148,11 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.7(@types/node@24.5.2) - vite-node: 3.2.4(@types/node@24.5.2) + vite: 7.1.7(@types/node@25.0.10) + vite-node: 3.2.4(@types/node@25.0.10) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.5.2 + '@types/node': 25.0.10 jsdom: 26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - jiti @@ -3167,10 +3168,10 @@ snapshots: - tsx - yaml - vitest@4.0.13(@types/node@24.5.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2)): + vitest@4.0.13(@types/node@25.0.10)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(msw@2.11.3(@types/node@25.0.10)(typescript@5.9.2)): dependencies: '@vitest/expect': 4.0.13 - '@vitest/mocker': 4.0.13(msw@2.11.3(@types/node@24.5.2)(typescript@5.9.2))(vite@7.1.7(@types/node@24.5.2)) + '@vitest/mocker': 4.0.13(msw@2.11.3(@types/node@25.0.10)(typescript@5.9.2))(vite@7.1.7(@types/node@25.0.10)) '@vitest/pretty-format': 4.0.13 '@vitest/runner': 4.0.13 '@vitest/snapshot': 4.0.13 @@ -3187,10 +3188,10 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.1.7(@types/node@24.5.2) + vite: 7.1.7(@types/node@25.0.10) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.5.2 + '@types/node': 25.0.10 jsdom: 26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - jiti