Commit 26876708 authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: add shared PostgreSQL instance for platform components

parent 8513f361
Loading
Loading
Loading
Loading
+131 −3
Changes for README.md: 131 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -13,10 +13,11 @@ Intended for local development, integration testing, and demos. **Not for produc
| Artifact Manager | `oop` | Stores and serves artefacts |
| OEG (Open Exposure Gateway) | `oop` | Northbound API entry point for tenants |
| Federation Manager | `oop` | Inter-operator federation workflows |
| Keycloak | `oop` | OAuth2/OIDC authentication for FM |
| Keycloak | `oop` | OAuth2/OIDC identity provider: realm `oop` (portal users, app providers, OEG tokens) and realm `federation` (FM partners) |
| PostgreSQL | `oop` | Shared instance; one database and role per component |
| AI2 MCP | `oop` | MCP server exposing CAMARA-compliant tools |
| AI2 AI Agent | `oop` | Natural language interface backed by Groq |
| Portal | `oop` | Next.js web frontend for the OOP platform |
| Portal | `oop` | Developer portal: web UI and its backend (login, OEG and AI2 proxy, API clients) |

---

@@ -44,6 +45,108 @@ ai2:

If the file is absent, `helm-deploy.sh` will warn and continue — AI2 deploys with an empty key.

PostgreSQL needs no entries: its passwords are generated on first install and kept in the
`postgres-credentials` Secret across upgrades. To pin a value instead, add it to the same file:

```yaml
postgresql:
  auth:
    postgresPassword: "<postgres-superuser-password>"
```

Read a generated value back with, for example:

```bash
kubectl -n oop get secret postgres-credentials -o jsonpath='{.data.postgres-password}' | base64 -d
```

---

## PostgreSQL

A single shared instance (`charts/postgresql`) with one database and role per component. Not a replacement for a managed database or an operator such as CloudNativePG.

Pick the volume size and storage class (`postgresql.persistence`) before the first install: they
come from a StatefulSet claim template, which Kubernetes does not change in place.

Give a component a database: add an entry to `postgresql.databases`, read its password from
`postgres-credentials` → `<user>-password`, and let its pods through the NetworkPolicy by labelling
them `postgres-client: "true"` or adding a selector to `postgresql.networkPolicy.allowFrom`.

---

## Identity (Keycloak)

Keycloak (`charts/keycloak`) runs as a platform service in `oop`, backed by the shared PostgreSQL
(database `keycloak`). Two realms are imported on first start from
`charts/keycloak/templates/realms-configmap.yaml`:

| Realm | Used by | Clients |
|---|---|---|
| `oop` | Portal users, app providers, OEG | `oop-portal` (confidential, Auth Code + PKCE), `oeg` (audience only) |
| `federation` | FM, partner OPs (ADR-0042) | `originating-op-1`, `originating-op-2` (client credentials, scope `fed-mgmt`; `fed-mgmt-notif` optional, for callbacks) |

- Realm roles in `oop`: `app-provider` (developers; given to every new user, including
  self-registered ones), `operator` (runs the platform) and `platform-admin` (manages users). None
  implies another; one person can hold several.
- Client `oop-portal-admin` is the portal backend's user-management account: its service account holds
  only `realm-management` `manage-users`, `view-users`, `query-users` and `view-realm`, and its secret is
  `keycloak-credentials` → `oop-portal-admin-client-secret`. People never get Keycloak admin rights
  through the portal; the portal checks `platform-admin` on its user pages.
- Keycloak's public URL is `http://<global.publicHost>:30081` (kind sets `publicHost: localhost`; other
  clusters detect it at install). Every token carries `iss=<public URL>/realms/<realm>`, even when a pod
  fetches it from `http://keycloak:8080`. Services only know the in-cluster URL: they read the issuer
  from Keycloak's discovery document and fetch signing keys there, so no public URL is configured in them.
- Tokens from `oop-portal` carry `aud: oeg` and, when the user has one, an `organization` claim.
- **Tenancy.** The user attribute `organization` is the tenant OEG scopes data by; a user without one
  is a tenant of their own (`sub`). Only administrators see or set it (lowercase letters, digits and
  hyphens, e.g. `acme-labs`). The registration form asks for an optional *Organization*, stored as
  `requested_organization`. It is never trusted, since anyone can type any name: a platform
  administrator reads it and sets `organization` (Users → the user → Details → *Organization ID*), and
  the user's next token carries it.
