Skip to content

Commit 53c27e0

Browse files
committed
libimage: support parallel tag/untag
The c/storage SetNames API is depracated because it is not race free to first get the list of names and then append our new name then write the full list back. Instead a better Add/RemovesNames API has been added. Tag and Untag should use these to prevent race conditions that can be easily reproduce using podman tag in parallel. Tests have been added to ensure it is working correctly. Fixes containers/podman#17515 Signed-off-by: Paul Holzinger <pholzing@redhat.com>
1 parent 493ab45 commit 53c27e0

File tree

2 files changed

+78
-49
lines changed

2 files changed

+78
-49
lines changed

libimage/image.go

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -580,17 +580,13 @@ func (i *Image) Tag(name string) error {
580580
defer i.runtime.writeEvent(&Event{ID: i.ID(), Name: name, Time: time.Now(), Type: EventTypeImageTag})
581581
}
582582

583-
newNames := append(i.Names(), ref.String())
584-
if err := i.runtime.store.SetNames(i.ID(), newNames); err != nil {
583+
if err := i.runtime.store.AddNames(i.ID(), []string{ref.String()}); err != nil {
585584
return err
586585
}
587586

588587
return i.reload()
589588
}
590589

591-
// to have some symmetry with the errors from containers/storage.
592-
var errTagUnknown = errors.New("tag not known")
593-
594590
// TODO (@vrothberg) - `docker rmi sha256:` will remove the digest from the
595591
// image. However, that's something containers storage does not support.
596592
var errUntagDigest = errors.New("untag by digest not supported")
@@ -625,21 +621,7 @@ func (i *Image) Untag(name string) error {
625621
defer i.runtime.writeEvent(&Event{ID: i.ID(), Name: name, Time: time.Now(), Type: EventTypeImageUntag})
626622
}
627623

