Merge pull request #16890 from runcom/perf-boost

rmi and build cache miss performance improvements
This commit is contained in:
Tibor Vass 2015-10-21 16:00:25 -04:00
commit 56ef47e881
8 changed files with 161 additions and 70 deletions

View file

@ -1146,6 +1146,14 @@ func (daemon *Daemon) GetRemappedUIDGID() (int, int) {
// created. nil is returned if a child cannot be found. An error is
// returned if the parent image cannot be found.
func (daemon *Daemon) ImageGetCached(imgID string, config *runconfig.Config) (*image.Image, error) {
// for now just exit if imgID has no children.
// maybe parentRefs in graph could be used to store
// the Image obj children for faster lookup below but this can
// be quite memory hungry.
if !daemon.Graph().HasChildren(imgID) {
return nil, nil
}
// Retrieve all images
images := daemon.Graph().Map()

View file

@ -193,7 +193,7 @@ func (d Docker) Copy(c *daemon.Container, destPath string, src builder.FileInfo,
// GetCachedImage returns a reference to a cached image whose parent equals `parent`
// and runconfig equals `cfg`. A cache miss is expected to return an empty ID and a nil error.
func (d Docker) GetCachedImage(imgID string, cfg *runconfig.Config) (string, error) {
cache, err := d.Daemon.ImageGetCached(string(imgID), cfg)
cache, err := d.Daemon.ImageGetCached(imgID, cfg)
if cache == nil || err != nil {
return "", err
}

View file

@ -279,7 +279,7 @@ func (daemon *Daemon) checkImageDeleteHardConflict(img *image.Image) *imageDelet
}
// Check if the image has any descendent images.
if daemon.Graph().HasChildren(img) {
if daemon.Graph().HasChildren(img.ID) {
return &imageDeleteConflict{
hard: true,
imgID: img.ID,
@ -337,5 +337,5 @@ func (daemon *Daemon) checkImageDeleteSoftConflict(img *image.Image) *imageDelet
// that there are no repository references to the given image and it has no
// child images.
func (daemon *Daemon) imageIsDangling(img *image.Image) bool {
return !(daemon.repositories.HasReferences(img) || daemon.Graph().HasChildren(img))
return !(daemon.repositories.HasReferences(img) || daemon.Graph().HasChildren(img.ID))
}

View file

@ -99,11 +99,16 @@ type Graph struct {
root string
idIndex *truncindex.TruncIndex
driver graphdriver.Driver
imagesMutex sync.Mutex
imageMutex imageMutex // protect images in driver.
retained *retainedLayers
tarSplitDisabled bool
uidMaps []idtools.IDMap
gidMaps []idtools.IDMap
// access to parentRefs must be protected with imageMutex locking the image id
// on the key of the map (e.g. imageMutex.Lock(img.ID), parentRefs[img.ID]...)
parentRefs map[string]int
}
// file names for ./graph/<ID>/
@ -141,12 +146,13 @@ func NewGraph(root string, driver graphdriver.Driver, uidMaps, gidMaps []idtools
}
graph := &Graph{
root: abspath,
idIndex: truncindex.NewTruncIndex([]string{}),
driver: driver,
retained: &retainedLayers{layerHolders: make(map[string]map[string]struct{})},
uidMaps: uidMaps,
gidMaps: gidMaps,
root: abspath,
idIndex: truncindex.NewTruncIndex([]string{}),
driver: driver,
retained: &retainedLayers{layerHolders: make(map[string]map[string]struct{})},
uidMaps: uidMaps,
gidMaps: gidMaps,
parentRefs: make(map[string]int),
}
// Windows does not currently support tarsplit functionality.
@ -174,6 +180,13 @@ func (graph *Graph) restore() error {
for _, v := range dir {
id := v.Name()
if graph.driver.Exists(id) {
img, err := graph.loadImage(id)
if err != nil {
return err
}
graph.imageMutex.Lock(img.Parent)
graph.parentRefs[img.Parent]++
graph.imageMutex.Unlock(img.Parent)
ids = append(ids, id)
}
}
@ -262,6 +275,10 @@ func (graph *Graph) Register(im image.Descriptor, layerData io.Reader) (err erro
return err
}
// this is needed cause pull_v2 attemptIDReuse could deadlock
graph.imagesMutex.Lock()
defer graph.imagesMutex.Unlock()
// We need this entire operation to be atomic within the engine. Note that
// this doesn't mean Register is fully safe yet.
graph.imageMutex.Lock(imgID)
@ -302,10 +319,10 @@ func (graph *Graph) register(im image.Descriptor, layerData io.Reader) (err erro
graph.driver.Remove(imgID)
tmp, err := graph.mktemp()
defer os.RemoveAll(tmp)
if err != nil {
return fmt.Errorf("mktemp failed: %s", err)
return err
}
defer os.RemoveAll(tmp)
parent := im.Parent()
@ -326,7 +343,13 @@ func (graph *Graph) register(im image.Descriptor, layerData io.Reader) (err erro
if err := os.Rename(tmp, graph.imageRoot(imgID)); err != nil {
return err
}
graph.idIndex.Add(imgID)
graph.imageMutex.Lock(parent)
graph.parentRefs[parent]++
graph.imageMutex.Unlock(parent)
return nil
}
@ -349,6 +372,7 @@ func (graph *Graph) TempLayerArchive(id string, sf *streamformatter.StreamFormat
if err != nil {
return nil, err
}
defer os.RemoveAll(tmp)
a, err := graph.TarLayer(image)
if err != nil {
return nil, err
@ -379,34 +403,37 @@ func (graph *Graph) mktemp() (string, error) {
return dir, nil
}
func (graph *Graph) newTempFile() (*os.File, error) {
tmp, err := graph.mktemp()
if err != nil {
return nil, err
}
return ioutil.TempFile(tmp, "")
}
// Delete atomically removes an image from the graph.
func (graph *Graph) Delete(name string) error {
id, err := graph.idIndex.Get(name)
if err != nil {
return err
}
tmp, err := graph.mktemp()
img, err := graph.Get(id)
if err != nil {
return err
}
graph.idIndex.Delete(id)
if err == nil {
tmp, err := graph.mktemp()
if err != nil {
tmp = graph.imageRoot(id)
} else {
if err := os.Rename(graph.imageRoot(id), tmp); err != nil {
// On err make tmp point to old dir and cleanup unused tmp dir
os.RemoveAll(tmp)
tmp = graph.imageRoot(id)
}
} else {
// On err make tmp point to old dir for cleanup
tmp = graph.imageRoot(id)
}
// Remove rootfs data from the driver
graph.driver.Remove(id)
graph.imageMutex.Lock(img.Parent)
graph.parentRefs[img.Parent]--
if graph.parentRefs[img.Parent] == 0 {
delete(graph.parentRefs, img.Parent)
}
graph.imageMutex.Unlock(img.Parent)
// Remove the trashed image directory
return os.RemoveAll(tmp)
}
@ -424,9 +451,11 @@ func (graph *Graph) Map() map[string]*image.Image {
// The walking order is undetermined.
func (graph *Graph) walkAll(handler func(*image.Image)) {
graph.idIndex.Iterate(func(id string) {
if img, err := graph.Get(id); err != nil {
img, err := graph.Get(id)
if err != nil {
return
} else if handler != nil {
}
if handler != nil {
handler(img)
}
})
@ -453,8 +482,11 @@ func (graph *Graph) ByParent() map[string][]*image.Image {
}
// HasChildren returns whether the given image has any child images.
func (graph *Graph) HasChildren(img *image.Image) bool {
return len(graph.ByParent()[img.ID]) > 0
func (graph *Graph) HasChildren(imgID string) bool {
graph.imageMutex.Lock(imgID)
count := graph.parentRefs[imgID]
graph.imageMutex.Unlock(imgID)
return count > 0
}
// Retain keeps the images and layers that are in the pulling chain so that
@ -472,11 +504,9 @@ func (graph *Graph) Release(sessionID string, layerIDs ...string) {
// A head is an image which is not the parent of another image in the graph.
func (graph *Graph) Heads() map[string]*image.Image {
heads := make(map[string]*image.Image)
byParent := graph.ByParent()
graph.walkAll(func(image *image.Image) {
// If it's not in the byParent lookup table, then
// it's not a parent -> so it's a head!
if _, exists := byParent[image.ID]; !exists {
// if it has no children, then it's not a parent, so it's an head
if !graph.HasChildren(image.ID) {
heads[image.ID] = image
}
})

View file

@ -7,7 +7,6 @@ import (
"io"
"io/ioutil"
"os"
"sync"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution"
@ -359,6 +358,9 @@ func (p *v2Puller) pullV2Tag(out io.Writer, tag, taggedName string) (tagUpdated
Action: "Extracting",
})
p.graph.imagesMutex.Lock()
defer p.graph.imagesMutex.Unlock()
p.graph.imageMutex.Lock(d.img.id)
defer p.graph.imageMutex.Unlock(d.img.id)
@ -549,8 +551,6 @@ func (p *v2Puller) getImageInfos(m *manifest.Manifest) ([]contentAddressableDesc
return imgs, nil
}
var idReuseLock sync.Mutex
// attemptIDReuse does a best attempt to match verified compatibilityIDs
// already in the graph with the computed strongIDs so we can keep using them.
// This process will never fail but may just return the strongIDs if none of
@ -561,8 +561,8 @@ func (p *v2Puller) attemptIDReuse(imgs []contentAddressableDescriptor) {
// This function needs to be protected with a global lock, because it
// locks multiple IDs at once, and there's no good way to make sure
// the locking happens a deterministic order.
idReuseLock.Lock()
defer idReuseLock.Unlock()
p.graph.imagesMutex.Lock()
defer p.graph.imagesMutex.Unlock()
idMap := make(map[string]struct{})
for _, img := range imgs {

View file

@ -819,10 +819,10 @@ RUN [ $(ls -l /exists/exists_file | awk '{print $3":"$4}') = 'dockerio:dockerio'
"test_file3": "test3",
"test_file4": "test4",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
if _, err := buildImageFromContext(name, ctx, true); err != nil {
c.Fatal(err)
@ -839,10 +839,10 @@ func (s *DockerSuite) TestBuildAddMultipleFilesToFile(c *check.C) {
"file1.txt": "test1",
"file2.txt": "test1",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
expected := "When using ADD with more than one source file, the destination must be a directory and end with a /"
if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
@ -861,10 +861,10 @@ func (s *DockerSuite) TestBuildJSONAddMultipleFilesToFile(c *check.C) {
"file1.txt": "test1",
"file2.txt": "test1",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
expected := "When using ADD with more than one source file, the destination must be a directory and end with a /"
if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
@ -883,10 +883,10 @@ func (s *DockerSuite) TestBuildAddMultipleFilesToFileWild(c *check.C) {
"file1.txt": "test1",
"file2.txt": "test1",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
expected := "When using ADD with more than one source file, the destination must be a directory and end with a /"
if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
@ -905,10 +905,10 @@ func (s *DockerSuite) TestBuildJSONAddMultipleFilesToFileWild(c *check.C) {
"file1.txt": "test1",
"file2.txt": "test1",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
expected := "When using ADD with more than one source file, the destination must be a directory and end with a /"
if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
@ -927,10 +927,10 @@ func (s *DockerSuite) TestBuildCopyMultipleFilesToFile(c *check.C) {
"file1.txt": "test1",
"file2.txt": "test1",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
expected := "When using COPY with more than one source file, the destination must be a directory and end with a /"
if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
@ -949,10 +949,10 @@ func (s *DockerSuite) TestBuildJSONCopyMultipleFilesToFile(c *check.C) {
"file1.txt": "test1",
"file2.txt": "test1",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
expected := "When using COPY with more than one source file, the destination must be a directory and end with a /"
if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
@ -987,10 +987,10 @@ RUN [ $(cat "/test dir/test_file6") = 'test6' ]`,
"test_dir/test_file5": "test5",
"test dir/test_file6": "test6",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
if _, err := buildImageFromContext(name, ctx, true); err != nil {
c.Fatal(err)
@ -1023,10 +1023,10 @@ RUN [ $(cat "/test dir/test_file6") = 'test6' ]`,
"test_dir/test_file5": "test5",
"test dir/test_file6": "test6",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
if _, err := buildImageFromContext(name, ctx, true); err != nil {
c.Fatal(err)
@ -1043,10 +1043,10 @@ func (s *DockerSuite) TestBuildAddMultipleFilesToFileWithWhitespace(c *check.C)
"test file1": "test1",
"test file2": "test2",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
expected := "When using ADD with more than one source file, the destination must be a directory and end with a /"
if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
@ -1065,10 +1065,10 @@ func (s *DockerSuite) TestBuildCopyMultipleFilesToFileWithWhitespace(c *check.C)
"test file1": "test1",
"test file2": "test2",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
expected := "When using COPY with more than one source file, the destination must be a directory and end with a /"
if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
@ -1134,10 +1134,10 @@ func (s *DockerSuite) TestBuildCopyWildcardNoFind(c *check.C) {
ctx, err := fakeContext(`FROM busybox
COPY file*.txt /tmp/
`, nil)
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
_, err = buildImageFromContext(name, ctx, true)
if err == nil {
@ -1182,10 +1182,10 @@ func (s *DockerSuite) TestBuildCopyWildcardCache(c *check.C) {
map[string]string{
"file1.txt": "test1",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
id1, err := buildImageFromContext(name, ctx, true)
if err != nil {
@ -2214,10 +2214,10 @@ func (s *DockerSuite) TestBuildRelativeCopy(c *check.C) {
ctx, err := fakeContext(dockerfile, map[string]string{
"foo": "hello",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
_, err = buildImageFromContext(name, ctx, false)
if err != nil {
c.Fatal(err)
@ -2699,10 +2699,10 @@ func (s *DockerSuite) TestBuildAddLocalFileWithCache(c *check.C) {
ctx, err := fakeContext(dockerfile, map[string]string{
"foo": "hello",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
id1, err := buildImageFromContext(name, ctx, true)
if err != nil {
c.Fatal(err)
@ -2728,10 +2728,10 @@ func (s *DockerSuite) TestBuildAddMultipleLocalFileWithCache(c *check.C) {
ctx, err := fakeContext(dockerfile, map[string]string{
"foo": "hello",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
id1, err := buildImageFromContext(name, ctx, true)
if err != nil {
c.Fatal(err)
@ -2759,10 +2759,10 @@ func (s *DockerSuite) TestBuildAddLocalFileWithoutCache(c *check.C) {
ctx, err := fakeContext(dockerfile, map[string]string{
"foo": "hello",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
id1, err := buildImageFromContext(name, ctx, true)
if err != nil {
c.Fatal(err)
@ -2786,10 +2786,10 @@ func (s *DockerSuite) TestBuildCopyDirButNotFile(c *check.C) {
ctx, err := fakeContext(dockerfile, map[string]string{
"dir/foo": "hello",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
id1, err := buildImageFromContext(name, ctx, true)
if err != nil {
c.Fatal(err)
@ -2820,10 +2820,10 @@ func (s *DockerSuite) TestBuildAddCurrentDirWithCache(c *check.C) {
ctx, err := fakeContext(dockerfile, map[string]string{
"foo": "hello",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
id1, err := buildImageFromContext(name, ctx, true)
if err != nil {
c.Fatal(err)
@ -2876,10 +2876,10 @@ func (s *DockerSuite) TestBuildAddCurrentDirWithoutCache(c *check.C) {
ctx, err := fakeContext(dockerfile, map[string]string{
"foo": "hello",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
id1, err := buildImageFromContext(name, ctx, true)
if err != nil {
c.Fatal(err)
@ -3065,10 +3065,10 @@ CMD ["cat", "/foo"]`,
"foo": "bar",
},
)
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
context, err := archive.Tar(ctx.Dir, compression)
if err != nil {
c.Fatalf("failed to build context tar: %v", err)
@ -3186,10 +3186,10 @@ func (s *DockerSuite) TestBuildEntrypointRunCleanup(c *check.C) {
map[string]string{
"foo": "hello",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
if _, err := buildImageFromContext(name, ctx, true); err != nil {
c.Fatal(err)
}
@ -3213,10 +3213,10 @@ func (s *DockerSuite) TestBuildForbiddenContextPath(c *check.C) {
"test.txt": "test1",
"other.txt": "other",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
expected := "Forbidden path outside the build context: ../../ "
if _, err := buildImageFromContext(name, ctx, true); err == nil || !strings.Contains(err.Error(), expected) {
@ -3231,10 +3231,10 @@ func (s *DockerSuite) TestBuildAddFileNotFound(c *check.C) {
ctx, err := fakeContext(`FROM scratch
ADD foo /usr/local/bar`,
map[string]string{"bar": "hello"})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
if _, err := buildImageFromContext(name, ctx, true); err != nil {
if !strings.Contains(err.Error(), "foo: no such file or directory") {
c.Fatalf("Wrong error %v, must be about missing foo file or directory", err)
@ -3631,10 +3631,10 @@ func (s *DockerSuite) TestBuildDockerignoringDockerignore(c *check.C) {
"Dockerfile": dockerfile,
".dockerignore": ".dockerignore\n",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
if _, err = buildImageFromContext(name, ctx, true); err != nil {
c.Fatalf("Didn't ignore .dockerignore correctly:%s", err)
}
@ -3653,10 +3653,10 @@ func (s *DockerSuite) TestBuildDockerignoreTouchDockerfile(c *check.C) {
"Dockerfile": dockerfile,
".dockerignore": "Dockerfile\n",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
if id1, err = buildImageFromContext(name, ctx, true); err != nil {
c.Fatalf("Didn't build it correctly:%s", err)
@ -4926,10 +4926,10 @@ func (s *DockerSuite) TestBuildRenamedDockerfile(c *check.C) {
"dFile": "FROM busybox\nRUN echo from dFile",
"files/dFile2": "FROM busybox\nRUN echo from files/dFile2",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "-t", "test1", ".")
if err != nil {
@ -5028,10 +5028,10 @@ func (s *DockerSuite) TestBuildFromMixedcaseDockerfile(c *check.C) {
map[string]string{
"dockerfile": "FROM busybox\nRUN echo from dockerfile",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "-t", "test1", ".")
if err != nil {
@ -5053,10 +5053,10 @@ RUN echo from Dockerfile`,
map[string]string{
"dockerfile": "FROM busybox\nRUN echo from dockerfile",
})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "-t", "test1", ".")
if err != nil {
@ -5084,10 +5084,10 @@ RUN find /tmp/`})
ctx, err := fakeContext(`FROM busybox
RUN echo from Dockerfile`,
map[string]string{})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
// Make sure that -f is ignored and that we don't use the Dockerfile
// that's in the current dir
@ -5109,10 +5109,10 @@ func (s *DockerSuite) TestBuildFromStdinWithF(c *check.C) {
ctx, err := fakeContext(`FROM busybox
RUN echo from Dockerfile`,
map[string]string{})
defer ctx.Close()
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
// Make sure that -f is ignored and that we don't use the Dockerfile
// that's in the current dir
@ -5669,8 +5669,8 @@ func (s *DockerSuite) TestBuildNullStringInAddCopyVolume(c *check.C) {
"nullfile": "test2",
},
)
defer ctx.Close()
c.Assert(err, check.IsNil)
defer ctx.Close()
_, err = buildImageFromContext(name, ctx, true)
c.Assert(err, check.IsNil)

View file

@ -51,7 +51,6 @@ func (s *DockerSuite) TestImagesEnsureImageWithBadTagIsNotListed(c *check.C) {
if strings.Contains(out, "busybox") {
c.Fatal("images should not have listed busybox")
}
}
func (s *DockerSuite) TestImagesOrderedByCreationDate(c *check.C) {
@ -204,3 +203,45 @@ func (s *DockerSuite) TestImagesWithIncorrectFilter(c *check.C) {
c.Assert(err, check.NotNil)
c.Assert(out, checker.Contains, "Invalid filter")
}
func (s *DockerSuite) TestImagesEnsureOnlyHeadsImagesShown(c *check.C) {
testRequires(c, DaemonIsLinux)
dockerfile := `
FROM scratch
MAINTAINER docker
ENV foo bar`
head, out, err := buildImageWithOut("scratch-image", dockerfile, false)
c.Assert(err, check.IsNil)
// this is just the output of docker build
// we're interested in getting the image id of the MAINTAINER instruction
// and that's located at output, line 5, from 7 to end
split := strings.Split(out, "\n")
intermediate := strings.TrimSpace(split[5][7:])
out, _ = dockerCmd(c, "images")
if strings.Contains(out, intermediate) {
c.Fatalf("images shouldn't show non-heads images, got %s in %s", intermediate, out)
}
if !strings.Contains(out, head[:12]) {
c.Fatalf("images should contain final built images, want %s in out, got %s", head[:12], out)
}
}
func (s *DockerSuite) TestImagesEnsureImagesFromScratchShown(c *check.C) {
testRequires(c, DaemonIsLinux)
dockerfile := `
FROM scratch
MAINTAINER docker`
id, _, err := buildImageWithOut("scratch-image", dockerfile, false)
c.Assert(err, check.IsNil)
out, _ := dockerCmd(c, "images")
if !strings.Contains(out, id[:12]) {
c.Fatalf("images should contain images built from scratch (e.g. %s), got %s", id[:12], out)
}
}

View file

@ -284,3 +284,15 @@ RUN echo 2 #layer2
// should be allowed to untag with the -f flag
c.Assert(out, checker.Contains, fmt.Sprintf("Untagged: %s:latest", newTag))
}
func (*DockerSuite) TestRmiParentImageFail(c *check.C) {
testRequires(c, DaemonIsLinux)
parent, err := inspectField("busybox", "Parent")
c.Assert(err, check.IsNil)
out, _, err := dockerCmdWithError("rmi", parent)
c.Assert(err, check.NotNil)
if !strings.Contains(out, "image has dependent child images") {
c.Fatalf("rmi should have failed because it's a parent image, got %s", out)
}
}