diff --git a/cmd/k8s-operator/metrics_resources.go b/cmd/k8s-operator/metrics_resources.go index 0579e3466..94cd6a793 100644 --- a/cmd/k8s-operator/metrics_resources.go +++ b/cmd/k8s-operator/metrics_resources.go @@ -93,7 +93,7 @@ func reconcileMetricsResources(ctx context.Context, logger *zap.SugaredLogger, o Spec: corev1.ServiceSpec{ Selector: opts.proxyLabels, Type: corev1.ServiceTypeClusterIP, - Ports: []corev1.ServicePort{{Protocol: "TCP", Port: 9002, Name: "metrics"}}, + Ports: []corev1.ServicePort{{Protocol: "TCP", Port: 9002, Name: opts.metricsPortName}}, }, } var err error @@ -118,7 +118,7 @@ func reconcileMetricsResources(ctx context.Context, logger *zap.SugaredLogger, o } logger.Infof("ensuring ServiceMonitor for metrics Service %s/%s", metricsSvc.Namespace, metricsSvc.Name) - svcMonitor, err := newServiceMonitor(metricsSvc, pc.Spec.Metrics.ServiceMonitor) + svcMonitor, err := newServiceMonitor(metricsSvc, pc.Spec.Metrics.ServiceMonitor, opts.metricsPortName) if err != nil { return fmt.Errorf("error creating ServiceMonitor: %w", err) } @@ -174,7 +174,7 @@ func maybeCleanupServiceMonitor(ctx context.Context, cl client.Client, stsName, // newServiceMonitor takes a metrics Service created for a proxy and constructs and returns a ServiceMonitor for that // proxy that can be applied to the kube API server. // The ServiceMonitor is returned as Unstructured type - this allows us to avoid importing prometheus-operator API server client/schema. -func newServiceMonitor(metricsSvc *corev1.Service, spec *tsapi.ServiceMonitor) (*unstructured.Unstructured, error) { +func newServiceMonitor(metricsSvc *corev1.Service, spec *tsapi.ServiceMonitor, portName string) (*unstructured.Unstructured, error) { sm := serviceMonitorTemplate(metricsSvc.Name, metricsSvc.Namespace) sm.ObjectMeta.Labels = metricsSvc.Labels if spec != nil && len(spec.Labels) > 0 { @@ -185,7 +185,7 @@ func newServiceMonitor(metricsSvc *corev1.Service, spec *tsapi.ServiceMonitor) ( sm.Spec = ServiceMonitorSpec{ Selector: metav1.LabelSelector{MatchLabels: metricsSvc.Labels}, Endpoints: []ServiceMonitorEndpoint{{ - Port: "metrics", + Port: portName, }}, NamespaceSelector: ServiceMonitorNamespaceSelector{ MatchNames: []string{metricsSvc.Namespace}, @@ -274,10 +274,11 @@ func serviceMonitorTemplate(name, ns string) *ServiceMonitor { } type metricsOpts struct { - proxyStsName string // name of StatefulSet for proxy - tsNamespace string // namespace in which Tailscale is installed - proxyLabels map[string]string // labels of the proxy StatefulSet - proxyType string + proxyStsName string // name of StatefulSet for proxy + tsNamespace string // namespace in which Tailscale is installed + proxyLabels map[string]string // labels of the proxy StatefulSet + proxyType string + metricsPortName string // name for the metrics port (defaults to "metrics") } func isNamespacedProxyType(typ string) bool { @@ -294,3 +295,11 @@ func mergeMapKeys(a, b map[string]string) map[string]string { } return m } + +// metricsPortName returns the configured metrics port name from ProxyClass, or defaults to "metrics". +func metricsPortName(pc *tsapi.ProxyClass) string { + if pc != nil && pc.Spec.Metrics != nil && pc.Spec.Metrics.PortName != "" { + return pc.Spec.Metrics.PortName + } + return "metrics" +} diff --git a/cmd/k8s-operator/sts.go b/cmd/k8s-operator/sts.go index c52ffce85..21c67ff10 100644 --- a/cmd/k8s-operator/sts.go +++ b/cmd/k8s-operator/sts.go @@ -222,10 +222,11 @@ func (a *tailscaleSTSReconciler) Provision(ctx context.Context, logger *zap.Suga return nil, fmt.Errorf("failed to reconcile statefulset: %w", err) } mo := &metricsOpts{ - proxyStsName: hsvc.Name, - tsNamespace: hsvc.Namespace, - proxyLabels: hsvc.Labels, - proxyType: sts.proxyType, + proxyStsName: hsvc.Name, + tsNamespace: hsvc.Namespace, + proxyLabels: hsvc.Labels, + proxyType: sts.proxyType, + metricsPortName: metricsPortName(sts.ProxyClass), } if err = reconcileMetricsResources(ctx, logger, mo, sts.ProxyClass, a.Client); err != nil { return nil, fmt.Errorf("failed to ensure metrics resources: %w", err) diff --git a/k8s-operator/apis/v1alpha1/types_proxyclass.go b/k8s-operator/apis/v1alpha1/types_proxyclass.go index 4026f9084..2006a7938 100644 --- a/k8s-operator/apis/v1alpha1/types_proxyclass.go +++ b/k8s-operator/apis/v1alpha1/types_proxyclass.go @@ -329,6 +329,14 @@ type Metrics struct { // // Defaults to false. Enable bool `json:"enable"` + // PortName is the name to use for the metrics port exposed on the Service and + // Pod containers. This allows customization when the default "metrics" name + // conflicts with other ports or monitoring configurations. + // The port name must be a valid DNS label (RFC 1123) and cannot exceed 15 characters. + // +optional + // +kubebuilder:validation:MaxLength=15 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` + PortName string `json:"portName,omitempty"` // Enable to create a Prometheus ServiceMonitor for scraping the proxy's Tailscale metrics. // The ServiceMonitor will select the metrics Service that gets created when metrics are enabled. // The ingested metrics for each Service monitor will have labels to identify the proxy: