Kubernetes operator for automated Homer dashboard deployment and management
Automatically generate beautiful, dynamic dashboards from your Kubernetes Ingress, Gateway API, and Service resources.
- Kubernetes cluster (v1.21+; v1.23+ when enabling the optional HPA)
kubectlconfigured- Helm 3.x (recommended)
# Create namespace and install latest stable release
kubectl create namespace homer-operator
helm install homer-operator oci://ghcr.io/rajsinghtech/homer-operator/charts/homer-operator \
--namespace homer-operator
# Install with Gateway API support
kubectl create namespace homer-operator
helm install homer-operator oci://ghcr.io/rajsinghtech/homer-operator/charts/homer-operator \
--namespace homer-operator \
--set operator.enableGatewayAPI=trueThe default Kustomize installation does not include a Prometheus Operator
ServiceMonitor, so it can be applied to a plain Kubernetes cluster:
kubectl apply -k config/defaultThe tracked Kustomize overlay follows the development main image. For a
version-pinned installation, use the Helm chart or the version-specific
homer-operator-<version>-install.yaml asset attached to the corresponding
GitHub Release.
The operator metrics Service is included in this installation. To add the
optional Prometheus integration, first install the Prometheus Operator and
then apply the optional operator-plus-monitor overlay documented in
config/prometheus/README.md.
apiVersion: homer.rajsingh.info/v1alpha1
kind: Dashboard
metadata:
name: my-dashboard
namespace: default
spec:
replicas: 2
homerConfig:
title: "My Dashboard"
subtitle: "Welcome to my services"
theme: "default"
header: true
footer: '<p>Powered by Homer Operator</p>'
logo: "https://raw.githubusercontent.com/rajsinghtech/homer-operator/main/homer/Homer-Operator.png"
message:
url: "https://api.chucknorris.io/jokes/random"
mapping:
title: "id"
content: "value"
refreshInterval: 10000
services:
- name: "Applications"
icon: "fas fa-cloud"
items:
- name: "Homer"
url: "https://github.com/bastienwirtz/homer"
target: "_blank"
subtitle: "Upstream Homer"
updateIntervalMs: 30000
quick:
- name: "Homer docs"
icon: "fas fa-book"
url: "https://github.com/bastienwirtz/homer"
target: "_blank"
pages:
status:
subtitle: "A second Homer page"
services:
- name: "Status"
items:
- name: "Example"
url: "https://example.com"kubectl apply -f dashboard.yamlhomerConfig.services and pages use the same direct field names as
upstream Homer. The older parameters/annotation-oriented form remains
supported for existing dashboards and discovery workflows.
To use Homer's upstream external configuration behavior, set
homerConfig.externalConfig to a URL or path. Homer fetches that document and
ignores the remaining fields in the generated inline document; the operator
therefore skips discovery and inline Secret injection in this mode. Use
spec.configMap instead when the operator should manage a complete Homer YAML
document from a Kubernetes ConfigMap.
For example:
spec:
homerConfig:
externalConfig: https://config.example.com/homer.ymlFor legacy parameter data, prefer headers or headers.<Header-Name>; these
are normalized to Homer's item.headers object. The legacy customHeaders
alias remains supported: a bare parameters.customHeaders string such as
Authorization: Bearer token is parsed as a header map, as are
customHeaders.<Header-Name> annotations and object/dot/slash forms. For the
same key spelling, direct item.headers values take precedence over
parameters.headers, which takes precedence over parameters.customHeaders.
apiVersion: homer.rajsingh.info/v1alpha1
kind: Dashboard
metadata:
name: neon-dashboard
spec:
replicas: 1
assets:
pwa:
enabled: true
name: "My Dashboard PWA"
shortName: "Dashboard"
description: "Personal dashboard with PWA support"
themeColor: "#00d4aa"
backgroundColor: "#1b1b1b"
display: "standalone"
homerConfig:
title: "Neon Dashboard"
subtitle: "Cyberpunk vibes"
theme: "neon"
header: true
defaults:
layout: "columns"
colorTheme: "dark"apiVersion: v1
kind: Secret
metadata:
name: api-keys
type: Opaque
data:
emby-api-key: <base64-encoded-api-key>
---
apiVersion: homer.rajsingh.info/v1alpha1
kind: Dashboard
metadata:
name: media-dashboard
spec:
secrets:
apiKey:
name: api-keys
key: emby-api-key
homerConfig:
title: "Media Center"
services:
- parameters:
name: "Media Services"
items:
- parameters:
name: "Emby Server"
type: "Emby" # Smart card type
url: "https://emby.example.com"
libraryType: "series" # music, series, or moviesSmart-card Secrets must be in the Dashboard's namespace. API key, token,
username, and password references are resolved for smart-card service items
(items with a type) in homerConfig.services. Header references under
spec.secrets.headers are resolved for configured items and applied after
discovery to Ingress, HTTPRoute, and Service items, including generic items
without a type. A Secret-backed header is explicit Dashboard configuration:
for the same configured key, it overrides a direct item header and a
discovery header. Header-name matching is case-insensitive and duplicate
casing variants are normalized to one upstream header. Resolved values are
emitted in the item's upstream headers object. Cross-namespace references
are rejected;
remote-cluster kubeconfig references under spec.remoteClusters[].secretRef
are separate and may still use an explicit namespace.
apiVersion: v1
kind: ConfigMap
metadata:
name: custom-assets
binaryData:
logo.png: <base64-encoded-image>
custom.css: <base64-encoded-css>
---
apiVersion: homer.rajsingh.info/v1alpha1
kind: Dashboard
metadata:
name: custom-dashboard
spec:
assets:
configMapRef:
name: custom-assets
icons:
favicon: "logo.png"
appleTouchIcon: "logo.png"
homerConfig:
title: "Custom Dashboard"
stylesheet:
- "assets/custom.css"Use spec.configMap when the complete Homer configuration should live in a
ConfigMap rather than inline in the Dashboard resource. The ConfigMap must be
in the Dashboard's namespace; key defaults to config.yml. Changes to the
referenced ConfigMap are watched and trigger reconciliation.
apiVersion: homer.rajsingh.info/v1alpha1
kind: Dashboard
metadata:
name: external-config-dashboard
namespace: default
spec:
configMap:
name: homer-config
key: config.ymlCustom asset ConfigMaps can be shared from another namespace by setting
assets.configMapRef.namespace. The operator watches the source and mirrors
it into the Dashboard's namespace so the Dashboard pod can mount it. Omit the
namespace for a same-namespace reference.
spec:
assets:
configMapRef:
name: shared-homer-assets
namespace: platform-assetsAsset files are staged below Homer’s /www/assets directory. Use flat
filenames in the referenced ConfigMap: the operator mounts ConfigMap keys at
the asset root and does not create items[].path projections, so nested names
such as assets/tools/sample.png are not a portable ConfigMap representation.
Configured icon sources can still be copied to Homer’s canonical icons/
paths (favicon.ico, apple-touch-icon.png, and the PWA icon paths). When PWA
support is enabled, the operator also writes manifest.json; icon paths in
that manifest match the staged paths. Changing or replacing a cross-namespace
source updates its owned mirror, and stale mirrors are removed when the
Dashboard reference changes.
Enable HTTPRoute processing for modern Kubernetes networking:
# Install Gateway API CRDs
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/standard-install.yaml
# Install operator with Gateway API support
kubectl create namespace homer-operator
helm install homer-operator oci://ghcr.io/rajsinghtech/homer-operator/charts/homer-operator \
--namespace homer-operator \
--set operator.enableGatewayAPI=trueHTTPRoute parentRefs without a namespace refer to a Gateway in the
HTTPRoute's namespace. Gateway listeners default to accepting HTTPRoutes from
their own namespace (allowedRoutes.namespaces.from: Same). For a
cross-namespace attachment, set parentRefs.namespace and configure the
Gateway listener's allowedRoutes.namespaces with All or an appropriate
Selector; None disallows attachment. If allowedRoutes.kinds is present,
it must allow HTTPRoute from the Gateway API group (an omitted group uses
the Gateway API default). Only Gateway API Gateway parent references are
considered; omitted parent-reference group and kind use those same defaults.
sectionName and port restrict the matching listener. Listener hostnames
must overlap the route hostname, and a wildcard such as *.example.com
matches any non-apex subdomain, including deeper names, but not the apex. The
checked-in example at
homer/httpRouteExample.yaml keeps the HTTPRoute and Gateway in default and
relies on the same-namespace default.
When resolving an HTTPRoute URL scheme, an explicitly rejected GatewayClass
(Accepted=False), Gateway (Accepted=False, Programmed=False, or
Ready=False), or listener (Accepted=False, ResolvedRefs=False,
Programmed=False, or Conflicted=True) is not used. A listener status that
does not support HTTPRoute is also ignored. If a matching HTTPRoute parent
status is reported for the GatewayClass controller, Accepted=True is
required; statuses from other controllers are ignored. If a resource reports
no status at all, the resolver keeps the compatibility fallback; when a
Gateway reports listener statuses, the selected listener must have an eligible
status. Missing matching-parent status does not by itself exclude the route.
These checks affect protocol resolution rather than selector-based discovery.
The operator must be able to read gatewayclasses in addition to gateways
and httproutes to verify GatewayClass ownership; without that permission it
does not trust a listener to select an HTTPS URL.
Among multiple eligible HTTP/HTTPS listeners,
HTTPS is preferred; if no protocol can be resolved, the generated URL uses
HTTP.
Control exactly which resources are included in your dashboard with comprehensive filtering options:
apiVersion: homer.rajsingh.info/v1alpha1
kind: Dashboard
metadata:
name: production-dashboard
spec:
# Filter HTTPRoutes by Gateway labels
gatewaySelector:
matchLabels:
environment: "production"
gateway: "public"
matchExpressions:
- key: "app.kubernetes.io/name"
operator: In
values: ["istio-gateway", "envoy-gateway", "nginx-gateway"]
# Filter HTTPRoutes by their own labels
httpRouteSelector:
matchLabels:
team: "platform"
tier: "frontend"
matchExpressions:
- key: "app.kubernetes.io/component"
operator: In
values: ["api", "service", "web"]
# Filter Ingresses by labels
ingressSelector:
matchLabels:
environment: "production"
public: "true"
matchExpressions:
- key: "kubernetes.io/ingress.class"
operator: In
values: ["nginx", "traefik"]
# Filter by hostname/domain (works for both HTTPRoutes and Ingresses)
domainFilters:
- "mycompany.com" # Exact match: mycompany.com
- "internal.local" # Subdomain match: *.internal.local
- "rajsingh.info" # Both exact and subdomain matching
homerConfig:
title: "Production Services"
subtitle: "Filtered production endpoints"
# ... rest of configFiltering Capabilities:
| Filter Type | Description | Applies To | Default Behavior |
|---|---|---|---|
gatewaySelector |
Filter HTTPRoutes by parent Gateway labels | HTTPRoutes only | Include all HTTPRoutes |
httpRouteSelector |
Filter HTTPRoutes by their own labels | HTTPRoutes only | Include all HTTPRoutes |
ingressSelector |
Filter Ingresses by their labels | Ingresses only | Include all Ingresses |
domainFilters |
Filter by hostname/domain names | Both HTTPRoutes & Ingresses | Include all domains |
serviceSelector |
Discover Kubernetes Services by labels | Services only | No Services discovered |
Domain Filtering Examples:
example.com- Matches exactlyexample.cominternal.local- Matchesapi.internal.local,web.internal.local, etc.- Multiple filters are OR'd together (any match includes the resource)
Real-world Use Cases:
- Environment Separation: Production vs staging dashboards
- Team Dashboards: Platform team vs application team services
- Security Zones: Public vs internal service separation
- Domain Organization: Company domains vs personal projects
- Gateway Migration: Gradual migration between gateway implementations
Discover internal Kubernetes Services and add them to your dashboard — useful for cluster-internal services that don't have Ingress or HTTPRoute resources:
apiVersion: homer.rajsingh.info/v1alpha1
kind: Dashboard
metadata:
name: internal-dashboard
spec:
# Discover Services matching these labels
serviceSelector:
matchLabels:
homer.rajsingh.info/enabled: "true"
homerConfig:
title: "Internal Services"Annotate your Services to customize their dashboard appearance:
apiVersion: v1
kind: Service
metadata:
name: my-api
namespace: backend
labels:
homer.rajsingh.info/enabled: "true"
annotations:
item.homer.rajsingh.info/name: "Backend API"
item.homer.rajsingh.info/subtitle: "Core API service"
item.homer.rajsingh.info/logo: "https://example.com/api-logo.png"
item.homer.rajsingh.info/type: "Ping"
service.homer.rajsingh.info/name: "Backend"
service.homer.rajsingh.info/icon: "fas fa-cogs"
spec:
ports:
- port: 8080Service URLs are automatically generated as http://<name>.<namespace>.svc.cluster.local:<port> for local-cluster Services. Unlike Ingress/HTTPRoute discovery, Services are opt-in — they are only discovered when serviceSelector is specified. For remote-cluster Services, the operator keeps the item visible but omits that local-cluster DNS URL unless item.homer.rajsingh.info/url supplies an explicit reachable URL; this prevents a link that points at the wrong cluster.
Control how your services appear on dashboards using annotations on both Ingress and HTTPRoute resources:
# Traditional Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
annotations:
# Item configuration
item.homer.rajsingh.info/name: "My Application"
item.homer.rajsingh.info/subtitle: "Production instance"
item.homer.rajsingh.info/logo: "https://example.com/logo.png"
item.homer.rajsingh.info/tag: "production"
item.homer.rajsingh.info/keywords: "app, api, service"
# Service group configuration
service.homer.rajsingh.info/name: "Production Services"
service.homer.rajsingh.info/icon: "fas fa-server"
spec:
# ... ingress configuration
---
# Gateway API HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app-route
annotations:
# Same annotations work for HTTPRoute!
item.homer.rajsingh.info/name: "My Application (Gateway API)"
item.homer.rajsingh.info/subtitle: "Modern routing"
item.homer.rajsingh.info/logo: "https://example.com/logo.png"
item.homer.rajsingh.info/tag: "gateway-api"
service.homer.rajsingh.info/name: "Gateway Services"
service.homer.rajsingh.info/icon: "fas fa-route"
spec:
# ... httproute configurationThe Homer Operator features an intelligent, convention-based annotation system that supports any Homer configuration parameter automatically, without requiring code changes:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: smart-app
annotations:
# Basic parameters (auto-detected as strings)
item.homer.rajsingh.info/name: "Smart Application"
item.homer.rajsingh.info/subtitle: "AI-powered service"
# Boolean parameters (case-insensitive, multiple formats)
item.homer.rajsingh.info/useCredentials: "TRUE" # or "true", "yes", "1"
item.homer.rajsingh.info/legacyapi: "false" # or "FALSE", "no", "0"
# Integer parameters (auto-detected by naming patterns)
item.homer.rajsingh.info/timeout: "30" # *_value, *Interval patterns
item.homer.rajsingh.info/updateInterval: "5000" # Auto-detected as integer
item.homer.rajsingh.info/warning_value: "80" # Threshold parameters
item.homer.rajsingh.info/danger_value: "95" # Auto-validated
# Array parameters (comma-separated, auto-cleaned)
item.homer.rajsingh.info/keywords: " api , service , smart " # Spaces trimmed
# Headers are emitted as Homer's item.headers object.
item.homer.rajsingh.info/headers: "Authorization: Bearer token, Content-Type: application/json"
# A single header can also use dot notation.
item.homer.rajsingh.info/headers.Authorization: "Bearer token"Supports complex nested configurations using dot notation, which is valid in Kubernetes annotation keys:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: advanced-app
annotations:
# Legacy customHeaders dot notation is also converted to item.headers.
item.homer.rajsingh.info/customHeaders.Authorization: "Bearer secret-token"
item.homer.rajsingh.info/customHeaders.X-API-Key: "api-key-123"
item.homer.rajsingh.info/customHeaders.Content-Type: "application/json"
# Nested object: mapping (for smart cards)
item.homer.rajsingh.info/mapping.status: "health.status"
item.homer.rajsingh.info/mapping.version: "info.version"
# Any Homer parameter works automatically!
item.homer.rajsingh.info/checkInterval: "30000" # Smart card refresh
item.homer.rajsingh.info/location: "US-East" # Custom parameters
item.homer.rajsingh.info/environment: "production" # User-defined fieldsControl item visibility using the hide annotation:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: maintenance-app
annotations:
item.homer.rajsingh.info/name: "Maintenance Service"
item.homer.rajsingh.info/subtitle: "Currently under maintenance"
# Hide this item from the dashboard
# Supports flexible boolean values (case-insensitive)
item.homer.rajsingh.info/hide: "true" # true, yes, 1, on
# item.homer.rajsingh.info/hide: "false" # false, no, 0, off
# item.homer.rajsingh.info/hide: "maintenance-mode" # Any non-empty string = true
spec:
rules:
- host: maintenance.example.com
# ... rest of spec
---
# HTTPRoute example
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: beta-api
annotations:
item.homer.rajsingh.info/name: "Beta API"
item.homer.rajsingh.info/hide: "yes" # Hide during beta testing
spec:
hostnames:
- beta-api.example.com
# ... rest of specHide Annotation Values:
- Hidden:
true,yes,1,on, or any non-empty string - Visible:
false,no,0,off, or empty string (default) - Case-insensitive and works with both Ingress and HTTPRoute resources
The system automatically detects parameter types using intelligent patterns:
- Booleans: Parameters ending in
_enabled,_flagor namedusecredentials,legacyapi,hide - Integers: Parameters ending in
Interval,_value,Valueor namedtimeout,limit - Objects:
headersandmappinguse object notation; the legacycustomHeadersalias is normalized toheaders, includingcustomHeaders.<Header-Name>annotations and bareparameters.customHeadersvalues. New parameter data should useheadersorheaders.<Header-Name> - Arrays: Comma-separated values (automatically cleaned and trimmed)
- Validation: Built-in validation for
url,target, numeric values
| Annotation | Type | Description | Example Values |
|---|---|---|---|
item.homer.rajsingh.info/name |
String | Display name for the item | "My Application" |
item.homer.rajsingh.info/subtitle |
String | Subtitle/description | "Production API" |
item.homer.rajsingh.info/logo |
String | URL to logo/icon | "https://example.com/logo.png" |
item.homer.rajsingh.info/tag |
String | Tag label | "production", "api" |
item.homer.rajsingh.info/tagstyle |
String | Tag color style | "is-primary", "is-info" |
item.homer.rajsingh.info/keywords |
String | Comma-separated search keywords | "api, service, web" |
item.homer.rajsingh.info/hide |
Boolean | Hide item from dashboard | "true", "false", "yes", "no" |
item.homer.rajsingh.info/target |
String | Link target | "_blank", "_self" |
item.homer.rajsingh.info/type |
String | Smart card type | "Ping", "Emby", "AdGuardHome" |
service.homer.rajsingh.info/name |
String | Service group name | "Production Services" |
service.homer.rajsingh.info/icon |
String | Service group icon | "fas fa-server" |
# values.yaml
replicaCount: 3
operator:
enableGatewayAPI: true
metrics:
enabled: true
secureMetrics: true
resources:
limits:
memory: 512Mi
cpu: 1000m
requests:
memory: 256Mi
cpu: 100m
highAvailability:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 80
podDisruptionBudget:
enabled: true
minAvailable: 1
serviceMonitor:
enabled: true
interval: 30skubectl create namespace homer-operator
helm install homer-operator oci://ghcr.io/rajsinghtech/homer-operator/charts/homer-operator \
--namespace homer-operator -f values.yamlHelm selects the latest published chart when --version is omitted. Add
--version <version-from-GitHub-Releases> when pinning a release.
The operator exposes comprehensive metrics:
# Enable metrics and ServiceMonitor
kubectl create namespace homer-operator
helm install homer-operator oci://ghcr.io/rajsinghtech/homer-operator/charts/homer-operator \
--namespace homer-operator \
--set operator.metrics.enabled=true \
--set serviceMonitor.enabled=trueFor the static Kustomize installation, config/default includes the
authenticated metrics Service but intentionally omits ServiceMonitor.
kubectl apply -k config/prometheus adds the monitor only after the
Prometheus Operator CRD is installed. The overlay cannot infer the Prometheus
service account, so see its README for the intentional manual binding to
homer-operator-metrics-reader.
Available Metrics:
controller_runtime_reconcile_total- Reconciliation countercontroller_runtime_reconcile_time_seconds- Reconciliation duration- Standard controller-runtime metrics for monitoring operator health
The metrics Service is created only when both operator.metrics.enabled and
services.metrics.enabled are true. The ServiceMonitor additionally requires
serviceMonitor.enabled=true.
Secure metrics use controller-runtime TokenReview/SubjectAccessReview
authorization. When serviceMonitor.enabled=true and secure metrics remain
enabled, the Helm chart creates a dedicated scraper ServiceAccount and token
Secret, grants it /metrics access, and references that Secret from the
ServiceMonitor. Disabling secure metrics switches the ServiceMonitor to HTTP
and removes the auth resources.
- Liveness:
GET /healthz - Readiness:
GET /readyz - Metrics:
GET https://<operator-service>:8443/metrics(authenticated)
The operator maintains 95%+ compatibility with existing Homer configurations:
# Your existing config.yml works directly in homerConfig
apiVersion: homer.rajsingh.info/v1alpha1
kind: Dashboard
metadata:
name: migrated-dashboard
spec:
homerConfig:
# Paste your existing Homer config here
title: "My Existing Dashboard"
subtitle: "Migrated from static config"
# ... rest of your configThe operator supports both simultaneously for zero-downtime migration:
-
Phase 1: Enable Gateway API
helm upgrade homer-operator charts/homer-operator --set operator.enableGatewayAPI=true
-
Phase 2: Install Gateway API CRDs and create Gateway resources
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/standard-install.yaml
-
Phase 3: Migrate services to HTTPRoute resources
- Convert Ingress rules to HTTPRoute specs
- Use same label selectors and domain filters
- Test HTTPRoute discovery with existing dashboards
-
Phase 4: Update Dashboard selectors and deprecate Ingress
spec: ingressSelector: null # Disable Ingress discovery httpRouteSelector: # Enable HTTPRoute discovery matchLabels: homer.rajsingh.info/enabled: "true"
Discover and aggregate services from multiple Kubernetes clusters into a single unified dashboard.
- Multiple Cluster Connections: Connect to any number of remote clusters using kubeconfig secrets
- Automatic Secret Rotation: Detects kubeconfig changes and automatically reconnects without pod restarts
- Per-Cluster Filtering: Apply namespace, label, domain, and service filters independently per cluster
- Cluster Metadata: Automatically enriches discovered services with cluster information
- Status Tracking: Monitor connection status and resource counts per cluster
- Secure Authentication: Token-based authentication with RBAC support
- Create a read-only service account in the remote cluster:
# On remote cluster
kubectl create namespace kube-system
kubectl create serviceaccount homer-reader -n kube-system
# Create ClusterRole with read permissions
kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: homer-reader
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["httproutes", "gateways", "gatewayclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["namespaces", "services"]
verbs: ["get", "list", "watch"]
EOF
# Bind the role
kubectl create clusterrolebinding homer-reader \
--clusterrole=homer-reader \
--serviceaccount=kube-system:homer-reader
# Create long-lived token
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: homer-reader-token
namespace: kube-system
annotations:
kubernetes.io/service-account.name: homer-reader
type: kubernetes.io/service-account-token
EOF- Generate kubeconfig for the remote cluster:
# Get token and CA cert
TOKEN=$(kubectl get secret homer-reader-token -n kube-system -o jsonpath='{.data.token}' | base64 -d)
CA_CERT=$(kubectl get secret homer-reader-token -n kube-system -o jsonpath='{.data.ca\.crt}')
SERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
# Create kubeconfig
cat > remote-kubeconfig.yaml <<EOF
apiVersion: v1
kind: Config
clusters:
- name: production
cluster:
certificate-authority-data: ${CA_CERT}
server: ${SERVER}
contexts:
- name: production
context:
cluster: production
user: homer-reader
current-context: production
users:
- name: homer-reader
user:
token: ${TOKEN}
EOF- Create secret in the main cluster:
# On main cluster where operator is running
kubectl create secret generic remote-cluster-kubeconfig \
--from-file=kubeconfig=remote-kubeconfig.yaml \
-n default- Create multi-cluster dashboard:
apiVersion: homer.rajsingh.info/v1alpha1
kind: Dashboard
metadata:
name: multi-cluster-dashboard
namespace: default
spec:
replicas: 1
# Configure remote clusters
remoteClusters:
- name: production
enabled: true
secretRef:
name: remote-cluster-kubeconfig
namespace: default
key: kubeconfig
# Optional: Filter resources in remote cluster
namespaceFilter:
- default
- production
# Optional: Add cluster labels to discovered resources
clusterLabels:
cluster: production
region: us-east-1
# Optional: Apply label selectors to this remote cluster
ingressSelector:
matchLabels:
environment: production
domainFilters:
- "mycompany.com"
homerConfig:
title: "Multi-Cluster Dashboard"
subtitle: "Services from multiple clusters"apiVersion: homer.rajsingh.info/v1alpha1
kind: Dashboard
metadata:
name: advanced-multicluster
spec:
remoteClusters:
- name: cluster-name
enabled: true # Set to false to temporarily disable
# Kubeconfig secret reference (required)
secretRef:
name: kubeconfig-secret-name
namespace: secret-namespace
key: kubeconfig # Key in secret containing kubeconfig
# Namespace filtering (optional)
namespaceFilter:
- namespace-1
- namespace-2
# Cluster labels (optional) - added to all discovered resources
clusterLabels:
cluster: production
region: us-west-2
team: platform
cluster-tagstyle: "is-danger" # Optional: Customize badge color (red/yellow/blue/green/gray)
# Resource filtering (optional) - same as main cluster
ingressSelector:
matchLabels:
app: web
httpRouteSelector:
matchLabels:
gateway: public
gatewaySelector:
matchLabels:
type: ingress
# Service discovery (optional) - discover K8s Services by labels
serviceSelector:
matchLabels:
homer.rajsingh.info/enabled: "true"
# Per-cluster domain filtering (optional); remote clusters do not inherit
# the dashboard-level domainFilters.
domainFilters:
- "prod.example.com" # Only production domains from this cluster
- "api.example.com" # API endpointsCluster Name Suffix:
Append custom suffixes to service display names to distinguish items from different clusters. Configure using the cluster-name-suffix label in clusterLabels:
remoteClusters:
- name: ottawa
clusterLabels:
cluster-name-suffix: " (ottawa)" # -> "Service Name (ottawa)"
# Other formats:
# " - ottawa" -> "Service Name - ottawa"
# " [ottawa]" -> "Service Name [ottawa]"This suffix is only applied to items from remote clusters, not the local cluster. If the label is not set, no suffix is appended.
Automatic Cluster Tagging (Optional):
Services from remote clusters can also get badge tags with the cluster name when cluster-tagstyle is set in clusterLabels. Available colors:
is-danger(red) - productionis-warning(yellow) - stagingis-info(blue) - developmentis-success(green) - QA/testingis-light(gray) - deprecated
Important: Tags are only added when cluster-tagstyle is explicitly configured. You can use both suffix and tags, or just one approach for cluster identification.
Multi-cluster selector and domain semantics:
- Dashboard-level
ingressSelector,httpRouteSelector, andgatewaySelectorapply only to the local cluster. Remote clusters use their corresponding selector, when configured; omission includes all resources of that type. - A remote
serviceSelectoroverrides the dashboard-levelserviceSelector. If omitted, the dashboard-level service selector is inherited. Services remain opt-in: without either selector, no Services are discovered. - Dashboard-level
domainFiltersapply only to the local cluster. Remote clusters use their owndomainFilters; omission includes all remote Ingress and HTTPRoute hostnames. namespaceFilterlimits discovery in that remote cluster. An empty or omitted filter means all namespaces allowed by the remote RBAC rules.
Check multi-cluster connection status:
kubectl get dashboard multi-cluster-dashboard -o jsonpath='{.status.clusterStatuses}' | jqExample output:
[
{
"name": "production",
"connected": true,
"lastConnectionTime": "2025-10-02T10:39:15Z",
"discoveredIngresses": 5,
"discoveredHTTPRoutes": 3
},
{
"name": "staging",
"connected": false,
"lastError": "failed to connect: unauthorized",
"lastConnectionTime": "2025-10-02T10:38:00Z"
}
]The operator automatically detects kubeconfig changes and reconnects:
# Update the secret with new credentials
kubectl create secret generic remote-cluster-kubeconfig \
--from-file=kubeconfig=new-kubeconfig.yaml \
-n default \
--dry-run=client -o yaml | kubectl apply -f -
# Operator automatically detects the change and reconnects
# Check logs to verify:
kubectl logs -n <operator-namespace> deployment/<operator-deployment>Resources discovered through the multi-cluster path carry the source cluster
in the homer.rajsingh.info/cluster annotation. Remote resources also receive
the user-defined clusterLabels as Kubernetes labels; the operator does not
create a separate homer.rajsingh.info/source-cluster annotation. The source
annotation is used internally for deterministic item cleanup and for optional
cluster suffixes/tags in the generated Homer configuration.
- Use read-only service accounts with minimal RBAC permissions
- Store kubeconfigs in Kubernetes secrets
- Use network policies to restrict operator egress
- Rotate tokens regularly using automatic secret rotation
- Consider using certificate-based authentication for production
Connection failures:
# Check Dashboard status
kubectl get dashboard <name> -o yaml
# Check operator logs
kubectl logs -n <operator-namespace> deployment/<operator-deployment>
# Verify secret exists and is readable
kubectl get secret <secret-name> -n <namespace> -o yaml
# Test kubeconfig manually
kubectl --kubeconfig=<path> get namespacesCommon issues:
- Connection refused: Ensure cluster API server is accessible from operator pod
- Unauthorized: Verify service account has correct RBAC permissions
- Certificate errors: Check certificate-authority-data in kubeconfig
- No resources discovered: Verify namespace filters and label selectors
# Clone repository
git clone https://github.com/rajsinghtech/homer-operator.git
cd homer-operator
# Install dependencies
make install
# Run locally against cluster
make run
# Build and deploy
make docker-build IMG=your-registry/homer-operator:dev
make deploy IMG=your-registry/homer-operator:devPushes to main publish the development :main, :latest, and commit-SHA
images through the dedicated development-image workflow. Versioned releases
are published only from vMAJOR.MINOR.PATCH tags after the release workflow
passes code generation, tests, Helm validation, isolated Kind E2E, and
publication checks. Release images, the Helm chart, and the installer are
published with digest and signature/provenance verification; the release
notes include the complete change summary and the published SBOM.
Version tags matching v* are protected against deletion and non-fast-forward
updates. Keep the tag target fixed after a release workflow starts. Release
notes are the project's changelog and are published in GitHub
Releases; this
repository intentionally does not maintain a CHANGELOG.md.
# Run unit tests
make test
# Run the black-box suite against a dedicated, isolated cluster
E2E_KUBECONFIG=$PWD/.kube/homer-operator-e2e \
make test-e2e
# For the default Kustomize installation, override the operator location
E2E_KUBECONFIG=$PWD/.kube/homer-operator-e2e \
E2E_OPERATOR_NAMESPACE=homer-operator-system \
E2E_OPERATOR_DEPLOYMENT=homer-operator-controller-manager \
make test-e2e
# CI installs the Helm chart, applies config/samples/
# homer_v1alpha1_dashboard.yaml, verifies its generated page asset, and runs
# the same black-box suite.
# Generate manifests
make manifestsThis project is licensed under the Apache License 2.0.
Built with love using Kubebuilder and Homer