diff --git a/pkg/kubelet/dockershim/BUILD b/pkg/kubelet/dockershim/BUILD index d73a27b007e..3d82bfedd4d 100644 --- a/pkg/kubelet/dockershim/BUILD +++ b/pkg/kubelet/dockershim/BUILD @@ -17,6 +17,7 @@ go_library( "docker_logs.go", "docker_sandbox.go", "docker_service.go", + "docker_stats.go", "docker_stats_linux.go", "docker_stats_unsupported.go", "docker_stats_windows.go", @@ -93,6 +94,7 @@ go_test( "docker_image_test.go", "docker_sandbox_test.go", "docker_service_test.go", + "docker_stats_test.go", "helpers_linux_test.go", "helpers_test.go", "naming_test.go", diff --git a/pkg/kubelet/dockershim/docker_image_linux.go b/pkg/kubelet/dockershim/docker_image_linux.go index e68d83b19ab..25e3d7a074e 100644 --- a/pkg/kubelet/dockershim/docker_image_linux.go +++ b/pkg/kubelet/dockershim/docker_image_linux.go @@ -20,12 +20,58 @@ package dockershim import ( "context" - "fmt" + "os" + "path/filepath" + "time" + + "k8s.io/klog" runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" ) // ImageFsInfo returns information of the filesystem that is used to store images. -func (ds *dockerService) ImageFsInfo(_ context.Context, r *runtimeapi.ImageFsInfoRequest) (*runtimeapi.ImageFsInfoResponse, error) { - return nil, fmt.Errorf("not implemented") +func (ds *dockerService) ImageFsInfo(_ context.Context, _ *runtimeapi.ImageFsInfoRequest) (*runtimeapi.ImageFsInfoResponse, error) { + info, err := ds.client.Info() + if err != nil { + klog.Errorf("Failed to get docker info: %v", err) + return nil, err + } + + bytes, inodes, err := dirSize(filepath.Join(info.DockerRootDir, "image")) + if err != nil { + return nil, err + } + + return &runtimeapi.ImageFsInfoResponse{ + ImageFilesystems: []*runtimeapi.FilesystemUsage{ + { + Timestamp: time.Now().Unix(), + FsId: &runtimeapi.FilesystemIdentifier{ + Mountpoint: info.DockerRootDir, + }, + UsedBytes: &runtimeapi.UInt64Value{ + Value: uint64(bytes), + }, + InodesUsed: &runtimeapi.UInt64Value{ + Value: uint64(inodes), + }, + }, + }, + }, nil +} + +func dirSize(path string) (int64, int64, error) { + bytes := int64(0) + inodes := int64(0) + err := filepath.Walk(path, func(dir string, info os.FileInfo, err error) error { + if err != nil { + return err + } + inodes += 1 + if !info.IsDir() { + bytes += info.Size() + } + return nil + }) + return bytes, inodes, err } diff --git a/pkg/kubelet/dockershim/docker_stats.go b/pkg/kubelet/dockershim/docker_stats.go new file mode 100644 index 00000000000..ede30c4efbb --- /dev/null +++ b/pkg/kubelet/dockershim/docker_stats.go @@ -0,0 +1,61 @@ +/* +Copyright 2019 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 ( + "context" + + runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +// ContainerStats returns stats for a container stats request based on container id. +func (ds *dockerService) ContainerStats(_ context.Context, r *runtimeapi.ContainerStatsRequest) (*runtimeapi.ContainerStatsResponse, error) { + stats, err := ds.getContainerStats(r.ContainerId) + if err != nil { + return nil, err + } + return &runtimeapi.ContainerStatsResponse{Stats: stats}, nil +} + +// ListContainerStats returns stats for a list container stats request based on a filter. +func (ds *dockerService) ListContainerStats(ctx context.Context, r *runtimeapi.ListContainerStatsRequest) (*runtimeapi.ListContainerStatsResponse, error) { + containerStatsFilter := r.GetFilter() + filter := &runtimeapi.ContainerFilter{} + + if containerStatsFilter != nil { + filter.Id = containerStatsFilter.Id + filter.PodSandboxId = containerStatsFilter.PodSandboxId + filter.LabelSelector = containerStatsFilter.LabelSelector + } + + listResp, err := ds.ListContainers(ctx, &runtimeapi.ListContainersRequest{Filter: filter}) + if err != nil { + return nil, err + } + + var stats []*runtimeapi.ContainerStats + for _, container := range listResp.Containers { + containerStats, err := ds.getContainerStats(container.Id) + if err != nil { + return nil, err + } + + stats = append(stats, containerStats) + } + + return &runtimeapi.ListContainerStatsResponse{Stats: stats}, nil +} diff --git a/pkg/kubelet/dockershim/docker_stats_linux.go b/pkg/kubelet/dockershim/docker_stats_linux.go index b78ff9c3c95..11d6add29c4 100644 --- a/pkg/kubelet/dockershim/docker_stats_linux.go +++ b/pkg/kubelet/dockershim/docker_stats_linux.go @@ -20,17 +20,55 @@ package dockershim import ( "context" - "fmt" + "time" runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" ) -// ContainerStats returns stats for a container stats request based on container id. -func (ds *dockerService) ContainerStats(_ context.Context, r *runtimeapi.ContainerStatsRequest) (*runtimeapi.ContainerStatsResponse, error) { - return nil, fmt.Errorf("not implemented") -} +func (ds *dockerService) getContainerStats(containerID string) (*runtimeapi.ContainerStats, error) { + info, err := ds.client.Info() + if err != nil { + return nil, err + } -// ListContainerStats returns stats for a list container stats request based on a filter. -func (ds *dockerService) ListContainerStats(_ context.Context, r *runtimeapi.ListContainerStatsRequest) (*runtimeapi.ListContainerStatsResponse, error) { - return nil, fmt.Errorf("not implemented") + statsJSON, err := ds.client.GetContainerStats(containerID) + if err != nil { + return nil, err + } + + containerJSON, err := ds.client.InspectContainerWithSize(containerID) + if err != nil { + return nil, err + } + + statusResp, err := ds.ContainerStatus(context.Background(), &runtimeapi.ContainerStatusRequest{ContainerId: containerID}) + if err != nil { + return nil, err + } + status := statusResp.GetStatus() + + dockerStats := statsJSON.Stats + timestamp := time.Now().UnixNano() + containerStats := &runtimeapi.ContainerStats{ + Attributes: &runtimeapi.ContainerAttributes{ + Id: containerID, + Metadata: status.Metadata, + Labels: status.Labels, + Annotations: status.Annotations, + }, + Cpu: &runtimeapi.CpuUsage{ + Timestamp: timestamp, + UsageCoreNanoSeconds: &runtimeapi.UInt64Value{Value: dockerStats.CPUStats.CPUUsage.TotalUsage}, + }, + Memory: &runtimeapi.MemoryUsage{ + Timestamp: timestamp, + WorkingSetBytes: &runtimeapi.UInt64Value{Value: dockerStats.MemoryStats.PrivateWorkingSet}, + }, + WritableLayer: &runtimeapi.FilesystemUsage{ + Timestamp: timestamp, + FsId: &runtimeapi.FilesystemIdentifier{Mountpoint: info.DockerRootDir}, + UsedBytes: &runtimeapi.UInt64Value{Value: uint64(*containerJSON.SizeRw)}, + }, + } + return containerStats, nil } diff --git a/pkg/kubelet/dockershim/docker_stats_test.go b/pkg/kubelet/dockershim/docker_stats_test.go new file mode 100644 index 00000000000..b4ce51ae6be --- /dev/null +++ b/pkg/kubelet/dockershim/docker_stats_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2019 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/docker/api/types" + "github.com/stretchr/testify/assert" + runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + "k8s.io/kubernetes/pkg/kubelet/dockershim/libdocker" +) + +func TestContainerStats(t *testing.T) { + tests := map[string]struct { + containerID string + container *libdocker.FakeContainer + containerStats *dockertypes.StatsJSON + calledDetails []libdocker.CalledDetail + }{ + "container exists": { + "fake_container", + &libdocker.FakeContainer{ID: "fake_container"}, + &dockertypes.StatsJSON{}, + []libdocker.CalledDetail{ + libdocker.NewCalledDetail("get_container_stats", nil), + libdocker.NewCalledDetail("inspect_container_withsize", nil), + libdocker.NewCalledDetail("inspect_container", nil), + libdocker.NewCalledDetail("inspect_image", nil), + }, + }, + "container doesn't exists": { + "nonexistant_fake_container", + &libdocker.FakeContainer{ID: "fake_container"}, + &dockertypes.StatsJSON{}, + []libdocker.CalledDetail{ + libdocker.NewCalledDetail("get_container_stats", nil), + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ds, fakeDocker, _ := newTestDockerService() + fakeDocker.SetFakeContainers([]*libdocker.FakeContainer{test.container}) + fakeDocker.InjectContainerStats(map[string]*dockertypes.StatsJSON{test.container.ID: test.containerStats}) + ds.ContainerStats(getTestCTX(), &runtimeapi.ContainerStatsRequest{ContainerId: test.containerID}) + err := fakeDocker.AssertCallDetails(test.calledDetails...) + assert.NoError(t, err) + }) + } +} diff --git a/pkg/kubelet/dockershim/docker_stats_unsupported.go b/pkg/kubelet/dockershim/docker_stats_unsupported.go index 751624cfb1f..736215a9d64 100644 --- a/pkg/kubelet/dockershim/docker_stats_unsupported.go +++ b/pkg/kubelet/dockershim/docker_stats_unsupported.go @@ -19,18 +19,11 @@ limitations under the License. package dockershim import ( - "context" "fmt" runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" ) -// ContainerStats returns stats for a container stats request based on container id. -func (ds *dockerService) ContainerStats(_ context.Context, r *runtimeapi.ContainerStatsRequest) (*runtimeapi.ContainerStatsResponse, error) { - return nil, fmt.Errorf("not implemented") -} - -// ListContainerStats returns stats for a list container stats request based on a filter. -func (ds *dockerService) ListContainerStats(_ context.Context, r *runtimeapi.ListContainerStatsRequest) (*runtimeapi.ListContainerStatsResponse, error) { +func (ds *dockerService) getContainerStats(containerID string) (*runtimeapi.ContainerStats, error) { return nil, fmt.Errorf("not implemented") } diff --git a/pkg/kubelet/dockershim/docker_stats_windows.go b/pkg/kubelet/dockershim/docker_stats_windows.go index 460730257f7..053c11d93ad 100644 --- a/pkg/kubelet/dockershim/docker_stats_windows.go +++ b/pkg/kubelet/dockershim/docker_stats_windows.go @@ -25,44 +25,6 @@ import ( runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" ) -// ContainerStats returns stats for a container stats request based on container id. -func (ds *dockerService) ContainerStats(_ context.Context, r *runtimeapi.ContainerStatsRequest) (*runtimeapi.ContainerStatsResponse, error) { - stats, err := ds.getContainerStats(r.ContainerId) - if err != nil { - return nil, err - } - return &runtimeapi.ContainerStatsResponse{Stats: stats}, nil -} - -// ListContainerStats returns stats for a list container stats request based on a filter. -func (ds *dockerService) ListContainerStats(ctx context.Context, r *runtimeapi.ListContainerStatsRequest) (*runtimeapi.ListContainerStatsResponse, error) { - containerStatsFilter := r.GetFilter() - filter := &runtimeapi.ContainerFilter{} - - if containerStatsFilter != nil { - filter.Id = containerStatsFilter.Id - filter.PodSandboxId = containerStatsFilter.PodSandboxId - filter.LabelSelector = containerStatsFilter.LabelSelector - } - - listResp, err := ds.ListContainers(ctx, &runtimeapi.ListContainersRequest{Filter: filter}) - if err != nil { - return nil, err - } - - var stats []*runtimeapi.ContainerStats - for _, container := range listResp.Containers { - containerStats, err := ds.getContainerStats(container.Id) - if err != nil { - return nil, err - } - - stats = append(stats, containerStats) - } - - return &runtimeapi.ListContainerStatsResponse{Stats: stats}, nil -} - func (ds *dockerService) getContainerStats(containerID string) (*runtimeapi.ContainerStats, error) { info, err := ds.client.Info() if err != nil { diff --git a/pkg/kubelet/dockershim/libdocker/fake_client.go b/pkg/kubelet/dockershim/libdocker/fake_client.go index 7e95cf36516..57cdad82a48 100644 --- a/pkg/kubelet/dockershim/libdocker/fake_client.go +++ b/pkg/kubelet/dockershim/libdocker/fake_client.go @@ -71,12 +71,13 @@ type FakeDockerClient struct { // Images pulled by ref (name or ID). ImagesPulled []string - VersionInfo dockertypes.Version - Information dockertypes.Info - ExecInspect *dockertypes.ContainerExecInspect - execCmd []string - EnableSleep bool - ImageHistoryMap map[string][]dockerimagetypes.HistoryResponseItem + VersionInfo dockertypes.Version + Information dockertypes.Info + ExecInspect *dockertypes.ContainerExecInspect + execCmd []string + EnableSleep bool + ImageHistoryMap map[string][]dockerimagetypes.HistoryResponseItem + ContainerStatsMap map[string]*dockertypes.StatsJSON } const ( @@ -913,9 +914,19 @@ func (f *FakeDockerPuller) GetImageRef(image string) (string, error) { return image, err } +func (f *FakeDockerClient) InjectContainerStats(data map[string]*dockertypes.StatsJSON) { + f.Lock() + defer f.Unlock() + f.ContainerStatsMap = data +} + func (f *FakeDockerClient) GetContainerStats(id string) (*dockertypes.StatsJSON, error) { f.Lock() defer f.Unlock() - f.appendCalled(CalledDetail{name: "getContainerStats"}) - return nil, fmt.Errorf("not implemented") + f.appendCalled(CalledDetail{name: "get_container_stats"}) + stats, ok := f.ContainerStatsMap[id] + if !ok { + return nil, fmt.Errorf("container %q not found", id) + } + return stats, nil }