- There is no mail server, so nothing is sent by email (no verification, no reset links). "Forgot
  password?" stays on the login page, but its texts (`keycloak.realms.oop.forgotPassword`) tell users to
  ask a platform administrator. An admin resets the password in the admin console: Users → the user →
  Credentials → Reset password, marked *Temporary* so the user picks a new one at the next login.
  Logged-in users change their own password through the portal (`kc_action=UPDATE_PASSWORD`).
- Every install creates a first `platform-admin` user, `oop-admin`, so someone can log in and manage
  users on a cluster without sample users. Its password is generated into `keycloak-credentials` →
  `initial-admin-password` and is temporary: the first login asks for a new password, and for an email
  address since it has none. Turn it off with
  `keycloak.realms.oop.initialAdmin.enabled: false`.
- The admin password and the `oop-portal` client secret are generated on first install into Secret
  `keycloak-credentials`, which is kept on `helm uninstall` because the realms keep using them. Pin them
  with `keycloak.admin.password` / `keycloak.realms.oop.portal.clientSecret` in `secrets.values.yaml`.
- The kind environment also creates sample users, one per role: `dev-provider` (`app-provider`),
  `dev-operator` (`operator`) and `dev-admin` (`platform-admin`), sharing the password in
  `keycloak-credentials` → `dev-user-password`, and all belong to organization `dev-org`. They come from
  `keycloak.realms.oop.sampleUsers`, which refuses to render unless Keycloak's public URL is
  `http://localhost[:port]` or `http://127.0.0.1[:port]`.
- **Realm import only creates missing realms.** After a realm exists, changes to its JSON are ignored:
  apply them in the admin console, or delete the realm (or the `keycloak` database) and restart Keycloak.

Read the admin password, or the seed users' password:

```bash
kubectl -n oop get secret keycloak-credentials -o jsonpath='{.data.admin-password}' | base64 -d; echo
kubectl -n oop get secret keycloak-credentials -o jsonpath='{.data.dev-user-password}' | base64 -d; echo
```

Seed users can log in to the account console at http://localhost:30081/realms/oop/account.

### Trying OOP without login

For a quick look on your own machine, turn login off for the whole platform:

```bash
helm upgrade --install oop-platform ./oop-platform-chart -n oop \
  -f environments/kind/values.yaml --set global.auth.enabled=false
```

Keycloak is not deployed, and neither is Federation Manager, since partner OPs must authenticate.
Rendering fails unless `global.publicHost` is `localhost` or `127.0.0.1`: without login, anyone who can
reach the portal or OEG can deploy workloads on the cluster.

---

## Deploy on kind (one command)
@@ -68,6 +171,30 @@ The scripts can also be run individually from any directory.

---

## Install on another cluster

No values are required: passwords and client secrets are generated, and the public address is
detected at install time from the first node (its ExternalIP, else its InternalIP). If that is not
the address users reach, pass it once:

```bash
helm upgrade --install oop-platform ./oop-platform-chart -n oop --create-namespace \
  --set global.publicHost=10.20.30.40          # optional
```

Keycloak is then at `http://<publicHost>:30081`, and the portal's login redirects go to
`http://<publicHost>:30082`. Services inside the cluster only use `http://keycloak:8080` and read
the issuer from Keycloak's discovery document, so nothing else needs the address.

- Detection reads the cluster, so it only works with `helm upgrade --install`; with `helm template` or
  GitOps tools such as Argo CD, set `global.publicHost`.
- Changing the address later does not update the redirect URLs already stored in the `oop` realm
  (realm import only runs for a new realm); update the `oop-portal` client in the admin console.
- `scripts/helm-deploy.sh` also passes SRM a token for the cluster's API, which a bare `helm` command
  does not.

---

## Access URLs (after deployment)

| Service | URL |
@@ -76,7 +203,8 @@ The scripts can also be run individually from any directory.
| Artifact Manager | http://localhost:30080 |
| OEG API (Swagger) | http://localhost:32263/oeg/1.0.0/docs/ |
| Keycloak | http://localhost:30081 |
| Keycloak Admin | http://localhost:30081/admin — `admin / admin` |
| Keycloak Admin | http://localhost:30081/admin — user `admin`, password in Secret `keycloak-credentials` (see [Identity](#identity-keycloak)) |
| OIDC discovery (realm `oop`) | http://localhost:30081/realms/oop/.well-known/openid-configuration |
| Federation Manager | http://localhost:30989 |
| AI2 MCP | http://localhost:32004 |
| AI2 AI Agent | http://localhost:32013 |
+14 −33
Changes for environments/kind/values.yaml: 14 added lines, 33 removed lines.
Original line number Diff line number Diff line
@@ -56,39 +56,21 @@ oeg:
      tag: "1.1"
      pullPolicy: IfNotPresent

