mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-11-03 16:31:20 +00:00 
			
		
		
		
	cmd/k8s-operator: configure HA Ingress replicas to share certs Creates TLS certs Secret and RBAC that allows HA Ingress replicas to read/write to the Secret. Configures HA Ingress replicas to run in read-only mode. Updates tailscale/corp#24795 Signed-off-by: Irbe Krumina <irbe@tailscale.com>
		
			
				
	
	
		
			297 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			297 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright (c) Tailscale Inc & AUTHORS
 | 
						|
// SPDX-License-Identifier: BSD-3-Clause
 | 
						|
 | 
						|
//go:build !plan9
 | 
						|
 | 
						|
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"fmt"
 | 
						|
	"reflect"
 | 
						|
 | 
						|
	"go.uber.org/zap"
 | 
						|
	corev1 "k8s.io/api/core/v1"
 | 
						|
	apierrors "k8s.io/apimachinery/pkg/api/errors"
 | 
						|
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
						|
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
						|
	"k8s.io/apimachinery/pkg/runtime"
 | 
						|
	"k8s.io/apimachinery/pkg/types"
 | 
						|
	"sigs.k8s.io/controller-runtime/pkg/client"
 | 
						|
	tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
 | 
						|
	"tailscale.com/kube/kubetypes"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	labelMetricsTarget = "tailscale.com/metrics-target"
 | 
						|
 | 
						|
	// These labels get transferred from the metrics Service to the ingested Prometheus metrics.
 | 
						|
	labelPromProxyType            = "ts_proxy_type"
 | 
						|
	labelPromProxyParentName      = "ts_proxy_parent_name"
 | 
						|
	labelPromProxyParentNamespace = "ts_proxy_parent_namespace"
 | 
						|
	labelPromJob                  = "ts_prom_job"
 | 
						|
 | 
						|
	serviceMonitorCRD = "servicemonitors.monitoring.coreos.com"
 | 
						|
)
 | 
						|
 | 
						|
// ServiceMonitor contains a subset of fields of servicemonitors.monitoring.coreos.com Custom Resource Definition.
 | 
						|
// Duplicating it here allows us to avoid importing prometheus-operator library.
 | 
						|
// https://github.com/prometheus-operator/prometheus-operator/blob/bb4514e0d5d69f20270e29cfd4ad39b87865ccdf/pkg/apis/monitoring/v1/servicemonitor_types.go#L40
 | 
						|
