Create image reference cache.
Signed-off-by: Lantao Liu <lantaol@google.com>
This commit is contained in:
		| @@ -115,13 +115,9 @@ func (c *criService) CreateContainer(ctx context.Context, r *runtime.CreateConta | ||||
|  | ||||
| 	// Prepare container image snapshot. For container, the image should have | ||||
| 	// been pulled before creating the container, so do not ensure the image. | ||||
| 	imageRef := config.GetImage().GetImage() | ||||
| 	image, err := c.localResolve(ctx, imageRef) | ||||
| 	image, err := c.localResolve(config.GetImage().GetImage()) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrapf(err, "failed to resolve image %q", imageRef) | ||||
| 	} | ||||
| 	if image == nil { | ||||
| 		return nil, errors.Errorf("image %q not found", imageRef) | ||||
| 		return nil, errors.Wrapf(err, "failed to resolve image %q", config.GetImage().GetImage()) | ||||
| 	} | ||||
|  | ||||
| 	// Run container using the same runtime with sandbox. | ||||
|   | ||||
| @@ -46,14 +46,15 @@ func (c *criService) ContainerStatus(ctx context.Context, r *runtime.ContainerSt | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrapf(err, "failed to get image %q", imageRef) | ||||
| 	} | ||||
| 	if len(image.RepoTags) > 0 { | ||||
| 	repoTags, repoDigests := parseImageReferences(image.References) | ||||
| 	if len(repoTags) > 0 { | ||||
| 		// Based on current behavior of dockershim, this field should be | ||||
| 		// image tag. | ||||
| 		spec = &runtime.ImageSpec{Image: image.RepoTags[0]} | ||||
| 		spec = &runtime.ImageSpec{Image: repoTags[0]} | ||||
| 	} | ||||
| 	if len(image.RepoDigests) > 0 { | ||||
| 	if len(repoDigests) > 0 { | ||||
| 		// Based on the CRI definition, this field will be consumed by user. | ||||
| 		imageRef = image.RepoDigests[0] | ||||
| 		imageRef = repoDigests[0] | ||||
| 	} | ||||
| 	status := toCRIContainerStatus(container, spec, imageRef) | ||||
| 	info, err := toCRIContainerInfo(ctx, container, r.GetVerbose()) | ||||
|   | ||||
| @@ -63,9 +63,11 @@ func getContainerStatusTestData() (*containerstore.Metadata, *containerstore.Sta | ||||
| 		StartedAt: startedAt, | ||||
| 	} | ||||
| 	image := &imagestore.Image{ | ||||
| 		ID:          imageID, | ||||
| 		RepoTags:    []string{"test-image-repo-tag"}, | ||||
| 		RepoDigests: []string{"test-image-repo-digest"}, | ||||
| 		ID: imageID, | ||||
| 		References: []string{ | ||||
| 			"gcr.io/library/busybox:latest", | ||||
| 			"gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 		}, | ||||
| 	} | ||||
| 	expected := &runtime.ContainerStatus{ | ||||
| 		Id:          testID, | ||||
| @@ -73,8 +75,8 @@ func getContainerStatusTestData() (*containerstore.Metadata, *containerstore.Sta | ||||
| 		State:       runtime.ContainerState_CONTAINER_RUNNING, | ||||
| 		CreatedAt:   createdAt, | ||||
| 		StartedAt:   startedAt, | ||||
| 		Image:       &runtime.ImageSpec{Image: "test-image-repo-tag"}, | ||||
| 		ImageRef:    "test-image-repo-digest", | ||||
| 		Image:       &runtime.ImageSpec{Image: "gcr.io/library/busybox:latest"}, | ||||
| 		ImageRef:    "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 		Reason:      completeExitReason, | ||||
| 		Labels:      config.GetLabels(), | ||||
| 		Annotations: config.GetAnnotations(), | ||||
| @@ -120,7 +122,7 @@ func TestToCRIContainerStatus(t *testing.T) { | ||||
| 			expectedReason: errorExitReason, | ||||
| 		}, | ||||
| 	} { | ||||
| 		metadata, status, image, expected := getContainerStatusTestData() | ||||
| 		metadata, status, _, expected := getContainerStatusTestData() | ||||
| 		// Update status with test case. | ||||
| 		status.FinishedAt = test.finishedAt | ||||
| 		status.ExitCode = test.exitCode | ||||
| @@ -138,8 +140,8 @@ func TestToCRIContainerStatus(t *testing.T) { | ||||
| 		expected.ExitCode = test.exitCode | ||||
| 		expected.Message = test.message | ||||
| 		containerStatus := toCRIContainerStatus(container, | ||||
| 			&runtime.ImageSpec{Image: image.RepoTags[0]}, | ||||
| 			image.RepoDigests[0]) | ||||
| 			expected.Image, | ||||
| 			expected.ImageRef) | ||||
| 		assert.Equal(t, expected, containerStatus, desc) | ||||
| 	} | ||||
| } | ||||
| @@ -207,7 +209,8 @@ func TestContainerStatus(t *testing.T) { | ||||
| 			assert.NoError(t, c.containerStore.Add(container)) | ||||
| 		} | ||||
| 		if test.imageExist { | ||||
| 			c.imageStore.Add(*image) | ||||
| 			c.imageStore, err = imagestore.NewFakeStore([]imagestore.Image{*image}) | ||||
| 			assert.NoError(t, err) | ||||
| 		} | ||||
| 		resp, err := c.ContainerStatus(context.Background(), &runtime.ContainerStatusRequest{ContainerId: container.ID}) | ||||
| 		if test.expectErr { | ||||
|   | ||||
| @@ -34,6 +34,7 @@ import ( | ||||
| 	ctrdutil "github.com/containerd/cri/pkg/containerd/util" | ||||
| 	"github.com/containerd/cri/pkg/store" | ||||
| 	containerstore "github.com/containerd/cri/pkg/store/container" | ||||
| 	imagestore "github.com/containerd/cri/pkg/store/image" | ||||
| 	sandboxstore "github.com/containerd/cri/pkg/store/sandbox" | ||||
| ) | ||||
|  | ||||
| @@ -49,6 +50,7 @@ const ( | ||||
| type eventMonitor struct { | ||||
| 	containerStore *containerstore.Store | ||||
| 	sandboxStore   *sandboxstore.Store | ||||
| 	imageStore     *imagestore.Store | ||||
| 	ch             <-chan *events.Envelope | ||||
| 	errCh          <-chan error | ||||
| 	ctx            context.Context | ||||
| @@ -76,12 +78,13 @@ type backOffQueue struct { | ||||
|  | ||||
| // Create new event monitor. New event monitor will start subscribing containerd event. All events | ||||
| // happen after it should be monitored. | ||||
| func newEventMonitor(c *containerstore.Store, s *sandboxstore.Store) *eventMonitor { | ||||
| func newEventMonitor(c *containerstore.Store, s *sandboxstore.Store, i *imagestore.Store) *eventMonitor { | ||||
| 	// event subscribe doesn't need namespace. | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	return &eventMonitor{ | ||||
| 		containerStore: c, | ||||
| 		sandboxStore:   s, | ||||
| 		imageStore:     i, | ||||
| 		ctx:            ctx, | ||||
| 		cancel:         cancel, | ||||
| 		backOff:        newBackOff(), | ||||
| @@ -93,12 +96,13 @@ func (em *eventMonitor) subscribe(subscriber events.Subscriber) { | ||||
| 	filters := []string{ | ||||
| 		`topic=="/tasks/exit"`, | ||||
| 		`topic=="/tasks/oom"`, | ||||
| 		`topic~="/images/"`, | ||||
| 	} | ||||
| 	em.ch, em.errCh = subscriber.Subscribe(em.ctx, filters...) | ||||
| } | ||||
|  | ||||
| func convertEvent(e *gogotypes.Any) (string, interface{}, error) { | ||||
| 	containerID := "" | ||||
| 	id := "" | ||||
| 	evt, err := typeurl.UnmarshalAny(e) | ||||
| 	if err != nil { | ||||
| 		return "", nil, errors.Wrap(err, "failed to unmarshalany") | ||||
| @@ -106,16 +110,22 @@ func convertEvent(e *gogotypes.Any) (string, interface{}, error) { | ||||
|  | ||||
| 	switch evt.(type) { | ||||
| 	case *eventtypes.TaskExit: | ||||
| 		containerID = evt.(*eventtypes.TaskExit).ContainerID | ||||
| 		id = evt.(*eventtypes.TaskExit).ContainerID | ||||
| 	case *eventtypes.TaskOOM: | ||||
| 		containerID = evt.(*eventtypes.TaskOOM).ContainerID | ||||
| 		id = evt.(*eventtypes.TaskOOM).ContainerID | ||||
| 	case *eventtypes.ImageCreate: | ||||
| 		id = evt.(*eventtypes.ImageCreate).Name | ||||
| 	case *eventtypes.ImageUpdate: | ||||
| 		id = evt.(*eventtypes.ImageUpdate).Name | ||||
| 	case *eventtypes.ImageDelete: | ||||
| 		id = evt.(*eventtypes.ImageDelete).Name | ||||
| 	default: | ||||
| 		return "", nil, errors.New("unsupported event") | ||||
| 	} | ||||
| 	return containerID, evt, nil | ||||
| 	return id, evt, nil | ||||
| } | ||||
|  | ||||
| // start starts the event monitor which monitors and handles all container events. It returns | ||||
| // start starts the event monitor which monitors and handles all subscribed events. It returns | ||||
| // an error channel for the caller to wait for stop errors from the event monitor. | ||||
| // start must be called after subscribe. | ||||
| func (em *eventMonitor) start() <-chan error { | ||||
| @@ -130,19 +140,19 @@ func (em *eventMonitor) start() <-chan error { | ||||
| 			select { | ||||
| 			case e := <-em.ch: | ||||
| 				logrus.Debugf("Received containerd event timestamp - %v, namespace - %q, topic - %q", e.Timestamp, e.Namespace, e.Topic) | ||||
| 				cID, evt, err := convertEvent(e.Event) | ||||
| 				id, evt, err := convertEvent(e.Event) | ||||
| 				if err != nil { | ||||
| 					logrus.WithError(err).Errorf("Failed to convert event %+v", e) | ||||
| 					break | ||||
| 				} | ||||
| 				if em.backOff.isInBackOff(cID) { | ||||
| 					logrus.Infof("Events for container %q is in backoff, enqueue event %+v", cID, evt) | ||||
| 					em.backOff.enBackOff(cID, evt) | ||||
| 				if em.backOff.isInBackOff(id) { | ||||
| 					logrus.Infof("Events for %q is in backoff, enqueue event %+v", id, evt) | ||||
| 					em.backOff.enBackOff(id, evt) | ||||
| 					break | ||||
| 				} | ||||
| 				if err := em.handleEvent(evt); err != nil { | ||||
| 					logrus.WithError(err).Errorf("Failed to handle event %+v for container %s", evt, cID) | ||||
| 					em.backOff.enBackOff(cID, evt) | ||||
| 					logrus.WithError(err).Errorf("Failed to handle event %+v for %s", evt, id) | ||||
| 					em.backOff.enBackOff(id, evt) | ||||
| 				} | ||||
| 			case err := <-em.errCh: | ||||
| 				// Close errCh in defer directly if there is no error. | ||||
| @@ -152,13 +162,13 @@ func (em *eventMonitor) start() <-chan error { | ||||
| 				} | ||||
| 				return | ||||
| 			case <-backOffCheckCh: | ||||
| 				cIDs := em.backOff.getExpiredContainers() | ||||
| 				for _, cID := range cIDs { | ||||
| 					queue := em.backOff.deBackOff(cID) | ||||
| 				ids := em.backOff.getExpiredIDs() | ||||
| 				for _, id := range ids { | ||||
| 					queue := em.backOff.deBackOff(id) | ||||
| 					for i, any := range queue.events { | ||||
| 						if err := em.handleEvent(any); err != nil { | ||||
| 							logrus.WithError(err).Errorf("Failed to handle backOff event %+v for container %s", any, cID) | ||||
| 							em.backOff.reBackOff(cID, queue.events[i:], queue.duration) | ||||
| 							logrus.WithError(err).Errorf("Failed to handle backOff event %+v for %s", any, id) | ||||
| 							em.backOff.reBackOff(id, queue.events[i:], queue.duration) | ||||
| 							break | ||||
| 						} | ||||
| 					} | ||||
| @@ -230,6 +240,18 @@ func (em *eventMonitor) handleEvent(any interface{}) error { | ||||
| 		if err != nil { | ||||
| 			return errors.Wrap(err, "failed to update container status for TaskOOM event") | ||||
| 		} | ||||
| 	case *eventtypes.ImageCreate: | ||||
| 		e := any.(*eventtypes.ImageCreate) | ||||
| 		logrus.Infof("ImageCreate event %+v", e) | ||||
| 		return em.imageStore.Update(ctx, e.Name) | ||||
| 	case *eventtypes.ImageUpdate: | ||||
| 		e := any.(*eventtypes.ImageUpdate) | ||||
| 		logrus.Infof("ImageUpdate event %+v", e) | ||||
| 		return em.imageStore.Update(ctx, e.Name) | ||||
| 	case *eventtypes.ImageDelete: | ||||
| 		e := any.(*eventtypes.ImageDelete) | ||||
| 		logrus.Infof("ImageDelete event %+v", e) | ||||
| 		return em.imageStore.Update(ctx, e.Name) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| @@ -331,14 +353,14 @@ func newBackOff() *backOff { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (b *backOff) getExpiredContainers() []string { | ||||
| 	var containers []string | ||||
| 	for c, q := range b.queuePool { | ||||
| func (b *backOff) getExpiredIDs() []string { | ||||
| 	var ids []string | ||||
| 	for id, q := range b.queuePool { | ||||
| 		if q.isExpire() { | ||||
| 			containers = append(containers, c) | ||||
| 			ids = append(ids, id) | ||||
| 		} | ||||
| 	} | ||||
| 	return containers | ||||
| 	return ids | ||||
| } | ||||
|  | ||||
| func (b *backOff) isInBackOff(key string) bool { | ||||
|   | ||||
| @@ -94,11 +94,11 @@ func TestBackOff(t *testing.T) { | ||||
| 	assert.Equal(t, actual.isInBackOff(notExistKey), false) | ||||
|  | ||||
| 	t.Logf("No containers should be expired") | ||||
| 	assert.Empty(t, actual.getExpiredContainers()) | ||||
| 	assert.Empty(t, actual.getExpiredIDs()) | ||||
|  | ||||
| 	t.Logf("Should be able to get all keys which are expired for backOff") | ||||
| 	testClock.Sleep(backOffInitDuration) | ||||
| 	actKeyList := actual.getExpiredContainers() | ||||
| 	actKeyList := actual.getExpiredIDs() | ||||
| 	assert.Equal(t, len(inputQueues), len(actKeyList)) | ||||
| 	for k := range inputQueues { | ||||
| 		assert.Contains(t, actKeyList, k) | ||||
|   | ||||
| @@ -17,7 +17,6 @@ limitations under the License. | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path" | ||||
| @@ -26,15 +25,11 @@ import ( | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/containerd/containerd" | ||||
| 	"github.com/containerd/containerd/containers" | ||||
| 	"github.com/containerd/containerd/content" | ||||
| 	"github.com/containerd/containerd/runtime/linux/runctypes" | ||||
| 	"github.com/containerd/typeurl" | ||||
| 	"github.com/docker/distribution/reference" | ||||
| 	imagedigest "github.com/opencontainers/go-digest" | ||||
| 	"github.com/opencontainers/image-spec/identity" | ||||
| 	imagespec "github.com/opencontainers/image-spec/specs-go/v1" | ||||
| 	runtimespec "github.com/opencontainers/runtime-spec/specs-go" | ||||
| 	"github.com/opencontainers/runtime-tools/generate" | ||||
| 	"github.com/opencontainers/selinux/go-selinux" | ||||
| @@ -236,28 +231,25 @@ func getRepoDigestAndTag(namedRef reference.Named, digest imagedigest.Digest, sc | ||||
| 	return repoDigest, repoTag | ||||
| } | ||||
|  | ||||
| // localResolve resolves image reference locally and returns corresponding image metadata. It returns | ||||
| // nil without error if the reference doesn't exist. | ||||
| func (c *criService) localResolve(ctx context.Context, refOrID string) (*imagestore.Image, error) { | ||||
| // localResolve resolves image reference locally and returns corresponding image metadata. It | ||||
| // returns store.ErrNotExist if the reference doesn't exist. | ||||
| func (c *criService) localResolve(refOrID string) (imagestore.Image, error) { | ||||
| 	getImageID := func(refOrId string) string { | ||||
| 		if _, err := imagedigest.Parse(refOrID); err == nil { | ||||
| 			return refOrID | ||||
| 		} | ||||
| 		return func(ref string) string { | ||||
| 			// ref is not image id, try to resolve it locally. | ||||
| 			// TODO(random-liu): Handle this error better for debugging. | ||||
| 			normalized, err := util.NormalizeImageRef(ref) | ||||
| 			if err != nil { | ||||
| 				return "" | ||||
| 			} | ||||
| 			image, err := c.client.GetImage(ctx, normalized.String()) | ||||
| 			id, err := c.imageStore.Resolve(normalized.String()) | ||||
| 			if err != nil { | ||||
| 				return "" | ||||
| 			} | ||||
| 			desc, err := image.Config(ctx) | ||||
| 			if err != nil { | ||||
| 				return "" | ||||
| 			} | ||||
| 			return desc.Digest.String() | ||||
| 			return id | ||||
| 		}(refOrID) | ||||
| 	} | ||||
|  | ||||
| @@ -266,14 +258,7 @@ func (c *criService) localResolve(ctx context.Context, refOrID string) (*imagest | ||||
| 		// Try to treat ref as imageID | ||||
| 		imageID = refOrID | ||||
| 	} | ||||
| 	image, err := c.imageStore.Get(imageID) | ||||
| 	if err != nil { | ||||
| 		if err == store.ErrNotExist { | ||||
| 			return nil, nil | ||||
| 		} | ||||
| 		return nil, errors.Wrapf(err, "failed to get image %q", imageID) | ||||
| 	} | ||||
| 	return &image, nil | ||||
| 	return c.imageStore.Get(imageID) | ||||
| } | ||||
|  | ||||
| // getUserFromImage gets uid or user name of the image user. | ||||
| @@ -298,12 +283,12 @@ func getUserFromImage(user string) (*int64, string) { | ||||
| // ensureImageExists returns corresponding metadata of the image reference, if image is not | ||||
| // pulled yet, the function will pull the image. | ||||
| func (c *criService) ensureImageExists(ctx context.Context, ref string) (*imagestore.Image, error) { | ||||
| 	image, err := c.localResolve(ctx, ref) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrapf(err, "failed to resolve image %q", ref) | ||||
| 	image, err := c.localResolve(ref) | ||||
| 	if err != nil && err != store.ErrNotExist { | ||||
| 		return nil, errors.Wrapf(err, "failed to get image %q", ref) | ||||
| 	} | ||||
| 	if image != nil { | ||||
| 		return image, nil | ||||
| 	if err == nil { | ||||
| 		return &image, nil | ||||
| 	} | ||||
| 	// Pull image to ensure the image exists | ||||
| 	resp, err := c.PullImage(ctx, &runtime.PullImageRequest{Image: &runtime.ImageSpec{Image: ref}}) | ||||
| @@ -314,56 +299,11 @@ func (c *criService) ensureImageExists(ctx context.Context, ref string) (*images | ||||
| 	newImage, err := c.imageStore.Get(imageID) | ||||
| 	if err != nil { | ||||
| 		// It's still possible that someone removed the image right after it is pulled. | ||||
| 		return nil, errors.Wrapf(err, "failed to get image %q metadata after pulling", imageID) | ||||
| 		return nil, errors.Wrapf(err, "failed to get image %q after pulling", imageID) | ||||
| 	} | ||||
| 	return &newImage, nil | ||||
| } | ||||
|  | ||||
| // imageInfo is the information about the image got from containerd. | ||||
| type imageInfo struct { | ||||
| 	id        string | ||||
| 	chainID   imagedigest.Digest | ||||
| 	size      int64 | ||||
| 	imagespec imagespec.Image | ||||
| } | ||||
|  | ||||
| // getImageInfo gets image info from containerd. | ||||
| func getImageInfo(ctx context.Context, image containerd.Image) (*imageInfo, error) { | ||||
| 	// Get image information. | ||||
| 	diffIDs, err := image.RootFS(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "failed to get image diffIDs") | ||||
| 	} | ||||
| 	chainID := identity.ChainID(diffIDs) | ||||
|  | ||||
| 	size, err := image.Size(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "failed to get image compressed resource size") | ||||
| 	} | ||||
|  | ||||
| 	desc, err := image.Config(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "failed to get image config descriptor") | ||||
| 	} | ||||
| 	id := desc.Digest.String() | ||||
|  | ||||
| 	rb, err := content.ReadBlob(ctx, image.ContentStore(), desc) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "failed to read image config from content store") | ||||
| 	} | ||||
| 	var ociimage imagespec.Image | ||||
| 	if err := json.Unmarshal(rb, &ociimage); err != nil { | ||||
| 		return nil, errors.Wrapf(err, "failed to unmarshal image config %s", rb) | ||||
| 	} | ||||
|  | ||||
| 	return &imageInfo{ | ||||
| 		id:        id, | ||||
| 		chainID:   chainID, | ||||
| 		size:      size, | ||||
| 		imagespec: ociimage, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func initSelinuxOpts(selinuxOpt *runtime.SELinuxOption) (string, string, error) { | ||||
| 	if selinuxOpt == nil { | ||||
| 		return "", "", nil | ||||
| @@ -500,3 +440,21 @@ func (m orderedMounts) Swap(i, j int) { | ||||
| func (m orderedMounts) parts(i int) int { | ||||
| 	return strings.Count(filepath.Clean(m[i].ContainerPath), string(os.PathSeparator)) | ||||
| } | ||||
|  | ||||
| // parseImageReferences parses a list of arbitrary image references and returns | ||||
| // the repotags and repodigests | ||||
| func parseImageReferences(refs []string) ([]string, []string) { | ||||
| 	var tags, digests []string | ||||
| 	for _, ref := range refs { | ||||
| 		parsed, err := reference.ParseAnyReference(ref) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		if _, ok := parsed.(reference.Canonical); ok { | ||||
| 			digests = append(digests, parsed.String()) | ||||
| 		} else if _, ok := parsed.(reference.Tagged); ok { | ||||
| 			tags = append(tags, parsed.String()) | ||||
| 		} | ||||
| 	} | ||||
| 	return tags, digests | ||||
| } | ||||
|   | ||||
| @@ -29,6 +29,8 @@ import ( | ||||
| 	runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2" | ||||
|  | ||||
| 	criconfig "github.com/containerd/cri/pkg/config" | ||||
| 	"github.com/containerd/cri/pkg/store" | ||||
| 	imagestore "github.com/containerd/cri/pkg/store/image" | ||||
| 	"github.com/containerd/cri/pkg/util" | ||||
| ) | ||||
|  | ||||
| @@ -213,3 +215,58 @@ func TestOrderedMounts(t *testing.T) { | ||||
| 	sort.Stable(orderedMounts(mounts)) | ||||
| 	assert.Equal(t, expected, mounts) | ||||
| } | ||||
|  | ||||
| func TestParseImageReferences(t *testing.T) { | ||||
| 	refs := []string{ | ||||
| 		"gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 		"gcr.io/library/busybox:1.2", | ||||
| 		"sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 		"arbitrary-ref", | ||||
| 	} | ||||
| 	expectedTags := []string{ | ||||
| 		"gcr.io/library/busybox:1.2", | ||||
| 	} | ||||
| 	expectedDigests := []string{"gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582"} | ||||
| 	tags, digests := parseImageReferences(refs) | ||||
| 	assert.Equal(t, expectedTags, tags) | ||||
| 	assert.Equal(t, expectedDigests, digests) | ||||
| } | ||||
|  | ||||
| func TestLocalResolve(t *testing.T) { | ||||
| 	image := imagestore.Image{ | ||||
| 		ID:      "sha256:c75bebcdd211f41b3a460c7bf82970ed6c75acaab9cd4c9a4e125b03ca113799", | ||||
| 		ChainID: "test-chain-id-1", | ||||
| 		References: []string{ | ||||
| 			"docker.io/library/busybox:latest", | ||||
| 			"docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 		}, | ||||
| 		Size: 10, | ||||
| 	} | ||||
| 	c := newTestCRIService() | ||||
| 	var err error | ||||
| 	c.imageStore, err = imagestore.NewFakeStore([]imagestore.Image{image}) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	for _, ref := range []string{ | ||||
| 		"sha256:c75bebcdd211f41b3a460c7bf82970ed6c75acaab9cd4c9a4e125b03ca113799", | ||||
| 		"busybox", | ||||
| 		"busybox:latest", | ||||
| 		"busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 		"library/busybox", | ||||
| 		"library/busybox:latest", | ||||
| 		"library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 		"docker.io/busybox", | ||||
| 		"docker.io/busybox:latest", | ||||
| 		"docker.io/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 		"docker.io/library/busybox", | ||||
| 		"docker.io/library/busybox:latest", | ||||
| 		"docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 	} { | ||||
| 		img, err := c.localResolve(ref) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, image, img) | ||||
| 	} | ||||
| 	img, err := c.localResolve("randomid") | ||||
| 	assert.Equal(t, store.ErrNotExist, err) | ||||
| 	assert.Equal(t, imagestore.Image{}, img) | ||||
| } | ||||
|   | ||||
| @@ -19,8 +19,6 @@ package server | ||||
| import ( | ||||
| 	"golang.org/x/net/context" | ||||
| 	runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2" | ||||
|  | ||||
| 	imagestore "github.com/containerd/cri/pkg/store/image" | ||||
| ) | ||||
|  | ||||
| // ListImages lists existing images. | ||||
| @@ -38,19 +36,3 @@ func (c *criService) ListImages(ctx context.Context, r *runtime.ListImagesReques | ||||
|  | ||||
| 	return &runtime.ListImagesResponse{Images: images}, nil | ||||
| } | ||||
|  | ||||
| // toCRIImage converts image to CRI image type. | ||||
| func toCRIImage(image imagestore.Image) *runtime.Image { | ||||
| 	runtimeImage := &runtime.Image{ | ||||
| 		Id:          image.ID, | ||||
| 		RepoTags:    image.RepoTags, | ||||
| 		RepoDigests: image.RepoDigests, | ||||
| 		Size_:       uint64(image.Size), | ||||
| 	} | ||||
| 	uid, username := getUserFromImage(image.ImageSpec.Config.User) | ||||
| 	if uid != nil { | ||||
| 		runtimeImage.Uid = &runtime.Int64Value{Value: *uid} | ||||
| 	} | ||||
| 	runtimeImage.Username = username | ||||
| 	return runtimeImage | ||||
| } | ||||
|   | ||||
| @@ -32,11 +32,13 @@ func TestListImages(t *testing.T) { | ||||
| 	c := newTestCRIService() | ||||
| 	imagesInStore := []imagestore.Image{ | ||||
| 		{ | ||||
| 			ID:          "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			ChainID:     "test-chainid-1", | ||||
| 			RepoTags:    []string{"tag-a-1", "tag-b-1"}, | ||||
| 			RepoDigests: []string{"digest-a-1", "digest-b-1"}, | ||||
| 			Size:        1000, | ||||
| 			ID:      "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			ChainID: "test-chainid-1", | ||||
| 			References: []string{ | ||||
| 				"gcr.io/library/busybox:latest", | ||||
| 				"gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 			}, | ||||
| 			Size: 1000, | ||||
| 			ImageSpec: imagespec.Image{ | ||||
| 				Config: imagespec.ImageConfig{ | ||||
| 					User: "root", | ||||
| @@ -44,11 +46,13 @@ func TestListImages(t *testing.T) { | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:          "sha256:2123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			ChainID:     "test-chainid-2", | ||||
| 			RepoTags:    []string{"tag-a-2", "tag-b-2"}, | ||||
| 			RepoDigests: []string{"digest-a-2", "digest-b-2"}, | ||||
| 			Size:        2000, | ||||
| 			ID:      "sha256:2123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			ChainID: "test-chainid-2", | ||||
| 			References: []string{ | ||||
| 				"gcr.io/library/alpine:latest", | ||||
| 				"gcr.io/library/alpine@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 			}, | ||||
| 			Size: 2000, | ||||
| 			ImageSpec: imagespec.Image{ | ||||
| 				Config: imagespec.ImageConfig{ | ||||
| 					User: "1234:1234", | ||||
| @@ -56,11 +60,13 @@ func TestListImages(t *testing.T) { | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:          "sha256:3123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			ChainID:     "test-chainid-3", | ||||
| 			RepoTags:    []string{"tag-a-3", "tag-b-3"}, | ||||
| 			RepoDigests: []string{"digest-a-3", "digest-b-3"}, | ||||
| 			Size:        3000, | ||||
| 			ID:      "sha256:3123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			ChainID: "test-chainid-3", | ||||
| 			References: []string{ | ||||
| 				"gcr.io/library/ubuntu:latest", | ||||
| 				"gcr.io/library/ubuntu@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 			}, | ||||
| 			Size: 3000, | ||||
| 			ImageSpec: imagespec.Image{ | ||||
| 				Config: imagespec.ImageConfig{ | ||||
| 					User: "nobody", | ||||
| @@ -71,30 +77,30 @@ func TestListImages(t *testing.T) { | ||||
| 	expect := []*runtime.Image{ | ||||
| 		{ | ||||
| 			Id:          "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			RepoTags:    []string{"tag-a-1", "tag-b-1"}, | ||||
| 			RepoDigests: []string{"digest-a-1", "digest-b-1"}, | ||||
| 			RepoTags:    []string{"gcr.io/library/busybox:latest"}, | ||||
| 			RepoDigests: []string{"gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582"}, | ||||
| 			Size_:       uint64(1000), | ||||
| 			Username:    "root", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Id:          "sha256:2123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			RepoTags:    []string{"tag-a-2", "tag-b-2"}, | ||||
| 			RepoDigests: []string{"digest-a-2", "digest-b-2"}, | ||||
| 			RepoTags:    []string{"gcr.io/library/alpine:latest"}, | ||||
| 			RepoDigests: []string{"gcr.io/library/alpine@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582"}, | ||||
| 			Size_:       uint64(2000), | ||||
| 			Uid:         &runtime.Int64Value{Value: 1234}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Id:          "sha256:3123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			RepoTags:    []string{"tag-a-3", "tag-b-3"}, | ||||
| 			RepoDigests: []string{"digest-a-3", "digest-b-3"}, | ||||
| 			RepoTags:    []string{"gcr.io/library/ubuntu:latest"}, | ||||
| 			RepoDigests: []string{"gcr.io/library/ubuntu@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582"}, | ||||
| 			Size_:       uint64(3000), | ||||
| 			Username:    "nobody", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, i := range imagesInStore { | ||||
| 		c.imageStore.Add(i) | ||||
| 	} | ||||
| 	var err error | ||||
| 	c.imageStore, err = imagestore.NewFakeStore(imagesInStore) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	resp, err := c.ListImages(context.Background(), &runtime.ListImagesRequest{}) | ||||
| 	assert.NoError(t, err) | ||||
|   | ||||
| @@ -26,7 +26,6 @@ import ( | ||||
|  | ||||
| 	api "github.com/containerd/cri/pkg/api/v1" | ||||
| 	"github.com/containerd/cri/pkg/containerd/importer" | ||||
| 	imagestore "github.com/containerd/cri/pkg/store/image" | ||||
| ) | ||||
|  | ||||
| // LoadImage loads a image into containerd. | ||||
| @@ -44,33 +43,11 @@ func (c *criService) LoadImage(ctx context.Context, r *api.LoadImageRequest) (*a | ||||
| 		return nil, errors.Wrap(err, "failed to import image") | ||||
| 	} | ||||
| 	for _, repoTag := range repoTags { | ||||
| 		image, err := c.client.GetImage(ctx, repoTag) | ||||
| 		if err != nil { | ||||
| 			return nil, errors.Wrapf(err, "failed to get image %q", repoTag) | ||||
| 		// Update image store to reflect the newest state in containerd. | ||||
| 		if err := c.imageStore.Update(ctx, repoTag); err != nil { | ||||
| 			return nil, errors.Wrapf(err, "failed to update image store %q", repoTag) | ||||
| 		} | ||||
| 		info, err := getImageInfo(ctx, image) | ||||
| 		if err != nil { | ||||
| 			return nil, errors.Wrapf(err, "failed to get image %q info", repoTag) | ||||
| 		} | ||||
| 		id := info.id | ||||
|  | ||||
| 		if err := c.createImageReference(ctx, id, image.Target()); err != nil { | ||||
| 			return nil, errors.Wrapf(err, "failed to create image reference %q", id) | ||||
| 		} | ||||
|  | ||||
| 		img := imagestore.Image{ | ||||
| 			ID:        id, | ||||
| 			RepoTags:  []string{repoTag}, | ||||
| 			ChainID:   info.chainID.String(), | ||||
| 			Size:      info.size, | ||||
| 			ImageSpec: info.imagespec, | ||||
| 			Image:     image, | ||||
| 		} | ||||
|  | ||||
| 		if err := c.imageStore.Add(img); err != nil { | ||||
| 			return nil, errors.Wrapf(err, "failed to add image %q into store", id) | ||||
| 		} | ||||
| 		logrus.Debugf("Imported image with id %q, repo tag %q", id, repoTag) | ||||
| 		logrus.Debugf("Imported image %q", repoTag) | ||||
| 	} | ||||
| 	return &api.LoadImageResponse{Images: repoTags}, nil | ||||
| } | ||||
|   | ||||
| @@ -34,7 +34,6 @@ import ( | ||||
| 	"golang.org/x/net/context" | ||||
| 	runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2" | ||||
|  | ||||
| 	imagestore "github.com/containerd/cri/pkg/store/image" | ||||
| 	"github.com/containerd/cri/pkg/util" | ||||
| ) | ||||
|  | ||||
| @@ -108,49 +107,34 @@ func (c *criService) PullImage(ctx context.Context, r *runtime.PullImageRequest) | ||||
| 		return nil, errors.Wrapf(err, "failed to pull and unpack image %q", ref) | ||||
| 	} | ||||
|  | ||||
| 	// Get image information. | ||||
| 	info, err := getImageInfo(ctx, image) | ||||
| 	configDesc, err := image.Config(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "failed to get image information") | ||||
| 		return nil, errors.Wrap(err, "get image config descriptor") | ||||
| 	} | ||||
| 	imageID := info.id | ||||
| 	imageID := configDesc.Digest.String() | ||||
|  | ||||
| 	repoDigest, repoTag := getRepoDigestAndTag(namedRef, image.Target().Digest, isSchema1) | ||||
| 	for _, r := range []string{repoTag, repoDigest, imageID} { | ||||
| 	for _, r := range []string{repoTag, repoDigest} { | ||||
| 		if r == "" { | ||||
| 			continue | ||||
| 		} | ||||
| 		if err := c.createImageReference(ctx, r, image.Target()); err != nil { | ||||
| 			return nil, errors.Wrapf(err, "failed to update image reference %q", r) | ||||
| 		} | ||||
| 		// Update image store to reflect the newest state in containerd. | ||||
| 		if err := c.imageStore.Update(ctx, r); err != nil { | ||||
| 			return nil, errors.Wrapf(err, "failed to update image store %q", r) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("Pulled image %q with image id %q, repo tag %q, repo digest %q", imageRef, imageID, | ||||
| 		repoTag, repoDigest) | ||||
| 	img := imagestore.Image{ | ||||
| 		ID:        imageID, | ||||
| 		ChainID:   info.chainID.String(), | ||||
| 		Size:      info.size, | ||||
| 		ImageSpec: info.imagespec, | ||||
| 		Image:     image, | ||||
| 	} | ||||
| 	if repoDigest != "" { | ||||
| 		img.RepoDigests = []string{repoDigest} | ||||
| 	} | ||||
| 	if repoTag != "" { | ||||
| 		img.RepoTags = []string{repoTag} | ||||
| 	} | ||||
|  | ||||
| 	if err := c.imageStore.Add(img); err != nil { | ||||
| 		return nil, errors.Wrapf(err, "failed to add image %q into store", img.ID) | ||||
| 	} | ||||
|  | ||||
| 	// NOTE(random-liu): the actual state in containerd is the source of truth, even we maintain | ||||
| 	// in-memory image store, it's only for in-memory indexing. The image could be removed | ||||
| 	// by someone else anytime, before/during/after we create the metadata. We should always | ||||
| 	// check the actual state in containerd before using the image or returning status of the | ||||
| 	// image. | ||||
| 	return &runtime.PullImageResponse{ImageRef: img.ID}, nil | ||||
| 	return &runtime.PullImageResponse{ImageRef: imageID}, nil | ||||
| } | ||||
|  | ||||
| // ParseAuth parses AuthConfig and returns username and password/secret required by containerd. | ||||
|   | ||||
| @@ -20,9 +20,10 @@ import ( | ||||
| 	"github.com/containerd/containerd/errdefs" | ||||
| 	"github.com/containerd/containerd/images" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"golang.org/x/net/context" | ||||
| 	runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2" | ||||
|  | ||||
| 	"github.com/containerd/cri/pkg/store" | ||||
| ) | ||||
|  | ||||
| // RemoveImage removes the image. | ||||
| @@ -32,62 +33,33 @@ import ( | ||||
| // Remove the whole image no matter the it's image id or reference. This is the | ||||
| // semantic defined in CRI now. | ||||
| func (c *criService) RemoveImage(ctx context.Context, r *runtime.RemoveImageRequest) (*runtime.RemoveImageResponse, error) { | ||||
| 	image, err := c.localResolve(ctx, r.GetImage().GetImage()) | ||||
| 	image, err := c.localResolve(r.GetImage().GetImage()) | ||||
| 	if err != nil { | ||||
| 		if err == store.ErrNotExist { | ||||
| 			// return empty without error when image not found. | ||||
| 			return &runtime.RemoveImageResponse{}, nil | ||||
| 		} | ||||
| 		return nil, errors.Wrapf(err, "can not resolve %q locally", r.GetImage().GetImage()) | ||||
| 	} | ||||
| 	if image == nil { | ||||
| 		// return empty without error when image not found. | ||||
| 		return &runtime.RemoveImageResponse{}, nil | ||||
| 	} | ||||
|  | ||||
| 	// Exclude outdated image tag. | ||||
| 	for i, tag := range image.RepoTags { | ||||
| 		cImage, err := c.client.GetImage(ctx, tag) | ||||
| 		if err != nil { | ||||
| 			if errdefs.IsNotFound(err) { | ||||
| 				continue | ||||
| 			} | ||||
| 			return nil, errors.Wrapf(err, "failed to get image %q", tag) | ||||
| 	// Remove all image references. | ||||
| 	for i, ref := range image.References { | ||||
| 		var opts []images.DeleteOpt | ||||
| 		if i == len(image.References)-1 { | ||||
| 			// Delete the last image reference synchronously to trigger garbage collection. | ||||
| 			// This is best effort. It is possible that the image reference is deleted by | ||||
| 			// someone else before this point. | ||||
| 			opts = []images.DeleteOpt{images.SynchronousDelete()} | ||||
| 		} | ||||
| 		desc, err := cImage.Config(ctx) | ||||
| 		if err != nil { | ||||
| 			// We can only get image id by reading Config from content. | ||||
| 			// If the config is missing, we will fail to get image id, | ||||
| 			// So we won't be able to remove the image forever, | ||||
| 			// and the cri plugin always reports the image is ok. | ||||
| 			// But we also don't check it by manifest, | ||||
| 			// It's possible that two manifest digests have the same image ID in theory. | ||||
| 			// In theory it's possible that an image is compressed with different algorithms, | ||||
| 			// then they'll have the same uncompressed id - image id, | ||||
| 			// but different ids generated from compressed contents - manifest digest. | ||||
| 			// So we decide to leave it. | ||||
| 			// After all, the user can override the repoTag by pulling image again. | ||||
| 			logrus.WithError(err).Errorf("Can't remove image,failed to get config for Image tag %q,id %q", tag, image.ID) | ||||
| 			image.RepoTags = append(image.RepoTags[:i], image.RepoTags[i+1:]...) | ||||
| 			continue | ||||
| 		} | ||||
| 		cID := desc.Digest.String() | ||||
| 		if cID != image.ID { | ||||
| 			logrus.Debugf("Image tag %q for %q is outdated, it's currently used by %q", tag, image.ID, cID) | ||||
| 			image.RepoTags = append(image.RepoTags[:i], image.RepoTags[i+1:]...) | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Include all image references, including RepoTag, RepoDigest and id. | ||||
| 	for _, ref := range append(image.RepoTags, image.RepoDigests...) { | ||||
| 		err = c.client.ImageService().Delete(ctx, ref) | ||||
| 		err = c.client.ImageService().Delete(ctx, ref, opts...) | ||||
| 		if err == nil || errdefs.IsNotFound(err) { | ||||
| 			// Update image store to reflect the newest state in containerd. | ||||
| 			if err := c.imageStore.Update(ctx, ref); err != nil { | ||||
| 				return nil, errors.Wrapf(err, "failed to update image reference %q for %q", ref, image.ID) | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
| 		return nil, errors.Wrapf(err, "failed to delete image reference %q for image %q", ref, image.ID) | ||||
| 		return nil, errors.Wrapf(err, "failed to delete image reference %q for %q", ref, image.ID) | ||||
| 	} | ||||
| 	// Delete image id synchronously to trigger garbage collection. | ||||
| 	err = c.client.ImageService().Delete(ctx, image.ID, images.SynchronousDelete()) | ||||
| 	if err != nil && !errdefs.IsNotFound(err) { | ||||
| 		return nil, errors.Wrapf(err, "failed to delete image id %q", image.ID) | ||||
| 	} | ||||
| 	c.imageStore.Delete(image.ID) | ||||
| 	return &runtime.RemoveImageResponse{}, nil | ||||
| } | ||||
|   | ||||
| @@ -24,6 +24,7 @@ import ( | ||||
| 	"golang.org/x/net/context" | ||||
| 	runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2" | ||||
|  | ||||
| 	"github.com/containerd/cri/pkg/store" | ||||
| 	imagestore "github.com/containerd/cri/pkg/store/image" | ||||
| 	imagespec "github.com/opencontainers/image-spec/specs-go/v1" | ||||
| ) | ||||
| @@ -32,19 +33,19 @@ import ( | ||||
| // TODO(random-liu): We should change CRI to distinguish image id and image spec. (See | ||||
| // kubernetes/kubernetes#46255) | ||||
| func (c *criService) ImageStatus(ctx context.Context, r *runtime.ImageStatusRequest) (*runtime.ImageStatusResponse, error) { | ||||
| 	image, err := c.localResolve(ctx, r.GetImage().GetImage()) | ||||
| 	image, err := c.localResolve(r.GetImage().GetImage()) | ||||
| 	if err != nil { | ||||
| 		if err == store.ErrNotExist { | ||||
| 			// return empty without error when image not found. | ||||
| 			return &runtime.ImageStatusResponse{}, nil | ||||
| 		} | ||||
| 		return nil, errors.Wrapf(err, "can not resolve %q locally", r.GetImage().GetImage()) | ||||
| 	} | ||||
| 	if image == nil { | ||||
| 		// return empty without error when image not found. | ||||
| 		return &runtime.ImageStatusResponse{}, nil | ||||
| 	} | ||||
| 	// TODO(random-liu): [P0] Make sure corresponding snapshot exists. What if snapshot | ||||
| 	// doesn't exist? | ||||
|  | ||||
| 	runtimeImage := toCRIRuntimeImage(image) | ||||
| 	info, err := c.toCRIImageInfo(ctx, image, r.GetVerbose()) | ||||
| 	runtimeImage := toCRIImage(image) | ||||
| 	info, err := c.toCRIImageInfo(ctx, &image, r.GetVerbose()) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "failed to generate image info") | ||||
| 	} | ||||
| @@ -55,12 +56,13 @@ func (c *criService) ImageStatus(ctx context.Context, r *runtime.ImageStatusRequ | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // toCRIRuntimeImage converts internal image object to CRI runtime.Image. | ||||
| func toCRIRuntimeImage(image *imagestore.Image) *runtime.Image { | ||||
| // toCRIImage converts internal image object to CRI runtime.Image. | ||||
| func toCRIImage(image imagestore.Image) *runtime.Image { | ||||
| 	repoTags, repoDigests := parseImageReferences(image.References) | ||||
| 	runtimeImage := &runtime.Image{ | ||||
| 		Id:          image.ID, | ||||
| 		RepoTags:    image.RepoTags, | ||||
| 		RepoDigests: image.RepoDigests, | ||||
| 		RepoTags:    repoTags, | ||||
| 		RepoDigests: repoDigests, | ||||
| 		Size_:       uint64(image.Size), | ||||
| 	} | ||||
| 	uid, username := getUserFromImage(image.ImageSpec.Config.User) | ||||
|   | ||||
| @@ -31,11 +31,13 @@ import ( | ||||
| func TestImageStatus(t *testing.T) { | ||||
| 	testID := "sha256:d848ce12891bf78792cda4a23c58984033b0c397a55e93a1556202222ecc5ed4" | ||||
| 	image := imagestore.Image{ | ||||
| 		ID:          testID, | ||||
| 		ChainID:     "test-chain-id", | ||||
| 		RepoTags:    []string{"a", "b"}, | ||||
| 		RepoDigests: []string{"c", "d"}, | ||||
| 		Size:        1234, | ||||
| 		ID:      testID, | ||||
| 		ChainID: "test-chain-id", | ||||
| 		References: []string{ | ||||
| 			"gcr.io/library/busybox:latest", | ||||
| 			"gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", | ||||
| 		}, | ||||
| 		Size: 1234, | ||||
| 		ImageSpec: imagespec.Image{ | ||||
| 			Config: imagespec.ImageConfig{ | ||||
| 				User: "user:group", | ||||
| @@ -44,8 +46,8 @@ func TestImageStatus(t *testing.T) { | ||||
| 	} | ||||
| 	expected := &runtime.Image{ | ||||
| 		Id:          testID, | ||||
| 		RepoTags:    []string{"a", "b"}, | ||||
| 		RepoDigests: []string{"c", "d"}, | ||||
| 		RepoTags:    []string{"gcr.io/library/busybox:latest"}, | ||||
| 		RepoDigests: []string{"gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582"}, | ||||
| 		Size_:       uint64(1234), | ||||
| 		Username:    "user", | ||||
| 	} | ||||
| @@ -59,7 +61,8 @@ func TestImageStatus(t *testing.T) { | ||||
| 	require.NotNil(t, resp) | ||||
| 	assert.Nil(t, resp.GetImage()) | ||||
|  | ||||
| 	c.imageStore.Add(image) | ||||
| 	c.imageStore, err = imagestore.NewFakeStore([]imagestore.Image{image}) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	t.Logf("should return correct image status for exist image") | ||||
| 	resp, err = c.ImageStatus(context.Background(), &runtime.ImageStatusRequest{ | ||||
|   | ||||
| @@ -28,7 +28,6 @@ import ( | ||||
| 	containerdimages "github.com/containerd/containerd/images" | ||||
| 	"github.com/containerd/containerd/platforms" | ||||
| 	"github.com/containerd/typeurl" | ||||
| 	"github.com/docker/distribution/reference" | ||||
| 	"github.com/docker/docker/pkg/system" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| @@ -97,16 +96,7 @@ func (c *criService) recover(ctx context.Context) error { | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "failed to list images") | ||||
| 	} | ||||
| 	images, err := loadImages(ctx, cImages, c.config.ContainerdConfig.Snapshotter) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "failed to load images") | ||||
| 	} | ||||
| 	for _, image := range images { | ||||
| 		logrus.Debugf("Loaded image %+v", image) | ||||
| 		if err := c.imageStore.Add(image); err != nil { | ||||
| 			return errors.Wrapf(err, "failed to add image %q to store", image.ID) | ||||
| 		} | ||||
| 	} | ||||
| 	loadImages(ctx, c.imageStore, cImages, c.config.ContainerdConfig.Snapshotter) | ||||
|  | ||||
| 	// It's possible that containerd containers are deleted unexpectedly. In that case, | ||||
| 	// we can't even get metadata, we should cleanup orphaned sandbox/container directories | ||||
| @@ -404,26 +394,9 @@ func loadSandbox(ctx context.Context, cntr containerd.Container) (sandboxstore.S | ||||
| } | ||||
|  | ||||
| // loadImages loads images from containerd. | ||||
| // TODO(random-liu): Check whether image is unpacked, because containerd put image reference | ||||
| // into store before image is unpacked. | ||||
| func loadImages(ctx context.Context, cImages []containerd.Image, | ||||
| 	snapshotter string) ([]imagestore.Image, error) { | ||||
| 	// Group images by image id. | ||||
| 	imageMap := make(map[string][]containerd.Image) | ||||
| func loadImages(ctx context.Context, store *imagestore.Store, cImages []containerd.Image, | ||||
| 	snapshotter string) { | ||||
| 	for _, i := range cImages { | ||||
| 		desc, err := i.Config(ctx) | ||||
| 		if err != nil { | ||||
| 			logrus.WithError(err).Warnf("Failed to get image config for %q", i.Name()) | ||||
| 			continue | ||||
| 		} | ||||
| 		id := desc.Digest.String() | ||||
| 		imageMap[id] = append(imageMap[id], i) | ||||
| 	} | ||||
| 	var images []imagestore.Image | ||||
| 	for id, imgs := range imageMap { | ||||
| 		// imgs len must be > 0, or else the entry will not be created in | ||||
| 		// previous loop. | ||||
| 		i := imgs[0] | ||||
| 		ok, _, _, _, err := containerdimages.Check(ctx, i.ContentStore(), i.Target(), platforms.Default()) | ||||
| 		if err != nil { | ||||
| 			logrus.WithError(err).Errorf("Failed to check image content readiness for %q", i.Name()) | ||||
| @@ -436,48 +409,19 @@ func loadImages(ctx context.Context, cImages []containerd.Image, | ||||
| 		// Checking existence of top-level snapshot for each image being recovered. | ||||
| 		unpacked, err := i.IsUnpacked(ctx, snapshotter) | ||||
| 		if err != nil { | ||||
| 			logrus.WithError(err).Warnf("Failed to Check whether image is unpacked for image %s", i.Name()) | ||||
| 			logrus.WithError(err).Warnf("Failed to check whether image is unpacked for image %s", i.Name()) | ||||
| 			continue | ||||
| 		} | ||||
| 		if !unpacked { | ||||
| 			logrus.Warnf("The image %s is not unpacked.", i.Name()) | ||||
| 			// TODO(random-liu): Consider whether we should try unpack here. | ||||
| 		} | ||||
|  | ||||
| 		info, err := getImageInfo(ctx, i) | ||||
| 		if err != nil { | ||||
| 			logrus.WithError(err).Warnf("Failed to get image info for %q", i.Name()) | ||||
| 		if err := store.Update(ctx, i.Name()); err != nil { | ||||
| 			logrus.WithError(err).Warnf("Failed to update reference for image %q", i.Name()) | ||||
| 			continue | ||||
| 		} | ||||
| 		image := imagestore.Image{ | ||||
| 			ID:        id, | ||||
| 			ChainID:   info.chainID.String(), | ||||
| 			Size:      info.size, | ||||
| 			ImageSpec: info.imagespec, | ||||
| 			Image:     i, | ||||
| 		} | ||||
| 		// Recover repo digests and repo tags. | ||||
| 		for _, i := range imgs { | ||||
| 			name := i.Name() | ||||
| 			r, err := reference.ParseAnyReference(name) | ||||
| 			if err != nil { | ||||
| 				logrus.WithError(err).Warnf("Failed to parse image reference %q", name) | ||||
| 				continue | ||||
| 			} | ||||
| 			if _, ok := r.(reference.Canonical); ok { | ||||
| 				image.RepoDigests = append(image.RepoDigests, name) | ||||
| 			} else if _, ok := r.(reference.Tagged); ok { | ||||
| 				image.RepoTags = append(image.RepoTags, name) | ||||
| 			} else if _, ok := r.(reference.Digested); ok { | ||||
| 				// This is an image id. | ||||
| 				continue | ||||
| 			} else { | ||||
| 				logrus.Warnf("Invalid image reference %q", name) | ||||
| 			} | ||||
| 		} | ||||
| 		images = append(images, image) | ||||
| 		logrus.Debugf("Loaded image %q", i.Name()) | ||||
| 	} | ||||
| 	return images, nil | ||||
| } | ||||
|  | ||||
| func cleanupOrphanedIDDirs(cntrs []containerd.Container, base string) error { | ||||
|   | ||||
| @@ -113,7 +113,7 @@ func NewCRIService(config criconfig.Config, client *containerd.Client) (CRIServi | ||||
| 		os:                 osinterface.RealOS{}, | ||||
| 		sandboxStore:       sandboxstore.NewStore(), | ||||
| 		containerStore:     containerstore.NewStore(), | ||||
| 		imageStore:         imagestore.NewStore(), | ||||
| 		imageStore:         imagestore.NewStore(client), | ||||
| 		snapshotStore:      snapshotstore.NewStore(), | ||||
| 		sandboxNameIndex:   registrar.NewRegistrar(), | ||||
| 		containerNameIndex: registrar.NewRegistrar(), | ||||
| @@ -157,7 +157,7 @@ func NewCRIService(config criconfig.Config, client *containerd.Client) (CRIServi | ||||
| 		return nil, errors.Wrap(err, "failed to create stream server") | ||||
| 	} | ||||
|  | ||||
| 	c.eventMonitor = newEventMonitor(c.containerStore, c.sandboxStore) | ||||
| 	c.eventMonitor = newEventMonitor(c.containerStore, c.sandboxStore, c.imageStore) | ||||
|  | ||||
| 	return c, nil | ||||
| } | ||||
|   | ||||
| @@ -50,7 +50,7 @@ func newTestCRIService() *criService { | ||||
| 		imageFSPath:        testImageFSPath, | ||||
| 		os:                 ostesting.NewFakeOS(), | ||||
| 		sandboxStore:       sandboxstore.NewStore(), | ||||
| 		imageStore:         imagestore.NewStore(), | ||||
| 		imageStore:         imagestore.NewStore(nil), | ||||
| 		snapshotStore:      snapshotstore.NewStore(), | ||||
| 		sandboxNameIndex:   registrar.NewRegistrar(), | ||||
| 		containerStore:     containerstore.NewStore(), | ||||
|   | ||||
							
								
								
									
										34
									
								
								pkg/store/image/fake_image.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								pkg/store/image/fake_image.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| /* | ||||
| Copyright 2018 The Containerd Authors. | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
|  | ||||
| package image | ||||
|  | ||||
| import "github.com/pkg/errors" | ||||
|  | ||||
| // NewFakeStore returns an image store with predefined images. | ||||
| // Update is not allowed for this fake store. | ||||
| func NewFakeStore(images []Image) (*Store, error) { | ||||
| 	s := NewStore(nil) | ||||
| 	for _, i := range images { | ||||
| 		for _, ref := range i.References { | ||||
| 			s.refCache[ref] = i.ID | ||||
| 		} | ||||
| 		if err := s.store.add(i); err != nil { | ||||
| 			return nil, errors.Wrapf(err, "add image %q", i) | ||||
| 		} | ||||
| 	} | ||||
| 	return s, nil | ||||
| } | ||||
| @@ -17,14 +17,21 @@ limitations under the License. | ||||
| package image | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/containerd/containerd" | ||||
| 	"github.com/containerd/containerd/content" | ||||
| 	"github.com/containerd/containerd/errdefs" | ||||
| 	"github.com/docker/distribution/digestset" | ||||
| 	godigest "github.com/opencontainers/go-digest" | ||||
| 	imagedigest "github.com/opencontainers/go-digest" | ||||
| 	imageidentity "github.com/opencontainers/image-spec/identity" | ||||
| 	imagespec "github.com/opencontainers/image-spec/specs-go/v1" | ||||
| 	"github.com/pkg/errors" | ||||
|  | ||||
| 	"github.com/containerd/cri/pkg/store" | ||||
| 	storeutil "github.com/containerd/cri/pkg/store" | ||||
| 	"github.com/containerd/cri/pkg/util" | ||||
| ) | ||||
|  | ||||
| // Image contains all resources associated with the image. All fields | ||||
| @@ -32,10 +39,8 @@ import ( | ||||
| type Image struct { | ||||
| 	// Id of the image. Normally the digest of image config. | ||||
| 	ID string | ||||
| 	// Other names by which this image is known. | ||||
| 	RepoTags []string | ||||
| 	// Digests by which this image is known. | ||||
| 	RepoDigests []string | ||||
| 	// References are references to the image, e.g. RepoTag and RepoDigest. | ||||
| 	References []string | ||||
| 	// ChainID is the chainID of the image. | ||||
| 	ChainID string | ||||
| 	// Size is the compressed size of the image. | ||||
| @@ -48,28 +53,156 @@ type Image struct { | ||||
|  | ||||
| // Store stores all images. | ||||
| type Store struct { | ||||
| 	lock sync.RWMutex | ||||
| 	// refCache is a containerd image reference to image id cache. | ||||
| 	refCache map[string]string | ||||
| 	// client is the containerd client. | ||||
| 	client *containerd.Client | ||||
| 	// store is the internal image store indexed by image id. | ||||
| 	store *store | ||||
| } | ||||
|  | ||||
| // NewStore creates an image store. | ||||
| func NewStore(client *containerd.Client) *Store { | ||||
| 	return &Store{ | ||||
| 		refCache: make(map[string]string), | ||||
| 		client:   client, | ||||
| 		store: &store{ | ||||
| 			images:    make(map[string]Image), | ||||
| 			digestSet: digestset.NewSet(), | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Update updates cache for a reference. | ||||
| func (s *Store) Update(ctx context.Context, ref string) error { | ||||
| 	s.lock.Lock() | ||||
| 	defer s.lock.Unlock() | ||||
| 	i, err := s.client.GetImage(ctx, ref) | ||||
| 	if err != nil && !errdefs.IsNotFound(err) { | ||||
| 		return errors.Wrap(err, "get image from containerd") | ||||
| 	} | ||||
| 	var img *Image | ||||
| 	if err == nil { | ||||
| 		img, err = getImage(ctx, i) | ||||
| 		if err != nil { | ||||
| 			return errors.Wrap(err, "get image info from containerd") | ||||
| 		} | ||||
| 	} | ||||
| 	return s.update(ref, img) | ||||
| } | ||||
|  | ||||
| // update updates the internal cache. img == nil means that | ||||
| // the image does not exist in containerd. | ||||
| func (s *Store) update(ref string, img *Image) error { | ||||
| 	oldID, oldExist := s.refCache[ref] | ||||
| 	if img == nil { | ||||
| 		// The image reference doesn't exist in containerd. | ||||
| 		if oldExist { | ||||
| 			// Remove the reference from the store. | ||||
| 			s.store.delete(oldID, ref) | ||||
| 			delete(s.refCache, ref) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| 	if oldExist { | ||||
| 		if oldID == img.ID { | ||||
| 			return nil | ||||
| 		} | ||||
| 		// Updated. Remove tag from old image. | ||||
| 		s.store.delete(oldID, ref) | ||||
| 	} | ||||
| 	// New image. Add new image. | ||||
| 	s.refCache[ref] = img.ID | ||||
| 	return s.store.add(*img) | ||||
| } | ||||
|  | ||||
| // getImage gets image information from containerd. | ||||
| func getImage(ctx context.Context, i containerd.Image) (*Image, error) { | ||||
| 	// Get image information. | ||||
| 	diffIDs, err := i.RootFS(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "get image diffIDs") | ||||
| 	} | ||||
| 	chainID := imageidentity.ChainID(diffIDs) | ||||
|  | ||||
| 	size, err := i.Size(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "get image compressed resource size") | ||||
| 	} | ||||
|  | ||||
| 	desc, err := i.Config(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "get image config descriptor") | ||||
| 	} | ||||
| 	id := desc.Digest.String() | ||||
|  | ||||
| 	rb, err := content.ReadBlob(ctx, i.ContentStore(), desc) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "read image config from content store") | ||||
| 	} | ||||
| 	var ociimage imagespec.Image | ||||
| 	if err := json.Unmarshal(rb, &ociimage); err != nil { | ||||
| 		return nil, errors.Wrapf(err, "unmarshal image config %s", rb) | ||||
| 	} | ||||
|  | ||||
| 	return &Image{ | ||||
| 		ID:         id, | ||||
| 		References: []string{i.Name()}, | ||||
| 		ChainID:    chainID.String(), | ||||
| 		Size:       size, | ||||
| 		ImageSpec:  ociimage, | ||||
| 		Image:      i, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // Resolve resolves a image reference to image id. | ||||
| func (s *Store) Resolve(ref string) (string, error) { | ||||
| 	s.lock.RLock() | ||||
| 	defer s.lock.RUnlock() | ||||
| 	id, ok := s.refCache[ref] | ||||
| 	if !ok { | ||||
| 		return "", storeutil.ErrNotExist | ||||
| 	} | ||||
| 	return id, nil | ||||
| } | ||||
|  | ||||
| // Get gets image metadata by image id. The id can be truncated. | ||||
| // Returns various validation errors if the image id is invalid. | ||||
| // Returns storeutil.ErrNotExist if the image doesn't exist. | ||||
| func (s *Store) Get(id string) (Image, error) { | ||||
| 	return s.store.get(id) | ||||
| } | ||||
|  | ||||
| // List lists all images. | ||||
| func (s *Store) List() []Image { | ||||
| 	return s.store.list() | ||||
| } | ||||
|  | ||||
| type store struct { | ||||
| 	lock      sync.RWMutex | ||||
| 	images    map[string]Image | ||||
| 	digestSet *digestset.Set | ||||
| } | ||||
|  | ||||
| // NewStore creates an image store. | ||||
| func NewStore() *Store { | ||||
| 	return &Store{ | ||||
| 		images:    make(map[string]Image), | ||||
| 		digestSet: digestset.NewSet(), | ||||
| func (s *store) list() []Image { | ||||
| 	s.lock.RLock() | ||||
| 	defer s.lock.RUnlock() | ||||
| 	var images []Image | ||||
| 	for _, i := range s.images { | ||||
| 		images = append(images, i) | ||||
| 	} | ||||
| 	return images | ||||
| } | ||||
|  | ||||
| // Add an image into the store. | ||||
| func (s *Store) Add(img Image) error { | ||||
| func (s *store) add(img Image) error { | ||||
| 	s.lock.Lock() | ||||
| 	defer s.lock.Unlock() | ||||
| 	if _, err := s.digestSet.Lookup(img.ID); err != nil { | ||||
| 		if err != digestset.ErrDigestNotFound { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err := s.digestSet.Add(godigest.Digest(img.ID)); err != nil { | ||||
| 		if err := s.digestSet.Add(imagedigest.Digest(img.ID)); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| @@ -80,44 +213,29 @@ func (s *Store) Add(img Image) error { | ||||
| 		s.images[img.ID] = img | ||||
| 		return nil | ||||
| 	} | ||||
| 	// Or else, merge the repo tags/digests. | ||||
| 	i.RepoTags = mergeStringSlices(i.RepoTags, img.RepoTags) | ||||
| 	i.RepoDigests = mergeStringSlices(i.RepoDigests, img.RepoDigests) | ||||
| 	// Or else, merge the references. | ||||
| 	i.References = util.MergeStringSlices(i.References, img.References) | ||||
| 	s.images[img.ID] = i | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Get returns the image with specified id. Returns store.ErrNotExist if the | ||||
| // image doesn't exist. | ||||
| func (s *Store) Get(id string) (Image, error) { | ||||
| func (s *store) get(id string) (Image, error) { | ||||
| 	s.lock.RLock() | ||||
| 	defer s.lock.RUnlock() | ||||
| 	digest, err := s.digestSet.Lookup(id) | ||||
| 	if err != nil { | ||||
| 		if err == digestset.ErrDigestNotFound { | ||||
| 			err = store.ErrNotExist | ||||
| 			err = storeutil.ErrNotExist | ||||
| 		} | ||||
| 		return Image{}, err | ||||
| 	} | ||||
| 	if i, ok := s.images[digest.String()]; ok { | ||||
| 		return i, nil | ||||
| 	} | ||||
| 	return Image{}, store.ErrNotExist | ||||
| 	return Image{}, storeutil.ErrNotExist | ||||
| } | ||||
|  | ||||
| // List lists all images. | ||||
| func (s *Store) List() []Image { | ||||
| 	s.lock.RLock() | ||||
| 	defer s.lock.RUnlock() | ||||
| 	var images []Image | ||||
| 	for _, i := range s.images { | ||||
| 		images = append(images, i) | ||||
| 	} | ||||
| 	return images | ||||
| } | ||||
|  | ||||
| // Delete deletes the image with specified id. | ||||
| func (s *Store) Delete(id string) { | ||||
| func (s *store) delete(id, ref string) { | ||||
| 	s.lock.Lock() | ||||
| 	defer s.lock.Unlock() | ||||
| 	digest, err := s.digestSet.Lookup(id) | ||||
| @@ -126,22 +244,16 @@ func (s *Store) Delete(id string) { | ||||
| 		// So we need to return if there are error. | ||||
| 		return | ||||
| 	} | ||||
| 	i, ok := s.images[digest.String()] | ||||
| 	if !ok { | ||||
| 		return | ||||
| 	} | ||||
| 	i.References = util.SubtractStringSlice(i.References, ref) | ||||
| 	if len(i.References) != 0 { | ||||
| 		s.images[digest.String()] = i | ||||
| 		return | ||||
| 	} | ||||
| 	// Remove the image if it is not referenced any more. | ||||
| 	s.digestSet.Remove(digest) // nolint: errcheck | ||||
| 	delete(s.images, digest.String()) | ||||
| } | ||||
|  | ||||
| // mergeStringSlices merges 2 string slices into one and remove duplicated elements. | ||||
| func mergeStringSlices(a []string, b []string) []string { | ||||
| 	set := map[string]struct{}{} | ||||
| 	for _, s := range a { | ||||
| 		set[s] = struct{}{} | ||||
| 	} | ||||
| 	for _, s := range b { | ||||
| 		set[s] = struct{}{} | ||||
| 	} | ||||
| 	var ss []string | ||||
| 	for s := range set { | ||||
| 		ss = append(ss, s) | ||||
| 	} | ||||
| 	return ss | ||||
| } | ||||
|   | ||||
| @@ -17,65 +17,61 @@ limitations under the License. | ||||
| package image | ||||
|  | ||||
| import ( | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	imagespec "github.com/opencontainers/image-spec/specs-go/v1" | ||||
| 	"github.com/docker/distribution/digestset" | ||||
| 	assertlib "github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	"github.com/containerd/cri/pkg/store" | ||||
| 	storeutil "github.com/containerd/cri/pkg/store" | ||||
| ) | ||||
|  | ||||
| func TestImageStore(t *testing.T) { | ||||
| func TestInternalStore(t *testing.T) { | ||||
| 	images := []Image{ | ||||
| 		{ | ||||
| 			ID:          "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			ChainID:     "test-chain-id-1", | ||||
| 			RepoTags:    []string{"tag-1"}, | ||||
| 			RepoDigests: []string{"digest-1"}, | ||||
| 			Size:        10, | ||||
| 			ImageSpec:   imagespec.Image{}, | ||||
| 			ID:         "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			ChainID:    "test-chain-id-1", | ||||
| 			References: []string{"ref-1"}, | ||||
| 			Size:       10, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:          "sha256:2123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			ChainID:     "test-chain-id-2abcd", | ||||
| 			RepoTags:    []string{"tag-2abcd"}, | ||||
| 			RepoDigests: []string{"digest-2abcd"}, | ||||
| 			Size:        20, | ||||
| 			ImageSpec:   imagespec.Image{}, | ||||
| 			ID:         "sha256:2123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			ChainID:    "test-chain-id-2abcd", | ||||
| 			References: []string{"ref-2abcd"}, | ||||
| 			Size:       20, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:          "sha256:3123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			RepoTags:    []string{"tag-4a333"}, | ||||
| 			RepoDigests: []string{"digest-4a333"}, | ||||
| 			ChainID:     "test-chain-id-4a333", | ||||
| 			Size:        30, | ||||
| 			ImageSpec:   imagespec.Image{}, | ||||
| 			ID:         "sha256:3123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			References: []string{"ref-4a333"}, | ||||
| 			ChainID:    "test-chain-id-4a333", | ||||
| 			Size:       30, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:          "sha256:4123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			RepoTags:    []string{"tag-4abcd"}, | ||||
| 			RepoDigests: []string{"digest-4abcd"}, | ||||
| 			ChainID:     "test-chain-id-4abcd", | ||||
| 			Size:        40, | ||||
| 			ImageSpec:   imagespec.Image{}, | ||||
| 			ID:         "sha256:4123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", | ||||
| 			References: []string{"ref-4abcd"}, | ||||
| 			ChainID:    "test-chain-id-4abcd", | ||||
| 			Size:       40, | ||||
| 		}, | ||||
| 	} | ||||
| 	assert := assertlib.New(t) | ||||
| 	genTruncIndex := func(normalName string) string { return normalName[:(len(normalName)+1)/2] } | ||||
|  | ||||
| 	s := NewStore() | ||||
| 	s := &store{ | ||||
| 		images:    make(map[string]Image), | ||||
| 		digestSet: digestset.NewSet(), | ||||
| 	} | ||||
|  | ||||
| 	t.Logf("should be able to add image") | ||||
| 	for _, img := range images { | ||||
| 		err := s.Add(img) | ||||
| 		err := s.add(img) | ||||
| 		assert.NoError(err) | ||||
| 	} | ||||
|  | ||||
| 	t.Logf("should be able to get image") | ||||
| 	for _, v := range images { | ||||
| 		truncID := genTruncIndex(v.ID) | ||||
| 		got, err := s.Get(truncID) | ||||
| 		got, err := s.get(truncID) | ||||
| 		assert.NoError(err, "truncID:%s, fullID:%s", truncID, v.ID) | ||||
| 		assert.Equal(v, got) | ||||
| 	} | ||||
| @@ -83,7 +79,7 @@ func TestImageStore(t *testing.T) { | ||||
| 	t.Logf("should be able to get image by truncated imageId without algorithm") | ||||
| 	for _, v := range images { | ||||
| 		truncID := genTruncIndex(v.ID[strings.Index(v.ID, ":")+1:]) | ||||
| 		got, err := s.Get(truncID) | ||||
| 		got, err := s.get(truncID) | ||||
| 		assert.NoError(err, "truncID:%s, fullID:%s", truncID, v.ID) | ||||
| 		assert.Equal(v, got) | ||||
| 	} | ||||
| @@ -91,54 +87,162 @@ func TestImageStore(t *testing.T) { | ||||
| 	t.Logf("should not be able to get image by ambiguous prefix") | ||||
| 	ambiguousPrefixs := []string{"sha256", "sha256:"} | ||||
| 	for _, v := range ambiguousPrefixs { | ||||
| 		_, err := s.Get(v) | ||||
| 		_, err := s.get(v) | ||||
| 		assert.NotEqual(nil, err) | ||||
| 	} | ||||
|  | ||||
| 	t.Logf("should be able to list images") | ||||
| 	imgs := s.List() | ||||
| 	imgs := s.list() | ||||
| 	assert.Len(imgs, len(images)) | ||||
|  | ||||
| 	imageNum := len(images) | ||||
| 	for _, v := range images { | ||||
| 		truncID := genTruncIndex(v.ID) | ||||
| 		oldRepoTag := v.RepoTags[0] | ||||
| 		oldRepoDigest := v.RepoDigests[0] | ||||
| 		newRepoTag := oldRepoTag + "new" | ||||
| 		newRepoDigest := oldRepoDigest + "new" | ||||
| 		oldRef := v.References[0] | ||||
| 		newRef := oldRef + "new" | ||||
|  | ||||
| 		t.Logf("should be able to add new repo tags/digests") | ||||
| 		t.Logf("should be able to add new references") | ||||
| 		newImg := v | ||||
| 		newImg.RepoTags = []string{newRepoTag} | ||||
| 		newImg.RepoDigests = []string{newRepoDigest} | ||||
| 		err := s.Add(newImg) | ||||
| 		newImg.References = []string{newRef} | ||||
| 		err := s.add(newImg) | ||||
| 		assert.NoError(err) | ||||
| 		got, err := s.Get(truncID) | ||||
| 		got, err := s.get(truncID) | ||||
| 		assert.NoError(err) | ||||
| 		assert.Len(got.RepoTags, 2) | ||||
| 		assert.Contains(got.RepoTags, oldRepoTag, newRepoTag) | ||||
| 		assert.Len(got.RepoDigests, 2) | ||||
| 		assert.Contains(got.RepoDigests, oldRepoDigest, newRepoDigest) | ||||
| 		assert.Len(got.References, 2) | ||||
| 		assert.Contains(got.References, oldRef, newRef) | ||||
|  | ||||
| 		t.Logf("should not be able to add duplicated repo tags/digests") | ||||
| 		err = s.Add(newImg) | ||||
| 		t.Logf("should not be able to add duplicated references") | ||||
| 		err = s.add(newImg) | ||||
| 		assert.NoError(err) | ||||
| 		got, err = s.Get(truncID) | ||||
| 		got, err = s.get(truncID) | ||||
| 		assert.NoError(err) | ||||
| 		assert.Len(got.RepoTags, 2) | ||||
| 		assert.Contains(got.RepoTags, oldRepoTag, newRepoTag) | ||||
| 		assert.Len(got.RepoDigests, 2) | ||||
| 		assert.Contains(got.RepoDigests, oldRepoDigest, newRepoDigest) | ||||
| 		assert.Len(got.References, 2) | ||||
| 		assert.Contains(got.References, oldRef, newRef) | ||||
|  | ||||
| 		t.Logf("should be able to delete image references") | ||||
| 		s.delete(truncID, oldRef) | ||||
| 		got, err = s.get(truncID) | ||||
| 		assert.NoError(err) | ||||
| 		assert.Equal([]string{newRef}, got.References) | ||||
|  | ||||
| 		t.Logf("should be able to delete image") | ||||
| 		s.Delete(truncID) | ||||
| 		imageNum-- | ||||
| 		imgs = s.List() | ||||
| 		assert.Len(imgs, imageNum) | ||||
| 		s.delete(truncID, newRef) | ||||
| 		got, err = s.get(truncID) | ||||
| 		assert.Equal(storeutil.ErrNotExist, err) | ||||
| 		assert.Equal(Image{}, got) | ||||
|  | ||||
| 		t.Logf("get should return empty struct and ErrNotExist after deletion") | ||||
| 		img, err := s.Get(truncID) | ||||
| 		assert.Equal(Image{}, img) | ||||
| 		assert.Equal(store.ErrNotExist, err) | ||||
| 		imageNum-- | ||||
| 		imgs = s.list() | ||||
| 		assert.Len(imgs, imageNum) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestImageStore(t *testing.T) { | ||||
| 	id := "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" | ||||
| 	newID := "sha256:9923456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" | ||||
| 	image := Image{ | ||||
| 		ID:         id, | ||||
| 		ChainID:    "test-chain-id-1", | ||||
| 		References: []string{"ref-1"}, | ||||
| 		Size:       10, | ||||
| 	} | ||||
| 	assert := assertlib.New(t) | ||||
|  | ||||
| 	equal := func(i1, i2 Image) { | ||||
| 		sort.Strings(i1.References) | ||||
| 		sort.Strings(i2.References) | ||||
| 		assert.Equal(i1, i2) | ||||
| 	} | ||||
| 	for desc, test := range map[string]struct { | ||||
| 		ref      string | ||||
| 		image    *Image | ||||
| 		expected []Image | ||||
| 	}{ | ||||
| 		"nothing should happen if a non-exist ref disappear": { | ||||
| 			ref:      "ref-2", | ||||
| 			image:    nil, | ||||
| 			expected: []Image{image}, | ||||
| 		}, | ||||
| 		"new ref for an existing image": { | ||||
| 			ref: "ref-2", | ||||
| 			image: &Image{ | ||||
| 				ID:         id, | ||||
| 				ChainID:    "test-chain-id-1", | ||||
| 				References: []string{"ref-2"}, | ||||
| 				Size:       10, | ||||
| 			}, | ||||
| 			expected: []Image{ | ||||
| 				{ | ||||
| 					ID:         id, | ||||
| 					ChainID:    "test-chain-id-1", | ||||
| 					References: []string{"ref-1", "ref-2"}, | ||||
| 					Size:       10, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"new ref for a new image": { | ||||
| 			ref: "ref-2", | ||||
| 			image: &Image{ | ||||
| 				ID:         newID, | ||||
| 				ChainID:    "test-chain-id-2", | ||||
| 				References: []string{"ref-2"}, | ||||
| 				Size:       20, | ||||
| 			}, | ||||
| 			expected: []Image{ | ||||
| 				image, | ||||
| 				{ | ||||
| 					ID:         newID, | ||||
| 					ChainID:    "test-chain-id-2", | ||||
| 					References: []string{"ref-2"}, | ||||
| 					Size:       20, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"existing ref point to a new image": { | ||||
| 			ref: "ref-1", | ||||
| 			image: &Image{ | ||||
| 				ID:         newID, | ||||
| 				ChainID:    "test-chain-id-2", | ||||
| 				References: []string{"ref-1"}, | ||||
| 				Size:       20, | ||||
| 			}, | ||||
| 			expected: []Image{ | ||||
| 				{ | ||||
| 					ID:         newID, | ||||
| 					ChainID:    "test-chain-id-2", | ||||
| 					References: []string{"ref-1"}, | ||||
| 					Size:       20, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"existing ref disappear": { | ||||
| 			ref:      "ref-1", | ||||
| 			image:    nil, | ||||
| 			expected: []Image{}, | ||||
| 		}, | ||||
| 	} { | ||||
| 		t.Logf("TestCase %q", desc) | ||||
| 		s, err := NewFakeStore([]Image{image}) | ||||
| 		assert.NoError(err) | ||||
| 		assert.NoError(s.update(test.ref, test.image)) | ||||
|  | ||||
| 		assert.Len(s.List(), len(test.expected)) | ||||
| 		for _, expect := range test.expected { | ||||
| 			got, err := s.Get(expect.ID) | ||||
| 			assert.NoError(err) | ||||
| 			equal(got, expect) | ||||
| 			for _, ref := range expect.References { | ||||
| 				id, err := s.Resolve(ref) | ||||
| 				assert.NoError(err) | ||||
| 				assert.Equal(expect.ID, id) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if test.image == nil { | ||||
| 			// Shouldn't be able to index by removed ref. | ||||
| 			id, err := s.Resolve(test.ref) | ||||
| 			assert.Equal(storeutil.ErrNotExist, err) | ||||
| 			assert.Empty(id) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -41,3 +41,19 @@ func SubtractStringSlice(ss []string, str string) []string { | ||||
| 	} | ||||
| 	return res | ||||
| } | ||||
|  | ||||
| // MergeStringSlices merges 2 string slices into one and remove duplicated elements. | ||||
| func MergeStringSlices(a []string, b []string) []string { | ||||
| 	set := map[string]struct{}{} | ||||
| 	for _, s := range a { | ||||
| 		set[s] = struct{}{} | ||||
| 	} | ||||
| 	for _, s := range b { | ||||
| 		set[s] = struct{}{} | ||||
| 	} | ||||
| 	var ss []string | ||||
| 	for s := range set { | ||||
| 		ss = append(ss, s) | ||||
| 	} | ||||
| 	return ss | ||||
| } | ||||
|   | ||||
| @@ -46,3 +46,14 @@ func TestSubtractStringSlice(t *testing.T) { | ||||
| 	assert.Empty(t, SubtractStringSlice(nil, "hij")) | ||||
| 	assert.Empty(t, SubtractStringSlice([]string{}, "hij")) | ||||
| } | ||||
|  | ||||
| func TestMergeStringSlices(t *testing.T) { | ||||
| 	s1 := []string{"abc", "def", "ghi"} | ||||
| 	s2 := []string{"def", "jkl", "mno"} | ||||
| 	expect := []string{"abc", "def", "ghi", "jkl", "mno"} | ||||
| 	result := MergeStringSlices(s1, s2) | ||||
| 	assert.Len(t, result, len(expect)) | ||||
| 	for _, s := range expect { | ||||
| 		assert.Contains(t, result, s) | ||||
| 	} | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Lantao Liu
					Lantao Liu