github.com/projectcontour/contour@v1.28.2/cmd/contour/ingressstatus.go (about)

     1  // Copyright Project Contour Authors
     2  // Licensed under the Apache License, Version 2.0 (the "License");
     3  // you may not use this file except in compliance with the License.
     4  // You may obtain a copy of the License at
     5  //
     6  //     http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package main
    15  
    16  import (
    17  	"context"
    18  	"net"
    19  	"strings"
    20  
    21  	contour_api_v1 "github.com/projectcontour/contour/apis/projectcontour/v1"
    22  	"github.com/projectcontour/contour/internal/k8s"
    23  	"github.com/sirupsen/logrus"
    24  	v1 "k8s.io/api/core/v1"
    25  	networking_v1 "k8s.io/api/networking/v1"
    26  	"k8s.io/apimachinery/pkg/types"
    27  	"sigs.k8s.io/controller-runtime/pkg/cache"
    28  	"sigs.k8s.io/controller-runtime/pkg/client"
    29  	gatewayapi_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
    30  )
    31  
    32  // loadBalancerStatusWriter orchestrates LoadBalancer address status
    33  // updates for HTTPProxy, Ingress and Gateway objects. Actually updating the
    34  // address in the object status is performed by k8s.StatusAddressUpdater.
    35  //
    36  // The theory of operation of the loadBalancerStatusWriter is as follows:
    37  //
    38  //  1. On startup the loadBalancerStatusWriter waits to be elected leader.
    39  //  2. Once elected leader, the loadBalancerStatusWriter waits to receive a
    40  //     v1.LoadBalancerStatus value.
    41  //  3. Once a v1.LoadBalancerStatus value has been received, the
    42  //     cached address is updated so that it will be applied to objects
    43  //     received in any subsequent informer events.
    44  //  4. All Ingress, HTTPProxy and Gateway objects are listed from the informer
    45  //     cache and an attempt is made to update their status with the new
    46  //     address. This update may end up being a no-op in which case it
    47  //     doesn't make an API server call.
    48  //  5. If the worker is stopped, the informer continues but no further
    49  //     status updates are made.
    50  type loadBalancerStatusWriter struct {
    51  	log                   logrus.FieldLogger
    52  	cache                 cache.Cache
    53  	lbStatus              chan v1.LoadBalancerStatus
    54  	statusUpdater         k8s.StatusUpdater
    55  	ingressClassNames     []string
    56  	gatewayControllerName string
    57  	gatewayRef            *types.NamespacedName
    58  }
    59  
    60  func (isw *loadBalancerStatusWriter) NeedLeaderElection() bool {
    61  	return true
    62  }
    63  
    64  func (isw *loadBalancerStatusWriter) Start(ctx context.Context) error {
    65  	u := &k8s.StatusAddressUpdater{
    66  		Logger: func() logrus.FieldLogger {
    67  			// Configure the StatusAddressUpdater logger.
    68  			log := isw.log.WithField("context", "StatusAddressUpdater")
    69  			if len(isw.ingressClassNames) > 0 {
    70  				return log.WithField("target-ingress-classes", isw.ingressClassNames)
    71  			}
    72  
    73  			return log
    74  		}(),
    75  		Cache:                 isw.cache,
    76  		IngressClassNames:     isw.ingressClassNames,
    77  		GatewayControllerName: isw.gatewayControllerName,
    78  		GatewayRef:            isw.gatewayRef,
    79  		StatusUpdater:         isw.statusUpdater,
    80  	}
    81  
    82  	// Create informers for the types that need load balancer
    83  	// address status. The cache should have already started
    84  	// informers, so new informers will auto-start.
    85  	resources := []client.Object{
    86  		&contour_api_v1.HTTPProxy{},
    87  		&networking_v1.Ingress{},
    88  	}
    89  
    90  	// Only create Gateway informer if a controller or specific gateway was provided,
    91  	// otherwise the API may not exist in the cluster.
    92  	if len(isw.gatewayControllerName) > 0 || isw.gatewayRef != nil {
    93  		resources = append(resources, &gatewayapi_v1beta1.Gateway{})
    94  	}
    95  
    96  	for _, r := range resources {
    97  		inf, err := isw.cache.GetInformer(context.Background(), r)
    98  		if err != nil {
    99  			isw.log.WithError(err).WithField("resource", r).Fatal("failed to create informer")
   100  		}
   101  
   102  		_, err = inf.AddEventHandler(u)
   103  		if err != nil {
   104  			isw.log.WithError(err).WithField("resource", r).Fatal("failed to add event handler to informer")
   105  		}
   106  	}
   107  
   108  	for {
   109  		select {
   110  		case <-ctx.Done():
   111  			// Once started, there's no way to stop the
   112  			// informer from here. Clear the load balancer
   113  			// status so that subsequent informer events
   114  			// will have no effect.
   115  			u.Set(v1.LoadBalancerStatus{})
   116  			return nil
   117  		case lbs := <-isw.lbStatus:
   118  			isw.log.WithField("loadbalancer-address", lbAddress(lbs)).
   119  				Info("received a new address for status.loadBalancer")
   120  
   121  			u.Set(lbs)
   122  
   123  			var ingressList networking_v1.IngressList
   124  			if err := isw.cache.List(context.Background(), &ingressList); err != nil {
   125  				isw.log.WithError(err).WithField("kind", "Ingress").Error("failed to list objects")
   126  			} else {
   127  				for i := range ingressList.Items {
   128  					u.OnAdd(&ingressList.Items[i], false)
   129  				}
   130  			}
   131  
   132  			var proxyList contour_api_v1.HTTPProxyList
   133  			if err := isw.cache.List(context.Background(), &proxyList); err != nil {
   134  				isw.log.WithError(err).WithField("kind", "HTTPProxy").Error("failed to list objects")
   135  			} else {
   136  				for i := range proxyList.Items {
   137  					u.OnAdd(&proxyList.Items[i], false)
   138  				}
   139  			}
   140  
   141  			// Only list Gateways if a controller or specific gateway was configured,
   142  			// otherwise the API may not exist in the cluster.
   143  			if len(isw.gatewayControllerName) > 0 || isw.gatewayRef != nil {
   144  				var gatewayList gatewayapi_v1beta1.GatewayList
   145  				if err := isw.cache.List(context.Background(), &gatewayList); err != nil {
   146  					isw.log.WithError(err).WithField("kind", "Gateway").Error("failed to list objects")
   147  				} else {
   148  					for i := range gatewayList.Items {
   149  						u.OnAdd(&gatewayList.Items[i], false)
   150  					}
   151  				}
   152  			}
   153  		}
   154  	}
   155  }
   156  
   157  func parseStatusFlag(status string) v1.LoadBalancerStatus {
   158  	// Support ','-separated lists.
   159  	var ingresses []v1.LoadBalancerIngress
   160  
   161  	for _, item := range strings.Split(status, ",") {
   162  		item = strings.TrimSpace(item)
   163  		if len(item) == 0 {
   164  			continue
   165  		}
   166  
   167  		// Use the parseability by net.ParseIP as a signal, since we need
   168  		// to pass a string into the v1.LoadBalancerIngress anyway.
   169  		if ip := net.ParseIP(item); ip != nil {
   170  			ingresses = append(ingresses, v1.LoadBalancerIngress{
   171  				IP: item,
   172  			})
   173  		} else {
   174  			ingresses = append(ingresses, v1.LoadBalancerIngress{
   175  				Hostname: item,
   176  			})
   177  		}
   178  	}
   179  
   180  	return v1.LoadBalancerStatus{
   181  		Ingress: ingresses,
   182  	}
   183  }
   184  
   185  // lbAddress gets the string representation of the first address, for logging.
   186  func lbAddress(lb v1.LoadBalancerStatus) string {
   187  	if len(lb.Ingress) == 0 {
   188  		return ""
   189  	}
   190  
   191  	if lb.Ingress[0].IP != "" {
   192  		return lb.Ingress[0].IP
   193  	}
   194  
   195  	return lb.Ingress[0].Hostname
   196  }