1package containerimage
2
3import (
4	"context"
5	"encoding/json"
6	"runtime"
7	"time"
8
9	"github.com/moby/buildkit/cache"
10	"github.com/moby/buildkit/util/progress"
11	"github.com/moby/buildkit/util/system"
12	digest "github.com/opencontainers/go-digest"
13	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
14	"github.com/pkg/errors"
15	"github.com/sirupsen/logrus"
16)
17
18// const (
19// 	emptyGZLayer = digest.Digest("sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1")
20// )
21
22func emptyImageConfig() ([]byte, error) {
23	img := ocispec.Image{
24		Architecture: runtime.GOARCH,
25		OS:           runtime.GOOS,
26	}
27	img.RootFS.Type = "layers"
28	img.Config.WorkingDir = "/"
29	img.Config.Env = []string{"PATH=" + system.DefaultPathEnv}
30	dt, err := json.Marshal(img)
31	return dt, errors.Wrap(err, "failed to create empty image config")
32}
33
34func parseHistoryFromConfig(dt []byte) ([]ocispec.History, error) {
35	var config struct {
36		History []ocispec.History
37	}
38	if err := json.Unmarshal(dt, &config); err != nil {
39		return nil, errors.Wrap(err, "failed to unmarshal history from config")
40	}
41	return config.History, nil
42}
43
44func patchImageConfig(dt []byte, dps []digest.Digest, history []ocispec.History, cache []byte) ([]byte, error) {
45	m := map[string]json.RawMessage{}
46	if err := json.Unmarshal(dt, &m); err != nil {
47		return nil, errors.Wrap(err, "failed to parse image config for patch")
48	}
49
50	var rootFS ocispec.RootFS
51	rootFS.Type = "layers"
52	rootFS.DiffIDs = append(rootFS.DiffIDs, dps...)
53
54	dt, err := json.Marshal(rootFS)
55	if err != nil {
56		return nil, errors.Wrap(err, "failed to marshal rootfs")
57	}
58	m["rootfs"] = dt
59
60	dt, err = json.Marshal(history)
61	if err != nil {
62		return nil, errors.Wrap(err, "failed to marshal history")
63	}
64	m["history"] = dt
65
66	if _, ok := m["created"]; !ok {
67		var tm *time.Time
68		for _, h := range history {
69			if h.Created != nil {
70				tm = h.Created
71			}
72		}
73		dt, err = json.Marshal(&tm)
74		if err != nil {
75			return nil, errors.Wrap(err, "failed to marshal creation time")
76		}
77		m["created"] = dt
78	}
79
80	if cache != nil {
81		dt, err := json.Marshal(cache)
82		if err != nil {
83			return nil, err
84		}
85		m["moby.buildkit.cache.v0"] = dt
86	}
87
88	dt, err = json.Marshal(m)
89	return dt, errors.Wrap(err, "failed to marshal config after patch")
90}
91
92func normalizeLayersAndHistory(diffs []digest.Digest, history []ocispec.History, ref cache.ImmutableRef) ([]digest.Digest, []ocispec.History) {
93	refMeta := getRefMetadata(ref, len(diffs))
94	var historyLayers int
95	for _, h := range history {
96		if !h.EmptyLayer {
97			historyLayers++
98		}
99	}
100	if historyLayers > len(diffs) {
101		// this case shouldn't happen but if it does force set history layers empty
102		// from the bottom
103		logrus.Warn("invalid image config with unaccounted layers")
104		historyCopy := make([]ocispec.History, 0, len(history))
105		var l int
106		for _, h := range history {
107			if l >= len(diffs) {
108				h.EmptyLayer = true
109			}
110			if !h.EmptyLayer {
111				l++
112			}
113			historyCopy = append(historyCopy, h)
114		}
115		history = historyCopy
116	}
117
118	if len(diffs) > historyLayers {
119		// some history items are missing. add them based on the ref metadata
120		for _, md := range refMeta[historyLayers:] {
121			history = append(history, ocispec.History{
122				Created:   &md.createdAt,
123				CreatedBy: md.description,
124				Comment:   "buildkit.exporter.image.v0",
125			})
126		}
127	}
128
129	var layerIndex int
130	for i, h := range history {
131		if !h.EmptyLayer {
132			if h.Created == nil {
133				h.Created = &refMeta[layerIndex].createdAt
134			}
135			layerIndex++
136		}
137		history[i] = h
138	}
139
140	return diffs, history
141}
142
143type refMetadata struct {
144	description string
145	createdAt   time.Time
146}
147
148func getRefMetadata(ref cache.ImmutableRef, limit int) []refMetadata {
149	if limit <= 0 {
150		return nil
151	}
152	meta := refMetadata{
153		description: "created by buildkit", // shouldn't be shown but don't fail build
154		createdAt:   time.Now(),
155	}
156	if ref == nil {
157		return append(getRefMetadata(nil, limit-1), meta)
158	}
159	if descr := cache.GetDescription(ref.Metadata()); descr != "" {
160		meta.description = descr
161	}
162	meta.createdAt = cache.GetCreatedAt(ref.Metadata())
163	p := ref.Parent()
164	if p != nil {
165		defer p.Release(context.TODO())
166	}
167	return append(getRefMetadata(p, limit-1), meta)
168}
169
170func oneOffProgress(ctx context.Context, id string) func(err error) error {
171	pw, _, _ := progress.FromContext(ctx)
172	now := time.Now()
173	st := progress.Status{
174		Started: &now,
175	}
176	pw.Write(id, st)
177	return func(err error) error {
178		// TODO: set error on status
179		now := time.Now()
180		st.Completed = &now
181		pw.Write(id, st)
182		pw.Close()
183		return err
184	}
185}
186