* Add docker pullable support.

* Fix inspect image bug.
* Fix remove image bug.
This commit is contained in:
Random-Liu 2016-10-07 21:35:18 -07:00
parent ead65fc25f
commit afa3414779
15 changed files with 166 additions and 38 deletions

View File

@ -17,7 +17,6 @@ limitations under the License.
package testing package testing
import ( import (
"fmt"
"sync" "sync"
runtimeApi "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" runtimeApi "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime"
@ -89,11 +88,7 @@ func (r *FakeImageService) ImageStatus(image *runtimeApi.ImageSpec) (*runtimeApi
r.Called = append(r.Called, "ImageStatus") r.Called = append(r.Called, "ImageStatus")
if img, ok := r.Images[image.GetImage()]; ok { return r.Images[image.GetImage()], nil
return img, nil
}
return nil, fmt.Errorf("image %q not found", image.GetImage())
} }
func (r *FakeImageService) PullImage(image *runtimeApi.ImageSpec, auth *runtimeApi.AuthConfig) error { func (r *FakeImageService) PullImage(image *runtimeApi.ImageSpec, auth *runtimeApi.AuthConfig) error {

View File

@ -2963,7 +2963,8 @@ var _RuntimeService_serviceDesc = grpc.ServiceDesc{
type ImageServiceClient interface { type ImageServiceClient interface {
// ListImages lists existing images. // ListImages lists existing images.
ListImages(ctx context.Context, in *ListImagesRequest, opts ...grpc.CallOption) (*ListImagesResponse, error) ListImages(ctx context.Context, in *ListImagesRequest, opts ...grpc.CallOption) (*ListImagesResponse, error)
// ImageStatus returns the status of the image. // ImageStatus returns the status of the image. If the image is not
// present, returns nil.
ImageStatus(ctx context.Context, in *ImageStatusRequest, opts ...grpc.CallOption) (*ImageStatusResponse, error) ImageStatus(ctx context.Context, in *ImageStatusRequest, opts ...grpc.CallOption) (*ImageStatusResponse, error)
// PullImage pulls an image with authentication config. // PullImage pulls an image with authentication config.
PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (*PullImageResponse, error) PullImage(ctx context.Context, in *PullImageRequest, opts ...grpc.CallOption) (*PullImageResponse, error)
@ -3021,7 +3022,8 @@ func (c *imageServiceClient) RemoveImage(ctx context.Context, in *RemoveImageReq
type ImageServiceServer interface { type ImageServiceServer interface {
// ListImages lists existing images. // ListImages lists existing images.
ListImages(context.Context, *ListImagesRequest) (*ListImagesResponse, error) ListImages(context.Context, *ListImagesRequest) (*ListImagesResponse, error)
// ImageStatus returns the status of the image. // ImageStatus returns the status of the image. If the image is not
// present, returns nil.
ImageStatus(context.Context, *ImageStatusRequest) (*ImageStatusResponse, error) ImageStatus(context.Context, *ImageStatusRequest) (*ImageStatusResponse, error)
// PullImage pulls an image with authentication config. // PullImage pulls an image with authentication config.
PullImage(context.Context, *PullImageRequest) (*PullImageResponse, error) PullImage(context.Context, *PullImageRequest) (*PullImageResponse, error)

View File

@ -46,7 +46,8 @@ service RuntimeService {
service ImageService { service ImageService {
// ListImages lists existing images. // ListImages lists existing images.
rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {} rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {}
// ImageStatus returns the status of the image. // ImageStatus returns the status of the image. If the image is not
// present, returns nil.
rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {} rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {}
// PullImage pulls an image with authentication config. // PullImage pulls an image with authentication config.
rpc PullImage(PullImageRequest) returns (PullImageResponse) {} rpc PullImage(PullImageRequest) returns (PullImageResponse) {}

View File

@ -36,7 +36,7 @@ const (
statusExitedPrefix = "Exited" statusExitedPrefix = "Exited"
) )
func toRuntimeAPIImage(image *dockertypes.Image) (*runtimeApi.Image, error) { func imageToRuntimeAPIImage(image *dockertypes.Image) (*runtimeApi.Image, error) {
if image == nil { if image == nil {
return nil, fmt.Errorf("unable to convert a nil pointer to a runtime API image") return nil, fmt.Errorf("unable to convert a nil pointer to a runtime API image")
} }
@ -50,6 +50,31 @@ func toRuntimeAPIImage(image *dockertypes.Image) (*runtimeApi.Image, error) {
}, nil }, nil
} }
func imageInspectToRuntimeAPIImage(image *dockertypes.ImageInspect) (*runtimeApi.Image, error) {
if image == nil {
return nil, fmt.Errorf("unable to convert a nil pointer to a runtime API image")
}
size := uint64(image.VirtualSize)
return &runtimeApi.Image{
Id: &image.ID,
RepoTags: image.RepoTags,
RepoDigests: image.RepoDigests,
Size_: &size,
}, nil
}
func toPullableImageID(id string, image *dockertypes.ImageInspect) string {
// Default to the image ID, but if RepoDigests is not empty, use
// the first digest instead.
imageID := DockerImageIDPrefix + id
if len(image.RepoDigests) > 0 {
imageID = DockerPullableImageIDPrefix + image.RepoDigests[0]
}
return imageID
}
func toRuntimeAPIContainer(c *dockertypes.Container) (*runtimeApi.Container, error) { func toRuntimeAPIContainer(c *dockertypes.Container) (*runtimeApi.Container, error) {
state := toRuntimeAPIContainerState(c.Status) state := toRuntimeAPIContainerState(c.Status)
metadata, err := parseContainerName(c.Names[0]) metadata, err := parseContainerName(c.Names[0])

View File

@ -19,6 +19,7 @@ package dockershim
import ( import (
"testing" "testing"
dockertypes "github.com/docker/engine-api/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
runtimeApi "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" runtimeApi "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime"
@ -40,3 +41,31 @@ func TestConvertDockerStatusToRuntimeAPIState(t *testing.T) {
assert.Equal(t, test.expected, actual) assert.Equal(t, test.expected, actual)
} }
} }
func TestConvertToPullableImageID(t *testing.T) {
testCases := []struct {
id string
image *dockertypes.ImageInspect
expected string
}{
{
id: "image-1",
image: &dockertypes.ImageInspect{
RepoDigests: []string{"digest-1"},
},
expected: DockerPullableImageIDPrefix + "digest-1",
},
{
id: "image-2",
image: &dockertypes.ImageInspect{
RepoDigests: []string{},
},
expected: DockerImageIDPrefix + "image-2",
},
}
for _, test := range testCases {
actual := toPullableImageID(test.id, test.image)
assert.Equal(t, test.expected, actual)
}
}

View File

@ -229,6 +229,13 @@ func (ds *dockerService) ContainerStatus(containerID string) (*runtimeApi.Contai
return nil, fmt.Errorf("failed to parse timestamp for container %q: %v", containerID, err) return nil, fmt.Errorf("failed to parse timestamp for container %q: %v", containerID, err)
} }
// Convert the image id to pullable id.
ir, err := ds.client.InspectImageByID(r.Image)
if err != nil {
return nil, fmt.Errorf("unable to inspect docker image %q while inspecting docker container %q: %v", r.Image, containerID, err)
}
imageID := toPullableImageID(r.Image, ir)
// Convert the mounts. // Convert the mounts.
mounts := []*runtimeApi.Mount{} mounts := []*runtimeApi.Mount{}
for i := range r.Mounts { for i := range r.Mounts {
@ -293,7 +300,7 @@ func (ds *dockerService) ContainerStatus(containerID string) (*runtimeApi.Contai
Id: &r.ID, Id: &r.ID,
Metadata: metadata, Metadata: metadata,
Image: &runtimeApi.ImageSpec{Image: &r.Config.Image}, Image: &runtimeApi.ImageSpec{Image: &r.Config.Image},
ImageRef: &r.Image, ImageRef: &imageID,
Mounts: mounts, Mounts: mounts,
ExitCode: &exitCode, ExitCode: &exitCode,
State: &state, State: &state,

View File

@ -105,7 +105,7 @@ func TestContainerStatus(t *testing.T) {
ct, st, ft := dt, dt, dt ct, st, ft := dt, dt, dt
state := runtimeApi.ContainerState_CREATED state := runtimeApi.ContainerState_CREATED
// The following variables are not set in FakeDockerClient. // The following variables are not set in FakeDockerClient.
imageRef := "" imageRef := DockerImageIDPrefix + ""
exitCode := int32(0) exitCode := int32(0)
var reason, message string var reason, message string

View File

@ -17,10 +17,9 @@ limitations under the License.
package dockershim package dockershim
import ( import (
"fmt"
dockertypes "github.com/docker/engine-api/types" dockertypes "github.com/docker/engine-api/types"
runtimeApi "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" runtimeApi "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/dockertools"
) )
// This file implements methods in ImageManagerService. // This file implements methods in ImageManagerService.
@ -41,7 +40,7 @@ func (ds *dockerService) ListImages(filter *runtimeApi.ImageFilter) ([]*runtimeA
result := []*runtimeApi.Image{} result := []*runtimeApi.Image{}
for _, i := range images { for _, i := range images {
apiImage, err := toRuntimeAPIImage(&i) apiImage, err := imageToRuntimeAPIImage(&i)
if err != nil { if err != nil {
// TODO: log an error message? // TODO: log an error message?
continue continue
@ -51,16 +50,16 @@ func (ds *dockerService) ListImages(filter *runtimeApi.ImageFilter) ([]*runtimeA
return result, nil return result, nil
} }
// ImageStatus returns the status of the image. // ImageStatus returns the status of the image, returns nil if the image doesn't present.
func (ds *dockerService) ImageStatus(image *runtimeApi.ImageSpec) (*runtimeApi.Image, error) { func (ds *dockerService) ImageStatus(image *runtimeApi.ImageSpec) (*runtimeApi.Image, error) {
images, err := ds.ListImages(&runtimeApi.ImageFilter{Image: image}) imageInspect, err := ds.client.InspectImageByRef(image.GetImage())
if err != nil { if err != nil {
if dockertools.IsImageNotFoundError(err) {
return nil, nil
}
return nil, err return nil, err
} }
if len(images) != 1 { return imageInspectToRuntimeAPIImage(imageInspect)
return nil, fmt.Errorf("ImageStatus returned more than one image: %+v", images)
}
return images[0], nil
} }
// PullImage pulls an image with authentication config. // PullImage pulls an image with authentication config.
@ -79,6 +78,19 @@ func (ds *dockerService) PullImage(image *runtimeApi.ImageSpec, auth *runtimeApi
// RemoveImage removes the image. // RemoveImage removes the image.
func (ds *dockerService) RemoveImage(image *runtimeApi.ImageSpec) error { func (ds *dockerService) RemoveImage(image *runtimeApi.ImageSpec) error {
_, err := ds.client.RemoveImage(image.GetImage(), dockertypes.ImageRemoveOptions{PruneChildren: true}) // If the image has multiple tags, we need to remove all the tags
// TODO: We assume image.Image is image ID here, which is true in the current implementation
// of kubelet, but we should still clarify this in CRI.
imageInspect, err := ds.client.InspectImageByID(image.GetImage())
if err == nil && imageInspect != nil && len(imageInspect.RepoTags) > 1 {
for _, tag := range imageInspect.RepoTags {
if _, err := ds.client.RemoveImage(tag, dockertypes.ImageRemoveOptions{PruneChildren: true}); err != nil {
return err
}
}
return nil
}
_, err = ds.client.RemoveImage(image.GetImage(), dockertypes.ImageRemoveOptions{PruneChildren: true})
return err return err
} }

View File

@ -0,0 +1,45 @@
/*
Copyright 2016 The Kubernetes 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 dockershim
import (
"testing"
dockertypes "github.com/docker/engine-api/types"
runtimeApi "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/dockertools"
)
func TestRemoveImage(t *testing.T) {
ds, fakeDocker, _ := newTestDockerService()
id := "1111"
fakeDocker.Image = &dockertypes.ImageInspect{ID: id, RepoTags: []string{"foo"}}
ds.RemoveImage(&runtimeApi.ImageSpec{Image: &id})
fakeDocker.AssertCallDetails(dockertools.NewCalledDetail("inspect_image", nil),
dockertools.NewCalledDetail("remove_image", []interface{}{id, dockertypes.ImageRemoveOptions{PruneChildren: true}}))
}
func TestRemoveImageWithMultipleTags(t *testing.T) {
ds, fakeDocker, _ := newTestDockerService()
id := "1111"
fakeDocker.Image = &dockertypes.ImageInspect{ID: id, RepoTags: []string{"foo", "bar"}}
ds.RemoveImage(&runtimeApi.ImageSpec{Image: &id})
fakeDocker.AssertCallDetails(dockertools.NewCalledDetail("inspect_image", nil),
dockertools.NewCalledDetail("remove_image", []interface{}{"foo", dockertypes.ImageRemoveOptions{PruneChildren: true}}),
dockertools.NewCalledDetail("remove_image", []interface{}{"bar", dockertypes.ImageRemoveOptions{PruneChildren: true}}))
}

View File

@ -22,6 +22,7 @@ import (
"strings" "strings"
runtimeApi "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" runtimeApi "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime"
"k8s.io/kubernetes/pkg/kubelet/dockertools"
"k8s.io/kubernetes/pkg/kubelet/leaky" "k8s.io/kubernetes/pkg/kubelet/leaky"
) )
@ -48,6 +49,10 @@ const (
sandboxContainerName = leaky.PodInfraContainerName sandboxContainerName = leaky.PodInfraContainerName
// Delimiter used to construct docker container names. // Delimiter used to construct docker container names.
nameDelimiter = "_" nameDelimiter = "_"
// DockerImageIDPrefix is the prefix of image id in container status.
DockerImageIDPrefix = dockertools.DockerPrefix
// DockerPullableImageIDPrefix is the prefix of pullable image id in container status.
DockerPullableImageIDPrefix = dockertools.DockerPullablePrefix
) )
func makeSandboxName(s *runtimeApi.PodSandboxConfig) string { func makeSandboxName(s *runtimeApi.PodSandboxConfig) string {

View File

@ -401,7 +401,7 @@ func (dm *DockerManager) inspectContainer(id string, podName, podNamespace strin
imageID := DockerPrefix + iResult.Image imageID := DockerPrefix + iResult.Image
imgInspectResult, err := dm.client.InspectImageByID(iResult.Image) imgInspectResult, err := dm.client.InspectImageByID(iResult.Image)
if err != nil { if err != nil {
utilruntime.HandleError(fmt.Errorf("unable to inspect docker image %q while inspecting docker container %q: %v", containerName, iResult.Image, err)) utilruntime.HandleError(fmt.Errorf("unable to inspect docker image %q while inspecting docker container %q: %v", iResult.Image, containerName, err))
} else { } else {
if len(imgInspectResult.RepoDigests) > 1 { if len(imgInspectResult.RepoDigests) > 1 {
glog.V(4).Infof("Container %q had more than one associated RepoDigest (%v), only using the first", containerName, imgInspectResult.RepoDigests) glog.V(4).Infof("Container %q had more than one associated RepoDigest (%v), only using the first", containerName, imgInspectResult.RepoDigests)

View File

@ -425,17 +425,17 @@ func TestDeleteImage(t *testing.T) {
manager, fakeDocker := newTestDockerManager() manager, fakeDocker := newTestDockerManager()
fakeDocker.Image = &dockertypes.ImageInspect{ID: "1111", RepoTags: []string{"foo"}} fakeDocker.Image = &dockertypes.ImageInspect{ID: "1111", RepoTags: []string{"foo"}}
manager.RemoveImage(kubecontainer.ImageSpec{Image: "1111"}) manager.RemoveImage(kubecontainer.ImageSpec{Image: "1111"})
fakeDocker.AssertCallDetails([]calledDetail{{name: "inspect_image"}, {name: "remove_image", fakeDocker.AssertCallDetails(NewCalledDetail("inspect_image", nil), NewCalledDetail("remove_image",
arguments: []interface{}{"1111", dockertypes.ImageRemoveOptions{PruneChildren: true}}}}) []interface{}{"1111", dockertypes.ImageRemoveOptions{PruneChildren: true}}))
} }
func TestDeleteImageWithMultipleTags(t *testing.T) { func TestDeleteImageWithMultipleTags(t *testing.T) {
manager, fakeDocker := newTestDockerManager() manager, fakeDocker := newTestDockerManager()
fakeDocker.Image = &dockertypes.ImageInspect{ID: "1111", RepoTags: []string{"foo", "bar"}} fakeDocker.Image = &dockertypes.ImageInspect{ID: "1111", RepoTags: []string{"foo", "bar"}}
manager.RemoveImage(kubecontainer.ImageSpec{Image: "1111"}) manager.RemoveImage(kubecontainer.ImageSpec{Image: "1111"})
fakeDocker.AssertCallDetails([]calledDetail{{name: "inspect_image"}, fakeDocker.AssertCallDetails(NewCalledDetail("inspect_image", nil),
{name: "remove_image", arguments: []interface{}{"foo", dockertypes.ImageRemoveOptions{PruneChildren: true}}}, NewCalledDetail("remove_image", []interface{}{"foo", dockertypes.ImageRemoveOptions{PruneChildren: true}}),
{name: "remove_image", arguments: []interface{}{"bar", dockertypes.ImageRemoveOptions{PruneChildren: true}}}}) NewCalledDetail("remove_image", []interface{}{"bar", dockertypes.ImageRemoveOptions{PruneChildren: true}}))
} }
func TestKillContainerInPod(t *testing.T) { func TestKillContainerInPod(t *testing.T) {
@ -1409,6 +1409,7 @@ func TestVerifyNonRoot(t *testing.T) {
}, },
"nil image in inspect": { "nil image in inspect": {
container: &api.Container{}, container: &api.Container{},
inspectImage: nil,
expectedError: "unable to inspect image", expectedError: "unable to inspect image",
}, },
"nil config in image inspect": { "nil config in image inspect": {

View File

@ -38,6 +38,11 @@ type calledDetail struct {
arguments []interface{} arguments []interface{}
} }
// NewCalledDetail create a new call detail item.
func NewCalledDetail(name string, arguments []interface{}) calledDetail {
return calledDetail{name: name, arguments: arguments}
}
// FakeDockerClient is a simple fake docker client, so that kubelet can be run for testing without requiring a real docker setup. // FakeDockerClient is a simple fake docker client, so that kubelet can be run for testing without requiring a real docker setup.
type FakeDockerClient struct { type FakeDockerClient struct {
sync.Mutex sync.Mutex
@ -86,7 +91,6 @@ func newClientWithVersionAndClock(version, apiVersion string, c clock.Clock) *Fa
Errors: make(map[string]error), Errors: make(map[string]error),
ContainerMap: make(map[string]*dockertypes.ContainerJSON), ContainerMap: make(map[string]*dockertypes.ContainerJSON),
Clock: c, Clock: c,
// default this to an empty result, so that we never have a nil non-error response from InspectImage // default this to an empty result, so that we never have a nil non-error response from InspectImage
Image: &dockertypes.ImageInspect{}, Image: &dockertypes.ImageInspect{},
} }
@ -213,7 +217,7 @@ func (f *FakeDockerClient) AssertCalls(calls []string) (err error) {
return return
} }
func (f *FakeDockerClient) AssertCallDetails(calls []calledDetail) (err error) { func (f *FakeDockerClient) AssertCallDetails(calls ...calledDetail) (err error) {
f.Lock() f.Lock()
defer f.Unlock() defer f.Unlock()

View File

@ -626,3 +626,10 @@ type imageNotFoundError struct {
func (e imageNotFoundError) Error() string { func (e imageNotFoundError) Error() string {
return fmt.Sprintf("no such image: %q", e.ID) return fmt.Sprintf("no such image: %q", e.ID)
} }
// IsImageNotFoundError checks whether the error is image not found error. This is exposed
// to share with dockershim.
func IsImageNotFoundError(err error) bool {
_, ok := err.(imageNotFoundError)
return ok
}

View File

@ -80,17 +80,12 @@ func (m *kubeGenericRuntimeManager) PullImage(image kubecontainer.ImageSpec, pul
// IsImagePresent checks whether the container image is already in the local storage. // IsImagePresent checks whether the container image is already in the local storage.
func (m *kubeGenericRuntimeManager) IsImagePresent(image kubecontainer.ImageSpec) (bool, error) { func (m *kubeGenericRuntimeManager) IsImagePresent(image kubecontainer.ImageSpec) (bool, error) {
images, err := m.imageService.ListImages(&runtimeApi.ImageFilter{ status, err := m.imageService.ImageStatus(&runtimeApi.ImageSpec{Image: &image.Image})
Image: &runtimeApi.ImageSpec{
Image: &image.Image,
},
})
if err != nil { if err != nil {
glog.Errorf("ListImages failed: %v", err) glog.Errorf("ImageStatus for image %q failed: %v", image, err)
return false, err return false, err
} }
return status != nil, nil
return len(images) > 0, nil
} }
// ListImages gets all images currently on the machine. // ListImages gets all images currently on the machine.