Skip to content

Commit 92c877e

Browse files
authored
Handle rebasing images with empty layers properly (#733)
Manifest v2 specs do not include layer entries for empty layers, but the history in the config will include an entry for empty layers (e.g. produced by `ENV` directive). History entries for empty layers include `empty_layer` field set to `true`. As a result, when rebasing and copying from the new base and original to the rebased image, it's not sufficient to simply iterate layers and pull from the same position/index in the history from config. Instead, history should be iterated and used to generate `Addendum`, while simultaneously iterating layers, but only including the layer (and advancing the iterator) if it's for a history item for a non-empty layer. Since the history in config is optional per the OCI spec [1], iteration via layers will still happen in the event that not all layers are consumed during the history pass. (This should also guard against a malformed history). [1]: https://github.com/opencontainers/image-spec/blob/master/config.md
1 parent 2a1a46d commit 92c877e

File tree

4 files changed

+107
-37
lines changed

4 files changed

+107
-37
lines changed

pkg/v1/mutate/image.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,15 @@ func (i *image) compute() error {
6969
digestMap := make(map[v1.Hash]v1.Layer)
7070

7171
for _, add := range i.adds {
72-
diffID, err := add.Layer.DiffID()
73-
if err != nil {
74-
return err
75-
}
76-
diffIDs = append(diffIDs, diffID)
7772
history = append(history, add.History)
78-
diffIDMap[diffID] = add.Layer
73+
if add.Layer != nil {
74+
diffID, err := add.Layer.DiffID()
75+
if err != nil {
76+
return err
77+
}
78+
diffIDs = append(diffIDs, diffID)
79+
diffIDMap[diffID] = add.Layer
80+
}
7981
}
8082

8183
m, err := i.base.Manifest()
@@ -85,6 +87,11 @@ func (i *image) compute() error {
8587
manifest := m.DeepCopy()
8688
manifestLayers := manifest.Layers
8789
for _, add := range i.adds {
90+
if add.Layer == nil {
91+
// Empty layers include only history in manifest.
92+
continue
93+
}
94+
8895
desc, err := partial.Descriptor(add.Layer)
8996
if err != nil {
9097
return err
@@ -251,7 +258,7 @@ func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) {
251258

252259
func validate(adds []Addendum) error {
253260
for _, add := range adds {
254-
if add.Layer == nil {
261+
if add.Layer == nil && !add.History.EmptyLayer {
255262
return errors.New("unable to add a nil layer to the image")
256263
}
257264
}

pkg/v1/mutate/rebase.go

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
5050
}
5151
}
5252

53+
oldConfig, err := oldBase.ConfigFile()
54+
if err != nil {
55+
return nil, fmt.Errorf("failed to get config for old base: %v", err)
56+
}
57+
5358
origConfig, err := orig.ConfigFile()
5459
if err != nil {
5560
return nil, fmt.Errorf("failed to get config for original: %v", err)
@@ -73,25 +78,48 @@ func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
7378
return nil, fmt.Errorf("could not get config for new base: %v", err)
7479
}
7580
// Add new base layers.
76-
for i := range newBaseLayers {
77-
rebasedImage, err = Append(rebasedImage, Addendum{
78-
Layer: newBaseLayers[i],
79-
History: newConfig.History[i],
80-
})
81-
if err != nil {
82-
return nil, fmt.Errorf("failed to append layer %d of new base layers", i)
83-
}
81+
rebasedImage, err = Append(rebasedImage, createAddendums(0, 0, newConfig.History, newBaseLayers)...)
82+
if err != nil {
83+
return nil, fmt.Errorf("failed to append new base image: %v", err)
8484
}
85+
8586
// Add original layers above the old base.
86-
start := len(oldBaseLayers)
87-
for i := range origLayers[start:] {
88-
rebasedImage, err = Append(rebasedImage, Addendum{
89-
Layer: origLayers[start+i],
90-
History: origConfig.History[start+i],
91-
})
92-
if err != nil {
93-
return nil, fmt.Errorf("failed to append layer %d of original layers", i)
94-
}
87+
rebasedImage, err = Append(rebasedImage, createAddendums(len(oldConfig.History), len(oldBaseLayers)+1, origConfig.History, origLayers)...)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to append original image: %v", err)
9590
}
91+
9692
return rebasedImage, nil
9793
}
94+
95+
// createAddendums makes a list of addendums from a history and layers starting from a specific history and layer
96+
// indexes.
97+
func createAddendums(startHistory, startLayer int, history []v1.History, layers []v1.Layer) []Addendum {
98+
var adds []Addendum
99+
// History should be a superset of layers; empty layers (e.g. ENV statements) only exist in history.
100+
// They cannot be iterated identically but must be walked independently, only advancing the iterator for layers
101+
// when a history entry for a non-empty layer is seen.
102+
layerIndex := 0
103+
for historyIndex := range history {
104+
var layer v1.Layer
105+
emptyLayer := history[historyIndex].EmptyLayer
106+
if !emptyLayer {
107+
layer = layers[layerIndex]
108+
layerIndex++
109+
}
110+
if historyIndex >= startHistory || layerIndex >= startLayer {
111+
adds = append(adds, Addendum{
112+
Layer: layer,
113+
History: history[historyIndex],
114+
})
115+
}
116+
}
117+
// In the event history was malformed or non-existent, append the remaining layers.
118+
for i := layerIndex; i < len(layers); i++ {
119+
if i >= startLayer {
120+
adds = append(adds, Addendum{Layer: layers[layerIndex]})
121+
}
122+
}
123+
124+
return adds
125+
}

pkg/v1/mutate/rebase_test.go

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@ func layerDigests(t *testing.T, img v1.Image) []string {
4444
// random.Image layers.
4545
func TestRebase(t *testing.T) {
4646
// Create a random old base image of 5 layers and get those layers' digests.
47-
oldBase, err := random.Image(100, 5)
47+
const oldBaseLayerCount = 5
48+
oldBase, err := random.Image(100, oldBaseLayerCount)
4849
if err != nil {
4950
t.Fatalf("random.Image (oldBase): %v", err)
5051
}
5152
t.Log("Old base:")
5253
_ = layerDigests(t, oldBase)
5354

54-
// Construct an image with 1 layer on top of oldBase.
55+
// Construct an image with 2 layers on top of oldBase (an empty layer and a random layer).
5556
top, err := random.Image(100, 1)
5657
if err != nil {
5758
t.Fatalf("random.Image (top): %v", err)
@@ -60,16 +61,27 @@ func TestRebase(t *testing.T) {
6061
if err != nil {
6162
t.Fatalf("top.Layers: %v", err)
6263
}
63-
topHistory := v1.History{
64-
Author: "me",
65-
Created: v1.Time{time.Now()},
66-
CreatedBy: "test",
67-
Comment: "this is a test",
68-
}
69-
orig, err := mutate.Append(oldBase, mutate.Addendum{
70-
Layer: topLayers[0],
71-
History: topHistory,
72-
})
64+
orig, err := mutate.Append(oldBase,
65+
mutate.Addendum{
66+
Layer: nil,
67+
History: v1.History{
68+
Author: "me",
69+
Created: v1.Time{Time: time.Now()},
70+
CreatedBy: "test-empty",
71+
Comment: "this is an empty test",
72+
EmptyLayer: true,
73+
},
74+
},
75+
mutate.Addendum{
76+
Layer: topLayers[0],
77+
History: v1.History{
78+
Author: "me",
79+
Created: v1.Time{Time: time.Now()},
80+
CreatedBy: "test",
81+
Comment: "this is a test",
82+
},
83+
},
84+
)
7385
if err != nil {
7486
t.Fatalf("Append: %v", err)
7587
}
@@ -116,4 +128,27 @@ func TestRebase(t *testing.T) {
116128
t.Errorf("Layer %d mismatch, got %q, want %q", i, got, want)
117129
}
118130
}
131+
132+
// Compare rebased history.
133+
origConfig, err := orig.ConfigFile()
134+
if err != nil {
135+
t.Fatalf("orig.ConfigFile: %v", err)
136+
}
137+
newBaseConfig, err := newBase.ConfigFile()
138+
if err != nil {
139+
t.Fatalf("newBase.ConfigFile: %v", err)
140+
}
141+
rebasedConfig, err := rebased.ConfigFile()
142+
if err != nil {
143+
t.Fatalf("rebased.ConfigFile: %v", err)
144+
}
145+
wantHistories := append(newBaseConfig.History, origConfig.History[oldBaseLayerCount:]...)
146+
if len(wantHistories) != len(rebasedConfig.History) {
147+
t.Fatalf("Rebased image contained %d history, want %d", len(rebasedConfig.History), len(wantHistories))
148+
}
149+
for i, rh := range rebasedConfig.History {
150+
if got, want := rh.Comment, wantHistories[i].Comment; got != want {
151+
t.Errorf("Layer %d mismatch, got %q, want %q", i, got, want)
152+
}
153+
}
119154
}

pkg/v1/random/image.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func Image(byteSize, layers int64) (v1.Image, error) {
6969
Layer: layer,
7070
History: v1.History{
7171
Author: "random.Image",
72-
Comment: fmt.Sprintf("this is a random history %d", i),
72+
Comment: fmt.Sprintf("this is a random history %d of %d", i, layers),
7373
CreatedBy: "random",
7474
Created: v1.Time{time.Now()},
7575
},

0 commit comments

Comments
 (0)