diff --git a/.gitignore b/.gitignore index a6e7e83..d01554b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ build/ secrets/ fournos.log fournos.pid +.vscode/ diff --git a/fournos-ui/.gitignore b/fournos-ui/.gitignore index bb12cb0..01391ee 100644 --- a/fournos-ui/.gitignore +++ b/fournos-ui/.gitignore @@ -33,4 +33,5 @@ app/mock_data.py kustomize/overlays/*/params.env kustomize/overlays/*/projects.yaml kustomize/overlays/*/kustomization.yaml +kustomize/overlays/*/oauth-cookie-secret.yaml kustomize/overlays/*/rolebinding-*.yaml diff --git a/fournos-ui/README.md b/fournos-ui/README.md index 1df9566..0eb1dc9 100644 --- a/fournos-ui/README.md +++ b/fournos-ui/README.md @@ -14,15 +14,17 @@ A web dashboard for managing [Fournos](https://github.com/openshift-psap/fournos ## Architecture ``` -┌─────────────┐ ┌──────────────────┐ ┌────────────┐ -│ Browser │────▶│ FastAPI + HTMX │────▶│ Kubernetes │ -│ │◀────│ (Dashboard) │◀────│ API │ -└─────────────┘ └────────┬─────────┘ └────────────┘ - │ - ┌────────▼─────────┐ - │ PostgreSQL │ - │ (job history) │ - └──────────────────┘ + ┌─── Pod ──────────────────────────────┐ +┌─────────┐ ┌───────┐ │ ┌─────────────┐ ┌───────────────┐ │ ┌────────────┐ +│ Browser │──▶│ Route │──┼▶│ OAuth Proxy │──▶│ FastAPI + HTMX│─┼──▶│ Kubernetes │ +│ │◀──│ (TLS) │◀─┼─│ (:8443) │◀──│ (:8000) │◀┼───│ API │ +└─────────┘ └───────┘ │ └──────┬──────┘ └───────┬───────┘ │ └────────────┘ + │ │ │ │ + │ ▼ ▼ │ + │ OpenShift OAuth ┌────────────┐ │ + │ Server │ PostgreSQL │ │ + │ └────────────┘ │ + └──────────────────────────────────────┘ ``` - **FastAPI** backend with **Jinja2** templates and **HTMX** for dynamic updates. @@ -32,59 +34,161 @@ A web dashboard for managing [Fournos](https://github.com/openshift-psap/fournos ## Prerequisites -- A Kubernetes / OpenShift cluster with the [Fournos Operator](https://github.com/openshift-psap/fournos-operator) installed. +- An OpenShift cluster (4.14+) with the [Fournos Operator](https://github.com/openshift-psap/fournos-operator) installed. +- **cert-manager** operator installed on the cluster (for TLS certificate issuance). - A container registry to push the dashboard image. -- `kubectl` or `oc` CLI configured with cluster access. +- `oc` CLI configured with cluster-admin access (needed for initial setup). ## Getting Started -### 1. Clone and configure the overlay +### 1. Configure the overlay ```bash cd kustomize/overlays/ocp/ +``` + +Create the required config files from the examples: -# Copy example files +```bash cp kustomization.yaml.example kustomization.yaml +cp oauth-cookie-secret.yaml.example oauth-cookie-secret.yaml cp projects.yaml.example projects.yaml -cp params.env.example params.env cp ../../base/postgresql-secret.env.example postgresql-secret.env ``` +These files are gitignored because they contain secrets or cluster-specific values. +For an existing deployment, you can pull values from the cluster instead (see "Pulling config from a live cluster" below). + Edit each file with your values: -- **`kustomization.yaml`** -- Set your dashboard image, PostgreSQL image, target namespace, and storage class. - **`projects.yaml`** -- Define your Forge projects, clusters, and presets. -- **`postgresql-secret.env`** -- Set your database credentials. -- **`params.env`** -- Set your storage class and size. +- **`postgresql-secret.env`** -- Set your database credentials (PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE). + +Edit `kustomization.yaml` and replace these values: +- Dashboard container image (e.g. `quay.io/your-org/fournos-dashboard:latest`) +- `FOURNOS_NAMESPACE` -- the namespace where FournosJobs run +- `storageClassName` -- your cluster's storage class (`oc get sc` to list) + +Generate the OAuth cookie secret: + +```bash +# Replace the placeholder in oauth-cookie-secret.yaml +COOKIE=$(openssl rand -base64 32) +# macOS: +sed -i '' "s|REPLACE_ME_WITH_OUTPUT_OF_openssl_rand_base64_32|${COOKIE}|" oauth-cookie-secret.yaml +# Linux: +# sed -i "s|REPLACE_ME_WITH_OUTPUT_OF_openssl_rand_base64_32|${COOKIE}|" oauth-cookie-secret.yaml +``` + +Verify the OAuth proxy image matches your cluster version: + +```bash +# Get the correct image for your cluster (digest may differ per OCP version) +oc adm release info --image-for=oauth-proxy +``` + +If the output differs from what's in `patch-deployment-oauth-proxy.yaml`, update the image field in that file. ### 2. Build and push the dashboard image -### 3. Deploy to the cluster +```bash +podman build -t quay.io/your-org/fournos-dashboard:latest . +podman push quay.io/your-org/fournos-dashboard:latest +``` + +### 3. Deploy TLS certificate (one-time) + +The dashboard uses a Let's Encrypt certificate for trusted HTTPS. This requires cert-manager to be installed on the cluster. ```bash -cd kustomize/overlays/ocp/ +# Apply the ClusterIssuer (cluster-scoped, only needed once) +oc apply -f kustomize/overlays/ocp/letsencrypt-clusterissuer.yaml + +# Verify it's ready +oc get clusterissuer letsencrypt-production +``` + +### 4. Deploy to the cluster +```bash # Apply the main stack -oc kustomize . | oc apply -f - +oc apply -k kustomize/overlays/ocp/ # Apply the cross-namespace RoleBinding (grants dashboard access to the jobs namespace) -oc apply -f rolebinding-psap-automation.yaml +# Replace FOURNOS_NAMESPACE with your target namespace (e.g. psap-automation) +oc apply -f - < ``` -Open http://localhost:8000 +You will be redirected to the OpenShift login page. After authenticating with your cluster credentials, you'll land on the dashboard. Any user who can log into the OpenShift cluster can access the UI. + +### Pulling config from a live cluster + +If the dashboard is already deployed and you need to recreate the overlay files: + +```bash +# projects.yaml +oc get configmap fournos-projects -n fournos-dashboard -o jsonpath='{.data.projects\.yaml}' > projects.yaml + +# postgresql-secret.env +oc get secret postgresql-secret -n fournos-dashboard -o go-template='PGHOST={{index .data "PGHOST" | base64decode}} +PGPORT={{index .data "PGPORT" | base64decode}} +PGUSER={{index .data "PGUSER" | base64decode}} +PGPASSWORD={{index .data "PGPASSWORD" | base64decode}} +PGDATABASE={{index .data "PGDATABASE" | base64decode}} +' > postgresql-secret.env + +# Dashboard image +oc get deployment fournos-dashboard -n fournos-dashboard \ + -o jsonpath='{.spec.template.spec.containers[?(@.name=="dashboard")].image}' + +# Storage class +oc get pvc -n fournos-dashboard -o jsonpath='{.items[0].spec.storageClassName}' +``` ## Configuration @@ -101,9 +205,17 @@ All configuration is via environment variables (set in the deployment manifest): | `KUBECONFIG` | Path to kubeconfig (local dev only) | in-cluster config | | `FORGE_GITHUB_REPO` | GitHub `owner/repo` for PR listing | `openshift-psap/forge` | -## Security Considerations +## Security + +Authentication is handled by the **OpenShift OAuth proxy** sidecar container. The proxy intercepts all requests to the Route, redirects unauthenticated users to the OpenShift login page, and only forwards traffic to the FastAPI app after successful authentication. + +- **Who can access:** Any user who can authenticate to the OpenShift cluster. +- **TLS:** The Route uses a Let's Encrypt certificate (auto-renewed by cert-manager). Traffic between the Route and the pod is re-encrypted using a service-ca cert. +- **Local dev bypass:** When developing locally or using `oc port-forward` to port 8000, the OAuth proxy is bypassed entirely (traffic goes directly to FastAPI). + + + -This dashboard is designed as an **internal tool** and does **not** include built-in authentication or authorization. As described above, the tool is accessible when port-forwarding from the cluster where it's running. Future development may include auth. ## Local Development @@ -130,7 +242,17 @@ fournos-ui/ │ └── templates/ # Jinja2 HTML templates ├── kustomize/ │ ├── base/ # Generic K8s manifests -│ └── overlays/ocp/ # Environment-specific overrides +│ └── overlays/ocp/ # OpenShift deployment overlay +│ ├── kustomization.yaml +│ ├── dashboard-route.yaml # Route with cert-manager annotations +│ ├── dashboard-certificate.yaml # Let's Encrypt Certificate CR +│ ├── letsencrypt-clusterissuer.yaml # ACME ClusterIssuer (apply separately) +│ ├── oauth-cookie-secret.yaml # OAuth proxy session secret +│ ├── patch-deployment-oauth-proxy.yaml # Adds OAuth sidecar to Deployment +│ ├── patch-service-oauth.yaml # Adds TLS port to Service +│ ├── patch-serviceaccount-oauth.yaml # Adds OAuth redirect annotation +│ ├── projects.yaml # (user-created) project config +│ └── postgresql-secret.env # (user-created) DB credentials ├── Dockerfile └── requirements.txt ``` diff --git a/fournos-ui/kustomize/overlays/ocp/dashboard-certificate.yaml b/fournos-ui/kustomize/overlays/ocp/dashboard-certificate.yaml new file mode 100644 index 0000000..b3d2817 --- /dev/null +++ b/fournos-ui/kustomize/overlays/ocp/dashboard-certificate.yaml @@ -0,0 +1,13 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: fournos-dashboard-cert + labels: + app.kubernetes.io/name: fournos-dashboard +spec: + secretName: fournos-dashboard-letsencrypt-tls + issuerRef: + name: letsencrypt-production + kind: ClusterIssuer + dnsNames: + - fournos-dashboard.apps.CLUSTER_DOMAIN diff --git a/fournos-ui/kustomize/overlays/ocp/dashboard-route.yaml b/fournos-ui/kustomize/overlays/ocp/dashboard-route.yaml new file mode 100644 index 0000000..1f10ca3 --- /dev/null +++ b/fournos-ui/kustomize/overlays/ocp/dashboard-route.yaml @@ -0,0 +1,19 @@ +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: fournos-dashboard + labels: + app.kubernetes.io/name: fournos-dashboard + annotations: + cert-manager.io/issuer-kind: ClusterIssuer + cert-manager.io/issuer-name: letsencrypt-production +spec: + host: fournos-dashboard.apps.CLUSTER_DOMAIN + to: + kind: Service + name: fournos-dashboard + weight: 100 + port: + targetPort: https + tls: + termination: reencrypt diff --git a/fournos-ui/kustomize/overlays/ocp/kustomization.yaml.example b/fournos-ui/kustomize/overlays/ocp/kustomization.yaml.example index 1c6d7fe..c7b4a4d 100644 --- a/fournos-ui/kustomize/overlays/ocp/kustomization.yaml.example +++ b/fournos-ui/kustomize/overlays/ocp/kustomization.yaml.example @@ -5,6 +5,11 @@ namespace: fournos-dashboard resources: - ../../base + - dashboard-route.yaml + - oauth-cookie-secret.yaml + - dashboard-certificate.yaml + # NOTE: letsencrypt-clusterissuer.yaml is cluster-scoped and must be applied + # separately: oc apply -f kustomize/overlays/ocp/letsencrypt-clusterissuer.yaml secretGenerator: - name: postgresql-secret @@ -24,6 +29,18 @@ configMapGenerator: app.kubernetes.io/name: fournos-dashboard patches: + # ServiceAccount: add OAuth redirect annotation + - path: patch-serviceaccount-oauth.yaml + target: + kind: ServiceAccount + name: fournos-dashboard + + # Service: add TLS cert annotation + change port to 8443 + - path: patch-service-oauth.yaml + target: + kind: Service + name: fournos-dashboard + # Dashboard container image -- replace with your registry - target: kind: Deployment @@ -32,7 +49,8 @@ patches: - op: replace path: /spec/template/spec/containers/0/image value: YOUR_REGISTRY/fournos-dashboard:latest - # Init container image (optional -- override if you need a subscription-gated image) + + # Init container image - target: kind: Deployment name: fournos-dashboard @@ -40,7 +58,8 @@ patches: - op: replace path: /spec/template/spec/initContainers/0/image value: postgres:15-alpine - # FOURNOS_NAMESPACE -- the namespace where FournosJobs run (env[6] in base) + + # FOURNOS_NAMESPACE - target: kind: Deployment name: fournos-dashboard @@ -48,7 +67,8 @@ patches: - op: replace path: /spec/template/spec/containers/0/env/6/value value: YOUR_FOURNOS_NAMESPACE - # PostgreSQL image (optional) + + # PostgreSQL image - target: kind: StatefulSet name: postgresql @@ -56,7 +76,8 @@ patches: - op: replace path: /spec/template/spec/containers/0/image value: postgres:15-alpine - # Storage class -- replace with your cluster's storage class + + # Storage class - target: kind: StatefulSet name: postgresql @@ -65,9 +86,8 @@ patches: path: /spec/volumeClaimTemplates/0/spec/storageClassName value: YOUR_STORAGE_CLASS -# NOTE: The cross-namespace RoleBinding (granting access to YOUR_FOURNOS_NAMESPACE) -# must be applied separately since kustomize's global namespace override interferes: -# -# cp rolebinding-psap-automation.yaml rolebinding-YOUR_NAMESPACE.yaml -# # Edit the file: set metadata.namespace to YOUR_FOURNOS_NAMESPACE -# oc apply -f rolebinding-YOUR_NAMESPACE.yaml + # Deployment: add oauth-proxy sidecar container + TLS volume (must be last) + - path: patch-deployment-oauth-proxy.yaml + target: + kind: Deployment + name: fournos-dashboard diff --git a/fournos-ui/kustomize/overlays/ocp/letsencrypt-clusterissuer.yaml b/fournos-ui/kustomize/overlays/ocp/letsencrypt-clusterissuer.yaml new file mode 100644 index 0000000..c4a9419 --- /dev/null +++ b/fournos-ui/kustomize/overlays/ocp/letsencrypt-clusterissuer.yaml @@ -0,0 +1,14 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-production +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: YOUR_EMAIL@example.com + privateKeySecretRef: + name: letsencrypt-production-account-key + solvers: + - http01: + ingress: + ingressClassName: openshift-default diff --git a/fournos-ui/kustomize/overlays/ocp/oauth-cookie-secret.yaml.example b/fournos-ui/kustomize/overlays/ocp/oauth-cookie-secret.yaml.example new file mode 100644 index 0000000..c2341d7 --- /dev/null +++ b/fournos-ui/kustomize/overlays/ocp/oauth-cookie-secret.yaml.example @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: fournos-dashboard-oauth-cookie + labels: + app.kubernetes.io/name: fournos-dashboard +type: Opaque +stringData: + cookie-secret: REPLACE_ME_WITH_OUTPUT_OF_openssl_rand_base64_32 diff --git a/fournos-ui/kustomize/overlays/ocp/params.env.example b/fournos-ui/kustomize/overlays/ocp/params.env.example deleted file mode 100644 index 78d372e..0000000 --- a/fournos-ui/kustomize/overlays/ocp/params.env.example +++ /dev/null @@ -1,2 +0,0 @@ -POSTGRES_STORAGE_CLASS=STORAGE_CLASS_NAME -POSTGRES_STORAGE_SIZE=10Gi diff --git a/fournos-ui/kustomize/overlays/ocp/patch-deployment-oauth-proxy.yaml b/fournos-ui/kustomize/overlays/ocp/patch-deployment-oauth-proxy.yaml new file mode 100644 index 0000000..4202b72 --- /dev/null +++ b/fournos-ui/kustomize/overlays/ocp/patch-deployment-oauth-proxy.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fournos-dashboard +spec: + template: + spec: + containers: + - name: oauth-proxy + image: quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:57240081b02f882102b95dee4f2e9c491e62934a9c23d36298011473fb7fdb97 + args: + - --https-address=:8443 + - --provider=openshift + - --openshift-service-account=fournos-dashboard + - --upstream=http://localhost:8000 + - --tls-cert=/etc/tls/private/tls.crt + - --tls-key=/etc/tls/private/tls.key + - --cookie-secret-file=/etc/oauth-proxy/cookie-secret + - --email-domain=* + ports: + - containerPort: 8443 + name: https + protocol: TCP + volumeMounts: + - name: tls + mountPath: /etc/tls/private + readOnly: true + - name: cookie-secret + mountPath: /etc/oauth-proxy + readOnly: true + resources: + requests: + memory: 32Mi + cpu: 10m + limits: + memory: 64Mi + cpu: 50m + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + volumes: + - name: tls + secret: + secretName: fournos-dashboard-tls + - name: cookie-secret + secret: + secretName: fournos-dashboard-oauth-cookie diff --git a/fournos-ui/kustomize/overlays/ocp/patch-service-oauth.yaml b/fournos-ui/kustomize/overlays/ocp/patch-service-oauth.yaml new file mode 100644 index 0000000..cc2763d --- /dev/null +++ b/fournos-ui/kustomize/overlays/ocp/patch-service-oauth.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: fournos-dashboard + annotations: + service.beta.openshift.io/serving-cert-secret-name: fournos-dashboard-tls +spec: + ports: + - name: https + port: 8443 + targetPort: 8443 + protocol: TCP diff --git a/fournos-ui/kustomize/overlays/ocp/patch-serviceaccount-oauth.yaml b/fournos-ui/kustomize/overlays/ocp/patch-serviceaccount-oauth.yaml new file mode 100644 index 0000000..3354e15 --- /dev/null +++ b/fournos-ui/kustomize/overlays/ocp/patch-serviceaccount-oauth.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: fournos-dashboard + annotations: + serviceaccounts.openshift.io/oauth-redirectreference.fournos: '{"kind":"OAuthRedirectReference","apiVersion":"v1","reference":{"kind":"Route","name":"fournos-dashboard"}}'