github.com/jfrazelle/docker@v1.1.2-0.20210712172922-bf78e25fe508/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 // confirmedTLSRegistries is a map indicating which registries 77 // are known to be using TLS. There should never be a plaintext 78 // retry for any of these. 79 confirmedTLSRegistries = make(map[string]struct{}) 80 ) 81 82 for _, endpoint := range endpoints { 83 if endpoint.URL.Scheme != "https" { 84 if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS { 85 logrus.Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL) 86 continue 87 } 88 } 89 90 logrus.Debugf("Trying to push %s to %s %s", repoInfo.Name.Name(), endpoint.URL, endpoint.Version) 91 92 pusher, err := NewPusher(ref, endpoint, repoInfo, imagePushConfig) 93 if err != nil { 94 lastErr = err 95 continue 96 } 97 if err := pusher.Push(ctx); err != nil { 98 // Was this push cancelled? If so, don't try to fall 99 // back. 100 select { 101 case <-ctx.Done(): 102 default: 103 if fallbackErr, ok := err.(fallbackError); ok { 104 if fallbackErr.transportOK && endpoint.URL.Scheme == "https" { 105 confirmedTLSRegistries[endpoint.URL.Host] = struct{}{} 106 } 107 err = fallbackErr.err 108 lastErr = err 109 logrus.Infof("Attempting next endpoint for push after error: %v", err) 110 continue 111 } 112 } 113 114 logrus.Errorf("Not continuing with push after error: %v", err) 115 return err 116 } 117 118 imagePushConfig.ImageEventLogger(reference.FamiliarString(ref), reference.FamiliarName(repoInfo.Name), "push") 119 return nil 120 } 121 122 if lastErr == nil { 123 lastErr = fmt.Errorf("no endpoints found for %s", repoInfo.Name.Name()) 124 } 125 return lastErr 126 } 127 128 // compress returns an io.ReadCloser which will supply a compressed version of 129 // the provided Reader. The caller must close the ReadCloser after reading the 130 // compressed data. 131 // 132 // Note that this function returns a reader instead of taking a writer as an 133 // argument so that it can be used with httpBlobWriter's ReadFrom method. 134 // Using httpBlobWriter's Write method would send a PATCH request for every 135 // Write call. 136 // 137 // The second return value is a channel that gets closed when the goroutine 138 // is finished. This allows the caller to make sure the goroutine finishes 139 // before it releases any resources connected with the reader that was 140 // passed in. 141 func compress(in io.Reader) (io.ReadCloser, chan struct{}) { 142 compressionDone := make(chan struct{}) 143 144 pipeReader, pipeWriter := io.Pipe() 145 // Use a bufio.Writer to avoid excessive chunking in HTTP request. 146 bufWriter := bufio.NewWriterSize(pipeWriter, compressionBufSize) 147 compressor := gzip.NewWriter(bufWriter) 148 149 go func() { 150 _, err := io.Copy(compressor, in) 151 if err == nil { 152 err = compressor.Close() 153 } 154 if err == nil { 155 err = bufWriter.Flush() 156 } 157 if err != nil { 158 pipeWriter.CloseWithError(err) 159 } else { 160 pipeWriter.Close() 161 } 162 close(compressionDone) 163 }() 164 165 return pipeReader, compressionDone 166 }