| ... | ... |
@@ -968,3 +968,66 @@ func TestRunCidFile(t *testing.T) {
|
| 968 | 968 |
}) |
| 969 | 969 |
|
| 970 | 970 |
} |
| 971 |
+ |
|
| 972 |
+func TestContainerOrphaning(t *testing.T) {
|
|
| 973 |
+ |
|
| 974 |
+ // setup a temporary directory |
|
| 975 |
+ tmpDir, err := ioutil.TempDir("", "project")
|
|
| 976 |
+ if err != nil {
|
|
| 977 |
+ t.Fatal(err) |
|
| 978 |
+ } |
|
| 979 |
+ defer os.RemoveAll(tmpDir) |
|
| 980 |
+ |
|
| 981 |
+ // setup a CLI and server |
|
| 982 |
+ cli := docker.NewDockerCli(nil, ioutil.Discard, ioutil.Discard, testDaemonProto, testDaemonAddr) |
|
| 983 |
+ defer cleanup(globalEngine, t) |
|
| 984 |
+ srv := mkServerFromEngine(globalEngine, t) |
|
| 985 |
+ |
|
| 986 |
+ // closure to build something |
|
| 987 |
+ buildSomething := func(template string, image string) string {
|
|
| 988 |
+ dockerfile := path.Join(tmpDir, "Dockerfile") |
|
| 989 |
+ replacer := strings.NewReplacer("{IMAGE}", unitTestImageID)
|
|
| 990 |
+ contents := replacer.Replace(template) |
|
| 991 |
+ ioutil.WriteFile(dockerfile, []byte(contents), 0x777) |
|
| 992 |
+ if err := cli.CmdBuild("-t", image, tmpDir); err != nil {
|
|
| 993 |
+ t.Fatal(err) |
|
| 994 |
+ } |
|
| 995 |
+ img, err := srv.ImageInspect(image) |
|
| 996 |
+ if err != nil {
|
|
| 997 |
+ t.Fatal(err) |
|
| 998 |
+ } |
|
| 999 |
+ return img.ID |
|
| 1000 |
+ } |
|
| 1001 |
+ |
|
| 1002 |
+ // build an image |
|
| 1003 |
+ imageName := "orphan-test" |
|
| 1004 |
+ template1 := ` |
|
| 1005 |
+ from {IMAGE}
|
|
| 1006 |
+ cmd ["/bin/echo", "holla"] |
|
| 1007 |
+ ` |
|
| 1008 |
+ img1 := buildSomething(template1, imageName) |
|
| 1009 |
+ |
|
| 1010 |
+ // create a container using the fist image |
|
| 1011 |
+ if err := cli.CmdRun(imageName); err != nil {
|
|
| 1012 |
+ t.Fatal(err) |
|
| 1013 |
+ } |
|
| 1014 |
+ |
|
| 1015 |
+ // build a new image that splits lineage |
|
| 1016 |
+ template2 := ` |
|
| 1017 |
+ from {IMAGE}
|
|
| 1018 |
+ cmd ["/bin/echo", "holla"] |
|
| 1019 |
+ expose 22 |
|
| 1020 |
+ ` |
|
| 1021 |
+ buildSomething(template2, imageName) |
|
| 1022 |
+ |
|
| 1023 |
+ // remove the second image by name |
|
| 1024 |
+ resp, err := srv.ImageDelete(imageName, true) |
|
| 1025 |
+ |
|
| 1026 |
+ // see if we deleted the first image (and orphaned the container) |
|
| 1027 |
+ for _, i := range resp {
|
|
| 1028 |
+ if img1 == i.Deleted {
|
|
| 1029 |
+ t.Fatal("Orphaned image with container")
|
|
| 1030 |
+ } |
|
| 1031 |
+ } |
|
| 1032 |
+ |
|
| 1033 |
+} |
| ... | ... |
@@ -1543,7 +1543,7 @@ func (srv *Server) deleteImageAndChildren(id string, imgs *[]APIRmi, byParents m |
| 1543 | 1543 |
if err != nil {
|
| 1544 | 1544 |
return err |
| 1545 | 1545 |
} |
| 1546 |
- if len(byParents[id]) == 0 {
|
|
| 1546 |
+ if len(byParents[id]) == 0 && srv.canDeleteImage(id) == nil {
|
|
| 1547 | 1547 |
if err := srv.runtime.repositories.DeleteAll(id); err != nil {
|
| 1548 | 1548 |
return err |
| 1549 | 1549 |
} |
| ... | ... |
@@ -1631,9 +1631,8 @@ func (srv *Server) deleteImage(img *Image, repoName, tag string) ([]APIRmi, erro |
| 1631 | 1631 |
func (srv *Server) ImageDelete(name string, autoPrune bool) ([]APIRmi, error) {
|
| 1632 | 1632 |
var ( |
| 1633 | 1633 |
repository, tag string |
| 1634 |
- validate = true |
|
| 1634 |
+ img, err = srv.runtime.repositories.LookupImage(name) |
|
| 1635 | 1635 |
) |
| 1636 |
- img, err := srv.runtime.repositories.LookupImage(name) |
|
| 1637 | 1636 |
if err != nil {
|
| 1638 | 1637 |
return nil, fmt.Errorf("No such image: %s", name)
|
| 1639 | 1638 |
} |
| ... | ... |
@@ -1655,29 +1654,32 @@ func (srv *Server) ImageDelete(name string, autoPrune bool) ([]APIRmi, error) {
|
| 1655 | 1655 |
// |
| 1656 | 1656 |
// i.e. only validate if we are performing an actual delete and not |
| 1657 | 1657 |
// an untag op |
| 1658 |
- if repository != "" {
|
|
| 1659 |
- validate = len(srv.runtime.repositories.ByID()[img.ID]) == 1 |
|
| 1658 |
+ if repository != "" && len(srv.runtime.repositories.ByID()[img.ID]) == 1 {
|
|
| 1659 |
+ // Prevent deletion if image is used by a container |
|
| 1660 |
+ if err := srv.canDeleteImage(img.ID); err != nil {
|
|
| 1661 |
+ return nil, err |
|
| 1662 |
+ } |
|
| 1660 | 1663 |
} |
| 1664 |
+ return srv.deleteImage(img, repository, tag) |
|
| 1665 |
+} |
|
| 1661 | 1666 |
|
| 1662 |
- if validate {
|
|
| 1663 |
- // Prevent deletion if image is used by a container |
|
| 1664 |
- for _, container := range srv.runtime.List() {
|
|
| 1665 |
- parent, err := srv.runtime.repositories.LookupImage(container.Image) |
|
| 1666 |
- if err != nil {
|
|
| 1667 |
- return nil, err |
|
| 1668 |
- } |
|
| 1667 |
+func (srv *Server) canDeleteImage(imgID string) error {
|
|
| 1668 |
+ for _, container := range srv.runtime.List() {
|
|
| 1669 |
+ parent, err := srv.runtime.repositories.LookupImage(container.Image) |
|
| 1670 |
+ if err != nil {
|
|
| 1671 |
+ return err |
|
| 1672 |
+ } |
|
| 1669 | 1673 |
|
| 1670 |
- if err := parent.WalkHistory(func(p *Image) error {
|
|
| 1671 |
- if img.ID == p.ID {
|
|
| 1672 |
- return fmt.Errorf("Conflict, cannot delete %s because the container %s is using it", name, container.ID)
|
|
| 1673 |
- } |
|
| 1674 |
- return nil |
|
| 1675 |
- }); err != nil {
|
|
| 1676 |
- return nil, err |
|
| 1674 |
+ if err := parent.WalkHistory(func(p *Image) error {
|
|
| 1675 |
+ if imgID == p.ID {
|
|
| 1676 |
+ return fmt.Errorf("Conflict, cannot delete %s because the container %s is using it", utils.TruncateID(imgID), utils.TruncateID(container.ID))
|
|
| 1677 | 1677 |
} |
| 1678 |
+ return nil |
|
| 1679 |
+ }); err != nil {
|
|
| 1680 |
+ return err |
|
| 1678 | 1681 |
} |
| 1679 | 1682 |
} |
| 1680 |
- return srv.deleteImage(img, repository, tag) |
|
| 1683 |
+ return nil |
|
| 1681 | 1684 |
} |
| 1682 | 1685 |
|
| 1683 | 1686 |
func (srv *Server) ImageGetCached(imgID string, config *Config) (*Image, error) {
|