github.com/webmeshproj/webmesh-cni@v0.0.27/internal/controllers/pod_controller.go (about)

     1  /*
     2  Copyright 2023 Avi Zimmerman <avi.zimmerman@gmail.com>.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package controllers
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/netip"
    23  	"time"
    24  
    25  	v1 "github.com/webmeshproj/api/go/v1"
    26  	"github.com/webmeshproj/storage-provider-k8s/provider"
    27  	meshtypes "github.com/webmeshproj/webmesh/pkg/storage/types"
    28  	corev1 "k8s.io/api/core/v1"
    29  	ctrl "sigs.k8s.io/controller-runtime"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  	"sigs.k8s.io/controller-runtime/pkg/handler"
    32  	"sigs.k8s.io/controller-runtime/pkg/log"
    33  
    34  	"github.com/webmeshproj/webmesh-cni/internal/host"
    35  )
    36  
    37  //+kubebuilder:rbac:groups="",resources=pods;configmaps,verbs=get;list;watch
    38  
    39  // PodReconciler watches for pods of interest to the outside world
    40  // that have become ready and ensures their features are advertised.
    41  // TODO: This reoconciler should be used to match up all pod names
    42  // with containers for easier lookup.
    43  type PodReconciler struct {
    44  	client.Client
    45  	Host         host.Node
    46  	Provider     *provider.Provider
    47  	DNSSelector  map[string]string
    48  	DNSNamespace string
    49  	DNSPort      string
    50  }
    51  
    52  // SetupWithManager sets up the node reconciler with the manager.
    53  func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error {
    54  	return ctrl.NewControllerManagedBy(mgr).
    55  		Named("pod-features").
    56  		Watches(
    57  			&corev1.Pod{},
    58  			handler.EnqueueRequestsFromMapFunc(r.enqueueIfReadyAndInNetwork),
    59  		).
    60  		Complete(r)
    61  }
    62  
    63  // Reconcile reconciles a node.
    64  func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    65  	log := log.FromContext(ctx)
    66  	if !r.Host.Started() {
    67  		// Requeue until the host is started.
    68  		log.Info("Host not started, requeuing")
    69  		return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 2}, nil
    70  	}
    71  	log.Info("Reconciling available features for pod")
    72  	var pod corev1.Pod
    73  	if err := r.Get(ctx, req.NamespacedName, &pod); err != nil {
    74  		return ctrl.Result{}, client.IgnoreNotFound(err)
    75  	}
    76  	// Get the peer from the storage provider by their IP address.
    77  	db := r.Provider.Datastore()
    78  	var peer meshtypes.MeshNode
    79  	for _, ip := range pod.Status.PodIPs {
    80  		if ip.IP == "" {
    81  			continue
    82  		}
    83  		addr, err := netip.ParseAddr(ip.IP)
    84  		if err != nil {
    85  			log.Error(err, "Failed to parse pod IP")
    86  			continue
    87  		}
    88  		switch {
    89  		case addr.Is4():
    90  			peer, err = db.GetPeerByIPv4Addr(ctx, netip.PrefixFrom(addr, 32))
    91  		case addr.Is6():
    92  			peer, err = db.GetPeerByIPv6Addr(ctx, netip.PrefixFrom(addr, 112))
    93  		default:
    94  			log.Info("Ignoring invalid IP address", "addr", addr.String())
    95  			continue
    96  		}
    97  		if err != nil {
    98  			log.Error(err, "Failed to lookup peer by IP address")
    99  			continue
   100  		}
   101  	}
   102  	if peer.MeshNode == nil {
   103  		return ctrl.Result{}, fmt.Errorf("failed to find peer for pod")
   104  	}
   105  	// Ensure the pod has the DNS feature.
   106  	dnsPort := func() int32 {
   107  		for _, container := range pod.Spec.Containers {
   108  			for _, port := range container.Ports {
   109  				if port.Name == r.DNSPort {
   110  					return port.ContainerPort
   111  				}
   112  			}
   113  		}
   114  		// Assume the DNS port is 53.
   115  		return 53
   116  	}()
   117  	if !peer.HasFeature(v1.Feature_MESH_DNS) {
   118  		log.Info("Ensuring pod has MeshDNS feature")
   119  		peer.Features = append(peer.Features, &v1.FeaturePort{
   120  			Feature: v1.Feature_MESH_DNS,
   121  			Port:    dnsPort,
   122  		}, &v1.FeaturePort{
   123  			Feature: v1.Feature_FORWARD_MESH_DNS,
   124  			Port:    dnsPort,
   125  		})
   126  		if err := r.Provider.MeshDB().Peers().Put(ctx, peer); err != nil {
   127  			log.Error(err, "Failed to add DNS feature to peer")
   128  			return ctrl.Result{}, nil
   129  		}
   130  	}
   131  	return ctrl.Result{}, nil
   132  }
   133  
   134  func (r *PodReconciler) enqueueIfReadyAndInNetwork(ctx context.Context, o client.Object) []ctrl.Request {
   135  	// Fast path, if the host isn't running there isn't anything we care about
   136  	if !r.Host.Started() {
   137  		return nil
   138  	}
   139  	pod := o.(*corev1.Pod)
   140  	// Ignore deleted pods.
   141  	if pod.GetDeletionTimestamp() != nil {
   142  		return nil
   143  	}
   144  	// Ignore host network pods.
   145  	if pod.Spec.HostNetwork {
   146  		return nil
   147  	}
   148  	// Ignore pods that aren't running yet.
   149  	if pod.Status.Phase != corev1.PodRunning {
   150  		return nil
   151  	}
   152  	// Ignore pods that aren't in the DNS namespace.
   153  	if pod.GetNamespace() != r.DNSNamespace {
   154  		return nil
   155  	}
   156  	// Ignore pods that don't match the DNS selector.
   157  	for k, v := range r.DNSSelector {
   158  		if pod.GetLabels()[k] != v {
   159  			return nil
   160  		}
   161  	}
   162  	// Match the request if the pod has an in network IP.
   163  	key := client.ObjectKeyFromObject(pod)
   164  	req := ctrl.Request{NamespacedName: key}
   165  	for _, ip := range pod.Status.PodIPs {
   166  		if ip.IP == "" {
   167  			continue
   168  		}
   169  		addr, err := netip.ParseAddr(ip.IP)
   170  		if err != nil {
   171  			log.FromContext(ctx).Error(err, "Failed to parse pod IP")
   172  			continue
   173  		}
   174  		switch {
   175  		case addr.Is6() && r.Host.Node().Network().NetworkV6().Contains(addr):
   176  			return []ctrl.Request{req}
   177  		case addr.Is4() && r.Host.Node().Network().NetworkV4().Contains(addr):
   178  			return []ctrl.Request{req}
   179  		}
   180  	}
   181  	return nil
   182  }