1
16
17 package registry
18
19 import (
20 "bytes"
21 "context"
22 "crypto/tls"
23 "fmt"
24 "io"
25 "net"
26 "net/http"
27 "net/http/httptest"
28 "net/url"
29 "os"
30 "path/filepath"
31 "strings"
32 "time"
33
34 "github.com/distribution/distribution/v3/configuration"
35 "github.com/distribution/distribution/v3/registry"
36 _ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
37 _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
38 "github.com/foxcpp/go-mockdns"
39 "github.com/phayes/freeport"
40 "github.com/stretchr/testify/suite"
41 "golang.org/x/crypto/bcrypt"
42
43 "helm.sh/helm/v3/internal/tlsutil"
44 )
45
46 const (
47 tlsServerKey = "./testdata/tls/server.key"
48 tlsServerCert = "./testdata/tls/server.crt"
49 tlsCA = "./testdata/tls/ca.crt"
50 tlsKey = "./testdata/tls/client.key"
51 tlsCert = "./testdata/tls/client.crt"
52 )
53
54 var (
55 testWorkspaceDir = "helm-registry-test"
56 testHtpasswdFileBasename = "authtest.htpasswd"
57 testUsername = "myuser"
58 testPassword = "mypass"
59 )
60
61 type TestSuite struct {
62 suite.Suite
63 Out io.Writer
64 DockerRegistryHost string
65 CompromisedRegistryHost string
66 WorkspaceDir string
67 RegistryClient *Client
68
69
70 srv *mockdns.Server
71 }
72
73 func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry {
74 suite.WorkspaceDir = testWorkspaceDir
75 os.RemoveAll(suite.WorkspaceDir)
76 os.Mkdir(suite.WorkspaceDir, 0700)
77
78 var (
79 out bytes.Buffer
80 err error
81 )
82 suite.Out = &out
83 credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename)
84
85
86 opts := []ClientOption{
87 ClientOptDebug(true),
88 ClientOptEnableCache(true),
89 ClientOptWriter(suite.Out),
90 ClientOptCredentialsFile(credentialsFile),
91 ClientOptResolver(nil),
92 }
93
94 if tlsEnabled {
95 var tlsConf *tls.Config
96 if insecure {
97 tlsConf, err = tlsutil.NewClientTLS("", "", "", true)
98 } else {
99 tlsConf, err = tlsutil.NewClientTLS(tlsCert, tlsKey, tlsCA, false)
100 }
101 httpClient := &http.Client{
102 Transport: &http.Transport{
103 TLSClientConfig: tlsConf,
104 },
105 }
106 suite.Nil(err, "no error loading tls config")
107 opts = append(opts, ClientOptHTTPClient(httpClient))
108 } else {
109 opts = append(opts, ClientOptPlainHTTP())
110 }
111
112 suite.RegistryClient, err = NewClient(opts...)
113 suite.Nil(err, "no error creating registry client")
114
115
116 pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost)
117 suite.Nil(err, "no error generating bcrypt password for test htpasswd file")
118 htpasswdPath := filepath.Join(suite.WorkspaceDir, testHtpasswdFileBasename)
119 err = os.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644)
120 suite.Nil(err, "no error creating test htpasswd file")
121
122
123 config := &configuration.Configuration{}
124 port, err := freeport.GetFreePort()
125 suite.Nil(err, "no error finding free port for test registry")
126
127
128
129
130 suite.DockerRegistryHost = fmt.Sprintf("helm-test-registry:%d", port)
131 suite.srv, _ = mockdns.NewServer(map[string]mockdns.Zone{
132 "helm-test-registry.": {
133 A: []string{"127.0.0.1"},
134 },
135 }, false)
136 suite.srv.PatchNet(net.DefaultResolver)
137
138 config.HTTP.Addr = fmt.Sprintf(":%d", port)
139 config.HTTP.DrainTimeout = time.Duration(10) * time.Second
140 config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
141
142
143 if tlsEnabled {
144 config.Auth = configuration.Auth{
145 "htpasswd": configuration.Parameters{
146 "realm": "localhost",
147 "path": htpasswdPath,
148 },
149 }
150 }
151
152
153 if tlsEnabled {
154
155
156
157 config.HTTP.TLS.Certificate = tlsServerCert
158 config.HTTP.TLS.Key = tlsServerKey
159
160 if !insecure {
161 config.HTTP.TLS.ClientCAs = []string{tlsCA}
162 }
163 }
164 dockerRegistry, err := registry.NewRegistry(context.Background(), config)
165 suite.Nil(err, "no error creating test registry")
166
167 suite.CompromisedRegistryHost = initCompromisedRegistryTestServer()
168 return dockerRegistry
169 }
170
171 func teardown(suite *TestSuite) {
172 if suite.srv != nil {
173 mockdns.UnpatchNet(net.DefaultResolver)
174 suite.srv.Close()
175 }
176 }
177
178 func initCompromisedRegistryTestServer() string {
179 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
180 if strings.Contains(r.URL.Path, "manifests") {
181 w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
182 w.WriteHeader(200)
183
184
185 w.Write([]byte(
186 fmt.Sprintf(`{ "schemaVersion": 2, "config": {
187 "mediaType": "%s",
188 "digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133",
189 "size": 181
190 },
191 "layers": [
192 {
193 "mediaType": "%s",
194 "digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
195 "size": 1
196 }
197 ]
198 }`, ConfigMediaType, ChartLayerMediaType)))
199 } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" {
200 w.Header().Set("Content-Type", "application/json")
201 w.WriteHeader(200)
202 w.Write([]byte("{\"name\":\"mychart\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\\n" +
203 "an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" +
204 "\"application\"}"))
205 } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" {
206 w.Header().Set("Content-Type", ChartLayerMediaType)
207 w.WriteHeader(200)
208 w.Write([]byte("b"))
209 } else {
210 w.WriteHeader(500)
211 }
212 }))
213
214 u, _ := url.Parse(s.URL)
215 return fmt.Sprintf("localhost:%s", u.Port())
216 }
217
218 func testPush(suite *TestSuite) {
219
220 testingChartCreationTime := "1977-09-02T22:04:05Z"
221
222
223 ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)
224 _, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptCreationTime(testingChartCreationTime))
225 suite.NotNil(err, "error pushing non-chart bytes")
226
227
228 chartData, err := os.ReadFile("../repo/repotest/testdata/examplechart-0.1.0.tgz")
229 suite.Nil(err, "no error loading test chart")
230 meta, err := extractChartMeta(chartData)
231 suite.Nil(err, "no error extracting chart meta")
232
233
234 ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version)
235 _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime))
236 suite.NotNil(err, "error pushing non-strict ref (bad basename)")
237
238
239 _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime))
240 suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled")
241
242
243 ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name)
244 _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime))
245 suite.NotNil(err, "error pushing non-strict ref (bad tag)")
246
247
248 _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime))
249 suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled")
250
251
252 chartData, err = os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz")
253 suite.Nil(err, "no error loading test chart")
254 meta, err = extractChartMeta(chartData)
255 suite.Nil(err, "no error extracting chart meta")
256 ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
257 _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime))
258 suite.Nil(err, "no error pushing good ref")
259
260 _, err = suite.RegistryClient.Pull(ref)
261 suite.Nil(err, "no error pulling a simple chart")
262
263
264 chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz")
265 suite.Nil(err, "no error loading test chart")
266 meta, err = extractChartMeta(chartData)
267 suite.Nil(err, "no error extracting chart meta")
268
269
270 provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov")
271 suite.Nil(err, "no error loading test prov")
272
273
274 ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
275 result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData), PushOptCreationTime(testingChartCreationTime))
276 suite.Nil(err, "no error pushing good ref with prov")
277
278 _, err = suite.RegistryClient.Pull(ref)
279 suite.Nil(err, "no error pulling a simple chart")
280
281
282
283
284 suite.Equal(ref, result.Ref)
285 suite.Equal(meta.Name, result.Chart.Meta.Name)
286 suite.Equal(meta.Version, result.Chart.Meta.Version)
287 suite.Equal(int64(742), result.Manifest.Size)
288 suite.Equal(int64(99), result.Config.Size)
289 suite.Equal(int64(973), result.Chart.Size)
290 suite.Equal(int64(695), result.Prov.Size)
291 suite.Equal(
292 "sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2",
293 result.Manifest.Digest)
294 suite.Equal(
295 "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
296 result.Config.Digest)
297 suite.Equal(
298 "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
299 result.Chart.Digest)
300 suite.Equal(
301 "sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
302 result.Prov.Digest)
303 }
304
305 func testPull(suite *TestSuite) {
306
307 ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost)
308 _, err := suite.RegistryClient.Pull(ref)
309 suite.NotNil(err, "error on bad/missing ref")
310
311
312 chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz")
313 suite.Nil(err, "no error loading test chart")
314 meta, err := extractChartMeta(chartData)
315 suite.Nil(err, "no error extracting chart meta")
316 ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
317
318
319 _, err = suite.RegistryClient.Pull(ref)
320 suite.Nil(err, "no error pulling a simple chart")
321
322
323 _, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true))
324 suite.NotNil(err, "error pulling a chart with prov when no prov exists")
325
326
327 _, err = suite.RegistryClient.Pull(ref,
328 PullOptWithProv(true),
329 PullOptIgnoreMissingProv(true))
330 suite.Nil(err,
331 "no error pulling a chart with prov when no prov exists, ignoring missing")
332
333
334 chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz")
335 suite.Nil(err, "no error loading test chart")
336 meta, err = extractChartMeta(chartData)
337 suite.Nil(err, "no error extracting chart meta")
338 ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
339
340
341 provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov")
342 suite.Nil(err, "no error loading test prov")
343
344
345 _, err = suite.RegistryClient.Pull(ref,
346 PullOptWithChart(false),
347 PullOptWithProv(false))
348 suite.NotNil(err, "error on both no chart and no prov")
349
350
351 result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true))
352 suite.Nil(err, "no error pulling a chart with prov")
353
354
355
356
357 suite.Equal(ref, result.Ref)
358 suite.Equal(meta.Name, result.Chart.Meta.Name)
359 suite.Equal(meta.Version, result.Chart.Meta.Version)
360 suite.Equal(int64(742), result.Manifest.Size)
361 suite.Equal(int64(99), result.Config.Size)
362 suite.Equal(int64(973), result.Chart.Size)
363 suite.Equal(int64(695), result.Prov.Size)
364 suite.Equal(
365 "sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2",
366 result.Manifest.Digest)
367 suite.Equal(
368 "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
369 result.Config.Digest)
370 suite.Equal(
371 "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
372 result.Chart.Digest)
373 suite.Equal(
374 "sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
375 result.Prov.Digest)
376 suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.created\":\"1977-09-02T22:04:05Z\",\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}",
377 string(result.Manifest.Data))
378 suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}",
379 string(result.Config.Data))
380 suite.Equal(chartData, result.Chart.Data)
381 suite.Equal(provData, result.Prov.Data)
382 }
383
384 func testTags(suite *TestSuite) {
385
386 chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz")
387 suite.Nil(err, "no error loading test chart")
388 meta, err := extractChartMeta(chartData)
389 suite.Nil(err, "no error extracting chart meta")
390 ref := fmt.Sprintf("%s/testrepo/%s", suite.DockerRegistryHost, meta.Name)
391
392
393 tags, err := suite.RegistryClient.Tags(ref)
394 suite.Nil(err, "no error retrieving tags")
395 suite.Equal(1, len(tags))
396 }
397
View as plain text