Merge pull request #3097 from brendandburns/img

Add support for garbage collecting images.
This commit is contained in:
Daniel Smith 2014-12-22 17:08:41 -08:00
commit 05a0c5ca17
6 changed files with 108 additions and 19 deletions

View File

@ -45,7 +45,9 @@ type DockerInterface interface {
StopContainer(id string, timeout uint) error StopContainer(id string, timeout uint) error
RemoveContainer(opts docker.RemoveContainerOptions) error RemoveContainer(opts docker.RemoveContainerOptions) error
InspectImage(image string) (*docker.Image, error) InspectImage(image string) (*docker.Image, error)
ListImages(opts docker.ListImagesOptions) ([]docker.APIImages, error)
PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error
RemoveImage(image string) error
Logs(opts docker.LogsOptions) error Logs(opts docker.LogsOptions) error
Version() (*docker.Env, error) Version() (*docker.Env, error)
CreateExec(docker.CreateExecOptions) (*docker.Exec, error) CreateExec(docker.CreateExecOptions) (*docker.Exec, error)
@ -620,3 +622,21 @@ func parseImageName(image string) (string, string) {
type ContainerCommandRunner interface { type ContainerCommandRunner interface {
RunInContainer(containerID string, cmd []string) ([]byte, error) RunInContainer(containerID string, cmd []string) ([]byte, error)
} }
func GetUnusedImages(client DockerInterface) ([]string, error) {
// IMPORTANT: this is _unsafe_ to do while there are active pulls
// See https://github.com/docker/docker/issues/8926 for details
images, err := client.ListImages(docker.ListImagesOptions{
Filters: map[string][]string{
"dangling": {"true"},
},
})
if err != nil {
return nil, err
}
result := make([]string, len(images))
for ix := range images {
result[ix] = images[ix].ID
}
return result, nil
}

View File

@ -21,6 +21,7 @@ import (
"reflect" "reflect"
"sync" "sync"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/fsouza/go-dockerclient" "github.com/fsouza/go-dockerclient"
) )
@ -31,12 +32,14 @@ type FakeDockerClient struct {
Container *docker.Container Container *docker.Container
ContainerMap map[string]*docker.Container ContainerMap map[string]*docker.Container
Image *docker.Image Image *docker.Image
Images []docker.APIImages
Err error Err error
called []string called []string
Stopped []string Stopped []string
pulled []string pulled []string
Created []string Created []string
Removed []string Removed []string
RemovedImages util.StringSet
VersionInfo docker.Env VersionInfo docker.Env
} }
@ -172,10 +175,20 @@ func (f *FakeDockerClient) Version() (*docker.Env, error) {
func (f *FakeDockerClient) CreateExec(_ docker.CreateExecOptions) (*docker.Exec, error) { func (f *FakeDockerClient) CreateExec(_ docker.CreateExecOptions) (*docker.Exec, error) {
return &docker.Exec{"12345678"}, nil return &docker.Exec{"12345678"}, nil
} }
func (f *FakeDockerClient) StartExec(_ string, _ docker.StartExecOptions) error { func (f *FakeDockerClient) StartExec(_ string, _ docker.StartExecOptions) error {
return nil return nil
} }
func (f *FakeDockerClient) ListImages(opts docker.ListImagesOptions) ([]docker.APIImages, error) {
return f.Images, f.Err
}
func (f *FakeDockerClient) RemoveImage(image string) error {
f.RemovedImages.Insert(image)
return f.Err
}
// FakeDockerPuller is a stub implementation of DockerPuller. // FakeDockerPuller is a stub implementation of DockerPuller.
type FakeDockerPuller struct { type FakeDockerPuller struct {
sync.Mutex sync.Mutex

View File

@ -109,6 +109,11 @@ type Kubelet struct {
dockerIDToRef map[dockertools.DockerID]*api.ObjectReference dockerIDToRef map[dockertools.DockerID]*api.ObjectReference
refLock sync.RWMutex refLock sync.RWMutex
// Tracks active pulls. Needed to protect image garbage collection
// See: https://github.com/docker/docker/issues/8926 for details
// TODO: Remove this when (if?) that issue is fixed.
pullLock sync.RWMutex
// Optional, no events will be sent without it // Optional, no events will be sent without it
etcdClient tools.EtcdClient etcdClient tools.EtcdClient
// Optional, defaults to simple implementaiton // Optional, defaults to simple implementaiton
@ -203,6 +208,36 @@ func (kl *Kubelet) purgeOldest(ids []string) error {
return nil return nil
} }
func (kl *Kubelet) GarbageCollectLoop() {
util.Forever(func() {
if err := kl.GarbageCollectContainers(); err != nil {
glog.Errorf("Garbage collect failed: %v", err)
}
if err := kl.GarbageCollectImages(); err != nil {
glog.Errorf("Garbage collect images failed: %v", err)
}
}, time.Minute*1)
}
func (kl *Kubelet) getUnusedImages() ([]string, error) {
kl.pullLock.Lock()
defer kl.pullLock.Unlock()
return dockertools.GetUnusedImages(kl.dockerClient)
}
func (kl *Kubelet) GarbageCollectImages() error {
images, err := kl.getUnusedImages()
if err != nil {
return err
}
for ix := range images {
if err := kl.dockerClient.RemoveImage(images[ix]); err != nil {
glog.Errorf("Failed to remove image: %s (%v)", images[ix], err)
}
}
return nil
}
// TODO: Also enforce a maximum total number of containers. // TODO: Also enforce a maximum total number of containers.
func (kl *Kubelet) GarbageCollectContainers() error { func (kl *Kubelet) GarbageCollectContainers() error {
if kl.maxContainerCount == 0 { if kl.maxContainerCount == 0 {
@ -607,10 +642,7 @@ func (kl *Kubelet) createNetworkContainer(pod *api.BoundPod) (dockertools.Docker
return "", err return "", err
} }
if !ok { if !ok {
if err := kl.dockerPuller.Pull(container.Image); err != nil { if err := kl.pullImage(container.Image, ref); err != nil {
if ref != nil {
record.Eventf(ref, "failed", "failed", "Failed to pull image %s", container.Image)
}
return "", err return "", err
} }
} }
@ -620,6 +652,18 @@ func (kl *Kubelet) createNetworkContainer(pod *api.BoundPod) (dockertools.Docker
return kl.runContainer(pod, container, nil, "") return kl.runContainer(pod, container, nil, "")
} }
func (kl *Kubelet) pullImage(img string, ref *api.ObjectReference) error {
kl.pullLock.RLock()
defer kl.pullLock.RUnlock()
if err := kl.dockerPuller.Pull(img); err != nil {
if ref != nil {
record.Eventf(ref, "failed", "failed", "Failed to pull image %s", img)
}
return err
}
return nil
}
// Kill all containers in a pod. Returns the number of containers deleted and an error if one occurs. // Kill all containers in a pod. Returns the number of containers deleted and an error if one occurs.
func (kl *Kubelet) killContainersInPod(pod *api.BoundPod, dockerContainers dockertools.DockerContainers) (int, error) { func (kl *Kubelet) killContainersInPod(pod *api.BoundPod, dockerContainers dockertools.DockerContainers) (int, error) {
podFullName := GetPodFullName(pod) podFullName := GetPodFullName(pod)

View File

@ -47,7 +47,9 @@ func init() {
func newTestKubelet(t *testing.T) (*Kubelet, *tools.FakeEtcdClient, *dockertools.FakeDockerClient) { func newTestKubelet(t *testing.T) (*Kubelet, *tools.FakeEtcdClient, *dockertools.FakeDockerClient) {
fakeEtcdClient := tools.NewFakeEtcdClient(t) fakeEtcdClient := tools.NewFakeEtcdClient(t)
fakeDocker := &dockertools.FakeDockerClient{} fakeDocker := &dockertools.FakeDockerClient{
RemovedImages: util.StringSet{},
}
kubelet := &Kubelet{} kubelet := &Kubelet{}
kubelet.dockerClient = fakeDocker kubelet.dockerClient = fakeDocker
@ -1698,3 +1700,26 @@ func TestSyncPodsWithPullPolicy(t *testing.T) {
} }
fakeDocker.Unlock() fakeDocker.Unlock()
} }
func TestGarbageCollectImages(t *testing.T) {
kubelet, _, fakeDocker := newTestKubelet(t)
fakeDocker.Images = []docker.APIImages{
{
ID: "foo",
},
{
ID: "bar",
},
}
if err := kubelet.GarbageCollectImages(); err != nil {
t.Errorf("unexpected error: %v", err)
}
if len(fakeDocker.RemovedImages) != 2 ||
!fakeDocker.RemovedImages.Has("foo") ||
!fakeDocker.RemovedImages.Has("bar") {
t.Errorf("unexpected images removed: %v", fakeDocker.RemovedImages)
}
}

View File

@ -24,7 +24,6 @@ import (
"path" "path"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client"
@ -77,18 +76,6 @@ func ConnectToDockerOrDie(dockerEndpoint string) *docker.Client {
return client return client
} }
// TODO: move this into the kubelet itself
func GarbageCollectLoop(k *Kubelet) {
func() {
util.Forever(func() {
err := k.GarbageCollectContainers()
if err != nil {
glog.Errorf("Garbage collect failed: %v", err)
}
}, time.Minute*1)
}()
}
// TODO: move this into the kubelet itself // TODO: move this into the kubelet itself
func MonitorCAdvisor(k *Kubelet, cp uint) { func MonitorCAdvisor(k *Kubelet, cp uint) {
defer util.HandleCrash() defer util.HandleCrash()

View File

@ -269,7 +269,7 @@ func createAndInitKubelet(kc *KubeletConfig, pc *config.PodConfig) *kubelet.Kube
k.BirthCry() k.BirthCry()
go kubelet.GarbageCollectLoop(k) go k.GarbageCollectLoop()
go kubelet.MonitorCAdvisor(k, kc.CAdvisorPort) go kubelet.MonitorCAdvisor(k, kc.CAdvisorPort)
kubelet.InitHealthChecking(k) kubelet.InitHealthChecking(k)