github.com/cilium/cilium@v1.16.2/pkg/bgpv1/manager/reconcilerv2/crd_status.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package reconcilerv2
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  
    11  	"github.com/cilium/hive/cell"
    12  	"github.com/cilium/hive/job"
    13  	"github.com/lthibault/jitterbug"
    14  	"github.com/sirupsen/logrus"
    15  	k8sErrors "k8s.io/apimachinery/pkg/api/errors"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	k8s_types "k8s.io/apimachinery/pkg/types"
    18  	"k8s.io/apimachinery/pkg/util/wait"
    19  	"k8s.io/utils/ptr"
    20  
    21  	daemon_k8s "github.com/cilium/cilium/daemon/k8s"
    22  	"github.com/cilium/cilium/pkg/bgpv1/agent/mode"
    23  	"github.com/cilium/cilium/pkg/bgpv1/manager/instance"
    24  	"github.com/cilium/cilium/pkg/bgpv1/types"
    25  	"github.com/cilium/cilium/pkg/k8s"
    26  	"github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1"
    27  	k8s_client "github.com/cilium/cilium/pkg/k8s/client"
    28  	"github.com/cilium/cilium/pkg/k8s/resource"
    29  	"github.com/cilium/cilium/pkg/lock"
    30  	"github.com/cilium/cilium/pkg/time"
    31  )
    32  
    33  const (
    34  	CRDStatusUpdateInterval = 5 * time.Second
    35  )
    36  
    37  type StatusReconciler struct {
    38  	lock.Mutex
    39  
    40  	Logger            logrus.FieldLogger
    41  	ClientSet         k8s_client.Clientset
    42  	LocalNodeResource daemon_k8s.LocalCiliumNodeResource
    43  
    44  	nodeName      string
    45  	desiredStatus *v2alpha1.CiliumBGPNodeStatus
    46  	runningStatus *v2alpha1.CiliumBGPNodeStatus
    47  }
    48  
    49  type StatusReconcilerIn struct {
    50  	cell.In
    51  
    52  	Job       job.Group
    53  	ClientSet k8s_client.Clientset
    54  	Logger    logrus.FieldLogger
    55  	LocalNode daemon_k8s.LocalCiliumNodeResource
    56  }
    57  
    58  type StatusReconcilerOut struct {
    59  	cell.Out
    60  
    61  	Reconciler StateReconciler `group:"bgp-state-reconciler-v2"`
    62  }
    63  
    64  func NewStatusReconciler(in StatusReconcilerIn) StatusReconcilerOut {
    65  	// CRD Status reconciler is disabled if there is no kubernetes support
    66  	if !in.ClientSet.IsEnabled() {
    67  		return StatusReconcilerOut{}
    68  	}
    69  
    70  	r := &StatusReconciler{
    71  		Logger:            in.Logger.WithField(types.ReconcilerLogField, "CRD_Status"),
    72  		LocalNodeResource: in.LocalNode,
    73  		ClientSet:         in.ClientSet,
    74  		desiredStatus:     &v2alpha1.CiliumBGPNodeStatus{},
    75  		runningStatus:     &v2alpha1.CiliumBGPNodeStatus{},
    76  	}
    77  
    78  	in.Job.Add(job.OneShot("bgp-crd-status-initialize", func(ctx context.Context, health cell.Health) error {
    79  		r.Logger.Debug("Initializing")
    80  
    81  		for event := range r.LocalNodeResource.Events(ctx) {
    82  			switch event.Kind {
    83  			case resource.Upsert:
    84  				r.Lock()
    85  				r.nodeName = event.Object.GetName()
    86  				r.Unlock()
    87  			}
    88  			event.Done(nil)
    89  		}
    90  		return nil
    91  	}))
    92  
    93  	in.Job.Add(job.OneShot("bgp-crd-status-update-job", func(ctx context.Context, health cell.Health) (err error) {
    94  		r.Logger.Debug("Update job running")
    95  
    96  		// Ticker with jitter is used to avoid all nodes updating API server at the same time.
    97  		// BGP updates will simultaneously on all nodes ( on external or internal changes),
    98  		// which will result in status update.
    99  		// We want to stagger the status updates to avoid thundering herd problem.
   100  		ticker := jitterbug.New(
   101  			CRDStatusUpdateInterval,
   102  			&jitterbug.Norm{Stdev: time.Millisecond * 500},
   103  		)
   104  		defer ticker.Stop()
   105  
   106  		for {
   107  			select {
   108  			case <-ticker.C:
   109  				// Reconciliation of CRD status is done every CRDStatusUpdateInterval seconds, if there is an error it will be retried
   110  				// with exponential backoff. Exponential backoff is capped at 10 retries, after which we will again fall back to
   111  				// starting interval of CRDStatusUpdateInterval.
   112  				// This will result in see-saw pattern of retries, which provides some level of backoff mechanism.
   113  				// Error will be logged once 10 retries fails consecutively, so we do not flood the logs with errors on each retry.
   114  				err := r.reconcileWithRetry(ctx)
   115  				if err != nil {
   116  					r.Logger.WithError(err).Error("Failed to update CiliumBGPNodeConfig status after retries")
   117  				}
   118  
   119  			case <-ctx.Done():
   120  				r.Logger.Debug("CRD status update job stopped")
   121  				return nil
   122  			}
   123  		}
   124  	}))
   125  
   126  	return StatusReconcilerOut{
   127  		Reconciler: r,
   128  	}
   129  }
   130  
   131  func (r *StatusReconciler) Name() string {
   132  	return "CiliumBGPNodeConfigStatusReconciler"
   133  }
   134  
   135  func (r *StatusReconciler) Priority() int {
   136  	return 50
   137  }
   138  
   139  func (r *StatusReconciler) Reconcile(ctx context.Context, params StateReconcileParams) error {
   140  	r.Lock()
   141  	defer r.Unlock()
   142  
   143  	// do not reconcile if not in BGPv2 mode
   144  	if params.ConfigMode.Get() != mode.BGPv2 {
   145  		// reset status to empty if not in BGPv2 mode
   146  		r.desiredStatus = &v2alpha1.CiliumBGPNodeStatus{}
   147  		return nil
   148  	}
   149  
   150  	current := r.desiredStatus.DeepCopy()
   151  
   152  	if params.UpdatedInstance != nil {
   153  		r.Logger.WithFields(logrus.Fields{
   154  			types.InstanceLogField: params.UpdatedInstance.Config.Name,
   155  		}).Debug("Reconciling CRD status")
   156  
   157  		// get updated status for the instance
   158  		instanceStatus, err := r.getInstanceStatus(ctx, params.UpdatedInstance)
   159  		if err != nil {
   160  			return err
   161  		}
   162  
   163  		found := false
   164  		for idx, instance := range current.BGPInstances {
   165  			if instance.Name == instanceStatus.Name {
   166  				current.BGPInstances[idx] = *instanceStatus
   167  				found = true
   168  				break
   169  			}
   170  		}
   171  		if !found {
   172  			current.BGPInstances = append(current.BGPInstances, *instanceStatus)
   173  		}
   174  	}
   175  
   176  	if params.DeletedInstance != "" {
   177  		r.Logger.WithFields(logrus.Fields{
   178  			types.InstanceLogField: params.DeletedInstance,
   179  		}).Debug("Deleting instance from CRD status")
   180  
   181  		// remove instance from status
   182  		for idx, instance := range current.BGPInstances {
   183  			if instance.Name == params.DeletedInstance {
   184  				current.BGPInstances = append(current.BGPInstances[:idx], current.BGPInstances[idx+1:]...)
   185  				break
   186  			}
   187  		}
   188  	}
   189  
   190  	r.desiredStatus = current
   191  	return nil
   192  }
   193  
   194  func (r *StatusReconciler) getInstanceStatus(ctx context.Context, instance *instance.BGPInstance) (*v2alpha1.CiliumBGPNodeInstanceStatus, error) {
   195  	res := &v2alpha1.CiliumBGPNodeInstanceStatus{
   196  		Name:     instance.Config.Name,
   197  		LocalASN: instance.Config.LocalASN,
   198  	}
   199  
   200  	// get peer status
   201  	peers, err := instance.Router.GetPeerState(ctx)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	for _, configuredPeers := range instance.Config.Peers {
   207  		if configuredPeers.PeerASN == nil || configuredPeers.PeerAddress == nil {
   208  			continue
   209  		}
   210  
   211  		peerStatus := v2alpha1.CiliumBGPNodePeerStatus{
   212  			Name:        configuredPeers.Name,
   213  			PeerAddress: *configuredPeers.PeerAddress,
   214  			PeerASN:     configuredPeers.PeerASN,
   215  		}
   216  
   217  		for _, runningPeerState := range peers.Peers {
   218  			if runningPeerState.PeerAddress != *configuredPeers.PeerAddress || runningPeerState.PeerAsn != *configuredPeers.PeerASN {
   219  				continue
   220  			}
   221  
   222  			peerStatus.PeeringState = ptr.To[string](runningPeerState.SessionState)
   223  
   224  			// Update established timestamp
   225  			if runningPeerState.SessionState == types.SessionEstablished.String() {
   226  				// Time API only allows add with duration, to go back in time from uptime timestamp we need to subtract
   227  				// uptime from current time.
   228  				establishedTime := time.Now().Add(-time.Duration(runningPeerState.UptimeNanoseconds))
   229  				peerStatus.EstablishedTime = ptr.To[string](establishedTime.Format(time.RFC3339))
   230  			}
   231  
   232  			// applied timers
   233  			peerStatus.Timers = &v2alpha1.CiliumBGPTimersState{
   234  				AppliedHoldTimeSeconds:  ptr.To[int32](int32(runningPeerState.AppliedHoldTimeSeconds)),
   235  				AppliedKeepaliveSeconds: ptr.To[int32](int32(runningPeerState.AppliedKeepAliveTimeSeconds)),
   236  			}
   237  
   238  			// update route counts
   239  			for _, af := range runningPeerState.Families {
   240  				peerStatus.RouteCount = append(peerStatus.RouteCount, v2alpha1.BGPFamilyRouteCount{
   241  					Afi:        af.Afi,
   242  					Safi:       af.Safi,
   243  					Advertised: ptr.To[int32](int32(af.Advertised)),
   244  					Received:   ptr.To[int32](int32(af.Received)),
   245  				})
   246  			}
   247  
   248  			// peer status updated, no need to iterate further
   249  			break
   250  		}
   251  
   252  		res.PeerStatuses = append(res.PeerStatuses, peerStatus)
   253  	}
   254  
   255  	return res, nil
   256  }
   257  
   258  func (r *StatusReconciler) reconcileWithRetry(ctx context.Context) error {
   259  	bo := wait.Backoff{
   260  		Duration: CRDStatusUpdateInterval,
   261  		Factor:   1.2,
   262  		Jitter:   0.5,
   263  		Steps:    10,
   264  	}
   265  
   266  	retryFn := func(ctx context.Context) (bool, error) {
   267  		err := r.reconcileCRDStatus(ctx)
   268  		if err != nil {
   269  			r.Logger.WithError(err).Debug("Failed to update CiliumBGPNodeConfig status")
   270  			return false, nil
   271  		}
   272  		return true, nil
   273  	}
   274  
   275  	return wait.ExponentialBackoffWithContext(ctx, bo, retryFn)
   276  }
   277  
   278  func (r *StatusReconciler) reconcileCRDStatus(ctx context.Context) error {
   279  	r.Lock()
   280  	defer r.Unlock()
   281  
   282  	// Node name is not set yet, on subsequent retries status field will get updated.
   283  	if r.nodeName == "" {
   284  		return nil
   285  	}
   286  
   287  	if r.desiredStatus.DeepEqual(r.runningStatus) {
   288  		return nil
   289  	}
   290  
   291  	statusCpy := r.desiredStatus.DeepCopy()
   292  
   293  	replaceStatus := []k8s.JSONPatch{
   294  		{
   295  			OP:    "replace",
   296  			Path:  "/status",
   297  			Value: statusCpy,
   298  		},
   299  	}
   300  
   301  	createStatusPatch, err := json.Marshal(replaceStatus)
   302  	if err != nil {
   303  		return fmt.Errorf("json.Marshal(%v) failed: %w", replaceStatus, err)
   304  	}
   305  
   306  	client := r.ClientSet.CiliumV2alpha1().CiliumBGPNodeConfigs()
   307  	_, err = client.Patch(ctx, r.nodeName,
   308  		k8s_types.JSONPatchType, createStatusPatch, metav1.PatchOptions{
   309  			FieldManager: r.Name(),
   310  		}, "status")
   311  	if err != nil {
   312  		if k8sErrors.IsNotFound(err) {
   313  			// it is possible that CiliumBGPNodeConfig is deleted, in that case we set running config to
   314  			// empty and return. Desired config will eventually be set to empty by state reconciler.
   315  			r.runningStatus = &v2alpha1.CiliumBGPNodeStatus{}
   316  			return nil
   317  		}
   318  
   319  		return fmt.Errorf("failed to update CRD status: %w", err)
   320  	}
   321  
   322  	r.runningStatus = statusCpy
   323  	r.Logger.WithField(types.BGPNodeConfigLogField, r.nodeName).Debug("Updated resource status")
   324  	return nil
   325  }