From 322b6ef33399e9c5ea06b943272ea6423708d4cd Mon Sep 17 00:00:00 2001 From: Lantao Liu Date: Tue, 16 May 2017 19:49:07 +0000 Subject: [PATCH] Add unit test. Signed-off-by: Lantao Liu --- pkg/server/container_create_test.go | 157 ++++++++ pkg/server/container_list_test.go | 229 +++++++++++ pkg/server/container_remove_test.go | 175 +++++++++ pkg/server/container_start_test.go | 402 ++++++++++++++++++++ pkg/server/container_status_test.go | 173 +++++++++ pkg/server/container_stop_test.go | 199 ++++++++++ pkg/server/events.go | 23 +- pkg/server/events_test.go | 154 ++++++++ pkg/server/helpers_test.go | 93 +++++ pkg/server/service_test.go | 19 +- pkg/server/testing/fake_execution_client.go | 24 +- 11 files changed, 1633 insertions(+), 15 deletions(-) create mode 100644 pkg/server/container_create_test.go create mode 100644 pkg/server/container_list_test.go create mode 100644 pkg/server/container_remove_test.go create mode 100644 pkg/server/container_start_test.go create mode 100644 pkg/server/container_status_test.go create mode 100644 pkg/server/container_stop_test.go create mode 100644 pkg/server/events_test.go diff --git a/pkg/server/container_create_test.go b/pkg/server/container_create_test.go new file mode 100644 index 000000000..6deee479f --- /dev/null +++ b/pkg/server/container_create_test.go @@ -0,0 +1,157 @@ +/* +Copyright 2017 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 server + +import ( + "errors" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" + + "github.com/kubernetes-incubator/cri-containerd/pkg/metadata" + ostesting "github.com/kubernetes-incubator/cri-containerd/pkg/os/testing" +) + +func TestCreateContainer(t *testing.T) { + testSandboxID := "test-sandbox-id" + testNameMeta := &runtime.ContainerMetadata{ + Name: "test-name", + Attempt: 1, + } + testSandboxNameMeta := &runtime.PodSandboxMetadata{ + Name: "test-sandbox-name", + Uid: "test-sandbox-uid", + Namespace: "test-sandbox-namespace", + Attempt: 2, + } + testConfig := &runtime.ContainerConfig{ + Metadata: testNameMeta, + Image: &runtime.ImageSpec{ + Image: "test-image", + }, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + } + testSandboxConfig := &runtime.PodSandboxConfig{ + Metadata: testSandboxNameMeta, + } + + for desc, test := range map[string]struct { + sandboxMetadata *metadata.SandboxMetadata + reserveNameErr bool + createRootDirErr error + createMetadataErr bool + expectErr bool + expectMeta *metadata.ContainerMetadata + }{ + "should return error if sandbox does not exist": { + sandboxMetadata: nil, + expectErr: true, + }, + "should return error if name is reserved": { + sandboxMetadata: &metadata.SandboxMetadata{ + ID: testSandboxID, + Name: makeSandboxName(testSandboxNameMeta), + Config: testSandboxConfig, + }, + reserveNameErr: true, + expectErr: true, + }, + "should return error if fail to create root directory": { + sandboxMetadata: &metadata.SandboxMetadata{ + ID: testSandboxID, + Name: makeSandboxName(testSandboxNameMeta), + Config: testSandboxConfig, + }, + createRootDirErr: errors.New("random error"), + expectErr: true, + }, + "should be able to create container successfully": { + sandboxMetadata: &metadata.SandboxMetadata{ + ID: testSandboxID, + Name: makeSandboxName(testSandboxNameMeta), + Config: testSandboxConfig, + }, + expectErr: false, + expectMeta: &metadata.ContainerMetadata{ + Name: makeContainerName(testNameMeta, testSandboxNameMeta), + SandboxID: testSandboxID, + Config: testConfig, + }, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + fakeOS := c.os.(*ostesting.FakeOS) + if test.sandboxMetadata != nil { + assert.NoError(t, c.sandboxStore.Create(*test.sandboxMetadata)) + } + containerName := makeContainerName(testNameMeta, testSandboxNameMeta) + if test.reserveNameErr { + assert.NoError(t, c.containerNameIndex.Reserve(containerName, "random id")) + } + rootExists := false + rootPath := "" + fakeOS.MkdirAllFn = func(path string, perm os.FileMode) error { + assert.Equal(t, os.FileMode(0755), perm) + rootPath = path + if test.createRootDirErr == nil { + rootExists = true + } + return test.createRootDirErr + } + fakeOS.RemoveAllFn = func(path string) error { + assert.Equal(t, rootPath, path) + rootExists = false + return nil + } + resp, err := c.CreateContainer(context.Background(), &runtime.CreateContainerRequest{ + PodSandboxId: testSandboxID, + Config: testConfig, + SandboxConfig: testSandboxConfig, + }) + if test.expectErr { + assert.Error(t, err) + assert.Nil(t, resp) + assert.False(t, rootExists, "root directory should be cleaned up") + if !test.reserveNameErr { + assert.NoError(t, c.containerNameIndex.Reserve(containerName, "random id"), + "container name should be released") + } + metas, err := c.containerStore.List() + assert.NoError(t, err) + assert.Empty(t, metas, "container metadata should not be created") + continue + } + assert.NoError(t, err) + assert.NotNil(t, resp) + id := resp.GetContainerId() + assert.True(t, rootExists) + assert.Equal(t, getContainerRootDir(c.rootDir, id), rootPath, "root directory should be created") + meta, err := c.containerStore.Get(id) + assert.NoError(t, err) + require.NotNil(t, meta) + test.expectMeta.ID = id + // TODO(random-liu): Use fake clock to test CreatedAt. + test.expectMeta.CreatedAt = meta.CreatedAt + assert.Equal(t, test.expectMeta, meta, "container metadata should be created") + } +} diff --git a/pkg/server/container_list_test.go b/pkg/server/container_list_test.go new file mode 100644 index 000000000..2c3b6b44c --- /dev/null +++ b/pkg/server/container_list_test.go @@ -0,0 +1,229 @@ +/* +Copyright 2017 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 server + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + + "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" + + "github.com/kubernetes-incubator/cri-containerd/pkg/metadata" +) + +func TestToCRIContainer(t *testing.T) { + config := &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "test-name", + Attempt: 1, + }, + Image: &runtime.ImageSpec{Image: "test-image"}, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + } + createdAt := time.Now().UnixNano() + meta := &metadata.ContainerMetadata{ + ID: "test-id", + Name: "test-name", + SandboxID: "test-sandbox-id", + Config: config, + ImageRef: "test-image-ref", + Pid: 1234, + CreatedAt: createdAt, + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + ExitCode: 1, + Reason: "test-reason", + Message: "test-message", + } + expect := &runtime.Container{ + Id: "test-id", + PodSandboxId: "test-sandbox-id", + Metadata: config.GetMetadata(), + Image: config.GetImage(), + ImageRef: "test-image-ref", + State: runtime.ContainerState_CONTAINER_EXITED, + CreatedAt: createdAt, + Labels: config.GetLabels(), + Annotations: config.GetAnnotations(), + } + c := toCRIContainer(meta) + assert.Equal(t, expect, c) +} + +func TestFilterContainers(t *testing.T) { + c := newTestCRIContainerdService() + + testContainers := []*runtime.Container{ + { + Id: "1", + PodSandboxId: "s-1", + Metadata: &runtime.ContainerMetadata{Name: "name-1", Attempt: 1}, + State: runtime.ContainerState_CONTAINER_RUNNING, + }, + { + Id: "2", + PodSandboxId: "s-2", + Metadata: &runtime.ContainerMetadata{Name: "name-2", Attempt: 2}, + State: runtime.ContainerState_CONTAINER_EXITED, + Labels: map[string]string{"a": "b"}, + }, + { + Id: "3", + PodSandboxId: "s-2", + Metadata: &runtime.ContainerMetadata{Name: "name-2", Attempt: 3}, + State: runtime.ContainerState_CONTAINER_CREATED, + Labels: map[string]string{"c": "d"}, + }, + } + for desc, test := range map[string]struct { + filter *runtime.ContainerFilter + expect []*runtime.Container + }{ + "no filter": { + expect: testContainers, + }, + "id filter": { + filter: &runtime.ContainerFilter{Id: "2"}, + expect: []*runtime.Container{testContainers[1]}, + }, + "state filter": { + filter: &runtime.ContainerFilter{ + State: &runtime.ContainerStateValue{ + State: runtime.ContainerState_CONTAINER_EXITED, + }, + }, + expect: []*runtime.Container{testContainers[1]}, + }, + "label filter": { + filter: &runtime.ContainerFilter{ + LabelSelector: map[string]string{"a": "b"}, + }, + expect: []*runtime.Container{testContainers[1]}, + }, + "sandbox id filter": { + filter: &runtime.ContainerFilter{PodSandboxId: "s-2"}, + expect: []*runtime.Container{testContainers[1], testContainers[2]}, + }, + "mixed filter not matched": { + filter: &runtime.ContainerFilter{ + Id: "1", + PodSandboxId: "s-2", + LabelSelector: map[string]string{"a": "b"}, + }, + expect: []*runtime.Container{}, + }, + "mixed filter matched": { + filter: &runtime.ContainerFilter{ + PodSandboxId: "s-2", + State: &runtime.ContainerStateValue{ + State: runtime.ContainerState_CONTAINER_CREATED, + }, + LabelSelector: map[string]string{"c": "d"}, + }, + expect: []*runtime.Container{testContainers[2]}, + }, + } { + filtered := c.filterCRIContainers(testContainers, test.filter) + assert.Equal(t, test.expect, filtered, desc) + } +} + +func TestListContainers(t *testing.T) { + c := newTestCRIContainerdService() + + createdAt := time.Now().UnixNano() + startedAt := time.Now().UnixNano() + finishedAt := time.Now().UnixNano() + containersInStore := []metadata.ContainerMetadata{ + { + ID: "1", + Name: "name-1", + SandboxID: "s-1", + Config: &runtime.ContainerConfig{Metadata: &runtime.ContainerMetadata{Name: "name-1"}}, + CreatedAt: createdAt, + }, + { + ID: "2", + Name: "name-2", + SandboxID: "s-1", + Config: &runtime.ContainerConfig{Metadata: &runtime.ContainerMetadata{Name: "name-2"}}, + CreatedAt: createdAt, + StartedAt: startedAt, + }, + { + ID: "3", + Name: "name-3", + SandboxID: "s-1", + Config: &runtime.ContainerConfig{Metadata: &runtime.ContainerMetadata{Name: "name-3"}}, + CreatedAt: createdAt, + StartedAt: startedAt, + FinishedAt: finishedAt, + }, + { + ID: "4", + Name: "name-4", + SandboxID: "s-2", + Config: &runtime.ContainerConfig{Metadata: &runtime.ContainerMetadata{Name: "name-4"}}, + CreatedAt: createdAt, + }, + } + filter := &runtime.ContainerFilter{ + PodSandboxId: "s-1", + } + expect := []*runtime.Container{ + { + Id: "1", + PodSandboxId: "s-1", + Metadata: &runtime.ContainerMetadata{Name: "name-1"}, + State: runtime.ContainerState_CONTAINER_CREATED, + CreatedAt: createdAt, + }, + { + Id: "2", + PodSandboxId: "s-1", + Metadata: &runtime.ContainerMetadata{Name: "name-2"}, + State: runtime.ContainerState_CONTAINER_RUNNING, + CreatedAt: createdAt, + }, + { + Id: "3", + PodSandboxId: "s-1", + Metadata: &runtime.ContainerMetadata{Name: "name-3"}, + State: runtime.ContainerState_CONTAINER_EXITED, + CreatedAt: createdAt, + }, + } + + // Inject test metadata + for _, cntr := range containersInStore { + c.containerStore.Create(cntr) + } + + resp, err := c.ListContainers(context.Background(), &runtime.ListContainersRequest{Filter: filter}) + assert.NoError(t, err) + require.NotNil(t, resp) + containers := resp.GetContainers() + assert.Len(t, containers, len(expect)) + for _, cntr := range expect { + assert.Contains(t, containers, cntr) + } +} diff --git a/pkg/server/container_remove_test.go b/pkg/server/container_remove_test.go new file mode 100644 index 000000000..10a18d22d --- /dev/null +++ b/pkg/server/container_remove_test.go @@ -0,0 +1,175 @@ +/* +Copyright 2017 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 server + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" + + "github.com/kubernetes-incubator/cri-containerd/pkg/metadata" + ostesting "github.com/kubernetes-incubator/cri-containerd/pkg/os/testing" +) + +// TestSetContainerRemoving tests setContainerRemoving sets removing +// state correctly. +func TestSetContainerRemoving(t *testing.T) { + testID := "test-id" + for desc, test := range map[string]struct { + metadata *metadata.ContainerMetadata + expectErr bool + }{ + "should return error when container is in running state": { + metadata: &metadata.ContainerMetadata{ + ID: testID, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + }, + expectErr: true, + }, + "should return error when container is in removing state": { + metadata: &metadata.ContainerMetadata{ + ID: testID, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + Removing: true, + }, + expectErr: true, + }, + "should not return error when container is not running and removing": { + metadata: &metadata.ContainerMetadata{ + ID: testID, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + }, + expectErr: false, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + if test.metadata != nil { + assert.NoError(t, c.containerStore.Create(*test.metadata)) + } + err := c.setContainerRemoving(testID) + meta, getErr := c.containerStore.Get(testID) + assert.NoError(t, getErr) + if test.expectErr { + assert.Error(t, err) + assert.Equal(t, test.metadata, meta, "metadata should not be updated") + } else { + assert.NoError(t, err) + assert.True(t, meta.Removing, "removing should be set") + } + } +} + +func TestRemoveContainer(t *testing.T) { + testID := "test-id" + testName := "test-name" + for desc, test := range map[string]struct { + metadata *metadata.ContainerMetadata + removeDirErr error + expectErr bool + expectUnsetRemoving bool + }{ + "should return error when container is still running": { + metadata: &metadata.ContainerMetadata{ + ID: testID, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + }, + expectErr: true, + }, + "should return error when there is ongoing removing": { + metadata: &metadata.ContainerMetadata{ + ID: testID, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + Removing: true, + }, + expectErr: true, + }, + "should not return error if container does not exist": { + metadata: nil, + expectErr: false, + }, + "should return error if remove container root fails": { + metadata: &metadata.ContainerMetadata{ + ID: testID, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + }, + removeDirErr: errors.New("random error"), + expectErr: true, + expectUnsetRemoving: true, + }, + "should be able to remove container successfully": { + metadata: &metadata.ContainerMetadata{ + ID: testID, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + }, + expectErr: false, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + fakeOS := c.os.(*ostesting.FakeOS) + if test.metadata != nil { + assert.NoError(t, c.containerNameIndex.Reserve(testName, testID)) + assert.NoError(t, c.containerStore.Create(*test.metadata)) + } + fakeOS.RemoveAllFn = func(path string) error { + assert.Equal(t, getContainerRootDir(c.rootDir, testID), path) + return test.removeDirErr + } + resp, err := c.RemoveContainer(context.Background(), &runtime.RemoveContainerRequest{ + ContainerId: testID, + }) + if test.expectErr { + assert.Error(t, err) + assert.Nil(t, resp) + if !test.expectUnsetRemoving { + continue + } + meta, err := c.containerStore.Get(testID) + assert.NoError(t, err) + require.NotNil(t, meta) + // Also covers resetContainerRemoving. + assert.False(t, meta.Removing, "removing state should be unset") + continue + } + assert.NoError(t, err) + assert.NotNil(t, resp) + meta, err := c.containerStore.Get(testID) + assert.Error(t, err) + assert.True(t, metadata.IsNotExistError(err)) + assert.Nil(t, meta, "container metadata should be removed") + assert.NoError(t, c.containerNameIndex.Reserve(testName, testID), + "container name should be released") + } +} diff --git a/pkg/server/container_start_test.go b/pkg/server/container_start_test.go new file mode 100644 index 000000000..7ea788560 --- /dev/null +++ b/pkg/server/container_start_test.go @@ -0,0 +1,402 @@ +/* +Copyright 2017 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 server + +import ( + "encoding/json" + "errors" + "io" + "os" + "testing" + "time" + + "github.com/containerd/containerd/api/services/execution" + "github.com/containerd/containerd/api/types/container" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" + + "github.com/kubernetes-incubator/cri-containerd/pkg/metadata" + ostesting "github.com/kubernetes-incubator/cri-containerd/pkg/os/testing" + servertesting "github.com/kubernetes-incubator/cri-containerd/pkg/server/testing" +) + +func getStartContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandboxConfig, + func(*testing.T, string, uint32, *runtimespec.Spec)) { + config := &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "test-name", + Attempt: 1, + }, + Command: []string{"test", "command"}, + Args: []string{"test", "args"}, + WorkingDir: "test-cwd", + Envs: []*runtime.KeyValue{ + {Key: "k1", Value: "v1"}, + {Key: "k2", Value: "v2"}, + }, + Mounts: []*runtime.Mount{ + { + ContainerPath: "container-path-1", + HostPath: "host-path-1", + }, + { + ContainerPath: "container-path-2", + HostPath: "host-path-2", + Readonly: true, + }, + }, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + Linux: &runtime.LinuxContainerConfig{ + Resources: &runtime.LinuxContainerResources{ + CpuPeriod: 100, + CpuQuota: 200, + CpuShares: 300, + MemoryLimitInBytes: 400, + OomScoreAdj: 500, + }, + SecurityContext: &runtime.LinuxContainerSecurityContext{ + Capabilities: &runtime.Capability{ + AddCapabilities: []string{"CAP_SYS_ADMIN"}, + DropCapabilities: []string{"CAP_CHOWN"}, + }, + SupplementalGroups: []int64{1111, 2222}, + }, + }, + } + sandboxConfig := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-sandbox-name", + Uid: "test-sandbox-uid", + Namespace: "test-sandbox-ns", + Attempt: 2, + }, + Linux: &runtime.LinuxPodSandboxConfig{ + CgroupParent: "/test/cgroup/parent", + }, + } + specCheck := func(t *testing.T, id string, sandboxPid uint32, spec *runtimespec.Spec) { + assert.Equal(t, relativeRootfsPath, spec.Root.Path) + assert.Equal(t, []string{"test", "command", "test", "args"}, spec.Process.Args) + assert.Equal(t, "test-cwd", spec.Process.Cwd) + assert.Contains(t, spec.Process.Env, "k1=v1", "k2=v2") + + t.Logf("Check bind mount") + found1, found2 := false, false + for _, m := range spec.Mounts { + if m.Source == "host-path-1" { + assert.Equal(t, m.Destination, "container-path-1") + assert.Contains(t, m.Options, "rw") + found1 = true + } + if m.Source == "host-path-2" { + assert.Equal(t, m.Destination, "container-path-2") + assert.Contains(t, m.Options, "ro") + found2 = true + } + } + assert.True(t, found1) + assert.True(t, found2) + + t.Logf("Check resource limits") + assert.EqualValues(t, *spec.Linux.Resources.CPU.Period, 100) + assert.EqualValues(t, *spec.Linux.Resources.CPU.Quota, 200) + assert.EqualValues(t, *spec.Linux.Resources.CPU.Shares, 300) + assert.EqualValues(t, *spec.Linux.Resources.Memory.Limit, 400) + assert.EqualValues(t, *spec.Linux.Resources.OOMScoreAdj, 500) + + t.Logf("Check capabilities") + assert.Contains(t, spec.Process.Capabilities.Bounding, "CAP_SYS_ADMIN") + assert.Contains(t, spec.Process.Capabilities.Effective, "CAP_SYS_ADMIN") + assert.Contains(t, spec.Process.Capabilities.Inheritable, "CAP_SYS_ADMIN") + assert.Contains(t, spec.Process.Capabilities.Permitted, "CAP_SYS_ADMIN") + assert.Contains(t, spec.Process.Capabilities.Ambient, "CAP_SYS_ADMIN") + assert.NotContains(t, spec.Process.Capabilities.Bounding, "CAP_CHOWN") + assert.NotContains(t, spec.Process.Capabilities.Effective, "CAP_CHOWN") + assert.NotContains(t, spec.Process.Capabilities.Inheritable, "CAP_CHOWN") + assert.NotContains(t, spec.Process.Capabilities.Permitted, "CAP_CHOWN") + assert.NotContains(t, spec.Process.Capabilities.Ambient, "CAP_CHOWN") + + t.Logf("Check supplemental groups") + assert.Contains(t, spec.Process.User.AdditionalGids, uint32(1111)) + assert.Contains(t, spec.Process.User.AdditionalGids, uint32(2222)) + + t.Logf("Check cgroup path") + assert.Equal(t, getCgroupsPath("/test/cgroup/parent", id), spec.Linux.CgroupsPath) + + t.Logf("Check namespaces") + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.NetworkNamespace, + Path: getNetworkNamespace(sandboxPid), + }) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.IPCNamespace, + Path: getIPCNamespace(sandboxPid), + }) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.UTSNamespace, + Path: getUTSNamespace(sandboxPid), + }) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.PIDNamespace, + Path: getPIDNamespace(sandboxPid), + }) + } + return config, sandboxConfig, specCheck +} + +func TestGeneralContainerSpec(t *testing.T) { + testID := "test-id" + testPid := uint32(1234) + config, sandboxConfig, specCheck := getStartContainerTestData() + c := newTestCRIContainerdService() + spec, err := c.generateContainerSpec(testID, testPid, config, sandboxConfig) + assert.NoError(t, err) + specCheck(t, testID, testPid, spec) +} + +func TestContainerSpecTty(t *testing.T) { + testID := "test-id" + testPid := uint32(1234) + config, sandboxConfig, specCheck := getStartContainerTestData() + c := newTestCRIContainerdService() + for _, tty := range []bool{true, false} { + config.Tty = tty + spec, err := c.generateContainerSpec(testID, testPid, config, sandboxConfig) + assert.NoError(t, err) + specCheck(t, testID, testPid, spec) + assert.Equal(t, tty, spec.Process.Terminal) + } +} + +func TestContainerSpecReadonlyRootfs(t *testing.T) { + testID := "test-id" + testPid := uint32(1234) + config, sandboxConfig, specCheck := getStartContainerTestData() + c := newTestCRIContainerdService() + for _, readonly := range []bool{true, false} { + config.Linux.SecurityContext.ReadonlyRootfs = readonly + spec, err := c.generateContainerSpec(testID, testPid, config, sandboxConfig) + assert.NoError(t, err) + specCheck(t, testID, testPid, spec) + assert.Equal(t, readonly, spec.Root.Readonly) + } +} + +func TestStartContainer(t *testing.T) { + testID := "test-id" + testSandboxID := "test-sandbox-id" + testSandboxPid := uint32(4321) + config, sandboxConfig, specCheck := getStartContainerTestData() + testMetadata := &metadata.ContainerMetadata{ + ID: testID, + Name: "test-name", + SandboxID: testSandboxID, + Config: config, + CreatedAt: time.Now().UnixNano(), + } + testSandboxMetadata := &metadata.SandboxMetadata{ + ID: testSandboxID, + Name: "test-sandbox-name", + Config: sandboxConfig, + } + testSandboxContainer := &container.Container{ + ID: testSandboxID, + Pid: testSandboxPid, + Status: container.Status_RUNNING, + } + for desc, test := range map[string]struct { + containerMetadata *metadata.ContainerMetadata + sandboxMetadata *metadata.SandboxMetadata + sandboxContainerdContainer *container.Container + prepareFIFOErr error + createContainerErr error + startContainerErr error + expectStateChange bool + expectCalls []string + expectErr bool + }{ + "should return error when container does not exist": { + containerMetadata: nil, + sandboxMetadata: testSandboxMetadata, + sandboxContainerdContainer: testSandboxContainer, + expectCalls: []string{}, + expectErr: true, + }, + "should return error when container is not in created state": { + containerMetadata: &metadata.ContainerMetadata{ + ID: testID, + Name: "test-name", + SandboxID: testSandboxID, + Config: config, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + }, + sandboxMetadata: testSandboxMetadata, + sandboxContainerdContainer: testSandboxContainer, + expectCalls: []string{}, + expectErr: true, + }, + "should return error when container is in removing state": { + containerMetadata: &metadata.ContainerMetadata{ + ID: testID, + Name: "test-name", + SandboxID: testSandboxID, + Config: config, + CreatedAt: time.Now().UnixNano(), + Removing: true, + }, + sandboxMetadata: testSandboxMetadata, + sandboxContainerdContainer: testSandboxContainer, + expectCalls: []string{}, + expectErr: true, + }, + "should return error when sandbox does not exist": { + containerMetadata: testMetadata, + sandboxMetadata: nil, + sandboxContainerdContainer: testSandboxContainer, + expectStateChange: true, + expectCalls: []string{}, + expectErr: true, + }, + "should return error when sandbox is not running": { + containerMetadata: testMetadata, + sandboxMetadata: testSandboxMetadata, + sandboxContainerdContainer: &container.Container{ + ID: testSandboxID, + Pid: testSandboxPid, + Status: container.Status_STOPPED, + }, + expectStateChange: true, + expectCalls: []string{"info"}, + expectErr: true, + }, + "should return error when fail to open streaming pipes": { + containerMetadata: testMetadata, + sandboxMetadata: testSandboxMetadata, + sandboxContainerdContainer: testSandboxContainer, + prepareFIFOErr: errors.New("open error"), + expectStateChange: true, + expectCalls: []string{"info"}, + expectErr: true, + }, + "should return error when fail to create container": { + containerMetadata: testMetadata, + sandboxMetadata: testSandboxMetadata, + sandboxContainerdContainer: testSandboxContainer, + createContainerErr: errors.New("create error"), + expectStateChange: true, + expectCalls: []string{"info", "create"}, + expectErr: true, + }, + "should return error when fail to start container": { + containerMetadata: testMetadata, + sandboxMetadata: testSandboxMetadata, + sandboxContainerdContainer: testSandboxContainer, + startContainerErr: errors.New("start error"), + expectStateChange: true, + // cleanup the containerd container. + expectCalls: []string{"info", "create", "start", "delete"}, + expectErr: true, + }, + "should be able to start container successfully": { + containerMetadata: testMetadata, + sandboxMetadata: testSandboxMetadata, + sandboxContainerdContainer: testSandboxContainer, + expectStateChange: true, + expectCalls: []string{"info", "create", "start"}, + expectErr: false, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + fake := c.containerService.(*servertesting.FakeExecutionClient) + fakeOS := c.os.(*ostesting.FakeOS) + if test.containerMetadata != nil { + assert.NoError(t, c.containerStore.Create(*test.containerMetadata)) + } + if test.sandboxMetadata != nil { + assert.NoError(t, c.sandboxStore.Create(*test.sandboxMetadata)) + } + if test.sandboxContainerdContainer != nil { + fake.SetFakeContainers([]container.Container{*test.sandboxContainerdContainer}) + } + // TODO(random-liu): Test behavior with different streaming config. + fakeOS.OpenFifoFn = func(context.Context, string, int, os.FileMode) (io.ReadWriteCloser, error) { + return nopReadWriteCloser{}, test.prepareFIFOErr + } + if test.createContainerErr != nil { + fake.InjectError("create", test.createContainerErr) + } + if test.startContainerErr != nil { + fake.InjectError("start", test.startContainerErr) + } + resp, err := c.StartContainer(context.Background(), &runtime.StartContainerRequest{ + ContainerId: testID, + }) + // Check containerd functions called. + assert.Equal(t, test.expectCalls, fake.GetCalledNames()) + // Check results returned. + if test.expectErr { + assert.Error(t, err) + assert.Nil(t, resp) + } else { + assert.NoError(t, err) + assert.NotNil(t, resp) + } + // Check container state. + meta, err := c.containerStore.Get(testID) + if !test.expectStateChange { + // Do not check the error, because container may not exist + // in the test case. + assert.Equal(t, meta, test.containerMetadata) + continue + } + assert.NoError(t, err) + require.NotNil(t, meta) + if test.expectErr { + t.Logf("container state should be in exited state when fail to start") + assert.Equal(t, runtime.ContainerState_CONTAINER_EXITED, meta.State()) + assert.Zero(t, meta.Pid) + assert.EqualValues(t, errorStartExitCode, meta.ExitCode) + assert.Equal(t, errorStartReason, meta.Reason) + assert.NotEmpty(t, meta.Message) + _, err := fake.Info(context.Background(), &execution.InfoRequest{ID: testID}) + assert.True(t, isContainerdContainerNotExistError(err), + "containerd container should be cleaned up after when fail to start") + continue + } + t.Logf("container state should be running when start successfully") + assert.Equal(t, runtime.ContainerState_CONTAINER_RUNNING, meta.State()) + info, err := fake.Info(context.Background(), &execution.InfoRequest{ID: testID}) + assert.NoError(t, err) + pid := info.Pid + assert.Equal(t, pid, meta.Pid) + assert.Equal(t, container.Status_RUNNING, info.Status) + // Check runtime spec + calls := fake.GetCalledDetails() + createOpts, ok := calls[1].Argument.(*execution.CreateRequest) + assert.True(t, ok, "2nd call should be create") + // TODO(random-liu): Test other create options. + spec := &runtimespec.Spec{} + assert.NoError(t, json.Unmarshal(createOpts.Spec.Value, spec)) + specCheck(t, testID, testSandboxPid, spec) + } +} diff --git a/pkg/server/container_status_test.go b/pkg/server/container_status_test.go new file mode 100644 index 000000000..acd536967 --- /dev/null +++ b/pkg/server/container_status_test.go @@ -0,0 +1,173 @@ +/* +Copyright 2017 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 server + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + + "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" + + "github.com/kubernetes-incubator/cri-containerd/pkg/metadata" +) + +func getContainerStatusTestData() (*metadata.ContainerMetadata, *runtime.ContainerStatus) { + testID := "test-id" + config := &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "test-name", + Attempt: 1, + }, + Image: &runtime.ImageSpec{Image: "test-image"}, + Mounts: []*runtime.Mount{{ + ContainerPath: "test-container-path", + HostPath: "test-host-path", + }}, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + } + + createdAt := time.Now().UnixNano() + startedAt := time.Now().UnixNano() + + metadata := &metadata.ContainerMetadata{ + ID: testID, + Name: "test-long-name", + SandboxID: "test-sandbox-id", + Config: config, + ImageRef: "test-image-ref", + Pid: 1234, + CreatedAt: createdAt, + StartedAt: startedAt, + } + + expected := &runtime.ContainerStatus{ + Id: testID, + Metadata: config.GetMetadata(), + State: runtime.ContainerState_CONTAINER_RUNNING, + CreatedAt: createdAt, + StartedAt: startedAt, + Image: config.GetImage(), + ImageRef: "test-image-ref", + Reason: completeExitReason, + Labels: config.GetLabels(), + Annotations: config.GetAnnotations(), + Mounts: config.GetMounts(), + } + + return metadata, expected +} + +func TestToCRIContainerStatus(t *testing.T) { + for desc, test := range map[string]struct { + finishedAt int64 + exitCode int32 + reason string + message string + expectedState runtime.ContainerState + expectedReason string + }{ + "container running": { + expectedState: runtime.ContainerState_CONTAINER_RUNNING, + }, + "container exited with reason": { + finishedAt: time.Now().UnixNano(), + exitCode: 1, + reason: "test-reason", + message: "test-message", + expectedState: runtime.ContainerState_CONTAINER_EXITED, + expectedReason: "test-reason", + }, + "container exited with exit code 0 without reason": { + finishedAt: time.Now().UnixNano(), + exitCode: 0, + message: "test-message", + expectedState: runtime.ContainerState_CONTAINER_EXITED, + expectedReason: completeExitReason, + }, + "container exited with non-zero exit code without reason": { + finishedAt: time.Now().UnixNano(), + exitCode: 1, + message: "test-message", + expectedState: runtime.ContainerState_CONTAINER_EXITED, + expectedReason: errorExitReason, + }, + } { + meta, expected := getContainerStatusTestData() + // Update metadata with test case. + meta.FinishedAt = test.finishedAt + meta.ExitCode = test.exitCode + meta.Reason = test.reason + meta.Message = test.message + // Set expectation based on test case. + expected.State = test.expectedState + expected.Reason = test.expectedReason + expected.FinishedAt = test.finishedAt + expected.ExitCode = test.exitCode + expected.Message = test.message + assert.Equal(t, expected, toCRIContainerStatus(meta), desc) + } +} + +func TestContainerStatus(t *testing.T) { + for desc, test := range map[string]struct { + exist bool + finishedAt int64 + reason string + expectedState runtime.ContainerState + expectErr bool + }{ + "container running": { + exist: true, + expectedState: runtime.ContainerState_CONTAINER_RUNNING, + }, + "container exited": { + exist: true, + finishedAt: time.Now().UnixNano(), + reason: "test-reason", + expectedState: runtime.ContainerState_CONTAINER_EXITED, + }, + "container not exist": { + exist: false, + expectErr: true, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + meta, expected := getContainerStatusTestData() + // Update metadata with test case. + meta.FinishedAt = test.finishedAt + meta.Reason = test.reason + if test.exist { + assert.NoError(t, c.containerStore.Create(*meta)) + } + resp, err := c.ContainerStatus(context.Background(), &runtime.ContainerStatusRequest{ContainerId: meta.ID}) + if test.expectErr { + assert.Error(t, err) + assert.Nil(t, resp) + continue + } + // Set expectation based on test case. + expected.FinishedAt = test.finishedAt + expected.Reason = test.reason + expected.State = test.expectedState + assert.Equal(t, expected, resp.GetStatus()) + } +} diff --git a/pkg/server/container_stop_test.go b/pkg/server/container_stop_test.go new file mode 100644 index 000000000..31db1bab8 --- /dev/null +++ b/pkg/server/container_stop_test.go @@ -0,0 +1,199 @@ +/* +Copyright 2017 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 server + +import ( + "errors" + "testing" + "time" + + "github.com/containerd/containerd/api/services/execution" + "github.com/containerd/containerd/api/types/container" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" + + "github.com/kubernetes-incubator/cri-containerd/pkg/metadata" + servertesting "github.com/kubernetes-incubator/cri-containerd/pkg/server/testing" +) + +func TestWaitContainerStop(t *testing.T) { + id := "test-id" + timeout := 2 * stopCheckPollInterval + for desc, test := range map[string]struct { + metadata *metadata.ContainerMetadata + expectErr bool + }{ + "should return error if timeout exceeds": { + metadata: &metadata.ContainerMetadata{ + ID: id, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + }, + expectErr: true, + }, + "should not return error if container is removed before timeout": { + metadata: nil, + expectErr: false, + }, + "should not return error if container is stopped before timeout": { + metadata: &metadata.ContainerMetadata{ + ID: id, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + }, + expectErr: false, + }, + } { + c := newTestCRIContainerdService() + if test.metadata != nil { + assert.NoError(t, c.containerStore.Create(*test.metadata)) + } + err := c.waitContainerStop(id, timeout) + assert.Equal(t, test.expectErr, err != nil, desc) + } +} + +func TestStopContainer(t *testing.T) { + testID := "test-id" + testPid := uint32(1234) + testMetadata := metadata.ContainerMetadata{ + ID: testID, + Pid: testPid, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + } + testContainer := container.Container{ + ID: testID, + Pid: testPid, + Status: container.Status_RUNNING, + } + for desc, test := range map[string]struct { + metadata *metadata.ContainerMetadata + containerdContainer *container.Container + killErr error + deleteErr error + discardEvents int + expectErr bool + expectCalls []string + }{ + "should return error when container does not exist": { + metadata: nil, + expectErr: true, + expectCalls: []string{}, + }, + "should not return error when container is not running": { + metadata: &metadata.ContainerMetadata{ + ID: testID, + CreatedAt: time.Now().UnixNano(), + }, + expectErr: false, + expectCalls: []string{}, + }, + "should not return error if containerd container does not exist": { + metadata: &testMetadata, + expectErr: false, + expectCalls: []string{"kill"}, + }, + "should not return error if containerd container is killed": { + metadata: &testMetadata, + containerdContainer: &testContainer, + expectErr: false, + // deleted by the event monitor. + expectCalls: []string{"kill", "delete"}, + }, + "should not return error if containerd container is deleted": { + metadata: &testMetadata, + containerdContainer: &testContainer, + // discard killed events to force a delete. This is only + // for testing. Actually real containerd should only generate + // one EXIT event. + discardEvents: 1, + expectErr: false, + // one more delete from the event monitor. + expectCalls: []string{"kill", "delete", "delete"}, + }, + "should return error if kill failed": { + metadata: &testMetadata, + containerdContainer: &testContainer, + killErr: errors.New("random error"), + expectErr: true, + expectCalls: []string{"kill"}, + }, + "should return error if delete failed": { + metadata: &testMetadata, + containerdContainer: &testContainer, + deleteErr: errors.New("random error"), + discardEvents: 1, + expectErr: true, + expectCalls: []string{"kill", "delete"}, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + fake := servertesting.NewFakeExecutionClient().WithEvents() + defer fake.Stop() + c.containerService = fake + + // Inject metadata. + if test.metadata != nil { + assert.NoError(t, c.containerStore.Create(*test.metadata)) + } + // Inject containerd container. + if test.containerdContainer != nil { + fake.SetFakeContainers([]container.Container{*test.containerdContainer}) + } + if test.killErr != nil { + fake.InjectError("kill", test.killErr) + } + if test.deleteErr != nil { + fake.InjectError("delete", test.deleteErr) + } + eventClient, err := fake.Events(context.Background(), &execution.EventsRequest{}) + assert.NoError(t, err) + // Start a simple test event monitor. + go func(e execution.ContainerService_EventsClient, discard int) { + for { + e, err := e.Recv() // nolint: vetshadow + if err != nil { + return + } + if discard > 0 { + discard-- + continue + } + c.handleEvent(e) + } + }(eventClient, test.discardEvents) + fake.ClearCalls() + // 1 second timeout should be enough for the unit test. + // TODO(random-liu): Use fake clock for this test. + resp, err := c.StopContainer(context.Background(), &runtime.StopContainerRequest{ + ContainerId: testID, + Timeout: 1, + }) + if test.expectErr { + assert.Error(t, err) + assert.Nil(t, resp) + } else { + assert.NoError(t, err) + assert.NotNil(t, resp) + } + assert.Equal(t, test.expectCalls, fake.GetCalledNames()) + } +} diff --git a/pkg/server/events.go b/pkg/server/events.go index 38446126c..8d4d82998 100644 --- a/pkg/server/events.go +++ b/pkg/server/events.go @@ -28,6 +28,10 @@ import ( // startEventMonitor starts an event monitor which monitors and handles all // container events. +// TODO(random-liu): [P1] Figure out: +// 1) Is it possible to drop event during containerd is running? +// 2) How to deal with containerd down? We should restart event monitor, and +// we should recover all container state. func (c *criContainerdService) startEventMonitor() error { events, err := c.containerService.Events(context.Background(), &execution.EventsRequest{}) if err != nil { @@ -35,25 +39,33 @@ func (c *criContainerdService) startEventMonitor() error { } go func() { for { - c.handleEvent(events) + c.handleEventStream(events) } }() return nil } -// handleEvent receives an event from contaienrd and handles the event. -func (c *criContainerdService) handleEvent(events execution.ContainerService_EventsClient) { +// handleEventStream receives an event from containerd and handles the event. +func (c *criContainerdService) handleEventStream(events execution.ContainerService_EventsClient) { + // TODO(random-liu): [P1] Should backoff on this error, or else this will + // cause a busy loop. e, err := events.Recv() if err != nil { glog.Errorf("Failed to receive event: %v", err) return } glog.V(2).Infof("Received container event: %+v", e) + c.handleEvent(e) + return +} + +// handleEvent handles a containerd event. +func (c *criContainerdService) handleEvent(e *container.Event) { switch e.Type { // If containerd-shim exits unexpectedly, there will be no corresponding event. // However, containerd could not retrieve container state in that case, so it's // fine to leave out that case for now. - // TODO(random-liu): [P2] Handle container-shim exit. + // TODO(random-liu): [P2] Handle containerd-shim exit. case container.Event_EXIT: meta, err := c.containerStore.Get(e.ID) if err != nil { @@ -61,7 +73,7 @@ func (c *criContainerdService) handleEvent(events execution.ContainerService_Eve return } if e.Pid != meta.Pid { - // Not init process dies, ignore the event. + // Non-init process died, ignore the event. return } // Delete the container from containerd. @@ -90,5 +102,4 @@ func (c *criContainerdService) handleEvent(events execution.ContainerService_Eve case container.Event_OOM: // TODO(random-liu): [P1] Handle OOM event. } - return } diff --git a/pkg/server/events_test.go b/pkg/server/events_test.go new file mode 100644 index 000000000..79d75d9ff --- /dev/null +++ b/pkg/server/events_test.go @@ -0,0 +1,154 @@ +/* +Copyright 2017 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 server + +import ( + "fmt" + "testing" + "time" + + "github.com/containerd/containerd/api/services/execution" + "github.com/containerd/containerd/api/types/container" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" + + "github.com/kubernetes-incubator/cri-containerd/pkg/metadata" + servertesting "github.com/kubernetes-incubator/cri-containerd/pkg/server/testing" +) + +func TestHandleEvent(t *testing.T) { + testID := "test-id" + testPid := uint32(1234) + testCreatedAt := time.Now().UnixNano() + testStartedAt := time.Now().UnixNano() + // Container metadata in running state. + testMetadata := metadata.ContainerMetadata{ + ID: testID, + Name: "test-name", + SandboxID: "test-sandbox-id", + Pid: testPid, + CreatedAt: testCreatedAt, + StartedAt: testStartedAt, + } + testExitedAt := time.Now() + testExitEvent := container.Event{ + ID: testID, + Type: container.Event_EXIT, + Pid: testPid, + ExitStatus: 1, + ExitedAt: testExitedAt, + } + testFinishedMetadata := metadata.ContainerMetadata{ + ID: testID, + Name: "test-name", + SandboxID: "test-sandbox-id", + Pid: 0, + CreatedAt: testCreatedAt, + StartedAt: testStartedAt, + FinishedAt: testExitedAt.UnixNano(), + ExitCode: 1, + } + assert.Equal(t, runtime.ContainerState_CONTAINER_RUNNING, testMetadata.State()) + testContainerdContainer := container.Container{ + ID: testID, + Pid: testPid, + Status: container.Status_RUNNING, + } + + for desc, test := range map[string]struct { + event *container.Event + metadata *metadata.ContainerMetadata + containerdContainer *container.Container + containerdErr error + expected *metadata.ContainerMetadata + }{ + "should not update state when no corresponding metadata for event": { + event: &testExitEvent, + expected: nil, + }, + "should not update state when exited process is not init process": { + event: &container.Event{ + ID: testID, + Type: container.Event_EXIT, + Pid: 9999, + ExitStatus: 1, + ExitedAt: testExitedAt, + }, + metadata: &testMetadata, + containerdContainer: &testContainerdContainer, + expected: &testMetadata, + }, + "should not update state when fail to delete containerd container": { + event: &testExitEvent, + metadata: &testMetadata, + containerdContainer: &testContainerdContainer, + containerdErr: fmt.Errorf("random error"), + expected: &testMetadata, + }, + "should not update state for non-exited events": { + event: &container.Event{ + ID: testID, + Type: container.Event_OOM, + Pid: testPid, + ExitStatus: 1, + ExitedAt: testExitedAt, + }, + metadata: &testMetadata, + containerdContainer: &testContainerdContainer, + expected: &testMetadata, + }, + "should update state when containerd container is already deleted": { + event: &testExitEvent, + metadata: &testMetadata, + expected: &testFinishedMetadata, + }, + "should update state when delete containerd container successfully": { + event: &testExitEvent, + metadata: &testMetadata, + containerdContainer: &testContainerdContainer, + expected: &testFinishedMetadata, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + fake := c.containerService.(*servertesting.FakeExecutionClient) + e, err := fake.Events(context.Background(), &execution.EventsRequest{}) + assert.NoError(t, err) + fakeEvents := e.(*servertesting.EventClient) + // Inject event. + if test.event != nil { + fakeEvents.Events <- test.event + } + // Inject metadata. + if test.metadata != nil { + // Make sure that original data will not be changed. + assert.NoError(t, c.containerStore.Create(*test.metadata)) + } + // Inject containerd container. + if test.containerdContainer != nil { + fake.SetFakeContainers([]container.Container{*test.containerdContainer}) + } + // Inject containerd delete error. + if test.containerdErr != nil { + fake.InjectError("delete", test.containerdErr) + } + c.handleEventStream(e) + got, _ := c.containerStore.Get(testID) + assert.Equal(t, test.expected, got) + } +} diff --git a/pkg/server/helpers_test.go b/pkg/server/helpers_test.go index b1ccb40cc..0b2370f1d 100644 --- a/pkg/server/helpers_test.go +++ b/pkg/server/helpers_test.go @@ -17,13 +17,106 @@ limitations under the License. package server import ( + "fmt" + "io" + "os" + "syscall" "testing" "github.com/stretchr/testify/assert" + "golang.org/x/net/context" "github.com/kubernetes-incubator/cri-containerd/pkg/metadata" + ostesting "github.com/kubernetes-incubator/cri-containerd/pkg/os/testing" ) +func TestPrepareStreamingPipes(t *testing.T) { + for desc, test := range map[string]struct { + stdin string + stdout string + stderr string + }{ + "empty stdin": { + stdout: "/test/stdout", + stderr: "/test/stderr", + }, + "empty stdout/stderr": { + stdin: "/test/stdin", + }, + "non-empty stdio": { + stdin: "/test/stdin", + stdout: "/test/stdout", + stderr: "/test/stderr", + }, + "empty stdio": {}, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + fakeOS := c.os.(*ostesting.FakeOS) + fakeOS.OpenFifoFn = func(ctx context.Context, fn string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { + expectFlag := syscall.O_RDONLY | syscall.O_CREAT | syscall.O_NONBLOCK + if fn == test.stdin { + expectFlag = syscall.O_WRONLY | syscall.O_CREAT | syscall.O_NONBLOCK + } + assert.Equal(t, expectFlag, flag) + assert.Equal(t, os.FileMode(0700), perm) + return nopReadWriteCloser{}, nil + } + i, o, e, err := c.prepareStreamingPipes(context.Background(), test.stdin, test.stdout, test.stderr) + assert.NoError(t, err) + assert.Equal(t, test.stdin != "", i != nil) + assert.Equal(t, test.stdout != "", o != nil) + assert.Equal(t, test.stderr != "", e != nil) + } +} + +type closeTestReadWriteCloser struct { + CloseFn func() error + nopReadWriteCloser +} + +func (c closeTestReadWriteCloser) Close() error { + return c.CloseFn() +} + +func TestPrepareStreamingPipesError(t *testing.T) { + stdin, stdout, stderr := "/test/stdin", "/test/stdout", "/test/stderr" + for desc, inject := range map[string]map[string]error{ + "should cleanup on stdin error": {stdin: fmt.Errorf("stdin error")}, + "should cleanup on stdout error": {stdout: fmt.Errorf("stdout error")}, + "should cleanup on stderr error": {stderr: fmt.Errorf("stderr error")}, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + fakeOS := c.os.(*ostesting.FakeOS) + openFlags := map[string]bool{ + stdin: false, + stdout: false, + stderr: false, + } + fakeOS.OpenFifoFn = func(ctx context.Context, fn string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { + if inject[fn] != nil { + return nil, inject[fn] + } + openFlags[fn] = !openFlags[fn] + testCloser := closeTestReadWriteCloser{} + testCloser.CloseFn = func() error { + openFlags[fn] = !openFlags[fn] + return nil + } + return testCloser, nil + } + i, o, e, err := c.prepareStreamingPipes(context.Background(), stdin, stdout, stderr) + assert.Error(t, err) + assert.Nil(t, i) + assert.Nil(t, o) + assert.Nil(t, e) + assert.False(t, openFlags[stdin]) + assert.False(t, openFlags[stdout]) + assert.False(t, openFlags[stderr]) + } +} + func TestGetSandbox(t *testing.T) { c := newTestCRIContainerdService() testID := "abcdefg" diff --git a/pkg/server/service_test.go b/pkg/server/service_test.go index 9ac434ed3..c753815dd 100644 --- a/pkg/server/service_test.go +++ b/pkg/server/service_test.go @@ -39,8 +39,9 @@ import ( type nopReadWriteCloser struct{} -func (nopReadWriteCloser) Read(p []byte) (n int, err error) { return len(p), nil } -func (nopReadWriteCloser) Write(p []byte) (n int, err error) { return len(p), nil } +// Return error directly to avoid read/write. +func (nopReadWriteCloser) Read(p []byte) (n int, err error) { return 0, io.EOF } +func (nopReadWriteCloser) Write(p []byte) (n int, err error) { return 0, io.ErrShortWrite } func (nopReadWriteCloser) Close() error { return nil } const testRootDir = "/test/rootfs" @@ -48,12 +49,14 @@ const testRootDir = "/test/rootfs" // newTestCRIContainerdService creates a fake criContainerdService for test. func newTestCRIContainerdService() *criContainerdService { return &criContainerdService{ - os: ostesting.NewFakeOS(), - rootDir: testRootDir, - containerService: servertesting.NewFakeExecutionClient(), - sandboxStore: metadata.NewSandboxStore(store.NewMetadataStore()), - sandboxNameIndex: registrar.NewRegistrar(), - sandboxIDIndex: truncindex.NewTruncIndex(nil), + os: ostesting.NewFakeOS(), + rootDir: testRootDir, + containerService: servertesting.NewFakeExecutionClient(), + sandboxStore: metadata.NewSandboxStore(store.NewMetadataStore()), + sandboxNameIndex: registrar.NewRegistrar(), + sandboxIDIndex: truncindex.NewTruncIndex(nil), + containerStore: metadata.NewContainerStore(store.NewMetadataStore()), + containerNameIndex: registrar.NewRegistrar(), } } diff --git a/pkg/server/testing/fake_execution_client.go b/pkg/server/testing/fake_execution_client.go index 73ff3da89..a241f97e6 100644 --- a/pkg/server/testing/fake_execution_client.go +++ b/pkg/server/testing/fake_execution_client.go @@ -51,7 +51,10 @@ type EventClient struct { // Recv is a test implementation of Recv func (cli *EventClient) Recv() (*container.Event, error) { - event := <-cli.Events + event, ok := <-cli.Events + if !ok { + return nil, fmt.Errorf("event channel closed") + } return event, nil } @@ -76,6 +79,18 @@ func NewFakeExecutionClient() *FakeExecutionClient { } } +// Stop the fake execution service. Needed when event is enabled. +func (f *FakeExecutionClient) Stop() { + if f.eventsQueue != nil { + close(f.eventsQueue) + } + f.Lock() + defer f.Unlock() + for _, client := range f.eventClients { + close(client.Events) + } +} + // WithEvents setup events publisher for FakeExecutionClient func (f *FakeExecutionClient) WithEvents() *FakeExecutionClient { f.eventsQueue = make(chan *container.Event, 1024) @@ -154,6 +169,13 @@ func (f *FakeExecutionClient) GetCalledNames() []string { return names } +// ClearCalls clear all call detail. +func (f *FakeExecutionClient) ClearCalls() { + f.Lock() + defer f.Unlock() + f.called = []CalledDetail{} +} + // GetCalledDetails get detail of each call. func (f *FakeExecutionClient) GetCalledDetails() []CalledDetail { f.Lock()