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 }