1
16
17 package registry
18
19 import (
20 "context"
21 "encoding/json"
22 "fmt"
23 "io"
24 "net/http"
25 "sort"
26 "strings"
27
28 "github.com/Masterminds/semver/v3"
29 "github.com/containerd/containerd/remotes"
30 ocispec "github.com/opencontainers/image-spec/specs-go/v1"
31 "github.com/pkg/errors"
32 "oras.land/oras-go/pkg/auth"
33 dockerauth "oras.land/oras-go/pkg/auth/docker"
34 "oras.land/oras-go/pkg/content"
35 "oras.land/oras-go/pkg/oras"
36 "oras.land/oras-go/pkg/registry"
37 registryremote "oras.land/oras-go/pkg/registry/remote"
38 registryauth "oras.land/oras-go/pkg/registry/remote/auth"
39
40 "helm.sh/helm/v3/internal/version"
41 "helm.sh/helm/v3/pkg/chart"
42 "helm.sh/helm/v3/pkg/helmpath"
43 )
44
45
46 const registryUnderscoreMessage = `
47 OCI artifact references (e.g. tags) do not support the plus sign (+). To support
48 storing semantic versions, Helm adopts the convention of changing plus (+) to
49 an underscore (_) in chart version tags when pushing to a registry and back to
50 a plus (+) when pulling from a registry.`
51
52 type (
53
54 Client struct {
55 debug bool
56 enableCache bool
57
58 credentialsFile string
59 out io.Writer
60 authorizer auth.Client
61 registryAuthorizer *registryauth.Client
62 resolver func(ref registry.Reference) (remotes.Resolver, error)
63 httpClient *http.Client
64 plainHTTP bool
65 }
66
67
68
69 ClientOption func(*Client)
70 )
71
72
73 func NewClient(options ...ClientOption) (*Client, error) {
74 client := &Client{
75 out: io.Discard,
76 }
77 for _, option := range options {
78 option(client)
79 }
80 if client.credentialsFile == "" {
81 client.credentialsFile = helmpath.ConfigPath(CredentialsFileBasename)
82 }
83 if client.authorizer == nil {
84 authClient, err := dockerauth.NewClientWithDockerFallback(client.credentialsFile)
85 if err != nil {
86 return nil, err
87 }
88 client.authorizer = authClient
89 }
90
91 resolverFn := client.resolver
92 client.resolver = func(ref registry.Reference) (remotes.Resolver, error) {
93 if resolverFn != nil {
94
95 if resolver, err := resolverFn(ref); resolver != nil && err == nil {
96 return resolver, nil
97 }
98 }
99 headers := http.Header{}
100 headers.Set("User-Agent", version.GetUserAgent())
101 opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)}
102 if client.httpClient != nil {
103 opts = append(opts, auth.WithResolverClient(client.httpClient))
104 }
105 if client.plainHTTP {
106 opts = append(opts, auth.WithResolverPlainHTTP())
107 }
108 resolver, err := client.authorizer.ResolverWithOpts(opts...)
109 if err != nil {
110 return nil, err
111 }
112 return resolver, nil
113 }
114
115
116 var cache registryauth.Cache
117 if client.enableCache {
118 cache = registryauth.DefaultCache
119 }
120 if client.registryAuthorizer == nil {
121 client.registryAuthorizer = ®istryauth.Client{
122 Client: client.httpClient,
123 Header: http.Header{
124 "User-Agent": {version.GetUserAgent()},
125 },
126 Cache: cache,
127 Credential: func(_ context.Context, reg string) (registryauth.Credential, error) {
128 dockerClient, ok := client.authorizer.(*dockerauth.Client)
129 if !ok {
130 return registryauth.EmptyCredential, errors.New("unable to obtain docker client")
131 }
132
133 username, password, err := dockerClient.Credential(reg)
134 if err != nil {
135 return registryauth.EmptyCredential, errors.New("unable to retrieve credentials")
136 }
137
138
139 if username == "" && password != "" {
140 return registryauth.Credential{
141 RefreshToken: password,
142 }, nil
143 }
144
145 return registryauth.Credential{
146 Username: username,
147 Password: password,
148 }, nil
149
150 },
151 }
152
153 }
154 return client, nil
155 }
156
157
158 func ClientOptDebug(debug bool) ClientOption {
159 return func(client *Client) {
160 client.debug = debug
161 }
162 }
163
164
165 func ClientOptEnableCache(enableCache bool) ClientOption {
166 return func(client *Client) {
167 client.enableCache = enableCache
168 }
169 }
170
171
172 func ClientOptWriter(out io.Writer) ClientOption {
173 return func(client *Client) {
174 client.out = out
175 }
176 }
177
178
179 func ClientOptCredentialsFile(credentialsFile string) ClientOption {
180 return func(client *Client) {
181 client.credentialsFile = credentialsFile
182 }
183 }
184
185
186 func ClientOptHTTPClient(httpClient *http.Client) ClientOption {
187 return func(client *Client) {
188 client.httpClient = httpClient
189 }
190 }
191
192 func ClientOptPlainHTTP() ClientOption {
193 return func(c *Client) {
194 c.plainHTTP = true
195 }
196 }
197
198
199 func ClientOptResolver(resolver remotes.Resolver) ClientOption {
200 return func(client *Client) {
201 client.resolver = func(_ registry.Reference) (remotes.Resolver, error) {
202 return resolver, nil
203 }
204 }
205 }
206
207 type (
208
209 LoginOption func(*loginOperation)
210
211 loginOperation struct {
212 username string
213 password string
214 insecure bool
215 certFile string
216 keyFile string
217 caFile string
218 }
219 )
220
221
222 func (c *Client) Login(host string, options ...LoginOption) error {
223 operation := &loginOperation{}
224 for _, option := range options {
225 option(operation)
226 }
227 authorizerLoginOpts := []auth.LoginOption{
228 auth.WithLoginContext(ctx(c.out, c.debug)),
229 auth.WithLoginHostname(host),
230 auth.WithLoginUsername(operation.username),
231 auth.WithLoginSecret(operation.password),
232 auth.WithLoginUserAgent(version.GetUserAgent()),
233 auth.WithLoginTLS(operation.certFile, operation.keyFile, operation.caFile),
234 }
235 if operation.insecure {
236 authorizerLoginOpts = append(authorizerLoginOpts, auth.WithLoginInsecure())
237 }
238 if err := c.authorizer.LoginWithOpts(authorizerLoginOpts...); err != nil {
239 return err
240 }
241 fmt.Fprintln(c.out, "Login Succeeded")
242 return nil
243 }
244
245
246 func LoginOptBasicAuth(username string, password string) LoginOption {
247 return func(operation *loginOperation) {
248 operation.username = username
249 operation.password = password
250 }
251 }
252
253
254 func LoginOptInsecure(insecure bool) LoginOption {
255 return func(operation *loginOperation) {
256 operation.insecure = insecure
257 }
258 }
259
260
261 func LoginOptTLSClientConfig(certFile, keyFile, caFile string) LoginOption {
262 return func(operation *loginOperation) {
263 operation.certFile = certFile
264 operation.keyFile = keyFile
265 operation.caFile = caFile
266 }
267 }
268
269 type (
270
271 LogoutOption func(*logoutOperation)
272
273 logoutOperation struct{}
274 )
275
276
277 func (c *Client) Logout(host string, opts ...LogoutOption) error {
278 operation := &logoutOperation{}
279 for _, opt := range opts {
280 opt(operation)
281 }
282 if err := c.authorizer.Logout(ctx(c.out, c.debug), host); err != nil {
283 return err
284 }
285 fmt.Fprintf(c.out, "Removing login credentials for %s\n", host)
286 return nil
287 }
288
289 type (
290
291 PullOption func(*pullOperation)
292
293
294 PullResult struct {
295 Manifest *DescriptorPullSummary `json:"manifest"`
296 Config *DescriptorPullSummary `json:"config"`
297 Chart *DescriptorPullSummaryWithMeta `json:"chart"`
298 Prov *DescriptorPullSummary `json:"prov"`
299 Ref string `json:"ref"`
300 }
301
302 DescriptorPullSummary struct {
303 Data []byte `json:"-"`
304 Digest string `json:"digest"`
305 Size int64 `json:"size"`
306 }
307
308 DescriptorPullSummaryWithMeta struct {
309 DescriptorPullSummary
310 Meta *chart.Metadata `json:"meta"`
311 }
312
313 pullOperation struct {
314 withChart bool
315 withProv bool
316 ignoreMissingProv bool
317 }
318 )
319
320
321 func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
322 parsedRef, err := parseReference(ref)
323 if err != nil {
324 return nil, err
325 }
326
327 operation := &pullOperation{
328 withChart: true,
329 }
330 for _, option := range options {
331 option(operation)
332 }
333 if !operation.withChart && !operation.withProv {
334 return nil, errors.New(
335 "must specify at least one layer to pull (chart/prov)")
336 }
337 memoryStore := content.NewMemory()
338 allowedMediaTypes := []string{
339 ConfigMediaType,
340 }
341 minNumDescriptors := 1
342 if operation.withChart {
343 minNumDescriptors++
344 allowedMediaTypes = append(allowedMediaTypes, ChartLayerMediaType, LegacyChartLayerMediaType)
345 }
346 if operation.withProv {
347 if !operation.ignoreMissingProv {
348 minNumDescriptors++
349 }
350 allowedMediaTypes = append(allowedMediaTypes, ProvLayerMediaType)
351 }
352
353 var descriptors, layers []ocispec.Descriptor
354 remotesResolver, err := c.resolver(parsedRef)
355 if err != nil {
356 return nil, err
357 }
358 registryStore := content.Registry{Resolver: remotesResolver}
359
360 manifest, err := oras.Copy(ctx(c.out, c.debug), registryStore, parsedRef.String(), memoryStore, "",
361 oras.WithPullEmptyNameAllowed(),
362 oras.WithAllowedMediaTypes(allowedMediaTypes),
363 oras.WithLayerDescriptors(func(l []ocispec.Descriptor) {
364 layers = l
365 }))
366 if err != nil {
367 return nil, err
368 }
369
370 descriptors = append(descriptors, manifest)
371 descriptors = append(descriptors, layers...)
372
373 numDescriptors := len(descriptors)
374 if numDescriptors < minNumDescriptors {
375 return nil, fmt.Errorf("manifest does not contain minimum number of descriptors (%d), descriptors found: %d",
376 minNumDescriptors, numDescriptors)
377 }
378 var configDescriptor *ocispec.Descriptor
379 var chartDescriptor *ocispec.Descriptor
380 var provDescriptor *ocispec.Descriptor
381 for _, descriptor := range descriptors {
382 d := descriptor
383 switch d.MediaType {
384 case ConfigMediaType:
385 configDescriptor = &d
386 case ChartLayerMediaType:
387 chartDescriptor = &d
388 case ProvLayerMediaType:
389 provDescriptor = &d
390 case LegacyChartLayerMediaType:
391 chartDescriptor = &d
392 fmt.Fprintf(c.out, "Warning: chart media type %s is deprecated\n", LegacyChartLayerMediaType)
393 }
394 }
395 if configDescriptor == nil {
396 return nil, fmt.Errorf("could not load config with mediatype %s", ConfigMediaType)
397 }
398 if operation.withChart && chartDescriptor == nil {
399 return nil, fmt.Errorf("manifest does not contain a layer with mediatype %s",
400 ChartLayerMediaType)
401 }
402 var provMissing bool
403 if operation.withProv && provDescriptor == nil {
404 if operation.ignoreMissingProv {
405 provMissing = true
406 } else {
407 return nil, fmt.Errorf("manifest does not contain a layer with mediatype %s",
408 ProvLayerMediaType)
409 }
410 }
411 result := &PullResult{
412 Manifest: &DescriptorPullSummary{
413 Digest: manifest.Digest.String(),
414 Size: manifest.Size,
415 },
416 Config: &DescriptorPullSummary{
417 Digest: configDescriptor.Digest.String(),
418 Size: configDescriptor.Size,
419 },
420 Chart: &DescriptorPullSummaryWithMeta{},
421 Prov: &DescriptorPullSummary{},
422 Ref: parsedRef.String(),
423 }
424 var getManifestErr error
425 if _, manifestData, ok := memoryStore.Get(manifest); !ok {
426 getManifestErr = errors.Errorf("Unable to retrieve blob with digest %s", manifest.Digest)
427 } else {
428 result.Manifest.Data = manifestData
429 }
430 if getManifestErr != nil {
431 return nil, getManifestErr
432 }
433 var getConfigDescriptorErr error
434 if _, configData, ok := memoryStore.Get(*configDescriptor); !ok {
435 getConfigDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", configDescriptor.Digest)
436 } else {
437 result.Config.Data = configData
438 var meta *chart.Metadata
439 if err := json.Unmarshal(configData, &meta); err != nil {
440 return nil, err
441 }
442 result.Chart.Meta = meta
443 }
444 if getConfigDescriptorErr != nil {
445 return nil, getConfigDescriptorErr
446 }
447 if operation.withChart {
448 var getChartDescriptorErr error
449 if _, chartData, ok := memoryStore.Get(*chartDescriptor); !ok {
450 getChartDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", chartDescriptor.Digest)
451 } else {
452 result.Chart.Data = chartData
453 result.Chart.Digest = chartDescriptor.Digest.String()
454 result.Chart.Size = chartDescriptor.Size
455 }
456 if getChartDescriptorErr != nil {
457 return nil, getChartDescriptorErr
458 }
459 }
460 if operation.withProv && !provMissing {
461 var getProvDescriptorErr error
462 if _, provData, ok := memoryStore.Get(*provDescriptor); !ok {
463 getProvDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", provDescriptor.Digest)
464 } else {
465 result.Prov.Data = provData
466 result.Prov.Digest = provDescriptor.Digest.String()
467 result.Prov.Size = provDescriptor.Size
468 }
469 if getProvDescriptorErr != nil {
470 return nil, getProvDescriptorErr
471 }
472 }
473
474 fmt.Fprintf(c.out, "Pulled: %s\n", result.Ref)
475 fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
476
477 if strings.Contains(result.Ref, "_") {
478 fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref)
479 fmt.Fprint(c.out, registryUnderscoreMessage+"\n")
480 }
481
482 return result, nil
483 }
484
485
486 func PullOptWithChart(withChart bool) PullOption {
487 return func(operation *pullOperation) {
488 operation.withChart = withChart
489 }
490 }
491
492
493 func PullOptWithProv(withProv bool) PullOption {
494 return func(operation *pullOperation) {
495 operation.withProv = withProv
496 }
497 }
498
499
500 func PullOptIgnoreMissingProv(ignoreMissingProv bool) PullOption {
501 return func(operation *pullOperation) {
502 operation.ignoreMissingProv = ignoreMissingProv
503 }
504 }
505
506 type (
507
508 PushOption func(*pushOperation)
509
510
511 PushResult struct {
512 Manifest *descriptorPushSummary `json:"manifest"`
513 Config *descriptorPushSummary `json:"config"`
514 Chart *descriptorPushSummaryWithMeta `json:"chart"`
515 Prov *descriptorPushSummary `json:"prov"`
516 Ref string `json:"ref"`
517 }
518
519 descriptorPushSummary struct {
520 Digest string `json:"digest"`
521 Size int64 `json:"size"`
522 }
523
524 descriptorPushSummaryWithMeta struct {
525 descriptorPushSummary
526 Meta *chart.Metadata `json:"meta"`
527 }
528
529 pushOperation struct {
530 provData []byte
531 strictMode bool
532 creationTime string
533 }
534 )
535
536
537 func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResult, error) {
538 parsedRef, err := parseReference(ref)
539 if err != nil {
540 return nil, err
541 }
542
543 operation := &pushOperation{
544 strictMode: true,
545 }
546 for _, option := range options {
547 option(operation)
548 }
549 meta, err := extractChartMeta(data)
550 if err != nil {
551 return nil, err
552 }
553 if operation.strictMode {
554 if !strings.HasSuffix(ref, fmt.Sprintf("/%s:%s", meta.Name, meta.Version)) {
555 return nil, errors.New(
556 "strict mode enabled, ref basename and tag must match the chart name and version")
557 }
558 }
559 memoryStore := content.NewMemory()
560 chartDescriptor, err := memoryStore.Add("", ChartLayerMediaType, data)
561 if err != nil {
562 return nil, err
563 }
564
565 configData, err := json.Marshal(meta)
566 if err != nil {
567 return nil, err
568 }
569
570 configDescriptor, err := memoryStore.Add("", ConfigMediaType, configData)
571 if err != nil {
572 return nil, err
573 }
574
575 descriptors := []ocispec.Descriptor{chartDescriptor}
576 var provDescriptor ocispec.Descriptor
577 if operation.provData != nil {
578 provDescriptor, err = memoryStore.Add("", ProvLayerMediaType, operation.provData)
579 if err != nil {
580 return nil, err
581 }
582
583 descriptors = append(descriptors, provDescriptor)
584 }
585
586 ociAnnotations := generateOCIAnnotations(meta, operation.creationTime)
587
588 manifestData, manifest, err := content.GenerateManifest(&configDescriptor, ociAnnotations, descriptors...)
589 if err != nil {
590 return nil, err
591 }
592
593 if err := memoryStore.StoreManifest(parsedRef.String(), manifest, manifestData); err != nil {
594 return nil, err
595 }
596
597 remotesResolver, err := c.resolver(parsedRef)
598 if err != nil {
599 return nil, err
600 }
601 registryStore := content.Registry{Resolver: remotesResolver}
602 _, err = oras.Copy(ctx(c.out, c.debug), memoryStore, parsedRef.String(), registryStore, "",
603 oras.WithNameValidation(nil))
604 if err != nil {
605 return nil, err
606 }
607 chartSummary := &descriptorPushSummaryWithMeta{
608 Meta: meta,
609 }
610 chartSummary.Digest = chartDescriptor.Digest.String()
611 chartSummary.Size = chartDescriptor.Size
612 result := &PushResult{
613 Manifest: &descriptorPushSummary{
614 Digest: manifest.Digest.String(),
615 Size: manifest.Size,
616 },
617 Config: &descriptorPushSummary{
618 Digest: configDescriptor.Digest.String(),
619 Size: configDescriptor.Size,
620 },
621 Chart: chartSummary,
622 Prov: &descriptorPushSummary{},
623 Ref: parsedRef.String(),
624 }
625 if operation.provData != nil {
626 result.Prov = &descriptorPushSummary{
627 Digest: provDescriptor.Digest.String(),
628 Size: provDescriptor.Size,
629 }
630 }
631 fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref)
632 fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
633 if strings.Contains(parsedRef.Reference, "_") {
634 fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref)
635 fmt.Fprint(c.out, registryUnderscoreMessage+"\n")
636 }
637
638 return result, err
639 }
640
641
642 func PushOptProvData(provData []byte) PushOption {
643 return func(operation *pushOperation) {
644 operation.provData = provData
645 }
646 }
647
648
649 func PushOptStrictMode(strictMode bool) PushOption {
650 return func(operation *pushOperation) {
651 operation.strictMode = strictMode
652 }
653 }
654
655
656 func PushOptCreationTime(creationTime string) PushOption {
657 return func(operation *pushOperation) {
658 operation.creationTime = creationTime
659 }
660 }
661
662
663 func (c *Client) Tags(ref string) ([]string, error) {
664 parsedReference, err := registry.ParseReference(ref)
665 if err != nil {
666 return nil, err
667 }
668
669 repository := registryremote.Repository{
670 Reference: parsedReference,
671 Client: c.registryAuthorizer,
672 PlainHTTP: c.plainHTTP,
673 }
674
675 var registryTags []string
676
677 registryTags, err = registry.Tags(ctx(c.out, c.debug), &repository)
678 if err != nil {
679 return nil, err
680 }
681
682 var tagVersions []*semver.Version
683 for _, tag := range registryTags {
684
685
686 tagVersion, err := semver.StrictNewVersion(strings.ReplaceAll(tag, "_", "+"))
687 if err == nil {
688 tagVersions = append(tagVersions, tagVersion)
689 }
690 }
691
692
693 sort.Sort(sort.Reverse(semver.Collection(tagVersions)))
694
695 tags := make([]string, len(tagVersions))
696
697 for iTv, tv := range tagVersions {
698 tags[iTv] = tv.String()
699 }
700
701 return tags, nil
702
703 }
704
View as plain text