Skip to content
Merged
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: 2 additions & 2 deletions src/core/alertmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ func (s *alertmanagerService) DeleteSilence(silenceID string) error {
}

func alertmanagerUrl(config cfg.ConfigModule) (string, error) {
namespace, serviceName, port, err := kubernetes.FindAlertmanagerService()
namespace, serviceName, port, basePath, err := kubernetes.FindAlertmanagerService()
if err != nil {
return "", err
}
Expand All @@ -279,5 +279,5 @@ func alertmanagerUrl(config cfg.ConfigModule) (string, error) {
clusterDomain = "cluster.local"
}

return fmt.Sprintf("http://%s.%s.svc.%s:%d", serviceName, namespace, clusterDomain, port), nil
return fmt.Sprintf("http://%s.%s.svc.%s:%d%s", serviceName, namespace, clusterDomain, port, basePath), nil
}
4 changes: 2 additions & 2 deletions src/core/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ func prometheusUrlAndHeader(data PrometheusRequest, customEndpoint string, confi

result := data.Prometheus_API_URL
if data.Prometheus_API_URL == "" {
namespace, serviceName, port, err := kubernetes.FindPrometheusService()
namespace, serviceName, port, basePath, err := kubernetes.FindPrometheusService()
if err != nil {
logger.Warn("prometheus service auto-discovery failed; no prometheus URL was provided either", "error", err)
return "", header
Expand All @@ -270,7 +270,7 @@ func prometheusUrlAndHeader(data PrometheusRequest, customEndpoint string, confi
clusterDomain = "cluster.local"
}

result = fmt.Sprintf("http://%s.%s.svc.%s:%d", serviceName, namespace, clusterDomain, port)
result = fmt.Sprintf("http://%s.%s.svc.%s:%d%s", serviceName, namespace, clusterDomain, port, basePath)
logger.Debug("prometheus auto-discovered", "service", serviceName, "namespace", namespace, "port", port, "url", result)
}

Expand Down
94 changes: 65 additions & 29 deletions src/kubernetes/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
cfg "mogenius-operator/src/config"
"mogenius-operator/src/store"
"mogenius-operator/src/utils"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -33,57 +34,92 @@ func AllServices(namespaceName string) []v1.Service {
return result
}

// prometheusNames lists the well-known Prometheus service names in priority
// order. The first name that matches a service in the cluster wins, so the
// canonical kube-prometheus-stack names are checked before the generic ones.
var prometheusNames = []string{
"prometheus-kube-prometheus-prometheus",
"kube-prometheus-stack-prometheus",
"prometheus-server",
"prometheus-service",
"prometheus",
"prometheus-prometheus-server",
// serviceCandidate describes how to recognize a well-known monitoring service:
// by exact name, or by name prefix for operator-managed services whose names
// embed the Helm release name (e.g. VictoriaMetrics' vmsingle-<release>).
// basePath is appended to the discovered service URL when the Prometheus API
// is not served at the root of the service.
type serviceCandidate struct {
name string
prefix bool
basePath string
}

// alertmanagerNames lists the well-known Alertmanager service names in priority order.
var alertmanagerNames = []string{
"alertmanager-kube-prometheus-alertmanager",
"kube-prometheus-stack-alertmanager",
"alertmanager-service",
"alertmanager",
"prometheus-alertmanager",
// prometheusCandidates lists the well-known Prometheus-compatible services in
// priority order. The first candidate that matches a service in the cluster
// wins, so the canonical kube-prometheus-stack names are checked before the
// generic ones, and VictoriaMetrics single-node before cluster mode.
var prometheusCandidates = []serviceCandidate{
{name: "prometheus-kube-prometheus-prometheus"},
{name: "kube-prometheus-stack-prometheus"},
{name: "prometheus-server"},
{name: "prometheus-service"},
{name: "prometheus"},
{name: "prometheus-prometheus-server"},
// VictoriaMetrics (victoria-metrics-k8s-stack) single-node: serves the
// Prometheus query API at the service root.
{name: "vmsingle-", prefix: true},
// VictoriaMetrics cluster mode: vmselect serves the Prometheus query API
// under the tenant path (tenant 0 is the default single-tenant setup).
{name: "vmselect-", prefix: true, basePath: "/select/0/prometheus"},
}

func FindPrometheusService() (namespace string, service string, port int32, err error) {
return findServiceByPriority(prometheusNames, "prometheus")
// alertmanagerCandidates lists the well-known Alertmanager services in priority order.
var alertmanagerCandidates = []serviceCandidate{
{name: "alertmanager-kube-prometheus-alertmanager"},
{name: "kube-prometheus-stack-alertmanager"},
{name: "alertmanager-service"},
{name: "alertmanager"},
{name: "prometheus-alertmanager"},
// VictoriaMetrics (victoria-metrics-k8s-stack) managed Alertmanager.
{name: "vmalertmanager-", prefix: true},
}

func FindAlertmanagerService() (namespace string, service string, port int32, err error) {
return findServiceByPriority(alertmanagerNames, "alertmanager")
func FindPrometheusService() (namespace string, service string, port int32, basePath string, err error) {
return matchServiceByPriority(prometheusCandidates, "prometheus")
}

// findServiceByPriority returns the first cluster service whose name matches one
// of candidateNames, evaluated in the order given. The matched service's
func FindAlertmanagerService() (namespace string, service string, port int32, basePath string, err error) {
return matchServiceByPriority(alertmanagerCandidates, "alertmanager")
}

// matchServiceByPriority returns the first service whose name matches one of
// the candidates, evaluated in the order given. The matched service's
// HTTP/web port is preferred over an arbitrary first port (see selectHTTPPort).
func findServiceByPriority(candidateNames []string, kind string) (namespace string, service string, port int32, err error) {
func matchServiceByPriority(candidates []serviceCandidate, kind string) (namespace string, service string, port int32, basePath string, err error) {
byName := make(map[string]v1.Service)
for _, svc := range clusterServicesCached.Get() {
services := clusterServicesCached.Get()
names := make([]string, 0, len(services))
for _, svc := range services {
if len(svc.Spec.Ports) == 0 {
continue
}
// Keep the first occurrence so behaviour is deterministic when the same
// service name exists in multiple namespaces.
if _, seen := byName[svc.Name]; !seen {
byName[svc.Name] = svc
names = append(names, svc.Name)
}
}
// Prefix candidates can match several services; pick the lexicographically
// smallest name so discovery is deterministic across restarts.
sort.Strings(names)

for _, name := range candidateNames {
if svc, ok := byName[name]; ok {
return svc.Namespace, svc.Name, selectHTTPPort(svc.Spec.Ports), nil
for _, c := range candidates {
if !c.prefix {
if svc, ok := byName[c.name]; ok {
return svc.Namespace, svc.Name, selectHTTPPort(svc.Spec.Ports), c.basePath, nil
}
continue
}
for _, name := range names {
if strings.HasPrefix(name, c.name) {
svc := byName[name]
return svc.Namespace, svc.Name, selectHTTPPort(svc.Spec.Ports), c.basePath, nil
}
}
}
return "", "", -1, fmt.Errorf("%s service not found in any namespace", kind)
return "", "", -1, "", fmt.Errorf("%s service not found in any namespace", kind)
}

// selectHTTPPort picks the most likely HTTP API port from a service's ports:
Expand Down
121 changes: 121 additions & 0 deletions src/kubernetes/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,131 @@ package kubernetes

import (
"testing"
"time"

"mogenius-operator/src/utils"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func makeService(namespace, name string, ports ...v1.ServicePort) v1.Service {
return v1.Service{
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name},
Spec: v1.ServiceSpec{Ports: ports},
}
}

func TestMatchServiceByPriority(t *testing.T) {
tests := []struct {
name string
services []v1.Service
candidates []serviceCandidate
wantService string
wantPort int32
wantBasePath string
wantErr bool
}{
{
name: "kube-prometheus-stack exact name wins over VictoriaMetrics",
services: []v1.Service{
makeService("vm", "vmsingle-vm-stack", v1.ServicePort{Name: "http", Port: 8429}),
makeService("monitoring", "kube-prometheus-stack-prometheus", v1.ServicePort{Name: "http-web", Port: 9090}),
},
candidates: prometheusCandidates,
wantService: "kube-prometheus-stack-prometheus",
wantPort: 9090,
},
{
name: "victoria-metrics single-node matched by prefix",
services: []v1.Service{
makeService("vm", "vmagent-vm-stack", v1.ServicePort{Name: "http", Port: 8429}),
makeService("vm", "vmsingle-vm-stack", v1.ServicePort{Name: "http", Port: 8429}),
},
candidates: prometheusCandidates,
wantService: "vmsingle-vm-stack",
wantPort: 8429,
},
{
name: "victoria-metrics cluster mode gets the vmselect tenant base path",
services: []v1.Service{
makeService("vm", "vminsert-vm-stack", v1.ServicePort{Name: "http", Port: 8480}),
makeService("vm", "vmselect-vm-stack", v1.ServicePort{Name: "http", Port: 8481}),
},
candidates: prometheusCandidates,
wantService: "vmselect-vm-stack",
wantPort: 8481,
wantBasePath: "/select/0/prometheus",
},
{
name: "vmsingle preferred over vmselect when both exist",
services: []v1.Service{
makeService("vm", "vmselect-vm-stack", v1.ServicePort{Name: "http", Port: 8481}),
makeService("vm", "vmsingle-vm-stack", v1.ServicePort{Name: "http", Port: 8429}),
},
candidates: prometheusCandidates,
wantService: "vmsingle-vm-stack",
wantPort: 8429,
},
{
name: "victoria-metrics alertmanager matched by prefix",
services: []v1.Service{
makeService("vm", "vmalertmanager-vm-stack", v1.ServicePort{Name: "http", Port: 9093}),
},
candidates: alertmanagerCandidates,
wantService: "vmalertmanager-vm-stack",
wantPort: 9093,
},
{
name: "multiple prefix matches pick the lexicographically smallest name",
services: []v1.Service{
makeService("vm-b", "vmsingle-stack-b", v1.ServicePort{Name: "http", Port: 8429}),
makeService("vm-a", "vmsingle-stack-a", v1.ServicePort{Name: "http", Port: 8429}),
},
candidates: prometheusCandidates,
wantService: "vmsingle-stack-a",
wantPort: 8429,
},
{
name: "services without ports are ignored",
services: []v1.Service{
makeService("vm", "vmsingle-vm-stack"),
},
candidates: prometheusCandidates,
wantErr: true,
},
{
name: "no match returns an error",
services: []v1.Service{makeService("default", "kubernetes", v1.ServicePort{Name: "https", Port: 443})},
candidates: prometheusCandidates,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
orig := clusterServicesCached
clusterServicesCached = utils.NewTTLCache(time.Minute, func() []v1.Service { return tt.services })
t.Cleanup(func() { clusterServicesCached = orig })

_, service, port, basePath, err := matchServiceByPriority(tt.candidates, "prometheus")
if tt.wantErr {
if err == nil {
t.Fatalf("matchServiceByPriority() expected error, got service %q", service)
}
return
}
if err != nil {
t.Fatalf("matchServiceByPriority() unexpected error: %v", err)
}
if service != tt.wantService || port != tt.wantPort || basePath != tt.wantBasePath {
t.Errorf("matchServiceByPriority() = (%q, %d, %q), want (%q, %d, %q)",
service, port, basePath, tt.wantService, tt.wantPort, tt.wantBasePath)
}
})
}
}

func TestSelectHTTPPort(t *testing.T) {
tests := []struct {
name string
Expand Down