github.com/docker/cnab-to-oci@v0.3.0-beta4/remotes/push.go (about)

     1  package remotes
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  
    11  	"github.com/cnabio/cnab-go/bundle"
    12  	"github.com/containerd/containerd/errdefs"
    13  	"github.com/containerd/containerd/images"
    14  	"github.com/containerd/containerd/log"
    15  	"github.com/containerd/containerd/remotes"
    16  	"github.com/docker/cli/cli/config"
    17  	"github.com/docker/cli/cli/config/credentials"
    18  	configtypes "github.com/docker/cli/cli/config/types"
    19  	"github.com/docker/cnab-to-oci/converter"
    20  	"github.com/docker/cnab-to-oci/internal"
    21  	"github.com/docker/cnab-to-oci/relocation"
    22  	"github.com/docker/distribution/reference"
    23  	"github.com/docker/docker/api/types"
    24  	registrytypes "github.com/docker/docker/api/types/registry"
    25  	"github.com/docker/docker/pkg/jsonmessage"
    26  	"github.com/docker/docker/registry"
    27  	"github.com/opencontainers/go-digest"
    28  	ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1"
    29  	"github.com/pkg/errors"
    30  )
    31  
    32  // ManifestOption is a callback used to customize a manifest before pushing it
    33  type ManifestOption func(*ocischemav1.Index) error
    34  
    35  // Push pushes a bundle as an OCI Image Index manifest
    36  func Push(ctx context.Context,
    37  	b *bundle.Bundle,
    38  	relocationMap relocation.ImageRelocationMap,
    39  	ref reference.Named,
    40  	resolver remotes.Resolver,
    41  	allowFallbacks bool,
    42  	options ...ManifestOption) (ocischemav1.Descriptor, error) {
    43  	log.G(ctx).Debugf("Pushing CNAB Bundle %s", ref)
    44  
    45  	confManifestDescriptor, err := pushConfig(ctx, b, ref, resolver, allowFallbacks)
    46  	if err != nil {
    47  		return ocischemav1.Descriptor{}, err
    48  	}
    49  
    50  	indexDescriptor, err := pushIndex(ctx, b, relocationMap, ref, resolver, allowFallbacks, confManifestDescriptor, options...)
    51  	if err != nil {
    52  		return ocischemav1.Descriptor{}, err
    53  	}
    54  
    55  	log.G(ctx).Debug("CNAB Bundle pushed")
    56  	return indexDescriptor, nil
    57  }
    58  
    59  func pushConfig(ctx context.Context,
    60  	b *bundle.Bundle,
    61  	ref reference.Named, //nolint:interfacer
    62  	resolver remotes.Resolver,
    63  	allowFallbacks bool) (ocischemav1.Descriptor, error) {
    64  	logger := log.G(ctx)
    65  	logger.Debugf("Pushing CNAB Bundle Config")
    66  
    67  	bundleConfig, err := converter.PrepareForPush(b)
    68  	if err != nil {
    69  		return ocischemav1.Descriptor{}, err
    70  	}
    71  	confManifestDescriptor, err := pushBundleConfig(ctx, resolver, ref.Name(), bundleConfig, allowFallbacks)
    72  	if err != nil {
    73  		return ocischemav1.Descriptor{}, fmt.Errorf("error while pushing bundle config manifest: %s", err)
    74  	}
    75  
    76  	logger.Debug("CNAB Bundle Config pushed")
    77  	return confManifestDescriptor, nil
    78  }
    79  
    80  func pushIndex(ctx context.Context, b *bundle.Bundle, relocationMap relocation.ImageRelocationMap, ref reference.Named, resolver remotes.Resolver, allowFallbacks bool,
    81  	confManifestDescriptor ocischemav1.Descriptor, options ...ManifestOption) (ocischemav1.Descriptor, error) {
    82  	logger := log.G(ctx)
    83  	logger.Debug("Pushing CNAB Index")
    84  
    85  	indexDescriptor, indexPayload, err := prepareIndex(b, relocationMap, ref, confManifestDescriptor, options...)
    86  	if err != nil {
    87  		return ocischemav1.Descriptor{}, err
    88  	}
    89  	// Push the bundle index
    90  	logger.Debug("Trying to push OCI Index")
    91  	logger.Debug(string(indexPayload))
    92  	logger.Debug("OCI Index Descriptor")
    93  	logPayload(logger, indexDescriptor)
    94  
    95  	if err := pushPayload(ctx, resolver, ref.String(), indexDescriptor, indexPayload); err != nil {
    96  		if !allowFallbacks {
    97  			logger.Debug("Not using fallbacks, giving up")
    98  			return ocischemav1.Descriptor{}, err
    99  		}
   100  		logger.Debugf("Unable to push OCI Index: %v", err)
   101  		// retry with a docker manifestlist
   102  		return pushDockerManifestList(ctx, b, relocationMap, ref, resolver, confManifestDescriptor, options...)
   103  	}
   104  
   105  	logger.Debugf("CNAB Index pushed")
   106  	return indexDescriptor, nil
   107  }
   108  
   109  func pushDockerManifestList(ctx context.Context, b *bundle.Bundle, relocationMap relocation.ImageRelocationMap, ref reference.Named, resolver remotes.Resolver,
   110  	confManifestDescriptor ocischemav1.Descriptor, options ...ManifestOption) (ocischemav1.Descriptor, error) {
   111  	logger := log.G(ctx)
   112  
   113  	indexDescriptor, indexPayload, err := prepareIndexNonOCI(b, relocationMap, ref, confManifestDescriptor, options...)
   114  	if err != nil {
   115  		return ocischemav1.Descriptor{}, err
   116  	}
   117  	logger.Debug("Trying to push Index with Manifest list as fallback")
   118  	logger.Debug(string(indexPayload))
   119  	logger.Debug("Manifest list Descriptor")
   120  	logPayload(logger, indexDescriptor)
   121  
   122  	if err := pushPayload(ctx,
   123  		resolver, ref.String(),
   124  		indexDescriptor,
   125  		indexPayload); err != nil {
   126  		return ocischemav1.Descriptor{}, err
   127  	}
   128  	return indexDescriptor, nil
   129  }
   130  
   131  func prepareIndex(b *bundle.Bundle,
   132  	relocationMap relocation.ImageRelocationMap,
   133  	ref reference.Named,
   134  	confDescriptor ocischemav1.Descriptor,
   135  	options ...ManifestOption) (ocischemav1.Descriptor, []byte, error) {
   136  	ix, err := convertIndexAndApplyOptions(b, relocationMap, ref, confDescriptor, options...)
   137  	if err != nil {
   138  		return ocischemav1.Descriptor{}, nil, err
   139  	}
   140  	indexPayload, err := json.Marshal(ix)
   141  	if err != nil {
   142  		return ocischemav1.Descriptor{}, nil, fmt.Errorf("invalid bundle manifest %q: %s", ref, err)
   143  	}
   144  	indexDescriptor := ocischemav1.Descriptor{
   145  		Digest:    digest.FromBytes(indexPayload),
   146  		MediaType: ocischemav1.MediaTypeImageIndex,
   147  		Size:      int64(len(indexPayload)),
   148  	}
   149  	return indexDescriptor, indexPayload, nil
   150  }
   151  
   152  type ociIndexWrapper struct {
   153  	ocischemav1.Index
   154  	MediaType string `json:"mediaType,omitempty"`
   155  }
   156  
   157  func convertIndexAndApplyOptions(b *bundle.Bundle,
   158  	relocationMap relocation.ImageRelocationMap,
   159  	ref reference.Named,
   160  	confDescriptor ocischemav1.Descriptor,
   161  	options ...ManifestOption) (*ocischemav1.Index, error) {
   162  	ix, err := converter.ConvertBundleToOCIIndex(b, ref, confDescriptor, relocationMap)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	for _, opts := range options {
   167  		if err := opts(ix); err != nil {
   168  			return nil, fmt.Errorf("failed to prepare bundle manifest %q: %s", ref, err)
   169  		}
   170  	}
   171  	return ix, nil
   172  }
   173  
   174  func prepareIndexNonOCI(b *bundle.Bundle,
   175  	relocationMap relocation.ImageRelocationMap,
   176  	ref reference.Named,
   177  	confDescriptor ocischemav1.Descriptor,
   178  	options ...ManifestOption) (ocischemav1.Descriptor, []byte, error) {
   179  	ix, err := convertIndexAndApplyOptions(b, relocationMap, ref, confDescriptor, options...)
   180  	if err != nil {
   181  		return ocischemav1.Descriptor{}, nil, err
   182  	}
   183  	w := &ociIndexWrapper{Index: *ix, MediaType: images.MediaTypeDockerSchema2ManifestList}
   184  	w.SchemaVersion = 2
   185  	indexPayload, err := json.Marshal(w)
   186  	if err != nil {
   187  		return ocischemav1.Descriptor{}, nil, fmt.Errorf("invalid bundle manifest %q: %s", ref, err)
   188  	}
   189  	indexDescriptor := ocischemav1.Descriptor{
   190  		Digest:    digest.FromBytes(indexPayload),
   191  		MediaType: images.MediaTypeDockerSchema2ManifestList,
   192  		Size:      int64(len(indexPayload)),
   193  	}
   194  	return indexDescriptor, indexPayload, nil
   195  }
   196  
   197  func pushPayload(ctx context.Context, resolver remotes.Resolver, reference string, descriptor ocischemav1.Descriptor, payload []byte) error {
   198  	ctx = withMutedContext(ctx)
   199  	pusher, err := resolver.Pusher(ctx, reference)
   200  	if err != nil {
   201  		return err
   202  	}
   203  	writer, err := pusher.Push(ctx, descriptor)
   204  	if err != nil {
   205  		if errors.Cause(err) == errdefs.ErrAlreadyExists {
   206  			return nil
   207  		}
   208  		return err
   209  	}
   210  	defer writer.Close()
   211  	if _, err := writer.Write(payload); err != nil {
   212  		if errors.Cause(err) == errdefs.ErrAlreadyExists {
   213  			return nil
   214  		}
   215  		return err
   216  	}
   217  	err = writer.Commit(ctx, descriptor.Size, descriptor.Digest)
   218  	if errors.Cause(err) == errdefs.ErrAlreadyExists {
   219  		return nil
   220  	}
   221  	return err
   222  }
   223  
   224  func pushBundleConfig(ctx context.Context, resolver remotes.Resolver, reference string, bundleConfig *converter.PreparedBundleConfig, allowFallbacks bool) (ocischemav1.Descriptor, error) {
   225  	if d, err := pushBundleConfigDescriptor(ctx, "Config", resolver, reference,
   226  		bundleConfig.ConfigBlobDescriptor, bundleConfig.ConfigBlob, bundleConfig.Fallback, allowFallbacks); err != nil {
   227  		return d, err
   228  	}
   229  	return pushBundleConfigDescriptor(ctx, "Config Manifest", resolver, reference,
   230  		bundleConfig.ManifestDescriptor, bundleConfig.Manifest, bundleConfig.Fallback, allowFallbacks)
   231  }
   232  
   233  func pushBundleConfigDescriptor(ctx context.Context, name string, resolver remotes.Resolver, reference string,
   234  	descriptor ocischemav1.Descriptor, payload []byte, fallback *converter.PreparedBundleConfig, allowFallbacks bool) (ocischemav1.Descriptor, error) {
   235  	logger := log.G(ctx)
   236  	logger.Debugf("Trying to push CNAB Bundle %s", name)
   237  	logger.Debugf("CNAB Bundle %s Descriptor", name)
   238  	logPayload(logger, descriptor)
   239  
   240  	if err := pushPayload(ctx, resolver, reference, descriptor, payload); err != nil {
   241  		if allowFallbacks && fallback != nil {
   242  			logger.Debugf("Failed to push CNAB Bundle %s, trying with a fallback method", name)
   243  			return pushBundleConfig(ctx, resolver, reference, fallback, allowFallbacks)
   244  		}
   245  		return ocischemav1.Descriptor{}, err
   246  	}
   247  	return descriptor, nil
   248  }
   249  
   250  func pushTaggedImage(ctx context.Context, imageClient internal.ImageClient, targetRef reference.Named, out io.Writer) error {
   251  	repoInfo, err := registry.ParseRepositoryInfo(targetRef)
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	authConfig := resolveAuthConfig(repoInfo.Index)
   257  	encodedAuth, err := encodeAuthToBase64(authConfig)
   258  	if err != nil {
   259  		return err
   260  	}
   261  
   262  	reader, err := imageClient.ImagePush(ctx, targetRef.String(), types.ImagePushOptions{
   263  		RegistryAuth: encodedAuth,
   264  	})
   265  	if err != nil {
   266  		return err
   267  	}
   268  	defer reader.Close()
   269  	return jsonmessage.DisplayJSONMessagesStream(reader, out, 0, false, nil)
   270  }
   271  
   272  func encodeAuthToBase64(authConfig configtypes.AuthConfig) (string, error) {
   273  	buf, err := json.Marshal(authConfig)
   274  	if err != nil {
   275  		return "", err
   276  	}
   277  	return base64.URLEncoding.EncodeToString(buf), nil
   278  }
   279  
   280  func resolveAuthConfig(index *registrytypes.IndexInfo) configtypes.AuthConfig {
   281  	cfg := config.LoadDefaultConfigFile(os.Stderr)
   282  
   283  	hostName := index.Name
   284  	if index.Official {
   285  		hostName = registry.IndexServer
   286  	}
   287  
   288  	configs, err := cfg.GetAllCredentials()
   289  	if err != nil {
   290  		return configtypes.AuthConfig{}
   291  	}
   292  
   293  	// See https://github.com/docker/cli/blob/23446275646041f9b598d64c51be24d5d0e49376/cli/config/credentials/file_store.go#L32-L47
   294  	// We are looking for the hostname in the configuration, and if not we are trying with a pure hostname (so without
   295  	// http/https).
   296  	authConfig, ok := configs[hostName]
   297  	if !ok {
   298  		for reg, config := range configs {
   299  			if hostName == credentials.ConvertToHostname(reg) {
   300  				return config
   301  			}
   302  		}
   303  		return configtypes.AuthConfig{}
   304  	}
   305  	return authConfig
   306  }