From e4e9f30c5d0ed66fb1f14275ac5cc4081dbf8777 Mon Sep 17 00:00:00 2001 From: Random-Liu Date: Fri, 12 May 2017 13:14:25 -0700 Subject: [PATCH] Add unit test. Signed-off-by: Random-Liu --- pkg/registrar/registrar_test.go | 54 ++++++++ pkg/server/helpers_test.go | 59 +++++++++ pkg/server/sandbox_list_test.go | 210 ++++++++++++++++++++++++++++++ pkg/server/sandbox_remove_test.go | 122 +++++++++++++++++ pkg/server/sandbox_run_test.go | 191 +++++++++++++++++++++++++++ pkg/server/sandbox_status_test.go | 192 +++++++++++++++++++++++++++ pkg/server/sandbox_stop_test.go | 106 +++++++++++++++ pkg/server/service_test.go | 162 +++++++++++++++++++++++ 8 files changed, 1096 insertions(+) create mode 100644 pkg/registrar/registrar_test.go create mode 100644 pkg/server/helpers_test.go create mode 100644 pkg/server/sandbox_list_test.go create mode 100644 pkg/server/sandbox_remove_test.go create mode 100644 pkg/server/sandbox_run_test.go create mode 100644 pkg/server/sandbox_status_test.go create mode 100644 pkg/server/sandbox_stop_test.go create mode 100644 pkg/server/service_test.go diff --git a/pkg/registrar/registrar_test.go b/pkg/registrar/registrar_test.go new file mode 100644 index 000000000..35c8aa1a9 --- /dev/null +++ b/pkg/registrar/registrar_test.go @@ -0,0 +1,54 @@ +/* +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 registrar + +import ( + "testing" + + assertlib "github.com/stretchr/testify/assert" +) + +func TestRegistrar(t *testing.T) { + r := NewRegistrar() + assert := assertlib.New(t) + + t.Logf("should be able to reserve a name<->key mapping") + assert.NoError(r.Reserve("test-name-1", "test-id-1")) + + t.Logf("should be able to reserve a new name<->key mapping") + assert.NoError(r.Reserve("test-name-2", "test-id-2")) + + t.Logf("should be able to reserve the same name<->key mapping") + assert.NoError(r.Reserve("test-name-1", "test-id-1")) + + t.Logf("should not be able to reserve conflict name<->key mapping") + assert.Error(r.Reserve("test-name-1", "test-id-conflict")) + assert.Error(r.Reserve("test-name-conflict", "test-id-2")) + + t.Logf("should be able to release name<->key mapping by key") + r.ReleaseByKey("test-id-1") + + t.Logf("should be able to release name<->key mapping by name") + r.ReleaseByName("test-name-2") + + t.Logf("should be able to reserve new name<->key mapping after release") + assert.NoError(r.Reserve("test-name-1", "test-id-new")) + assert.NoError(r.Reserve("test-name-new", "test-id-2")) + + t.Logf("should be able to reserve same name/key name<->key") + assert.NoError(r.Reserve("same-name-id", "same-name-id")) +} diff --git a/pkg/server/helpers_test.go b/pkg/server/helpers_test.go new file mode 100644 index 000000000..b14a9a8fb --- /dev/null +++ b/pkg/server/helpers_test.go @@ -0,0 +1,59 @@ +/* +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" + + "github.com/stretchr/testify/assert" + + "github.com/kubernetes-incubator/cri-containerd/pkg/metadata" +) + +func TestGetSandbox(t *testing.T) { + c := newTestCRIContainerdService() + testID := "abcdefg" + testSandbox := metadata.SandboxMetadata{ + ID: testID, + Name: "test-name", + } + assert.NoError(t, c.sandboxStore.Create(testSandbox)) + assert.NoError(t, c.sandboxIDIndex.Add(testID)) + + for desc, test := range map[string]struct { + id string + expected *metadata.SandboxMetadata + }{ + "full id": { + id: testID, + expected: &testSandbox, + }, + "partial id": { + id: testID[:3], + expected: &testSandbox, + }, + "non-exist id": { + id: "gfedcba", + expected: nil, + }, + } { + t.Logf("TestCase %q", desc) + sb, err := c.getSandbox(test.id) + assert.NoError(t, err) + assert.Equal(t, test.expected, sb) + } +} diff --git a/pkg/server/sandbox_list_test.go b/pkg/server/sandbox_list_test.go new file mode 100644 index 000000000..a07de2bfb --- /dev/null +++ b/pkg/server/sandbox_list_test.go @@ -0,0 +1,210 @@ +/* +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" + + "github.com/containerd/containerd/api/types/container" + + "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 TestToCRISandbox(t *testing.T) { + config := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-name", + Uid: "test-uid", + Namespace: "test-ns", + Attempt: 1, + }, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + } + createdAt := time.Now().UnixNano() + meta := &metadata.SandboxMetadata{ + ID: "test-id", + Name: "test-name", + Config: config, + CreatedAt: createdAt, + NetNS: "test-netns", + } + state := runtime.PodSandboxState_SANDBOX_READY + expect := &runtime.PodSandbox{ + Id: "test-id", + Metadata: config.GetMetadata(), + State: state, + CreatedAt: createdAt, + Labels: config.GetLabels(), + Annotations: config.GetAnnotations(), + } + s := toCRISandbox(meta, state) + assert.Equal(t, expect, s) +} + +func TestFilterSandboxes(t *testing.T) { + c := newTestCRIContainerdService() + + testSandboxes := []*runtime.PodSandbox{ + { + Id: "1", + Metadata: &runtime.PodSandboxMetadata{Name: "name-1", Uid: "uid-1", Namespace: "ns-1", Attempt: 1}, + State: runtime.PodSandboxState_SANDBOX_READY, + }, + { + Id: "2", + Metadata: &runtime.PodSandboxMetadata{Name: "name-2", Uid: "uid-2", Namespace: "ns-2", Attempt: 2}, + State: runtime.PodSandboxState_SANDBOX_NOTREADY, + Labels: map[string]string{"a": "b"}, + }, + { + Id: "3", + Metadata: &runtime.PodSandboxMetadata{Name: "name-2", Uid: "uid-2", Namespace: "ns-2", Attempt: 2}, + State: runtime.PodSandboxState_SANDBOX_READY, + Labels: map[string]string{"c": "d"}, + }, + } + for desc, test := range map[string]struct { + filter *runtime.PodSandboxFilter + expect []*runtime.PodSandbox + }{ + "no filter": { + expect: testSandboxes, + }, + "id filter": { + filter: &runtime.PodSandboxFilter{Id: "2"}, + expect: []*runtime.PodSandbox{testSandboxes[1]}, + }, + "state filter": { + filter: &runtime.PodSandboxFilter{ + State: &runtime.PodSandboxStateValue{ + State: runtime.PodSandboxState_SANDBOX_READY, + }, + }, + expect: []*runtime.PodSandbox{testSandboxes[0], testSandboxes[2]}, + }, + "label filter": { + filter: &runtime.PodSandboxFilter{ + LabelSelector: map[string]string{"a": "b"}, + }, + expect: []*runtime.PodSandbox{testSandboxes[1]}, + }, + "mixed filter not matched": { + filter: &runtime.PodSandboxFilter{ + Id: "1", + LabelSelector: map[string]string{"a": "b"}, + }, + expect: []*runtime.PodSandbox{}, + }, + "mixed filter matched": { + filter: &runtime.PodSandboxFilter{ + State: &runtime.PodSandboxStateValue{ + State: runtime.PodSandboxState_SANDBOX_READY, + }, + LabelSelector: map[string]string{"c": "d"}, + }, + expect: []*runtime.PodSandbox{testSandboxes[2]}, + }, + } { + filtered := c.filterCRISandboxes(testSandboxes, test.filter) + assert.Equal(t, test.expect, filtered, desc) + } +} + +func TestListPodSandbox(t *testing.T) { + c := newTestCRIContainerdService() + + fake := c.containerService.(*servertesting.FakeExecutionClient) + + sandboxesInStore := []metadata.SandboxMetadata{ + { + ID: "1", + Name: "name-1", + Config: &runtime.PodSandboxConfig{Metadata: &runtime.PodSandboxMetadata{Name: "name-1"}}, + }, + { + ID: "2", + Name: "name-2", + Config: &runtime.PodSandboxConfig{Metadata: &runtime.PodSandboxMetadata{Name: "name-2"}}, + }, + { + ID: "3", + Name: "name-3", + Config: &runtime.PodSandboxConfig{Metadata: &runtime.PodSandboxMetadata{Name: "name-3"}}, + }, + } + sandboxesInContainerd := []container.Container{ + // Running container with corresponding metadata + { + ID: "1", + Pid: 1, + Status: container.Status_RUNNING, + }, + // Stopped container with corresponding metadata + { + ID: "2", + Pid: 2, + Status: container.Status_STOPPED, + }, + // Container without corresponding metadata + { + ID: "4", + Pid: 4, + Status: container.Status_STOPPED, + }, + } + expect := []*runtime.PodSandbox{ + { + Id: "1", + Metadata: &runtime.PodSandboxMetadata{Name: "name-1"}, + State: runtime.PodSandboxState_SANDBOX_READY, + }, + { + Id: "2", + Metadata: &runtime.PodSandboxMetadata{Name: "name-2"}, + State: runtime.PodSandboxState_SANDBOX_NOTREADY, + }, + { + Id: "3", + Metadata: &runtime.PodSandboxMetadata{Name: "name-3"}, + State: runtime.PodSandboxState_SANDBOX_NOTREADY, + }, + } + + // Inject test metadata + for _, s := range sandboxesInStore { + c.sandboxStore.Create(s) + } + + // Inject fake containerd containers + fake.SetFakeContainers(sandboxesInContainerd) + + resp, err := c.ListPodSandbox(context.Background(), &runtime.ListPodSandboxRequest{}) + assert.NoError(t, err) + sandboxes := resp.GetItems() + assert.Len(t, sandboxes, len(expect)) + for _, s := range expect { + assert.Contains(t, sandboxes, s) + } +} diff --git a/pkg/server/sandbox_remove_test.go b/pkg/server/sandbox_remove_test.go new file mode 100644 index 000000000..7820919af --- /dev/null +++ b/pkg/server/sandbox_remove_test.go @@ -0,0 +1,122 @@ +/* +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" + + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + + "github.com/containerd/containerd/api/types/container" + + "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" + + "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" +) + +func TestRemovePodSandbox(t *testing.T) { + testID := "test-id" + testName := "test-name" + testMetadata := metadata.SandboxMetadata{ + ID: testID, + Name: testName, + } + for desc, test := range map[string]struct { + sandboxContainers []container.Container + injectMetadata bool + injectContainerdErr error + injectFSErr error + expectErr bool + expectRemoved string + expectCalls []string + }{ + "should not return error if sandbox does not exist": { + injectMetadata: false, + expectErr: false, + expectCalls: []string{}, + }, + "should return error when sandbox container is not deleted": { + injectMetadata: true, + sandboxContainers: []container.Container{{ID: testID}}, + expectErr: true, + expectCalls: []string{"info"}, + }, + "should return error when arbitrary containerd error is injected": { + injectMetadata: true, + injectContainerdErr: fmt.Errorf("arbitrary error"), + expectErr: true, + expectCalls: []string{"info"}, + }, + "should return error when error fs error is injected": { + injectMetadata: true, + injectFSErr: fmt.Errorf("fs error"), + expectRemoved: getSandboxRootDir(testRootDir, testID), + expectErr: true, + expectCalls: []string{"info"}, + }, + "should be able to successfully delete": { + injectMetadata: true, + expectRemoved: getSandboxRootDir(testRootDir, testID), + expectCalls: []string{"info"}, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + fake := c.containerService.(*servertesting.FakeExecutionClient) + fakeOS := c.os.(*ostesting.FakeOS) + fake.SetFakeContainers(test.sandboxContainers) + if test.injectMetadata { + c.sandboxNameIndex.Reserve(testName, testID) + c.sandboxIDIndex.Add(testID) + c.sandboxStore.Create(testMetadata) + } + if test.injectContainerdErr != nil { + fake.InjectError("info", test.injectContainerdErr) + } + fakeOS.RemoveAllFn = func(path string) error { + assert.Equal(t, test.expectRemoved, path) + return test.injectFSErr + } + res, err := c.RemovePodSandbox(context.Background(), &runtime.RemovePodSandboxRequest{ + PodSandboxId: testID, + }) + assert.Equal(t, test.expectCalls, fake.GetCalledNames()) + if test.expectErr { + assert.Error(t, err) + assert.Nil(t, res) + continue + } + assert.NoError(t, err) + assert.NotNil(t, res) + assert.NoError(t, c.sandboxNameIndex.Reserve(testName, testID), + "sandbox name should be released") + _, err = c.sandboxIDIndex.Get(testID) + assert.Error(t, err, "sandbox id should be removed") + meta, err := c.sandboxStore.Get(testID) + assert.NoError(t, err) + assert.Nil(t, meta, "sandbox metadata should be removed") + res, err = c.RemovePodSandbox(context.Background(), &runtime.RemovePodSandboxRequest{ + PodSandboxId: testID, + }) + assert.NoError(t, err) + assert.NotNil(t, res, "remove should be idempotent") + } +} diff --git a/pkg/server/sandbox_run_test.go b/pkg/server/sandbox_run_test.go new file mode 100644 index 000000000..3eaffd31c --- /dev/null +++ b/pkg/server/sandbox_run_test.go @@ -0,0 +1,191 @@ +/* +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" + "io" + "os" + "syscall" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + + "github.com/containerd/containerd/api/services/execution" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + + ostesting "github.com/kubernetes-incubator/cri-containerd/pkg/os/testing" + servertesting "github.com/kubernetes-incubator/cri-containerd/pkg/server/testing" + + "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" +) + +func getRunPodSandboxTestData() (*runtime.PodSandboxConfig, func(*testing.T, string, *runtimespec.Spec)) { + config := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-name", + Uid: "test-uid", + Namespace: "test-ns", + Attempt: 1, + }, + Hostname: "test-hostname", + LogDirectory: "test-log-directory", + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + Linux: &runtime.LinuxPodSandboxConfig{ + CgroupParent: "/test/cgroup/parent", + }, + } + specCheck := func(t *testing.T, id string, spec *runtimespec.Spec) { + assert.Equal(t, "test-hostname", spec.Hostname) + assert.Equal(t, getCgroupsPath("/test/cgroup/parent", id), spec.Linux.CgroupsPath) + assert.Equal(t, relativeRootfsPath, spec.Root.Path) + assert.Equal(t, true, spec.Root.Readonly) + } + return config, specCheck +} + +func TestGenerateSandboxContainerSpec(t *testing.T) { + testID := "test-id" + for desc, test := range map[string]struct { + configChange func(*runtime.PodSandboxConfig) + specCheck func(*testing.T, *runtimespec.Spec) + }{ + "spec should reflect original config": { + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + // runtime spec should have expected namespaces enabled by default. + require.NotNil(t, spec.Linux) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.NetworkNamespace, + }) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.PIDNamespace, + }) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.IPCNamespace, + }) + }, + }, + "host namespace": { + configChange: func(c *runtime.PodSandboxConfig) { + c.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{ + NamespaceOptions: &runtime.NamespaceOption{ + HostNetwork: true, + HostPid: true, + HostIpc: true, + }, + } + }, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + // runtime spec should disable expected namespaces in host mode. + require.NotNil(t, spec.Linux) + assert.NotContains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.NetworkNamespace, + }) + assert.NotContains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.PIDNamespace, + }) + assert.NotContains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.IPCNamespace, + }) + }, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + config, specCheck := getRunPodSandboxTestData() + if test.configChange != nil { + test.configChange(config) + } + spec := c.generateSandboxContainerSpec(testID, config) + specCheck(t, testID, spec) + if test.specCheck != nil { + test.specCheck(t, spec) + } + } +} + +func TestRunPodSandbox(t *testing.T) { + config, specCheck := getRunPodSandboxTestData() + c := newTestCRIContainerdService() + fake := c.containerService.(*servertesting.FakeExecutionClient) + fakeOS := c.os.(*ostesting.FakeOS) + var dirs []string + var pipes []string + fakeOS.MkdirAllFn = func(path string, perm os.FileMode) error { + dirs = append(dirs, path) + assert.Equal(t, os.FileMode(0755), perm) + return nil + } + fakeOS.OpenFifoFn = func(ctx context.Context, fn string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { + pipes = append(pipes, fn) + assert.Equal(t, syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, flag) + assert.Equal(t, os.FileMode(0700), perm) + return nopReadWriteCloser{}, nil + } + expectCalls := []string{"create", "start"} + + res, err := c.RunPodSandbox(context.Background(), &runtime.RunPodSandboxRequest{Config: config}) + assert.NoError(t, err) + require.NotNil(t, res) + id := res.GetPodSandboxId() + + assert.Len(t, dirs, 1) + assert.Equal(t, getSandboxRootDir(c.rootDir, id), dirs[0], "sandbox root directory should be created") + + assert.Len(t, pipes, 2) + _, stdout, stderr := getStreamingPipes(getSandboxRootDir(c.rootDir, id)) + assert.Contains(t, pipes, stdout, "sandbox stdout pipe should be created") + assert.Contains(t, pipes, stderr, "sandbox stderr pipe should be created") + + assert.Equal(t, expectCalls, fake.GetCalledNames(), "expect containerd functions should be called") + calls := fake.GetCalledDetails() + createOpts := calls[0].Argument.(*execution.CreateRequest) + assert.Equal(t, id, createOpts.ID, "create id should be correct") + // TODO(random-liu): Test rootfs mount when image management part is integrated. + assert.Equal(t, stdout, createOpts.Stdout, "stdout pipe should be passed to containerd") + assert.Equal(t, stderr, createOpts.Stderr, "stderr pipe should be passed to containerd") + spec := &runtimespec.Spec{} + assert.NoError(t, json.Unmarshal(createOpts.Spec.Value, spec)) + t.Logf("oci spec check") + specCheck(t, id, spec) + + startID := calls[1].Argument.(*execution.StartRequest).ID + assert.Equal(t, id, startID, "start id should be correct") + + meta, err := c.sandboxStore.Get(id) + assert.NoError(t, err) + assert.Equal(t, id, meta.ID, "metadata id should be correct") + err = c.sandboxNameIndex.Reserve(meta.Name, "random-id") + assert.Error(t, err, "metadata name should be reserved") + assert.Equal(t, config, meta.Config, "metadata config should be correct") + // TODO(random-liu): [P2] Add clock interface and use fake clock. + assert.NotZero(t, meta.CreatedAt, "metadata CreatedAt should be set") + info, err := fake.Info(context.Background(), &execution.InfoRequest{ID: id}) + assert.NoError(t, err) + pid := info.Pid + assert.Equal(t, meta.NetNS, getNetworkNamespace(pid), "metadata network namespace should be correct") + + gotID, err := c.sandboxIDIndex.Get(id) + assert.NoError(t, err) + assert.Equal(t, id, gotID, "sandbox id should be indexed") +} + +// TODO(random-liu): [P1] Add unit test for different error cases to make sure +// the function cleans up on error properly. diff --git a/pkg/server/sandbox_status_test.go b/pkg/server/sandbox_status_test.go new file mode 100644 index 000000000..2f253e572 --- /dev/null +++ b/pkg/server/sandbox_status_test.go @@ -0,0 +1,192 @@ +/* +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" + + "github.com/containerd/containerd/api/types/container" + + "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" +) + +// Variables used in the following test. + +const sandboxStatusTestID = "test-id" + +func getSandboxStatusTestData() (*metadata.SandboxMetadata, *runtime.PodSandboxStatus) { + config := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-name", + Uid: "test-uid", + Namespace: "test-ns", + Attempt: 1, + }, + Linux: &runtime.LinuxPodSandboxConfig{ + SecurityContext: &runtime.LinuxSandboxSecurityContext{ + NamespaceOptions: &runtime.NamespaceOption{ + HostNetwork: true, + HostPid: false, + HostIpc: true, + }, + }, + }, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + } + + createdAt := time.Now().UnixNano() + + metadata := &metadata.SandboxMetadata{ + ID: sandboxStatusTestID, + Name: "test-name", + Config: config, + CreatedAt: createdAt, + } + + expectedStatus := &runtime.PodSandboxStatus{ + Id: sandboxStatusTestID, + Metadata: config.GetMetadata(), + CreatedAt: createdAt, + Network: &runtime.PodSandboxNetworkStatus{}, + Linux: &runtime.LinuxPodSandboxStatus{ + Namespaces: &runtime.Namespace{ + Options: &runtime.NamespaceOption{ + HostNetwork: true, + HostPid: false, + HostIpc: true, + }, + }, + }, + Labels: config.GetLabels(), + Annotations: config.GetAnnotations(), + } + + return metadata, expectedStatus +} + +func TestToCRISandboxStatus(t *testing.T) { + for desc, test := range map[string]struct { + state runtime.PodSandboxState + expectNetNS string + }{ + "ready sandbox should have network namespace": { + state: runtime.PodSandboxState_SANDBOX_READY, + expectNetNS: "test-netns", + }, + "not ready sandbox should not have network namespace": { + state: runtime.PodSandboxState_SANDBOX_NOTREADY, + expectNetNS: "", + }, + } { + metadata, expect := getSandboxStatusTestData() + metadata.NetNS = "test-netns" + status := toCRISandboxStatus(metadata, test.state) + expect.Linux.Namespaces.Network = test.expectNetNS + expect.State = test.state + assert.Equal(t, expect, status, desc) + } +} + +func TestPodSandboxStatus(t *testing.T) { + for desc, test := range map[string]struct { + sandboxContainers []container.Container + injectMetadata bool + injectErr error + expectState runtime.PodSandboxState + expectErr bool + expectCalls []string + }{ + "sandbox status without metadata": { + injectMetadata: false, + expectErr: true, + expectCalls: []string{}, + }, + "sandbox status with running sandbox container": { + sandboxContainers: []container.Container{{ + ID: sandboxStatusTestID, + Pid: 1, + Status: container.Status_RUNNING, + }}, + injectMetadata: true, + expectState: runtime.PodSandboxState_SANDBOX_READY, + expectCalls: []string{"info"}, + }, + "sandbox status with stopped sandbox container": { + sandboxContainers: []container.Container{{ + ID: sandboxStatusTestID, + Pid: 1, + Status: container.Status_STOPPED, + }}, + injectMetadata: true, + expectState: runtime.PodSandboxState_SANDBOX_NOTREADY, + expectCalls: []string{"info"}, + }, + "sandbox status with non-existing sandbox container": { + sandboxContainers: []container.Container{}, + injectMetadata: true, + expectState: runtime.PodSandboxState_SANDBOX_NOTREADY, + expectCalls: []string{"info"}, + }, + "sandbox status with arbitrary error": { + sandboxContainers: []container.Container{{ + ID: sandboxStatusTestID, + Pid: 1, + Status: container.Status_RUNNING, + }}, + injectMetadata: true, + injectErr: errors.New("arbitrary error"), + expectErr: true, + expectCalls: []string{"info"}, + }, + } { + t.Logf("TestCase %q", desc) + metadata, expect := getSandboxStatusTestData() + c := newTestCRIContainerdService() + fake := c.containerService.(*servertesting.FakeExecutionClient) + fake.SetFakeContainers(test.sandboxContainers) + if test.injectMetadata { + assert.NoError(t, c.sandboxIDIndex.Add(metadata.ID)) + assert.NoError(t, c.sandboxStore.Create(*metadata)) + } + if test.injectErr != nil { + fake.InjectError("info", test.injectErr) + } + res, err := c.PodSandboxStatus(context.Background(), &runtime.PodSandboxStatusRequest{ + PodSandboxId: sandboxStatusTestID, + }) + assert.Equal(t, test.expectCalls, fake.GetCalledNames()) + if test.expectErr { + assert.Error(t, err) + assert.Nil(t, res) + continue + } + assert.NoError(t, err) + require.NotNil(t, res) + expect.State = test.expectState + assert.Equal(t, expect, res.GetStatus()) + } +} diff --git a/pkg/server/sandbox_stop_test.go b/pkg/server/sandbox_stop_test.go new file mode 100644 index 000000000..7efabb823 --- /dev/null +++ b/pkg/server/sandbox_stop_test.go @@ -0,0 +1,106 @@ +/* +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" + + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/api/types/container" + + "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 TestStopPodSandbox(t *testing.T) { + testID := "test-id" + testSandbox := metadata.SandboxMetadata{ + ID: testID, + Name: "test-name", + } + testContainer := container.Container{ + ID: testID, + Pid: 1, + Status: container.Status_RUNNING, + } + + for desc, test := range map[string]struct { + sandboxContainers []container.Container + injectSandbox bool + injectErr error + expectErr bool + expectCalls []string + }{ + "stop non-existing sandbox": { + injectSandbox: false, + expectErr: true, + expectCalls: []string{}, + }, + "stop sandbox with sandbox container": { + sandboxContainers: []container.Container{testContainer}, + injectSandbox: true, + expectErr: false, + expectCalls: []string{"delete"}, + }, + "stop sandbox with sandbox container not exist error": { + sandboxContainers: []container.Container{}, + injectSandbox: true, + // Inject error to make sure fake execution client returns error. + injectErr: grpc.Errorf(codes.Unknown, containerd.ErrContainerNotExist.Error()), + expectErr: false, + expectCalls: []string{"delete"}, + }, + "stop sandbox with with arbitrary error": { + injectSandbox: true, + injectErr: grpc.Errorf(codes.Unknown, "arbitrary error"), + expectErr: true, + expectCalls: []string{"delete"}, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIContainerdService() + fake := c.containerService.(*servertesting.FakeExecutionClient) + fake.SetFakeContainers(test.sandboxContainers) + + if test.injectSandbox { + assert.NoError(t, c.sandboxStore.Create(testSandbox)) + c.sandboxIDIndex.Add(testID) + } + if test.injectErr != nil { + fake.InjectError("delete", test.injectErr) + } + + res, err := c.StopPodSandbox(context.Background(), &runtime.StopPodSandboxRequest{ + PodSandboxId: testID, + }) + if test.expectErr { + assert.Error(t, err) + assert.Nil(t, res) + } else { + assert.NoError(t, err) + assert.NotNil(t, res) + } + assert.Equal(t, test.expectCalls, fake.GetCalledNames()) + } +} diff --git a/pkg/server/service_test.go b/pkg/server/service_test.go new file mode 100644 index 000000000..9ac434ed3 --- /dev/null +++ b/pkg/server/service_test.go @@ -0,0 +1,162 @@ +/* +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 ( + "io" + "os" + "testing" + + "github.com/docker/docker/pkg/truncindex" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + + "github.com/kubernetes-incubator/cri-containerd/pkg/metadata" + "github.com/kubernetes-incubator/cri-containerd/pkg/metadata/store" + ostesting "github.com/kubernetes-incubator/cri-containerd/pkg/os/testing" + "github.com/kubernetes-incubator/cri-containerd/pkg/registrar" + servertesting "github.com/kubernetes-incubator/cri-containerd/pkg/server/testing" + + "github.com/containerd/containerd/api/services/execution" + + "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/runtime" +) + +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 } +func (nopReadWriteCloser) Close() error { return nil } + +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), + } +} + +// Test all sandbox operations. +func TestSandboxOperations(t *testing.T) { + c := newTestCRIContainerdService() + fake := c.containerService.(*servertesting.FakeExecutionClient) + fakeOS := c.os.(*ostesting.FakeOS) + fakeOS.OpenFifoFn = func(ctx context.Context, fn string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { + return nopReadWriteCloser{}, nil + } + config := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-name", + Uid: "test-uid", + Namespace: "test-ns", + Attempt: 1, + }, + Hostname: "test-hostname", + LogDirectory: "test-log-directory", + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + } + + t.Logf("should be able to run a pod sandbox") + runRes, err := c.RunPodSandbox(context.Background(), &runtime.RunPodSandboxRequest{Config: config}) + assert.NoError(t, err) + require.NotNil(t, runRes) + id := runRes.GetPodSandboxId() + + t.Logf("should be able to get pod sandbox status") + info, err := fake.Info(context.Background(), &execution.InfoRequest{ID: id}) + assert.NoError(t, err) + expectSandboxStatus := &runtime.PodSandboxStatus{ + Id: id, + Metadata: config.GetMetadata(), + // TODO(random-liu): [P2] Use fake clock for CreatedAt. + Network: &runtime.PodSandboxNetworkStatus{}, + Linux: &runtime.LinuxPodSandboxStatus{ + Namespaces: &runtime.Namespace{ + Network: getNetworkNamespace(info.Pid), + Options: &runtime.NamespaceOption{ + HostNetwork: false, + HostPid: false, + HostIpc: false, + }, + }, + }, + Labels: config.GetLabels(), + Annotations: config.GetAnnotations(), + } + statusRes, err := c.PodSandboxStatus(context.Background(), &runtime.PodSandboxStatusRequest{PodSandboxId: id}) + assert.NoError(t, err) + require.NotNil(t, statusRes) + status := statusRes.GetStatus() + expectSandboxStatus.CreatedAt = status.GetCreatedAt() + assert.Equal(t, expectSandboxStatus, status) + + t.Logf("should be able to list pod sandboxes") + expectSandbox := &runtime.PodSandbox{ + Id: id, + Metadata: config.GetMetadata(), + State: runtime.PodSandboxState_SANDBOX_READY, + Labels: config.GetLabels(), + Annotations: config.GetAnnotations(), + } + listRes, err := c.ListPodSandbox(context.Background(), &runtime.ListPodSandboxRequest{}) + assert.NoError(t, err) + require.NotNil(t, listRes) + sandboxes := listRes.GetItems() + assert.Len(t, sandboxes, 1) + expectSandbox.CreatedAt = sandboxes[0].CreatedAt + assert.Equal(t, expectSandbox, sandboxes[0]) + + t.Logf("should be able to stop a pod sandbox") + stopRes, err := c.StopPodSandbox(context.Background(), &runtime.StopPodSandboxRequest{PodSandboxId: id}) + assert.NoError(t, err) + require.NotNil(t, stopRes) + statusRes, err = c.PodSandboxStatus(context.Background(), &runtime.PodSandboxStatusRequest{PodSandboxId: id}) + assert.NoError(t, err) + require.NotNil(t, statusRes) + assert.Equal(t, runtime.PodSandboxState_SANDBOX_NOTREADY, statusRes.GetStatus().GetState(), + "sandbox status should be NOTREADY after stopped") + listRes, err = c.ListPodSandbox(context.Background(), &runtime.ListPodSandboxRequest{}) + assert.NoError(t, err) + require.NotNil(t, listRes) + assert.Len(t, listRes.GetItems(), 1) + assert.Equal(t, runtime.PodSandboxState_SANDBOX_NOTREADY, listRes.GetItems()[0].State, + "sandbox in list should be NOTREADY after stopped") + + t.Logf("should be able to remove a pod sandbox") + removeRes, err := c.RemovePodSandbox(context.Background(), &runtime.RemovePodSandboxRequest{PodSandboxId: id}) + assert.NoError(t, err) + require.NotNil(t, removeRes) + _, err = c.PodSandboxStatus(context.Background(), &runtime.PodSandboxStatusRequest{PodSandboxId: id}) + assert.Error(t, err, "should not be able to get sandbox status after removed") + listRes, err = c.ListPodSandbox(context.Background(), &runtime.ListPodSandboxRequest{}) + assert.NoError(t, err) + require.NotNil(t, listRes) + assert.Empty(t, listRes.GetItems(), "should not be able to list the sandbox after removed") + + t.Logf("should be able to create the sandbox again") + runRes, err = c.RunPodSandbox(context.Background(), &runtime.RunPodSandboxRequest{Config: config}) + assert.NoError(t, err) + require.NotNil(t, runRes) +}