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 }