Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ docs/.hugo_build.lock

# Generated docs-gen binary
/gen-ai-docs

# Generated drop-ui binary
/ui
/drop-ui
.kubeconfig
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Image URL to use all building/pushing image targets
IMG ?= controller:latest
IMG_UI ?= drop-ui:latest

# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
Expand Down Expand Up @@ -27,10 +28,18 @@ help: ## Display this help.
build: ## Build manager binary.
go build -o bin/manager cmd/main.go

.PHONY: build-ui
build-ui: ## Build Drop Control Center UI binary.
go build -o bin/drop-ui ./cmd/ui/

.PHONY: run
run: ## Run controller from your host.
go run ./cmd/main.go

.PHONY: run-ui
run-ui: ## Run Drop Control Center UI from your host (requires kubeconfig).
go run ./cmd/ui/

.PHONY: fmt
fmt: ## Run go fmt.
go fmt ./...
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,46 @@ go build ./... # compile
tilt up
```

## Drop Control Center UI

A cyberpunk-themed tactical web UI for visualising the drop operator in real-time.

### Views

| View | Description |
|------|-------------|
| **TACTICAL** | Animated canvas radar showing nodes as stations, live "drop pod" ships flying from the central nexus to nodes during pulls, particle bursts on success |
| **MATRIX** | Searchable & sortable table of all `CachedImage` resources with progress bars, cached-node chips, and phase filters |
| **RECON** | Discovery policy inspector with YAML viewer, sync metadata, and a ranked list of auto-discovered images |

### Run locally

```bash
make build-ui
./bin/drop-ui --bind-address :8888
# or directly:
go run ./cmd/ui/ --bind-address :8888
```

Then open **http://localhost:8888** in your browser.

### Deploy via Helm

```bash
helm install drop charts/drop -n drop-system --create-namespace \
--set ui.enabled=true \
--set ui.image.repository=ghcr.io/breee/drop-ui
```

To expose the UI with a `NodePort`:

```bash
helm upgrade drop charts/drop -n drop-system \
--set ui.enabled=true \
--set ui.service.type=NodePort \
--set ui.service.nodePort=30888
```

## Docs

Full documentation at **[breee.github.io/drop/](https://breee.github.io/drop/)** (GitHub Pages).
19 changes: 19 additions & 0 deletions charts/drop/templates/ui-clusterrole.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{{- if .Values.ui.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "drop.fullname" . }}-ui
labels:
{{- include "drop.labels" . | nindent 4 }}
app.kubernetes.io/component: ui
rules:
- apiGroups: ["drop.corewire.io"]
resources: ["cachedimages", "cachedimagesets", "discoverypolicies", "pullpolicies"]
verbs: ["get", "list", "watch"]
- apiGroups: ["drop.corewire.io"]
resources: ["cachedimages/status", "cachedimagesets/status", "discoverypolicies/status"]
verbs: ["get"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch"]
{{- end }}
17 changes: 17 additions & 0 deletions charts/drop/templates/ui-clusterrolebinding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{{- if .Values.ui.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "drop.fullname" . }}-ui
labels:
{{- include "drop.labels" . | nindent 4 }}
app.kubernetes.io/component: ui
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "drop.fullname" . }}-ui
subjects:
- kind: ServiceAccount
name: {{ include "drop.fullname" . }}-ui
namespace: {{ .Release.Namespace }}
{{- end }}
66 changes: 66 additions & 0 deletions charts/drop/templates/ui-deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{{- if .Values.ui.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "drop.fullname" . }}-ui
labels:
{{- include "drop.labels" . | nindent 4 }}
app.kubernetes.io/component: ui
spec:
replicas: 1
selector:
matchLabels:
{{- include "drop.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: ui
template:
metadata:
labels:
{{- include "drop.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: ui
spec:
serviceAccountName: {{ include "drop.fullname" . }}-ui
securityContext:
runAsNonRoot: true
containers:
- name: ui
image: "{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.ui.image.pullPolicy }}
args:
- --bind-address=:{{ .Values.ui.port }}
- --poll-interval={{ .Values.ui.pollInterval }}
ports:
- name: http
containerPort: {{ .Values.ui.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 20
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 3
periodSeconds: 10
resources:
{{- toYaml .Values.ui.resources | nindent 12 }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
22 changes: 22 additions & 0 deletions charts/drop/templates/ui-service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{{- if .Values.ui.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "drop.fullname" . }}-ui
labels:
{{- include "drop.labels" . | nindent 4 }}
app.kubernetes.io/component: ui
spec:
type: {{ .Values.ui.service.type }}
ports:
- port: {{ .Values.ui.service.port }}
targetPort: http
protocol: TCP
name: http
{{- if and (eq .Values.ui.service.type "NodePort") .Values.ui.service.nodePort }}
nodePort: {{ .Values.ui.service.nodePort }}
{{- end }}
selector:
{{- include "drop.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: ui
{{- end }}
9 changes: 9 additions & 0 deletions charts/drop/templates/ui-serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{{- if .Values.ui.enabled }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "drop.fullname" . }}-ui
labels:
{{- include "drop.labels" . | nindent 4 }}
app.kubernetes.io/component: ui
{{- end }}
24 changes: 24 additions & 0 deletions charts/drop/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,27 @@ certManager:
nodeSelector: {}
tolerations: []
affinity: {}

# Drop Control Center UI
ui:
# Set to true to deploy the Drop Control Center UI alongside the operator.
enabled: false
image:
repository: ghcr.io/breee/drop-ui
pullPolicy: IfNotPresent
tag: "" # Defaults to Chart appVersion
# Port the UI container listens on.
port: 8888
# How often the UI polls Kubernetes for live SSE updates.
pollInterval: 10s
service:
type: ClusterIP
port: 8888
# nodePort: 30888 # Uncomment when type=NodePort
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 10m
memory: 32Mi
82 changes: 82 additions & 0 deletions cmd/ui/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Copyright (c) 2026 Breee

SPDX-License-Identifier: MIT
*/

// drop-ui is a standalone web server that serves the Drop Control Center UI.
// It connects to the Kubernetes API to read drop.corewire.io CRDs and
// exposes a REST + SSE API consumed by the browser.
//
// Usage:
//
// drop-ui --bind-address :8888
// drop-ui --kubeconfig ~/.kube/config --bind-address :8888
package main

import (
"flag"
"net/http"
"os"
"time"

_ "k8s.io/client-go/plugin/pkg/client/auth"

"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

dropv1alpha1 "github.com/Breee/drop/api/v1alpha1"
"github.com/Breee/drop/internal/ui"
)

var scheme = runtime.NewScheme()

func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(dropv1alpha1.AddToScheme(scheme))
}

func main() {
var bindAddr string
var pollInterval time.Duration

opts := zap.Options{Development: true}
opts.BindFlags(flag.CommandLine)
flag.StringVar(&bindAddr, "bind-address", ":8888", "Address the UI server listens on.")
flag.DurationVar(&pollInterval, "poll-interval", 10*time.Second, "How often to poll Kubernetes for live SSE updates.")
flag.Parse()

ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
logger := ctrl.Log.WithName("drop-ui")

cfg, err := ctrl.GetConfig()
if err != nil {
logger.Error(err, "failed to get kubeconfig")
os.Exit(1)
}

c, err := client.New(cfg, client.Options{Scheme: scheme})
if err != nil {
logger.Error(err, "failed to create Kubernetes client")
os.Exit(1)
}

srv := ui.NewServer(c, pollInterval)
httpSrv := &http.Server{
Addr: bindAddr,
Handler: srv.Handler(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 0, // SSE streams need no write timeout
IdleTimeout: 120 * time.Second,
}

logger.Info("Drop Control Center UI starting", "address", bindAddr)
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error(err, "server exited with error")
os.Exit(1)
}
}
2 changes: 0 additions & 2 deletions docs/go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
module github.com/Breee/drop/docs

go 1.26.0

require github.com/imfing/hextra v0.12.3 // indirect
2 changes: 0 additions & 2 deletions docs/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
github.com/imfing/hextra v0.12.3 h1:DZHY2rUWYteyzjlHi9r4n7Bb5e2Q+6LXe4C1Dqn0ZjM=
github.com/imfing/hextra v0.12.3/go.mod h1:vi+yhpq8YPp/aghvJlNKVnJKcPJ/VyAEcfC1BSV9ARo=
Loading