Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions pkg/controller/route/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"

Expand Down Expand Up @@ -1134,10 +1135,28 @@ func (rc *RouteController) sync(ctx context.Context, key string) error {
route.Spec.TLS.Key = string(certPemData.Key)
route.Spec.TLS.Certificate = string(certPemData.Crt)

// TODO: consider RetryOnConflict with rechecking the managed annotation
_, err = rc.routeClient.RouteV1().Routes(routeReadOnly.Namespace).Update(ctx, route, metav1.UpdateOptions{})
// Use RetryOnConflict to prevent dropping the in-memory private key and
// burning Let's Encrypt rate limits on transient Kubernetes update conflicts.
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
freshRoute, getErr := rc.routeClient.RouteV1().Routes(routeReadOnly.Namespace).Get(ctx, routeReadOnly.Name, metav1.GetOptions{})
if getErr != nil {
return getErr
}

if freshRoute.Spec.TLS == nil {
freshRoute.Spec.TLS = &routev1.TLSConfig{
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
Termination: routev1.TLSTerminationEdge,
}
}
freshRoute.Spec.TLS.Key = string(certPemData.Key)
freshRoute.Spec.TLS.Certificate = string(certPemData.Crt)

_, updateErr := rc.routeClient.RouteV1().Routes(freshRoute.Namespace).Update(ctx, freshRoute, metav1.UpdateOptions{})
return updateErr
})
if err != nil {
return fmt.Errorf("can't update route %s/%s with new certificates: %v", routeReadOnly.Namespace, route.Name, err)
return fmt.Errorf("can't update route %s/%s with new certificates after retries: %v", routeReadOnly.Namespace, routeReadOnly.Name, err)
}

err = rc.CleanupExposerObjects(ctx, routeReadOnly)
Expand All @@ -1149,11 +1168,23 @@ func (rc *RouteController) sync(ctx context.Context, key string) error {
return nil

case acme.StatusValid:
// TODO: fix the golang acme lib
// Unfortunately the golang acme lib actively waits in 'CreateOrderCert'
// so we can't take the appropriate asynchronous action here.
// The logic is included in handling acme.StatusReady
return nil
// We have a Valid order on the ACME server, but we lost the in-memory private key
// needed to attach the certificate (likely due to a previous Route Update conflict).
// We must abandon this orphaned order, trigger a backoff, and start fresh.
klog.Warningf("Route %q: Order %q is Valid but we lack the private key. Restarting order.", key, order.URI)

if status.ProvisioningStatus.OrderStatus != previousOrderStatus {
status.ProvisioningStatus.Failures += 1
}

err = rc.CleanupExposerObjects(ctx, routeReadOnly)
if err != nil {
klog.Errorf("Can't cleanup exposer objects: %v", err)
}

status.ProvisioningStatus.OrderURI = ""
status.ProvisioningStatus.OrderStatus = ""
return rc.updateStatus(ctx, routeReadOnly, status)

case acme.StatusInvalid:
rc.recorder.Eventf(routeReadOnly, corev1.EventTypeWarning, "AcmeFailedOrder", "Order %q for domain %q failed: %v", order.URI, routeReadOnly.Spec.Host, order.Error)
Expand Down
Loading