628-
removedName := false
629-
newNames := []string{}
630-
for _, n := range i.Names() {
631-
if n == name {
632-
removedName = true
633-
continue
634-
}
635-
newNames = append(newNames, n)
636-
}
637-
638-
if !removedName {
639-
return fmt.Errorf("%s: %w", name, errTagUnknown)
640-
}
641-
642-
if err := i.runtime.store.SetNames(i.ID(), newNames); err != nil {
624+
if err := i.runtime.store.RemoveNames(i.ID(), []string{name}); err != nil {
643625
return err
644626
}
645627

libimage/image_test.go

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package libimage
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"os"
8+
"sync"
79
"testing"
810

911
"github.com/containers/common/pkg/config"
@@ -261,21 +263,8 @@ func TestInspectHealthcheck(t *testing.T) {
261263
}
262264

263265
func TestTag(t *testing.T) {
264-
// Note: this will resolve pull from the GCR registry (see
265-
// testdata/registries.conf).
266-
busyboxLatest := "docker.io/library/busybox:latest"
267-
268-
runtime, cleanup := testNewRuntime(t)
266+
runtime, image, cleanup := getImageAndRuntime(t)
269267
defer cleanup()
270-
ctx := context.Background()
271-
272-
pullOptions := &PullOptions{}
273-
pullOptions.Writer = os.Stdout
274-
pulledImages, err := runtime.Pull(ctx, busyboxLatest, config.PullPolicyMissing, pullOptions)
275-
require.NoError(t, err)
276-
require.Len(t, pulledImages, 1)
277-
278-
image := pulledImages[0]
279268

280269
digest := "sha256:adab3844f497ab9171f070d4cae4114b5aec565ac772e2f2579405b78be67c96"
281270

@@ -307,26 +296,65 @@ func TestTag(t *testing.T) {
307296
}
308297

309298
// Check for specific error.
310-
err = image.Tag("foo@" + digest)
299+
err := image.Tag("foo@" + digest)
311300
require.True(t, errors.Is(err, errTagDigest), "check for specific digest error")
312301
}
313302

314-
func TestUntag(t *testing.T) {
315-
// Note: this will resolve pull from the GCR registry (see
316-
// testdata/registries.conf).
317-
busyboxLatest := "docker.io/library/busybox:latest"
318-
319-
runtime, cleanup := testNewRuntime(t)
303+
func TestTagAndUntagParallel(t *testing.T) {
304+
runtime, image, cleanup := getImageAndRuntime(t)
320305
defer cleanup()
321-
ctx := context.Background()
322306

323-
pullOptions := &PullOptions{}
324-
pullOptions.Writer = os.Stdout
325-
pulledImages, err := runtime.Pull(ctx, busyboxLatest, config.PullPolicyMissing, pullOptions)
326-
require.NoError(t, err)
327-
require.Len(t, pulledImages, 1)
307+
tagCount := 10
308+
wg := sync.WaitGroup{}
328309

329-
image := pulledImages[0]
310+
origNames := image.Names()
311+
312+
names := make([]string, 0, tagCount)
313+
names = append(names, origNames...)
314+
315+
// Test tag in parallel, the extra go routine is critical for the test do not remove that.
316+
wg.Add(tagCount)
317+
for i := 0; i < tagCount; i++ {
318+
name := fmt.Sprintf("localhost/tag-%d:latest", i)
319+
names = append(names, name)
320+
go func(name string) {
321+
defer wg.Done()
322+
err := image.Tag(name)
323+
require.NoError(t, err, "parallel tag should have succeeded")
324+
}(name)
325+
}
326+
327+
// wait for all routines to finish
328+
wg.Wait()
329+
330+
newImg, _, err := runtime.LookupImage(image.ID(), nil)
331+
require.NoError(t, err, "image should have resolved locally")
332+
// Note use ElementsMatch because teh order is unspecified to the parallel nature
333+
require.ElementsMatch(t, names, newImg.Names(), "tag image names should contain same elements")
334+
335+
// Test untag in parallel
336+
wg.Add(tagCount)
337+
for i := 0; i < tagCount; i++ {
338+
name := fmt.Sprintf("localhost/tag-%d:latest", i)
339+
names = append(names, name)
340+
go func(name string) {
341+
defer wg.Done()
342+
err := image.Untag(name)
343+
require.NoError(t, err, "parallel untag should have succeeded")
344+
}(name)
345+
}
346+
// wait for all routines to finish
347+
wg.Wait()
348+
349+
newImg, _, err = runtime.LookupImage(image.ID(), nil)
350+
require.NoError(t, err, "image should have resolved locally")
351+
// Note use ElementsMatch because teh order is unspecified to the parallel nature
352+
require.ElementsMatch(t, origNames, newImg.Names(), "untag image names should contain same elements")
353+
}
354+
355+
func TestUntag(t *testing.T) {
356+
runtime, image, cleanup := getImageAndRuntime(t)
357+
defer cleanup()
330358

331359
digest := "sha256:adab3844f497ab9171f070d4cae4114b5aec565ac772e2f2579405b78be67c96"
332360

@@ -360,6 +388,25 @@ func TestUntag(t *testing.T) {
360388
}
361389

362390
// Check for specific error.
363-
err = image.Untag(digest)
391+
err := image.Untag(digest)
364392
require.True(t, errors.Is(err, errUntagDigest), "check for specific digest error")
365393
}
394+
395+
func getImageAndRuntime(t *testing.T) (*Runtime, *Image, func()) {
396+
// Note: this will resolve pull from the GCR registry (see
397+
// testdata/registries.conf).
398+
busyboxLatest := "docker.io/library/busybox:latest"
399+
400+
runtime, cleanup := testNewRuntime(t)
401+
ctx := context.Background()
402+
403+
pullOptions := &PullOptions{}
404+
pullOptions.Writer = os.Stdout
405+
pulledImages, err := runtime.Pull(ctx, busyboxLatest, config.PullPolicyMissing, pullOptions)
406+
require.NoError(t, err)
407+
require.Len(t, pulledImages, 1)
408+
409+
image := pulledImages[0]
410+
411+
return runtime, image, cleanup
412+
}

0 commit comments

Comments
 (0)