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

feat: update Helm chart for OOP Portal with new configurations and remove deprecated files

parent 18455b6e
Loading
Loading
Loading
Loading
Loading
+4 −5
Changes for deployment/Chart.yaml: 4 added lines, 5 removed lines.
Original line number Diff line number Diff line
apiVersion: v2
name: portal
description: Helm chart for the Portal frontend (Next.js)
description: Helm chart for the OOP Portal (developer portal UI and its backend)
type: application
version: 0.1.0
appVersion: "1.0.0"
version: 1.0.0
appVersion: "2.0.0"
keywords:
  - frontend
  - oop
  - etsi
  - portal
home: https://labs.etsi.org/rep/oop/code/dev-portal
home: https://labs.etsi.org/rep/oop/code/portal
maintainers:
  - name: OOP Team
+41 −70
Changes for deployment/README.md: 41 added lines, 70 removed lines.
Original line number Diff line number Diff line
# Portal Helm Chart

Deploy the Portal frontend (Next.js) to Kubernetes.
Deploys the OOP developer portal: one container in which the backend serves the web UI and
its API (`/api/*`) on the same address, so the login cookies stay first-party.

## Prerequisites
The portal needs, from the umbrella chart:

- Helm 3
- Kubernetes cluster
- Portal image (build from `portal-gui/` or use a registry)
- **PostgreSQL**: database and role `portal`, an entry in `postgresql.databases`. An init
  container waits until the role can log in.
- **Keycloak** (unless `global.auth.enabled=false`): the `oop-portal` client logs users in, and
  `oop-portal-provisioner` creates developers' API clients. Their secrets come from the
  `keycloak-credentials` Secret.
- **OEG** and the **AI2 agent**, which the backend forwards `/api/oeg/*` and `/api/ai2/chat` to.

## Build the image
## Addresses

From the repo root:
Browsers reach the portal and Keycloak on public URLs built from `global.publicHost`, `global.scheme`
and `global.nodePorts`, as the keycloak chart builds them. The backend's own calls to Keycloak use
`global.keycloak.host`/`port` instead, since the public address may not be reachable from pods.

```bash
docker build -t portal-gui:latest -f portal/portal-gui/Dockerfile portal/portal-gui
```

For **Kind**, load the image into the cluster:

```bash
kind load docker-image portal-gui:latest
```

## Install

```bash
cd portal/deployment
helm install portal .
```
If you set the keycloak chart's `hostname` or `realms.oop.portal.baseUrl`, set `keycloak.publicUrl`
or `publicUrl` here to match.

Override the image tag to use your built image:
## Configuration

```bash
helm install portal . --set image.tag=latest
```
| Parameter | Description | Default |
|-----------|-------------|---------|
| `image.repository` | Image repository | `labs.etsi.org:5050/oop/code/portal` |
| `image.tag` | Image tag, also reported as the app version | `2.0.0` |
| `publicUrl` | Address browsers reach the portal on | `<scheme>://<public host>:<NodePort>` |
| `oeg.url` | Where `/api/oeg/*` is forwarded to | `http://oeg:80` |
| `oeg.publicUrl` | OEG address shown with developers' API clients | `""` (not shown) |
| `ai2.chatUrl` | AI2 agent chat endpoint; empty turns the chat off | `http://ai2-ai-agent:9013/groq-mcp` |
| `keycloak.publicUrl` | Keycloak's public URL (token issuer) | `<scheme>://<public host>:<global.nodePorts.keycloak>` |
| `keycloak.existingSecret` | Secret with the client secrets | `keycloak-credentials` |
| `keycloak.provisioner.enabled` | API clients feature | `true` |
| `keycloak.provisioner.oegClientId` | The keycloak chart's `realms.oop.oeg.internalId` | `oeg-client` |
| `database.name` / `database.user` | Database and role | `portal` |
| `database.passwordKey` | Key of the role's password in `global.postgresql.secretName` | `portal-password` |
| `service.type` | Service type | `ClusterIP` (`NodePort` in the umbrella chart) |
| `service.nodePort` | NodePort | `global.nodePorts.portal` |

Set the OEG service URL (used by the frontend as `OEG_SERVICE_URL`):
The API products shown in the portal come from `global.apiProducts`, the same list Keycloak's
roles are built from.

```bash
helm install portal . \
  --set image.tag=latest \
  --set config.oegServiceUrl=http://oeg-service:8080
```
API clients' registration tokens are encrypted with a key generated into the Secret
`portal-credentials` on first install. It is kept on `helm uninstall`, like the database it
belongs with.

