Ctrl K
ComunidadGuías…

k8s-operator/reconciler/nameserver/nameserver.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build !plan9

// Package nameserver provides reconciliation logic for the DNSConfig custom resource definition.
// It is responsible for creating and managing nameserver resources in response to DNSConfig objects.
package nameserver

import (
	"context"
	_ "embed"
	"errors"
	"fmt"
	"time"

	"go.uber.org/zap"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	apiequality "k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/builder"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/manager"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
	"sigs.k8s.io/yaml"

	tsoperator "tailscale.com/k8s-operator"
	tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
	"tailscale.com/k8s-operator/reconciler"
	"tailscale.com/kube/kubetypes"
	"tailscale.com/tstime"
	"tailscale.com/util/clientmetric"
)

const (
	reconcilerName = "nameserver-reconciler"

	reasonNameserverCreationFailed  = "NameserverCreationFailed"
	reasonMultipleDNSConfigsPresent = "MultipleDNSConfigsPresent"

	// ReasonNameserverCreated is the condition reason set when nameserver resources have been created successfully.
	ReasonNameserverCreated = "NameserverCreated"

	messageNameserverCreationFailed  = "Failed creating nameserver resources: %v"
	messageMultipleDNSConfigsPresent = "Multiple DNSConfig resources found in cluster. Please ensure no more than one is present."

	defaultNameserverImageRepo = "tailscale/k8s-nameserver"
	defaultNameserverImageTag  = "stable"

	shortRequeue = 5 * time.Second
)

var gaugeNameserverResources = clientmetric.NewGauge(kubetypes.MetricNameserverCount)

// ReconcilerOptions contains the options for creating a new Reconciler.
type ReconcilerOptions struct {
	Client             client.Client
	Recorder           record.EventRecorder
	TailscaleNamespace string
	Logger             *zap.SugaredLogger
	Clock              tstime.Clock
}

// Reconciler knows how to create nameserver resources in cluster in
// response to users applying DNSConfig.
type Reconciler struct {
	client.Client
	logger      *zap.SugaredLogger
	recorder    record.EventRecorder
	clock       tstime.Clock
	tsNamespace string
	tracker     *reconciler.ResourceTracker
}

// NewReconciler creates a new Reconciler.
func NewReconciler(options ReconcilerOptions) *Reconciler {
	clock := options.Clock
	if clock == nil {
		clock = tstime.DefaultClock{}
	}
	return &Reconciler{
		Client:      options.Client,
		recorder:    options.Recorder,
		tsNamespace: options.TailscaleNamespace,
		logger:      options.Logger.Named(reconcilerName),
		clock:       clock,
		tracker:     reconciler.NewResourceTracker(gaugeNameserverResources),
	}
}

// Register registers the nameserver reconciler with the controller manager.
func (r *Reconciler) Register(mgr manager.Manager) error {
	nameserverFilter := handler.EnqueueRequestsFromMapFunc(reconciler.EnqueueForChild("nameserver"))
	return builder.ControllerManagedBy(mgr).
		For(&tsapi.DNSConfig{}).
		Named(reconcilerName).
		Watches(&appsv1.Deployment{}, nameserverFilter).
		Watches(&corev1.ConfigMap{}, nameserverFilter).
		Watches(&corev1.Service{}, nameserverFilter).
		Watches(&corev1.ServiceAccount{}, nameserverFilter).
		Complete(r)
}

