From 7b548b79c5b310f868626b7f7b2c9e7629171423 Mon Sep 17 00:00:00 2001 From: Elisa Date: Wed, 12 Aug 2026 11:00:03 +0200 Subject: [PATCH 1/3] feat: deploy solar-ui as part of the solar chart --- .github/workflows/docker.yaml | 2 + Dockerfile | 1 - charts/solar/README.md | 31 +++++ charts/solar/templates/_helpers.tpl | 50 +++++++ charts/solar/templates/ui/clusterrole.yaml | 22 ++++ .../templates/ui/clusterrolebinding.yaml | 16 +++ charts/solar/templates/ui/deployment.yaml | 121 +++++++++++++++++ charts/solar/templates/ui/service.yaml | 22 ++++ charts/solar/templates/ui/serviceaccount.yaml | 13 ++ charts/solar/values.yaml | 123 ++++++++++++++++++ 10 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 charts/solar/templates/ui/clusterrole.yaml create mode 100644 charts/solar/templates/ui/clusterrolebinding.yaml create mode 100644 charts/solar/templates/ui/deployment.yaml create mode 100644 charts/solar/templates/ui/service.yaml create mode 100644 charts/solar/templates/ui/serviceaccount.yaml diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index ccbc31b6..57703ba3 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -32,6 +32,8 @@ jobs: target: renderer - name: solar-discovery target: discovery + - name: solar-ui + target: ui permissions: contents: read packages: write diff --git a/Dockerfile b/Dockerfile index 4a4c474b..562ebb8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,6 @@ COPY api/ api/ COPY client-go/ client-go/ COPY cmd/ cmd/ COPY pkg/ pkg/ -COPY web/ web/ ARG TARGETOS ARG TARGETARCH diff --git a/charts/solar/README.md b/charts/solar/README.md index 791c0738..2cd9b631 100644 --- a/charts/solar/README.md +++ b/charts/solar/README.md @@ -390,6 +390,37 @@ helm uninstall solar --namespace solar-system | renderer.image.repository | string | `"ghcr.io/opendefensecloud/solar-renderer"` | | | renderer.image.tag | string | `""` | | | renderer.imagePullSecrets | list | `[]` | Image pull secrets for the renderer Pod. Use the Kubernetes shape `[{name: my-secret}]` (matches `apiserver.imagePullSecrets` etc.). Each referenced Secret must exist (type `kubernetes.io/dockerconfigjson`) in every namespace where Targets/RenderTasks are created — the renderer Pod runs in the RenderTask's namespace, so cross-namespace references don't work. Merged with `global.imagePullSecrets`. See the chart README for the recommended External Secrets Operator pattern that distributes a single source-of-truth credential to every namespace. | +| ui.affinity | object | `{}` | Affinity for pod assignment | +| ui.args.authMode | string | `"token"` | How the user's OIDC identity reaches the K8s API: "token" forwards the id_token, "impersonate" makes the BFF's ServiceAccount impersonate the user (and grants it impersonate RBAC). | +| ui.command | list | `["/solar-ui"]` | Command to run in the container | +| ui.enabled | bool | `false` | Enable UI deployment. Off by default: the UI needs an OIDC issuer to authenticate against, which is site-specific. | +| ui.extraArgs | object | `{}` | Additional command-line arguments as key-value pairs | +| ui.extraEnv | list | `[]` | Additional environment variables | +| ui.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| ui.image.repository | string | `"ghcr.io/opendefensecloud/solar-ui"` | UI image repository | +| ui.image.tag | string | `""` | UI image tag (defaults to chart appVersion if not set) | +| ui.imagePullSecrets | list | `[]` | Image pull secrets for UI | +| ui.livenessProbe | object | `{"httpGet":{"path":"/","port":"http"},"initialDelaySeconds":15,"periodSeconds":20}` | Liveness probe configuration | +| ui.nodeSelector | object | `{}` | Node selector for pod assignment | +| ui.oidc.clientID | string | `"solar-ui"` | OIDC client ID | +| ui.oidc.existingSecret | string | `""` | Name of an existing Secret holding the OIDC client secret. Left empty for public clients (no client secret). | +| ui.oidc.existingSecretKey | string | `"client-secret"` | Key within `existingSecret` holding the client secret | +| ui.oidc.issuer | string | `""` | OIDC issuer URL, e.g. https://dex.example.com. Required when ui.enabled=true. | +| ui.oidc.redirectURL | string | `""` | OIDC redirect URL — the externally reachable /api/auth/callback of this UI, e.g. https://solar.example.com/api/auth/callback. Required when ui.enabled=true. | +| ui.podAnnotations | object | `{}` | Pod annotations | +| ui.podLabels | object | `{}` | Pod labels | +| ui.podSecurityContext | object | `{"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}}` | Pod security context | +| ui.readinessProbe | object | `{"httpGet":{"path":"/","port":"http"},"initialDelaySeconds":5,"periodSeconds":10}` | Readiness probe configuration | +| ui.replicaCount | int | `1` | Number of UI replicas. Keep at 1 unless a session key is pinned: the BFF generates one per process, so a second replica cannot read the first one's session cookies. | +| ui.resources | object | `{"limits":{"cpu":"300m","memory":"128Mi"},"requests":{"cpu":"100m","memory":"64Mi"}}` | Resource limits and requests | +| ui.securityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}}` | Container security context | +| ui.service.annotations | object | `{}` | UI service annotations | +| ui.service.port | int | `8090` | UI service port. Also the port the BFF listens on. | +| ui.service.type | string | `"ClusterIP"` | UI service type | +| ui.serviceAccount.annotations | object | `{}` | Service account annotations | +| ui.serviceAccount.create | bool | `true` | Create service account | +| ui.serviceAccount.name | string | `""` | Service account name (auto-generated if not set) | +| ui.tolerations | list | `[]` | Tolerations for pod assignment | ## Contributing diff --git a/charts/solar/templates/_helpers.tpl b/charts/solar/templates/_helpers.tpl index a80fcb17..002697c5 100644 --- a/charts/solar/templates/_helpers.tpl +++ b/charts/solar/templates/_helpers.tpl @@ -132,6 +132,48 @@ Controller service account name {{- end }} {{- end }} +{{/* +UI fullname +*/}} +{{- define "solar.ui.fullname" -}} +{{- printf "%s-ui" (include "solar.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +UI component labels +*/}} +{{- define "solar.ui.labels" -}} +{{ include "solar.labels" . }} +app.kubernetes.io/component: ui +app.kubernetes.io/part-of: solar +{{- end }} + +{{/* +UI selector labels +*/}} +{{- define "solar.ui.selectorLabels" -}} +{{ include "solar.selectorLabels" . }} +app.kubernetes.io/component: ui +{{- end }} + +{{/* +UI service account name +*/}} +{{- define "solar.ui.serviceAccountName" -}} +{{- if .Values.ui.serviceAccount.create }} +{{- default (include "solar.ui.fullname" .) .Values.ui.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.ui.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +UI service name +*/}} +{{- define "solar.ui.serviceName" -}} +{{- include "solar.ui.fullname" . }} +{{- end }} + {{/* etcd fullname */}} @@ -216,6 +258,14 @@ etcd image {{- printf "%s:%s" .Values.etcd.image.repository .Values.etcd.image.tag }} {{- end }} +{{/* +UI image +*/}} +{{- define "solar.ui.image" -}} +{{- $tag := .Values.ui.image.tag | default .Chart.AppVersion }} +{{- printf "%s:%s" .Values.ui.image.repository $tag }} +{{- end }} + {{/* Renderer image */}} diff --git a/charts/solar/templates/ui/clusterrole.yaml b/charts/solar/templates/ui/clusterrole.yaml new file mode 100644 index 00000000..22c578de --- /dev/null +++ b/charts/solar/templates/ui/clusterrole.yaml @@ -0,0 +1,22 @@ +{{- if and .Values.ui.enabled .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "solar.ui.fullname" . }} + labels: + {{- include "solar.ui.labels" . | nindent 4 }} +rules: + # The BFF enumerates namespaces with its own identity and then filters them + # per user via SelfSubjectRulesReview, so users need no namespace RBAC of + # their own (see pkg/ui/api.Handler.HandleListNamespaces). + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list"] + {{- if eq .Values.ui.args.authMode "impersonate" }} + # In impersonate mode the BFF, not the user's id_token, talks to the API + # server on the user's behalf. + - apiGroups: [""] + resources: ["users", "groups"] + verbs: ["impersonate"] + {{- end }} +{{- end }} diff --git a/charts/solar/templates/ui/clusterrolebinding.yaml b/charts/solar/templates/ui/clusterrolebinding.yaml new file mode 100644 index 00000000..2e892ce1 --- /dev/null +++ b/charts/solar/templates/ui/clusterrolebinding.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.ui.enabled .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "solar.ui.fullname" . }} + labels: + {{- include "solar.ui.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "solar.ui.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "solar.ui.serviceAccountName" . }} + namespace: {{ include "solar.namespace" . }} +{{- end }} diff --git a/charts/solar/templates/ui/deployment.yaml b/charts/solar/templates/ui/deployment.yaml new file mode 100644 index 00000000..7463ea26 --- /dev/null +++ b/charts/solar/templates/ui/deployment.yaml @@ -0,0 +1,121 @@ +{{- if .Values.ui.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "solar.ui.fullname" . }} + namespace: {{ include "solar.namespace" . }} + labels: + {{- include "solar.ui.labels" . | nindent 4 }} + {{- with .Values.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.ui.replicaCount }} + selector: + matchLabels: + {{- include "solar.ui.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: ui + {{- with .Values.ui.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "solar.ui.selectorLabels" . | nindent 8 }} + {{- with .Values.ui.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.ui.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.global.imagePullSecrets }} + {{- if not $.Values.ui.imagePullSecrets }} + imagePullSecrets: + {{- end }} + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "solar.ui.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.ui.podSecurityContext | nindent 8 }} + containers: + - name: ui + image: {{ include "solar.ui.image" . }} + imagePullPolicy: {{ .Values.ui.image.pullPolicy }} + {{- with .Values.ui.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + args: + - --listen=0.0.0.0:{{ .Values.ui.service.port }} + - --oidc-issuer={{ required "ui.oidc.issuer is required when ui.enabled=true" .Values.ui.oidc.issuer }} + - --oidc-client-id={{ .Values.ui.oidc.clientID }} + - --oidc-redirect-url={{ required "ui.oidc.redirectURL is required when ui.enabled=true" .Values.ui.oidc.redirectURL }} + {{- if .Values.ui.oidc.existingSecret }} + - --oidc-client-secret=$(SOLAR_UI_OIDC_CLIENT_SECRET) + {{- end }} + - --auth-mode={{ .Values.ui.args.authMode }} + {{- range $key, $value := .Values.ui.extraArgs }} + - --{{ $key }}={{ $value }} + {{- end }} + {{- if or .Values.ui.oidc.existingSecret .Values.ui.extraEnv .Values.caBundle.enabled }} + env: + {{- if .Values.ui.oidc.existingSecret }} + - name: SOLAR_UI_OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.ui.oidc.existingSecret }} + key: {{ .Values.ui.oidc.existingSecretKey }} + {{- end }} + {{- with .Values.ui.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.caBundle.enabled }} + - name: SSL_CERT_FILE + value: /etc/ssl/certs/ca-bundle.pem + {{- end }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.ui.service.port }} + protocol: TCP + securityContext: + {{- toYaml .Values.ui.securityContext | nindent 12 }} + livenessProbe: + {{- toYaml .Values.ui.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.ui.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.ui.resources | nindent 12 }} + {{- if .Values.caBundle.enabled }} + volumeMounts: + - name: ca-bundle + mountPath: /etc/ssl/certs + readOnly: true + {{- end }} + {{- if .Values.caBundle.enabled }} + volumes: + - name: ca-bundle + configMap: + name: {{ required "caBundle.configMapName must be set when caBundle.enabled=true" .Values.caBundle.configMapName }} + items: + - key: {{ required "caBundle.key must be set when caBundle.enabled=true" .Values.caBundle.key }} + path: ca-bundle.pem + {{- end }} + {{- with .Values.ui.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.ui.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: 10 +{{- end }} diff --git a/charts/solar/templates/ui/service.yaml b/charts/solar/templates/ui/service.yaml new file mode 100644 index 00000000..086a0633 --- /dev/null +++ b/charts/solar/templates/ui/service.yaml @@ -0,0 +1,22 @@ +{{- if .Values.ui.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "solar.ui.serviceName" . }} + namespace: {{ include "solar.namespace" . }} + labels: + {{- include "solar.ui.labels" . | nindent 4 }} + {{- with .Values.ui.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.ui.service.type }} + ports: + - port: {{ .Values.ui.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "solar.ui.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/solar/templates/ui/serviceaccount.yaml b/charts/solar/templates/ui/serviceaccount.yaml new file mode 100644 index 00000000..4064cc53 --- /dev/null +++ b/charts/solar/templates/ui/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if and .Values.ui.enabled .Values.ui.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "solar.ui.serviceAccountName" . }} + namespace: {{ include "solar.namespace" . }} + labels: + {{- include "solar.ui.labels" . | nindent 4 }} + {{- with .Values.ui.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/solar/values.yaml b/charts/solar/values.yaml index 601d8cd8..b75e72e6 100644 --- a/charts/solar/values.yaml +++ b/charts/solar/values.yaml @@ -323,6 +323,129 @@ controller: # -- Service account name (auto-generated if not set) name: "" +# UI configuration (web frontend + BFF) +ui: + # -- Enable UI deployment. Off by default: the UI needs an OIDC issuer to + # authenticate against, which is site-specific. + enabled: false + + # -- Number of UI replicas. Keep at 1 unless a session key is pinned: the BFF + # generates one per process, so a second replica cannot read the first one's + # session cookies. + replicaCount: 1 + + image: + # -- UI image repository + repository: ghcr.io/opendefensecloud/solar-ui + # -- UI image tag (defaults to chart appVersion if not set) + tag: "" + # -- Image pull policy + pullPolicy: IfNotPresent + + # -- Image pull secrets for UI + imagePullSecrets: [] + + # -- Command to run in the container + command: ["/solar-ui"] + + # OIDC configuration. The BFF is an OIDC client; users log in against the + # issuer and the BFF forwards their identity to the K8s API. + oidc: + # -- OIDC issuer URL, e.g. https://dex.example.com. Required when ui.enabled=true. + issuer: "" + # -- OIDC client ID + clientID: "solar-ui" + # -- OIDC redirect URL — the externally reachable /api/auth/callback of this + # UI, e.g. https://solar.example.com/api/auth/callback. Required when ui.enabled=true. + redirectURL: "" + # -- Name of an existing Secret holding the OIDC client secret. Left empty + # for public clients (no client secret). + existingSecret: "" + # -- Key within `existingSecret` holding the client secret + existingSecretKey: "client-secret" + + # UI command-line arguments + args: + # -- How the user's OIDC identity reaches the K8s API: "token" forwards the + # id_token, "impersonate" makes the BFF's ServiceAccount impersonate the user + # (and grants it impersonate RBAC). + authMode: "token" + + # -- Additional command-line arguments as key-value pairs + extraArgs: {} + + # -- Additional environment variables + extraEnv: [] + + service: + # -- UI service type + type: ClusterIP + # -- UI service port. Also the port the BFF listens on. + port: 8090 + # -- UI service annotations + annotations: {} + + # -- Resource limits and requests + resources: + limits: + cpu: 300m + memory: 128Mi + requests: + cpu: 100m + memory: 64Mi + + # -- Pod security context + podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + + # -- Container security context + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + + # -- Liveness probe configuration + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + + # -- Readiness probe configuration + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + + # -- Node selector for pod assignment + nodeSelector: {} + + # -- Tolerations for pod assignment + tolerations: [] + + # -- Affinity for pod assignment + affinity: {} + + # -- Pod annotations + podAnnotations: {} + + # -- Pod labels + podLabels: {} + + serviceAccount: + # -- Create service account + create: true + # -- Service account annotations + annotations: {} + # -- Service account name (auto-generated if not set) + name: "" + # -- CA certificate bundle configuration. # Mount a ConfigMap containing a CA bundle for TLS connections to registries. caBundle: From 5c26aad47a425184788227b3e5366d9547b4760a Mon Sep 17 00:00:00 2001 From: Elisa Date: Wed, 12 Aug 2026 14:22:24 +0200 Subject: [PATCH 2/3] docs: minor spelling changes to improve consistency --- charts/solar/values.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/solar/values.yaml b/charts/solar/values.yaml index b75e72e6..2a8afe01 100644 --- a/charts/solar/values.yaml +++ b/charts/solar/values.yaml @@ -349,7 +349,7 @@ ui: command: ["/solar-ui"] # OIDC configuration. The BFF is an OIDC client; users log in against the - # issuer and the BFF forwards their identity to the K8s API. + # issuer and the BFF forwards their identity to the Kubernetes API. oidc: # -- OIDC issuer URL, e.g. https://dex.example.com. Required when ui.enabled=true. issuer: "" @@ -366,7 +366,7 @@ ui: # UI command-line arguments args: - # -- How the user's OIDC identity reaches the K8s API: "token" forwards the + # -- How the user's OIDC identity reaches the Kubernetes API: "token" forwards the # id_token, "impersonate" makes the BFF's ServiceAccount impersonate the user # (and grants it impersonate RBAC). authMode: "token" From 4a048c71b4b431700fc3ef0c901983a49d9463c5 Mon Sep 17 00:00:00 2001 From: Chris Bargmann Date: Mon, 17 Aug 2026 15:12:25 +0200 Subject: [PATCH 3/3] docs(chart): regenerate helm-docs readme --- charts/solar/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/solar/README.md b/charts/solar/README.md index 2cd9b631..0a96b4b4 100644 --- a/charts/solar/README.md +++ b/charts/solar/README.md @@ -391,7 +391,7 @@ helm uninstall solar --namespace solar-system | renderer.image.tag | string | `""` | | | renderer.imagePullSecrets | list | `[]` | Image pull secrets for the renderer Pod. Use the Kubernetes shape `[{name: my-secret}]` (matches `apiserver.imagePullSecrets` etc.). Each referenced Secret must exist (type `kubernetes.io/dockerconfigjson`) in every namespace where Targets/RenderTasks are created — the renderer Pod runs in the RenderTask's namespace, so cross-namespace references don't work. Merged with `global.imagePullSecrets`. See the chart README for the recommended External Secrets Operator pattern that distributes a single source-of-truth credential to every namespace. | | ui.affinity | object | `{}` | Affinity for pod assignment | -| ui.args.authMode | string | `"token"` | How the user's OIDC identity reaches the K8s API: "token" forwards the id_token, "impersonate" makes the BFF's ServiceAccount impersonate the user (and grants it impersonate RBAC). | +| ui.args.authMode | string | `"token"` | How the user's OIDC identity reaches the Kubernetes API: "token" forwards the id_token, "impersonate" makes the BFF's ServiceAccount impersonate the user (and grants it impersonate RBAC). | | ui.command | list | `["/solar-ui"]` | Command to run in the container | | ui.enabled | bool | `false` | Enable UI deployment. Off by default: the UI needs an OIDC issuer to authenticate against, which is site-specific. | | ui.extraArgs | object | `{}` | Additional command-line arguments as key-value pairs |