github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/registry/client.go (about)

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