[linstor-gui] Add package for LINBIT linstor-gui web UI
Ships LINBIT's LINSTOR web UI (GPL-3.0) as an opt-in system package under packages/system/linstor-gui so operators can manage LINSTOR nodes, resources, and volumes from a browser instead of the linstor CLI. The UI is built from the upstream pkg.linbit.com tarball onto an nginx-unprivileged base image. A chart-supplied nginx.conf proxies /v1 and /metrics to the LINSTOR controller REST API over mTLS using the existing linstor-client-tls secret created by the linstor package. Only a ClusterIP Service is created; no Ingress is shipped, because LINSTOR's controller API is a privileged cluster-wide storage surface and auth depends on the deployment's OIDC setup. Operators opt in via bundles.enabledPackages and wire up ingress + auth themselves (port-forward works out of the box). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
parent
1436fee2dd
commit
ae88fe3779
13 changed files with 453 additions and 0 deletions
21
packages/core/platform/sources/linstor-gui.yaml
Normal file
21
packages/core/platform/sources/linstor-gui.yaml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
kind: PackageSource
|
||||
metadata:
|
||||
name: cozystack.linstor-gui
|
||||
spec:
|
||||
sourceRef:
|
||||
kind: OCIRepository
|
||||
name: cozystack-packages
|
||||
namespace: cozy-system
|
||||
path: /
|
||||
variants:
|
||||
- name: default
|
||||
dependsOn:
|
||||
- cozystack.linstor
|
||||
components:
|
||||
- name: linstor-gui
|
||||
path: system/linstor-gui
|
||||
install:
|
||||
namespace: cozy-linstor
|
||||
releaseName: linstor-gui
|
||||
|
|
@ -149,6 +149,7 @@
|
|||
{{include "cozystack.platform.package.optional.default" (list "cozystack.external-dns-application" $) }}
|
||||
{{include "cozystack.platform.package.optional.default" (list "cozystack.external-secrets-operator" $) }}
|
||||
{{include "cozystack.platform.package.optional.default" (list "cozystack.velero" $) }}
|
||||
{{include "cozystack.platform.package.optional.default" (list "cozystack.linstor-gui" $) }}
|
||||
{{- if has "cozystack.bootbox" (default (list) .Values.bundles.enabledPackages) }}
|
||||
{{include "cozystack.platform.package.default" (list "cozystack.bootbox-application" $) }}
|
||||
{{include "cozystack.platform.package.default" (list "cozystack.bootbox" $) }}
|
||||
|
|
|
|||
3
packages/system/linstor-gui/Chart.yaml
Normal file
3
packages/system/linstor-gui/Chart.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
apiVersion: v2
|
||||
name: cozy-linstor-gui
|
||||
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
|
||||
32
packages/system/linstor-gui/Makefile
Normal file
32
packages/system/linstor-gui/Makefile
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export NAME=linstor-gui
|
||||
export NAMESPACE=cozy-linstor
|
||||
|
||||
include ../../../hack/common-envs.mk
|
||||
include ../../../hack/package.mk
|
||||
|
||||
LINSTOR_GUI_VERSION ?= 2.3.0
|
||||
|
||||
image: image-linstor-gui
|
||||
|
||||
image-linstor-gui:
|
||||
docker buildx build images/linstor-gui \
|
||||
--provenance false \
|
||||
--builder=$(BUILDER) \
|
||||
--platform=linux/amd64,linux/arm64 \
|
||||
--build-arg LINSTOR_GUI_VERSION=$(LINSTOR_GUI_VERSION) \
|
||||
--tag $(REGISTRY)/linstor-gui:$(call settag,$(LINSTOR_GUI_VERSION)) \
|
||||
--tag $(REGISTRY)/linstor-gui:$(call settag,$(LINSTOR_GUI_VERSION)-$(TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/linstor-gui:latest \
|
||||
--cache-to type=inline \
|
||||
--metadata-file images/linstor-gui.json \
|
||||
--push=$(PUSH) \
|
||||
--label "org.opencontainers.image.source=https://github.com/cozystack/cozystack" \
|
||||
--load=$(LOAD)
|
||||
REPOSITORY="$(REGISTRY)/linstor-gui" \
|
||||
yq -i '.image.repository = strenv(REPOSITORY)' values.yaml
|
||||
TAG="$(call settag,$(LINSTOR_GUI_VERSION))@$$(yq e '."containerimage.digest"' images/linstor-gui.json -o json -r)" \
|
||||
yq -i '.image.tag = strenv(TAG)' values.yaml
|
||||
rm -f images/linstor-gui.json
|
||||
|
||||
test:
|
||||
helm unittest .
|
||||
43
packages/system/linstor-gui/README.md
Normal file
43
packages/system/linstor-gui/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# linstor-gui
|
||||
|
||||
Cozystack system package for [LINBIT/linstor-gui](https://github.com/LINBIT/linstor-gui)
|
||||
— a web UI for managing LINSTOR nodes, resources, volumes and snapshots.
|
||||
|
||||
Installed alongside the `linstor` package in the `cozy-linstor` namespace. The UI
|
||||
proxies the LINSTOR controller REST API at `https://linstor-controller.cozy-linstor.svc:3371`
|
||||
using mTLS with the `linstor-client-tls` secret created by the `linstor` package.
|
||||
|
||||
## Exposing the UI
|
||||
|
||||
This package only creates a `ClusterIP` Service. It does **not** ship an ingress,
|
||||
because authentication depends on the deployment's Keycloak / OIDC setup and
|
||||
LINSTOR's controller API is a privileged cluster-wide storage management
|
||||
surface. Cluster admins should wire up ingress + auth explicitly, for example:
|
||||
|
||||
```bash
|
||||
kubectl -n cozy-linstor port-forward svc/linstor-gui 3373:80
|
||||
```
|
||||
|
||||
then open <http://localhost:3373>.
|
||||
|
||||
## Parameters
|
||||
|
||||
### Image
|
||||
|
||||
| Name | Description | Value |
|
||||
| ------------------ | ---------------------------------------------------------- | ----------------------------------------- |
|
||||
| `image.repository` | LINSTOR GUI container image repository | `ghcr.io/cozystack/cozystack/linstor-gui` |
|
||||
| `image.tag` | LINSTOR GUI container image tag (digest recommended) | `2.3.0` |
|
||||
|
||||
### Deployment
|
||||
|
||||
| Name | Description | Value |
|
||||
| ---------- | ------------------------------- | ----- |
|
||||
| `replicas` | Number of linstor-gui replicas | `1` |
|
||||
|
||||
### LINSTOR controller connection
|
||||
|
||||
| Name | Description | Value |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
|
||||
| `linstor.endpoint` | In-cluster URL of the LINSTOR controller REST API (HTTPS, mTLS) | `https://linstor-controller.cozy-linstor.svc:3371` |
|
||||
| `linstor.clientSecret` | Kubernetes Secret with `tls.crt`, `tls.key`, `ca.crt` used as the mTLS client certificate against the LINSTOR controller. Created by the `linstor` package. | `linstor-client-tls` |
|
||||
19
packages/system/linstor-gui/images/linstor-gui/Dockerfile
Normal file
19
packages/system/linstor-gui/images/linstor-gui/Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Upstream: https://github.com/LINBIT/linstor-gui (GPL-3.0)
|
||||
# Serves the pre-built LINSTOR GUI tarball published by LINBIT from a hardened
|
||||
# nginx-unprivileged image. nginx.conf is supplied by the chart via ConfigMap,
|
||||
# so the upstream docker-entrypoint.sh / nginx.conf.template is not used.
|
||||
FROM nginxinc/nginx-unprivileged:1.29-alpine
|
||||
|
||||
ARG LINSTOR_GUI_VERSION
|
||||
USER root
|
||||
RUN apk add --no-cache curl tar && \
|
||||
curl -fsSL "https://pkg.linbit.com/downloads/linstor/linstor-gui-${LINSTOR_GUI_VERSION}.tar.gz" -o /tmp/linstor-gui.tar.gz && \
|
||||
mkdir -p /usr/share/nginx/html && \
|
||||
tar -xzf /tmp/linstor-gui.tar.gz -C /usr/share/nginx/html --strip-components=2 "linstor-gui-${LINSTOR_GUI_VERSION}/dist" && \
|
||||
rm -f /tmp/linstor-gui.tar.gz && \
|
||||
apk del curl tar && \
|
||||
chown -R 101:0 /usr/share/nginx/html
|
||||
|
||||
USER 101
|
||||
EXPOSE 3373
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
17
packages/system/linstor-gui/templates/_helpers.tpl
Normal file
17
packages/system/linstor-gui/templates/_helpers.tpl
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "linstor-gui.labels" -}}
|
||||
app.kubernetes.io/name: linstor-gui
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: cozystack
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "linstor-gui.selectorLabels" -}}
|
||||
app.kubernetes.io/name: linstor-gui
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
82
packages/system/linstor-gui/templates/configmap-nginx.yaml
Normal file
82
packages/system/linstor-gui/templates/configmap-nginx.yaml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
{{/*
|
||||
Parse the LINSTOR endpoint so nginx can set `proxy_ssl_name` correctly.
|
||||
The endpoint is expected to be an https:// URL like
|
||||
https://linstor-controller.cozy-linstor.svc:3371
|
||||
*/}}
|
||||
{{- $endpoint := trimPrefix "https://" (trimPrefix "http://" .Values.linstor.endpoint) -}}
|
||||
{{- $endpointHost := (splitList ":" $endpoint) | first -}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: linstor-gui-nginx
|
||||
labels:
|
||||
{{- include "linstor-gui.labels" . | nindent 4 }}
|
||||
data:
|
||||
nginx.conf: |
|
||||
worker_processes auto;
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
server_tokens off;
|
||||
|
||||
client_body_temp_path /tmp/client_body;
|
||||
proxy_temp_path /tmp/proxy;
|
||||
fastcgi_temp_path /tmp/fastcgi;
|
||||
uwsgi_temp_path /tmp/uwsgi;
|
||||
scgi_temp_path /tmp/scgi;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 3373;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Static UI assets
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Proxy LINSTOR REST API over mTLS to the controller
|
||||
location /v1 {
|
||||
proxy_pass {{ .Values.linstor.endpoint }};
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_ssl_certificate /etc/linstor/client/tls.crt;
|
||||
proxy_ssl_certificate_key /etc/linstor/client/tls.key;
|
||||
proxy_ssl_trusted_certificate /etc/linstor/client/ca.crt;
|
||||
proxy_ssl_verify on;
|
||||
proxy_ssl_verify_depth 2;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name {{ $endpointHost }};
|
||||
}
|
||||
|
||||
location /metrics {
|
||||
proxy_pass {{ .Values.linstor.endpoint }};
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_ssl_certificate /etc/linstor/client/tls.crt;
|
||||
proxy_ssl_certificate_key /etc/linstor/client/tls.key;
|
||||
proxy_ssl_trusted_certificate /etc/linstor/client/ca.crt;
|
||||
proxy_ssl_verify on;
|
||||
proxy_ssl_verify_depth 2;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name {{ $endpointHost }};
|
||||
}
|
||||
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
return 200 'ok';
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
}
|
||||
101
packages/system/linstor-gui/templates/deployment.yaml
Normal file
101
packages/system/linstor-gui/templates/deployment.yaml
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: linstor-gui
|
||||
labels:
|
||||
{{- include "linstor-gui.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
spec:
|
||||
replicas: {{ .Values.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "linstor-gui.selectorLabels" . | nindent 6 }}
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/nginx-config: {{ include (print $.Template.BasePath "/configmap-nginx.yaml") . | sha256sum }}
|
||||
labels:
|
||||
{{- include "linstor-gui.labels" . | nindent 8 }}
|
||||
spec:
|
||||
serviceAccountName: linstor-gui
|
||||
automountServiceAccountToken: false
|
||||
priorityClassName: system-cluster-critical
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
{{- include "linstor-gui.selectorLabels" . | nindent 20 }}
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers:
|
||||
- name: linstor-gui
|
||||
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3373
|
||||
protocol: TCP
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 2
|
||||
periodSeconds: 10
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 101
|
||||
runAsGroup: 101
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/nginx.conf
|
||||
subPath: nginx.conf
|
||||
readOnly: true
|
||||
- name: linstor-client-tls
|
||||
mountPath: /etc/linstor/client
|
||||
readOnly: true
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: nginx-cache
|
||||
mountPath: /var/cache/nginx
|
||||
volumes:
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: linstor-gui-nginx
|
||||
- name: linstor-client-tls
|
||||
secret:
|
||||
secretName: {{ .Values.linstor.clientSecret }}
|
||||
defaultMode: 0400
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: nginx-cache
|
||||
emptyDir: {}
|
||||
15
packages/system/linstor-gui/templates/service.yaml
Normal file
15
packages/system/linstor-gui/templates/service.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: linstor-gui
|
||||
labels:
|
||||
{{- include "linstor-gui.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
selector:
|
||||
{{- include "linstor-gui.selectorLabels" . | nindent 4 }}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: linstor-gui
|
||||
labels:
|
||||
{{- include "linstor-gui.labels" . | nindent 4 }}
|
||||
automountServiceAccountToken: false
|
||||
95
packages/system/linstor-gui/tests/deployment_test.yaml
Normal file
95
packages/system/linstor-gui/tests/deployment_test.yaml
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
suite: linstor-gui deployment
|
||||
templates:
|
||||
- templates/deployment.yaml
|
||||
- templates/service.yaml
|
||||
- templates/configmap-nginx.yaml
|
||||
|
||||
tests:
|
||||
- it: renders a ClusterIP service on port 80 -> http
|
||||
template: templates/service.yaml
|
||||
release:
|
||||
name: linstor-gui
|
||||
namespace: cozy-linstor
|
||||
asserts:
|
||||
- isKind:
|
||||
of: Service
|
||||
- equal:
|
||||
path: spec.type
|
||||
value: ClusterIP
|
||||
- equal:
|
||||
path: spec.ports[0].port
|
||||
value: 80
|
||||
- equal:
|
||||
path: spec.ports[0].targetPort
|
||||
value: http
|
||||
|
||||
- it: mounts the LINSTOR client TLS secret into the pod
|
||||
template: templates/deployment.yaml
|
||||
release:
|
||||
name: linstor-gui
|
||||
namespace: cozy-linstor
|
||||
asserts:
|
||||
- isKind:
|
||||
of: Deployment
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: linstor-client-tls
|
||||
secret:
|
||||
secretName: linstor-client-tls
|
||||
defaultMode: 0400
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].volumeMounts
|
||||
content:
|
||||
name: linstor-client-tls
|
||||
mountPath: /etc/linstor/client
|
||||
readOnly: true
|
||||
|
||||
- it: runs as a non-root user with a read-only root filesystem
|
||||
template: templates/deployment.yaml
|
||||
release:
|
||||
name: linstor-gui
|
||||
namespace: cozy-linstor
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].securityContext.runAsNonRoot
|
||||
value: true
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem
|
||||
value: true
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation
|
||||
value: false
|
||||
|
||||
- it: proxies /v1 to the configured LINSTOR controller with mTLS
|
||||
template: templates/configmap-nginx.yaml
|
||||
release:
|
||||
name: linstor-gui
|
||||
namespace: cozy-linstor
|
||||
asserts:
|
||||
- isKind:
|
||||
of: ConfigMap
|
||||
- matchRegex:
|
||||
path: data["nginx.conf"]
|
||||
pattern: "location /v1"
|
||||
- matchRegex:
|
||||
path: data["nginx.conf"]
|
||||
pattern: "proxy_pass https://linstor-controller.cozy-linstor.svc:3371"
|
||||
- matchRegex:
|
||||
path: data["nginx.conf"]
|
||||
pattern: "proxy_ssl_certificate /etc/linstor/client/tls.crt"
|
||||
|
||||
- it: overrides linstor endpoint and client secret
|
||||
template: templates/configmap-nginx.yaml
|
||||
release:
|
||||
name: linstor-gui
|
||||
namespace: cozy-linstor
|
||||
set:
|
||||
linstor.endpoint: https://my-controller.example.svc:3371
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["nginx.conf"]
|
||||
pattern: "proxy_pass https://my-controller.example.svc:3371"
|
||||
- matchRegex:
|
||||
path: data["nginx.conf"]
|
||||
pattern: "proxy_ssl_name my-controller.example.svc"
|
||||
17
packages/system/linstor-gui/values.yaml
Normal file
17
packages/system/linstor-gui/values.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
## @section Image
|
||||
## @param image.repository LINSTOR GUI container image repository
|
||||
## @param image.tag LINSTOR GUI container image tag (digest recommended)
|
||||
image:
|
||||
repository: ghcr.io/cozystack/cozystack/linstor-gui
|
||||
tag: 2.3.0
|
||||
|
||||
## @section Deployment
|
||||
## @param replicas Number of linstor-gui replicas
|
||||
replicas: 1
|
||||
|
||||
## @section LINSTOR controller connection
|
||||
## @param linstor.endpoint In-cluster URL of the LINSTOR controller REST API (HTTPS, mTLS)
|
||||
## @param linstor.clientSecret Kubernetes Secret with `tls.crt`, `tls.key`, `ca.crt` used as the mTLS client certificate against the LINSTOR controller. Created by the `linstor` package.
|
||||
linstor:
|
||||
endpoint: "https://linstor-controller.cozy-linstor.svc:3371"
|
||||
clientSecret: "linstor-client-tls"
|
||||
Loading…
Add table
Add a link
Reference in a new issue