@@ -2,10 +2,16 @@ package containerd
22
33import (
44 "context"
5+ "fmt"
6+ "sort"
7+ "strings"
58
69 "github.com/containerd/containerd/images"
710 "github.com/docker/distribution/reference"
811 "github.com/docker/docker/api/types"
12+ "github.com/docker/docker/container"
13+ "github.com/docker/docker/image"
14+ "github.com/docker/docker/pkg/stringid"
915 "github.com/opencontainers/go-digest"
1016 ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1117 "github.com/sirupsen/logrus"
@@ -30,8 +36,6 @@ import (
3036// are divided into two categories grouped by their severity:
3137//
3238// Hard Conflict:
33- // - a pull or build using the image.
34- // - any descendant image.
3539// - any running container using the image.
3640//
3741// Soft Conflict:
@@ -45,8 +49,6 @@ import (
4549// meaning any delete conflicts will cause the image to not be deleted and the
4650// conflict will not be reported.
4751//
48- // TODO(thaJeztah): implement ImageDelete "force" options; see https://github.com/moby/moby/issues/43850
49- // TODO(thaJeztah): implement ImageDelete "prune" options; see https://github.com/moby/moby/issues/43849
5052// TODO(thaJeztah): image delete should send prometheus counters; see https://github.com/moby/moby/issues/45268
5153func (i * ImageService ) ImageDelete (ctx context.Context , imageRef string , force , prune bool ) ([]types.ImageDeleteResponseItem , error ) {
5254 parsedRef , err := reference .ParseNormalizedNamed (imageRef )
@@ -59,28 +61,278 @@ func (i *ImageService) ImageDelete(ctx context.Context, imageRef string, force,
5961 return nil , err
6062 }
6163
64+ imgID := image .ID (img .Target .Digest )
65+
66+ if isImageIDPrefix (imgID .String (), imageRef ) {
67+ return i .deleteAll (ctx , img , force , prune )
68+ }
69+
70+ singleRef , err := i .isSingleReference (ctx , img )
71+ if err != nil {
72+ return nil , err
73+ }
74+ if ! singleRef {
75+ err := i .client .ImageService ().Delete (ctx , img .Name )
76+ if err != nil {
77+ return nil , err
78+ }
79+ i .LogImageEvent (imgID .String (), imgID .String (), "untag" )
80+ records := []types.ImageDeleteResponseItem {{Untagged : reference .FamiliarString (reference .TagNameOnly (parsedRef ))}}
81+ return records , nil
82+ }
83+
84+ using := func (c * container.Container ) bool {
85+ return c .ImageID == imgID
86+ }
87+ ctr := i .containers .First (using )
88+ if ctr != nil {
89+ if ! force {
90+ // If we removed the repository reference then
91+ // this image would remain "dangling" and since
92+ // we really want to avoid that the client must
93+ // explicitly force its removal.
94+ refString := reference .FamiliarString (reference .TagNameOnly (parsedRef ))
95+ err := & imageDeleteConflict {
96+ reference : refString ,
97+ used : true ,
98+ message : fmt .Sprintf ("container %s is using its referenced image %s" ,
99+ stringid .TruncateID (ctr .ID ),
100+ stringid .TruncateID (imgID .String ())),
101+ }
102+ return nil , err
103+ }
104+
105+ err := i .softImageDelete (ctx , img )
106+ if err != nil {
107+ return nil , err
108+ }
109+
110+ i .LogImageEvent (imgID .String (), imgID .String (), "untag" )
111+ records := []types.ImageDeleteResponseItem {{Untagged : reference .FamiliarString (reference .TagNameOnly (parsedRef ))}}
112+ return records , nil
113+ }
114+
115+ return i .deleteAll (ctx , img , force , prune )
116+ }
117+
118+ // deleteAll deletes the image from the daemon, and if prune is true,
119+ // also deletes dangling parents if there is no conflict in doing so.
120+ // Parent images are removed quietly, and if there is any issue/conflict
121+ // it is logged but does not halt execution/an error is not returned.
122+ func (i * ImageService ) deleteAll (ctx context.Context , img images.Image , force , prune bool ) ([]types.ImageDeleteResponseItem , error ) {
123+ var records []types.ImageDeleteResponseItem
124+
125+ // Workaround for: https://github.com/moby/buildkit/issues/3797
62126 possiblyDeletedConfigs := map [digest.Digest ]struct {}{}
63- if err := i .walkPresentChildren (ctx , img .Target , func (_ context.Context , d ocispec.Descriptor ) {
127+ err := i .walkPresentChildren (ctx , img .Target , func (_ context.Context , d ocispec.Descriptor ) {
64128 if images .IsConfigType (d .MediaType ) {
65129 possiblyDeletedConfigs [d .Digest ] = struct {}{}
66130 }
67- }); err != nil {
131+ })
132+ if err != nil {
68133 return nil , err
69134 }
135+ defer func () {
136+ if err := i .unleaseSnapshotsFromDeletedConfigs (context .Background (), possiblyDeletedConfigs ); err != nil {
137+ logrus .WithError (err ).Warn ("failed to unlease snapshots" )
138+ }
139+ }()
70140
71- err = i .client .ImageService ().Delete (ctx , img .Name , images .SynchronousDelete ())
141+ imgID := img .Target .Digest .String ()
142+
143+ var parents []imageWithRootfs
144+ if prune {
145+ parents , err = i .parents (ctx , image .ID (imgID ))
146+ if err != nil {
147+ logrus .WithError (err ).Warn ("failed to get image parents" )
148+ }
149+ sortParentsByAffinity (parents )
150+ }
151+
152+ imageRefs , err := i .client .ImageService ().List (ctx , "target.digest==" + imgID )
72153 if err != nil {
73154 return nil , err
74155 }
156+ for _ , imageRef := range imageRefs {
157+ if err := i .imageDeleteHelper (ctx , imageRef , & records , force ); err != nil {
158+ return records , err
159+ }
160+ }
161+ i .LogImageEvent (imgID , imgID , "delete" )
162+ records = append (records , types.ImageDeleteResponseItem {Deleted : imgID })
75163
76- // Workaround for: https://github.com/moby/buildkit/issues/3797
77- if err := i .unleaseSnapshotsFromDeletedConfigs (context .Background (), possiblyDeletedConfigs ); err != nil {
78- logrus .WithError (err ).Warn ("failed to unlease snapshots" )
164+ for _ , parent := range parents {
165+ if ! isDanglingImage (parent .img ) {
166+ break
167+ }
168+ err = i .imageDeleteHelper (ctx , parent .img , & records , false )
169+ if err != nil {
170+ logrus .WithError (err ).Warn ("failed to remove image parent" )
171+ break
172+ }
173+ parentID := parent .img .Target .Digest .String ()
174+ i .LogImageEvent (parentID , parentID , "delete" )
175+ records = append (records , types.ImageDeleteResponseItem {Deleted : parentID })
79176 }
80177
81- imgID := string (img .Target .Digest )
82- i .LogImageEvent (imgID , imgID , "untag" )
83- i .LogImageEvent (imgID , imgID , "delete" )
178+ return records , nil
179+ }
180+
181+ // isImageIDPrefix returns whether the given
182+ // possiblePrefix is a prefix of the given imageID.
183+ func isImageIDPrefix (imageID , possiblePrefix string ) bool {
184+ if strings .HasPrefix (imageID , possiblePrefix ) {
185+ return true
186+ }
187+ if i := strings .IndexRune (imageID , ':' ); i >= 0 {
188+ return strings .HasPrefix (imageID [i + 1 :], possiblePrefix )
189+ }
190+ return false
191+ }
192+
193+ func sortParentsByAffinity (parents []imageWithRootfs ) {
194+ sort .Slice (parents , func (i , j int ) bool {
195+ lenRootfsI := len (parents [i ].rootfs .DiffIDs )
196+ lenRootfsJ := len (parents [j ].rootfs .DiffIDs )
197+ if lenRootfsI == lenRootfsJ {
198+ return isDanglingImage (parents [i ].img )
199+ }
200+ return lenRootfsI > lenRootfsJ
201+ })
202+ }
203+
204+ // isSingleReference returns true if there are no other images in the
205+ // daemon targeting the same content as `img` that are not dangling.
206+ func (i * ImageService ) isSingleReference (ctx context.Context , img images.Image ) (bool , error ) {
207+ refs , err := i .client .ImageService ().List (ctx , "target.digest==" + img .Target .Digest .String ())
208+ if err != nil {
209+ return false , err
210+ }
211+ for _ , ref := range refs {
212+ if ! isDanglingImage (ref ) && ref .Name != img .Name {
213+ return false , nil
214+ }
215+ }
216+ return true , nil
217+ }
218+
219+ type conflictType int
220+
221+ const (
222+ conflictRunningContainer conflictType = 1 << iota
223+ conflictActiveReference
224+ conflictStoppedContainer
225+ conflictHard = conflictRunningContainer
226+ conflictSoft = conflictActiveReference | conflictStoppedContainer
227+ )
228+
229+ // imageDeleteHelper attempts to delete the given image from this daemon.
230+ // If the image has any hard delete conflicts (running containers using
231+ // the image) then it cannot be deleted. If the image has any soft delete
232+ // conflicts (any tags/digests referencing the image or any stopped container
233+ // using the image) then it can only be deleted if force is true. Any deleted
234+ // images and untagged references are appended to the given records. If any
235+ // error or conflict is encountered, it will be returned immediately without
236+ // deleting the image.
237+ func (i * ImageService ) imageDeleteHelper (ctx context.Context , img images.Image , records * []types.ImageDeleteResponseItem , force bool ) error {
238+ // First, determine if this image has any conflicts. Ignore soft conflicts
239+ // if force is true.
240+ c := conflictHard
241+ if ! force {
242+ c |= conflictSoft
243+ }
244+
245+ imgID := image .ID (img .Target .Digest )
246+
247+ err := i .checkImageDeleteConflict (ctx , imgID , c )
248+ if err != nil {
249+ return err
250+ }
251+
252+ untaggedRef , err := reference .ParseAnyReference (img .Name )
253+ if err != nil {
254+ return err
255+ }
256+ err = i .client .ImageService ().Delete (ctx , img .Name , images .SynchronousDelete ())
257+ if err != nil {
258+ return err
259+ }
260+
261+ i .LogImageEvent (imgID .String (), imgID .String (), "untag" )
262+ * records = append (* records , types.ImageDeleteResponseItem {Untagged : reference .FamiliarString (untaggedRef )})
263+
264+ return nil
265+ }
266+
267+ // ImageDeleteConflict holds a soft or hard conflict and associated
268+ // error. A hard conflict represents a running container using the
269+ // image, while a soft conflict is any tags/digests referencing the
270+ // given image or any stopped container using the image.
271+ // Implements the error interface.
272+ type imageDeleteConflict struct {
273+ hard bool
274+ used bool
275+ reference string
276+ message string
277+ }
278+
279+ func (idc * imageDeleteConflict ) Error () string {
280+ var forceMsg string
281+ if idc .hard {
282+ forceMsg = "cannot be forced"
283+ } else {
284+ forceMsg = "must be forced"
285+ }
286+ return fmt .Sprintf ("conflict: unable to delete %s (%s) - %s" , idc .reference , forceMsg , idc .message )
287+ }
288+
289+ func (imageDeleteConflict ) Conflict () {}
290+
291+ // checkImageDeleteConflict returns a conflict representing
292+ // any issue preventing deletion of the given image ID, and
293+ // nil if there are none. It takes a bitmask representing a
294+ // filter for which conflict types the caller cares about,
295+ // and will only check for these conflict types.
296+ func (i * ImageService ) checkImageDeleteConflict (ctx context.Context , imgID image.ID , mask conflictType ) error {
297+ if mask & conflictRunningContainer != 0 {
298+ running := func (c * container.Container ) bool {
299+ return c .ImageID == imgID && c .IsRunning ()
300+ }
301+ if ctr := i .containers .First (running ); ctr != nil {
302+ return & imageDeleteConflict {
303+ reference : stringid .TruncateID (imgID .String ()),
304+ hard : true ,
305+ used : true ,
306+ message : fmt .Sprintf ("image is being used by running container %s" , stringid .TruncateID (ctr .ID )),
307+ }
308+ }
309+ }
310+
311+ if mask & conflictStoppedContainer != 0 {
312+ stopped := func (c * container.Container ) bool {
313+ return ! c .IsRunning () && c .ImageID == imgID
314+ }
315+ if ctr := i .containers .First (stopped ); ctr != nil {
316+ return & imageDeleteConflict {
317+ reference : stringid .TruncateID (imgID .String ()),
318+ used : true ,
319+ message : fmt .Sprintf ("image is being used by stopped container %s" , stringid .TruncateID (ctr .ID )),
320+ }
321+ }
322+ }
323+
324+ if mask & conflictActiveReference != 0 {
325+ refs , err := i .client .ImageService ().List (ctx , "target.digest==" + imgID .String ())
326+ if err != nil {
327+ return err
328+ }
329+ if len (refs ) > 1 {
330+ return & imageDeleteConflict {
331+ reference : stringid .TruncateID (imgID .String ()),
332+ message : "image is referenced in multiple repositories" ,
333+ }
334+ }
335+ }
84336
85- return []types. ImageDeleteResponseItem {{ Untagged : reference . FamiliarString ( parsedRef )}}, nil
337+ return nil
86338}
0 commit comments