...

Source file src/helm.sh/helm/v3/pkg/registry/client.go

Documentation: helm.sh/helm/v3/pkg/registry

     1  /*
     2  Copyright The Helm 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 registry // import "helm.sh/helm/v3/pkg/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  // See https://github.com/helm/helm/issues/10166
    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  	// Client works with OCI-compliant registries
    54  	Client struct {
    55  		debug       bool
    56  		enableCache bool
    57  		// path to repository config file e.g. ~/.docker/config.json
    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  	// ClientOption allows specifying various settings configurable by the user for overriding the defaults
    68  	// used when creating a new default client
    69  	ClientOption func(*Client)
    70  )
    71  
    72  // NewClient returns a new registry client with config
    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 // copy for avoiding recursive call
    92  	client.resolver = func(ref registry.Reference) (remotes.Resolver, error) {
    93  		if resolverFn != nil {
    94  			// validate if the resolverFn returns a valid resolver
    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  	// allocate a cache if option is set
   116  	var cache registryauth.Cache
   117  	if client.enableCache {
   118  		cache = registryauth.DefaultCache
   119  	}
   120  	if client.registryAuthorizer == nil {
   121  		client.registryAuthorizer = &registryauth.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  				// A blank returned username and password value is a bearer token
   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  // ClientOptDebug returns a function that sets the debug setting on client options set
   158  func ClientOptDebug(debug bool) ClientOption {
   159  	return func(client *Client) {
   160  		client.debug = debug
   161  	}
   162  }
   163  
   164  // ClientOptEnableCache returns a function that sets the enableCache setting on a client options set
   165  func ClientOptEnableCache(enableCache bool) ClientOption {
   166  	return func(client *Client) {
   167  		client.enableCache = enableCache
   168  	}
   169  }
   170  
   171  // ClientOptWriter returns a function that sets the writer setting on client options set
   172  func ClientOptWriter(out io.Writer) ClientOption {
   173  	return func(client *Client) {
   174  		client.out = out
   175  	}
   176  }
   177  
   178  // ClientOptCredentialsFile returns a function that sets the credentialsFile setting on a client options set
   179  func ClientOptCredentialsFile(credentialsFile string) ClientOption {
   180  	return func(client *Client) {
   181  		client.credentialsFile = credentialsFile
   182  	}
   183  }
   184  
   185  // ClientOptHTTPClient returns a function that sets the httpClient setting on a client options set
   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  // ClientOptResolver returns a function that sets the resolver setting on a client options set
   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  	// LoginOption allows specifying various settings on login
   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  // Login logs into a registry
   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  // LoginOptBasicAuth returns a function that sets the username/password settings on login
   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  // LoginOptInsecure returns a function that sets the insecure setting on login
   254  func LoginOptInsecure(insecure bool) LoginOption {
   255  	return func(operation *loginOperation) {
   256  		operation.insecure = insecure
   257  	}
   258  }
   259  
   260  // LoginOptTLSClientConfig returns a function that sets the TLS settings on login.
   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  	// LogoutOption allows specifying various settings on logout
   271  	LogoutOption func(*logoutOperation)
   272  
   273  	logoutOperation struct{}
   274  )
   275  
   276  // Logout logs out of a registry
   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  	// PullOption allows specifying various settings on pull
   291  	PullOption func(*pullOperation)
   292  
   293  	// PullResult is the result returned upon successful pull.
   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  // Pull downloads a chart from a registry
   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, // By default, always download the chart layer
   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 // 1 for the config
   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  // PullOptWithChart returns a function that sets the withChart setting on pull
   486  func PullOptWithChart(withChart bool) PullOption {
   487  	return func(operation *pullOperation) {
   488  		operation.withChart = withChart
   489  	}
   490  }
   491  
   492  // PullOptWithProv returns a function that sets the withProv setting on pull
   493  func PullOptWithProv(withProv bool) PullOption {
   494  	return func(operation *pullOperation) {
   495  		operation.withProv = withProv
   496  	}
   497  }
   498  
   499  // PullOptIgnoreMissingProv returns a function that sets the ignoreMissingProv setting on pull
   500  func PullOptIgnoreMissingProv(ignoreMissingProv bool) PullOption {
   501  	return func(operation *pullOperation) {
   502  		operation.ignoreMissingProv = ignoreMissingProv
   503  	}
   504  }
   505  
   506  type (
   507  	// PushOption allows specifying various settings on push
   508  	PushOption func(*pushOperation)
   509  
   510  	// PushResult is the result returned upon successful push.
   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  // Push uploads a chart to a registry.
   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, // By default, enable strict mode
   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{}, // prevent nil references
   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  // PushOptProvData returns a function that sets the prov bytes setting on push
   642  func PushOptProvData(provData []byte) PushOption {
   643  	return func(operation *pushOperation) {
   644  		operation.provData = provData
   645  	}
   646  }
   647  
   648  // PushOptStrictMode returns a function that sets the strictMode setting on push
   649  func PushOptStrictMode(strictMode bool) PushOption {
   650  	return func(operation *pushOperation) {
   651  		operation.strictMode = strictMode
   652  	}
   653  }
   654  
   655  // PushOptCreationDate returns a function that sets the creation time
   656  func PushOptCreationTime(creationTime string) PushOption {
   657  	return func(operation *pushOperation) {
   658  		operation.creationTime = creationTime
   659  	}
   660  }
   661  
   662  // Tags provides a sorted list all semver compliant tags for a given repository
   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  		// Change underscore (_) back to plus (+) for Helm
   685  		// See https://github.com/helm/helm/issues/10166
   686  		tagVersion, err := semver.StrictNewVersion(strings.ReplaceAll(tag, "_", "+"))
   687  		if err == nil {
   688  			tagVersions = append(tagVersions, tagVersion)
   689  		}
   690  	}
   691  
   692  	// Sort the collection
   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