github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/util/imagetools/create.go (about) 1 package imagetools 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "maps" 8 "net/url" 9 "strings" 10 11 "github.com/containerd/containerd/content" 12 "github.com/containerd/containerd/errdefs" 13 "github.com/containerd/containerd/images" 14 "github.com/containerd/containerd/platforms" 15 "github.com/containerd/containerd/remotes" 16 "github.com/distribution/reference" 17 "github.com/docker/buildx/util/buildflags" 18 "github.com/moby/buildkit/exporter/containerimage/exptypes" 19 "github.com/moby/buildkit/util/contentutil" 20 "github.com/opencontainers/go-digest" 21 "github.com/opencontainers/image-spec/specs-go" 22 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 23 "github.com/pkg/errors" 24 "golang.org/x/sync/errgroup" 25 ) 26 27 type Source struct { 28 Desc ocispec.Descriptor 29 Ref reference.Named 30 } 31 32 func (r *Resolver) Combine(ctx context.Context, srcs []*Source, ann []string) ([]byte, ocispec.Descriptor, error) { 33 eg, ctx := errgroup.WithContext(ctx) 34 35 dts := make([][]byte, len(srcs)) 36 for i := range dts { 37 func(i int) { 38 eg.Go(func() error { 39 dt, err := r.GetDescriptor(ctx, srcs[i].Ref.String(), srcs[i].Desc) 40 if err != nil { 41 return err 42 } 43 dts[i] = dt 44 45 if srcs[i].Desc.MediaType == "" { 46 mt, err := detectMediaType(dt) 47 if err != nil { 48 return err 49 } 50 srcs[i].Desc.MediaType = mt 51 } 52 53 mt := srcs[i].Desc.MediaType 54 55 switch mt { 56 case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest: 57 p := srcs[i].Desc.Platform 58 if srcs[i].Desc.Platform == nil { 59 p = &ocispec.Platform{} 60 } 61 if p.OS == "" || p.Architecture == "" { 62 if err := r.loadPlatform(ctx, p, srcs[i].Ref.String(), dt); err != nil { 63 return err 64 } 65 } 66 srcs[i].Desc.Platform = p 67 case images.MediaTypeDockerSchema1Manifest: 68 return errors.Errorf("schema1 manifests are not allowed in manifest lists") 69 } 70 71 return nil 72 }) 73 }(i) 74 } 75 76 if err := eg.Wait(); err != nil { 77 return nil, ocispec.Descriptor{}, err 78 } 79 80 // on single source, return original bytes 81 if len(srcs) == 1 && len(ann) == 0 { 82 if mt := srcs[0].Desc.MediaType; mt == images.MediaTypeDockerSchema2ManifestList || mt == ocispec.MediaTypeImageIndex { 83 return dts[0], srcs[0].Desc, nil 84 } 85 } 86 87 m := map[digest.Digest]int{} 88 newDescs := make([]ocispec.Descriptor, 0, len(srcs)) 89 90 addDesc := func(d ocispec.Descriptor) { 91 idx, ok := m[d.Digest] 92 if ok { 93 old := newDescs[idx] 94 if old.MediaType == "" { 95 old.MediaType = d.MediaType 96 } 97 if d.Platform != nil { 98 old.Platform = d.Platform 99 } 100 if old.Annotations == nil { 101 old.Annotations = map[string]string{} 102 } 103 for k, v := range d.Annotations { 104 old.Annotations[k] = v 105 } 106 newDescs[idx] = old 107 } else { 108 m[d.Digest] = len(newDescs) 109 newDescs = append(newDescs, d) 110 } 111 } 112 113 for i, src := range srcs { 114 switch src.Desc.MediaType { 115 case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: 116 var mfst ocispec.Index 117 if err := json.Unmarshal(dts[i], &mfst); err != nil { 118 return nil, ocispec.Descriptor{}, errors.WithStack(err) 119 } 120 for _, d := range mfst.Manifests { 121 addDesc(d) 122 } 123 default: 124 addDesc(src.Desc) 125 } 126 } 127 128 dockerMfsts := 0 129 for _, desc := range newDescs { 130 if strings.HasPrefix(desc.MediaType, "application/vnd.docker.") { 131 dockerMfsts++ 132 } 133 } 134 135 var mt string 136 if dockerMfsts == len(newDescs) { 137 // all manifests are Docker types, use Docker manifest list 138 mt = images.MediaTypeDockerSchema2ManifestList 139 } else { 140 // otherwise, use OCI index 141 mt = ocispec.MediaTypeImageIndex 142 } 143 144 // annotations are only allowed on OCI indexes 145 indexAnnotation := make(map[string]string) 146 if mt == ocispec.MediaTypeImageIndex { 147 annotations, err := buildflags.ParseAnnotations(ann) 148 if err != nil { 149 return nil, ocispec.Descriptor{}, err 150 } 151 for k, v := range annotations { 152 switch k.Type { 153 case exptypes.AnnotationIndex: 154 indexAnnotation[k.Key] = v 155 case exptypes.AnnotationManifestDescriptor: 156 for i := 0; i < len(newDescs); i++ { 157 if newDescs[i].Annotations == nil { 158 newDescs[i].Annotations = map[string]string{} 159 } 160 if k.Platform == nil || k.PlatformString() == platforms.Format(*newDescs[i].Platform) { 161 newDescs[i].Annotations[k.Key] = v 162 } 163 } 164 case exptypes.AnnotationManifest, "": 165 return nil, ocispec.Descriptor{}, errors.Errorf("%q annotations are not supported yet", k.Type) 166 case exptypes.AnnotationIndexDescriptor: 167 return nil, ocispec.Descriptor{}, errors.Errorf("%q annotations are invalid while creating an image", k.Type) 168 } 169 } 170 } 171 172 idxBytes, err := json.MarshalIndent(ocispec.Index{ 173 MediaType: mt, 174 Versioned: specs.Versioned{ 175 SchemaVersion: 2, 176 }, 177 Manifests: newDescs, 178 Annotations: indexAnnotation, 179 }, "", " ") 180 if err != nil { 181 return nil, ocispec.Descriptor{}, errors.Wrap(err, "failed to marshal index") 182 } 183 184 return idxBytes, ocispec.Descriptor{ 185 MediaType: mt, 186 Size: int64(len(idxBytes)), 187 Digest: digest.FromBytes(idxBytes), 188 }, nil 189 } 190 191 func (r *Resolver) Push(ctx context.Context, ref reference.Named, desc ocispec.Descriptor, dt []byte) error { 192 ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto") 193 194 ref = reference.TagNameOnly(ref) 195 p, err := r.resolver().Pusher(ctx, ref.String()) 196 if err != nil { 197 return err 198 } 199 cw, err := p.Push(ctx, desc) 200 if err != nil { 201 if errdefs.IsAlreadyExists(err) { 202 return nil 203 } 204 return err 205 } 206 207 err = content.Copy(ctx, cw, bytes.NewReader(dt), desc.Size, desc.Digest) 208 if errdefs.IsAlreadyExists(err) { 209 return nil 210 } 211 return err 212 } 213 214 func (r *Resolver) Copy(ctx context.Context, src *Source, dest reference.Named) error { 215 ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto") 216 217 dest = reference.TagNameOnly(dest) 218 p, err := r.resolver().Pusher(ctx, dest.String()) 219 if err != nil { 220 return err 221 } 222 223 srcRef := reference.TagNameOnly(src.Ref) 224 f, err := r.resolver().Fetcher(ctx, srcRef.String()) 225 if err != nil { 226 return err 227 } 228 229 refspec := reference.TrimNamed(src.Ref).String() 230 u, err := url.Parse("dummy://" + refspec) 231 if err != nil { 232 return err 233 } 234 235 desc := src.Desc 236 desc.Annotations = maps.Clone(desc.Annotations) 237 if desc.Annotations == nil { 238 desc.Annotations = make(map[string]string) 239 } 240 241 source, repo := u.Hostname(), strings.TrimPrefix(u.Path, "/") 242 desc.Annotations["containerd.io/distribution.source."+source] = repo 243 244 err = contentutil.CopyChain(ctx, contentutil.FromPusher(p), contentutil.FromFetcher(f), desc) 245 if err != nil { 246 return err 247 } 248 return nil 249 } 250 251 func (r *Resolver) loadPlatform(ctx context.Context, p2 *ocispec.Platform, in string, dt []byte) error { 252 var manifest ocispec.Manifest 253 if err := json.Unmarshal(dt, &manifest); err != nil { 254 return errors.WithStack(err) 255 } 256 257 dt, err := r.GetDescriptor(ctx, in, manifest.Config) 258 if err != nil { 259 return err 260 } 261 262 var p ocispec.Platform 263 if err := json.Unmarshal(dt, &p); err != nil { 264 return errors.WithStack(err) 265 } 266 267 p = platforms.Normalize(p) 268 269 if p2.Architecture == "" { 270 p2.Architecture = p.Architecture 271 if p2.Variant == "" { 272 p2.Variant = p.Variant 273 } 274 } 275 if p2.OS == "" { 276 p2.OS = p.OS 277 } 278 279 return nil 280 } 281 282 func detectMediaType(dt []byte) (string, error) { 283 var mfst struct { 284 MediaType string `json:"mediaType"` 285 Config json.RawMessage `json:"config"` 286 FSLayers []string `json:"fsLayers"` 287 } 288 289 if err := json.Unmarshal(dt, &mfst); err != nil { 290 return "", errors.WithStack(err) 291 } 292 293 if mfst.MediaType != "" { 294 return mfst.MediaType, nil 295 } 296 if mfst.Config != nil { 297 return images.MediaTypeDockerSchema2Manifest, nil 298 } 299 if len(mfst.FSLayers) > 0 { 300 return images.MediaTypeDockerSchema1Manifest, nil 301 } 302 303 return images.MediaTypeDockerSchema2ManifestList, nil 304 }