Skip to content

Commit a82dd25

Browse files
[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). Signed-off-by: Myasnikov Daniil <[email protected]>
1 parent 1436fee commit a82dd25

13 files changed

Lines changed: 453 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
apiVersion: cozystack.io/v1alpha1
3+
kind: PackageSource
4+
metadata:
5+
name: cozystack.linstor-gui
6+
spec:
7+
sourceRef:
8+
kind: OCIRepository
9+
name: cozystack-packages
10+
namespace: cozy-system
11+
path: /
12+
variants:
13+
- name: default
14+
dependsOn:
15+
- cozystack.linstor
16+
components:
17+
- name: linstor-gui
18+
path: system/linstor-gui
19+
install:
20+
namespace: cozy-linstor
21+
releaseName: linstor-gui

‎packages/core/platform/templates/bundles/system.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
{{include "cozystack.platform.package.optional.default" (list "cozystack.external-dns-application" $) }}
150150
{{include "cozystack.platform.package.optional.default" (list "cozystack.external-secrets-operator" $) }}
151151
{{include "cozystack.platform.package.optional.default" (list "cozystack.velero" $) }}
152+
{{include "cozystack.platform.package.optional.default" (list "cozystack.linstor-gui" $) }}
152153
{{- if has "cozystack.bootbox" (default (list) .Values.bundles.enabledPackages) }}
153154
{{include "cozystack.platform.package.default" (list "cozystack.bootbox-application" $) }}
154155
{{include "cozystack.platform.package.default" (list "cozystack.bootbox" $) }}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
apiVersion: v2
2+
name: cozy-linstor-gui
3+
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export NAME=linstor-gui
2+
export NAMESPACE=cozy-linstor
3+
4+
include ../../../hack/common-envs.mk
5+
include ../../../hack/package.mk
6+
7+
LINSTOR_GUI_VERSION ?= 2.3.0
8+
9+
image: image-linstor-gui
10+
11+
image-linstor-gui:
12+
docker buildx build images/linstor-gui \
13+
--provenance false \
14+
--builder=$(BUILDER) \
15+
--platform=linux/amd64,linux/arm64 \
16+
--build-arg LINSTOR_GUI_VERSION=$(LINSTOR_GUI_VERSION) \
17+
--tag $(REGISTRY)/linstor-gui:$(call settag,$(LINSTOR_GUI_VERSION)) \
18+
--tag $(REGISTRY)/linstor-gui:$(call settag,$(LINSTOR_GUI_VERSION)-$(TAG)) \
19+
--cache-from type=registry,ref=$(REGISTRY)/linstor-gui:latest \
20+
--cache-to type=inline \
21+
--metadata-file images/linstor-gui.json \
22+
--push=$(PUSH) \
23+
--label "org.opencontainers.image.source=https://github.com/cozystack/cozystack" \
24+
--load=$(LOAD)
25+
REPOSITORY="$(REGISTRY)/linstor-gui" \
26+
yq -i '.image.repository = strenv(REPOSITORY)' values.yaml
27+
TAG="$(call settag,$(LINSTOR_GUI_VERSION))@$$(yq e '."containerimage.digest"' images/linstor-gui.json -o json -r)" \
28+
yq -i '.image.tag = strenv(TAG)' values.yaml
29+
rm -f images/linstor-gui.json
30+
31+
test:
32+
helm unittest .
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# linstor-gui
2+
3+
Cozystack system package for [LINBIT/linstor-gui](https://github.com/LINBIT/linstor-gui)
4+
— a web UI for managing LINSTOR nodes, resources, volumes and snapshots.
5+
6+
Installed alongside the `linstor` package in the `cozy-linstor` namespace. The UI
7+
proxies the LINSTOR controller REST API at `https://linstor-controller.cozy-linstor.svc:3371`
8+
using mTLS with the `linstor-client-tls` secret created by the `linstor` package.
9+
10+
## Exposing the UI
11+
12+
This package only creates a `ClusterIP` Service. It does **not** ship an ingress,
13+
because authentication depends on the deployment's Keycloak / OIDC setup and
14+
LINSTOR's controller API is a privileged cluster-wide storage management
15+
surface. Cluster admins should wire up ingress + auth explicitly, for example:
16+
17+
```bash
18+
kubectl -n cozy-linstor port-forward svc/linstor-gui 3373:80
19+
```
20+
21+
then open <http://localhost:3373>.
22+
23+
## Parameters
24+
25+
### Image
26+
27+
| Name | Description | Value |
28+
| ------------------ | ---------------------------------------------------------- | ----------------------------------------- |
29+
| `image.repository` | LINSTOR GUI container image repository | `ghcr.io/cozystack/cozystack/linstor-gui` |
30+
| `image.tag` | LINSTOR GUI container image tag (digest recommended) | `2.3.0` |
31+
32+
### Deployment
33+
34+
| Name | Description | Value |
35+
| ---------- | ------------------------------- | ----- |
36+
| `replicas` | Number of linstor-gui replicas | `1` |
37+
38+
### LINSTOR controller connection
39+
40+
| Name | Description | Value |
41+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
42+
| `linstor.endpoint` | In-cluster URL of the LINSTOR controller REST API (HTTPS, mTLS) | `https://linstor-controller.cozy-linstor.svc:3371` |
43+
| `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` |
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Upstream: https://github.com/LINBIT/linstor-gui (GPL-3.0)
2+
# Serves the pre-built LINSTOR GUI tarball published by LINBIT from a hardened
3+
# nginx-unprivileged image. nginx.conf is supplied by the chart via ConfigMap,
4+
# so the upstream docker-entrypoint.sh / nginx.conf.template is not used.
5+
FROM nginxinc/nginx-unprivileged:1.29-alpine
6+
7+
ARG LINSTOR_GUI_VERSION
8+
USER root
9+
RUN apk add --no-cache curl tar && \
10+
curl -fsSL "https://pkg.linbit.com/downloads/linstor/linstor-gui-${LINSTOR_GUI_VERSION}.tar.gz" -o /tmp/linstor-gui.tar.gz && \
11+
mkdir -p /usr/share/nginx/html && \
12+
tar -xzf /tmp/linstor-gui.tar.gz -C /usr/share/nginx/html --strip-components=2 "linstor-gui-${LINSTOR_GUI_VERSION}/dist" && \
13+
rm -f /tmp/linstor-gui.tar.gz && \
14+
apk del curl tar && \
15+
chown -R 101:0 /usr/share/nginx/html
16+
17+
USER 101
18+
EXPOSE 3373
19+
CMD ["nginx", "-g", "daemon off;"]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{{/*
2+
Common labels
3+
*/}}
4+
{{- define "linstor-gui.labels" -}}
5+
app.kubernetes.io/name: linstor-gui
6+
app.kubernetes.io/instance: {{ .Release.Name }}
7+
app.kubernetes.io/managed-by: {{ .Release.Service }}
8+
app.kubernetes.io/part-of: cozystack
9+
{{- end }}
10+
11+
{{/*
12+
Selector labels
13+
*/}}
14+
{{- define "linstor-gui.selectorLabels" -}}
15+
app.kubernetes.io/name: linstor-gui
16+
app.kubernetes.io/instance: {{ .Release.Name }}
17+
{{- end }}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{{/*
2+
Parse the LINSTOR endpoint so nginx can set `proxy_ssl_name` correctly.
3+
The endpoint is expected to be an https:// URL like
4+
https://linstor-controller.cozy-linstor.svc:3371
5+
*/}}
6+
{{- $endpoint := trimPrefix "https://" (trimPrefix "http://" .Values.linstor.endpoint) -}}
7+
{{- $endpointHost := (splitList ":" $endpoint) | first -}}
8+
apiVersion: v1
9+
kind: ConfigMap
10+
metadata:
11+
name: linstor-gui-nginx
12+
labels:
13+
{{- include "linstor-gui.labels" . | nindent 4 }}
14+
data:
15+
nginx.conf: |
16+
worker_processes auto;
17+
pid /tmp/nginx.pid;
18+
19+
events {
20+
worker_connections 1024;
21+
}
22+
23+
http {
24+
include /etc/nginx/mime.types;
25+
default_type application/octet-stream;
26+
server_tokens off;
27+
28+
client_body_temp_path /tmp/client_body;
29+
proxy_temp_path /tmp/proxy;
30+
fastcgi_temp_path /tmp/fastcgi;
31+
uwsgi_temp_path /tmp/uwsgi;
32+
scgi_temp_path /tmp/scgi;
33+
34+
sendfile on;
35+
keepalive_timeout 65;
36+
37+
server {
38+
listen 3373;
39+
server_name _;
40+
41+
root /usr/share/nginx/html;
42+
index index.html;
43+
44+
# Static UI assets
45+
location / {
46+
try_files $uri $uri/ /index.html;
47+
}
48+
49+
# Proxy LINSTOR REST API over mTLS to the controller
50+
location /v1 {
51+
proxy_pass {{ .Values.linstor.endpoint }};
52+
proxy_set_header Host $host;
53+
proxy_set_header X-Real-IP $remote_addr;
54+
proxy_ssl_certificate /etc/linstor/client/tls.crt;
55+
proxy_ssl_certificate_key /etc/linstor/client/tls.key;
56+
proxy_ssl_trusted_certificate /etc/linstor/client/ca.crt;
57+
proxy_ssl_verify on;
58+
proxy_ssl_verify_depth 2;
59+
proxy_ssl_server_name on;
60+
proxy_ssl_name {{ $endpointHost }};
61+
}
62+
63+
location /metrics {
64+
proxy_pass {{ .Values.linstor.endpoint }};
65+
proxy_set_header Host $host;
66+
proxy_set_header X-Real-IP $remote_addr;
67+
proxy_ssl_certificate /etc/linstor/client/tls.crt;
68+
proxy_ssl_certificate_key /etc/linstor/client/tls.key;
69+
proxy_ssl_trusted_certificate /etc/linstor/client/ca.crt;
70+
proxy_ssl_verify on;
71+
proxy_ssl_verify_depth 2;
72+
proxy_ssl_server_name on;
73+
proxy_ssl_name {{ $endpointHost }};
74+
}
75+
76+
location = /healthz {
77+
access_log off;
78+
return 200 'ok';
79+
add_header Content-Type text/plain;
80+
}
81+
}
82+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: linstor-gui
5+
labels:
6+
{{- include "linstor-gui.labels" . | nindent 4 }}
7+
annotations:
8+
reloader.stakater.com/auto: "true"
9+
spec:
10+
replicas: {{ .Values.replicas }}
11+
selector:
12+
matchLabels:
13+
{{- include "linstor-gui.selectorLabels" . | nindent 6 }}
14+
strategy:
15+
type: RollingUpdate
16+
rollingUpdate:
17+
maxUnavailable: 0
18+
maxSurge: 1
19+
template:
20+
metadata:
21+
annotations:
22+
checksum/nginx-config: {{ include (print $.Template.BasePath "/configmap-nginx.yaml") . | sha256sum }}
23+
labels:
24+
{{- include "linstor-gui.labels" . | nindent 8 }}
25+
spec:
26+
serviceAccountName: linstor-gui
27+
automountServiceAccountToken: false
28+
priorityClassName: system-cluster-critical
29+
securityContext:
30+
runAsNonRoot: true
31+
seccompProfile:
32+
type: RuntimeDefault
33+
affinity:
34+
podAntiAffinity:
35+
preferredDuringSchedulingIgnoredDuringExecution:
36+
- weight: 100
37+
podAffinityTerm:
38+
labelSelector:
39+
matchLabels:
40+
{{- include "linstor-gui.selectorLabels" . | nindent 20 }}
41+
topologyKey: kubernetes.io/hostname
42+
containers:
43+
- name: linstor-gui
44+
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
45+
imagePullPolicy: IfNotPresent
46+
ports:
47+
- name: http
48+
containerPort: 3373
49+
protocol: TCP
50+
resources:
51+
requests:
52+
cpu: 50m
53+
memory: 64Mi
54+
limits:
55+
cpu: 200m
56+
memory: 128Mi
57+
livenessProbe:
58+
httpGet:
59+
path: /healthz
60+
port: http
61+
initialDelaySeconds: 5
62+
periodSeconds: 20
63+
readinessProbe:
64+
httpGet:
65+
path: /healthz
66+
port: http
67+
initialDelaySeconds: 2
68+
periodSeconds: 10
69+
securityContext:
70+
allowPrivilegeEscalation: false
71+
readOnlyRootFilesystem: true
72+
runAsNonRoot: true
73+
runAsUser: 101
74+
runAsGroup: 101
75+
capabilities:
76+
drop:
77+
- ALL
78+
volumeMounts:
79+
- name: nginx-config
80+
mountPath: /etc/nginx/nginx.conf
81+
subPath: nginx.conf
82+
readOnly: true
83+
- name: linstor-client-tls
84+
mountPath: /etc/linstor/client
85+
readOnly: true
86+
- name: tmp
87+
mountPath: /tmp
88+
- name: nginx-cache
89+
mountPath: /var/cache/nginx
90+
volumes:
91+
- name: nginx-config
92+
configMap:
93+
name: linstor-gui-nginx
94+
- name: linstor-client-tls
95+
secret:
96+
secretName: {{ .Values.linstor.clientSecret }}
97+
defaultMode: 0400
98+
- name: tmp
99+
emptyDir: {}
100+
- name: nginx-cache
101+
emptyDir: {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: linstor-gui
5+
labels:
6+
{{- include "linstor-gui.labels" . | nindent 4 }}
7+
spec:
8+
type: ClusterIP
9+
ports:
10+
- name: http
11+
port: 80
12+
targetPort: http
13+
protocol: TCP
14+
selector:
15+
{{- include "linstor-gui.selectorLabels" . | nindent 4 }}

0 commit comments

Comments
 (0)