func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
	logger := r.logger.With("dnsConfig", req.Name)
	logger.Debugf("starting reconcile")
	defer logger.Debugf("reconcile finished")

	var dnsCfg tsapi.DNSConfig
	err = r.Get(ctx, req.NamespacedName, &dnsCfg)
	if apierrors.IsNotFound(err) {
		// Request object not found, could have been deleted after reconcile request.
		logger.Debugf("dnsconfig not found, assuming it was deleted")
		return reconcile.Result{}, nil
	} else if err != nil {
		return reconcile.Result{}, fmt.Errorf("failed to get dnsconfig: %w", err)
	}
	if !dnsCfg.DeletionTimestamp.IsZero() {
		logger.Info("Cleaning up DNSConfig resources")
		if err := r.maybeCleanup(&dnsCfg); err != nil {
			logger.Errorf("error cleaning up reconciler resource: %v", err)
			return res, err
		}
		if err := reconciler.ClearFinalizer(ctx, r.Client, &dnsCfg, reconciler.Finalizer); err != nil {
			logger.Errorf("error removing finalizer: %v", err)
			return reconcile.Result{}, err
		}
		logger.Infof("Nameserver resources cleaned up")
		return reconcile.Result{}, nil
	}

	oldCnStatus := dnsCfg.Status.DeepCopy()
	setStatus := func(dnsCfg *tsapi.DNSConfig, status metav1.ConditionStatus, reason, message string) (reconcile.Result, error) {
		tsoperator.SetDNSConfigCondition(dnsCfg, tsapi.NameserverReady, status, reason, message, dnsCfg.Generation, r.clock, logger)
		if !apiequality.Semantic.DeepEqual(oldCnStatus, &dnsCfg.Status) {
			// An error encountered here should get returned by the Reconcile function.
			if updateErr := r.Client.Status().Update(ctx, dnsCfg); updateErr != nil {
				err = errors.Join(err, updateErr)
			}
		}
		return res, err
	}
	var dnsCfgs tsapi.DNSConfigList
	if err := r.List(ctx, &dnsCfgs); err != nil {
		return res, fmt.Errorf("error listing DNSConfigs: %w", err)
	}
	if len(dnsCfgs.Items) > 1 { // enforce DNSConfig to be a singleton
		msg := "invalid cluster configuration: more than one tailscale.com/dnsconfigs found. Please ensure that no more than one is created."
		logger.Error(msg)
		r.recorder.Event(&dnsCfg, corev1.EventTypeWarning, reasonMultipleDNSConfigsPresent, messageMultipleDNSConfigsPresent)
		setStatus(&dnsCfg, metav1.ConditionFalse, reasonMultipleDNSConfigsPresent, messageMultipleDNSConfigsPresent)
	}

	if err := reconciler.EnsureFinalizer(ctx, r.Client, &dnsCfg, reconciler.Finalizer); err != nil {
		msg := fmt.Sprintf(messageNameserverCreationFailed, err)
		logger.Error(msg)
		return setStatus(&dnsCfg, metav1.ConditionFalse, reasonNameserverCreationFailed, msg)
	}
	if err = r.maybeProvision(ctx, &dnsCfg); err != nil {
		if reconciler.IsOptimisticLockError(err) {
			logger.Debugf("update conflict, retrying: %v", err)
			return reconcile.Result{RequeueAfter: shortRequeue}, nil
		} else {
			return reconcile.Result{}, fmt.Errorf("error provisioning nameserver resources: %w", err)
		}
	}

	r.tracker.Add(dnsCfg.UID)

	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{Name: "nameserver", Namespace: r.tsNamespace},
	}
	if err := r.Client.Get(ctx, client.ObjectKeyFromObject(svc), svc); err != nil {
		return res, fmt.Errorf("error getting Service: %w", err)
	}
	if ip := svc.Spec.ClusterIP; ip != "" && ip != "None" {
		dnsCfg.Status.Nameserver = &tsapi.NameserverStatus{
			IP: ip,
		}
		return setStatus(&dnsCfg, metav1.ConditionTrue, ReasonNameserverCreated, ReasonNameserverCreated)
	}
	logger.Info("nameserver Service does not have an IP address allocated, waiting...")
	return reconcile.Result{}, nil
}

func nameserverResourceLabels(name, namespace string) map[string]string {
	labels := reconciler.Labels("nameserver", name, namespace)
	labels["app.kubernetes.io/name"] = "tailscale"
	labels["app.kubernetes.io/component"] = "nameserver"
	return labels
}

func (r *Reconciler) maybeProvision(ctx context.Context, tsDNSCfg *tsapi.DNSConfig) error {
	labels := nameserverResourceLabels(tsDNSCfg.Name, r.tsNamespace)
	dCfg := &deployConfig{
		ownerRefs: []metav1.OwnerReference{*metav1.NewControllerRef(tsDNSCfg, tsapi.SchemeGroupVersion.WithKind("DNSConfig"))},
		namespace: r.tsNamespace,
		labels:    labels,
		imageRepo: defaultNameserverImageRepo,
		imageTag:  defaultNameserverImageTag,
		replicas:  1,
	}

	if tsDNSCfg.Spec.Nameserver.Replicas != nil {
		dCfg.replicas = *tsDNSCfg.Spec.Nameserver.Replicas
	}
	if tsDNSCfg.Spec.Nameserver.Image != nil && tsDNSCfg.Spec.Nameserver.Image.Repo != "" {
		dCfg.imageRepo = tsDNSCfg.Spec.Nameserver.Image.Repo
	}
	if tsDNSCfg.Spec.Nameserver.Image != nil && tsDNSCfg.Spec.Nameserver.Image.Tag != "" {
		dCfg.imageTag = tsDNSCfg.Spec.Nameserver.Image.Tag
	}
	if tsDNSCfg.Spec.Nameserver.Service != nil {
		dCfg.clusterIP = tsDNSCfg.Spec.Nameserver.Service.ClusterIP
	}
	if tsDNSCfg.Spec.Nameserver.Pod != nil {
		dCfg.tolerations = tsDNSCfg.Spec.Nameserver.Pod.Tolerations
		dCfg.affinity = tsDNSCfg.Spec.Nameserver.Pod.Affinity
		dCfg.nodeSelector = tsDNSCfg.Spec.Nameserver.Pod.NodeSelector
		dCfg.imagePullSecrets = tsDNSCfg.Spec.Nameserver.Pod.ImagePullSecrets
	}

	for _, d := range []deployable{saDeployable, deployDeployable, svcDeployable, cmDeployable} {
		if err := d.updateObj(ctx, dCfg, r.Client); err != nil {
			return fmt.Errorf("error reconciling %s: %w", d.kind, err)
		}
	}
	return nil
}