If the default NodePort is already in use, pick another (30000–32767):
## Troubleshooting

```bash
helm install portal . --set service.nodePort=30081
kubectl get pods -n oop -l app.kubernetes.io/name=portal
kubectl logs -f deployment/portal -n oop
kubectl logs deployment/portal -n oop -c wait-for-database
```

## Upgrade

```bash
helm upgrade portal . --set image.tag=latest
```

## Get the URL

After install, Helm prints the URL. You can also run:

```bash
kubectl get svc -l app.kubernetes.io/name=portal -o jsonpath='http://{.items[0].spec.clusterIP}:{.items[0].spec.ports[0].port}'
```

For **NodePort**, use the node IP and `nodePort` (see `kubectl get svc -l app.kubernetes.io/name=portal`).

## Uninstall

```bash
helm uninstall portal
```

## Main values

| Value | Default | Description |
|-------|---------|-------------|
| `image.repository` | `portal-gui` | Image name |
| `image.tag` | `1.0.0` | Image tag |
| `config.oegServiceUrl` | `""` | OEG service base URL (env `OEG_SERVICE_URL`) |
| `service.type` | `NodePort` | Service type |
| `service.nodePort` | `30082` | NodePort (when type is NodePort) |
| `replicaCount` | `1` | Number of replicas |

See `values.yaml` for all options.

deployment/templates/NOTES.txt

deleted100644 → 0
+0 −16
Changes for deployment/templates/NOTES.txt: 0 added lines, 16 removed lines.
Original line number Diff line number Diff line
Portal frontend has been deployed.

1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "portal.fullname" . }})
  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
  kubectl get --namespace {{ .Release.Namespace }} svc {{ include "portal.fullname" . }} -w
{{- else if contains "ClusterIP" .Values.service.type }}
  kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "portal.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
  echo "Visit http://127.0.0.1:{{ .Values.service.port }}"
{{- end }}

