Skip to content

Commit 7fa1379

Browse files
authored
Merge pull request #389 from mhrabovcin/registry-client-idle-timeout
🐛 fix: add registry client TLS connection idle timeout
2 parents 8e795b1 + 6432b3f commit 7fa1379

File tree

3 files changed

+196
-10
lines changed

3 files changed

+196
-10
lines changed

internal/helm_client.go

+61-10
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@ package internal
1818

1919
import (
2020
"context"
21+
"crypto/tls"
22+
"crypto/x509"
2123
"fmt"
24+
"net/http"
2225
"net/url"
2326
"os"
2427
"path"
28+
"time"
2529

2630
"github.com/google/go-cmp/cmp"
2731
"github.com/pkg/errors"
@@ -255,21 +259,68 @@ func (c *HelmClient) InstallHelmRelease(ctx context.Context, restConfig *rest.Co
255259

256260
// newDefaultRegistryClient creates registry client object with default config which can be used to install/upgrade helm charts.
257261
func newDefaultRegistryClient(credentialsPath string, enableCache bool, caFilePath string, insecureSkipTLSVerify bool) (*registry.Client, error) {
258-
if caFilePath == "" && !insecureSkipTLSVerify {
259-
opts := []registry.ClientOption{
260-
registry.ClientOptDebug(true),
261-
registry.ClientOptEnableCache(enableCache),
262-
registry.ClientOptWriter(os.Stderr),
262+
opts := []registry.ClientOption{
263+
registry.ClientOptDebug(true),
264+
registry.ClientOptEnableCache(enableCache),
265+
registry.ClientOptWriter(os.Stderr),
266+
}
267+
if credentialsPath != "" {
268+
// Create a new registry client with credentials
269+
opts = append(opts, registry.ClientOptCredentialsFile(credentialsPath))
270+
}
271+
272+
if caFilePath != "" || insecureSkipTLSVerify {
273+
tlsConf, err := newClientTLS(caFilePath, insecureSkipTLSVerify)
274+
if err != nil {
275+
return nil, fmt.Errorf("can't create TLS config for client: %w", err)
263276
}
264-
if credentialsPath != "" {
265-
// Create a new registry client with credentials
266-
opts = append(opts, registry.ClientOptCredentialsFile(credentialsPath))
277+
opts = append(opts, registry.ClientOptHTTPClient(&http.Client{
278+
Transport: &http.Transport{
279+
TLSClientConfig: tlsConf,
280+
Proxy: http.ProxyFromEnvironment,
281+
// This registry client is not reused and is discarded after a single reconciliation
282+
// loop. Limit how long can be the idle connection open. Otherwise its possible that
283+
// a registry server that keeps the connection open for a long time could result in
284+
// connections being openend in the controller for a long time.
285+
IdleConnTimeout: 1 * time.Second,
286+
},
287+
}),
288+
)
289+
}
290+
291+
return registry.NewClient(opts...)
292+
}
293+
294+
// newClientTLS creates a new TLS config for the registry client based on the provided
295+
// CA file and insecureSkipTLSverify flag.
296+
func newClientTLS(caFile string, insecureSkipTLSverify bool) (*tls.Config, error) {
297+
config := tls.Config{
298+
InsecureSkipVerify: insecureSkipTLSverify,
299+
}
300+
301+
if caFile != "" {
302+
cp, err := certPoolFromFile(caFile)
303+
if err != nil {
304+
return nil, err
267305
}
306+
config.RootCAs = cp
307+
}
268308

269-
return registry.NewClient(opts...)
309+
return &config, nil
310+
}
311+
312+
// certPoolFromFile creates a new CertPool and appends the certificates from the given file.
313+
func certPoolFromFile(filename string) (*x509.CertPool, error) {
314+
b, err := os.ReadFile(filename)
315+
if err != nil {
316+
return nil, errors.Wrapf(err, "can't read CA file: %s", filename)
317+
}
318+
cp := x509.NewCertPool()
319+
if !cp.AppendCertsFromPEM(b) {
320+
return nil, errors.Errorf("failed to append certificates from file: %s", filename)
270321
}
271322

272-
return registry.NewRegistryClientWithTLS(os.Stderr, "", "", caFilePath, insecureSkipTLSVerify, credentialsPath, true)
323+
return cp, nil
273324
}
274325

275326
// getHelmChartAndRepoName returns chartName, repoURL as per the format requirred in helm install/upgrade config.

internal/helm_client_test.go

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package internal
18+
19+
import (
20+
"os"
21+
"path/filepath"
22+
"testing"
23+
24+
. "github.com/onsi/gomega"
25+
"github.com/onsi/gomega/types"
26+
)
27+
28+
func TestNewDefaultRegistryClient(t *testing.T) {
29+
t.Parallel()
30+
31+
emptyFilepath := filepath.Join(t.TempDir(), "empty-file")
32+
NewWithT(t).Expect(os.Create(emptyFilepath)).Error().ShouldNot(HaveOccurred())
33+
34+
testCases := []struct {
35+
name string
36+
credentialsPath string
37+
enableCache bool
38+
caFilePath string
39+
insecureSkipTLSVerify bool
40+
assertErr types.GomegaMatcher
41+
assertClient types.GomegaMatcher
42+
}{
43+
{
44+
name: "default config",
45+
credentialsPath: "",
46+
enableCache: true,
47+
caFilePath: "",
48+
insecureSkipTLSVerify: false,
49+
assertErr: Not(HaveOccurred()),
50+
assertClient: Not(BeNil()),
51+
},
52+
{
53+
name: "skip tls verification",
54+
credentialsPath: "",
55+
enableCache: true,
56+
caFilePath: "",
57+
insecureSkipTLSVerify: true,
58+
assertErr: Not(HaveOccurred()),
59+
assertClient: Not(BeNil()),
60+
},
61+
{
62+
name: "invalid ca bundle path",
63+
credentialsPath: "",
64+
enableCache: true,
65+
caFilePath: filepath.Join(t.TempDir(), "non-existing"),
66+
insecureSkipTLSVerify: false,
67+
assertErr: MatchError(ContainSubstring("can't create TLS config")),
68+
assertClient: BeNil(),
69+
},
70+
{
71+
name: "empty ca bundle file",
72+
credentialsPath: "",
73+
enableCache: true,
74+
caFilePath: emptyFilepath,
75+
insecureSkipTLSVerify: false,
76+
assertErr: MatchError(ContainSubstring("failed to append certificates from file")),
77+
assertClient: BeNil(),
78+
},
79+
{
80+
name: "valid ca bundle",
81+
credentialsPath: "",
82+
enableCache: true,
83+
caFilePath: "testdata/ca-bundle.pem",
84+
insecureSkipTLSVerify: false,
85+
assertErr: Not(HaveOccurred()),
86+
assertClient: Not(BeNil()),
87+
},
88+
}
89+
90+
for _, tc := range testCases {
91+
t.Run(tc.name, func(t *testing.T) {
92+
t.Parallel()
93+
g := NewWithT(t)
94+
client, err := newDefaultRegistryClient(
95+
tc.credentialsPath,
96+
tc.enableCache,
97+
tc.caFilePath,
98+
tc.insecureSkipTLSVerify,
99+
)
100+
g.Expect(err).To(tc.assertErr)
101+
g.Expect(client).To(tc.assertClient)
102+
})
103+
}
104+
}

internal/testdata/ca-bundle.pem

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)