// maybeCleanup removes DNSConfig from being tracked. The cluster resources
// created will be automatically garbage collected as they are owned by the
// DNSConfig.
func (r *Reconciler) maybeCleanup(dnsCfg *tsapi.DNSConfig) error {
	r.tracker.Remove(dnsCfg.UID)
	return nil
}

type deployable struct {
	kind      string
	updateObj func(context.Context, *deployConfig, client.Client) error
}

type deployConfig struct {
	replicas         int32
	imageRepo        string
	imageTag         string
	labels           map[string]string
	ownerRefs        []metav1.OwnerReference
	namespace        string
	clusterIP        string
	tolerations      []corev1.Toleration
	affinity         *corev1.Affinity
	nodeSelector     map[string]string
	imagePullSecrets []corev1.LocalObjectReference
}

var (
	//go:embed manifests/cm.yaml
	cmYaml []byte
	//go:embed manifests/deploy.yaml
	deployYaml []byte
	//go:embed manifests/sa.yaml
	saYaml []byte
	//go:embed manifests/svc.yaml
	svcYaml []byte

	deployDeployable = deployable{
		kind: "Deployment",
		updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
			d := new(appsv1.Deployment)
			if err := yaml.Unmarshal(deployYaml, &d); err != nil {
				return fmt.Errorf("error unmarshalling Deployment yaml: %w", err)
			}
			d.Spec.Replicas = new(cfg.replicas)
			d.Spec.Template.Spec.Containers[0].Image = fmt.Sprintf("%s:%s", cfg.imageRepo, cfg.imageTag)
			d.ObjectMeta.Namespace = cfg.namespace
			d.ObjectMeta.Labels = cfg.labels
			d.ObjectMeta.OwnerReferences = cfg.ownerRefs
			d.Spec.Template.Spec.Tolerations = cfg.tolerations
			d.Spec.Template.Spec.Affinity = cfg.affinity
			d.Spec.Template.Spec.NodeSelector = cfg.nodeSelector
			d.Spec.Template.Spec.ImagePullSecrets = cfg.imagePullSecrets
			updateF := func(oldD *appsv1.Deployment) {
				oldD.Spec = d.Spec
			}
			_, err := reconciler.CreateOrUpdate[appsv1.Deployment](ctx, kubeClient, cfg.namespace, d, updateF)
			return err
		},
	}
	saDeployable = deployable{
		kind: "ServiceAccount",
		updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
			sa := new(corev1.ServiceAccount)
			if err := yaml.Unmarshal(saYaml, &sa); err != nil {
				return fmt.Errorf("error unmarshalling ServiceAccount yaml: %w", err)
			}
			sa.ObjectMeta.Labels = cfg.labels
			sa.ObjectMeta.OwnerReferences = cfg.ownerRefs
			sa.ObjectMeta.Namespace = cfg.namespace
			_, err := reconciler.CreateOrUpdate(ctx, kubeClient, cfg.namespace, sa, func(*corev1.ServiceAccount) {})
			return err
		},
	}
	svcDeployable = deployable{
		kind: "Service",
		updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
			svc := new(corev1.Service)
			if err := yaml.Unmarshal(svcYaml, &svc); err != nil {
				return fmt.Errorf("error unmarshalling Service yaml: %w", err)
			}
			svc.ObjectMeta.Labels = cfg.labels
			svc.ObjectMeta.OwnerReferences = cfg.ownerRefs
			svc.ObjectMeta.Namespace = cfg.namespace
			svc.Spec.ClusterIP = cfg.clusterIP
			_, err := reconciler.CreateOrUpdate[corev1.Service](ctx, kubeClient, cfg.namespace, svc, func(*corev1.Service) {})
			return err
		},
	}
	cmDeployable = deployable{
		kind: "ConfigMap",
		updateObj: func(ctx context.Context, cfg *deployConfig, kubeClient client.Client) error {
			cm := new(corev1.ConfigMap)
			if err := yaml.Unmarshal(cmYaml, &cm); err != nil {
				return fmt.Errorf("error unmarshalling ConfigMap yaml: %w", err)
			}
			cm.ObjectMeta.Labels = cfg.labels
			cm.ObjectMeta.OwnerReferences = cfg.ownerRefs
			cm.ObjectMeta.Namespace = cfg.namespace
			_, err := reconciler.CreateOrUpdate[corev1.ConfigMap](ctx, kubeClient, cfg.namespace, cm, func(cm *corev1.ConfigMap) {})
			return err
		},
	}
)