This fixes an issue where two images with the same filesystem contents
and configuration but different remote IDs could share a v1Compatibility
file, resulting in corrupted manifests.
Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
| ... | ... |
@@ -3,6 +3,8 @@ package graph |
| 3 | 3 |
import ( |
| 4 | 4 |
"bufio" |
| 5 | 5 |
"compress/gzip" |
| 6 |
+ "encoding/json" |
|
| 7 |
+ "errors" |
|
| 6 | 8 |
"fmt" |
| 7 | 9 |
"io" |
| 8 | 10 |
"io/ioutil" |
| ... | ... |
@@ -205,6 +207,11 @@ func (p *v2Pusher) pushV2Tag(tag string) error {
|
| 205 | 205 |
p.layersPushed[dgst] = true |
| 206 | 206 |
} |
| 207 | 207 |
|
| 208 |
+ // Fix parent chain if necessary |
|
| 209 |
+ if err = fixHistory(m); err != nil {
|
|
| 210 |
+ return err |
|
| 211 |
+ } |
|
| 212 |
+ |
|
| 208 | 213 |
logrus.Infof("Signed manifest for %s:%s using daemon's key: %s", p.repo.Name(), tag, p.trustKey.KeyID())
|
| 209 | 214 |
signed, err := schema1.Sign(m, p.trustKey) |
| 210 | 215 |
if err != nil {
|
| ... | ... |
@@ -226,6 +233,90 @@ func (p *v2Pusher) pushV2Tag(tag string) error {
|
| 226 | 226 |
return manSvc.Put(signed) |
| 227 | 227 |
} |
| 228 | 228 |
|
| 229 |
+// fixHistory makes sure that the manifest has parent IDs that are consistent |
|
| 230 |
+// with its image IDs. Because local image IDs are generated from the |
|
| 231 |
+// configuration and filesystem contents, but IDs in the manifest are preserved |
|
| 232 |
+// from the original pull, it's possible to have inconsistencies where parent |
|
| 233 |
+// IDs don't match up with the other IDs in the manifest. This happens in the |
|
| 234 |
+// case where an engine pulls images where are identical except the IDs from the |
|
| 235 |
+// manifest - the local ID will be the same, and one of the v1Compatibility |
|
| 236 |
+// files gets discarded. |
|
| 237 |
+func fixHistory(m *schema1.Manifest) error {
|
|
| 238 |
+ var lastID string |
|
| 239 |
+ |
|
| 240 |
+ for i := len(m.History) - 1; i >= 0; i-- {
|
|
| 241 |
+ var historyEntry map[string]*json.RawMessage |
|
| 242 |
+ if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), &historyEntry); err != nil {
|
|
| 243 |
+ return err |
|
| 244 |
+ } |
|
| 245 |
+ |
|
| 246 |
+ idJSON, present := historyEntry["id"] |
|
| 247 |
+ if !present || idJSON == nil {
|
|
| 248 |
+ return errors.New("missing id key in v1compatibility file")
|
|
| 249 |
+ } |
|
| 250 |
+ var id string |
|
| 251 |
+ if err := json.Unmarshal(*idJSON, &id); err != nil {
|
|
| 252 |
+ return err |
|
| 253 |
+ } |
|
| 254 |
+ |
|
| 255 |
+ parentJSON, present := historyEntry["parent"] |
|
| 256 |
+ |
|
| 257 |
+ if i == len(m.History)-1 {
|
|
| 258 |
+ // The base layer must not reference a parent layer, |
|
| 259 |
+ // otherwise the manifest is incomplete. There is an |
|
| 260 |
+ // exception for Windows to handle base layers. |
|
| 261 |
+ if !allowBaseParentImage && present && parentJSON != nil {
|
|
| 262 |
+ var parent string |
|
| 263 |
+ if err := json.Unmarshal(*parentJSON, &parent); err != nil {
|
|
| 264 |
+ return err |
|
| 265 |
+ } |
|
| 266 |
+ if parent != "" {
|
|
| 267 |
+ logrus.Debugf("parent id mismatch detected; fixing. parent reference: %s", parent)
|
|
| 268 |
+ delete(historyEntry, "parent") |
|
| 269 |
+ fixedHistory, err := json.Marshal(historyEntry) |
|
| 270 |
+ if err != nil {
|
|
| 271 |
+ return err |
|
| 272 |
+ } |
|
| 273 |
+ m.History[i].V1Compatibility = string(fixedHistory) |
|
| 274 |
+ } |
|
| 275 |
+ } |
|
| 276 |
+ } else {
|
|
| 277 |
+ // For all other layers, the parent ID should equal the |
|
| 278 |
+ // ID of the next item in the history list. If it |
|
| 279 |
+ // doesn't, fix it up (but preserve all other fields, |
|
| 280 |
+ // possibly including fields that aren't known to this |
|
| 281 |
+ // engine version). |
|
| 282 |
+ if !present || parentJSON == nil {
|
|
| 283 |
+ return errors.New("missing parent key in v1compatibility file")
|
|
| 284 |
+ } |
|
| 285 |
+ var parent string |
|
| 286 |
+ if err := json.Unmarshal(*parentJSON, &parent); err != nil {
|
|
| 287 |
+ return err |
|
| 288 |
+ } |
|
| 289 |
+ if parent != lastID {
|
|
| 290 |
+ logrus.Debugf("parent id mismatch detected; fixing. parent reference: %s actual id: %s", parent, id)
|
|
| 291 |
+ historyEntry["parent"] = rawJSON(lastID) |
|
| 292 |
+ fixedHistory, err := json.Marshal(historyEntry) |
|
| 293 |
+ if err != nil {
|
|
| 294 |
+ return err |
|
| 295 |
+ } |
|
| 296 |
+ m.History[i].V1Compatibility = string(fixedHistory) |
|
| 297 |
+ } |
|
| 298 |
+ } |
|
| 299 |
+ lastID = id |
|
| 300 |
+ } |
|
| 301 |
+ |
|
| 302 |
+ return nil |
|
| 303 |
+} |
|
| 304 |
+ |
|
| 305 |
+func rawJSON(value interface{}) *json.RawMessage {
|
|
| 306 |
+ jsonval, err := json.Marshal(value) |
|
| 307 |
+ if err != nil {
|
|
| 308 |
+ return nil |
|
| 309 |
+ } |
|
| 310 |
+ return (*json.RawMessage)(&jsonval) |
|
| 311 |
+} |
|
| 312 |
+ |
|
| 229 | 313 |
func (p *v2Pusher) pushV2Image(bs distribution.BlobService, img *image.Image) (digest.Digest, error) {
|
| 230 | 314 |
out := p.config.OutStream |
| 231 | 315 |
|
| ... | ... |
@@ -2,13 +2,16 @@ package main |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"archive/tar" |
| 5 |
+ "encoding/json" |
|
| 5 | 6 |
"fmt" |
| 6 | 7 |
"io/ioutil" |
| 7 | 8 |
"os" |
| 8 | 9 |
"os/exec" |
| 10 |
+ "path/filepath" |
|
| 9 | 11 |
"strings" |
| 10 | 12 |
"time" |
| 11 | 13 |
|
| 14 |
+ "github.com/docker/docker/image" |
|
| 12 | 15 |
"github.com/docker/docker/pkg/integration/checker" |
| 13 | 16 |
"github.com/go-check/check" |
| 14 | 17 |
) |
| ... | ... |
@@ -83,6 +86,46 @@ func (s *DockerRegistrySuite) TestPushMultipleTags(c *check.C) {
|
| 83 | 83 |
} |
| 84 | 84 |
} |
| 85 | 85 |
|
| 86 |
+// TestPushBadParentChain tries to push an image with a corrupted parent chain |
|
| 87 |
+// in the v1compatibility files, and makes sure the push process fixes it. |
|
| 88 |
+func (s *DockerRegistrySuite) TestPushBadParentChain(c *check.C) {
|
|
| 89 |
+ repoName := fmt.Sprintf("%v/dockercli/badparent", privateRegistryURL)
|
|
| 90 |
+ |
|
| 91 |
+ id, err := buildImage(repoName, ` |
|
| 92 |
+ FROM busybox |
|
| 93 |
+ CMD echo "adding another layer" |
|
| 94 |
+ `, true) |
|
| 95 |
+ if err != nil {
|
|
| 96 |
+ c.Fatal(err) |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ // Push to create v1compatibility file |
|
| 100 |
+ dockerCmd(c, "push", repoName) |
|
| 101 |
+ |
|
| 102 |
+ // Corrupt the parent in the v1compatibility file from the top layer |
|
| 103 |
+ filename := filepath.Join(dockerBasePath, "graph", id, "v1Compatibility") |
|
| 104 |
+ |
|
| 105 |
+ jsonBytes, err := ioutil.ReadFile(filename) |
|
| 106 |
+ c.Assert(err, check.IsNil, check.Commentf("Could not read v1Compatibility file: %s", err))
|
|
| 107 |
+ |
|
| 108 |
+ var img image.Image |
|
| 109 |
+ err = json.Unmarshal(jsonBytes, &img) |
|
| 110 |
+ c.Assert(err, check.IsNil, check.Commentf("Could not unmarshal json: %s", err))
|
|
| 111 |
+ |
|
| 112 |
+ img.Parent = "1234123412341234123412341234123412341234123412341234123412341234" |
|
| 113 |
+ |
|
| 114 |
+ jsonBytes, err = json.Marshal(&img) |
|
| 115 |
+ c.Assert(err, check.IsNil, check.Commentf("Could not marshal json: %s", err))
|
|
| 116 |
+ |
|
| 117 |
+ err = ioutil.WriteFile(filename, jsonBytes, 0600) |
|
| 118 |
+ c.Assert(err, check.IsNil, check.Commentf("Could not write v1Compatibility file: %s", err))
|
|
| 119 |
+ |
|
| 120 |
+ dockerCmd(c, "push", repoName) |
|
| 121 |
+ |
|
| 122 |
+ // pull should succeed |
|
| 123 |
+ dockerCmd(c, "pull", repoName) |
|
| 124 |
+} |
|
| 125 |
+ |
|
| 86 | 126 |
func (s *DockerRegistrySuite) TestPushEmptyLayer(c *check.C) {
|
| 87 | 127 |
repoName := fmt.Sprintf("%v/dockercli/emptylayer", privateRegistryURL)
|
| 88 | 128 |
emptyTarball, err := ioutil.TempFile("", "empty_tarball")
|