github.com/cilium/cilium@v1.16.2/operator/pkg/gateway-api/gateway_reconcile.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package gateway_api 5 6 import ( 7 "context" 8 "encoding/pem" 9 "fmt" 10 11 "github.com/google/go-cmp/cmp" 12 "github.com/google/go-cmp/cmp/cmpopts" 13 "github.com/sirupsen/logrus" 14 corev1 "k8s.io/api/core/v1" 15 k8serrors "k8s.io/apimachinery/pkg/api/errors" 16 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 ctrl "sigs.k8s.io/controller-runtime" 18 "sigs.k8s.io/controller-runtime/pkg/client" 19 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 20 gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" 21 gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 22 gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" 23 mcsapiv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 24 25 controllerruntime "github.com/cilium/cilium/operator/pkg/controller-runtime" 26 "github.com/cilium/cilium/operator/pkg/gateway-api/helpers" 27 "github.com/cilium/cilium/operator/pkg/gateway-api/routechecks" 28 "github.com/cilium/cilium/operator/pkg/model" 29 "github.com/cilium/cilium/operator/pkg/model/ingestion" 30 ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" 31 "github.com/cilium/cilium/pkg/logging/logfields" 32 ) 33 34 // Reconcile is part of the main kubernetes reconciliation loop which aims to 35 // move the current state of the cluster closer to the desired state. 36 // 37 // For more details, check Reconcile and its Result here: 38 // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile 39 func (r *gatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 40 scopedLog := log.WithContext(ctx).WithFields(logrus.Fields{ 41 logfields.Controller: gateway, 42 logfields.Resource: req.NamespacedName, 43 }) 44 scopedLog.Info("Reconciling Gateway") 45 46 // Step 1: Retrieve the Gateway 47 original := &gatewayv1.Gateway{} 48 if err := r.Client.Get(ctx, req.NamespacedName, original); err != nil { 49 if k8serrors.IsNotFound(err) { 50 return controllerruntime.Success() 51 } 52 scopedLog.WithError(err).Error("Unable to get Gateway") 53 return controllerruntime.Fail(err) 54 } 55 56 // Ignore deleting Gateway, this can happen when foregroundDeletion is enabled 57 // The reconciliation loop will automatically kick off for related Gateway resources. 58 if original.GetDeletionTimestamp() != nil { 59 scopedLog.Info("Gateway is being deleted, doing nothing") 60 return controllerruntime.Success() 61 } 62 63 gw := original.DeepCopy() 64 65 // Step 2: Gather all required information for the ingestion model 66 gwc := &gatewayv1.GatewayClass{} 67 if err := r.Client.Get(ctx, client.ObjectKey{Name: string(gw.Spec.GatewayClassName)}, gwc); err != nil { 68 scopedLog.WithField(gatewayClass, gw.Spec.GatewayClassName). 69 WithError(err). 70 Error("Unable to get GatewayClass") 71 // Doing nothing till the GatewayClass is available and matching controller name 72 return controllerruntime.Success() 73 } 74 75 if string(gwc.Spec.ControllerName) != controllerName { 76 scopedLog.Debug("GatewayClass does not have matching controller name, doing nothing") 77 return controllerruntime.Success() 78 } 79 80 httpRouteList := &gatewayv1.HTTPRouteList{} 81 if err := r.Client.List(ctx, httpRouteList); err != nil { 82 scopedLog.WithError(err).Error("Unable to list HTTPRoutes") 83 return r.handleReconcileErrorWithStatus(ctx, err, original, gw) 84 } 85 86 grpcRouteList := &gatewayv1.GRPCRouteList{} 87 if err := r.Client.List(ctx, grpcRouteList); err != nil { 88 scopedLog.WithError(err).Error("Unable to list GRPCRoutes") 89 return r.handleReconcileErrorWithStatus(ctx, err, original, gw) 90 } 91 92 tlsRouteList := &gatewayv1alpha2.TLSRouteList{} 93 if err := r.Client.List(ctx, tlsRouteList); err != nil { 94 scopedLog.WithError(err).Error("Unable to list TLSRoutes") 95 return r.handleReconcileErrorWithStatus(ctx, err, original, gw) 96 } 97 98 // TODO(tam): Only list the services / ServiceImports used by accepted Routes 99 servicesList := &corev1.ServiceList{} 100 if err := r.Client.List(ctx, servicesList); err != nil { 101 scopedLog.WithError(err).Error("Unable to list Services") 102 return r.handleReconcileErrorWithStatus(ctx, err, original, gw) 103 } 104 105 serviceImportsList := &mcsapiv1alpha1.ServiceImportList{} 106 if helpers.HasServiceImportSupport(r.Client.Scheme()) { 107 if err := r.Client.List(ctx, serviceImportsList); err != nil { 108 scopedLog.WithError(err).Error("Unable to list ServiceImports") 109 return controllerruntime.Fail(err) 110 } 111 } 112 113 grants := &gatewayv1beta1.ReferenceGrantList{} 114 if err := r.Client.List(ctx, grants); err != nil { 115 scopedLog.WithError(err).Error("Unable to list ReferenceGrants") 116 return r.handleReconcileErrorWithStatus(ctx, err, original, gw) 117 } 118 119 httpListeners, tlsPassthroughListeners := ingestion.GatewayAPI(ingestion.Input{ 120 GatewayClass: *gwc, 121 Gateway: *gw, 122 HTTPRoutes: r.filterHTTPRoutesByGateway(ctx, gw, httpRouteList.Items), 123 TLSRoutes: r.filterTLSRoutesByGateway(ctx, gw, tlsRouteList.Items), 124 GRPCRoutes: r.filterGRPCRoutesByGateway(ctx, gw, grpcRouteList.Items), 125 Services: servicesList.Items, 126 ServiceImports: serviceImportsList.Items, 127 ReferenceGrants: grants.Items, 128 }) 129 130 if err := r.setListenerStatus(ctx, gw, httpRouteList, tlsRouteList); err != nil { 131 scopedLog.WithError(err).Error("Unable to set listener status") 132 setGatewayAccepted(gw, false, "Unable to set listener status") 133 return r.handleReconcileErrorWithStatus(ctx, err, original, gw) 134 } 135 setGatewayAccepted(gw, true, "Gateway successfully scheduled") 136 137 // Step 3: Translate the listeners into Cilium model 138 cec, svc, ep, err := r.translator.Translate(&model.Model{HTTP: httpListeners, TLSPassthrough: tlsPassthroughListeners}) 139 if err != nil { 140 scopedLog.WithError(err).Error("Unable to translate resources") 141 setGatewayAccepted(gw, false, "Unable to translate resources") 142 return r.handleReconcileErrorWithStatus(ctx, err, original, gw) 143 } 144 145 if err = r.ensureService(ctx, svc); err != nil { 146 scopedLog.WithError(err).Error("Unable to create Service") 147 setGatewayAccepted(gw, false, "Unable to create Service resource") 148 return r.handleReconcileErrorWithStatus(ctx, err, original, gw) 149 } 150 151 if err = r.ensureEndpoints(ctx, ep); err != nil { 152 scopedLog.WithError(err).Error("Unable to ensure Endpoints") 153 setGatewayAccepted(gw, false, "Unable to ensure Endpoints resource") 154 return r.handleReconcileErrorWithStatus(ctx, err, original, gw) 155 } 156 157 if err = r.ensureEnvoyConfig(ctx, cec); err != nil { 158 scopedLog.WithError(err).Error("Unable to ensure CiliumEnvoyConfig") 159 setGatewayAccepted(gw, false, "Unable to ensure CEC resource") 160 return r.handleReconcileErrorWithStatus(ctx, err, original, gw) 161 } 162 163 // Step 4: Update the status of the Gateway 164 if err = r.setAddressStatus(ctx, gw); err != nil { 165 scopedLog.WithError(err).Error("Address is not ready") 166 setGatewayProgrammed(gw, false, "Address is not ready") 167 return r.handleReconcileErrorWithStatus(ctx, err, original, gw) 168 } 169 170 setGatewayProgrammed(gw, true, "Gateway successfully reconciled") 171 if err := r.updateStatus(ctx, original, gw); err != nil { 172 return ctrl.Result{}, fmt.Errorf("failed to update Gateway status: %w", err) 173 } 174 175 scopedLog.Info("Successfully reconciled Gateway") 176 return controllerruntime.Success() 177 } 178 179 func (r *gatewayReconciler) ensureService(ctx context.Context, desired *corev1.Service) error { 180 svc := desired.DeepCopy() 181 _, err := controllerutil.CreateOrPatch(ctx, r.Client, svc, func() error { 182 // Save and restore loadBalancerClass 183 // e.g. if a mutating webhook writes this field 184 lbClass := svc.Spec.LoadBalancerClass 185 186 svc.Spec = desired.Spec 187 svc.OwnerReferences = desired.OwnerReferences 188 setMergedLabelsAndAnnotations(svc, desired) 189 190 // Ignore the loadBalancerClass if it was set by a mutating webhook 191 svc.Spec.LoadBalancerClass = lbClass 192 return nil 193 }) 194 return err 195 } 196 197 func (r *gatewayReconciler) ensureEndpoints(ctx context.Context, desired *corev1.Endpoints) error { 198 ep := desired.DeepCopy() 199 _, err := controllerutil.CreateOrPatch(ctx, r.Client, ep, func() error { 200 ep.Subsets = desired.Subsets 201 ep.OwnerReferences = desired.OwnerReferences 202 setMergedLabelsAndAnnotations(ep, desired) 203 return nil 204 }) 205 return err 206 } 207 208 func (r *gatewayReconciler) ensureEnvoyConfig(ctx context.Context, desired *ciliumv2.CiliumEnvoyConfig) error { 209 cec := desired.DeepCopy() 210 _, err := controllerutil.CreateOrPatch(ctx, r.Client, cec, func() error { 211 cec.Spec = desired.Spec 212 setMergedLabelsAndAnnotations(cec, desired) 213 return nil 214 }) 215 return err 216 } 217 218 func (r *gatewayReconciler) updateStatus(ctx context.Context, original *gatewayv1.Gateway, new *gatewayv1.Gateway) error { 219 oldStatus := original.Status.DeepCopy() 220 newStatus := new.Status.DeepCopy() 221 222 if cmp.Equal(oldStatus, newStatus, cmpopts.IgnoreFields(metav1.Condition{}, lastTransitionTime)) { 223 return nil 224 } 225 return r.Client.Status().Update(ctx, new) 226 } 227 228 func (r *gatewayReconciler) filterHTTPRoutesByGateway(ctx context.Context, gw *gatewayv1.Gateway, routes []gatewayv1.HTTPRoute) []gatewayv1.HTTPRoute { 229 var filtered []gatewayv1.HTTPRoute 230 allListenerHostNames := routechecks.GetAllListenerHostNames(gw.Spec.Listeners) 231 for _, route := range routes { 232 if isAttachable(ctx, gw, &route, route.Status.Parents) && isAllowed(ctx, r.Client, gw, &route) && len(computeHosts(gw, route.Spec.Hostnames, allListenerHostNames)) > 0 { 233 filtered = append(filtered, route) 234 } 235 } 236 return filtered 237 } 238 239 func (r *gatewayReconciler) filterGRPCRoutesByGateway(ctx context.Context, gw *gatewayv1.Gateway, routes []gatewayv1.GRPCRoute) []gatewayv1.GRPCRoute { 240 var filtered []gatewayv1.GRPCRoute 241 allListenerHostNames := routechecks.GetAllListenerHostNames(gw.Spec.Listeners) 242 243 for _, route := range routes { 244 if isAttachable(ctx, gw, &route, route.Status.Parents) && isAllowed(ctx, r.Client, gw, &route) && len(computeHosts(gw, route.Spec.Hostnames, allListenerHostNames)) > 0 { 245 filtered = append(filtered, route) 246 } 247 } 248 return filtered 249 } 250 251 func (r *gatewayReconciler) filterHTTPRoutesByListener(ctx context.Context, gw *gatewayv1.Gateway, listener *gatewayv1.Listener, routes []gatewayv1.HTTPRoute) []gatewayv1.HTTPRoute { 252 var filtered []gatewayv1.HTTPRoute 253 for _, route := range routes { 254 if isAttachable(ctx, gw, &route, route.Status.Parents) && 255 isAllowed(ctx, r.Client, gw, &route) && 256 len(computeHostsForListener(listener, route.Spec.Hostnames, nil)) > 0 && 257 parentRefMatched(gw, listener, route.GetNamespace(), route.Spec.ParentRefs) { 258 filtered = append(filtered, route) 259 } 260 } 261 return filtered 262 } 263 264 func parentRefMatched(gw *gatewayv1.Gateway, listener *gatewayv1.Listener, routeNamespace string, refs []gatewayv1.ParentReference) bool { 265 for _, ref := range refs { 266 if string(ref.Name) == gw.GetName() && gw.GetNamespace() == helpers.NamespaceDerefOr(ref.Namespace, routeNamespace) { 267 if ref.SectionName == nil && ref.Port == nil { 268 return true 269 } 270 sectionNameCheck := ref.SectionName == nil || *ref.SectionName == listener.Name 271 portCheck := ref.Port == nil || *ref.Port == listener.Port 272 if sectionNameCheck && portCheck { 273 return true 274 } 275 } 276 } 277 return false 278 } 279 280 func (r *gatewayReconciler) filterTLSRoutesByGateway(ctx context.Context, gw *gatewayv1.Gateway, routes []gatewayv1alpha2.TLSRoute) []gatewayv1alpha2.TLSRoute { 281 var filtered []gatewayv1alpha2.TLSRoute 282 for _, route := range routes { 283 if isAttachable(ctx, gw, &route, route.Status.Parents) && isAllowed(ctx, r.Client, gw, &route) && 284 len(computeHosts(gw, route.Spec.Hostnames, nil)) > 0 { 285 filtered = append(filtered, route) 286 } 287 } 288 return filtered 289 } 290 291 func (r *gatewayReconciler) filterTLSRoutesByListener(ctx context.Context, gw *gatewayv1.Gateway, listener *gatewayv1.Listener, routes []gatewayv1alpha2.TLSRoute) []gatewayv1alpha2.TLSRoute { 292 var filtered []gatewayv1alpha2.TLSRoute 293 for _, route := range routes { 294 if isAttachable(ctx, gw, &route, route.Status.Parents) && 295 isAllowed(ctx, r.Client, gw, &route) && 296 len(computeHostsForListener(listener, route.Spec.Hostnames, nil)) > 0 && 297 parentRefMatched(gw, listener, route.GetNamespace(), route.Spec.ParentRefs) { 298 filtered = append(filtered, route) 299 } 300 } 301 return filtered 302 } 303 304 func isAttachable(_ context.Context, gw *gatewayv1.Gateway, route metav1.Object, parents []gatewayv1.RouteParentStatus) bool { 305 for _, rps := range parents { 306 if helpers.NamespaceDerefOr(rps.ParentRef.Namespace, route.GetNamespace()) != gw.GetNamespace() || 307 string(rps.ParentRef.Name) != gw.GetName() { 308 continue 309 } 310 311 for _, cond := range rps.Conditions { 312 if cond.Type == string(gatewayv1.RouteConditionAccepted) && cond.Status == metav1.ConditionTrue { 313 return true 314 } 315 316 if cond.Type == string(gatewayv1.RouteConditionResolvedRefs) && cond.Status == metav1.ConditionFalse { 317 return true 318 } 319 } 320 } 321 return false 322 } 323 324 func (r *gatewayReconciler) setAddressStatus(ctx context.Context, gw *gatewayv1.Gateway) error { 325 svcList := &corev1.ServiceList{} 326 if err := r.Client.List(ctx, svcList, client.MatchingLabels{ 327 owningGatewayLabel: model.Shorten(gw.GetName()), 328 }, client.InNamespace(gw.GetNamespace())); err != nil { 329 return err 330 } 331 332 if len(svcList.Items) == 0 { 333 return fmt.Errorf("no service found") 334 } 335 336 svc := svcList.Items[0] 337 if len(svc.Status.LoadBalancer.Ingress) == 0 { 338 // Potential loadbalancer service isn't ready yet. No need to report as an error, because 339 // reconciliation should be triggered when the loadbalancer services gets updated. 340 return nil 341 } 342 343 var addresses []gatewayv1.GatewayStatusAddress 344 for _, s := range svc.Status.LoadBalancer.Ingress { 345 if len(s.IP) != 0 { 346 addresses = append(addresses, gatewayv1.GatewayStatusAddress{ 347 Type: GatewayAddressTypePtr(gatewayv1.IPAddressType), 348 Value: s.IP, 349 }) 350 } 351 if len(s.Hostname) != 0 { 352 addresses = append(addresses, gatewayv1.GatewayStatusAddress{ 353 Type: GatewayAddressTypePtr(gatewayv1.HostnameAddressType), 354 Value: s.Hostname, 355 }) 356 } 357 } 358 359 gw.Status.Addresses = addresses 360 return nil 361 } 362 363 func (r *gatewayReconciler) setListenerStatus(ctx context.Context, gw *gatewayv1.Gateway, httpRoutes *gatewayv1.HTTPRouteList, tlsRoutes *gatewayv1alpha2.TLSRouteList) error { 364 grants := &gatewayv1beta1.ReferenceGrantList{} 365 if err := r.Client.List(ctx, grants); err != nil { 366 return fmt.Errorf("failed to retrieve reference grants: %w", err) 367 } 368 369 for _, l := range gw.Spec.Listeners { 370 isValid := true 371 372 // SupportedKinds is a required field, so we can't declare it as nil. 373 supportedKinds := []gatewayv1.RouteGroupKind{} 374 invalidRouteKinds := false 375 protoGroup, protoKind := getSupportedGroupKind(l.Protocol) 376 377 if l.AllowedRoutes != nil && len(l.AllowedRoutes.Kinds) != 0 { 378 for _, k := range l.AllowedRoutes.Kinds { 379 if groupDerefOr(k.Group, gatewayv1.GroupName) == string(*protoGroup) && 380 k.Kind == protoKind { 381 supportedKinds = append(supportedKinds, k) 382 } else { 383 invalidRouteKinds = true 384 } 385 } 386 } else { 387 g, k := getSupportedGroupKind(l.Protocol) 388 supportedKinds = []gatewayv1.RouteGroupKind{ 389 { 390 Group: g, 391 Kind: k, 392 }, 393 } 394 } 395 var conds []metav1.Condition 396 if invalidRouteKinds { 397 conds = append(conds, gatewayListenerInvalidRouteKinds(gw, "Invalid Route Kinds")) 398 isValid = false 399 } else { 400 conds = append(conds, gatewayListenerProgrammedCondition(gw, true, "Listener Programmed")) 401 conds = append(conds, gatewayListenerAcceptedCondition(gw, true, "Listener Accepted")) 402 conds = append(conds, metav1.Condition{ 403 Type: string(gatewayv1.ListenerConditionResolvedRefs), 404 Status: metav1.ConditionTrue, 405 Reason: string(gatewayv1.ListenerReasonResolvedRefs), 406 Message: "Resolved Refs", 407 LastTransitionTime: metav1.Now(), 408 }) 409 } 410 411 if l.TLS != nil { 412 for _, cert := range l.TLS.CertificateRefs { 413 if !helpers.IsSecret(cert) { 414 conds = merge(conds, metav1.Condition{ 415 Type: string(gatewayv1.ListenerConditionResolvedRefs), 416 Status: metav1.ConditionFalse, 417 Reason: string(gatewayv1.ListenerReasonInvalidCertificateRef), 418 Message: "Invalid CertificateRef", 419 LastTransitionTime: metav1.Now(), 420 }) 421 isValid = false 422 break 423 } 424 425 if !helpers.IsSecretReferenceAllowed(gw.Namespace, cert, gatewayv1.SchemeGroupVersion.WithKind("Gateway"), grants.Items) { 426 conds = merge(conds, metav1.Condition{ 427 Type: string(gatewayv1.ListenerConditionResolvedRefs), 428 Status: metav1.ConditionFalse, 429 Reason: string(gatewayv1.ListenerReasonRefNotPermitted), 430 Message: "CertificateRef is not permitted", 431 LastTransitionTime: metav1.Now(), 432 }) 433 isValid = false 434 break 435 } 436 437 if err := validateTLSSecret(ctx, r.Client, helpers.NamespaceDerefOr(cert.Namespace, gw.GetNamespace()), string(cert.Name)); err != nil { 438 conds = merge(conds, metav1.Condition{ 439 Type: string(gatewayv1.ListenerConditionResolvedRefs), 440 Status: metav1.ConditionFalse, 441 Reason: string(gatewayv1.ListenerReasonInvalidCertificateRef), 442 Message: "Invalid CertificateRef", 443 LastTransitionTime: metav1.Now(), 444 }) 445 isValid = false 446 break 447 } 448 } 449 } 450 451 if !isValid { 452 conds = merge(conds, metav1.Condition{ 453 Type: string(gatewayv1.ListenerConditionProgrammed), 454 Status: metav1.ConditionFalse, 455 Reason: string(gatewayv1.ListenerReasonInvalid), 456 Message: "Invalid CertificateRef", 457 LastTransitionTime: metav1.Now(), 458 }) 459 } 460 461 var attachedRoutes int32 462 attachedRoutes += int32(len(r.filterHTTPRoutesByListener(ctx, gw, &l, httpRoutes.Items))) 463 attachedRoutes += int32(len(r.filterTLSRoutesByListener(ctx, gw, &l, tlsRoutes.Items))) 464 465 found := false 466 for i := range gw.Status.Listeners { 467 if l.Name == gw.Status.Listeners[i].Name { 468 found = true 469 gw.Status.Listeners[i].SupportedKinds = supportedKinds 470 gw.Status.Listeners[i].Conditions = conds 471 gw.Status.Listeners[i].AttachedRoutes = attachedRoutes 472 break 473 } 474 } 475 if !found { 476 gw.Status.Listeners = append(gw.Status.Listeners, gatewayv1.ListenerStatus{ 477 Name: l.Name, 478 SupportedKinds: supportedKinds, 479 Conditions: conds, 480 AttachedRoutes: attachedRoutes, 481 }) 482 } 483 } 484 485 // filter listener status to only have active listeners 486 var newListenersStatus []gatewayv1.ListenerStatus 487 for _, ls := range gw.Status.Listeners { 488 for _, l := range gw.Spec.Listeners { 489 if ls.Name == l.Name { 490 newListenersStatus = append(newListenersStatus, ls) 491 break 492 } 493 } 494 } 495 gw.Status.Listeners = newListenersStatus 496 return nil 497 } 498 499 func validateTLSSecret(ctx context.Context, c client.Client, namespace, name string) error { 500 secret := &corev1.Secret{} 501 if err := c.Get(ctx, client.ObjectKey{ 502 Namespace: namespace, 503 Name: name, 504 }, secret); err != nil { 505 return err 506 } 507 508 if !isValidPemFormat(secret.Data[corev1.TLSCertKey]) { 509 return fmt.Errorf("invalid certificate") 510 } 511 512 if !isValidPemFormat(secret.Data[corev1.TLSPrivateKeyKey]) { 513 return fmt.Errorf("invalid private key") 514 } 515 return nil 516 } 517 518 // isValidPemFormat checks if the given byte array is a valid PEM format. 519 // This function is not intended to be used for validating the actual 520 // content of the PEM block. 521 func isValidPemFormat(b []byte) bool { 522 if len(b) == 0 { 523 return false 524 } 525 526 p, rest := pem.Decode(b) 527 if p == nil { 528 return false 529 } 530 if len(rest) == 0 { 531 return true 532 } 533 return isValidPemFormat(rest) 534 } 535 536 func (r *gatewayReconciler) handleReconcileErrorWithStatus(ctx context.Context, reconcileErr error, original *gatewayv1.Gateway, modified *gatewayv1.Gateway) (ctrl.Result, error) { 537 if err := r.updateStatus(ctx, original, modified); err != nil { 538 return controllerruntime.Fail(fmt.Errorf("failed to update Gateway status while handling the reconcile error: %w: %w", reconcileErr, err)) 539 } 540 541 return controllerruntime.Fail(reconcileErr) 542 }