federationManager:
  federationManager:
    config:
      partner_op:
        host: "192.168.123.178"
        server: "/operatorplatform/federation/v1"
        port: "8992"
    service:
      type: NodePort
      nodePort: 30989
    image:
      repository: labs.etsi.org:5050/oop/code/federation-manager
      tag: "1.1"
      pullPolicy: IfNotPresent
    env:
      federationManagerHost: "http://federation-manager.oop.svc.cluster.local:8989/operatorplatform/federation/v1"
      tokenEndpoint: "http://keycloak.oop.svc.cluster.local:8080/realms/federation/protocol/openid-connect/token"

  mongodb:
postgresql:
  persistence:
      storageClass: manual
      hostPath:
        enabled: true
        path: /mnt/data/mongodb_fm
    image:
      pullPolicy: IfNotPresent
    storageClass: standard
  # Dev-only cluster: let any pod reach Postgres without the postgres-client label.
  networkPolicy:
    enabled: false

global:
  publicHost: localhost

keycloak:
    service:
      type: NodePort
      nodePort: 30081
    image:
      pullPolicy: IfNotPresent
  realms:
    oop:
      sampleUsers:
        enabled: true

ai2:
  images:
@@ -112,9 +94,8 @@ ai2:
portal:
  image:
    pullPolicy: IfNotPresent
  service:
    type: NodePort
    nodePort: 30082
  oeg:
    publicUrl: http://localhost:32263


databus:
+8 −2
Changes for oop-platform-chart/Chart.lock: 8 added lines, 2 removed lines.
Original line number Diff line number Diff line
dependencies:
- name: postgresql
  repository: ""
  version: 1.0.0
- name: keycloak
  repository: ""
  version: 1.0.0
- name: srm
  repository: ""
  version: 1.0.0
@@ -17,5 +23,5 @@ dependencies:
- name: nats
  repository: https://nats-io.github.io/k8s/helm/charts/
  version: 1.2.10
digest: sha256:365d4c7760163eba4461dbe9796140f0db9ca59bd998cbbc3982f6c563a87577
generated: "2026-06-02T12:59:48.852000682Z"
digest: sha256:7ca197be9b2d2b7c7826398016c8cc6628a968fcbfc077e46b1cca48d29f5863
generated: "2026-09-14T16:53:21.241543984+03:00"
+9 −3
Changes for oop-platform-chart/Chart.yaml: 9 added lines, 3 removed lines.
Original line number Diff line number Diff line
apiVersion: v2
name: oop-platform
description: Open Operator Platform - Complete platform deployment (SRM, OEG, Federation Manager, AI2 and Portal)
description: Open Operator Platform - Complete platform deployment (SRM, OEG, Federation Manager, AI2, Portal, PostgreSQL and Keycloak)
type: application
version: 1.0.0

@@ -21,14 +21,20 @@ keywords:
maintainers:
  - name: Open Operator Platform Team

repo: https://labs.etsi.org/rep/oop/deploy/helm
sources:
  - https://labs.etsi.org/rep/oop/deploy/helm
  - https://labs.etsi.org/rep/oop

icon: https://labs.etsi.org/rep/uploads/-/system/project/avatar/4685/logo.png

# Subchart dependencies
dependencies:
  - name: postgresql
    version: "1.0.0"
    condition: postgresql.enabled
  - name: keycloak
    version: "1.0.0"
    condition: keycloak.enabled
  - name: srm
    version: "1.0.0"
    condition: srm.enabled
@@ -37,7 +43,7 @@ dependencies:
    condition: oeg.enabled
  - name: federation-manager
    version: "1.0.0"
    condition: federationManager.enabled
    condition: federation-manager.enabled
  - name: ai2
    version: "1.0.0"
    condition: ai2.enabled
+54 −93
Changes for oop-platform-chart/charts/federation-manager/README.md: 54 added lines, 93 removed lines.
Original line number Diff line number Diff line
# Federation Manager Helm Chart

Helm chart for deploying Federation Manager with Keycloak authentication for the Open Operator Platform.
Helm chart for deploying Federation Manager for the Open Operator Platform.

## Components

This chart deploys:

