github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/manifest/push.go (about) 1 package manifest 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 9 "github.com/distribution/reference" 10 "github.com/docker/cli/cli" 11 "github.com/docker/cli/cli/command" 12 "github.com/docker/cli/cli/manifest/types" 13 registryclient "github.com/docker/cli/cli/registry/client" 14 "github.com/docker/distribution" 15 "github.com/docker/distribution/manifest/manifestlist" 16 "github.com/docker/distribution/manifest/ocischema" 17 "github.com/docker/distribution/manifest/schema2" 18 "github.com/docker/docker/registry" 19 "github.com/pkg/errors" 20 "github.com/spf13/cobra" 21 ) 22 23 type pushOpts struct { 24 insecure bool 25 purge bool 26 target string 27 } 28 29 type mountRequest struct { 30 ref reference.Named 31 manifest types.ImageManifest 32 } 33 34 type manifestBlob struct { 35 canonical reference.Canonical 36 os string 37 } 38 39 type pushRequest struct { 40 targetRef reference.Named 41 list *manifestlist.DeserializedManifestList 42 mountRequests []mountRequest 43 manifestBlobs []manifestBlob 44 insecure bool 45 } 46 47 func newPushListCommand(dockerCli command.Cli) *cobra.Command { 48 opts := pushOpts{} 49 50 cmd := &cobra.Command{ 51 Use: "push [OPTIONS] MANIFEST_LIST", 52 Short: "Push a manifest list to a repository", 53 Args: cli.ExactArgs(1), 54 RunE: func(cmd *cobra.Command, args []string) error { 55 opts.target = args[0] 56 return runPush(cmd.Context(), dockerCli, opts) 57 }, 58 } 59 60 flags := cmd.Flags() 61 flags.BoolVarP(&opts.purge, "purge", "p", false, "Remove the local manifest list after push") 62 flags.BoolVar(&opts.insecure, "insecure", false, "Allow push to an insecure registry") 63 return cmd 64 } 65 66 func runPush(ctx context.Context, dockerCli command.Cli, opts pushOpts) error { 67 targetRef, err := normalizeReference(opts.target) 68 if err != nil { 69 return err 70 } 71 72 manifests, err := dockerCli.ManifestStore().GetList(targetRef) 73 if err != nil { 74 return err 75 } 76 if len(manifests) == 0 { 77 return errors.Errorf("%s not found", targetRef) 78 } 79 80 req, err := buildPushRequest(manifests, targetRef, opts.insecure) 81 if err != nil { 82 return err 83 } 84 85 if err := pushList(ctx, dockerCli, req); err != nil { 86 return err 87 } 88 if opts.purge { 89 return dockerCli.ManifestStore().Remove(targetRef) 90 } 91 return nil 92 } 93 94 func buildPushRequest(manifests []types.ImageManifest, targetRef reference.Named, insecure bool) (pushRequest, error) { 95 req := pushRequest{targetRef: targetRef, insecure: insecure} 96 97 var err error 98 req.list, err = buildManifestList(manifests, targetRef) 99 if err != nil { 100 return req, err 101 } 102 103 targetRepo, err := registry.ParseRepositoryInfo(targetRef) 104 if err != nil { 105 return req, err 106 } 107 targetRepoName, err := registryclient.RepoNameForReference(targetRepo.Name) 108 if err != nil { 109 return req, err 110 } 111 112 for _, imageManifest := range manifests { 113 manifestRepoName, err := registryclient.RepoNameForReference(imageManifest.Ref) 114 if err != nil { 115 return req, err 116 } 117 118 repoName, _ := reference.WithName(manifestRepoName) 119 if repoName.Name() != targetRepoName { 120 blobs, err := buildBlobRequestList(imageManifest, repoName) 121 if err != nil { 122 return req, err 123 } 124 req.manifestBlobs = append(req.manifestBlobs, blobs...) 125 126 manifestPush, err := buildPutManifestRequest(imageManifest, targetRef) 127 if err != nil { 128 return req, err 129 } 130 req.mountRequests = append(req.mountRequests, manifestPush) 131 } 132 } 133 return req, nil 134 } 135 136 func buildManifestList(manifests []types.ImageManifest, targetRef reference.Named) (*manifestlist.DeserializedManifestList, error) { 137 targetRepoInfo, err := registry.ParseRepositoryInfo(targetRef) 138 if err != nil { 139 return nil, err 140 } 141 142 descriptors := []manifestlist.ManifestDescriptor{} 143 for _, imageManifest := range manifests { 144 if imageManifest.Descriptor.Platform == nil || 145 imageManifest.Descriptor.Platform.Architecture == "" || 146 imageManifest.Descriptor.Platform.OS == "" { 147 return nil, errors.Errorf( 148 "manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref) 149 } 150 descriptor, err := buildManifestDescriptor(targetRepoInfo, imageManifest) 151 if err != nil { 152 return nil, err 153 } 154 descriptors = append(descriptors, descriptor) 155 } 156 157 return manifestlist.FromDescriptors(descriptors) 158 } 159 160 func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest types.ImageManifest) (manifestlist.ManifestDescriptor, error) { 161 repoInfo, err := registry.ParseRepositoryInfo(imageManifest.Ref) 162 if err != nil { 163 return manifestlist.ManifestDescriptor{}, err 164 } 165 166 manifestRepoHostname := reference.Domain(repoInfo.Name) 167 targetRepoHostname := reference.Domain(targetRepo.Name) 168 if manifestRepoHostname != targetRepoHostname { 169 return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname) 170 } 171 172 manifest := manifestlist.ManifestDescriptor{ 173 Descriptor: distribution.Descriptor{ 174 Digest: imageManifest.Descriptor.Digest, 175 Size: imageManifest.Descriptor.Size, 176 MediaType: imageManifest.Descriptor.MediaType, 177 }, 178 } 179 180 platform := types.PlatformSpecFromOCI(imageManifest.Descriptor.Platform) 181 if platform != nil { 182 manifest.Platform = *platform 183 } 184 185 if err = manifest.Descriptor.Digest.Validate(); err != nil { 186 return manifestlist.ManifestDescriptor{}, errors.Wrapf(err, 187 "digest parse of image %q failed", imageManifest.Ref) 188 } 189 190 return manifest, nil 191 } 192 193 func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference.Named) ([]manifestBlob, error) { 194 blobs := imageManifest.Blobs() 195 blobReqs := make([]manifestBlob, 0, len(blobs)) 196 for _, blobDigest := range blobs { 197 canonical, err := reference.WithDigest(repoName, blobDigest) 198 if err != nil { 199 return nil, err 200 } 201 var os string 202 if imageManifest.Descriptor.Platform != nil { 203 os = imageManifest.Descriptor.Platform.OS 204 } 205 blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: os}) 206 } 207 return blobReqs, nil 208 } 209 210 func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef reference.Named) (mountRequest, error) { 211 refWithoutTag, err := reference.WithName(targetRef.Name()) 212 if err != nil { 213 return mountRequest{}, err 214 } 215 mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Descriptor.Digest) 216 if err != nil { 217 return mountRequest{}, err 218 } 219 220 // Attempt to reconstruct indentation of the manifest to ensure sha parity 221 // with the registry - if we haven't preserved the raw content. 222 // 223 // This is necessary because our previous internal storage format did not 224 // preserve whitespace. If we don't have the newer format present, we can 225 // attempt the reconstruction like before, but explicitly error if the 226 // reconstruction failed! 227 switch { 228 case imageManifest.SchemaV2Manifest != nil: 229 dt := imageManifest.Raw 230 if len(dt) == 0 { 231 dt, err = json.MarshalIndent(imageManifest.SchemaV2Manifest, "", " ") 232 if err != nil { 233 return mountRequest{}, err 234 } 235 } 236 237 dig := imageManifest.Descriptor.Digest 238 if dig2 := dig.Algorithm().FromBytes(dt); dig != dig2 { 239 return mountRequest{}, errors.Errorf("internal digest mismatch for %s: expected %s, got %s", imageManifest.Ref, dig, dig2) 240 } 241 242 var manifest schema2.DeserializedManifest 243 if err = manifest.UnmarshalJSON(dt); err != nil { 244 return mountRequest{}, err 245 } 246 imageManifest.SchemaV2Manifest = &manifest 247 case imageManifest.OCIManifest != nil: 248 dt := imageManifest.Raw 249 if len(dt) == 0 { 250 dt, err = json.MarshalIndent(imageManifest.OCIManifest, "", " ") 251 if err != nil { 252 return mountRequest{}, err 253 } 254 } 255 256 dig := imageManifest.Descriptor.Digest 257 if dig2 := dig.Algorithm().FromBytes(dt); dig != dig2 { 258 return mountRequest{}, errors.Errorf("internal digest mismatch for %s: expected %s, got %s", imageManifest.Ref, dig, dig2) 259 } 260 261 var manifest ocischema.DeserializedManifest 262 if err = manifest.UnmarshalJSON(dt); err != nil { 263 return mountRequest{}, err 264 } 265 imageManifest.OCIManifest = &manifest 266 } 267 268 return mountRequest{ref: mountRef, manifest: imageManifest}, err 269 } 270 271 func pushList(ctx context.Context, dockerCli command.Cli, req pushRequest) error { 272 rclient := dockerCli.RegistryClient(req.insecure) 273 274 if err := mountBlobs(ctx, rclient, req.targetRef, req.manifestBlobs); err != nil { 275 return err 276 } 277 if err := pushReferences(ctx, dockerCli.Out(), rclient, req.mountRequests); err != nil { 278 return err 279 } 280 dgst, err := rclient.PutManifest(ctx, req.targetRef, req.list) 281 if err != nil { 282 return err 283 } 284 285 fmt.Fprintln(dockerCli.Out(), dgst.String()) 286 return nil 287 } 288 289 func pushReferences(ctx context.Context, out io.Writer, client registryclient.RegistryClient, mounts []mountRequest) error { 290 for _, mount := range mounts { 291 newDigest, err := client.PutManifest(ctx, mount.ref, mount.manifest) 292 if err != nil { 293 return err 294 } 295 fmt.Fprintf(out, "Pushed ref %s with digest: %s\n", mount.ref, newDigest) 296 } 297 return nil 298 } 299 300 func mountBlobs(ctx context.Context, client registryclient.RegistryClient, ref reference.Named, blobs []manifestBlob) error { 301 for _, blob := range blobs { 302 err := client.MountBlob(ctx, blob.canonical, ref) 303 switch err.(type) { 304 case nil: 305 case registryclient.ErrBlobCreated: 306 if blob.os != "windows" { 307 return fmt.Errorf("error mounting %s to %s", blob.canonical, ref) 308 } 309 default: 310 return err 311 } 312 } 313 return nil 314 }