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 https://github.com/containers/podman/issues/17515

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
Paul Holzinger 2023-10-19 14:36:18 +02:00
parent 45c7b83635
commit a4a7c28aef
2 changed files with 77 additions and 49 deletions

View File

@ -580,17 +580,13 @@ func (i *Image) Tag(name string) error {
defer i.runtime.writeEvent(&Event{ID: i.ID(), Name: name, Time: time.Now(), Type: EventTypeImageTag}) defer i.runtime.writeEvent(&Event{ID: i.ID(), Name: name, Time: time.Now(), Type: EventTypeImageTag})
} }
newNames := append(i.Names(), ref.String()) if err := i.runtime.store.AddNames(i.ID(), []string{ref.String()}); err != nil {
if err := i.runtime.store.SetNames(i.ID(), newNames); err != nil {
return err return err
} }
return i.reload() return i.reload()
} }
// to have some symmetry with the errors from containers/storage.
var errTagUnknown = errors.New("tag not known")
// TODO (@vrothberg) - `docker rmi sha256:` will remove the digest from the // TODO (@vrothberg) - `docker rmi sha256:` will remove the digest from the
// image. However, that's something containers storage does not support. // image. However, that's something containers storage does not support.
var errUntagDigest = errors.New("untag by digest not supported") var errUntagDigest = errors.New("untag by digest not supported")
@ -625,21 +621,7 @@ func (i *Image) Untag(name string) error {
defer i.runtime.writeEvent(&Event{ID: i.ID(), Name: name, Time: time.Now(), Type: EventTypeImageUntag}) defer i.runtime.writeEvent(&Event{ID: i.ID(), Name: name, Time: time.Now(), Type: EventTypeImageUntag})
} }
removedName := false if err := i.runtime.store.RemoveNames(i.ID(), []string{name}); err != nil {
newNames := []string{}
for _, n := range i.Names() {
if n == name {
removedName = true
continue
}
newNames = append(newNames, n)
}
if !removedName {
return fmt.Errorf("%s: %w", name, errTagUnknown)
}
if err := i.runtime.store.SetNames(i.ID(), newNames); err != nil {
return err return err
} }

View File

@ -3,7 +3,9 @@ package libimage
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"os" "os"
"sync"
"testing" "testing"
"github.com/containers/common/pkg/config" "github.com/containers/common/pkg/config"
@ -261,21 +263,8 @@ func TestInspectHealthcheck(t *testing.T) {
} }
func TestTag(t *testing.T) { func TestTag(t *testing.T) {
// Note: this will resolve pull from the GCR registry (see runtime, image, cleanup := getImageAndRuntime(t)
// testdata/registries.conf).
busyboxLatest := "docker.io/library/busybox:latest"
runtime, cleanup := testNewRuntime(t)
defer cleanup() defer cleanup()
ctx := context.Background()
pullOptions := &PullOptions{}
pullOptions.Writer = os.Stdout
pulledImages, err := runtime.Pull(ctx, busyboxLatest, config.PullPolicyMissing, pullOptions)
require.NoError(t, err)
require.Len(t, pulledImages, 1)
image := pulledImages[0]
digest := "sha256:adab3844f497ab9171f070d4cae4114b5aec565ac772e2f2579405b78be67c96" digest := "sha256:adab3844f497ab9171f070d4cae4114b5aec565ac772e2f2579405b78be67c96"
@ -307,26 +296,64 @@ func TestTag(t *testing.T) {
} }
// Check for specific error. // Check for specific error.
err = image.Tag("foo@" + digest) err := image.Tag("foo@" + digest)
require.True(t, errors.Is(err, errTagDigest), "check for specific digest error") require.True(t, errors.Is(err, errTagDigest), "check for specific digest error")
} }
func TestUntag(t *testing.T) { func TestTagAndUntagParallel(t *testing.T) {
// Note: this will resolve pull from the GCR registry (see runtime, image, cleanup := getImageAndRuntime(t)
// testdata/registries.conf).
busyboxLatest := "docker.io/library/busybox:latest"
runtime, cleanup := testNewRuntime(t)
defer cleanup() defer cleanup()
ctx := context.Background()
pullOptions := &PullOptions{} tagCount := 10
pullOptions.Writer = os.Stdout wg := sync.WaitGroup{}
pulledImages, err := runtime.Pull(ctx, busyboxLatest, config.PullPolicyMissing, pullOptions)
require.NoError(t, err)
require.Len(t, pulledImages, 1)
image := pulledImages[0] origNames := image.Names()
names := make([]string, 0, tagCount)
names = append(names, origNames...)
// Test tag in parallel, the extra go routine is critical for the test do not remove that.
wg.Add(tagCount)
for i := 0; i < tagCount; i++ {
name := fmt.Sprintf("localhost/tag-%d:latest", i)
names = append(names, name)
go func(name string) {
defer wg.Done()
err := image.Tag(name)
require.NoError(t, err, "parallel tag should have succeeded")
}(name)
}
// wait for all routines to finish
wg.Wait()
newImg, _, err := runtime.LookupImage(image.ID(), nil)
require.NoError(t, err, "image should have resolved locally")
// Note use ElementsMatch because the order is unspecified to the parallel nature
require.ElementsMatch(t, names, newImg.Names(), "tag image names should contain same elements")
// Test untag in parallel
wg.Add(tagCount)
for i := 0; i < tagCount; i++ {
name := fmt.Sprintf("localhost/tag-%d:latest", i)
names = append(names, name)
go func(name string) {
defer wg.Done()
err := image.Untag(name)
require.NoError(t, err, "parallel untag should have succeeded")
}(name)
}
// wait for all routines to finish
wg.Wait()
newImg, _, err = runtime.LookupImage(image.ID(), nil)
require.NoError(t, err, "image should have resolved locally")
require.Equal(t, origNames, newImg.Names(), "untag image names should contain same elements")
}
func TestUntag(t *testing.T) {
runtime, image, cleanup := getImageAndRuntime(t)
defer cleanup()
digest := "sha256:adab3844f497ab9171f070d4cae4114b5aec565ac772e2f2579405b78be67c96" digest := "sha256:adab3844f497ab9171f070d4cae4114b5aec565ac772e2f2579405b78be67c96"
@ -360,6 +387,25 @@ func TestUntag(t *testing.T) {
} }
// Check for specific error. // Check for specific error.
err = image.Untag(digest) err := image.Untag(digest)
require.True(t, errors.Is(err, errUntagDigest), "check for specific digest error") require.True(t, errors.Is(err, errUntagDigest), "check for specific digest error")
} }
func getImageAndRuntime(t *testing.T) (*Runtime, *Image, func()) {
// Note: this will resolve pull from the GCR registry (see
// testdata/registries.conf).
busyboxLatest := "docker.io/library/busybox:latest"
runtime, cleanup := testNewRuntime(t)
ctx := context.Background()
pullOptions := &PullOptions{}
pullOptions.Writer = os.Stdout
pulledImages, err := runtime.Pull(ctx, busyboxLatest, config.PullPolicyMissing, pullOptions)
require.NoError(t, err)
require.Len(t, pulledImages, 1)
image := pulledImages[0]
return runtime, image, cleanup
}