- **Federation Manager** - Manages federation across multiple operators
- **Keycloak** - OAuth2/OpenID Connect authentication server
- **MongoDB** - Federation Manager's database
- **OpenVPN Sidecar** (Optional) - For remote federation connections

Authentication uses the platform Keycloak (realm `federation`), which is deployed by the umbrella
chart, not by this chart.

## Prerequisites

- Kubernetes 1.19+
- Helm 3.x
- kubectl configured
- A Keycloak with realm `federation` reachable at `federationManager.config.keycloak.host:port`

## Installation

@@ -47,47 +51,42 @@ helm install federation-manager . -n federation-manager -f my-values.yaml

## Configuration

### Keycloak Configuration

The default Keycloak realm configuration includes:

- **Realm**: `federation`
- **Admin User**: `admin` / `admin` (change in production!)
- **Client ID**: `originating-op-1`
- **Client Secret**: `dd7vNwFqjNpYwaghlEwMbw10g0klWDHb`

#### ⚠️ About Keycloak Credentials

The default client credentials provided are **generic OAuth2 credentials** and can be used for testing. However, for production deployments:
### Authentication (Keycloak)

**Option 1: Keep defaults** (for testing/development)
- The provided credentials will work out of the box
- Quick to get started
Federation Manager validates every incoming bearer token against Keycloak's realm `federation`:
signature (keys fetched from `http://<host>:<port>/realms/<realm>`), expiry, issuer, and the
`fed-mgmt` scope, and the audience `federation-manager` (the keycloak chart adds it to the partner
clients; `global.federation.audience`). Invalid or missing tokens get `401`; when Keycloak itself
cannot be reached, requests get `503` instead. The expected issuer is discovered from Keycloak's
discovery document at that same address, so it follows Keycloak's public URL with no extra
configuration.

**Option 2: Generate new credentials** (recommended for production)

1. Deploy with defaults first
2. Access Keycloak UI at `http://<NODE-IP>:30081`
3. Login with admin/admin
4. Navigate to: Realm Settings → Clients → originating-op-1
5. Regenerate secret under "Credentials" tab
6. Update `values.yaml` with new secret:
```yaml
   keycloak:
     realm:
       client:
         secret: "your-new-secret"
   
federationManager:
  config:
    keycloak:
         client1_secret: "your-new-secret"
   ```
7. Upgrade the deployment:
   ```bash
   helm upgrade federation-manager . -n federation-manager
      client1_id: ""            # empty = global.federation.clients[0]
      client2_id: ""            # empty = global.federation.clients[1]
      # Secret holding both client secrets. The umbrella chart points this at
      # keycloak-credentials, where the keycloak chart generates them
      # (keys federation-<client id>-secret).
      existingSecret: ""
      client1_secret: ""        # only used without existingSecret
      client2_secret: ""
      scope: "fed-mgmt"
      host: ""                  # empty = global.keycloak.host, else keycloak
      port: ""                  # empty = global.keycloak.port, else 8080
      realm: "federation"
      # Issuer Keycloak puts in tokens (`iss`), also the token URL the Swagger UI shows.
      # Empty = discovered from Keycloak; set it only to override that.
      issuer: ""
      # Audience partner tokens must carry. Empty = global.federation.audience,
      # else federation-manager.
      audience: ""
```

The client IDs and secrets must match the clients in Keycloak's `federation` realm.

### Federation Manager Configuration

Key configuration sections:
@@ -95,12 +94,6 @@ Key configuration sections:
```yaml
federationManager:
  config:
    # Keycloak OAuth2 settings
    keycloak:
      client1_id: "originating-op-1"
      client1_secret: "dd7vNwFqjNpYwaghlEwMbw10g0klWDHb"
      scope: "fed-mgmt"
    
    # MongoDB connection
    mongodb:
      host: "mongodb.mongodb.svc.cluster.local"
@@ -165,8 +158,6 @@ NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}

# Access URLs
echo "Federation Manager: http://$NODE_IP:30989"
echo "Keycloak: http://$NODE_IP:30081"
echo "Keycloak Admin: http://$NODE_IP:30081/admin (admin/admin)"
```

## Common Operations
@@ -184,9 +175,6 @@ kubectl get svc -n federation-manager
# Federation Manager logs
kubectl logs -f deployment/federation-manager -c federation-manager -n federation-manager

# Keycloak logs
kubectl logs -f deployment/keycloak -n federation-manager

