Skip to content

Commit 608610c

Browse files
authored
Merge pull request #900 from fluxcd/fix-timeout
Fix timeout created more than once
2 parents fbfb0e9 + 1879dcb commit 608610c

5 files changed

Lines changed: 108 additions & 9 deletions

File tree

internal/controller/imagepolicy_controller.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,12 +459,19 @@ func (r *ImagePolicyReconciler) fetchDigest(ctx context.Context,
459459
Namespace: obj.GetNamespace(),
460460
Operation: cache.OperationReconcile,
461461
}
462+
463+
// The timeout must span both building the auth options and the registry
464+
// request, as the authenticator fetches registry credentials lazily during
465+
// the request.
466+
ctx, cancel := context.WithTimeout(ctx, repo.GetTimeout())
467+
defer cancel()
468+
462469
opts, err := r.AuthOptionsGetter.GetOptions(ctx, repo, involvedObject)
463470
if err != nil {
464471
return "", fmt.Errorf("failed to configure authentication options: %w", err)
465472
}
466473

467-
desc, err := remote.Head(tagRef, opts...)
474+
desc, err := remote.Head(tagRef, append(opts, remote.WithContext(ctx))...)
468475
if err != nil {
469476
return "", fmt.Errorf("failed fetching descriptor for %q: %w", tagRef.String(), err)
470477
}

internal/controller/imagepolicy_controller_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,44 @@ func TestImagePolicyReconciler_getImageRepository(t *testing.T) {
829829
}
830830
}
831831

