github.com/devdivbcp/moby@v17.12.0-ce-rc1.0.20200726071732-2d4bfdc789ad+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 }