This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch horizon-ui-support in repository https://gitbox.apache.org/repos/asf/skywalking-swck.git
commit 9fc989dd860715ce312b777c407b9e7ead0f856d Author: Wu Sheng <[email protected]> AuthorDate: Tue May 19 09:17:59 2026 +0800 feat: support Horizon UI in UI CRD via spec.kind discriminator Add `spec.kind` (enum: horizon|booster, default horizon) to the UI CRD so the operator can deploy either the legacy booster-ui image or the next-generation horizon-ui image from a single CRD. For kind=horizon the operator: - defaults image to apache/skywalking-horizon-ui:<version> - generates a ConfigMap with horizon.yaml and mounts it at /app/horizon.yaml (read-only) on the container - mounts an emptyDir at /data for Horizon's writable state - listens on port 8081 and updates probes + Service targetPort - derives oap.adminUrl from <name>-oap.<ns>:17128 and oap.zipkinUrl from <OAPServerAddress>/zipkin when not explicitly set For kind=booster the existing behavior (image apache/skywalking-ui, port 8080, SW_OAP_ADDRESS env var) is preserved unchanged. Also exposes port 17128 (admin) on the OAPServer Service so Horizon can reach runtime-rule, DSL/MQE debug, and inspect endpoints. Existing e2e + sample YAMLs are pinned to `kind: booster` so their `apache/skywalking-ui:9.5.0` references keep working under the new horizon default. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --- docs/getting-started.md | 44 ++++++++++++- operator/apis/operator/v1alpha1/ui_types.go | 24 ++++++- operator/apis/operator/v1alpha1/ui_webhook.go | 22 ++++++- .../operator/v1alpha1/zz_generated.deepcopy.go | 2 +- .../bases/operator.skywalking.apache.org_uis.yaml | 31 ++++++++- operator/config/samples/default.yaml | 11 +++- operator/config/samples/fetcher.yaml | 1 + operator/controllers/operator/ui_controller.go | 2 + .../manifests/oapserver/templates/service.yaml | 2 + .../operator/manifests/ui/templates/configmap.yaml | 77 ++++++++++++++++++++++ .../manifests/ui/templates/deployment.yaml | 30 ++++++++- .../operator/manifests/ui/templates/service.yaml | 4 +- test/e2e/skywalking-components-with-banyandb.yaml | 1 + test/e2e/skywalking-components-with-satellite.yaml | 1 + test/e2e/skywalking-components-with-storage.yaml | 1 + ...ywalking-components-with-swagent-configmap.yaml | 1 + test/e2e/skywalking-components-with-swagent.yaml | 1 + test/e2e/skywalking-components.yaml | 1 + 18 files changed, 245 insertions(+), 11 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index a8f2ce7..9cf683b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -175,11 +175,14 @@ spec: type: ClusterIP --- apiVersion: operator.skywalking.apache.org/v1alpha1 -kind: UI +kind: UI metadata: name: skywalking-system namespace: skywalking-system spec: + # spec.kind selects the UI flavor. Default is "horizon" (next-gen UI). + # Use "booster" to deploy the legacy apache/skywalking-ui image. + kind: booster version: 9.5.0 instances: 1 image: apache/skywalking-ui:9.5.0 @@ -192,6 +195,45 @@ spec: EOF ``` +#### UI flavors (`spec.kind`) + +The `UI` resource supports two flavors via `spec.kind`: + +| `spec.kind` | Default image | Listens on | OAP wire-up | +|-----------------------|---------------------------------------|------------|------------------------------------------------| +| `horizon` *(default)* | `apache/skywalking-horizon-ui:<ver>` | `8081` | YAML config mounted at `/app/horizon.yaml` | +| `booster` | `apache/skywalking-ui:<ver>` | `8080` | `SW_OAP_ADDRESS` env var | + +When `spec.kind: horizon`, the operator additionally: + +- Creates `ConfigMap/<name>-ui-horizon` with key `horizon.yaml`, populated from `spec.OAPServerAddress` (→ `oap.queryUrl`), `spec.OAPServerAdminAddress` (default `http://<name>-oap.<ns>:17128` → `oap.adminUrl`), and `spec.OAPServerZipkinAddress` (default `<OAPServerAddress>/zipkin` → `oap.zipkinUrl`). +- Mounts that ConfigMap as `/app/horizon.yaml` (read-only) on the Horizon UI container. +- Mounts an `emptyDir` at `/data` for Horizon's writable state (audit log, setup state, alarm state, wire-debug log). State is lost on pod restart. + +Set `spec.config` to a raw `horizon.yaml` string to fully override the operator-generated config (e.g. to configure local users, LDAP, RBAC, or session settings). When set, the operator-generated fields above are ignored. + +Note: the `OAPServer` Service exposes port `17128` (admin) in addition to `12800`/`11800`/`1234` so Horizon UI can reach runtime-rule, DSL/MQE debug, and inspect endpoints. The admin port must be enabled in your OAP configuration for Horizon's admin features to work. + +Minimal horizon example (defaults pick image, admin URL, and Zipkin URL): + +```yaml +apiVersion: operator.skywalking.apache.org/v1alpha1 +kind: UI +metadata: + name: skywalking-system + namespace: skywalking-system +spec: + kind: horizon # default — can be omitted + version: 9.5.0 + instances: 1 + OAPServerAddress: http://skywalking-system-oap.skywalking-system:12800 + service: + template: + type: ClusterIP + ingress: + host: demo.ui.skywalking +``` + Check the status of the skywalking components. ```shell diff --git a/operator/apis/operator/v1alpha1/ui_types.go b/operator/apis/operator/v1alpha1/ui_types.go index 22d2050..b4d7e79 100644 --- a/operator/apis/operator/v1alpha1/ui_types.go +++ b/operator/apis/operator/v1alpha1/ui_types.go @@ -24,6 +24,13 @@ import ( // UISpec defines the desired state of UI type UISpec struct { + // Kind selects which SkyWalking web UI to deploy. + // "horizon" deploys the next-generation Horizon UI (default). + // "booster" deploys the legacy Booster UI image. + // +kubebuilder:validation:Enum=horizon;booster + // +kubebuilder:default=horizon + // +kubebuilder:validation:Optional + Kind string `json:"kind,omitempty"` // Version of UI. // +kubebuilder:validation:Required Version string `json:"version"` @@ -32,9 +39,24 @@ type UISpec struct { // Count is the number of UI pods // +kubebuilder:validation:Required Instances int32 `json:"instances"` - // Backend OAP server address + // Backend OAP server address. + // For kind=booster, exported as the SW_OAP_ADDRESS env var. + // For kind=horizon, used as oap.queryUrl in the generated horizon.yaml. // +kubebuilder:validation:Optional OAPServerAddress string `json:"OAPServerAddress,omitempty"` + // OAPServerAdminAddress is the OAP admin host (port 17128 by default; runtime-rule, + // dsl-debug, inspect, status). Only used when kind=horizon. If unset, defaults to + // http://<name>-oap.<namespace>:17128. + // +kubebuilder:validation:Optional + OAPServerAdminAddress string `json:"OAPServerAdminAddress,omitempty"` + // OAPServerZipkinAddress is the OAP Zipkin REST host. Only used when kind=horizon. + // If unset, defaults to <OAPServerAddress>/zipkin. + // +kubebuilder:validation:Optional + OAPServerZipkinAddress string `json:"OAPServerZipkinAddress,omitempty"` + // Config is a raw horizon.yaml that, when set, fully replaces the operator-generated + // config mounted into the Horizon UI container. Only used when kind=horizon. + // +kubebuilder:validation:Optional + Config string `json:"config,omitempty"` // Service relevant settings // +kubebuilder:validation:Optional Service Service `json:"service,omitempty"` diff --git a/operator/apis/operator/v1alpha1/ui_webhook.go b/operator/apis/operator/v1alpha1/ui_webhook.go index 0368d5c..8227de6 100644 --- a/operator/apis/operator/v1alpha1/ui_webhook.go +++ b/operator/apis/operator/v1alpha1/ui_webhook.go @@ -43,14 +43,31 @@ func (r *UI) SetupWebhookWithManager(mgr ctrl.Manager) error { func (r *UI) Default(_ context.Context, ui *UI) error { uilog.Info("default", "name", ui.Name) + if ui.Spec.Kind == "" { + ui.Spec.Kind = "horizon" + } + if ui.Spec.Image == "" { - ui.Spec.Image = fmt.Sprintf("apache/skywalking-ui:%s", ui.Spec.Version) + switch ui.Spec.Kind { + case "booster": + ui.Spec.Image = fmt.Sprintf("apache/skywalking-ui:%s", ui.Spec.Version) + default: + ui.Spec.Image = fmt.Sprintf("apache/skywalking-horizon-ui:%s", ui.Spec.Version) + } } ui.Spec.Service.Template.Default() if ui.Spec.OAPServerAddress == "" { ui.Spec.OAPServerAddress = fmt.Sprintf("http://%s-oap.%s:12800", ui.Name, ui.Namespace) } + if ui.Spec.Kind == "horizon" { + if ui.Spec.OAPServerAdminAddress == "" { + ui.Spec.OAPServerAdminAddress = fmt.Sprintf("http://%s-oap.%s:17128", ui.Name, ui.Namespace) + } + if ui.Spec.OAPServerZipkinAddress == "" { + ui.Spec.OAPServerZipkinAddress = ui.Spec.OAPServerAddress + "/zipkin" + } + } return nil } @@ -77,6 +94,9 @@ func (r *UI) ValidateDelete(_ context.Context, ui *UI) (admission.Warnings, erro } func (r *UI) validate() error { + if r.Spec.Kind != "" && r.Spec.Kind != "booster" && r.Spec.Kind != "horizon" { + return fmt.Errorf("unknown ui kind %q (allowed: horizon, booster)", r.Spec.Kind) + } if r.Spec.Image == "" { return fmt.Errorf("image is absent") } diff --git a/operator/apis/operator/v1alpha1/zz_generated.deepcopy.go b/operator/apis/operator/v1alpha1/zz_generated.deepcopy.go index 812490c..08f0a35 100644 --- a/operator/apis/operator/v1alpha1/zz_generated.deepcopy.go +++ b/operator/apis/operator/v1alpha1/zz_generated.deepcopy.go @@ -25,7 +25,7 @@ import ( v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/operator/config/crd/bases/operator.skywalking.apache.org_uis.yaml b/operator/config/crd/bases/operator.skywalking.apache.org_uis.yaml index 5682aca..23016f2 100644 --- a/operator/config/crd/bases/operator.skywalking.apache.org_uis.yaml +++ b/operator/config/crd/bases/operator.skywalking.apache.org_uis.yaml @@ -87,7 +87,26 @@ spec: description: UISpec defines the desired state of UI properties: OAPServerAddress: - description: Backend OAP server address + description: |- + Backend OAP server address. + For kind=booster, exported as the SW_OAP_ADDRESS env var. + For kind=horizon, used as oap.queryUrl in the generated horizon.yaml. + type: string + OAPServerAdminAddress: + description: |- + OAPServerAdminAddress is the OAP admin host (port 17128 by default; runtime-rule, + dsl-debug, inspect, status). Only used when kind=horizon. If unset, defaults to + http://<name>-oap.<namespace>:17128. + type: string + OAPServerZipkinAddress: + description: |- + OAPServerZipkinAddress is the OAP Zipkin REST host. Only used when kind=horizon. + If unset, defaults to <OAPServerAddress>/zipkin. + type: string + config: + description: |- + Config is a raw horizon.yaml that, when set, fully replaces the operator-generated + config mounted into the Horizon UI container. Only used when kind=horizon. type: string image: description: Image is the UI Docker image to deploy. @@ -96,6 +115,16 @@ spec: description: Count is the number of UI pods format: int32 type: integer + kind: + default: horizon + description: |- + Kind selects which SkyWalking web UI to deploy. + "horizon" deploys the next-generation Horizon UI (default). + "booster" deploys the legacy Booster UI image. + enum: + - horizon + - booster + type: string service: description: Service relevant settings properties: diff --git a/operator/config/samples/default.yaml b/operator/config/samples/default.yaml index bbd78a3..20d0680 100644 --- a/operator/config/samples/default.yaml +++ b/operator/config/samples/default.yaml @@ -29,14 +29,21 @@ spec: --- apiVersion: operator.skywalking.apache.org/v1alpha1 -kind: UI +kind: UI metadata: name: default spec: + # spec.kind selects the UI flavor: + # horizon (default) -> apache/skywalking-horizon-ui, config mounted at /app/horizon.yaml + # booster -> apache/skywalking-ui, OAP address via SW_OAP_ADDRESS env var + kind: horizon version: 9.5.0 instances: 1 - image: apache/skywalking-ui:9.5.0 + # image defaults to apache/skywalking-horizon-ui:<version> when omitted. OAPServerAddress: http://default-oap.default:12800 + # The following two are horizon-only and default to derived URLs if omitted: + # OAPServerAdminAddress: http://default-oap.default:17128 + # OAPServerZipkinAddress: http://default-oap.default:12800/zipkin service: template: type: ClusterIP diff --git a/operator/config/samples/fetcher.yaml b/operator/config/samples/fetcher.yaml index 92de199..4ce860d 100644 --- a/operator/config/samples/fetcher.yaml +++ b/operator/config/samples/fetcher.yaml @@ -46,6 +46,7 @@ kind: UI metadata: name: default spec: + kind: booster version: 9.5.0 instances: 1 image: apache/skywalking-ui:9.5.0 diff --git a/operator/controllers/operator/ui_controller.go b/operator/controllers/operator/ui_controller.go index 85b22e0..5290b3e 100644 --- a/operator/controllers/operator/ui_controller.go +++ b/operator/controllers/operator/ui_controller.go @@ -50,6 +50,7 @@ type UIReconciler struct { // +kubebuilder:rbac:groups=operator.skywalking.apache.org,resources=uis,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=operator.skywalking.apache.org,resources=uis/status,verbs=get;update;patch // +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch func (r *UIReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -150,6 +151,7 @@ func (r *UIReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&uiv1alpha1.UI{}). Owns(&apps.Deployment{}). Owns(&core.Service{}). + Owns(&core.ConfigMap{}). Owns(&networkingv1.Ingress{}). Complete(r) } diff --git a/operator/pkg/operator/manifests/oapserver/templates/service.yaml b/operator/pkg/operator/manifests/oapserver/templates/service.yaml index 65f2389..7014803 100644 --- a/operator/pkg/operator/manifests/oapserver/templates/service.yaml +++ b/operator/pkg/operator/manifests/oapserver/templates/service.yaml @@ -35,6 +35,8 @@ spec: name: grpc - port: 1234 name: http-monitoring + - port: 17128 + name: admin {{- if $svc.ExternalIPs }} externalIPs: {{- range $value := $svc.ExternalIPs }} diff --git a/operator/pkg/operator/manifests/ui/templates/configmap.yaml b/operator/pkg/operator/manifests/ui/templates/configmap.yaml new file mode 100644 index 0000000..ce64773 --- /dev/null +++ b/operator/pkg/operator/manifests/ui/templates/configmap.yaml @@ -0,0 +1,77 @@ +# Licensed to Apache Software Foundation (ASF) under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Apache Software Foundation (ASF) licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +{{- if eq .Spec.Kind "horizon" }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Name }}-ui-horizon + namespace: {{ .Namespace }} + labels: + app: ui + operator.skywalking.apache.org/ui-name: {{ .Name }} + operator.skywalking.apache.org/application: ui + operator.skywalking.apache.org/component: configmap +data: + horizon.yaml: | +{{- if .Spec.Config }} +{{ .Spec.Config | indent 4 }} +{{- else }} + server: + host: 0.0.0.0 + port: 8081 + oap: + queryUrl: {{ .Spec.OAPServerAddress }} + adminUrl: {{ .Spec.OAPServerAdminAddress }} + zipkinUrl: {{ .Spec.OAPServerZipkinAddress }} + timeoutMs: 15000 + auth: + backend: local + local: + users: [] + rbac: + enabled: true + roles: + viewer: + - metrics:read + - alarms:read + - traces:read + - logs:read + - topology:read + - profile:read + admin: + - "*" + landingByRole: + viewer: / + admin: /admin/cluster + session: + ttlMinutes: 60 + cookieName: horizon_sid + cookieSecure: false + audit: + file: /data/horizon-audit.jsonl + setup: + file: /data/horizon-setup.json + alarms: + file: /data/horizon-alarms.json + debugLog: + enabled: false + file: /data/horizon-wire.jsonl + maxBodyChars: 8192 + redactAuthHeaders: true +{{- end }} +{{- end }} diff --git a/operator/pkg/operator/manifests/ui/templates/deployment.yaml b/operator/pkg/operator/manifests/ui/templates/deployment.yaml index 998a366..39cdb50 100644 --- a/operator/pkg/operator/manifests/ui/templates/deployment.yaml +++ b/operator/pkg/operator/manifests/ui/templates/deployment.yaml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +{{- $isHorizon := eq .Spec.Kind "horizon" }} +{{- $port := 8080 }} +{{- if $isHorizon }}{{ $port = 8081 }}{{ end }} apiVersion: apps/v1 kind: Deployment metadata: @@ -55,7 +58,7 @@ spec: image: {{ .Spec.Image }} imagePullPolicy: IfNotPresent ports: - - containerPort: 8080 + - containerPort: {{ $port }} name: page livenessProbe: initialDelaySeconds: 10 @@ -65,7 +68,7 @@ spec: successThreshold: 1 httpGet: path: / - port: 8080 + port: {{ $port }} readinessProbe: initialDelaySeconds: 10 timeoutSeconds: 10 @@ -74,7 +77,28 @@ spec: successThreshold: 1 httpGet: path: / - port: 8080 + port: {{ $port }} + {{- if $isHorizon }} + volumeMounts: + - name: horizon-config + mountPath: /app/horizon.yaml + subPath: horizon.yaml + readOnly: true + - name: horizon-data + mountPath: /data + {{- else }} env: - name: SW_OAP_ADDRESS value: {{ .Spec.OAPServerAddress }} + {{- end }} + {{- if $isHorizon }} + volumes: + - name: horizon-config + configMap: + name: {{ .Name }}-ui-horizon + items: + - key: horizon.yaml + path: horizon.yaml + - name: horizon-data + emptyDir: {} + {{- end }} diff --git a/operator/pkg/operator/manifests/ui/templates/service.yaml b/operator/pkg/operator/manifests/ui/templates/service.yaml index 2deb15b..19b527a 100644 --- a/operator/pkg/operator/manifests/ui/templates/service.yaml +++ b/operator/pkg/operator/manifests/ui/templates/service.yaml @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. {{- $svc := .Spec.Service.Template }} +{{- $targetPort := 8080 }} +{{- if eq .Spec.Kind "horizon" }}{{ $targetPort = 8081 }}{{ end }} apiVersion: v1 kind: Service metadata: @@ -27,7 +29,7 @@ spec: type: {{ $svc.Type }} ports: - port: 80 - targetPort: 8080 + targetPort: {{ $targetPort }} name: page {{- if $svc.ExternalIPs }} externalIPs: diff --git a/test/e2e/skywalking-components-with-banyandb.yaml b/test/e2e/skywalking-components-with-banyandb.yaml index 304a2dd..5a30619 100644 --- a/test/e2e/skywalking-components-with-banyandb.yaml +++ b/test/e2e/skywalking-components-with-banyandb.yaml @@ -36,6 +36,7 @@ metadata: name: skywalking-system namespace: skywalking-system spec: + kind: booster version: 9.5.0 instances: 1 image: apache/skywalking-ui:9.5.0 diff --git a/test/e2e/skywalking-components-with-satellite.yaml b/test/e2e/skywalking-components-with-satellite.yaml index 85d75b1..8caa327 100644 --- a/test/e2e/skywalking-components-with-satellite.yaml +++ b/test/e2e/skywalking-components-with-satellite.yaml @@ -55,6 +55,7 @@ metadata: name: skywalking-system namespace: skywalking-system spec: + kind: booster version: 9.5.0 instances: 1 image: apache/skywalking-ui:9.5.0 diff --git a/test/e2e/skywalking-components-with-storage.yaml b/test/e2e/skywalking-components-with-storage.yaml index 3018851..f8ff6b0 100644 --- a/test/e2e/skywalking-components-with-storage.yaml +++ b/test/e2e/skywalking-components-with-storage.yaml @@ -35,6 +35,7 @@ metadata: name: skywalking-system namespace: skywalking-system spec: + kind: booster version: 9.5.0 instances: 1 image: apache/skywalking-ui:9.5.0 diff --git a/test/e2e/skywalking-components-with-swagent-configmap.yaml b/test/e2e/skywalking-components-with-swagent-configmap.yaml index 3b89dd9..8dfb2c7 100644 --- a/test/e2e/skywalking-components-with-swagent-configmap.yaml +++ b/test/e2e/skywalking-components-with-swagent-configmap.yaml @@ -35,6 +35,7 @@ metadata: name: skywalking-system namespace: skywalking-system spec: + kind: booster version: 9.5.0 instances: 1 image: apache/skywalking-ui:9.5.0 diff --git a/test/e2e/skywalking-components-with-swagent.yaml b/test/e2e/skywalking-components-with-swagent.yaml index 4150a5a..9c6ac97 100644 --- a/test/e2e/skywalking-components-with-swagent.yaml +++ b/test/e2e/skywalking-components-with-swagent.yaml @@ -35,6 +35,7 @@ metadata: name: skywalking-system namespace: skywalking-system spec: + kind: booster version: 9.5.0 instances: 1 image: apache/skywalking-ui:9.5.0 diff --git a/test/e2e/skywalking-components.yaml b/test/e2e/skywalking-components.yaml index 2e9b1d0..ff4aaff 100644 --- a/test/e2e/skywalking-components.yaml +++ b/test/e2e/skywalking-components.yaml @@ -34,6 +34,7 @@ metadata: name: skywalking-system namespace: skywalking-system spec: + kind: booster version: 9.5.0 instances: 1 image: apache/skywalking-ui:9.5.0
