Skip to content

Commit f352396

Browse files
authored
fix(collect): add context timeout to registry collector (#1846)
* fix(collect): add context timeout to registry collector * f * f
1 parent 50ada4a commit f352396

File tree

2 files changed

+74
-14
lines changed

2 files changed

+74
-14
lines changed

pkg/collect/registry.go

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func (c *CollectRegistry) Collect(progressChan chan<- interface{}) (CollectorRes
6262
}
6363

6464
for _, image := range c.Collector.Images {
65-
exists, err := imageExists(c.Namespace, c.ClientConfig, c.Collector, image)
65+
exists, err := imageExists(c.Namespace, c.ClientConfig, c.Collector, image, 10*time.Second)
6666
if err != nil {
6767
registryInfo.Images[image] = RegistryImage{
6868
Error: err.Error(),
@@ -90,7 +90,7 @@ func (c *CollectRegistry) Collect(progressChan chan<- interface{}) (CollectorRes
9090
return output, nil
9191
}
9292

93-
func imageExists(namespace string, clientConfig *rest.Config, registryCollector *troubleshootv1beta2.RegistryImages, image string) (bool, error) {
93+
func imageExists(namespace string, clientConfig *rest.Config, registryCollector *troubleshootv1beta2.RegistryImages, image string, deadline time.Duration) (bool, error) {
9494
imageRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", image))
9595
if err != nil {
9696
return false, errors.Wrapf(err, "failed to parse image name %s", image)
@@ -102,28 +102,45 @@ func imageExists(namespace string, clientConfig *rest.Config, registryCollector
102102
return false, errors.Wrap(err, "failed to get auth config")
103103
}
104104

105+
sysCtx := types.SystemContext{
106+
DockerDisableV1Ping: true,
107+
DockerInsecureSkipTLSVerify: types.OptionalBoolTrue,
108+
}
109+
if authConfig != nil {
110+
sysCtx.DockerAuthConfig = &types.DockerAuthConfig{
111+
Username: authConfig.username,
112+
Password: authConfig.password,
113+
}
114+
}
115+
116+
if deadline == 0 {
117+
deadline = 10 * time.Second
118+
}
119+
105120
var lastErr error
106121
for i := 0; i < 3; i++ {
107-
sysCtx := types.SystemContext{
108-
DockerDisableV1Ping: true,
109-
DockerInsecureSkipTLSVerify: types.OptionalBoolTrue,
110-
}
111-
if authConfig != nil {
112-
sysCtx.DockerAuthConfig = &types.DockerAuthConfig{
113-
Username: authConfig.username,
114-
Password: authConfig.password,
122+
err := func() error {
123+
ctx, cancel := context.WithTimeout(context.Background(), deadline)
124+
defer cancel()
125+
remoteImage, err := imageRef.NewImage(ctx, &sysCtx)
126+
if err != nil {
127+
return err
115128
}
116-
}
117-
118-
remoteImage, err := imageRef.NewImage(context.Background(), &sysCtx)
129+
remoteImage.Close()
130+
return nil
131+
}()
119132
if err == nil {
120133
klog.V(2).Infof("image %s exists", image)
121-
remoteImage.Close()
122134
return true, nil
123135
}
124136

125137
klog.Errorf("failed to get image %s: %v", image, err)
126138

139+
// if this is a context timeout, stop here so we dont run this check for too long
140+
if errors.Is(err, context.DeadlineExceeded) {
141+
return false, errors.Wrap(err, "failed to get image manifest")
142+
}
143+
127144
if strings.Contains(err.Error(), "no image found in manifest list for architecture") {
128145
// manifest was downloaded, but no matching architecture found in manifest
129146
// should this count as image does not exist?

pkg/collect/registry_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package collect
22

33
import (
4+
"context"
45
"encoding/base64"
56
"fmt"
7+
"net"
68
"testing"
9+
"time"
710

811
"github.com/containers/image/v5/transports/alltransports"
912
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
1013
"github.com/stretchr/testify/assert"
14+
"k8s.io/client-go/rest"
1115
)
1216

1317
func TestGetImageAuthConfigFromData(t *testing.T) {
@@ -76,3 +80,42 @@ func TestGetImageAuthConfigFromData(t *testing.T) {
7680
})
7781
}
7882
}
83+
84+
func TestImageExists_ContextDeadlineExceeded(t *testing.T) {
85+
// Start a server that accepts connections but doesn't respond
86+
// This will cause the context deadline exceeded error
87+
listener, err := net.Listen("tcp", "127.0.0.1:0")
88+
assert.NoError(t, err)
89+
defer listener.Close()
90+
91+
port := listener.Addr().(*net.TCPAddr).Port
92+
93+
// Start a goroutine that accepts connections but doesn't respond
94+
go func() {
95+
for {
96+
conn, err := listener.Accept()
97+
if err != nil {
98+
return
99+
}
100+
// Keep the connection open but don't respond
101+
// This will cause the client to timeout
102+
time.Sleep(2 * time.Second)
103+
conn.Close()
104+
}
105+
}()
106+
107+
// Create a test registry collector pointing to our test server
108+
registryCollector := &v1beta2.RegistryImages{
109+
Images: []string{fmt.Sprintf("127.0.0.1:%d/test-image:latest", port)},
110+
}
111+
112+
// Create a minimal client config
113+
clientConfig := &rest.Config{}
114+
115+
// Test the imageExists function - this should trigger context deadline exceeded
116+
exists, err := imageExists("default", clientConfig, registryCollector, fmt.Sprintf("127.0.0.1:%d/test-image:latest", port), 100*time.Millisecond)
117+
118+
// Verify the result - should return false without error due to context deadline exceeded handling
119+
assert.ErrorIs(t, err, context.DeadlineExceeded)
120+
assert.False(t, exists)
121+
}

0 commit comments

Comments
 (0)