diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index 6a29dbcc..b2fe8b29 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -251,6 +251,13 @@ func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State, _ = ref if len(img.RootFS.DiffIDs) == 0 { isScratch = true + // schema1 images can't return diffIDs so double check :( + for _, h := range img.History { + if !h.EmptyLayer { + isScratch = false + break + } + } } } } diff --git a/util/imageutil/config.go b/util/imageutil/config.go index 27ff4f42..a9890e73 100644 --- a/util/imageutil/config.go +++ b/util/imageutil/config.go @@ -14,12 +14,12 @@ import ( "github.com/pkg/errors" ) -type IngesterProvider interface { +type ContentCache interface { content.Ingester content.Provider } -func Config(ctx context.Context, str string, resolver remotes.Resolver, ingester IngesterProvider, p *specs.Platform) (digest.Digest, []byte, error) { +func Config(ctx context.Context, str string, resolver remotes.Resolver, cache ContentCache, p *specs.Platform) (digest.Digest, []byte, error) { // TODO: fix buildkit to take interface instead of struct var platform platforms.MatchComparer if p != nil { @@ -36,7 +36,7 @@ func Config(ctx context.Context, str string, resolver remotes.Resolver, ingester Digest: ref.Digest(), } if desc.Digest != "" { - ra, err := ingester.ReaderAt(ctx, desc) + ra, err := cache.ReaderAt(ctx, desc) if err == nil { desc.Size = ra.Size() mt, err := DetectManifestMediaType(ra) @@ -58,19 +58,23 @@ func Config(ctx context.Context, str string, resolver remotes.Resolver, ingester return "", nil, err } + if desc.MediaType == images.MediaTypeDockerSchema1Manifest { + return readSchema1Config(ctx, ref.String(), desc, fetcher, cache) + } + handlers := []images.Handler{ - remotes.FetchHandler(ingester, fetcher), - childrenConfigHandler(ingester, platform), + remotes.FetchHandler(cache, fetcher), + childrenConfigHandler(cache, platform), } if err := images.Dispatch(ctx, images.Handlers(handlers...), desc); err != nil { return "", nil, err } - config, err := images.Config(ctx, ingester, desc, platform) + config, err := images.Config(ctx, cache, desc, platform) if err != nil { return "", nil, err } - dt, err := content.ReadBlob(ctx, ingester, config) + dt, err := content.ReadBlob(ctx, cache, config) if err != nil { return "", nil, err } diff --git a/util/imageutil/schema1.go b/util/imageutil/schema1.go new file mode 100644 index 00000000..591676ff --- /dev/null +++ b/util/imageutil/schema1.go @@ -0,0 +1,87 @@ +package imageutil + +import ( + "context" + "encoding/json" + "io/ioutil" + "strings" + "time" + + "github.com/containerd/containerd/remotes" + digest "github.com/opencontainers/go-digest" + specs "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +func readSchema1Config(ctx context.Context, ref string, desc specs.Descriptor, fetcher remotes.Fetcher, cache ContentCache) (digest.Digest, []byte, error) { + rc, err := fetcher.Fetch(ctx, desc) + if err != nil { + return "", nil, err + } + defer rc.Close() + dt, err := ioutil.ReadAll(rc) + if err != nil { + return "", nil, errors.Wrap(err, "failed to fetch schema1 manifest") + } + dt, err = convertSchema1ConfigMeta(dt) + if err != nil { + return "", nil, err + } + return desc.Digest, dt, nil +} + +func convertSchema1ConfigMeta(in []byte) ([]byte, error) { + type history struct { + V1Compatibility string `json:"v1Compatibility"` + } + var m struct { + History []history `json:"history"` + } + if err := json.Unmarshal(in, &m); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal schema1 manifest") + } + if len(m.History) == 0 { + return nil, errors.Errorf("invalid schema1 manifest") + } + + var img specs.Image + if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), &img); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal image from schema 1 history") + } + + img.RootFS = specs.RootFS{ + Type: "layers", // filled in by exporter + } + img.History = make([]specs.History, len(m.History)) + + for i := range m.History { + var h v1History + if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), &h); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal history") + } + img.History[len(m.History)-i-1] = specs.History{ + Author: h.Author, + Comment: h.Comment, + Created: &h.Created, + CreatedBy: strings.Join(h.ContainerConfig.Cmd, " "), + EmptyLayer: (h.ThrowAway != nil && *h.ThrowAway) || (h.Size != nil && *h.Size == 0), + } + } + + dt, err := json.MarshalIndent(img, "", " ") + if err != nil { + return nil, errors.Wrap(err, "failed to marshal schema1 config") + } + return dt, nil +} + +type v1History struct { + Author string `json:"author,omitempty"` + Created time.Time `json:"created"` + Comment string `json:"comment,omitempty"` + ThrowAway *bool `json:"throwaway,omitempty"` + Size *int `json:"Size,omitempty"` // used before ThrowAway field + ContainerConfig struct { + Cmd []string `json:"Cmd,omitempty"` + } `json:"container_config,omitempty"` +}