# OpenVPN logs (if enabled)
kubectl logs -f deployment/federation-manager -c openvpn -n federation-manager
```
@@ -206,9 +194,6 @@ helm upgrade federation-manager . -n federation-manager
```bash
# Scale Federation Manager
helm upgrade federation-manager . -n federation-manager --set federationManager.replicaCount=2

# Scale Keycloak
helm upgrade federation-manager . -n federation-manager --set keycloak.replicaCount=2
```

## Integration with OOP Platform
@@ -227,13 +212,15 @@ To integrate with the main OOP platform:
   dependencies:
     - name: federation-manager
       version: "1.0.0"
       condition: federationManager.enabled
       condition: federation-manager.enabled
   ```

3. Add to OOP platform `values.yaml`:
3. Add to OOP platform `values.yaml`. Everything, the on/off switch included, goes under the
   chart's name, `federation-manager`: Helm passes a subchart only the values under its own name.
   ```yaml
   federationManager:
   federation-manager:
     enabled: true
     federationManager:
       # ... federation manager configuration
   ```

@@ -255,16 +242,13 @@ federationManager:

## Troubleshooting

### Keycloak Not Starting
### Every request returns 401

```bash
# Check logs
kubectl logs deployment/keycloak -n federation-manager

# Common issues:
# - Realm import failed: Check ConfigMap
# - Port conflict: Ensure port 8080 is available
```
- Check the token's `iss` claim equals the `issuer` in
  `http://<host>:<port>/realms/<realm>/.well-known/openid-configuration` (or
  `federationManager.config.keycloak.issuer` if that override is set).
- Check Federation Manager can reach `http://<host>:<port>/realms/<realm>/protocol/openid-connect/certs`.
- Check the token carries the `fed-mgmt` scope.

### Federation Manager Configuration Issues

@@ -297,36 +281,14 @@ kubectl exec -it deployment/federation-manager -c openvpn -n federation-manager

### Production Deployment

1. **Change Keycloak Admin Password**
   ```yaml
   keycloak:
     admin:
       username: admin
       password: "StrongPassword123!"
   ```
1. **Regenerate Client Secrets** in Keycloak's `federation` realm and update
   `federationManager.config.keycloak.client1_secret` / `client2_secret` to match

2. **Regenerate Client Secrets** (see Keycloak Configuration section above)
2. **Use Secrets for Sensitive Data**

3. **Use Secrets for Sensitive Data**
   ```bash
   kubectl create secret generic keycloak-admin \
     --from-literal=username=admin \
     --from-literal=password=secure-password \
     -n federation-manager
   ```

4. **Enable TLS/HTTPS**
   ```yaml
   keycloak:
     ingress:
       enabled: true
       tls:
         - secretName: keycloak-tls
           hosts:
             - keycloak.yourdomain.com
   ```
3. **Enable TLS/HTTPS** for Federation Manager and Keycloak

5. **Resource Limits**
4. **Resource Limits**
   - Already configured with appropriate limits
   - Adjust based on your workload

@@ -347,13 +309,12 @@ kubectl delete secret partner-ovpn -n federation-manager

| Parameter | Description | Default |
|-----------|-------------|---------|
| `keycloak.enabled` | Enable Keycloak deployment | `true` |
| `keycloak.service.nodePort` | NodePort for Keycloak | `30081` |
| `keycloak.admin.username` | Keycloak admin username | `admin` |
| `keycloak.admin.password` | Keycloak admin password | `admin` |
| `keycloak.realm.client.secret` | OAuth2 client secret | (see values.yaml) |
| `federationManager.enabled` | Enable Federation Manager | `true` |
| `federationManager.service.nodePort` | NodePort for Federation Manager | `30989` |
| `federationManager.config.keycloak.host` / `port` / `realm` | Where Federation Manager reaches Keycloak | `keycloak` / `8080` / `federation` |
| `federationManager.config.keycloak.issuer` | Override for the issuer expected in tokens | `""` (discovered from Keycloak) |
| `federationManager.config.keycloak.audience` | Audience partner tokens must carry | `""` (`global.federation.audience`, else `federation-manager`) |
| `federationManager.config.keycloak.existingSecret` | Secret holding the partner client secrets | `""` (umbrella: `keycloak-credentials`) |
| `federationManager.config.partner_op.role` | Role: `partner_op` or `originating_op` | `partner_op` |
| `openvpn.enabled` | Enable OpenVPN sidecar | `false` |
| `openvpn.secretName` | Name of VPN secret | `partner-ovpn` |
Loading