github.com/docker/docker@v299999999.0.0-20200612211812-aaf470eca7b5+incompatible/distribution/push.go (about)

     1  package distribution // import "github.com/docker/docker/distribution"
     2  
     3  import (
     4  	"bufio"
     5  	"compress/gzip"
     6  	"context"
     7  	"fmt"
     8  	"io"
     9  
    10  	"github.com/docker/distribution/reference"
    11  	"github.com/docker/docker/distribution/metadata"
    12  	"github.com/docker/docker/pkg/progress"
    13  	"github.com/docker/docker/registry"
    14  	"github.com/sirupsen/logrus"
    15  )
    16  
    17  // Pusher is an interface that abstracts pushing for different API versions.
    18  type Pusher interface {
    19  	// Push tries to push the image configured at the creation of Pusher.
    20  	// Push returns an error if any, as well as a boolean that determines whether to retry Push on the next configured endpoint.
    21  	//
    22  	// TODO(tiborvass): have Push() take a reference to repository + tag, so that the pusher itself is repository-agnostic.
    23  	Push(ctx context.Context) error
    24  }
    25  
    26  const compressionBufSize = 32768
    27  
    28  // NewPusher creates a new Pusher interface that will push to either a v1 or v2
    29  // registry. The endpoint argument contains a Version field that determines
    30  // whether a v1 or v2 pusher will be created. The other parameters are passed
    31  // through to the underlying pusher implementation for use during the actual
    32  // push operation.
    33  func NewPusher(ref reference.Named, endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, imagePushConfig *ImagePushConfig) (Pusher, error) {
    34  	switch endpoint.Version {
    35  	case registry.APIVersion2:
    36  		return &v2Pusher{
    37  			v2MetadataService: metadata.NewV2MetadataService(imagePushConfig.MetadataStore),
    38  			ref:               ref,
    39  			endpoint:          endpoint,
    40  			repoInfo:          repoInfo,
    41  			config:            imagePushConfig,
    42  		}, nil
    43  	case registry.APIVersion1:
    44  		return nil, fmt.Errorf("protocol version %d no longer supported. Please contact admins of registry %s", endpoint.Version, endpoint.URL)
    45  	}
    46  	return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL)
    47  }
    48  
    49  // Push initiates a push operation on ref.
    50  // ref is the specific variant of the image to be pushed.
    51  // If no tag is provided, all tags will be pushed.
    52  func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushConfig) error {
    53  	// FIXME: Allow to interrupt current push when new push of same image is done.
    54  
    55  	// Resolve the Repository name from fqn to RepositoryInfo
    56  	repoInfo, err := imagePushConfig.RegistryService.ResolveRepository(ref)
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	endpoints, err := imagePushConfig.RegistryService.LookupPushEndpoints(reference.Domain(repoInfo.Name))
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	progress.Messagef(imagePushConfig.ProgressOutput, "", "The push refers to repository [%s]", repoInfo.Name.Name())
    67  
    68  	associations := imagePushConfig.ReferenceStore.ReferencesByName(repoInfo.Name)
    69  	if len(associations) == 0 {
    70  		return fmt.Errorf("An image does not exist locally with the tag: %s", reference.FamiliarName(repoInfo.Name))
    71  	}
    72  
    73  	var (
    74  		lastErr error
    75  
    76  		// confirmedV2 is set to true if a push attempt managed to
    77  		// confirm that it was talking to a v2 registry. This will
    78  		// prevent fallback to the v1 protocol.
    79  		confirmedV2 bool
    80  
    81  		// confirmedTLSRegistries is a map indicating which registries
    82  		// are known to be using TLS. There should never be a plaintext
    83  		// retry for any of these.
    84  		confirmedTLSRegistries = make(map[string]struct{})
    85  	)
    86  
    87  	for _, endpoint := range endpoints {
    88  		if imagePushConfig.RequireSchema2 && endpoint.Version == registry.APIVersion1 {
    89  			continue
    90  		}
    91  		if confirmedV2 && endpoint.Version == registry.APIVersion1 {
    92  			logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
    93  			continue
    94  		}
    95  
    96  		if endpoint.URL.Scheme != "https" {
    97  			if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS {
    98  				logrus.Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL)
    99  				continue
   100  			}
   101  		}
   102  
   103  		logrus.Debugf("Trying to push %s to %s %s", repoInfo.Name.Name(), endpoint.URL, endpoint.Version)
   104  
   105  		pusher, err := NewPusher(ref, endpoint, repoInfo, imagePushConfig)
   106  		if err != nil {
   107  			lastErr = err
   108  			continue
   109  		}
   110  		if err := pusher.Push(ctx); err != nil {
   111  			// Was this push cancelled? If so, don't try to fall
   112  			// back.
   113  			select {
   114  			case <-ctx.Done():
   115  			default:
   116  				if fallbackErr, ok := err.(fallbackError); ok {
   117  					confirmedV2 = confirmedV2 || fallbackErr.confirmedV2
   118  					if fallbackErr.transportOK && endpoint.URL.Scheme == "https" {
   119  						confirmedTLSRegistries[endpoint.URL.Host] = struct{}{}
   120  					}
   121  					err = fallbackErr.err
   122  					lastErr = err
   123  					logrus.Infof("Attempting next endpoint for push after error: %v", err)
   124  					continue
   125  				}
   126  			}
   127  
   128  			logrus.Errorf("Not continuing with push after error: %v", err)
   129  			return err
   130  		}
   131  
   132  		imagePushConfig.ImageEventLogger(reference.FamiliarString(ref), reference.FamiliarName(repoInfo.Name), "push")
   133  		return nil
   134  	}
   135  
   136  	if lastErr == nil {
   137  		lastErr = fmt.Errorf("no endpoints found for %s", repoInfo.Name.Name())
   138  	}
   139  	return lastErr
   140  }
   141  
   142  // compress returns an io.ReadCloser which will supply a compressed version of
   143  // the provided Reader. The caller must close the ReadCloser after reading the
   144  // compressed data.
   145  //
   146  // Note that this function returns a reader instead of taking a writer as an
   147  // argument so that it can be used with httpBlobWriter's ReadFrom method.
   148  // Using httpBlobWriter's Write method would send a PATCH request for every
   149  // Write call.
   150  //
   151  // The second return value is a channel that gets closed when the goroutine
   152  // is finished. This allows the caller to make sure the goroutine finishes
   153  // before it releases any resources connected with the reader that was
   154  // passed in.
   155  func compress(in io.Reader) (io.ReadCloser, chan struct{}) {
   156  	compressionDone := make(chan struct{})
   157  
   158  	pipeReader, pipeWriter := io.Pipe()
   159  	// Use a bufio.Writer to avoid excessive chunking in HTTP request.
   160  	bufWriter := bufio.NewWriterSize(pipeWriter, compressionBufSize)
   161  	compressor := gzip.NewWriter(bufWriter)
   162  
   163  	go func() {
   164  		_, err := io.Copy(compressor, in)
   165  		if err == nil {
   166  			err = compressor.Close()
   167  		}
   168  		if err == nil {
   169  			err = bufWriter.Flush()
   170  		}
   171  		if err != nil {
   172  			pipeWriter.CloseWithError(err)
   173  		} else {
   174  			pipeWriter.Close()
   175  		}
   176  		close(compressionDone)
   177  	}()
   178  
   179  	return pipeReader, compressionDone
   180  }