type ServiceMonitor struct {
 | 
						|
	metav1.TypeMeta   `json:",inline"`
 | 
						|
	metav1.ObjectMeta `json:"metadata"`
 | 
						|
	Spec              ServiceMonitorSpec `json:"spec"`
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/prometheus-operator/prometheus-operator/blob/bb4514e0d5d69f20270e29cfd4ad39b87865ccdf/pkg/apis/monitoring/v1/servicemonitor_types.go#L55
 | 
						|
type ServiceMonitorSpec struct {
 | 
						|
	// Endpoints defines the endpoints to be scraped on the selected Service(s).
 | 
						|
	// https://github.com/prometheus-operator/prometheus-operator/blob/bb4514e0d5d69f20270e29cfd4ad39b87865ccdf/pkg/apis/monitoring/v1/servicemonitor_types.go#L82
 | 
						|
	Endpoints []ServiceMonitorEndpoint `json:"endpoints"`
 | 
						|
	// JobLabel is the label on the Service whose value will become the value of the Prometheus job label for the metrics ingested via this ServiceMonitor.
 | 
						|
	// https://github.com/prometheus-operator/prometheus-operator/blob/bb4514e0d5d69f20270e29cfd4ad39b87865ccdf/pkg/apis/monitoring/v1/servicemonitor_types.go#L66
 | 
						|
	JobLabel string `json:"jobLabel"`
 | 
						|
	// NamespaceSelector selects the namespace of Service(s) that this ServiceMonitor allows to scrape.
 | 
						|
	// https://github.com/prometheus-operator/prometheus-operator/blob/bb4514e0d5d69f20270e29cfd4ad39b87865ccdf/pkg/apis/monitoring/v1/servicemonitor_types.go#L88
 | 
						|
	NamespaceSelector ServiceMonitorNamespaceSelector `json:"namespaceSelector,omitempty"`
 | 
						|
	// Selector is the label selector for Service(s) that this ServiceMonitor allows to scrape.
 | 
						|
	// https://github.com/prometheus-operator/prometheus-operator/blob/bb4514e0d5d69f20270e29cfd4ad39b87865ccdf/pkg/apis/monitoring/v1/servicemonitor_types.go#L85
 | 
						|
	Selector metav1.LabelSelector `json:"selector"`
 | 
						|
	// TargetLabels are labels on the selected Service that should be applied as Prometheus labels to the ingested metrics.
 | 
						|
	// https://github.com/prometheus-operator/prometheus-operator/blob/bb4514e0d5d69f20270e29cfd4ad39b87865ccdf/pkg/apis/monitoring/v1/servicemonitor_types.go#L72
 | 
						|
	TargetLabels []string `json:"targetLabels"`
 | 
						|
}
 | 
						|
 | 
						|
// ServiceMonitorNamespaceSelector selects namespaces in which Prometheus operator will attempt to find Services for
 | 
						|
// this ServiceMonitor.
 | 
						|
// https://github.com/prometheus-operator/prometheus-operator/blob/bb4514e0d5d69f20270e29cfd4ad39b87865ccdf/pkg/apis/monitoring/v1/servicemonitor_types.go#L88
 | 
						|
type ServiceMonitorNamespaceSelector struct {
 | 
						|
	MatchNames []string `json:"matchNames,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
// ServiceMonitorEndpoint defines an endpoint of Service to scrape. We only define port here. Prometheus by default
 | 
						|
// scrapes /metrics path, which is what we want.
 | 
						|
type ServiceMonitorEndpoint struct {
 | 
						|
	// Port is the name of the Service port that Prometheus will scrape.
 | 
						|
	Port string `json:"port,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
func reconcileMetricsResources(ctx context.Context, logger *zap.SugaredLogger, opts *metricsOpts, pc *tsapi.ProxyClass, cl client.Client) error {
 | 
						|
	if opts.proxyType == proxyTypeEgress {
 | 
						|
		// Metrics are currently not being enabled for standalone egress proxies.
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	if pc == nil || pc.Spec.Metrics == nil || !pc.Spec.Metrics.Enable {
 | 
						|
		return maybeCleanupMetricsResources(ctx, opts, cl)
 | 
						|
	}
 | 
						|
	metricsSvc := &corev1.Service{
 | 
						|
		ObjectMeta: metav1.ObjectMeta{
 | 
						|
			Name:      metricsResourceName(opts.proxyStsName),
 | 
						|
			Namespace: opts.tsNamespace,
 | 
						|
			Labels:    metricsResourceLabels(opts),
 | 
						|
		},
 | 
						|
		Spec: corev1.ServiceSpec{
 | 
						|
			Selector: opts.proxyLabels,
 | 
						|
			Type:     corev1.ServiceTypeClusterIP,
 | 
						|
			Ports:    []corev1.ServicePort{{Protocol: "TCP", Port: 9002, Name: "metrics"}},
 | 
						|
		},
 | 
						|
	}
 | 
						|
	var err error
 | 
						|
	metricsSvc, err = createOrUpdate(ctx, cl, opts.tsNamespace, metricsSvc, func(svc *corev1.Service) {
 | 
						|
		svc.Spec.Ports = metricsSvc.Spec.Ports
 | 
						|
		svc.Spec.Selector = metricsSvc.Spec.Selector
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("error ensuring metrics Service: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	crdExists, err := hasServiceMonitorCRD(ctx, cl)
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("error verifying that %q CRD exists: %w", serviceMonitorCRD, err)
 | 
						|
	}
 | 
						|
	if !crdExists {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	if pc.Spec.Metrics.ServiceMonitor == nil || !pc.Spec.Metrics.ServiceMonitor.Enable {
 | 
						|
		return maybeCleanupServiceMonitor(ctx, cl, opts.proxyStsName, opts.tsNamespace)
 | 
						|
	}
 | 
						|
 | 
						|
	logger.Infof("ensuring ServiceMonitor for metrics Service %s/%s", metricsSvc.Namespace, metricsSvc.Name)
 | 
						|
	svcMonitor, err := newServiceMonitor(metricsSvc, pc.Spec.Metrics.ServiceMonitor)
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("error creating ServiceMonitor: %w", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// We don't use createOrUpdate here because that does not work with unstructured types.
 | 
						|
	existing := svcMonitor.DeepCopy()
 | 
						|
	err = cl.Get(ctx, client.ObjectKeyFromObject(metricsSvc), existing)
 | 
						|
	if apierrors.IsNotFound(err) {
 | 
						|
		if err := cl.Create(ctx, svcMonitor); err != nil {
 | 
						|
			return fmt.Errorf("error creating ServiceMonitor: %w", err)
 | 
						|
		}
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("error getting ServiceMonitor: %w", err)
 | 
						|
	}
 | 
						|
	// Currently, we only update labels on the ServiceMonitor as those are the only values that can change.
 | 
						|
	if !reflect.DeepEqual(existing.GetLabels(), svcMonitor.GetLabels()) {
 | 
						|
		existing.SetLabels(svcMonitor.GetLabels())
 | 
						|
		if err := cl.Update(ctx, existing); err != nil {
 | 
						|
			return fmt.Errorf("error updating ServiceMonitor: %w", err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// maybeCleanupMetricsResources ensures that any metrics resources created for a proxy are deleted. Only metrics Service
 | 
						|
// gets deleted explicitly because the ServiceMonitor has Service's owner reference, so gets garbage collected
 | 
						|
// automatically.
 | 
						|
func maybeCleanupMetricsResources(ctx context.Context, opts *metricsOpts, cl client.Client) error {
 | 
						|
	sel := metricsSvcSelector(opts.proxyLabels, opts.proxyType)
 | 
						|
	return cl.DeleteAllOf(ctx, &corev1.Service{}, client.InNamespace(opts.tsNamespace), client.MatchingLabels(sel))
 | 
						|
}
 | 
						|
 | 
						|
// maybeCleanupServiceMonitor cleans up any ServiceMonitor created for the named proxy StatefulSet.
 | 
						|
func maybeCleanupServiceMonitor(ctx context.Context, cl client.Client, stsName, ns string) error {
 | 
						|
	smName := metricsResourceName(stsName)
 | 
						|
	sm := serviceMonitorTemplate(smName, ns)
 | 
						|
	u, err := serviceMonitorToUnstructured(sm)
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("error building ServiceMonitor: %w", err)
 | 
						|
	}
 | 
						|
	err = cl.Get(ctx, types.NamespacedName{Name: smName, Namespace: ns}, u)
 | 
						|
	if apierrors.IsNotFound(err) {
 | 
						|
		return nil // nothing to do
 | 
						|
	}
 | 
						|
	if err != nil {
 | 
						|
		return fmt.Errorf("error verifying if ServiceMonitor %s/%s exists: %w", ns, stsName, err)
 | 
						|
	}
 | 
						|
	return cl.Delete(ctx, u)
 | 
						|
}
 | 
						|
 | 
						|
// 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) {
 | 
						|
	sm := serviceMonitorTemplate(metricsSvc.Name, metricsSvc.Namespace)
 | 
						|
	sm.ObjectMeta.Labels = metricsSvc.Labels
 | 
						|
	if spec != nil && len(spec.Labels) > 0 {
 | 
						|
		sm.ObjectMeta.Labels = mergeMapKeys(sm.ObjectMeta.Labels, spec.Labels.Parse())
 | 
						|
	}
 | 
						|
 | 
						|
	sm.ObjectMeta.OwnerReferences = []metav1.OwnerReference{*metav1.NewControllerRef(metricsSvc, corev1.SchemeGroupVersion.WithKind("Service"))}
 | 
						|
	sm.Spec = ServiceMonitorSpec{
 | 
						|
		Selector: metav1.LabelSelector{MatchLabels: metricsSvc.Labels},
 | 
						|
		Endpoints: []ServiceMonitorEndpoint{{
 | 
						|
			Port: "metrics",
 | 
						|
		}},
 | 
						|
		NamespaceSelector: ServiceMonitorNamespaceSelector{
 | 
						|
			MatchNames: []string{metricsSvc.Namespace},
 | 
						|
		},
 | 
						|
		JobLabel: labelPromJob,
 | 
						|
		TargetLabels: []string{
 | 
						|
			labelPromProxyParentName,
 | 
						|
			labelPromProxyParentNamespace,
 | 
						|
			labelPromProxyType,
 | 
						|
		},
 | 
						|
	}
 | 
						|
	return serviceMonitorToUnstructured(sm)
 | 
						|
}
 | 
						|
 | 
						|
// serviceMonitorToUnstructured takes a ServiceMonitor and converts it to Unstructured type that can be used by the c/r
 | 
						|
// client in Kubernetes API server calls.
 | 
						|
func serviceMonitorToUnstructured(sm *ServiceMonitor) (*unstructured.Unstructured, error) {
 | 
						|
	contents, err := runtime.DefaultUnstructuredConverter.ToUnstructured(sm)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("error converting ServiceMonitor to Unstructured: %w", err)
 | 
						|
	}
 | 
						|
	u := &unstructured.Unstructured{}
 | 
						|
	u.SetUnstructuredContent(contents)
 | 
						|
	u.SetGroupVersionKind(sm.GroupVersionKind())
 | 
						|
	return u, nil
 | 
						|
}
 | 
						|
 | 
						|
// metricsResourceName returns name for metrics Service and ServiceMonitor for a proxy StatefulSet.
 | 
						|
func metricsResourceName(stsName string) string {
 | 
						|
	// Maximum length of StatefulSet name if 52 chars, so this is fine.
 | 
						|
	return fmt.Sprintf("%s-metrics", stsName)
 | 
						|
}
 | 
						|
 | 
						|
// metricsResourceLabels constructs labels that will be applied to metrics Service and metrics ServiceMonitor for a
 | 
						|
// proxy.
 | 
						|
func metricsResourceLabels(opts *metricsOpts) map[string]string {
 | 
						|
	lbls := map[string]string{
 | 
						|
		kubetypes.LabelManaged:   "true",
 | 
						|
		labelMetricsTarget:       opts.proxyStsName,
 | 
						|
		labelPromProxyType:       opts.proxyType,
 | 
						|
		labelPromProxyParentName: opts.proxyLabels[LabelParentName],
 | 
						|
	}
 | 
						|
	// Include namespace label for proxies created for a namespaced type.
 | 
						|
	if isNamespacedProxyType(opts.proxyType) {
 | 
						|
		lbls[labelPromProxyParentNamespace] = opts.proxyLabels[LabelParentNamespace]
 | 
						|
	}
 | 
						|
	lbls[labelPromJob] = promJobName(opts)
 | 
						|
	return lbls
 | 
						|
}
 | 
						|
 | 
						|
// promJobName constructs the value of the Prometheus job label that will apply to all metrics for a ServiceMonitor.
 | 
						|
func promJobName(opts *metricsOpts) string {
 | 
						|
	// Include parent resource namespace for proxies created for namespaced types.
 | 
						|
	if opts.proxyType == proxyTypeIngressResource || opts.proxyType == proxyTypeIngressService {
 | 
						|
		return fmt.Sprintf("ts_%s_%s_%s", opts.proxyType, opts.proxyLabels[LabelParentNamespace], opts.proxyLabels[LabelParentName])
 | 
						|
	}
 | 
						|
	return fmt.Sprintf("ts_%s_%s", opts.proxyType, opts.proxyLabels[LabelParentName])
 | 
						|
}
 | 
						|
 | 
						|
// metricsSvcSelector returns the minimum label set to uniquely identify a metrics Service for a proxy.
 | 
						|
func metricsSvcSelector(proxyLabels map[string]string, proxyType string) map[string]string {
 | 
						|
	sel := map[string]string{
 | 
						|
		labelPromProxyType:       proxyType,
 | 
						|
		labelPromProxyParentName: proxyLabels[LabelParentName],
 | 
						|
	}
 | 
						|
	// Include namespace label for proxies created for a namespaced type.
 | 
						|
	if isNamespacedProxyType(proxyType) {
 | 
						|
		sel[labelPromProxyParentNamespace] = proxyLabels[LabelParentNamespace]
 | 
						|
	}
 | 
						|
	return sel
 | 
						|
}
 | 
						|
 | 
						|
// serviceMonitorTemplate returns a base ServiceMonitor type that, when converted to Unstructured, is a valid type that
 | 
						|
// can be used in kube API server calls via the c/r client.
 | 
						|
func serviceMonitorTemplate(name, ns string) *ServiceMonitor {
 | 
						|
	return &ServiceMonitor{
 | 
						|
		TypeMeta: metav1.TypeMeta{
 | 
						|
			Kind:       "ServiceMonitor",
 | 
						|
			APIVersion: "monitoring.coreos.com/v1",
 | 
						|
		},
 | 
						|
		ObjectMeta: metav1.ObjectMeta{
 | 
						|
			Name:      name,
 | 
						|
			Namespace: ns,
 | 
						|
		},
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
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
 | 
						|
}
 | 
						|
 | 
						|
func isNamespacedProxyType(typ string) bool {
 | 
						|
	return typ == proxyTypeIngressResource || typ == proxyTypeIngressService
 | 
						|
}
 | 
						|
 | 
						|
func mergeMapKeys(a, b map[string]string) map[string]string {
 | 
						|
	m := make(map[string]string, len(a)+len(b))
 | 
						|
	for key, val := range b {
 | 
						|
		m[key] = val
 | 
						|
	}
 | 
						|
	for key, val := range a {
 | 
						|
		m[key] = val
 | 
						|
	}
 | 
						|
	return m
 | 
						|
}
 |