832+
// TestImagePolicyReconciler_fetchDigest_respectsContext ensures fetchDigest
833+
// propagates the context to the registry request. The timeout that wraps the
834+
// digest fetch (and the lazily-fetched registry credentials) is derived from
835+
// this context, so if it is not passed to the request the fetch would run
836+
// unbounded on a background context. With a cancelled context, the fetch must
837+
// fail rather than silently succeed.
838+
func TestImagePolicyReconciler_fetchDigest_respectsContext(t *testing.T) {
839+
g := NewWithT(t)
840+
841+
registryServer := test.NewRegistryServer()
842+
defer registryServer.Close()
843+
844+
imgRepo, _, err := test.LoadImages(registryServer, "foo/bar", []string{"v1.0.0"})
845+
g.Expect(err).ToNot(HaveOccurred())
846+
847+
r := &ImagePolicyReconciler{
848+
EventRecorder: record.NewFakeRecorder(32),
849+
AuthOptionsGetter: &registry.AuthOptionsGetter{Client: fake.NewClientBuilder().Build()},
850+
}
851+
852+
repo := &imagev1.ImageRepository{}
853+
repo.Spec.Image = imgRepo
854+
855+
obj := &imagev1.ImagePolicy{}
856+
obj.Name = "test"
857+
obj.Namespace = "default"
858+
859+
// Cancel the context before fetching. fetchDigest must pass the context to
860+
// the registry request, so the call fails instead of running unbounded on a
861+
// background context.
862+
ctx, cancel := context.WithCancel(context.Background())
863+
cancel()
864+
865+
_, err = r.fetchDigest(ctx, repo, obj, "v1.0.0")
866+
g.Expect(err).To(HaveOccurred())
867+
g.Expect(err.Error()).To(ContainSubstring("context canceled"))
868+
}
869+
832870
func TestImagePolicyReconciler_digestReflection(t *testing.T) {
833871
registryServer := test.NewRegistryServer()
834872
defer registryServer.Close()

internal/controller/imagerepository_controller.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,12 @@ func (r *ImageRepositoryReconciler) reconcile(ctx context.Context, sp *patch.Ser
278278
Namespace: obj.GetNamespace(),
279279
Operation: cache.OperationReconcile,
280280
}
281+
282+
// The timeout must span both building the auth options and the scan, as the
283+
// authenticator fetches registry credentials lazily during the scan.
284+
ctx, cancel := context.WithTimeout(ctx, obj.GetTimeout())
285+
defer cancel()
286+
281287
opts, err := r.AuthOptionsGetter.GetOptions(ctx, obj, involvedObject)
282288
if err != nil {
283289
e := fmt.Errorf("failed to configure authentication options: %w", err)
@@ -418,10 +424,6 @@ func (r *ImageRepositoryReconciler) shouldScan(ctx context.Context, obj imagev1.
418424
// scan performs repository scanning and writes the scanned result in the
419425
// internal database and populates the status of the ImageRepository.
420426
func (r *ImageRepositoryReconciler) scan(ctx context.Context, obj *imagev1.ImageRepository, ref name.Reference, options []remote.Option) error {
421-
timeout := obj.GetTimeout()
422-
ctx, cancel := context.WithTimeout(ctx, timeout)
423-
defer cancel()
424-
425427
options = append(options, remote.WithContext(ctx))
426428

427429
tags, err := remote.List(ref.Context(), options...)

internal/controller/imagerepository_controller_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,62 @@ func TestImageRepositoryReconciler_scan(t *testing.T) {
419419
}
420420
}
421421

422+
// deadlineCapturingRoundTripper records whether the request context carried a
423+
// deadline, then delegates to the wrapped RoundTripper.
424+
type deadlineCapturingRoundTripper struct {
425+
rt http.RoundTripper
426+
sawDeadl *bool
427+
}
428+
429+
func (d *deadlineCapturingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
430+
if _, ok := req.Context().Deadline(); ok {
431+
*d.sawDeadl = true
432+
}
433+
return d.rt.RoundTrip(req)
434+
}
435+
436+
// TestImageRepositoryReconciler_scan_noOwnTimeout ensures scan does not apply a
437+
// timeout of its own. The timeout must be owned by the caller (reconcile) and
438+
// span the whole operation, so that the lazily-fetched registry credentials are
439+
// not cut short by a second, separate timeout. Given a context without a
440+
// deadline, scan must not introduce one.
441+
func TestImageRepositoryReconciler_scan_noOwnTimeout(t *testing.T) {
442+
g := NewWithT(t)
443+
444+
registryServer := test.NewRegistryServer()
445+
defer registryServer.Close()
446+
447+
imgRepo, _, err := test.LoadImages(registryServer, "test-scan-timeout-"+randStringRunes(5), []string{"a"})
448+
g.Expect(err).ToNot(HaveOccurred())
449+
450+
r := ImageRepositoryReconciler{
451+
EventRecorder: record.NewFakeRecorder(32),
452+
Database: &mockDatabase{},
453+
patchOptions: getPatchOptions(imageRepositoryOwnedConditions, "irc"),
454+
}
455+
456+
repo := &imagev1.ImageRepository{}
457+
repo.Spec = imagev1.ImageRepositorySpec{
458+
Image: imgRepo,
459+
Timeout: &metav1.Duration{Duration: time.Hour},
460+
}
461+
462+
ref, err := registry.ParseImageReference(imgRepo, false)
463+
g.Expect(err).ToNot(HaveOccurred())
464+
465+
var sawDeadline bool
466+
opts := []remote.Option{
467+
remote.WithTransport(&deadlineCapturingRoundTripper{rt: http.DefaultTransport, sawDeadl: &sawDeadline}),
468+
}
469+
470+
// Pass a context without a deadline. If scan adds its own timeout, the
471+
// registry request context will carry a deadline.
472+
err = r.scan(context.Background(), repo, ref, opts)
473+
g.Expect(err).ToNot(HaveOccurred())
474+
g.Expect(sawDeadline).To(BeFalse(),
475+
"scan must not apply its own timeout; the timeout is owned by reconcile and spans the whole scan")
476+
}
477+
422478
func TestSortTagsAndGetLatestTags(t *testing.T) {
423479
tests := []struct {
424480
name string

internal/registry/options.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ type AuthOptionsGetter struct {
5353

5454
func (r *AuthOptionsGetter) GetOptions(ctx context.Context, repo *imagev1.ImageRepository,
5555
involvedObject *cache.InvolvedObject) ([]remote.Option, error) {
56-
timeout := repo.GetTimeout()
57-
ctx, cancel := context.WithTimeout(ctx, timeout)
58-
defer cancel()
59-
6056
var transportOptions []func(*http.Transport)
6157

6258
// Load proxy configuration.

0 commit comments

Comments
 (0)