dotcloud/docker

View on GitHub
builder/builder-next/exporter/mobyexporter/writer.go

Summary

Maintainability
B
6 hrs
Test Coverage
package mobyexporter

import (
    "context"
    "encoding/json"
    "time"

    "github.com/containerd/log"
    "github.com/containerd/platforms"
    "github.com/moby/buildkit/cache"
    "github.com/moby/buildkit/exporter/containerimage/exptypes"
    "github.com/moby/buildkit/util/progress"
    "github.com/moby/buildkit/util/system"
    "github.com/opencontainers/go-digest"
    ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    "github.com/pkg/errors"
)

func emptyImageConfig() ([]byte, error) {
    pl := platforms.Normalize(platforms.DefaultSpec())
    img := ocispec.Image{}
    img.Architecture = pl.Architecture
    img.OS = pl.OS
    img.Variant = pl.Variant
    img.RootFS.Type = "layers"
    img.Config.WorkingDir = "/"
    img.Config.Env = []string{"PATH=" + system.DefaultPathEnv(pl.OS)}
    dt, err := json.Marshal(img)
    return dt, errors.Wrap(err, "failed to create empty image config")
}

func parseHistoryFromConfig(dt []byte) ([]ocispec.History, error) {
    var config struct {
        History []ocispec.History
    }
    if err := json.Unmarshal(dt, &config); err != nil {
        return nil, errors.Wrap(err, "failed to unmarshal history from config")
    }
    return config.History, nil
}

func patchImageConfig(dt []byte, dps []digest.Digest, history []ocispec.History, cache *exptypes.InlineCacheEntry) ([]byte, error) {
    m := map[string]json.RawMessage{}
    if err := json.Unmarshal(dt, &m); err != nil {
        return nil, errors.Wrap(err, "failed to parse image config for patch")
    }

    if m == nil {
        return nil, errors.New("null image config")
    }

    var rootFS ocispec.RootFS
    rootFS.Type = "layers"
    rootFS.DiffIDs = append(rootFS.DiffIDs, dps...)

    dt, err := json.Marshal(rootFS)
    if err != nil {
        return nil, errors.Wrap(err, "failed to marshal rootfs")
    }
    m["rootfs"] = dt

    dt, err = json.Marshal(history)
    if err != nil {
        return nil, errors.Wrap(err, "failed to marshal history")
    }
    m["history"] = dt

    if _, ok := m["created"]; !ok {
        var tm *time.Time
        for _, h := range history {
            if h.Created != nil {
                tm = h.Created
            }
        }
        dt, err = json.Marshal(&tm)
        if err != nil {
            return nil, errors.Wrap(err, "failed to marshal creation time")
        }
        m["created"] = dt
    }

    if cache != nil {
        dt, err := json.Marshal(cache.Data)
        if err != nil {
            return nil, err
        }
        m["moby.buildkit.cache.v0"] = dt
    }

    dt, err = json.Marshal(m)
    return dt, errors.Wrap(err, "failed to marshal config after patch")
}

func normalizeLayersAndHistory(diffs []digest.Digest, history []ocispec.History, ref cache.ImmutableRef) ([]digest.Digest, []ocispec.History) {
    refMeta := getRefMetadata(ref, len(diffs))
    var historyLayers int
    for _, h := range history {
        if !h.EmptyLayer {
            historyLayers++
        }
    }
    if historyLayers > len(diffs) {
        // this case shouldn't happen but if it does force set history layers empty
        // from the bottom
        log.G(context.TODO()).Warn("invalid image config with unaccounted layers")
        historyCopy := make([]ocispec.History, 0, len(history))
        var l int
        for _, h := range history {
            if l >= len(diffs) {
                h.EmptyLayer = true
            }
            if !h.EmptyLayer {
                l++
            }
            historyCopy = append(historyCopy, h)
        }
        history = historyCopy
    }

    if len(diffs) > historyLayers {
        // some history items are missing. add them based on the ref metadata
        for _, md := range refMeta[historyLayers:] {
            history = append(history, ocispec.History{
                Created:   md.createdAt,
                CreatedBy: md.description,
                Comment:   "buildkit.exporter.image.v0",
            })
        }
    }

    var layerIndex int
    for i, h := range history {
        if !h.EmptyLayer {
            if h.Created == nil {
                h.Created = refMeta[layerIndex].createdAt
            }
            layerIndex++
        }
        history[i] = h
    }

    // Find the first new layer time. Otherwise, the history item for a first
    // metadata command would be the creation time of a base image layer.
    // If there is no such then the last layer with timestamp.
    var created *time.Time
    var noCreatedTime bool
    for _, h := range history {
        if h.Created != nil {
            created = h.Created
            if noCreatedTime {
                break
            }
        } else {
            noCreatedTime = true
        }
    }

    // Fill in created times for all history items to be either the first new
    // layer time or the previous layer.
    noCreatedTime = false
    for i, h := range history {
        if h.Created != nil {
            if noCreatedTime {
                created = h.Created
            }
        } else {
            noCreatedTime = true
            h.Created = created
        }
        history[i] = h
    }

    return diffs, history
}

type refMetadata struct {
    description string
    createdAt   *time.Time
}

func getRefMetadata(ref cache.ImmutableRef, limit int) []refMetadata {
    if ref == nil {
        return make([]refMetadata, limit)
    }

    layerChain := ref.LayerChain()
    defer layerChain.Release(context.TODO())

    if limit < len(layerChain) {
        layerChain = layerChain[len(layerChain)-limit:]
    }

    metas := make([]refMetadata, len(layerChain))
    for i, layer := range layerChain {
        meta := &metas[i]

        if description := layer.GetDescription(); description != "" {
            meta.description = description
        } else {
            meta.description = "created by buildkit" // shouldn't be shown but don't fail build
        }

        createdAt := layer.GetCreatedAt()
        meta.createdAt = &createdAt
    }
    return metas
}

func oneOffProgress(ctx context.Context, id string) func(err error) error {
    pw, _, _ := progress.NewFromContext(ctx)
    now := time.Now()
    st := progress.Status{
        Started: &now,
    }
    _ = pw.Write(id, st)
    return func(err error) error {
        // TODO: set error on status
        now := time.Now()
        st.Completed = &now
        _ = pw.Write(id, st)
        _ = pw.Close()
        return err
    }
}