Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ build/
secrets/
fournos.log
fournos.pid
.vscode/
3 changes: 3 additions & 0 deletions fournos-ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ 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
projects-local.yaml
cpt.yaml
182 changes: 152 additions & 30 deletions fournos-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: fournos-dashboard
namespace: FOURNOS_NAMESPACE
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: fournos-dashboard
subjects:
- kind: ServiceAccount
name: fournos-dashboard
namespace: fournos-dashboard
EOF
```

This creates:
- A `fournos-dashboard` namespace
- PostgreSQL StatefulSet with persistent storage
- Dashboard Deployment, Service, ServiceAccount
- Dashboard Deployment with OAuth proxy sidecar (2 containers)
- Service, ServiceAccount (with OAuth redirect annotation)
- ClusterRole for FournosJob/CronJob/Pod access
- RoleBinding in the target namespace (e.g. `psap-automation`)
- Projects ConfigMap
- RoleBinding in the target namespace
- OpenShift Route with reencrypt TLS
- Let's Encrypt Certificate (auto-issued and auto-renewed by cert-manager)
- OAuth cookie secret

### 4. Access the dashboard
### 5. Verify the deployment

```bash
oc port-forward -n fournos-dashboard svc/fournos-dashboard 8000:8000
# Pod should show 2/2 Ready (dashboard + oauth-proxy)
oc get pods -n fournos-dashboard

# Check the TLS certificate was issued
oc get certificate fournos-dashboard-cert -n fournos-dashboard

# Get the Route URL
oc get route fournos-dashboard -n fournos-dashboard -o jsonpath='{.spec.host}'
```

### 6. Access the dashboard

Open the Route URL in your browser:

```
https://fournos-dashboard-fournos-dashboard.apps.<cluster-domain>
```

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
Expand All @@ -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

Expand All @@ -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
```
14 changes: 14 additions & 0 deletions fournos-ui/app/k8s_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,11 +355,22 @@ def list_pods_for_job(job_name: str, namespace: str | None = None) -> list[dict]

container_ready = False
restarts = 0
exit_code = None
term_reason = ""
term_message = ""
if pod.status.container_statuses:
for cs in pod.status.container_statuses:
if cs.ready:
container_ready = True
restarts += cs.restart_count
terminated = (
cs.state.terminated if cs.state and cs.state.terminated
else (cs.last_state.terminated if cs.last_state and cs.last_state.terminated else None)
)
if terminated:
exit_code = terminated.exit_code
term_reason = terminated.reason or ""
term_message = terminated.message or ""

if pod.metadata.name.startswith("affinity-assistant"):
continue
Expand All @@ -373,6 +384,9 @@ def list_pods_for_job(job_name: str, namespace: str | None = None) -> list[dict]
"ready": container_ready,
"restarts": restarts,
"age_minutes": age_minutes,
"exit_code": exit_code,
"term_reason": term_reason,
"term_message": term_message,
"_created": created,
})
pods.sort(key=lambda p: p["_created"] or datetime.min.replace(tzinfo=timezone.utc))
Expand Down
Loading