OEG_SERVICE_URL is set to: {{ .Values.config.oegServiceUrl }}
+84 −20
Changes for deployment/templates/_helpers.tpl: 84 added lines, 20 removed lines.
Original line number Diff line number Diff line
{{- define "portal.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "portal.fullname" -}}
{{- if .Values.fullnameOverride }}
  {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
  {{- $name := default .Chart.Name .Values.nameOverride }}
  {{- if contains $name .Release.Name }}
{{- else if contains .Chart.Name .Release.Name }}
  {{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
    {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
  {{- end }}
  {{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{- define "portal.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "portal.labels" -}}
helm.sh/chart: {{ include "portal.chart" . }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{ include "portal.selectorLabels" . }}
app.kubernetes.io/version: {{ .Values.image.tag | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
{{- end }}

{{- define "portal.selectorLabels" -}}
app.kubernetes.io/name: {{ include "portal.name" . }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: portal
{{- end }}

{{/*
Settings shared across the platform live in the umbrella chart's `global` block. Each helper
below takes this chart's own value when set, else the global one, else a built-in default,
so the chart also installs on its own.
*/}}
{{- define "portal.authEnabled" -}}
{{- if dig "auth" "enabled" true (.Values.global | default dict) }}true{{ end -}}
{{- end }}

{{- define "portal.scheme" -}}
{{- dig "scheme" "http" (.Values.global | default dict) -}}
{{- end }}

{{/*
global.publicHost if set, else the first node's ExternalIP, else its InternalIP, as the keycloak
chart does, so both build the same URLs.
*/}}
{{- define "portal.publicHost" -}}
{{- $host := dig "publicHost" "" (.Values.global | default dict) -}}
{{- if not $host -}}
{{- $nodes := lookup "v1" "Node" "" "" -}}
{{- if and $nodes $nodes.items -}}
{{- $addresses := (index $nodes.items 0).status.addresses -}}
{{- range $addresses }}{{ if and (not $host) (eq .type "ExternalIP") }}{{ $host = .address }}{{ end }}{{ end -}}
{{- range $addresses }}{{ if and (not $host) (eq .type "InternalIP") }}{{ $host = .address }}{{ end }}{{ end -}}
{{- end -}}
{{- end -}}
{{- $host | default "localhost" -}}
{{- end }}

{{- define "portal.publicUrl" -}}
{{- if .Values.publicUrl -}}
{{- trimSuffix "/" .Values.publicUrl -}}
{{- else -}}
{{- $port := .Values.service.nodePort | default (dig "nodePorts" "portal" 30082 (.Values.global | default dict)) -}}
{{- printf "%s://%s:%v" (include "portal.scheme" .) (include "portal.publicHost" .) $port -}}
{{- end -}}
{{- end }}

{{/*
Keycloak's public URL: the issuer of every token, and where browsers log in.
*/}}
{{- define "portal.keycloak.publicUrl" -}}
{{- if .Values.keycloak.publicUrl -}}
{{- trimSuffix "/" .Values.keycloak.publicUrl -}}
{{- else -}}
{{- $port := dig "nodePorts" "keycloak" 30081 (.Values.global | default dict) -}}
{{- printf "%s://%s:%v" (include "portal.scheme" .) (include "portal.publicHost" .) $port -}}
{{- end -}}
{{- end }}

{{/*
Keycloak inside the cluster, for the backend's own calls: the public URL may not be reachable
from pods (on kind it is localhost).
*/}}
{{- define "portal.keycloak.internalUrl" -}}
{{- $kc := (.Values.global | default dict).keycloak | default dict -}}
{{- printf "http://%s:%v" ($kc.host | default "keycloak") ($kc.port | default 8080) -}}
{{- end }}

{{- define "portal.db.host" -}}
{{- dig "postgresql" "host" "postgres" (.Values.global | default dict) -}}
{{- end }}

{{- define "portal.db.port" -}}
{{- dig "postgresql" "port" 5432 (.Values.global | default dict) -}}
{{- end }}

{{- define "portal.db.secretName" -}}
{{- dig "postgresql" "secretName" "postgres-credentials" (.Values.global | default dict) -}}
{{- end }}

{{- define "portal.db.waitImage" -}}
{{- dig "images" "postgres" "postgres:18.6-alpine" (.Values.global | default dict) -}}
{{- end }}

{{- define "portal.configMapName" -}}
{{- include "portal.fullname" . }}-config
{{- define "portal.secretName" -}}
{{- include "portal.fullname" . }}-credentials
{{- end }}
+27 −3
Changes for deployment/templates/configmap.yaml: 27 added lines, 3 removed lines.
Original line number Diff line number Diff line
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "portal.configMapName" . }}
  name: {{ include "portal.fullname" . }}
  namespace: {{ .Release.Namespace }}
  labels:
    {{- include "portal.labels" . | nindent 4 }}
    app.kubernetes.io/component: portal
data:
  OEG_SERVICE_URL: {{ .Values.config.oegServiceUrl | quote }}
  APP_VERSION: {{ .Values.image.tag | quote }}
  POSTGRESQL_SETTINGS__CREATE_SCHEMA_ON_STARTUP: "true"
  OEG__BASE_URL: {{ .Values.oeg.url | quote }}
  OEG__PUBLIC_BASE_URL: {{ .Values.oeg.publicUrl | quote }}
  AI2__CHAT_URL: {{ .Values.ai2.chatUrl | quote }}
  API_PRODUCTS__FILE: /etc/portal/api-products.json
  {{- if include "portal.authEnabled" . }}
  {{- $issuer := printf "%s/realms/%s" (include "portal.keycloak.publicUrl" .) .Values.keycloak.realm }}
  {{- $internal := printf "%s/realms/%s" (include "portal.keycloak.internalUrl" .) .Values.keycloak.realm }}
  AUTH__SECURITY_MODE: jwt
  SESSION__HTTPS_ONLY: {{ eq (include "portal.scheme" .) "https" | quote }}
  OIDC__ISSUER: {{ $issuer | quote }}
  OIDC__CLIENT_ID: {{ .Values.keycloak.clientId | quote }}
  OIDC__REDIRECT_URI: {{ printf "%s/api/auth/callback" (include "portal.publicUrl" .) | quote }}
  # Backend-to-Keycloak calls go in-cluster; the issuer stays public.
  OIDC__JWKS_URI: {{ printf "%s/protocol/openid-connect/certs" $internal | quote }}
  OIDC__TOKEN_ENDPOINT: {{ printf "%s/protocol/openid-connect/token" $internal | quote }}
  OIDC__ADMIN_BASE_URL: {{ include "portal.keycloak.internalUrl" . | quote }}
  {{- if .Values.keycloak.provisioner.enabled }}
  PROVISIONING__CLIENT_ID: {{ .Values.keycloak.provisioner.clientId | quote }}
  PROVISIONING__OEG_CLIENT_UUID: {{ .Values.keycloak.provisioner.oegClientId | quote }}
  {{- end }}
  {{- else }}
  AUTH__SECURITY_MODE: none
  {{- end }}
  api-products.json: {{ dict "products" (dig "apiProducts" (list) (.Values.global | default dict)) | toJson | quote }}
Loading