Skip to content

Commit 50e248c

Browse files
committed
fix: floor positive sub-seconds to 1s
1 parent 0421b9a commit 50e248c

2 files changed

Lines changed: 72 additions & 2 deletions

File tree

internal/controller/domain_controller.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,12 @@ func (r *DomainReconciler) Reconcile(ctx context.Context, req mcreconcile.Reques
150150
if wake != nil {
151151
// If the wake time is in the future, schedule a requeue after the remaining duration.
152152
if wake.After(now) {
153-
return ctrl.Result{RequeueAfter: wake.Sub(now).Truncate(time.Second)}, nil
153+
remaining := wake.Sub(now)
154+
// Floor sub-second values to a minimum of 1s to avoid a zero requeue
155+
if remaining < time.Second {
156+
return ctrl.Result{RequeueAfter: 1 * time.Second}, nil
157+
}
158+
return ctrl.Result{RequeueAfter: remaining.Truncate(time.Second)}, nil
154159
}
155160
// If the wake time is now or already in the past, use a minimal positive RequeueAfter
156161
// to avoid the deprecated Requeue flag.
@@ -216,13 +221,18 @@ func (r *DomainReconciler) reconcileVerification(ctx context.Context, domain *ne
216221
},
217222
}
218223

224+
// Schedule the first verification attempt immediately so the controller
225+
// will requeue and begin verification without waiting for an external trigger.
226+
domainStatus.Verification.NextVerificationAttempt = metav1.NewTime(r.timeNow())
227+
nextAttempt = domainStatus.Verification.NextVerificationAttempt.Time
228+
219229
verifiedCondition.Message = "Update your DNS provider with record defined in `status.verification.dnsRecord`, " +
220230
"or HTTP server with token defined in `status.verification.httpToken`."
221231
verifiedDNSCondition.Message = "Update your DNS provider with record defined in `status.verification.dnsRecord`."
222232
verifiedHTTPCondition.Message = "Update your HTTP server with token defined in `status.verification.httpToken`."
223233
} else {
224234
// If we're not yet due, short-circuit
225-
if remaining := domainStatus.Verification.NextVerificationAttempt.Sub(r.timeNow()).Truncate(time.Second); remaining > 0 {
235+
if remaining := domainStatus.Verification.NextVerificationAttempt.Sub(r.timeNow()); remaining > 0 {
226236
logger.Info("not attempting another validation until remaining time elapsed", "remaining", remaining)
227237
nextAttempt = r.timeNow().Add(remaining)
228238
} else {

internal/controller/domain_controller_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,66 @@ func TestVerification_RequeueImmediate_WhenWakeInPast(t *testing.T) {
677677
assert.Equal(t, 1*time.Second, res.RequeueAfter)
678678
}
679679

680+
func TestVerification_RequeueFloorsSubSecondToOneSecond(t *testing.T) {
681+
s := runtime.NewScheme()
682+
_ = scheme.AddToScheme(s)
683+
_ = networkingv1alpha.AddToScheme(s)
684+
685+
// Fixed reference time
686+
now := time.Date(2025, 10, 22, 15, 40, 14, 0, time.UTC)
687+
688+
// Domain has verification with next attempt 500ms in the future
689+
dom := newDomain("default", "due-subsecond", func(d *networkingv1alpha.Domain) {
690+
d.Status.Verification = &networkingv1alpha.DomainVerificationStatus{
691+
DNSRecord: networkingv1alpha.DNSVerificationRecord{
692+
Name: "_dnsverify.example.com",
693+
Type: "TXT",
694+
Content: "token",
695+
},
696+
NextVerificationAttempt: metav1.Time{Time: now.Add(500 * time.Millisecond)},
697+
}
698+
})
699+
700+
cl := fake.NewClientBuilder().WithScheme(s).WithObjects(dom).WithStatusSubresource(dom).Build()
701+
mgr := &fakeMockManager{cl: cl}
702+
703+
operatorConfig := config.NetworkServicesOperator{
704+
DomainVerification: config.DomainVerificationConfig{
705+
RetryIntervals: []config.RetryInterval{{Interval: metav1.Duration{Duration: time.Second}}},
706+
RetryJitterMaxFactor: 0,
707+
},
708+
DomainRegistration: config.DomainRegistrationConfig{
709+
LookupTimeout: &metav1.Duration{Duration: 3 * time.Second},
710+
RefreshInterval: &metav1.Duration{Duration: time.Hour},
711+
JitterMaxFactor: 0.1,
712+
RetryBackoff: &metav1.Duration{Duration: time.Minute},
713+
},
714+
}
715+
716+
r := &DomainReconciler{
717+
mgr: mgr,
718+
Config: operatorConfig,
719+
timeNow: func() time.Time { return now },
720+
// Stubs to avoid real I/O
721+
httpGet: func(ctx context.Context, url string) ([]byte, *http.Response, error) {
722+
return nil, nil, fmt.Errorf("not implemented")
723+
},
724+
lookupTXT: func(ctx context.Context, name string) ([]string, error) { return nil, &net.DNSError{IsNotFound: true} },
725+
rdapDo: func(ctx context.Context, req *rdap.Request) (*rdap.Response, error) {
726+
return &rdap.Response{Object: &rdap.Domain{LDHName: "example.com"}}, nil
727+
},
728+
lookupNS: func(ctx context.Context, name string) ([]*net.NS, error) { return nil, &net.DNSError{IsNotFound: true} },
729+
lookupIP: func(ctx context.Context, name string) ([]net.IPAddr, error) { return nil, nil },
730+
rdapQueryIP: func(ctx context.Context, ip string) (*rdap.IPNetwork, error) { return nil, nil },
731+
}
732+
733+
res, err := r.Reconcile(context.Background(), mcreconcile.Request{ClusterName: "test", Request: reconcile.Request{NamespacedName: client.ObjectKeyFromObject(dom)}})
734+
assert.NoError(t, err)
735+
736+
// We should schedule a 1s requeue minimum for sub-second remaining
737+
assert.Equal(t, 1*time.Second, res.RequeueAfter)
738+
}
739+
680740
func TestRegistration_Subdomain_DelegationOverridesApexNS(t *testing.T) {
681741
s := runtime.NewScheme()
682742
_ = scheme.AddToScheme(s)

0 commit comments

Comments
 (0)