Skip to content

Commit

Permalink
Pinned image support
Browse files Browse the repository at this point in the history
Signed-off-by: Aditi Sharma <adi.sky17@gmail.com>
(cherry picked from commit fe4f8bd)
Signed-off-by: ruiwen-zhao <ruiwen@google.com>
  • Loading branch information
adisky authored and ruiwen-zhao committed Jun 21, 2023
1 parent 3a6c2c5 commit f4713aa
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 13 deletions.
18 changes: 18 additions & 0 deletions integration/containerd_image_test.go
Expand Up @@ -206,3 +206,21 @@ func TestContainerdImageInOtherNamespaces(t *testing.T) {
}
assert.NoError(t, Consistently(checkImage, 100*time.Millisecond, time.Second))
}

func TestContainerdSandboxImage(t *testing.T) {
var pauseImage = GetImage(Pause)
ctx := context.Background()

t.Log("make sure the pause image exist")
pauseImg, err := containerdClient.GetImage(ctx, pauseImage)
require.NoError(t, err)
t.Log("ensure correct labels are set on pause image")
assert.Equal(t, pauseImg.Labels()["io.cri-containerd.pinned"], "pinned")

t.Log("pause image should be seen by cri plugin")
pimg, err := imageService.ImageStatus(&runtime.ImageSpec{Image: pauseImage})
require.NoError(t, err)
require.NotNil(t, pimg)
t.Log("verify pinned field is set for pause image")
assert.True(t, pimg.Pinned)
}
30 changes: 30 additions & 0 deletions pkg/cri/labels/labels.go
@@ -0,0 +1,30 @@
/*
Copyright 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 labels

const (
// criContainerdPrefix is common prefix for cri-containerd
criContainerdPrefix = "io.cri-containerd"
// ImageLabelKey is the label key indicating the image is managed by cri plugin.
ImageLabelKey = criContainerdPrefix + ".image"
// ImageLabelValue is the label value indicating the image is managed by cri plugin.
ImageLabelValue = "managed"
// PinnedImageLabelKey is the label value indicating the image is pinned.
PinnedImageLabelKey = criContainerdPrefix + ".pinned"
// PinnedImageLabelValue is the label value indicating the image is pinned.
PinnedImageLabelValue = "pinned"
)
4 changes: 0 additions & 4 deletions pkg/cri/server/helpers.go
Expand Up @@ -78,10 +78,6 @@ const (
containerKindSandbox = "sandbox"
// containerKindContainer is a label value indicating container is application container
containerKindContainer = "container"
// imageLabelKey is the label key indicating the image is managed by cri plugin.
imageLabelKey = criContainerdPrefix + ".image"
// imageLabelValue is the label value indicating the image is managed by cri plugin.
imageLabelValue = "managed"
// sandboxMetadataExtension is an extension name that identify metadata of sandbox in CreateContainerRequest
sandboxMetadataExtension = criContainerdPrefix + ".sandbox.metadata"
// containerMetadataExtension is an extension name that identify metadata of container in CreateContainerRequest
Expand Down
40 changes: 31 additions & 9 deletions pkg/cri/server/image_pull.go
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/containerd/containerd/errdefs"
containerdimages "github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
crilabels "github.com/containerd/containerd/pkg/cri/labels"
snpkg "github.com/containerd/containerd/pkg/snapshotters"
distribution "github.com/containerd/containerd/reference/docker"
"github.com/containerd/containerd/remotes/docker"
Expand Down Expand Up @@ -113,12 +114,14 @@ func (c *criService) PullImage(ctx context.Context, r *runtime.PullImageRequest)
}
)

labels := c.getLabels(ctx, ref)

pullOpts := []containerd.RemoteOpt{
containerd.WithSchema1Conversion,
containerd.WithResolver(resolver),
containerd.WithPullSnapshotter(c.config.ContainerdConfig.Snapshotter),
containerd.WithPullUnpack,
containerd.WithPullLabel(imageLabelKey, imageLabelValue),
containerd.WithPullLabels(labels),
containerd.WithMaxConcurrentDownloads(c.config.MaxConcurrentDownloads),
containerd.WithImageHandler(imageHandler),
containerd.WithUnpackOpts([]containerd.UnpackOpt{
Expand Down Expand Up @@ -154,7 +157,7 @@ func (c *criService) PullImage(ctx context.Context, r *runtime.PullImageRequest)
if r == "" {
continue
}
if err := c.createImageReference(ctx, r, image.Target()); err != nil {
if err := c.createImageReference(ctx, r, image.Target(), labels); err != nil {
return nil, fmt.Errorf("failed to create image reference %q: %w", r, err)
}
// Update image store to reflect the newest state in containerd.
Expand Down Expand Up @@ -219,26 +222,44 @@ func ParseAuth(auth *runtime.AuthConfig, host string) (string, string, error) {
// Note that because create and update are not finished in one transaction, there could be race. E.g.
// the image reference is deleted by someone else after create returns already exists, but before update
// happens.
func (c *criService) createImageReference(ctx context.Context, name string, desc imagespec.Descriptor) error {
func (c *criService) createImageReference(ctx context.Context, name string, desc imagespec.Descriptor, labels map[string]string) error {
img := containerdimages.Image{
Name: name,
Target: desc,
// Add a label to indicate that the image is managed by the cri plugin.
Labels: map[string]string{imageLabelKey: imageLabelValue},
Labels: labels,
}
// TODO(random-liu): Figure out which is the more performant sequence create then update or
// update then create.
oldImg, err := c.client.ImageService().Create(ctx, img)
if err == nil || !errdefs.IsAlreadyExists(err) {
return err
}
if oldImg.Target.Digest == img.Target.Digest && oldImg.Labels[imageLabelKey] == imageLabelValue {
if oldImg.Target.Digest == img.Target.Digest && oldImg.Labels[crilabels.ImageLabelKey] == labels[crilabels.ImageLabelKey] {
return nil
}
_, err = c.client.ImageService().Update(ctx, img, "target", "labels."+imageLabelKey)
_, err = c.client.ImageService().Update(ctx, img, "target", "labels."+crilabels.ImageLabelKey)
return err
}

// getLabels get image labels to be added on CRI image
func (c *criService) getLabels(ctx context.Context, name string) map[string]string {
labels := map[string]string{crilabels.ImageLabelKey: crilabels.ImageLabelValue}
configSandboxImage := c.config.SandboxImage
// parse sandbox image
sandboxNamedRef, err := distribution.ParseDockerRef(configSandboxImage)
if err != nil {
log.G(ctx).Errorf("failed to parse sandbox image from config %s", sandboxNamedRef)
return nil
}
sandboxRef := sandboxNamedRef.String()
// Adding pinned image label to sandbox image
if sandboxRef == name {
labels[crilabels.PinnedImageLabelKey] = crilabels.PinnedImageLabelValue
}
return labels
}

// updateImage updates image store to reflect the newest state of an image reference
// in containerd. If the reference is not managed by the cri plugin, the function also
// generates necessary metadata for the image and make it managed.
Expand All @@ -247,22 +268,23 @@ func (c *criService) updateImage(ctx context.Context, r string) error {
if err != nil && !errdefs.IsNotFound(err) {
return fmt.Errorf("get image by reference: %w", err)
}
if err == nil && img.Labels()[imageLabelKey] != imageLabelValue {
if err == nil && img.Labels()[crilabels.ImageLabelKey] != crilabels.ImageLabelValue {
// Make sure the image has the image id as its unique
// identifier that references the image in its lifetime.
configDesc, err := img.Config(ctx)
if err != nil {
return fmt.Errorf("get image id: %w", err)
}
id := configDesc.Digest.String()
if err := c.createImageReference(ctx, id, img.Target()); err != nil {
labels := c.getLabels(ctx, id)
if err := c.createImageReference(ctx, id, img.Target(), labels); err != nil {
return fmt.Errorf("create image id reference %q: %w", id, err)
}
if err := c.imageStore.Update(ctx, id); err != nil {
return fmt.Errorf("update image store for %q: %w", id, err)
}
// The image id is ready, add the label to mark the image as managed.
if err := c.createImageReference(ctx, r, img.Target()); err != nil {
if err := c.createImageReference(ctx, r, img.Target(), labels); err != nil {
return fmt.Errorf("create managed label: %w", err)
}
}
Expand Down
55 changes: 55 additions & 0 deletions pkg/cri/server/image_pull_test.go
Expand Up @@ -17,13 +17,15 @@
package server

import (
"context"
"encoding/base64"
"testing"

"github.com/stretchr/testify/assert"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"

criconfig "github.com/containerd/containerd/pkg/cri/config"
"github.com/containerd/containerd/pkg/cri/labels"
)

func TestParseAuth(t *testing.T) {
Expand Down Expand Up @@ -327,3 +329,56 @@ func TestEncryptedImagePullOpts(t *testing.T) {
assert.Equal(t, test.expectedOpts, got)
}
}
func TestImageGetLabels(t *testing.T) {

criService := newTestCRIService()

tests := []struct {
name string
expectedLabel map[string]string
configSandboxImage string
pullImageName string
}{
{
name: "pinned image labels should get added on sandbox image",
expectedLabel: map[string]string{labels.ImageLabelKey: labels.ImageLabelValue, labels.PinnedImageLabelKey: labels.PinnedImageLabelValue},
configSandboxImage: "k8s.gcr.io/pause:3.9",
pullImageName: "k8s.gcr.io/pause:3.9",
},
{
name: "pinned image labels should get added on sandbox image without tag",
expectedLabel: map[string]string{labels.ImageLabelKey: labels.ImageLabelValue, labels.PinnedImageLabelKey: labels.PinnedImageLabelValue},
configSandboxImage: "k8s.gcr.io/pause",
pullImageName: "k8s.gcr.io/pause:latest",
},
{
name: "pinned image labels should get added on sandbox image specified with tag and digest both",
expectedLabel: map[string]string{labels.ImageLabelKey: labels.ImageLabelValue, labels.PinnedImageLabelKey: labels.PinnedImageLabelValue},
configSandboxImage: "k8s.gcr.io/pause:3.9@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2",
pullImageName: "k8s.gcr.io/pause@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2",
},

{
name: "pinned image labels should get added on sandbox image specified with digest",
expectedLabel: map[string]string{labels.ImageLabelKey: labels.ImageLabelValue, labels.PinnedImageLabelKey: labels.PinnedImageLabelValue},
configSandboxImage: "k8s.gcr.io/pause@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2",
pullImageName: "k8s.gcr.io/pause@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2",
},

{
name: "pinned image labels should not get added on other image",
expectedLabel: map[string]string{labels.ImageLabelKey: labels.ImageLabelValue},
configSandboxImage: "k8s.gcr.io/pause:3.9",
pullImageName: "k8s.gcr.io/random:latest",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
criService.config.SandboxImage = tt.configSandboxImage
labels := criService.getLabels(context.Background(), tt.pullImageName)
assert.Equal(t, tt.expectedLabel, labels)

})
}
}
3 changes: 3 additions & 0 deletions pkg/cri/server/image_status.go
Expand Up @@ -59,12 +59,15 @@ func (c *criService) ImageStatus(ctx context.Context, r *runtime.ImageStatusRequ
// 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: repoTags,
RepoDigests: repoDigests,
Size_: uint64(image.Size),
Pinned: image.Pinned,
}

uid, username := getUserFromImage(image.ImageSpec.Config.User)
if uid != nil {
runtimeImage.Uid = &runtime.Int64Value{Value: *uid}
Expand Down
7 changes: 7 additions & 0 deletions pkg/cri/store/image/image.go
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/containerd/containerd"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/pkg/cri/labels"
"github.com/containerd/containerd/pkg/cri/util"

imagedigest "github.com/opencontainers/go-digest"
Expand All @@ -46,6 +47,8 @@ type Image struct {
Size int64
// ImageSpec is the oci image structure which describes basic information about the image.
ImageSpec imagespec.Image
// Pinned image to prevent it from garbage collection
Pinned bool
}

// Store stores all images.
Expand Down Expand Up @@ -143,13 +146,17 @@ func getImage(ctx context.Context, i containerd.Image) (*Image, error) {
return nil, fmt.Errorf("unmarshal image config %s: %w", rb, err)
}

pinned := i.Labels()[labels.PinnedImageLabelKey] == labels.PinnedImageLabelValue

return &Image{
ID: id,
References: []string{i.Name()},
ChainID: chainID.String(),
Size: size,
ImageSpec: ociimage,
Pinned: pinned,
}, nil

}

// Resolve resolves a image reference to image id.
Expand Down

0 comments on commit f4713aa

Please sign in to comment.