Merge pull request #78 from Random-Liu/switch-to-new-containerd-client

Switch to new containerd api
This commit is contained in:
Lantao Liu 2017-06-16 09:56:54 -07:00 committed by GitHub
commit cc43c86f86
225 changed files with 41650 additions and 7084 deletions

250
Godeps/Godeps.json generated
View File

@ -27,133 +27,162 @@
"Rev": "e9cf4fae01b5a8ff89d0ec6b32f0d9c9f79aefdd"
},
{
"ImportPath": "github.com/containerd/containerd",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"ImportPath": "github.com/containerd/containerd/api/services/containers",
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/api/services/content",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/api/services/diff",
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/api/services/execution",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/api/services/images",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/api/services/rootfs",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"ImportPath": "github.com/containerd/containerd/api/services/snapshot",
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/api/services/version",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
},
{
"ImportPath": "github.com/containerd/containerd/api/types/container",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/api/types/descriptor",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/api/types/mount",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/api/types/task",
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/archive",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/archive/compression",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/containers",
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/content",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/fs",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/images",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/log",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/metadata",
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/mount",
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/plugin",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/reference",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/remotes",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/remotes/docker",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/rootfs",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/services/content",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/services/diff",
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/services/images",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/services/rootfs",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"ImportPath": "github.com/containerd/containerd/services/snapshot",
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/snapshot",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/containerd/sys",
"Comment": "v0.2.3-858-g2562aca",
"Rev": "2562aca1a3c070bc9f571dec44bfb10993c326d6"
"Comment": "v0.2.8-126-g4ae34cc",
"Rev": "4ae34cccc5b496c6547ff28dbeed1bde4773fa7a"
},
{
"ImportPath": "github.com/containerd/continuity/sysx",
"Rev": "6414d06cab9e2fe082ea29ff42aab627e740d00c"
"Rev": "86cec1535a968310e7532819f699ff2830ed7463"
},
{
"ImportPath": "github.com/containerd/fifo",
"Rev": "69b99525e472735860a5269b75af1970142b3062"
},
{
"ImportPath": "github.com/containernetworking/cni/libcni",
@ -247,6 +276,10 @@
"ImportPath": "github.com/golang/protobuf/proto",
"Rev": "8ee79997227bf9b34611aee7946ae64735e6fd93"
},
{
"ImportPath": "github.com/golang/protobuf/ptypes/any",
"Rev": "8ee79997227bf9b34611aee7946ae64735e6fd93"
},
{
"ImportPath": "github.com/golang/protobuf/ptypes/empty",
"Rev": "8ee79997227bf9b34611aee7946ae64735e6fd93"
@ -348,41 +381,37 @@
"Comment": "v2.2.6",
"Rev": "666120de432aea38ab06bd5c818f04f4129882c9"
},
{
"ImportPath": "github.com/tonistiigi/fifo",
"Rev": "fe870ccf293940774c2b44e23f6c71fff8f7547d"
},
{
"ImportPath": "golang.org/x/net/context",
"Rev": "8b4af36cd21a1f85a7484b49feb7c79363106d8e"
"Rev": "7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6"
},
{
"ImportPath": "golang.org/x/net/context/ctxhttp",
"Rev": "8b4af36cd21a1f85a7484b49feb7c79363106d8e"
"Rev": "7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6"
},
{
"ImportPath": "golang.org/x/net/http2",
"Rev": "8b4af36cd21a1f85a7484b49feb7c79363106d8e"
"Rev": "7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6"
},
{
"ImportPath": "golang.org/x/net/http2/hpack",
"Rev": "8b4af36cd21a1f85a7484b49feb7c79363106d8e"
"Rev": "7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6"
},
{
"ImportPath": "golang.org/x/net/idna",
"Rev": "8b4af36cd21a1f85a7484b49feb7c79363106d8e"
"Rev": "7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6"
},
{
"ImportPath": "golang.org/x/net/internal/timeseries",
"Rev": "8b4af36cd21a1f85a7484b49feb7c79363106d8e"
"Rev": "7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6"
},
{
"ImportPath": "golang.org/x/net/lex/httplex",
"Rev": "8b4af36cd21a1f85a7484b49feb7c79363106d8e"
"Rev": "7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6"
},
{
"ImportPath": "golang.org/x/net/trace",
"Rev": "8b4af36cd21a1f85a7484b49feb7c79363106d8e"
"Rev": "7dcfb8076726a3fdd9353b6b8a1f1b6be6811bd6"
},
{
"ImportPath": "golang.org/x/sync/errgroup",
@ -396,65 +425,100 @@
"ImportPath": "golang.org/x/sys/windows",
"Rev": "f3918c30c5c2cb527c0b071a27c35120a6c0719a"
},
{
"ImportPath": "golang.org/x/text/secure/bidirule",
"Rev": "19e51611da83d6be54ddafce4a4af510cb3e9ea4"
},
{
"ImportPath": "golang.org/x/text/transform",
"Rev": "19e51611da83d6be54ddafce4a4af510cb3e9ea4"
},
{
"ImportPath": "golang.org/x/text/unicode/bidi",
"Rev": "19e51611da83d6be54ddafce4a4af510cb3e9ea4"
},
{
"ImportPath": "golang.org/x/text/unicode/norm",
"Rev": "19e51611da83d6be54ddafce4a4af510cb3e9ea4"
},
{
"ImportPath": "google.golang.org/genproto/googleapis/rpc/status",
"Rev": "d80a6e20e776b0b17a324d0ba1ab50a39c8e8944"
},
{
"ImportPath": "google.golang.org/grpc",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/codes",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/credentials",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/grpclb/grpc_lb_v1",
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/grpclog",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/health/grpc_health_v1",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/internal",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/keepalive",
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/metadata",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/naming",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/peer",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/stats",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/status",
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/tap",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "google.golang.org/grpc/transport",
"Comment": "v1.0.5",
"Rev": "708a7f9f3283aa2d4f6132d287d78683babe55c8"
"Comment": "v1.3.0",
"Rev": "d2e1b51f33ff8c5e4a15560ff049d200e83726c5"
},
{
"ImportPath": "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1",

View File

@ -56,6 +56,8 @@ type SandboxMetadata struct {
CreatedAt int64
// NetNS is the network namespace used by the sandbox.
NetNS string
// Pid is the process id of the sandbox.
Pid uint32
}
// SandboxUpdateFunc is the function used to update SandboxMetadata.

View File

@ -42,6 +42,7 @@ func TestSandboxStore(t *testing.T) {
},
CreatedAt: time.Now().UnixNano(),
NetNS: "TestNetNS-1",
Pid: 1001,
},
"2": {
ID: "2",
@ -56,6 +57,7 @@ func TestSandboxStore(t *testing.T) {
},
CreatedAt: time.Now().UnixNano(),
NetNS: "TestNetNS-2",
Pid: 1002,
},
"3": {
ID: "3",
@ -70,6 +72,7 @@ func TestSandboxStore(t *testing.T) {
},
CreatedAt: time.Now().UnixNano(),
NetNS: "TestNetNS-3",
Pid: 1003,
},
}
assert := assertlib.New(t)

View File

@ -21,7 +21,7 @@ import (
"io/ioutil"
"os"
"github.com/tonistiigi/fifo"
"github.com/containerd/fifo"
"golang.org/x/net/context"
"golang.org/x/sys/unix"
)

View File

@ -17,12 +17,18 @@ limitations under the License.
package server
import (
"encoding/json"
"fmt"
"strings"
"time"
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
"github.com/containerd/containerd/api/services/containers"
prototypes "github.com/gogo/protobuf/types"
"github.com/golang/glog"
imagedigest "github.com/opencontainers/go-digest"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runc/libcontainer/devices"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
"golang.org/x/net/context"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
@ -45,6 +51,7 @@ func (c *criContainerdService) CreateContainer(ctx context.Context, r *runtime.C
if err != nil {
return nil, fmt.Errorf("failed to find sandbox id %q: %v", r.GetPodSandboxId(), err)
}
sandboxID := sandbox.ID
// Generate unique id and name for the container and reserve the name.
// Reserve the container name to avoid concurrent `CreateContainer` request creating
@ -65,7 +72,7 @@ func (c *criContainerdService) CreateContainer(ctx context.Context, r *runtime.C
meta := metadata.ContainerMetadata{
ID: id,
Name: name,
SandboxID: sandbox.ID,
SandboxID: sandboxID,
Config: config,
}
@ -79,15 +86,36 @@ func (c *criContainerdService) CreateContainer(ctx context.Context, r *runtime.C
if imageMeta == nil {
return nil, fmt.Errorf("image %q not found", image)
}
if _, err := c.rootfsService.Prepare(ctx, &rootfsapi.PrepareRequest{
Name: id,
// We are sure that ChainID must be a digest.
ChainID: imagedigest.Digest(imageMeta.ChainID),
Readonly: config.GetLinux().GetSecurityContext().GetReadonlyRootfs(),
}); err != nil {
// Generate container runtime spec.
mounts := c.generateContainerMounts(getSandboxRootDir(c.rootDir, sandboxID), config)
spec, err := c.generateContainerSpec(id, sandbox.Pid, config, sandboxConfig, imageMeta.Config, mounts)
if err != nil {
return nil, fmt.Errorf("failed to generate container %q spec: %v", id, err)
}
rawSpec, err := json.Marshal(spec)
if err != nil {
return nil, fmt.Errorf("failed to marshal oci spec %+v: %v", spec, err)
}
glog.V(4).Infof("Container spec: %+v", spec)
// Prepare container rootfs.
if config.GetLinux().GetSecurityContext().GetReadonlyRootfs() {
if _, err := c.snapshotService.View(ctx, id, imageMeta.ChainID); err != nil {
return nil, fmt.Errorf("failed to view container rootfs %q: %v", imageMeta.ChainID, err)
}
} else {
if _, err := c.snapshotService.Prepare(ctx, id, imageMeta.ChainID); err != nil {
return nil, fmt.Errorf("failed to prepare container rootfs %q: %v", imageMeta.ChainID, err)
}
// TODO(random-liu): [P0] Cleanup snapshot on failure after switching to new rootfs api.
}
defer func() {
if retErr != nil {
if err := c.snapshotService.Remove(ctx, id); err != nil {
glog.Errorf("Failed to remove container snapshot %q: %v", id, err)
}
}
}()
meta.ImageRef = imageMeta.ID
// Create container root directory.
@ -106,6 +134,30 @@ func (c *criContainerdService) CreateContainer(ctx context.Context, r *runtime.C
}
}()
// Create containerd container.
if _, err = c.containerService.Create(ctx, &containers.CreateContainerRequest{
Container: containers.Container{
ID: id,
// TODO(random-liu): Checkpoint metadata into container labels.
Image: imageMeta.ID,
Runtime: defaultRuntime,
Spec: &prototypes.Any{
TypeUrl: runtimespec.Version,
Value: rawSpec,
},
RootFS: id,
},
}); err != nil {
return nil, fmt.Errorf("failed to create containerd container: %v", err)
}
defer func() {
if retErr != nil {
if _, err := c.containerService.Delete(ctx, &containers.DeleteContainerRequest{ID: id}); err != nil {
glog.Errorf("Failed to delete containerd container %q: %v", id, err)
}
}
}()
// Update container CreatedAt.
meta.CreatedAt = time.Now().UnixNano()
// Add container into container store.
@ -116,3 +168,283 @@ func (c *criContainerdService) CreateContainer(ctx context.Context, r *runtime.C
return &runtime.CreateContainerResponse{ContainerId: id}, nil
}
func (c *criContainerdService) generateContainerSpec(id string, sandboxPid uint32, config *runtime.ContainerConfig,
sandboxConfig *runtime.PodSandboxConfig, imageConfig *imagespec.ImageConfig, extraMounts []*runtime.Mount) (*runtimespec.Spec, error) {
// Creates a spec Generator with the default spec.
// TODO(random-liu): [P2] Move container runtime spec generation into a helper function.
g := generate.New()
// Set the relative path to the rootfs of the container from containerd's
// pre-defined directory.
g.SetRootPath(relativeRootfsPath)
if err := setOCIProcessArgs(&g, config, imageConfig); err != nil {
return nil, err
}
if config.GetWorkingDir() != "" {
g.SetProcessCwd(config.GetWorkingDir())
} else if imageConfig.WorkingDir != "" {
g.SetProcessCwd(imageConfig.WorkingDir)
}
// Apply envs from image config first, so that envs from container config
// can override them.
if err := addImageEnvs(&g, imageConfig.Env); err != nil {
return nil, err
}
for _, e := range config.GetEnvs() {
g.AddProcessEnv(e.GetKey(), e.GetValue())
}
// TODO: add setOCIPrivileged group all privileged logic together
securityContext := config.GetLinux().GetSecurityContext()
// Add extra mounts first so that CRI specified mounts can override.
addOCIBindMounts(&g, append(extraMounts, config.GetMounts()...), securityContext.GetPrivileged())
g.SetRootReadonly(securityContext.GetReadonlyRootfs())
if err := addOCIDevices(&g, config.GetDevices(), securityContext.GetPrivileged()); err != nil {
return nil, fmt.Errorf("failed to set devices mapping %+v: %v", config.GetDevices(), err)
}
// TODO(random-liu): [P1] Handle container logging, decorate and redirect to file.
setOCILinuxResource(&g, config.GetLinux().GetResources())
if sandboxConfig.GetLinux().GetCgroupParent() != "" {
cgroupsPath := getCgroupsPath(sandboxConfig.GetLinux().GetCgroupParent(), id)
g.SetLinuxCgroupsPath(cgroupsPath)
}
g.SetProcessTerminal(config.GetTty())
if err := setOCICapabilities(&g, securityContext.GetCapabilities(), securityContext.GetPrivileged()); err != nil {
return nil, fmt.Errorf("failed to set capabilities %+v: %v",
securityContext.GetCapabilities(), err)
}
// Set namespaces, share namespace with sandbox container.
setOCINamespaces(&g, securityContext.GetNamespaceOptions(), sandboxPid)
// TODO(random-liu): [P1] Set selinux options.
// TODO(random-liu): [P1] Set user/username.
supplementalGroups := securityContext.GetSupplementalGroups()
for _, group := range supplementalGroups {
g.AddProcessAdditionalGid(uint32(group))
}
// TODO(random-liu): [P2] Add apparmor and seccomp.
return g.Spec(), nil
}
// generateContainerMounts sets up necessary container mounts including /dev/shm, /etc/hosts
// and /etc/resolv.conf.
func (c *criContainerdService) generateContainerMounts(sandboxRootDir string, config *runtime.ContainerConfig) []*runtime.Mount {
var mounts []*runtime.Mount
securityContext := config.GetLinux().GetSecurityContext()
mounts = append(mounts, &runtime.Mount{
ContainerPath: etcHosts,
HostPath: getSandboxHosts(sandboxRootDir),
Readonly: securityContext.GetReadonlyRootfs(),
})
// Mount sandbox resolv.config.
// TODO: Need to figure out whether we should always mount it as read-only
mounts = append(mounts, &runtime.Mount{
ContainerPath: resolvConfPath,
HostPath: getResolvPath(sandboxRootDir),
Readonly: securityContext.GetReadonlyRootfs(),
})
sandboxDevShm := getSandboxDevShm(sandboxRootDir)
if securityContext.GetNamespaceOptions().GetHostIpc() {
sandboxDevShm = devShm
}
mounts = append(mounts, &runtime.Mount{
ContainerPath: devShm,
HostPath: sandboxDevShm,
Readonly: false,
})
return mounts
}
// setOCIProcessArgs sets process args. It returns error if the final arg list
// is empty.
func setOCIProcessArgs(g *generate.Generator, config *runtime.ContainerConfig, imageConfig *imagespec.ImageConfig) error {
command, args := config.GetCommand(), config.GetArgs()
// The following logic is migrated from https://github.com/moby/moby/blob/master/daemon/commit.go
// TODO(random-liu): Clearly define the commands overwrite behavior.
if len(command) == 0 {
if len(args) == 0 {
args = imageConfig.Cmd
}
if command == nil {
command = imageConfig.Entrypoint
}
}
if len(command) == 0 && len(args) == 0 {
return fmt.Errorf("no command specified")
}
g.SetProcessArgs(append(command, args...))
return nil
}
// addImageEnvs adds environment variables from image config. It returns error if
// an invalid environment variable is encountered.
func addImageEnvs(g *generate.Generator, imageEnvs []string) error {
for _, e := range imageEnvs {
kv := strings.Split(e, "=")
if len(kv) != 2 {
return fmt.Errorf("invalid environment variable %q", e)
}
g.AddProcessEnv(kv[0], kv[1])
}
return nil
}
func clearReadOnly(m *runtimespec.Mount) {
var opt []string
for _, o := range m.Options {
if o != "ro" {
opt = append(opt, o)
}
}
m.Options = opt
}
// addDevices set device mapping.
func addOCIDevices(g *generate.Generator, devs []*runtime.Device, privileged bool) error {
spec := g.Spec()
if privileged {
hostDevices, err := devices.HostDevices()
if err != nil {
return err
}
for _, hostDevice := range hostDevices {
rd := runtimespec.LinuxDevice{
Path: hostDevice.Path,
Type: string(hostDevice.Type),
Major: hostDevice.Major,
Minor: hostDevice.Minor,
UID: &hostDevice.Uid,
GID: &hostDevice.Gid,
}
g.AddDevice(rd)
}
spec.Linux.Resources.Devices = []runtimespec.LinuxDeviceCgroup{
{
Allow: true,
Access: "rwm",
},
}
return nil
}
for _, device := range devs {
dev, err := devices.DeviceFromPath(device.HostPath, device.Permissions)
if err != nil {
return err
}
rd := runtimespec.LinuxDevice{
Path: device.ContainerPath,
Type: string(dev.Type),
Major: dev.Major,
Minor: dev.Minor,
UID: &dev.Uid,
GID: &dev.Gid,
}
g.AddDevice(rd)
spec.Linux.Resources.Devices = append(spec.Linux.Resources.Devices, runtimespec.LinuxDeviceCgroup{
Allow: true,
Type: string(dev.Type),
Major: &dev.Major,
Minor: &dev.Minor,
Access: dev.Permissions,
})
}
return nil
}
// addOCIBindMounts adds bind mounts.
// TODO(random-liu): Figure out whether we need to change all CRI mounts to readonly when
// rootfs is readonly. (https://github.com/moby/moby/blob/master/daemon/oci_linux.go)
func addOCIBindMounts(g *generate.Generator, mounts []*runtime.Mount, privileged bool) {
// Mount cgroup into the container as readonly, which inherits docker's behavior.
g.AddCgroupsMount("ro") // nolint: errcheck
for _, mount := range mounts {
dst := mount.GetContainerPath()
src := mount.GetHostPath()
options := []string{"rw"}
if mount.GetReadonly() {
options = []string{"ro"}
}
// TODO(random-liu): [P1] Apply selinux label
g.AddBindMount(src, dst, options)
}
if !privileged {
return
}
spec := g.Spec()
// clear readonly for /sys and cgroup
for i, m := range spec.Mounts {
if spec.Mounts[i].Destination == "/sys" && !spec.Root.Readonly {
clearReadOnly(&spec.Mounts[i])
}
if m.Type == "cgroup" {
clearReadOnly(&spec.Mounts[i])
}
}
spec.Linux.ReadonlyPaths = nil
spec.Linux.MaskedPaths = nil
}
// setOCILinuxResource set container resource limit.
func setOCILinuxResource(g *generate.Generator, resources *runtime.LinuxContainerResources) {
if resources == nil {
return
}
g.SetLinuxResourcesCPUPeriod(uint64(resources.GetCpuPeriod()))
g.SetLinuxResourcesCPUQuota(resources.GetCpuQuota())
g.SetLinuxResourcesCPUShares(uint64(resources.GetCpuShares()))
g.SetLinuxResourcesMemoryLimit(uint64(resources.GetMemoryLimitInBytes()))
g.SetLinuxResourcesOOMScoreAdj(int(resources.GetOomScoreAdj()))
}
// setOCICapabilities adds/drops process capabilities.
func setOCICapabilities(g *generate.Generator, capabilities *runtime.Capability, privileged bool) error {
if privileged {
// Add all capabilities in privileged mode.
g.SetupPrivileged(true)
return nil
}
if capabilities == nil {
return nil
}
// Capabilities in CRI doesn't have `CAP_` prefix, so add it.
for _, c := range capabilities.GetAddCapabilities() {
if err := g.AddProcessCapability("CAP_" + c); err != nil {
return err
}
}
for _, c := range capabilities.GetDropCapabilities() {
if err := g.DropProcessCapability("CAP_" + c); err != nil {
return err
}
}
return nil
}
// setOCINamespaces sets namespaces.
func setOCINamespaces(g *generate.Generator, namespaces *runtime.NamespaceOption, sandboxPid uint32) {
g.AddOrReplaceLinuxNamespace(string(runtimespec.NetworkNamespace), getNetworkNamespace(sandboxPid)) // nolint: errcheck
g.AddOrReplaceLinuxNamespace(string(runtimespec.IPCNamespace), getIPCNamespace(sandboxPid)) // nolint: errcheck
g.AddOrReplaceLinuxNamespace(string(runtimespec.UTSNamespace), getUTSNamespace(sandboxPid)) // nolint: errcheck
g.AddOrReplaceLinuxNamespace(string(runtimespec.PIDNamespace), getPIDNamespace(sandboxPid)) // nolint: errcheck
}

View File

@ -17,13 +17,16 @@ limitations under the License.
package server
import (
"encoding/json"
"errors"
"os"
"testing"
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
imagedigest "github.com/opencontainers/go-digest"
"github.com/containerd/containerd/api/services/containers"
snapshotapi "github.com/containerd/containerd/api/services/snapshot"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
@ -34,38 +37,428 @@ import (
servertesting "github.com/kubernetes-incubator/cri-containerd/pkg/server/testing"
)
func TestCreateContainer(t *testing.T) {
testSandboxID := "test-sandbox-id"
testNameMeta := &runtime.ContainerMetadata{
func checkMount(t *testing.T, mounts []runtimespec.Mount, src, dest, typ string,
contains, notcontains []string) {
found := false
for _, m := range mounts {
if m.Source == src && m.Destination == dest {
assert.Equal(t, m.Type, typ)
for _, c := range contains {
assert.Contains(t, m.Options, c)
}
for _, n := range notcontains {
assert.NotContains(t, m.Options, n)
}
found = true
break
}
}
assert.True(t, found, "mount from %q to %q not found", src, dest)
}
func getCreateContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandboxConfig,
*imagespec.ImageConfig, func(*testing.T, string, uint32, *runtimespec.Spec)) {
config := &runtime.ContainerConfig{
Metadata: &runtime.ContainerMetadata{
Name: "test-name",
Attempt: 1,
}
testSandboxNameMeta := &runtime.PodSandboxMetadata{
Name: "test-sandbox-name",
Uid: "test-sandbox-uid",
Namespace: "test-sandbox-namespace",
Attempt: 2,
}
// Use an image id to avoid image name resolution.
// TODO(random-liu): Change this to image name after we have complete image
// management unit test framework.
testImage := "sha256:c75bebcdd211f41b3a460c7bf82970ed6c75acaab9cd4c9a4e125b03ca113799"
testChainID := imagedigest.Digest("test-chain-id")
testImageMetadata := metadata.ImageMetadata{
ID: testImage,
ChainID: testChainID.String(),
Config: &imagespec.ImageConfig{},
}
testConfig := &runtime.ContainerConfig{
Metadata: testNameMeta,
},
Image: &runtime.ImageSpec{
Image: testImage,
Image: "sha256:c75bebcdd211f41b3a460c7bf82970ed6c75acaab9cd4c9a4e125b03ca113799",
},
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{"SYS_ADMIN"},
DropCapabilities: []string{"CHOWN"},
},
SupplementalGroups: []int64{1111, 2222},
},
},
}
testSandboxConfig := &runtime.PodSandboxConfig{
Metadata: testSandboxNameMeta,
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",
},
}
imageConfig := &imagespec.ImageConfig{
Env: []string{"ik1=iv1", "ik2=iv2"},
Entrypoint: []string{"/entrypoint"},
Cmd: []string{"cmd"},
WorkingDir: "/workspace",
}
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", "ik1=iv1", "ik2=iv2")
t.Logf("Check cgroups bind mount")
checkMount(t, spec.Mounts, "cgroup", "/sys/fs/cgroup", "cgroup", []string{"ro"}, nil)
t.Logf("Check bind mount")
checkMount(t, spec.Mounts, "host-path-1", "container-path-1", "bind", []string{"rw"}, nil)
checkMount(t, spec.Mounts, "host-path-2", "container-path-2", "bind", []string{"ro"}, nil)
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, imageConfig, specCheck
}
func TestGeneralContainerSpec(t *testing.T) {
testID := "test-id"
testPid := uint32(1234)
config, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData()
c := newTestCRIContainerdService()
spec, err := c.generateContainerSpec(testID, testPid, config, sandboxConfig, imageConfig, nil)
assert.NoError(t, err)
specCheck(t, testID, testPid, spec)
}
func TestContainerSpecTty(t *testing.T) {
testID := "test-id"
testPid := uint32(1234)
config, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData()
c := newTestCRIContainerdService()
for _, tty := range []bool{true, false} {
config.Tty = tty
spec, err := c.generateContainerSpec(testID, testPid, config, sandboxConfig, imageConfig, nil)
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, imageConfig, specCheck := getCreateContainerTestData()
c := newTestCRIContainerdService()
for _, readonly := range []bool{true, false} {
config.Linux.SecurityContext.ReadonlyRootfs = readonly
spec, err := c.generateContainerSpec(testID, testPid, config, sandboxConfig, imageConfig, nil)
assert.NoError(t, err)
specCheck(t, testID, testPid, spec)
assert.Equal(t, readonly, spec.Root.Readonly)
}
}
func TestContainerSpecWithExtraMounts(t *testing.T) {
testID := "test-id"
testPid := uint32(1234)
config, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData()
c := newTestCRIContainerdService()
mountInConfig := &runtime.Mount{
ContainerPath: "test-container-path",
HostPath: "test-host-path",
Readonly: false,
}
config.Mounts = append(config.Mounts, mountInConfig)
extraMount := &runtime.Mount{
ContainerPath: "test-container-path",
HostPath: "test-host-path-extra",
Readonly: true,
}
spec, err := c.generateContainerSpec(testID, testPid, config, sandboxConfig, imageConfig, []*runtime.Mount{extraMount})
assert.NoError(t, err)
specCheck(t, testID, testPid, spec)
var mounts []runtimespec.Mount
for _, m := range spec.Mounts {
if m.Destination == "test-container-path" {
mounts = append(mounts, m)
}
}
t.Logf("Extra mounts should come first")
require.Len(t, mounts, 2)
assert.Equal(t, "test-host-path-extra", mounts[0].Source)
assert.Contains(t, mounts[0].Options, "ro")
assert.Equal(t, "test-host-path", mounts[1].Source)
assert.Contains(t, mounts[1].Options, "rw")
}
func TestContainerSpecCommand(t *testing.T) {
for desc, test := range map[string]struct {
criEntrypoint []string
criArgs []string
imageEntrypoint []string
imageArgs []string
expected []string
expectErr bool
}{
"should use cri entrypoint if it's specified": {
criEntrypoint: []string{"a", "b"},
imageEntrypoint: []string{"c", "d"},
imageArgs: []string{"e", "f"},
expected: []string{"a", "b"},
},
"should use cri entrypoint if it's specified even if it's empty": {
criEntrypoint: []string{},
criArgs: []string{"a", "b"},
imageEntrypoint: []string{"c", "d"},
imageArgs: []string{"e", "f"},
expected: []string{"a", "b"},
},
"should use cri entrypoint and args if they are specified": {
criEntrypoint: []string{"a", "b"},
criArgs: []string{"c", "d"},
imageEntrypoint: []string{"e", "f"},
imageArgs: []string{"g", "h"},
expected: []string{"a", "b", "c", "d"},
},
"should use image entrypoint if cri entrypoint is not specified": {
criArgs: []string{"a", "b"},
imageEntrypoint: []string{"c", "d"},
imageArgs: []string{"e", "f"},
expected: []string{"c", "d", "a", "b"},
},
"should use image args if both cri entrypoint and args are not specified": {
imageEntrypoint: []string{"c", "d"},
imageArgs: []string{"e", "f"},
expected: []string{"c", "d", "e", "f"},
},
"should return error if both entrypoint and args are empty": {
expectErr: true,
},
} {
config, _, imageConfig, _ := getCreateContainerTestData()
g := generate.New()
config.Command = test.criEntrypoint
config.Args = test.criArgs
imageConfig.Entrypoint = test.imageEntrypoint
imageConfig.Cmd = test.imageArgs
err := setOCIProcessArgs(&g, config, imageConfig)
if test.expectErr {
assert.Error(t, err)
continue
}
assert.NoError(t, err)
assert.Equal(t, test.expected, g.Spec().Process.Args, desc)
}
}
func TestGenerateContainerMounts(t *testing.T) {
testSandboxRootDir := "test-sandbox-root"
for desc, test := range map[string]struct {
securityContext *runtime.LinuxContainerSecurityContext
expectedMounts []*runtime.Mount
}{
"should setup ro mount when rootfs is read-only": {
securityContext: &runtime.LinuxContainerSecurityContext{
ReadonlyRootfs: true,
},
expectedMounts: []*runtime.Mount{
{
ContainerPath: "/etc/hosts",
HostPath: testSandboxRootDir + "/hosts",
Readonly: true,
},
{
ContainerPath: resolvConfPath,
HostPath: testSandboxRootDir + "/resolv.conf",
Readonly: true,
},
{
ContainerPath: "/dev/shm",
HostPath: testSandboxRootDir + "/shm",
Readonly: false,
},
},
},
"should setup rw mount when rootfs is read-write": {
securityContext: &runtime.LinuxContainerSecurityContext{},
expectedMounts: []*runtime.Mount{
{
ContainerPath: "/etc/hosts",
HostPath: testSandboxRootDir + "/hosts",
Readonly: false,
},
{
ContainerPath: resolvConfPath,
HostPath: testSandboxRootDir + "/resolv.conf",
Readonly: false,
},
{
ContainerPath: "/dev/shm",
HostPath: testSandboxRootDir + "/shm",
Readonly: false,
},
},
},
"should use host /dev/shm when host ipc is set": {
securityContext: &runtime.LinuxContainerSecurityContext{
NamespaceOptions: &runtime.NamespaceOption{HostIpc: true},
},
expectedMounts: []*runtime.Mount{
{
ContainerPath: "/etc/hosts",
HostPath: testSandboxRootDir + "/hosts",
Readonly: false,
},
{
ContainerPath: resolvConfPath,
HostPath: testSandboxRootDir + "/resolv.conf",
Readonly: false,
},
{
ContainerPath: "/dev/shm",
HostPath: "/dev/shm",
Readonly: false,
},
},
},
} {
config := &runtime.ContainerConfig{
Metadata: &runtime.ContainerMetadata{
Name: "test-name",
Attempt: 1,
},
Linux: &runtime.LinuxContainerConfig{
SecurityContext: test.securityContext,
},
}
c := newTestCRIContainerdService()
mounts := c.generateContainerMounts(testSandboxRootDir, config)
assert.Equal(t, test.expectedMounts, mounts, desc)
}
}
func TestPrivilegedBindMount(t *testing.T) {
for desc, test := range map[string]struct {
privileged bool
readonlyRootFS bool
expectedSysFSRO bool
expectedCgroupFSRO bool
}{
"sysfs and cgroupfs should mount as 'ro' by default": {
expectedSysFSRO: true,
expectedCgroupFSRO: true,
},
"sysfs and cgroupfs should not mount as 'ro' if privileged": {
privileged: true,
expectedSysFSRO: false,
expectedCgroupFSRO: false,
},
"sysfs should mount as 'ro' if root filrsystem is readonly": {
privileged: true,
readonlyRootFS: true,
expectedSysFSRO: true,
expectedCgroupFSRO: false,
},
} {
t.Logf("TestCase %q", desc)
g := generate.New()
g.SetRootReadonly(test.readonlyRootFS)
addOCIBindMounts(&g, nil, test.privileged)
spec := g.Spec()
if test.expectedSysFSRO {
checkMount(t, spec.Mounts, "sysfs", "/sys", "sysfs", []string{"ro"}, nil)
} else {
checkMount(t, spec.Mounts, "sysfs", "/sys", "sysfs", nil, []string{"ro"})
}
if test.expectedCgroupFSRO {
checkMount(t, spec.Mounts, "cgroup", "/sys/fs/cgroup", "cgroup", []string{"ro"}, nil)
} else {
checkMount(t, spec.Mounts, "cgroup", "/sys/fs/cgroup", "cgroup", nil, []string{"ro"})
}
}
}
func TestCreateContainer(t *testing.T) {
testSandboxID := "test-sandbox-id"
testSandboxPid := uint32(4321)
config, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData()
testSandboxMetadata := &metadata.SandboxMetadata{
ID: testSandboxID,
Name: "test-sandbox-name",
Config: sandboxConfig,
Pid: testSandboxPid,
}
testContainerName := makeContainerName(config.Metadata, sandboxConfig.Metadata)
// Use an image id to avoid image name resolution.
// TODO(random-liu): Change this to image name after we have complete image
// management unit test framework.
testImage := config.GetImage().GetImage()
testChainID := "test-chain-id"
testImageMetadata := metadata.ImageMetadata{
ID: testImage,
ChainID: testChainID,
Config: imageConfig,
}
for desc, test := range map[string]struct {
@ -83,74 +476,53 @@ func TestCreateContainer(t *testing.T) {
expectErr: true,
},
"should return error if name is reserved": {
sandboxMetadata: &metadata.SandboxMetadata{
ID: testSandboxID,
Name: makeSandboxName(testSandboxNameMeta),
Config: testSandboxConfig,
},
sandboxMetadata: testSandboxMetadata,
reserveNameErr: true,
expectErr: true,
},
"should return error if fail to create root directory": {
sandboxMetadata: &metadata.SandboxMetadata{
ID: testSandboxID,
Name: makeSandboxName(testSandboxNameMeta),
Config: testSandboxConfig,
},
sandboxMetadata: testSandboxMetadata,
createRootDirErr: errors.New("random error"),
expectErr: true,
},
"should return error if image is not pulled": {
sandboxMetadata: &metadata.SandboxMetadata{
ID: testSandboxID,
Name: makeSandboxName(testSandboxNameMeta),
Config: testSandboxConfig,
},
sandboxMetadata: testSandboxMetadata,
imageMetadataErr: true,
expectErr: true,
},
"should return error if prepare snapshot fails": {
sandboxMetadata: &metadata.SandboxMetadata{
ID: testSandboxID,
Name: makeSandboxName(testSandboxNameMeta),
Config: testSandboxConfig,
},
sandboxMetadata: testSandboxMetadata,
prepareSnapshotErr: errors.New("random error"),
expectErr: true,
},
"should be able to create container successfully": {
sandboxMetadata: &metadata.SandboxMetadata{
ID: testSandboxID,
Name: makeSandboxName(testSandboxNameMeta),
Config: testSandboxConfig,
},
sandboxMetadata: testSandboxMetadata,
expectErr: false,
expectMeta: &metadata.ContainerMetadata{
Name: makeContainerName(testNameMeta, testSandboxNameMeta),
Name: testContainerName,
SandboxID: testSandboxID,
ImageRef: testImage,
Config: testConfig,
Config: config,
},
},
} {
t.Logf("TestCase %q", desc)
c := newTestCRIContainerdService()
fakeRootfsClient := c.rootfsService.(*servertesting.FakeRootfsClient)
fake := c.containerService.(*servertesting.FakeContainersClient)
fakeSnapshotClient := WithFakeSnapshotClient(c)
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"))
assert.NoError(t, c.containerNameIndex.Reserve(testContainerName, "random id"))
}
if !test.imageMetadataErr {
assert.NoError(t, c.imageMetadataStore.Create(testImageMetadata))
}
if test.prepareSnapshotErr != nil {
fakeRootfsClient.InjectError("prepare", test.prepareSnapshotErr)
fakeSnapshotClient.InjectError("prepare", test.prepareSnapshotErr)
}
fakeRootfsClient.SetFakeChainIDs([]imagedigest.Digest{testChainID})
rootExists := false
rootPath := ""
fakeOS.MkdirAllFn = func(path string, perm os.FileMode) error {
@ -168,17 +540,21 @@ func TestCreateContainer(t *testing.T) {
}
resp, err := c.CreateContainer(context.Background(), &runtime.CreateContainerRequest{
PodSandboxId: testSandboxID,
Config: testConfig,
SandboxConfig: testSandboxConfig,
Config: config,
SandboxConfig: sandboxConfig,
})
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"),
assert.NoError(t, c.containerNameIndex.Reserve(testContainerName, "random id"),
"container name should be released")
}
assert.Empty(t, fakeSnapshotClient.ListMounts(), "snapshot should be cleaned up")
listResp, err := fake.List(context.Background(), &containers.ListContainersRequest{})
assert.NoError(t, err)
assert.Empty(t, listResp.Containers, "containerd container should be cleaned up")
metas, err := c.containerStore.List()
assert.NoError(t, err)
assert.Empty(t, metas, "container metadata should not be created")
@ -197,14 +573,25 @@ func TestCreateContainer(t *testing.T) {
test.expectMeta.CreatedAt = meta.CreatedAt
assert.Equal(t, test.expectMeta, meta, "container metadata should be created")
assert.Equal(t, []string{"prepare"}, fakeRootfsClient.GetCalledNames(), "prepare should be called")
calls := fakeRootfsClient.GetCalledDetails()
prepareOpts := calls[0].Argument.(*rootfsapi.PrepareRequest)
assert.Equal(t, &rootfsapi.PrepareRequest{
Name: id,
ChainID: testChainID,
// TODO(random-liu): Test readonly rootfs.
Readonly: false,
// Check runtime spec
containersCalls := fake.GetCalledDetails()
require.Len(t, containersCalls, 1)
createOpts, ok := containersCalls[0].Argument.(*containers.CreateContainerRequest)
assert.True(t, ok, "should create containerd container")
assert.Equal(t, id, createOpts.Container.ID, "container id should be correct")
assert.Equal(t, testImage, createOpts.Container.Image, "test image should be correct")
assert.Equal(t, id, createOpts.Container.RootFS, "rootfs should be correct")
spec := &runtimespec.Spec{}
assert.NoError(t, json.Unmarshal(createOpts.Container.Spec.Value, spec))
specCheck(t, id, testSandboxPid, spec)
assert.Equal(t, []string{"prepare"}, fakeSnapshotClient.GetCalledNames(), "prepare should be called")
snapshotCalls := fakeSnapshotClient.GetCalledDetails()
require.Len(t, snapshotCalls, 1)
prepareOpts := snapshotCalls[0].Argument.(*snapshotapi.PrepareRequest)
assert.Equal(t, &snapshotapi.PrepareRequest{
Key: id,
Parent: testChainID,
}, prepareOpts, "prepare request should be correct")
}
}

View File

@ -19,9 +19,10 @@ package server
import (
"fmt"
"github.com/containerd/containerd/api/services/containers"
"github.com/containerd/containerd/snapshot"
"github.com/golang/glog"
"golang.org/x/net/context"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
"github.com/kubernetes-incubator/cri-containerd/pkg/metadata"
@ -69,7 +70,13 @@ func (c *criContainerdService) RemoveContainer(ctx context.Context, r *runtime.R
// kubelet implementation, we'll never start a container once we decide to remove it,
// so we don't need the "Dead" state for now.
// TODO(random-liu): [P0] Cleanup snapshot after switching to new snapshot api.
// Remove container snapshot.
if err := c.snapshotService.Remove(ctx, id); err != nil {
if !snapshot.IsNotExist(err) {
return nil, fmt.Errorf("failed to remove container snapshot %q: %v", id, err)
}
glog.V(5).Infof("Remove called for snapshot %q that does not exist", id)
}
// Cleanup container root directory.
containerRootDir := getContainerRootDir(c.rootDir, id)
@ -78,6 +85,14 @@ func (c *criContainerdService) RemoveContainer(ctx context.Context, r *runtime.R
containerRootDir, err)
}
// Delete containerd container.
if _, err := c.containerService.Delete(ctx, &containers.DeleteContainerRequest{ID: id}); err != nil {
if !isContainerdGRPCNotFoundError(err) {
return nil, fmt.Errorf("failed to delete containerd container %q: %v", id, err)
}
glog.V(5).Infof("Remove called for containerd container %q that does not exist", id, err)
}
// Delete container metadata.
if err := c.containerStore.Delete(id); err != nil {
return nil, fmt.Errorf("failed to delete container metadata for %q: %v", id, err)

View File

@ -21,6 +21,9 @@ import (
"testing"
"time"
"github.com/containerd/containerd/api/services/containers"
snapshotapi "github.com/containerd/containerd/api/services/snapshot"
"github.com/containerd/containerd/api/types/mount"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
@ -28,6 +31,7 @@ import (
"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"
)
// TestSetContainerRemoving tests setContainerRemoving sets removing
@ -87,8 +91,17 @@ func TestSetContainerRemoving(t *testing.T) {
func TestRemoveContainer(t *testing.T) {
testID := "test-id"
testName := "test-name"
testContainerMetadata := &metadata.ContainerMetadata{
ID: testID,
CreatedAt: time.Now().UnixNano(),
StartedAt: time.Now().UnixNano(),
FinishedAt: time.Now().UnixNano(),
}
for desc, test := range map[string]struct {
metadata *metadata.ContainerMetadata
removeSnapshotErr error
deleteContainerErr error
removeDirErr error
expectErr bool
expectUnsetRemoving bool
@ -111,33 +124,47 @@ func TestRemoveContainer(t *testing.T) {
},
expectErr: true,
},
"should not return error if container does not exist": {
"should not return error if container metadata does not exist": {
metadata: nil,
removeSnapshotErr: servertesting.SnapshotNotExistError,
deleteContainerErr: servertesting.ContainerNotExistError,
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(),
"should not return error if snapshot does not exist": {
metadata: testContainerMetadata,
removeSnapshotErr: servertesting.SnapshotNotExistError,
expectErr: false,
},
"should return error if remove snapshot fails": {
metadata: testContainerMetadata,
removeSnapshotErr: errors.New("random error"),
expectErr: true,
},
"should not return error if containerd container does not exist": {
metadata: testContainerMetadata,
deleteContainerErr: servertesting.ContainerNotExistError,
expectErr: false,
},
"should return error if delete containerd container fails": {
metadata: testContainerMetadata,
deleteContainerErr: errors.New("random error"),
expectErr: true,
},
"should return error if remove container root fails": {
metadata: testContainerMetadata,
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(),
},
metadata: testContainerMetadata,
expectErr: false,
},
} {
t.Logf("TestCase %q", desc)
c := newTestCRIContainerdService()
fake := c.containerService.(*servertesting.FakeContainersClient)
fakeSnapshotClient := WithFakeSnapshotClient(c)
fakeOS := c.os.(*ostesting.FakeOS)
if test.metadata != nil {
assert.NoError(t, c.containerNameIndex.Reserve(testName, testID))
@ -147,6 +174,25 @@ func TestRemoveContainer(t *testing.T) {
assert.Equal(t, getContainerRootDir(c.rootDir, testID), path)
return test.removeDirErr
}
if test.removeSnapshotErr == nil {
fakeSnapshotClient.SetFakeMounts(testID, []*mount.Mount{
{
Type: "bind",
Source: "/test/source",
Target: "/test/target",
},
})
} else {
fakeSnapshotClient.InjectError("remove", test.removeSnapshotErr)
}
if test.deleteContainerErr == nil {
_, err := fake.Create(context.Background(), &containers.CreateContainerRequest{
Container: containers.Container{ID: testID},
})
assert.NoError(t, err)
} else {
fake.InjectError("delete", test.deleteContainerErr)
}
resp, err := c.RemoveContainer(context.Background(), &runtime.RemoveContainerRequest{
ContainerId: testID,
})
@ -171,5 +217,18 @@ func TestRemoveContainer(t *testing.T) {
assert.Nil(t, meta, "container metadata should be removed")
assert.NoError(t, c.containerNameIndex.Reserve(testName, testID),
"container name should be released")
mountsResp, err := fakeSnapshotClient.Mounts(context.Background(), &snapshotapi.MountsRequest{Key: testID})
assert.Equal(t, servertesting.SnapshotNotExistError, err, "snapshot should be removed")
assert.Nil(t, mountsResp)
getResp, err := fake.Get(context.Background(), &containers.GetContainerRequest{ID: testID})
assert.Equal(t, servertesting.ContainerNotExistError, err, "containerd container should be removed")
assert.Nil(t, getResp)
resp, err = c.RemoveContainer(context.Background(), &runtime.RemoveContainerRequest{
ContainerId: testID,
})
assert.NoError(t, err)
assert.NotNil(t, resp, "remove should be idempotent")
}
}

View File

@ -17,23 +17,16 @@ limitations under the License.
package server
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/containerd/containerd/api/services/execution"
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
"github.com/containerd/containerd/api/types/container"
prototypes "github.com/gogo/protobuf/types"
"github.com/containerd/containerd/api/types/mount"
"github.com/containerd/containerd/api/types/task"
"github.com/golang/glog"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runc/libcontainer/devices"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
"golang.org/x/net/context"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
@ -104,35 +97,15 @@ func (c *criContainerdService) startContainer(ctx context.Context, id string, me
sandboxConfig := sandboxMeta.Config
sandboxID := meta.SandboxID
// Make sure sandbox is running.
sandboxInfo, err := c.containerService.Info(ctx, &execution.InfoRequest{ID: sandboxID})
sandboxInfo, err := c.taskService.Info(ctx, &execution.InfoRequest{ContainerID: sandboxID})
if err != nil {
return fmt.Errorf("failed to get sandbox container %q info: %v", sandboxID, err)
}
// This is only a best effort check, sandbox may still exit after this. If sandbox fails
// before starting the container, the start will fail.
if sandboxInfo.Status != container.Status_RUNNING {
if sandboxInfo.Task.Status != task.StatusRunning {
return fmt.Errorf("sandbox container %q is not running", sandboxID)
}
sandboxPid := sandboxInfo.Pid
glog.V(2).Infof("Sandbox container %q is running with pid %d", sandboxID, sandboxPid)
// Generate containerd container create options.
imageMeta, err := c.imageMetadataStore.Get(meta.ImageRef)
if err != nil {
return fmt.Errorf("failed to get container image %q: %v", meta.ImageRef, err)
}
mounts := c.generateContainerMounts(getSandboxRootDir(c.rootDir, sandboxID), config)
spec, err := c.generateContainerSpec(id, sandboxPid, config, sandboxConfig, imageMeta.Config, mounts)
if err != nil {
return fmt.Errorf("failed to generate container %q spec: %v", id, err)
}
rawSpec, err := json.Marshal(spec)
if err != nil {
return fmt.Errorf("failed to marshal oci spec %+v: %v", spec, err)
}
glog.V(4).Infof("Container spec: %+v", spec)
containerRootDir := getContainerRootDir(c.rootDir, id)
stdin, stdout, stderr := getStreamingPipes(containerRootDir)
@ -154,7 +127,6 @@ func (c *criContainerdService) startContainer(ctx context.Context, id string, me
}
}()
// Redirect the stream to std for now.
// TODO(random-liu): [P1] Support container logging.
// TODO(random-liu): [P1] Support StdinOnce after container logging is added.
if stdinPipe != nil {
go func(w io.WriteCloser) {
@ -177,43 +149,46 @@ func (c *criContainerdService) startContainer(ctx context.Context, id string, me
}
// Get rootfs mounts.
mountsResp, err := c.rootfsService.Mounts(ctx, &rootfsapi.MountsRequest{Name: id})
rootfsMounts, err := c.snapshotService.Mounts(ctx, id)
if err != nil {
return fmt.Errorf("failed to get rootfs mounts %q: %v", id, err)
}
var rootfs []*mount.Mount
for _, m := range rootfsMounts {
rootfs = append(rootfs, &mount.Mount{
Type: m.Type,
Source: m.Source,
Options: m.Options,
})
}
// Create containerd container.
// Create containerd task.
createOpts := &execution.CreateRequest{
ID: id,
Spec: &prototypes.Any{
TypeUrl: runtimespec.Version,
Value: rawSpec,
},
Rootfs: mountsResp.Mounts,
Runtime: defaultRuntime,
ContainerID: id,
Rootfs: rootfs,
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
Terminal: config.GetTty(),
}
glog.V(5).Infof("Create containerd container (id=%q, name=%q) with options %+v.",
glog.V(5).Infof("Create containerd task (id=%q, name=%q) with options %+v.",
id, meta.Name, createOpts)
createResp, err := c.containerService.Create(ctx, createOpts)
createResp, err := c.taskService.Create(ctx, createOpts)
if err != nil {
return fmt.Errorf("failed to create containerd container: %v", err)
return fmt.Errorf("failed to create containerd task: %v", err)
}
defer func() {
if retErr != nil {
// Cleanup the containerd container if an error is returned.
if _, err := c.containerService.Delete(ctx, &execution.DeleteRequest{ID: id}); err != nil {
glog.Errorf("Failed to delete containerd container %q: %v", id, err)
// Cleanup the containerd task if an error is returned.
if _, err := c.taskService.Delete(ctx, &execution.DeleteRequest{ContainerID: id}); err != nil {
glog.Errorf("Failed to delete containerd task %q: %v", id, err)
}
}
}()
// Start containerd container.
if _, err := c.containerService.Start(ctx, &execution.StartRequest{ID: id}); err != nil {
return fmt.Errorf("failed to start containerd container %q: %v", id, err)
// Start containerd task.
if _, err := c.taskService.Start(ctx, &execution.StartRequest{ContainerID: id}); err != nil {
return fmt.Errorf("failed to start containerd task %q: %v", id, err)
}
// Update container start timestamp.
@ -221,283 +196,3 @@ func (c *criContainerdService) startContainer(ctx context.Context, id string, me
meta.StartedAt = time.Now().UnixNano()
return nil
}
func (c *criContainerdService) generateContainerSpec(id string, sandboxPid uint32, config *runtime.ContainerConfig,
sandboxConfig *runtime.PodSandboxConfig, imageConfig *imagespec.ImageConfig, extraMounts []*runtime.Mount) (*runtimespec.Spec, error) {
// Creates a spec Generator with the default spec.
// TODO(random-liu): [P2] Move container runtime spec generation into a helper function.
g := generate.New()
// Set the relative path to the rootfs of the container from containerd's
// pre-defined directory.
g.SetRootPath(relativeRootfsPath)
if err := setOCIProcessArgs(&g, config, imageConfig); err != nil {
return nil, err
}
if config.GetWorkingDir() != "" {
g.SetProcessCwd(config.GetWorkingDir())
} else if imageConfig.WorkingDir != "" {
g.SetProcessCwd(imageConfig.WorkingDir)
}
// Apply envs from image config first, so that envs from container config
// can override them.
if err := addImageEnvs(&g, imageConfig.Env); err != nil {
return nil, err
}
for _, e := range config.GetEnvs() {
g.AddProcessEnv(e.GetKey(), e.GetValue())
}
// TODO: add setOCIPrivileged group all privileged logic together
securityContext := config.GetLinux().GetSecurityContext()
// Add extra mounts first so that CRI specified mounts can override.
addOCIBindMounts(&g, append(extraMounts, config.GetMounts()...), securityContext.GetPrivileged())
g.SetRootReadonly(securityContext.GetReadonlyRootfs())
if err := addOCIDevices(&g, config.GetDevices(), securityContext.GetPrivileged()); err != nil {
return nil, fmt.Errorf("failed to set devices mapping %+v: %v", config.GetDevices(), err)
}
// TODO(random-liu): [P1] Handle container logging, decorate and redirect to file.
setOCILinuxResource(&g, config.GetLinux().GetResources())
if sandboxConfig.GetLinux().GetCgroupParent() != "" {
cgroupsPath := getCgroupsPath(sandboxConfig.GetLinux().GetCgroupParent(), id)
g.SetLinuxCgroupsPath(cgroupsPath)
}
g.SetProcessTerminal(config.GetTty())
if err := setOCICapabilities(&g, securityContext.GetCapabilities(), securityContext.GetPrivileged()); err != nil {
return nil, fmt.Errorf("failed to set capabilities %+v: %v",
securityContext.GetCapabilities(), err)
}
// Set namespaces, share namespace with sandbox container.
setOCINamespaces(&g, securityContext.GetNamespaceOptions(), sandboxPid)
// TODO(random-liu): [P1] Set selinux options.
// TODO(random-liu): [P1] Set user/username.
supplementalGroups := securityContext.GetSupplementalGroups()
for _, group := range supplementalGroups {
g.AddProcessAdditionalGid(uint32(group))
}
// TODO(random-liu): [P2] Add apparmor and seccomp.
return g.Spec(), nil
}
// generateContainerMounts sets up necessary container mounts including /dev/shm, /etc/hosts
// and /etc/resolv.conf.
func (c *criContainerdService) generateContainerMounts(sandboxRootDir string, config *runtime.ContainerConfig) []*runtime.Mount {
var mounts []*runtime.Mount
securityContext := config.GetLinux().GetSecurityContext()
mounts = append(mounts, &runtime.Mount{
ContainerPath: etcHosts,
HostPath: getSandboxHosts(sandboxRootDir),
Readonly: securityContext.GetReadonlyRootfs(),
})
// Mount sandbox resolv.config.
// TODO: Need to figure out whether we should always mount it as read-only
mounts = append(mounts, &runtime.Mount{
ContainerPath: resolvConfPath,
HostPath: getResolvPath(sandboxRootDir),
Readonly: securityContext.GetReadonlyRootfs(),
})
sandboxDevShm := getSandboxDevShm(sandboxRootDir)
if securityContext.GetNamespaceOptions().GetHostIpc() {
sandboxDevShm = devShm
}
mounts = append(mounts, &runtime.Mount{
ContainerPath: devShm,
HostPath: sandboxDevShm,
Readonly: false,
})
return mounts
}
// setOCIProcessArgs sets process args. It returns error if the final arg list
// is empty.
func setOCIProcessArgs(g *generate.Generator, config *runtime.ContainerConfig, imageConfig *imagespec.ImageConfig) error {
command, args := config.GetCommand(), config.GetArgs()
// The following logic is migrated from https://github.com/moby/moby/blob/master/daemon/commit.go
// TODO(random-liu): Clearly define the commands overwrite behavior.
if len(command) == 0 {
if len(args) == 0 {
args = imageConfig.Cmd
}
if command == nil {
command = imageConfig.Entrypoint
}
}
if len(command) == 0 && len(args) == 0 {
return fmt.Errorf("no command specified")
}
g.SetProcessArgs(append(command, args...))
return nil
}
// addImageEnvs adds environment variables from image config. It returns error if
// an invalid environment variable is encountered.
func addImageEnvs(g *generate.Generator, imageEnvs []string) error {
for _, e := range imageEnvs {
kv := strings.Split(e, "=")
if len(kv) != 2 {
return fmt.Errorf("invalid environment variable %q", e)
}
g.AddProcessEnv(kv[0], kv[1])
}
return nil
}
func clearReadOnly(m *runtimespec.Mount) {
var opt []string
for _, o := range m.Options {
if o != "ro" {
opt = append(opt, o)
}
}
m.Options = opt
}
// addDevices set device mapping.
func addOCIDevices(g *generate.Generator, devs []*runtime.Device, privileged bool) error {
spec := g.Spec()
if privileged {
hostDevices, err := devices.HostDevices()
if err != nil {
return err
}
for _, hostDevice := range hostDevices {
rd := runtimespec.LinuxDevice{
Path: hostDevice.Path,
Type: string(hostDevice.Type),
Major: hostDevice.Major,
Minor: hostDevice.Minor,
UID: &hostDevice.Uid,
GID: &hostDevice.Gid,
}
g.AddDevice(rd)
}
spec.Linux.Resources.Devices = []runtimespec.LinuxDeviceCgroup{
{
Allow: true,
Access: "rwm",
},
}
return nil
}
for _, device := range devs {
dev, err := devices.DeviceFromPath(device.HostPath, device.Permissions)
if err != nil {
return err
}
rd := runtimespec.LinuxDevice{
Path: device.ContainerPath,
Type: string(dev.Type),
Major: dev.Major,
Minor: dev.Minor,
UID: &dev.Uid,
GID: &dev.Gid,
}
g.AddDevice(rd)
spec.Linux.Resources.Devices = append(spec.Linux.Resources.Devices, runtimespec.LinuxDeviceCgroup{
Allow: true,
Type: string(dev.Type),
Major: &dev.Major,
Minor: &dev.Minor,
Access: dev.Permissions,
})
}
return nil
}
// addOCIBindMounts adds bind mounts.
// TODO(random-liu): Figure out whether we need to change all CRI mounts to readonly when
// rootfs is readonly. (https://github.com/moby/moby/blob/master/daemon/oci_linux.go)
func addOCIBindMounts(g *generate.Generator, mounts []*runtime.Mount, privileged bool) {
// Mount cgroup into the container as readonly, which inherits docker's behavior.
g.AddCgroupsMount("ro") // nolint: errcheck
for _, mount := range mounts {
dst := mount.GetContainerPath()
src := mount.GetHostPath()
options := []string{"rw"}
if mount.GetReadonly() {
options = []string{"ro"}
}
// TODO(random-liu): [P1] Apply selinux label
g.AddBindMount(src, dst, options)
}
if !privileged {
return
}
spec := g.Spec()
// clear readonly for /sys and cgroup
for i, m := range spec.Mounts {
if spec.Mounts[i].Destination == "/sys" && !spec.Root.Readonly {
clearReadOnly(&spec.Mounts[i])
}
if m.Type == "cgroup" {
clearReadOnly(&spec.Mounts[i])
}
}
spec.Linux.ReadonlyPaths = nil
spec.Linux.MaskedPaths = nil
}
// setOCILinuxResource set container resource limit.
func setOCILinuxResource(g *generate.Generator, resources *runtime.LinuxContainerResources) {
if resources == nil {
return
}
g.SetLinuxResourcesCPUPeriod(uint64(resources.GetCpuPeriod()))
g.SetLinuxResourcesCPUQuota(resources.GetCpuQuota())
g.SetLinuxResourcesCPUShares(uint64(resources.GetCpuShares()))
g.SetLinuxResourcesMemoryLimit(uint64(resources.GetMemoryLimitInBytes()))
g.SetLinuxResourcesOOMScoreAdj(int(resources.GetOomScoreAdj()))
}
// setOCICapabilities adds/drops process capabilities.
func setOCICapabilities(g *generate.Generator, capabilities *runtime.Capability, privileged bool) error {
if privileged {
// Add all capabilities in privileged mode.
g.SetupPrivileged(true)
return nil
}
if capabilities == nil {
return nil
}
// Capabilities in CRI doesn't have `CAP_` prefix, so add it.
for _, c := range capabilities.GetAddCapabilities() {
if err := g.AddProcessCapability("CAP_" + c); err != nil {
return err
}
}
for _, c := range capabilities.GetDropCapabilities() {
if err := g.DropProcessCapability("CAP_" + c); err != nil {
return err
}
}
return nil
}
// setOCINamespaces sets namespaces.
func setOCINamespaces(g *generate.Generator, namespaces *runtime.NamespaceOption, sandboxPid uint32) {
g.AddOrReplaceLinuxNamespace(string(runtimespec.NetworkNamespace), getNetworkNamespace(sandboxPid)) // nolint: errcheck
g.AddOrReplaceLinuxNamespace(string(runtimespec.IPCNamespace), getIPCNamespace(sandboxPid)) // nolint: errcheck
g.AddOrReplaceLinuxNamespace(string(runtimespec.UTSNamespace), getUTSNamespace(sandboxPid)) // nolint: errcheck
g.AddOrReplaceLinuxNamespace(string(runtimespec.PIDNamespace), getPIDNamespace(sandboxPid)) // nolint: errcheck
}

View File

@ -17,7 +17,6 @@ limitations under the License.
package server
import (
"encoding/json"
"errors"
"io"
"os"
@ -25,11 +24,8 @@ import (
"time"
"github.com/containerd/containerd/api/services/execution"
"github.com/containerd/containerd/api/types/container"
"github.com/containerd/containerd/api/types/mount"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
"github.com/containerd/containerd/api/types/task"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
@ -40,435 +36,29 @@ import (
servertesting "github.com/kubernetes-incubator/cri-containerd/pkg/server/testing"
)
func checkMount(t *testing.T, mounts []runtimespec.Mount, src, dest, typ string,
contains, notcontains []string) {
found := false
for _, m := range mounts {
if m.Source == src && m.Destination == dest {
assert.Equal(t, m.Type, typ)
for _, c := range contains {
assert.Contains(t, m.Options, c)
}
for _, n := range notcontains {
assert.NotContains(t, m.Options, n)
}
found = true
break
}
}
assert.True(t, found, "mount from %q to %q not found", src, dest)
}
func getStartContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandboxConfig,
*imagespec.ImageConfig, 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{"SYS_ADMIN"},
DropCapabilities: []string{"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",
},
}
imageConfig := &imagespec.ImageConfig{
Env: []string{"ik1=iv1", "ik2=iv2"},
Entrypoint: []string{"/entrypoint"},
Cmd: []string{"cmd"},
WorkingDir: "/workspace",
}
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", "ik1=iv1", "ik2=iv2")
t.Logf("Check cgroups bind mount")
checkMount(t, spec.Mounts, "cgroup", "/sys/fs/cgroup", "cgroup", []string{"ro"}, nil)
t.Logf("Check bind mount")
checkMount(t, spec.Mounts, "host-path-1", "container-path-1", "bind", []string{"rw"}, nil)
checkMount(t, spec.Mounts, "host-path-2", "container-path-2", "bind", []string{"ro"}, nil)
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, imageConfig, specCheck
}
func TestGeneralContainerSpec(t *testing.T) {
testID := "test-id"
testPid := uint32(1234)
config, sandboxConfig, imageConfig, specCheck := getStartContainerTestData()
c := newTestCRIContainerdService()
spec, err := c.generateContainerSpec(testID, testPid, config, sandboxConfig, imageConfig, nil)
assert.NoError(t, err)
specCheck(t, testID, testPid, spec)
}
func TestContainerSpecTty(t *testing.T) {
testID := "test-id"
testPid := uint32(1234)
config, sandboxConfig, imageConfig, specCheck := getStartContainerTestData()
c := newTestCRIContainerdService()
for _, tty := range []bool{true, false} {
config.Tty = tty
spec, err := c.generateContainerSpec(testID, testPid, config, sandboxConfig, imageConfig, nil)
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, imageConfig, specCheck := getStartContainerTestData()
c := newTestCRIContainerdService()
for _, readonly := range []bool{true, false} {
config.Linux.SecurityContext.ReadonlyRootfs = readonly
spec, err := c.generateContainerSpec(testID, testPid, config, sandboxConfig, imageConfig, nil)
assert.NoError(t, err)
specCheck(t, testID, testPid, spec)
assert.Equal(t, readonly, spec.Root.Readonly)
}
}
func TestContainerSpecWithExtraMounts(t *testing.T) {
testID := "test-id"
testPid := uint32(1234)
config, sandboxConfig, imageConfig, specCheck := getStartContainerTestData()
c := newTestCRIContainerdService()
mountInConfig := &runtime.Mount{
ContainerPath: "test-container-path",
HostPath: "test-host-path",
Readonly: false,
}
config.Mounts = append(config.Mounts, mountInConfig)
extraMount := &runtime.Mount{
ContainerPath: "test-container-path",
HostPath: "test-host-path-extra",
Readonly: true,
}
spec, err := c.generateContainerSpec(testID, testPid, config, sandboxConfig, imageConfig, []*runtime.Mount{extraMount})
assert.NoError(t, err)
specCheck(t, testID, testPid, spec)
var mounts []runtimespec.Mount
for _, m := range spec.Mounts {
if m.Destination == "test-container-path" {
mounts = append(mounts, m)
}
}
t.Logf("Extra mounts should come first")
require.Len(t, mounts, 2)
assert.Equal(t, "test-host-path-extra", mounts[0].Source)
assert.Contains(t, mounts[0].Options, "ro")
assert.Equal(t, "test-host-path", mounts[1].Source)
assert.Contains(t, mounts[1].Options, "rw")
}
func TestContainerSpecCommand(t *testing.T) {
for desc, test := range map[string]struct {
criEntrypoint []string
criArgs []string
imageEntrypoint []string
imageArgs []string
expected []string
expectErr bool
}{
"should use cri entrypoint if it's specified": {
criEntrypoint: []string{"a", "b"},
imageEntrypoint: []string{"c", "d"},
imageArgs: []string{"e", "f"},
expected: []string{"a", "b"},
},
"should use cri entrypoint if it's specified even if it's empty": {
criEntrypoint: []string{},
criArgs: []string{"a", "b"},
imageEntrypoint: []string{"c", "d"},
imageArgs: []string{"e", "f"},
expected: []string{"a", "b"},
},
"should use cri entrypoint and args if they are specified": {
criEntrypoint: []string{"a", "b"},
criArgs: []string{"c", "d"},
imageEntrypoint: []string{"e", "f"},
imageArgs: []string{"g", "h"},
expected: []string{"a", "b", "c", "d"},
},
"should use image entrypoint if cri entrypoint is not specified": {
criArgs: []string{"a", "b"},
imageEntrypoint: []string{"c", "d"},
imageArgs: []string{"e", "f"},
expected: []string{"c", "d", "a", "b"},
},
"should use image args if both cri entrypoint and args are not specified": {
imageEntrypoint: []string{"c", "d"},
imageArgs: []string{"e", "f"},
expected: []string{"c", "d", "e", "f"},
},
"should return error if both entrypoint and args are empty": {
expectErr: true,
},
} {
config, _, imageConfig, _ := getStartContainerTestData()
g := generate.New()
config.Command = test.criEntrypoint
config.Args = test.criArgs
imageConfig.Entrypoint = test.imageEntrypoint
imageConfig.Cmd = test.imageArgs
err := setOCIProcessArgs(&g, config, imageConfig)
if test.expectErr {
assert.Error(t, err)
continue
}
assert.NoError(t, err)
assert.Equal(t, test.expected, g.Spec().Process.Args, desc)
}
}
func TestGenerateContainerMounts(t *testing.T) {
testSandboxRootDir := "test-sandbox-root"
for desc, test := range map[string]struct {
securityContext *runtime.LinuxContainerSecurityContext
expectedMounts []*runtime.Mount
}{
"should setup ro mount when rootfs is read-only": {
securityContext: &runtime.LinuxContainerSecurityContext{
ReadonlyRootfs: true,
},
expectedMounts: []*runtime.Mount{
{
ContainerPath: "/etc/hosts",
HostPath: testSandboxRootDir + "/hosts",
Readonly: true,
},
{
ContainerPath: resolvConfPath,
HostPath: testSandboxRootDir + "/resolv.conf",
Readonly: true,
},
{
ContainerPath: "/dev/shm",
HostPath: testSandboxRootDir + "/shm",
Readonly: false,
},
},
},
"should setup rw mount when rootfs is read-write": {
securityContext: &runtime.LinuxContainerSecurityContext{},
expectedMounts: []*runtime.Mount{
{
ContainerPath: "/etc/hosts",
HostPath: testSandboxRootDir + "/hosts",
Readonly: false,
},
{
ContainerPath: resolvConfPath,
HostPath: testSandboxRootDir + "/resolv.conf",
Readonly: false,
},
{
ContainerPath: "/dev/shm",
HostPath: testSandboxRootDir + "/shm",
Readonly: false,
},
},
},
"should use host /dev/shm when host ipc is set": {
securityContext: &runtime.LinuxContainerSecurityContext{
NamespaceOptions: &runtime.NamespaceOption{HostIpc: true},
},
expectedMounts: []*runtime.Mount{
{
ContainerPath: "/etc/hosts",
HostPath: testSandboxRootDir + "/hosts",
Readonly: false,
},
{
ContainerPath: resolvConfPath,
HostPath: testSandboxRootDir + "/resolv.conf",
Readonly: false,
},
{
ContainerPath: "/dev/shm",
HostPath: "/dev/shm",
Readonly: false,
},
},
},
} {
config := &runtime.ContainerConfig{
Metadata: &runtime.ContainerMetadata{
Name: "test-name",
Attempt: 1,
},
Linux: &runtime.LinuxContainerConfig{
SecurityContext: test.securityContext,
},
}
c := newTestCRIContainerdService()
mounts := c.generateContainerMounts(testSandboxRootDir, config)
assert.Equal(t, test.expectedMounts, mounts, desc)
}
}
func TestPrivilegedBindMount(t *testing.T) {
for desc, test := range map[string]struct {
privileged bool
readonlyRootFS bool
expectedSysFSRO bool
expectedCgroupFSRO bool
}{
"sysfs and cgroupfs should mount as 'ro' by default": {
expectedSysFSRO: true,
expectedCgroupFSRO: true,
},
"sysfs and cgroupfs should not mount as 'ro' if privileged": {
privileged: true,
expectedSysFSRO: false,
expectedCgroupFSRO: false,
},
"sysfs should mount as 'ro' if root filrsystem is readonly": {
privileged: true,
readonlyRootFS: true,
expectedSysFSRO: true,
expectedCgroupFSRO: false,
},
} {
t.Logf("TestCase %q", desc)
g := generate.New()
g.SetRootReadonly(test.readonlyRootFS)
addOCIBindMounts(&g, nil, test.privileged)
spec := g.Spec()
if test.expectedSysFSRO {
checkMount(t, spec.Mounts, "sysfs", "/sys", "sysfs", []string{"ro"}, nil)
} else {
checkMount(t, spec.Mounts, "sysfs", "/sys", "sysfs", nil, []string{"ro"})
}
if test.expectedCgroupFSRO {
checkMount(t, spec.Mounts, "cgroup", "/sys/fs/cgroup", "cgroup", []string{"ro"}, nil)
} else {
checkMount(t, spec.Mounts, "cgroup", "/sys/fs/cgroup", "cgroup", nil, []string{"ro"})
}
}
}
func TestStartContainer(t *testing.T) {
testID := "test-id"
testSandboxID := "test-sandbox-id"
testSandboxPid := uint32(4321)
testImageID := "sha256:c75bebcdd211f41b3a460c7bf82970ed6c75acaab9cd4c9a4e125b03ca113799"
config, sandboxConfig, imageConfig, specCheck := getStartContainerTestData()
testMetadata := &metadata.ContainerMetadata{
ID: testID,
Name: "test-name",
SandboxID: testSandboxID,
Config: config,
ImageRef: testImageID,
CreatedAt: time.Now().UnixNano(),
}
testSandboxMetadata := &metadata.SandboxMetadata{
ID: testSandboxID,
Name: "test-sandbox-name",
Config: sandboxConfig,
}
testSandboxContainer := &container.Container{
testSandboxContainer := &task.Task{
ID: testSandboxID,
Pid: testSandboxPid,
Status: container.Status_RUNNING,
Pid: uint32(4321),
Status: task.StatusRunning,
}
testMounts := []*mount.Mount{{Type: "bind", Source: "test-source"}}
for desc, test := range map[string]struct {
containerMetadata *metadata.ContainerMetadata
sandboxMetadata *metadata.SandboxMetadata
sandboxContainerdContainer *container.Container
imageMetadataErr bool
sandboxContainerdContainer *task.Task
snapshotMountsErr bool
prepareFIFOErr error
createContainerErr error
@ -477,7 +67,7 @@ func TestStartContainer(t *testing.T) {
expectCalls []string
expectErr bool
}{
"should return error when container does not exist": {
"should return error when container metadata does not exist": {
containerMetadata: nil,
sandboxMetadata: testSandboxMetadata,
sandboxContainerdContainer: testSandboxContainer,
@ -489,7 +79,6 @@ func TestStartContainer(t *testing.T) {
ID: testID,
Name: "test-name",
SandboxID: testSandboxID,
Config: config,
CreatedAt: time.Now().UnixNano(),
StartedAt: time.Now().UnixNano(),
},
@ -503,7 +92,6 @@ func TestStartContainer(t *testing.T) {
ID: testID,
Name: "test-name",
SandboxID: testSandboxID,
Config: config,
CreatedAt: time.Now().UnixNano(),
Removing: true,
},
@ -523,24 +111,15 @@ func TestStartContainer(t *testing.T) {
"should return error when sandbox is not running": {
containerMetadata: testMetadata,
sandboxMetadata: testSandboxMetadata,
sandboxContainerdContainer: &container.Container{
sandboxContainerdContainer: &task.Task{
ID: testSandboxID,
Pid: testSandboxPid,
Status: container.Status_STOPPED,
Pid: uint32(4321),
Status: task.StatusStopped,
},
expectStateChange: true,
expectCalls: []string{"info"},
expectErr: true,
},
"should return error when image doesn't exist": {
containerMetadata: testMetadata,
sandboxMetadata: testSandboxMetadata,
sandboxContainerdContainer: testSandboxContainer,
imageMetadataErr: true,
expectStateChange: true,
expectCalls: []string{"info"},
expectErr: true,
},
"should return error when snapshot mounts fails": {
containerMetadata: testMetadata,
sandboxMetadata: testSandboxMetadata,
@ -574,7 +153,7 @@ func TestStartContainer(t *testing.T) {
sandboxContainerdContainer: testSandboxContainer,
startContainerErr: errors.New("start error"),
expectStateChange: true,
// cleanup the containerd container.
// cleanup the containerd task.
expectCalls: []string{"info", "create", "start", "delete"},
expectErr: true,
},
@ -589,9 +168,9 @@ func TestStartContainer(t *testing.T) {
} {
t.Logf("TestCase %q", desc)
c := newTestCRIContainerdService()
fake := c.containerService.(*servertesting.FakeExecutionClient)
fake := c.taskService.(*servertesting.FakeExecutionClient)
fakeOS := c.os.(*ostesting.FakeOS)
fakeRootfsClient := c.rootfsService.(*servertesting.FakeRootfsClient)
fakeSnapshotClient := WithFakeSnapshotClient(c)
if test.containerMetadata != nil {
assert.NoError(t, c.containerStore.Create(*test.containerMetadata))
}
@ -599,16 +178,10 @@ func TestStartContainer(t *testing.T) {
assert.NoError(t, c.sandboxStore.Create(*test.sandboxMetadata))
}
if test.sandboxContainerdContainer != nil {
fake.SetFakeContainers([]container.Container{*test.sandboxContainerdContainer})
}
if !test.imageMetadataErr {
assert.NoError(t, c.imageMetadataStore.Create(metadata.ImageMetadata{
ID: testImageID,
Config: imageConfig,
}))
fake.SetFakeTasks([]task.Task{*test.sandboxContainerdContainer})
}
if !test.snapshotMountsErr {
fakeRootfsClient.SetFakeMounts(testID, testMounts)
fakeSnapshotClient.SetFakeMounts(testID, testMounts)
}
// TODO(random-liu): Test behavior with different streaming config.
fakeOS.OpenFifoFn = func(context.Context, string, int, os.FileMode) (io.ReadWriteCloser, error) {
@ -650,26 +223,28 @@ func TestStartContainer(t *testing.T) {
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")
_, err := fake.Info(context.Background(), &execution.InfoRequest{ContainerID: testID})
assert.True(t, isContainerdGRPCNotFoundError(err),
"containerd task should be cleaned up 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})
info, err := fake.Info(context.Background(), &execution.InfoRequest{ContainerID: testID})
assert.NoError(t, err)
pid := info.Pid
pid := info.Task.Pid
assert.Equal(t, pid, meta.Pid)
assert.Equal(t, container.Status_RUNNING, info.Status)
// Check runtime spec
assert.Equal(t, task.StatusRunning, info.Task.Status)
calls := fake.GetCalledDetails()
createOpts, ok := calls[1].Argument.(*execution.CreateRequest)
assert.True(t, ok, "2nd call should be create")
assert.Equal(t, testMounts, createOpts.Rootfs, "rootfs mounts should be correct")
// TODO(random-liu): Test other create options.
spec := &runtimespec.Spec{}
assert.NoError(t, json.Unmarshal(createOpts.Spec.Value, spec))
specCheck(t, testID, testSandboxPid, spec)
_, stdout, stderr := getStreamingPipes(getContainerRootDir(c.rootDir, testID))
assert.Equal(t, &execution.CreateRequest{
ContainerID: testID,
Rootfs: testMounts,
// TODO(random-liu): Test streaming configurations.
Stdout: stdout,
Stderr: stderr,
}, createOpts, "create options should be correct")
}
}

View File

@ -69,9 +69,13 @@ func (c *criContainerdService) StopContainer(ctx context.Context, r *runtime.Sto
// TODO(random-liu): [P1] Get stop signal from image config.
stopSignal := unix.SIGTERM
glog.V(2).Infof("Stop container %q with signal %v", id, stopSignal)
_, err = c.containerService.Kill(ctx, &execution.KillRequest{ID: id, Signal: uint32(stopSignal)})
_, err = c.taskService.Kill(ctx, &execution.KillRequest{
ContainerID: id,
Signal: uint32(stopSignal),
PidOrAll: &execution.KillRequest_All{All: true},
})
if err != nil {
if !isContainerdContainerNotExistError(err) && !isRuncProcessAlreadyFinishedError(err) {
if !isContainerdGRPCNotFoundError(err) && !isRuncProcessAlreadyFinishedError(err) {
return nil, fmt.Errorf("failed to stop container %q: %v", id, err)
}
// Move on to make sure container status is updated.
@ -86,9 +90,13 @@ func (c *criContainerdService) StopContainer(ctx context.Context, r *runtime.Sto
// Event handler will Delete the container from containerd after it handles the Exited event.
glog.V(2).Infof("Kill container %q", id)
_, err = c.containerService.Kill(ctx, &execution.KillRequest{ID: id, Signal: uint32(unix.SIGKILL)})
_, err = c.taskService.Kill(ctx, &execution.KillRequest{
ContainerID: id,
Signal: uint32(unix.SIGKILL),
PidOrAll: &execution.KillRequest_All{All: true},
})
if err != nil {
if !isContainerdContainerNotExistError(err) && !isRuncProcessAlreadyFinishedError(err) {
if !isContainerdGRPCNotFoundError(err) && !isRuncProcessAlreadyFinishedError(err) {
return nil, fmt.Errorf("failed to kill container %q: %v", id, err)
}
// Move on to make sure container status is updated.

View File

@ -22,7 +22,7 @@ import (
"time"
"github.com/containerd/containerd/api/services/execution"
"github.com/containerd/containerd/api/types/container"
"github.com/containerd/containerd/api/types/task"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
"golang.org/x/sys/unix"
@ -99,14 +99,14 @@ func TestStopContainer(t *testing.T) {
CreatedAt: time.Now().UnixNano(),
StartedAt: time.Now().UnixNano(),
}
testContainer := container.Container{
testContainer := task.Task{
ID: testID,
Pid: testPid,
Status: container.Status_RUNNING,
Status: task.StatusRunning,
}
for desc, test := range map[string]struct {
metadata *metadata.ContainerMetadata
containerdContainer *container.Container
containerdContainer *task.Task
stopErr error
noTimeout bool
expectErr bool
@ -125,7 +125,7 @@ func TestStopContainer(t *testing.T) {
expectErr: false,
expectCalls: []servertesting.CalledDetail{},
},
"should not return error if containerd container does not exist": {
"should not return error if containerd task does not exist": {
metadata: &testMetadata,
containerdContainer: &testContainer,
// Since it's hard to inject event during `StopContainer` is running,
@ -133,24 +133,32 @@ func TestStopContainer(t *testing.T) {
// status is not updated yet.
// We also leverage this behavior to test that when graceful
// stop doesn't take effect, container should be SIGKILL-ed.
stopErr: servertesting.ContainerNotExistError,
stopErr: servertesting.TaskNotExistError,
expectErr: false,
expectCalls: []servertesting.CalledDetail{
{
Name: "kill",
Argument: &execution.KillRequest{ID: testID, Signal: uint32(unix.SIGTERM)},
Argument: &execution.KillRequest{
ContainerID: testID,
Signal: uint32(unix.SIGTERM),
PidOrAll: &execution.KillRequest_All{All: true},
},
},
{
Name: "kill",
Argument: &execution.KillRequest{ID: testID, Signal: uint32(unix.SIGKILL)},
Argument: &execution.KillRequest{
ContainerID: testID,
Signal: uint32(unix.SIGKILL),
PidOrAll: &execution.KillRequest_All{All: true},
},
},
{
Name: "delete",
Argument: &execution.DeleteRequest{ID: testID},
Argument: &execution.DeleteRequest{ContainerID: testID},
},
},
},
"should not return error if containerd container process already finished": {
"should not return error if containerd task process already finished": {
metadata: &testMetadata,
containerdContainer: &testContainer,
stopErr: errors.New("os: process already finished"),
@ -158,15 +166,23 @@ func TestStopContainer(t *testing.T) {
expectCalls: []servertesting.CalledDetail{
{
Name: "kill",
Argument: &execution.KillRequest{ID: testID, Signal: uint32(unix.SIGTERM)},
Argument: &execution.KillRequest{
ContainerID: testID,
Signal: uint32(unix.SIGTERM),
PidOrAll: &execution.KillRequest_All{All: true},
},
},
{
Name: "kill",
Argument: &execution.KillRequest{ID: testID, Signal: uint32(unix.SIGKILL)},
Argument: &execution.KillRequest{
ContainerID: testID,
Signal: uint32(unix.SIGKILL),
PidOrAll: &execution.KillRequest_All{All: true},
},
},
{
Name: "delete",
Argument: &execution.DeleteRequest{ID: testID},
Argument: &execution.DeleteRequest{ContainerID: testID},
},
},
},
@ -178,11 +194,15 @@ func TestStopContainer(t *testing.T) {
expectCalls: []servertesting.CalledDetail{
{
Name: "kill",
Argument: &execution.KillRequest{ID: testID, Signal: uint32(unix.SIGTERM)},
Argument: &execution.KillRequest{
ContainerID: testID,
Signal: uint32(unix.SIGTERM),
PidOrAll: &execution.KillRequest_All{All: true},
},
},
},
"should not return error if containerd container is gracefully stopped": {
},
"should not return error if containerd task is gracefully stopped": {
metadata: &testMetadata,
containerdContainer: &testContainer,
expectErr: false,
@ -190,11 +210,15 @@ func TestStopContainer(t *testing.T) {
expectCalls: []servertesting.CalledDetail{
{
Name: "kill",
Argument: &execution.KillRequest{ID: testID, Signal: uint32(unix.SIGTERM)},
Argument: &execution.KillRequest{
ContainerID: testID,
Signal: uint32(unix.SIGTERM),
PidOrAll: &execution.KillRequest_All{All: true},
},
},
{
Name: "delete",
Argument: &execution.DeleteRequest{ID: testID},
Argument: &execution.DeleteRequest{ContainerID: testID},
},
},
},
@ -206,11 +230,15 @@ func TestStopContainer(t *testing.T) {
expectCalls: []servertesting.CalledDetail{
{
Name: "kill",
Argument: &execution.KillRequest{ID: testID, Signal: uint32(unix.SIGKILL)},
Argument: &execution.KillRequest{
ContainerID: testID,
Signal: uint32(unix.SIGKILL),
PidOrAll: &execution.KillRequest_All{All: true},
},
},
{
Name: "delete",
Argument: &execution.DeleteRequest{ID: testID},
Argument: &execution.DeleteRequest{ContainerID: testID},
},
},
},
@ -221,15 +249,15 @@ func TestStopContainer(t *testing.T) {
c := newTestCRIContainerdService()
fake := servertesting.NewFakeExecutionClient().WithEvents()
defer fake.Stop()
c.containerService = fake
c.taskService = fake
// Inject metadata.
if test.metadata != nil {
assert.NoError(t, c.containerStore.Create(*test.metadata))
}
// Inject containerd container.
// Inject containerd task.
if test.containerdContainer != nil {
fake.SetFakeContainers([]container.Container{*test.containerdContainer})
fake.SetFakeTasks([]task.Task{*test.containerdContainer})
}
if test.stopErr != nil {
fake.InjectError("kill", test.stopErr)
@ -237,7 +265,7 @@ func TestStopContainer(t *testing.T) {
eventClient, err := fake.Events(context.Background(), &execution.EventsRequest{})
assert.NoError(t, err)
// Start a simple test event monitor.
go func(e execution.ContainerService_EventsClient) {
go func(e execution.Tasks_EventsClient) {
for {
if err := c.handleEventStream(e); err != nil { // nolint: vetshadow
return

View File

@ -20,7 +20,7 @@ import (
"time"
"github.com/containerd/containerd/api/services/execution"
"github.com/containerd/containerd/api/types/container"
"github.com/containerd/containerd/api/types/task"
"github.com/golang/glog"
"github.com/jpillora/backoff"
"golang.org/x/net/context"
@ -48,7 +48,7 @@ func (c *criContainerdService) startEventMonitor() {
}
go func() {
for {
events, err := c.containerService.Events(context.Background(), &execution.EventsRequest{})
events, err := c.taskService.Events(context.Background(), &execution.EventsRequest{})
if err != nil {
glog.Errorf("Failed to connect to containerd event stream: %v", err)
time.Sleep(b.Duration())
@ -69,7 +69,7 @@ func (c *criContainerdService) startEventMonitor() {
}
// handleEventStream receives an event from containerd and handles the event.
func (c *criContainerdService) handleEventStream(events execution.ContainerService_EventsClient) error {
func (c *criContainerdService) handleEventStream(events execution.Tasks_EventsClient) error {
e, err := events.Recv()
if err != nil {
return err
@ -80,13 +80,13 @@ func (c *criContainerdService) handleEventStream(events execution.ContainerServi
}
// handleEvent handles a containerd event.
func (c *criContainerdService) handleEvent(e *container.Event) {
func (c *criContainerdService) handleEvent(e *task.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 containerd-shim exit.
case container.Event_EXIT:
case task.Event_EXIT:
meta, err := c.containerStore.Get(e.ID)
if err != nil {
glog.Errorf("Failed to get container %q metadata: %v", e.ID, err)
@ -97,8 +97,8 @@ func (c *criContainerdService) handleEvent(e *container.Event) {
return
}
// Delete the container from containerd.
_, err = c.containerService.Delete(context.Background(), &execution.DeleteRequest{ID: e.ID})
if err != nil && !isContainerdContainerNotExistError(err) {
_, err = c.taskService.Delete(context.Background(), &execution.DeleteRequest{ContainerID: e.ID})
if err != nil && !isContainerdGRPCNotFoundError(err) {
// TODO(random-liu): [P0] Enqueue the event and retry.
glog.Errorf("Failed to delete container %q: %v", e.ID, err)
return
@ -119,7 +119,7 @@ func (c *criContainerdService) handleEvent(e *container.Event) {
// TODO(random-liu): [P0] Enqueue the event and retry.
return
}
case container.Event_OOM:
case task.Event_OOM:
// TODO(random-liu): [P1] Handle OOM event.
}
}

View File

@ -22,7 +22,7 @@ import (
"time"
"github.com/containerd/containerd/api/services/execution"
"github.com/containerd/containerd/api/types/container"
"github.com/containerd/containerd/api/types/task"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
@ -46,9 +46,9 @@ func TestHandleEvent(t *testing.T) {
StartedAt: testStartedAt,
}
testExitedAt := time.Now()
testExitEvent := container.Event{
testExitEvent := task.Event{
ID: testID,
Type: container.Event_EXIT,
Type: task.Event_EXIT,
Pid: testPid,
ExitStatus: 1,
ExitedAt: testExitedAt,
@ -64,16 +64,16 @@ func TestHandleEvent(t *testing.T) {
ExitCode: 1,
}
assert.Equal(t, runtime.ContainerState_CONTAINER_RUNNING, testMetadata.State())
testContainerdContainer := container.Container{
testContainerdContainer := task.Task{
ID: testID,
Pid: testPid,
Status: container.Status_RUNNING,
Status: task.StatusRunning,
}
for desc, test := range map[string]struct {
event *container.Event
event *task.Event
metadata *metadata.ContainerMetadata
containerdContainer *container.Container
containerdContainer *task.Task
containerdErr error
expected *metadata.ContainerMetadata
}{
@ -82,9 +82,9 @@ func TestHandleEvent(t *testing.T) {
expected: nil,
},
"should not update state when exited process is not init process": {
event: &container.Event{
event: &task.Event{
ID: testID,
Type: container.Event_EXIT,
Type: task.Event_EXIT,
Pid: 9999,
ExitStatus: 1,
ExitedAt: testExitedAt,
@ -93,7 +93,7 @@ func TestHandleEvent(t *testing.T) {
containerdContainer: &testContainerdContainer,
expected: &testMetadata,
},
"should not update state when fail to delete containerd container": {
"should not update state when fail to delete containerd task": {
event: &testExitEvent,
metadata: &testMetadata,
containerdContainer: &testContainerdContainer,
@ -101,9 +101,9 @@ func TestHandleEvent(t *testing.T) {
expected: &testMetadata,
},
"should not update state for non-exited events": {
event: &container.Event{
event: &task.Event{
ID: testID,
Type: container.Event_OOM,
Type: task.Event_OOM,
Pid: testPid,
ExitStatus: 1,
ExitedAt: testExitedAt,
@ -112,12 +112,12 @@ func TestHandleEvent(t *testing.T) {
containerdContainer: &testContainerdContainer,
expected: &testMetadata,
},
"should update state when containerd container is already deleted": {
"should update state when containerd task is already deleted": {
event: &testExitEvent,
metadata: &testMetadata,
expected: &testFinishedMetadata,
},
"should update state when delete containerd container successfully": {
"should update state when delete containerd task successfully": {
event: &testExitEvent,
metadata: &testMetadata,
containerdContainer: &testContainerdContainer,
@ -126,7 +126,7 @@ func TestHandleEvent(t *testing.T) {
} {
t.Logf("TestCase %q", desc)
c := newTestCRIContainerdService()
fake := c.containerService.(*servertesting.FakeExecutionClient)
fake := c.taskService.(*servertesting.FakeExecutionClient)
e, err := fake.Events(context.Background(), &execution.EventsRequest{})
assert.NoError(t, err)
fakeEvents := e.(*servertesting.EventClient)
@ -139,9 +139,9 @@ func TestHandleEvent(t *testing.T) {
// Make sure that original data will not be changed.
assert.NoError(t, c.containerStore.Create(*test.metadata))
}
// Inject containerd container.
// Inject containerd task.
if test.containerdContainer != nil {
fake.SetFakeContainers([]container.Container{*test.containerdContainer})
fake.SetFakeTasks([]task.Task{*test.containerdContainer})
}
// Inject containerd delete error.
if test.containerdErr != nil {

View File

@ -25,6 +25,7 @@ import (
"strings"
"syscall"
containerdmetadata "github.com/containerd/containerd/metadata"
"github.com/docker/distribution/reference"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/truncindex"
@ -33,13 +34,10 @@ import (
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/net/context"
"google.golang.org/grpc"
"github.com/containerd/containerd"
"github.com/containerd/containerd/images"
"google.golang.org/grpc/codes"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
"github.com/kubernetes-incubator/cri-containerd/pkg/metadata"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
)
const (
@ -219,11 +217,9 @@ func getPIDNamespace(pid uint32) string {
return fmt.Sprintf(pidNSFormat, pid)
}
// isContainerdContainerNotExistError checks whether a grpc error is containerd
// ErrContainerNotExist error.
// TODO(random-liu): Containerd should expose error better through api.
func isContainerdContainerNotExistError(grpcError error) bool {
return grpc.ErrorDesc(grpcError) == containerd.ErrContainerNotExist.Error()
// isContainerdGRPCNotFoundError checks whether a grpc error is not found error.
func isContainerdGRPCNotFoundError(grpcError error) bool {
return grpc.Code(grpcError) == codes.NotFound
}
// isRuncProcessAlreadyFinishedError checks whether a grpc error is a process already
@ -354,7 +350,7 @@ func (c *criContainerdService) localResolve(ctx context.Context, ref string) (*m
}
image, err := c.imageStoreService.Get(ctx, normalized.String())
if err != nil {
if images.IsNotFound(err) {
if containerdmetadata.IsNotFound(err) {
return nil, nil
}
return nil, fmt.Errorf("an error occurred when getting image %q from containerd image store: %v",

View File

@ -28,7 +28,7 @@ import (
containerdimages "github.com/containerd/containerd/images"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
rootfsservice "github.com/containerd/containerd/services/rootfs"
containerdrootfs "github.com/containerd/containerd/rootfs"
"github.com/golang/glog"
imagedigest "github.com/opencontainers/go-digest"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
@ -99,13 +99,11 @@ func (c *criContainerdService) PullImage(ctx context.Context, r *runtime.PullIma
// TODO(random-liu): [P1] Schema 1 image is not supported in containerd now, we need to support
// it for backward compatiblity.
cfgDigest, manifestDigest, err := c.pullImage(ctx, image)
// TODO(mikebrow): add truncIndex for image id
imageID, manifestDigest, err := c.pullImage(ctx, image)
if err != nil {
return nil, fmt.Errorf("failed to pull image %q: %v", image, err)
}
// Use config digest as imageID to conform to oci image spec.
// TODO(mikebrow): add truncIndex for image id
imageID := cfgDigest.String()
glog.V(4).Infof("Pulled image %q with image id %q, manifest digest %q", image, imageID, manifestDigest)
repoDigest, repoTag := getRepoDigestAndTag(namedRef, manifestDigest)
@ -187,7 +185,8 @@ func (r *resourceSet) all() map[string]struct{} {
// pullImage pulls image and returns image id (config digest) and manifest digest.
// The ref should be normalized image reference.
func (c *criContainerdService) pullImage(ctx context.Context, ref string) (
imagedigest.Digest, imagedigest.Digest, error) {
// TODO(random-liu): Replace with client.Pull.
string, imagedigest.Digest, error) {
// Resolve the image reference to get descriptor and fetcher.
resolver := docker.NewResolver(docker.ResolverOptions{
// TODO(random-liu): Add authentication by setting credentials.
@ -195,10 +194,14 @@ func (c *criContainerdService) pullImage(ctx context.Context, ref string) (
PlainHTTP: true,
Client: http.DefaultClient,
})
_, desc, fetcher, err := resolver.Resolve(ctx, ref)
_, desc, err := resolver.Resolve(ctx, ref)
if err != nil {
return "", "", fmt.Errorf("failed to resolve ref %q: %v", ref, err)
}
fetcher, err := resolver.Fetcher(ctx, ref)
if err != nil {
return "", "", fmt.Errorf("failed to get fetcher for ref %q: %v", ref, err)
}
// Currently, the resolved image name is the same with ref in docker resolver,
// but they may be different in the future.
// TODO(random-liu): Always resolve image reference and use resolved image name in
@ -208,15 +211,14 @@ func (c *criContainerdService) pullImage(ctx context.Context, ref string) (
// In the future, containerd will rely on the information in the image store to perform image
// garbage collection.
// For now, we simply use it to store and retrieve information required for pulling an image.
if putErr := c.imageStoreService.Put(ctx, ref, desc); putErr != nil {
if err = c.imageStoreService.Put(ctx, ref, desc); err != nil {
return "", "", fmt.Errorf("failed to put image %q desc %v into containerd image store: %v",
ref, desc, putErr)
ref, desc, err)
}
// TODO(random-liu): What if following operations fail? Do we need to do cleanup?
resources := newResourceSet()
// Do not cleanup if following operations fail so as to make resumable download possible.
glog.V(4).Infof("Start downloading resources for image %q", ref)
resources := newResourceSet()
// Fetch all image resources into content store.
// Dispatch a handler which will run a sequence of handlers to:
// 1) track all resources associated using a customized handler;
@ -246,6 +248,8 @@ func (c *criContainerdService) pullImage(ctx context.Context, ref string) (
}
glog.V(4).Infof("Finished downloading resources for image %q", ref)
// TODO(random-liu): Replace with image.Unpack.
// Unpack the image layers into snapshots.
image, err := c.imageStoreService.Get(ctx, ref)
if err != nil {
return "", "", fmt.Errorf("failed to get image %q from containerd image store: %v", ref, err)
@ -261,19 +265,40 @@ func (c *criContainerdService) pullImage(ctx context.Context, ref string) (
return "", "", fmt.Errorf("unmarshal blob to manifest failed for manifest digest %q: %v",
manifestDigest, err)
}
// Unpack the image layers into snapshots.
rootfsUnpacker := rootfsservice.NewUnpackerFromClient(c.rootfsService)
if _, err = rootfsUnpacker.Unpack(ctx, manifest.Layers); err != nil {
return "", "", fmt.Errorf("unpack failed for manifest layers %+v: %v", manifest.Layers, err)
diffIDs, err := image.RootFS(ctx, c.contentStoreService)
if err != nil {
return "", "", err
}
if len(diffIDs) != len(manifest.Layers) {
return "", "", fmt.Errorf("mismatched image rootfs and manifest layers")
}
layers := make([]containerdrootfs.Layer, len(diffIDs))
for i := range diffIDs {
layers[i].Diff = imagespec.Descriptor{
// TODO: derive media type from compressed type
MediaType: imagespec.MediaTypeImageLayer,
Digest: diffIDs[i],
}
layers[i].Blob = manifest.Layers[i]
}
if _, err = containerdrootfs.ApplyLayers(ctx, layers, c.snapshotService, c.diffService); err != nil {
return "", "", fmt.Errorf("failed to apply layers %+v: %v", layers, err)
}
// TODO(random-liu): Considering how to deal with the disk usage of content.
configDesc, err := image.Config(ctx, c.contentStoreService)
if err != nil {
return "", "", fmt.Errorf("failed to get config descriptor for image %q: %v", ref, err)
}
return configDesc.Digest, manifestDigest, nil
// Use config digest as imageID to conform to oci image spec, and also add image id as
// image reference.
imageID := configDesc.Digest.String()
if err = c.imageStoreService.Put(ctx, imageID, desc); err != nil {
return "", "", fmt.Errorf("failed to put image id %q into containerd image store: %v",
imageID, err)
}
return imageID, manifestDigest, nil
}
// waitDownloadingPollInterval is the interval to check resource downloading progress.

View File

@ -19,11 +19,12 @@ package server
import (
"fmt"
"github.com/containerd/containerd/images"
"github.com/golang/glog"
"golang.org/x/net/context"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
containerdmetadata "github.com/containerd/containerd/metadata"
"github.com/kubernetes-incubator/cri-containerd/pkg/metadata"
)
@ -49,19 +50,17 @@ func (c *criContainerdService) RemoveImage(ctx context.Context, r *runtime.Remov
// return empty without error when image not found.
return &runtime.RemoveImageResponse{}, nil
}
// Also include repo digest, because if user pull image with digest,
// there will also be a corresponding repo digest reference.
for _, ref := range append(meta.RepoTags, meta.RepoDigests...) {
// Include all image references, including RepoTag, RepoDigest and id.
for _, ref := range append(append(meta.RepoTags, meta.RepoDigests...), meta.ID) {
// TODO(random-liu): Containerd should schedule a garbage collection immediately,
// and we may want to wait for the garbage collection to be over here.
err = c.imageStoreService.Delete(ctx, ref)
if err == nil || images.IsNotFound(err) {
if err == nil || containerdmetadata.IsNotFound(err) {
continue
}
return nil, fmt.Errorf("failed to delete image reference %q for image %q: %v", ref, meta.ID, err)
}
err = c.imageMetadataStore.Delete(meta.ID)
if err != nil {
if err = c.imageMetadataStore.Delete(meta.ID); err != nil {
if metadata.IsNotExistError(err) {
return &runtime.RemoveImageResponse{}, nil
}

View File

@ -24,7 +24,7 @@ import (
"github.com/containerd/containerd/api/services/execution"
"github.com/containerd/containerd/api/types/container"
"github.com/containerd/containerd/api/types/task"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
"github.com/kubernetes-incubator/cri-containerd/pkg/metadata"
@ -45,15 +45,15 @@ func (c *criContainerdService) ListPodSandbox(ctx context.Context, r *runtime.Li
return nil, fmt.Errorf("failed to list metadata from sandbox store: %v", err)
}
resp, err := c.containerService.List(ctx, &execution.ListRequest{})
resp, err := c.taskService.List(ctx, &execution.ListRequest{})
if err != nil {
return nil, fmt.Errorf("failed to list sandbox containers: %v", err)
}
sandboxesInContainerd := resp.Containers
sandboxesInContainerd := resp.Tasks
var sandboxes []*runtime.PodSandbox
for _, sandboxInStore := range sandboxesInStore {
var sandboxInContainerd *container.Container
var sandboxInContainerd *task.Task
for _, s := range sandboxesInContainerd {
if s.ID == sandboxInStore.ID {
sandboxInContainerd = s
@ -64,7 +64,7 @@ func (c *criContainerdService) ListPodSandbox(ctx context.Context, r *runtime.Li
// Set sandbox state to NOTREADY by default.
state := runtime.PodSandboxState_SANDBOX_NOTREADY
// If the sandbox container is running, return the sandbox as READY.
if sandboxInContainerd != nil && sandboxInContainerd.Status == container.Status_RUNNING {
if sandboxInContainerd != nil && sandboxInContainerd.Status == task.StatusRunning {
state = runtime.PodSandboxState_SANDBOX_READY
}

View File

@ -23,7 +23,7 @@ import (
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
"github.com/containerd/containerd/api/types/container"
"github.com/containerd/containerd/api/types/task"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
@ -135,7 +135,7 @@ func TestFilterSandboxes(t *testing.T) {
func TestListPodSandbox(t *testing.T) {
c := newTestCRIContainerdService()
fake := c.containerService.(*servertesting.FakeExecutionClient)
fake := c.taskService.(*servertesting.FakeExecutionClient)
sandboxesInStore := []metadata.SandboxMetadata{
{
@ -154,24 +154,24 @@ func TestListPodSandbox(t *testing.T) {
Config: &runtime.PodSandboxConfig{Metadata: &runtime.PodSandboxMetadata{Name: "name-3"}},
},
}
sandboxesInContainerd := []container.Container{
sandboxesInContainerd := []task.Task{
// Running container with corresponding metadata
{
ID: "1",
Pid: 1,
Status: container.Status_RUNNING,
Status: task.StatusRunning,
},
// Stopped container with corresponding metadata
{
ID: "2",
Pid: 2,
Status: container.Status_STOPPED,
Status: task.StatusStopped,
},
// Container without corresponding metadata
{
ID: "4",
Pid: 4,
Status: container.Status_STOPPED,
Status: task.StatusStopped,
},
}
expect := []*runtime.PodSandbox{
@ -197,8 +197,8 @@ func TestListPodSandbox(t *testing.T) {
c.sandboxStore.Create(s)
}
// Inject fake containerd containers
fake.SetFakeContainers(sandboxesInContainerd)
// Inject fake containerd tasks
fake.SetFakeTasks(sandboxesInContainerd)
resp, err := c.ListPodSandbox(context.Background(), &runtime.ListPodSandboxRequest{})
assert.NoError(t, err)

View File

@ -19,11 +19,11 @@ package server
import (
"fmt"
"github.com/containerd/containerd/api/services/containers"
"github.com/containerd/containerd/api/services/execution"
"github.com/containerd/containerd/snapshot"
"github.com/golang/glog"
"golang.org/x/net/context"
"github.com/containerd/containerd/api/services/execution"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
"github.com/kubernetes-incubator/cri-containerd/pkg/metadata"
@ -57,15 +57,22 @@ func (c *criContainerdService) RemovePodSandbox(ctx context.Context, r *runtime.
// Return error if sandbox container is not fully stopped.
// TODO(random-liu): [P0] Make sure network is torn down, may need to introduce a state.
_, err = c.containerService.Info(ctx, &execution.InfoRequest{ID: id})
if err != nil && !isContainerdContainerNotExistError(err) {
_, err = c.taskService.Info(ctx, &execution.InfoRequest{ContainerID: id})
if err != nil && !isContainerdGRPCNotFoundError(err) {
return nil, fmt.Errorf("failed to get sandbox container info for %q: %v", id, err)
}
if err == nil {
return nil, fmt.Errorf("sandbox container %q is not fully stopped", id)
}
// TODO(random-liu): [P0] Cleanup snapshot after switching to new snapshot api.
// Remove sandbox container snapshot.
if err := c.snapshotService.Remove(ctx, id); err != nil {
if !snapshot.IsNotExist(err) {
return nil, fmt.Errorf("failed to remove sandbox container snapshot %q: %v", id, err)
}
glog.V(5).Infof("Remove called for snapshot %q that does not exist", id)
}
// TODO(random-liu): [P0] Cleanup shm created in RunPodSandbox.
// TODO(random-liu): [P1] Remove permanent namespace once used.
@ -76,6 +83,14 @@ func (c *criContainerdService) RemovePodSandbox(ctx context.Context, r *runtime.
sandboxRootDir, err)
}
// Delete sandbox container.
if _, err := c.containerService.Delete(ctx, &containers.DeleteContainerRequest{ID: id}); err != nil {
if !isContainerdGRPCNotFoundError(err) {
return nil, fmt.Errorf("failed to delete sandbox container %q: %v", id, err)
}
glog.V(5).Infof("Remove called for sandbox container %q that does not exist", id, err)
}
// Remove sandbox metadata from metadata store. Note that once the sandbox
// metadata is successfully deleted:
// 1) ListPodSandbox will not include this sandbox.

View File

@ -1,6 +1,4 @@
/*
Copyright 2017 The Kubernetes Authors.
/* 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
@ -20,16 +18,17 @@ import (
"fmt"
"testing"
"github.com/containerd/containerd/api/services/containers"
snapshotapi "github.com/containerd/containerd/api/services/snapshot"
"github.com/containerd/containerd/api/types/mount"
"github.com/containerd/containerd/api/types/task"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
"github.com/containerd/containerd/api/types/container"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
"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"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
)
func TestRemovePodSandbox(t *testing.T) {
@ -40,9 +39,11 @@ func TestRemovePodSandbox(t *testing.T) {
Name: testName,
}
for desc, test := range map[string]struct {
sandboxContainers []container.Container
sandboxTasks []task.Task
injectMetadata bool
injectContainerdErr error
removeSnapshotErr error
deleteContainerErr error
taskInfoErr error
injectFSErr error
expectErr bool
expectRemoved string
@ -50,18 +51,32 @@ func TestRemovePodSandbox(t *testing.T) {
}{
"should not return error if sandbox does not exist": {
injectMetadata: false,
removeSnapshotErr: servertesting.SnapshotNotExistError,
deleteContainerErr: servertesting.ContainerNotExistError,
expectErr: false,
expectCalls: []string{},
},
"should return error when sandbox container is not deleted": {
"should not return error if snapshot does not exist": {
injectMetadata: true,
sandboxContainers: []container.Container{{ID: testID}},
removeSnapshotErr: servertesting.SnapshotNotExistError,
expectRemoved: getSandboxRootDir(testRootDir, testID),
expectCalls: []string{"info"},
},
"should return error if remove snapshot fails": {
injectMetadata: true,
removeSnapshotErr: fmt.Errorf("arbitrary error"),
expectErr: true,
expectCalls: []string{"info"},
},
"should return error when sandbox container task is not deleted": {
injectMetadata: true,
sandboxTasks: []task.Task{{ID: testID}},
expectErr: true,
expectCalls: []string{"info"},
},
"should return error when arbitrary containerd error is injected": {
injectMetadata: true,
injectContainerdErr: fmt.Errorf("arbitrary error"),
taskInfoErr: fmt.Errorf("arbitrary error"),
expectErr: true,
expectCalls: []string{"info"},
},
@ -72,6 +87,19 @@ func TestRemovePodSandbox(t *testing.T) {
expectErr: true,
expectCalls: []string{"info"},
},
"should not return error if sandbox container does not exist": {
injectMetadata: true,
deleteContainerErr: servertesting.ContainerNotExistError,
expectRemoved: getSandboxRootDir(testRootDir, testID),
expectCalls: []string{"info"},
},
"should return error if delete sandbox container fails": {
injectMetadata: true,
deleteContainerErr: fmt.Errorf("arbitrary error"),
expectRemoved: getSandboxRootDir(testRootDir, testID),
expectErr: true,
expectCalls: []string{"info"},
},
"should be able to successfully delete": {
injectMetadata: true,
expectRemoved: getSandboxRootDir(testRootDir, testID),
@ -80,16 +108,37 @@ func TestRemovePodSandbox(t *testing.T) {
} {
t.Logf("TestCase %q", desc)
c := newTestCRIContainerdService()
fake := c.containerService.(*servertesting.FakeExecutionClient)
fake := c.containerService.(*servertesting.FakeContainersClient)
fakeOS := c.os.(*ostesting.FakeOS)
fake.SetFakeContainers(test.sandboxContainers)
fakeExecutionClient := c.taskService.(*servertesting.FakeExecutionClient)
fakeSnapshotClient := WithFakeSnapshotClient(c)
fakeExecutionClient.SetFakeTasks(test.sandboxTasks)
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)
if test.removeSnapshotErr == nil {
fakeSnapshotClient.SetFakeMounts(testID, []*mount.Mount{
{
Type: "bind",
Source: "/test/source",
Target: "/test/target",
},
})
} else {
fakeSnapshotClient.InjectError("remove", test.removeSnapshotErr)
}
if test.deleteContainerErr == nil {
_, err := fake.Create(context.Background(), &containers.CreateContainerRequest{
Container: containers.Container{ID: testID},
})
assert.NoError(t, err)
} else {
fake.InjectError("delete", test.deleteContainerErr)
}
if test.taskInfoErr != nil {
fakeExecutionClient.InjectError("info", test.taskInfoErr)
}
fakeOS.RemoveAllFn = func(path string) error {
assert.Equal(t, test.expectRemoved, path)
@ -98,7 +147,7 @@ func TestRemovePodSandbox(t *testing.T) {
res, err := c.RemovePodSandbox(context.Background(), &runtime.RemovePodSandboxRequest{
PodSandboxId: testID,
})
assert.Equal(t, test.expectCalls, fake.GetCalledNames())
assert.Equal(t, test.expectCalls, fakeExecutionClient.GetCalledNames())
if test.expectErr {
assert.Error(t, err)
assert.Nil(t, res)
@ -114,6 +163,12 @@ func TestRemovePodSandbox(t *testing.T) {
assert.Error(t, err)
assert.True(t, metadata.IsNotExistError(err))
assert.Nil(t, meta, "sandbox metadata should be removed")
mountsResp, err := fakeSnapshotClient.Mounts(context.Background(), &snapshotapi.MountsRequest{Key: testID})
assert.Equal(t, servertesting.SnapshotNotExistError, err, "snapshot should be removed")
assert.Nil(t, mountsResp)
getResp, err := fake.Get(context.Background(), &containers.GetContainerRequest{ID: testID})
assert.Equal(t, servertesting.ContainerNotExistError, err, "containerd container should be removed")
assert.Nil(t, getResp)
res, err = c.RemovePodSandbox(context.Background(), &runtime.RemovePodSandboxRequest{
PodSandboxId: testID,
})

View File

@ -23,11 +23,11 @@ import (
"strings"
"time"
"github.com/containerd/containerd/api/services/containers"
"github.com/containerd/containerd/api/services/execution"
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
"github.com/containerd/containerd/api/types/mount"
prototypes "github.com/gogo/protobuf/types"
"github.com/golang/glog"
imagedigest "github.com/opencontainers/go-digest"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/runtime-tools/generate"
@ -87,17 +87,59 @@ func (c *criContainerdService) RunPodSandbox(ctx context.Context, r *runtime.Run
if err != nil {
return nil, fmt.Errorf("failed to get sandbox image %q: %v", defaultSandboxImage, err)
}
prepareResp, err := c.rootfsService.Prepare(ctx, &rootfsapi.PrepareRequest{
Name: id,
// We are sure that ChainID must be a digest.
ChainID: imagedigest.Digest(imageMeta.ChainID),
Readonly: true,
})
rootfsMounts, err := c.snapshotService.View(ctx, id, imageMeta.ChainID)
if err != nil {
return nil, fmt.Errorf("failed to prepare sandbox rootfs %q: %v", imageMeta.ChainID, err)
}
// TODO(random-liu): [P0] Cleanup snapshot on failure after switching to new rootfs api.
rootfsMounts := prepareResp.Mounts
defer func() {
if retErr != nil {
if err := c.snapshotService.Remove(ctx, id); err != nil {
glog.Errorf("Failed to remove sandbox container snapshot %q: %v", id, err)
}
}
}()
var rootfs []*mount.Mount
for _, m := range rootfsMounts {
rootfs = append(rootfs, &mount.Mount{
Type: m.Type,
Source: m.Source,
Options: m.Options,
})
}
// Create sandbox container.
spec, err := c.generateSandboxContainerSpec(id, config, imageMeta.Config)
if err != nil {
return nil, fmt.Errorf("failed to generate sandbox container spec: %v", err)
}
rawSpec, err := json.Marshal(spec)
if err != nil {
return nil, fmt.Errorf("failed to marshal oci spec %+v: %v", spec, err)
}
glog.V(4).Infof("Sandbox container spec: %+v", spec)
if _, err = c.containerService.Create(ctx, &containers.CreateContainerRequest{
Container: containers.Container{
ID: id,
// TODO(random-liu): Checkpoint metadata into container labels.
Image: imageMeta.ID,
Runtime: defaultRuntime,
Spec: &prototypes.Any{
TypeUrl: runtimespec.Version,
Value: rawSpec,
},
RootFS: id,
},
}); err != nil {
return nil, fmt.Errorf("failed to create containerd container: %v", err)
}
defer func() {
if retErr != nil {
if _, err := c.containerService.Delete(ctx, &containers.DeleteContainerRequest{ID: id}); err != nil {
glog.Errorf("Failed to delete containerd container%q: %v", id, err)
}
}
}()
// Create sandbox container root directory.
// Prepare streaming named pipe.
@ -145,33 +187,17 @@ func (c *criContainerdService) RunPodSandbox(ctx context.Context, r *runtime.Run
}
}()
// Start sandbox container.
spec, err := c.generateSandboxContainerSpec(id, config, imageMeta.Config)
if err != nil {
return nil, fmt.Errorf("failed to generate sandbox container spec: %v", err)
}
rawSpec, err := json.Marshal(spec)
if err != nil {
return nil, fmt.Errorf("failed to marshal oci spec %+v: %v", spec, err)
}
glog.V(4).Infof("Sandbox container spec: %+v", spec)
createOpts := &execution.CreateRequest{
ID: id,
Spec: &prototypes.Any{
TypeUrl: runtimespec.Version,
Value: rawSpec,
},
Rootfs: rootfsMounts,
Runtime: defaultRuntime,
ContainerID: id,
Rootfs: rootfs,
// No stdin for sandbox container.
Stdout: stdout,
Stderr: stderr,
}
// Create sandbox container in containerd.
// Create sandbox task in containerd.
glog.V(5).Infof("Create sandbox container (id=%q, name=%q) with options %+v.",
id, name, createOpts)
createResp, err := c.containerService.Create(ctx, createOpts)
createResp, err := c.taskService.Create(ctx, createOpts)
if err != nil {
return nil, fmt.Errorf("failed to create sandbox container %q: %v",
id, err)
@ -179,13 +205,14 @@ func (c *criContainerdService) RunPodSandbox(ctx context.Context, r *runtime.Run
defer func() {
if retErr != nil {
// Cleanup the sandbox container if an error is returned.
if _, err = c.containerService.Delete(ctx, &execution.DeleteRequest{ID: id}); err != nil {
if _, err = c.taskService.Delete(ctx, &execution.DeleteRequest{ContainerID: id}); err != nil {
glog.Errorf("Failed to delete sandbox container %q: %v",
id, err)
}
}
}()
meta.Pid = createResp.Pid
meta.NetNS = getNetworkNamespace(createResp.Pid)
if !config.GetLinux().GetSecurityContext().GetNamespaceOptions().GetHostNetwork() {
// Setup network for sandbox.
@ -205,7 +232,7 @@ func (c *criContainerdService) RunPodSandbox(ctx context.Context, r *runtime.Run
}
// Start sandbox container in containerd.
if _, err := c.containerService.Start(ctx, &execution.StartRequest{ID: id}); err != nil {
if _, err := c.taskService.Start(ctx, &execution.StartRequest{ContainerID: id}); err != nil {
return nil, fmt.Errorf("failed to start sandbox container %q: %v",
id, err)
}

View File

@ -23,9 +23,9 @@ import (
"syscall"
"testing"
"github.com/containerd/containerd/api/services/containers"
"github.com/containerd/containerd/api/services/execution"
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
imagedigest "github.com/opencontainers/go-digest"
snapshotapi "github.com/containerd/containerd/api/services/snapshot"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
@ -270,8 +270,9 @@ options timeout:1
func TestRunPodSandbox(t *testing.T) {
config, imageConfig, specCheck := getRunPodSandboxTestData()
c := newTestCRIContainerdService()
fakeRootfsClient := c.rootfsService.(*servertesting.FakeRootfsClient)
fakeExecutionClient := c.containerService.(*servertesting.FakeExecutionClient)
fake := c.containerService.(*servertesting.FakeContainersClient)
fakeSnapshotClient := WithFakeSnapshotClient(c)
fakeExecutionClient := c.taskService.(*servertesting.FakeExecutionClient)
fakeCNIPlugin := c.netPlugin.(*servertesting.FakeCNIPlugin)
fakeOS := c.os.(*ostesting.FakeOS)
var dirs []string
@ -286,17 +287,16 @@ func TestRunPodSandbox(t *testing.T) {
assert.Equal(t, os.FileMode(0700), perm)
return nopReadWriteCloser{}, nil
}
testChainID := imagedigest.Digest("test-sandbox-chain-id")
testChainID := "test-sandbox-chain-id"
imageMetadata := metadata.ImageMetadata{
ID: testSandboxImage,
ChainID: testChainID.String(),
ChainID: testChainID,
Config: imageConfig,
}
// Insert sandbox image metadata.
assert.NoError(t, c.imageMetadataStore.Create(imageMetadata))
// Insert fake chainID
fakeRootfsClient.SetFakeChainIDs([]imagedigest.Digest{testChainID})
expectRootfsClientCalls := []string{"prepare"}
expectContainersClientCalls := []string{"create"}
expectSnapshotClientCalls := []string{"view"}
expectExecutionClientCalls := []string{"create", "start"}
res, err := c.RunPodSandbox(context.Background(), &runtime.RunPodSandboxRequest{Config: config})
@ -312,30 +312,39 @@ func TestRunPodSandbox(t *testing.T) {
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, expectRootfsClientCalls, fakeRootfsClient.GetCalledNames(), "expect rootfs functions should be called")
calls := fakeRootfsClient.GetCalledDetails()
prepareOpts := calls[0].Argument.(*rootfsapi.PrepareRequest)
assert.Equal(t, &rootfsapi.PrepareRequest{
Name: id,
ChainID: testChainID,
Readonly: true,
assert.Equal(t, expectSnapshotClientCalls, fakeSnapshotClient.GetCalledNames(), "expect snapshot functions should be called")
calls := fakeSnapshotClient.GetCalledDetails()
prepareOpts := calls[0].Argument.(*snapshotapi.PrepareRequest)
assert.Equal(t, &snapshotapi.PrepareRequest{
Key: id,
Parent: testChainID,
}, prepareOpts, "prepare request should be correct")
assert.Equal(t, expectExecutionClientCalls, fakeExecutionClient.GetCalledNames(), "expect containerd functions should be called")
calls = fakeExecutionClient.GetCalledDetails()
createOpts := calls[0].Argument.(*execution.CreateRequest)
assert.Equal(t, id, createOpts.ID, "create id should be correct")
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")
mountsResp, err := fakeRootfsClient.Mounts(context.Background(), &rootfsapi.MountsRequest{Name: id})
assert.NoError(t, err)
assert.Equal(t, mountsResp.Mounts, createOpts.Rootfs, "rootfs mount should be correct")
assert.Equal(t, expectContainersClientCalls, fake.GetCalledNames(), "expect containers functions should be called")
calls = fake.GetCalledDetails()
createOpts, ok := calls[0].Argument.(*containers.CreateContainerRequest)
assert.True(t, ok, "should create sandbox container")
assert.Equal(t, id, createOpts.Container.ID, "container id should be correct")
assert.Equal(t, testSandboxImage, createOpts.Container.Image, "test image should be correct")
assert.Equal(t, id, createOpts.Container.RootFS, "rootfs should be correct")
spec := &runtimespec.Spec{}
assert.NoError(t, json.Unmarshal(createOpts.Spec.Value, spec))
assert.NoError(t, json.Unmarshal(createOpts.Container.Spec.Value, spec))
t.Logf("oci spec check")
specCheck(t, id, spec)
startID := calls[1].Argument.(*execution.StartRequest).ID
assert.Equal(t, expectExecutionClientCalls, fakeExecutionClient.GetCalledNames(), "expect execution functions should be called")
calls = fakeExecutionClient.GetCalledDetails()
createTaskOpts := calls[0].Argument.(*execution.CreateRequest)
mountsResp, err := fakeSnapshotClient.Mounts(context.Background(), &snapshotapi.MountsRequest{Key: id})
assert.NoError(t, err)
assert.Equal(t, &execution.CreateRequest{
ContainerID: id,
Rootfs: mountsResp.Mounts,
Stdout: stdout,
Stderr: stderr,
}, createTaskOpts, "create options should be correct")
startID := calls[1].Argument.(*execution.StartRequest).ContainerID
assert.Equal(t, id, startID, "start id should be correct")
meta, err := c.sandboxStore.Get(id)
@ -346,9 +355,9 @@ func TestRunPodSandbox(t *testing.T) {
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 := fakeExecutionClient.Info(context.Background(), &execution.InfoRequest{ID: id})
info, err := fakeExecutionClient.Info(context.Background(), &execution.InfoRequest{ContainerID: id})
assert.NoError(t, err)
pid := info.Pid
pid := info.Task.Pid
assert.Equal(t, meta.NetNS, getNetworkNamespace(pid), "metadata network namespace should be correct")
gotID, err := c.sandboxIDIndex.Get(id)
@ -359,7 +368,12 @@ func TestRunPodSandbox(t *testing.T) {
assert.Equal(t, expectedCNICalls, fakeCNIPlugin.GetCalledNames(), "expect SetUpPod should be called")
calls = fakeCNIPlugin.GetCalledDetails()
pluginArgument := calls[0].Argument.(servertesting.CNIPluginArgument)
expectedPluginArgument := servertesting.CNIPluginArgument{meta.NetNS, config.GetMetadata().GetNamespace(), config.GetMetadata().GetName(), id}
expectedPluginArgument := servertesting.CNIPluginArgument{
NetnsPath: meta.NetNS,
Namespace: config.GetMetadata().GetNamespace(),
Name: config.GetMetadata().GetName(),
ContainerID: id,
}
assert.Equal(t, expectedPluginArgument, pluginArgument, "SetUpPod should be called with correct arguments")
}

View File

@ -23,7 +23,7 @@ import (
"golang.org/x/net/context"
"github.com/containerd/containerd/api/services/execution"
"github.com/containerd/containerd/api/types/container"
"github.com/containerd/containerd/api/types/task"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
@ -47,15 +47,15 @@ func (c *criContainerdService) PodSandboxStatus(ctx context.Context, r *runtime.
// Use the full sandbox id.
id := sandbox.ID
info, err := c.containerService.Info(ctx, &execution.InfoRequest{ID: id})
if err != nil && !isContainerdContainerNotExistError(err) {
info, err := c.taskService.Info(ctx, &execution.InfoRequest{ContainerID: id})
if err != nil && !isContainerdGRPCNotFoundError(err) {
return nil, fmt.Errorf("failed to get sandbox container info for %q: %v", id, err)
}
// Set sandbox state to NOTREADY by default.
state := runtime.PodSandboxState_SANDBOX_NOTREADY
// If the sandbox container is running, treat it as READY.
if info != nil && info.Status == container.Status_RUNNING {
if info != nil && info.Task.Status == task.StatusRunning {
state = runtime.PodSandboxState_SANDBOX_READY
}

View File

@ -25,7 +25,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
"github.com/containerd/containerd/api/types/container"
"github.com/containerd/containerd/api/types/task"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
@ -95,7 +95,7 @@ func getSandboxStatusTestData() (*metadata.SandboxMetadata, *runtime.PodSandboxS
func TestPodSandboxStatus(t *testing.T) {
for desc, test := range map[string]struct {
sandboxContainers []container.Container
sandboxTasks []task.Task
injectMetadata bool
injectErr error
injectIP bool
@ -112,10 +112,10 @@ func TestPodSandboxStatus(t *testing.T) {
expectedCNICalls: []string{},
},
"sandbox status with running sandbox container": {
sandboxContainers: []container.Container{{
sandboxTasks: []task.Task{{
ID: sandboxStatusTestID,
Pid: 1,
Status: container.Status_RUNNING,
Status: task.StatusRunning,
}},
injectMetadata: true,
expectState: runtime.PodSandboxState_SANDBOX_READY,
@ -123,10 +123,10 @@ func TestPodSandboxStatus(t *testing.T) {
expectedCNICalls: []string{"GetContainerNetworkStatus"},
},
"sandbox status with stopped sandbox container": {
sandboxContainers: []container.Container{{
sandboxTasks: []task.Task{{
ID: sandboxStatusTestID,
Pid: 1,
Status: container.Status_STOPPED,
Status: task.StatusStopped,
}},
injectMetadata: true,
expectState: runtime.PodSandboxState_SANDBOX_NOTREADY,
@ -134,17 +134,17 @@ func TestPodSandboxStatus(t *testing.T) {
expectedCNICalls: []string{"GetContainerNetworkStatus"},
},
"sandbox status with non-existing sandbox container": {
sandboxContainers: []container.Container{},
sandboxTasks: []task.Task{},
injectMetadata: true,
expectState: runtime.PodSandboxState_SANDBOX_NOTREADY,
expectCalls: []string{"info"},
expectedCNICalls: []string{"GetContainerNetworkStatus"},
},
"sandbox status with arbitrary error": {
sandboxContainers: []container.Container{{
sandboxTasks: []task.Task{{
ID: sandboxStatusTestID,
Pid: 1,
Status: container.Status_RUNNING,
Status: task.StatusRunning,
}},
injectMetadata: true,
expectState: runtime.PodSandboxState_SANDBOX_READY,
@ -154,10 +154,10 @@ func TestPodSandboxStatus(t *testing.T) {
expectedCNICalls: []string{},
},
"sandbox status with IP address": {
sandboxContainers: []container.Container{{
sandboxTasks: []task.Task{{
ID: sandboxStatusTestID,
Pid: 1,
Status: container.Status_RUNNING,
Status: task.StatusRunning,
}},
injectMetadata: true,
expectState: runtime.PodSandboxState_SANDBOX_READY,
@ -166,10 +166,10 @@ func TestPodSandboxStatus(t *testing.T) {
expectedCNICalls: []string{"GetContainerNetworkStatus"},
},
"sandbox status with GetContainerNetworkStatus returns error": {
sandboxContainers: []container.Container{{
sandboxTasks: []task.Task{{
ID: sandboxStatusTestID,
Pid: 1,
Status: container.Status_RUNNING,
Status: task.StatusRunning,
}},
injectMetadata: true,
expectState: runtime.PodSandboxState_SANDBOX_READY,
@ -182,9 +182,9 @@ func TestPodSandboxStatus(t *testing.T) {
metadata, expect := getSandboxStatusTestData()
expect.Network.Ip = ""
c := newTestCRIContainerdService()
fake := c.containerService.(*servertesting.FakeExecutionClient)
fake := c.taskService.(*servertesting.FakeExecutionClient)
fakeCNIPlugin := c.netPlugin.(*servertesting.FakeCNIPlugin)
fake.SetFakeContainers(test.sandboxContainers)
fake.SetFakeTasks(test.sandboxTasks)
if test.injectMetadata {
assert.NoError(t, c.sandboxIDIndex.Add(metadata.ID))
assert.NoError(t, c.sandboxStore.Create(*metadata))

View File

@ -60,8 +60,8 @@ func (c *criContainerdService) StopPodSandbox(ctx context.Context, r *runtime.St
// TODO(random-liu): [P1] Handle sandbox container graceful deletion.
// Delete the sandbox container from containerd.
_, err = c.containerService.Delete(ctx, &execution.DeleteRequest{ID: id})
if err != nil && !isContainerdContainerNotExistError(err) {
_, err = c.taskService.Delete(ctx, &execution.DeleteRequest{ContainerID: id})
if err != nil && !isContainerdGRPCNotFoundError(err) {
return nil, fmt.Errorf("failed to delete sandbox container %q: %v", id, err)
}

View File

@ -21,14 +21,11 @@ import (
"os"
"testing"
"github.com/containerd/containerd/api/types/task"
"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"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
"github.com/kubernetes-incubator/cri-containerd/pkg/metadata"
@ -49,14 +46,14 @@ func TestStopPodSandbox(t *testing.T) {
}},
NetNS: "test-netns",
}
testContainer := container.Container{
testContainer := task.Task{
ID: testID,
Pid: 1,
Status: container.Status_RUNNING,
Status: task.StatusRunning,
}
for desc, test := range map[string]struct {
sandboxContainers []container.Container
sandboxTasks []task.Task
injectSandbox bool
injectErr error
injectStatErr error
@ -72,17 +69,17 @@ func TestStopPodSandbox(t *testing.T) {
expectedCNICalls: []string{},
},
"stop sandbox with sandbox container": {
sandboxContainers: []container.Container{testContainer},
sandboxTasks: []task.Task{testContainer},
injectSandbox: true,
expectErr: false,
expectCalls: []string{"delete"},
expectedCNICalls: []string{"TearDownPod"},
},
"stop sandbox with sandbox container not exist error": {
sandboxContainers: []container.Container{},
sandboxTasks: []task.Task{},
injectSandbox: true,
// Inject error to make sure fake execution client returns error.
injectErr: grpc.Errorf(codes.Unknown, containerd.ErrContainerNotExist.Error()),
injectErr: servertesting.TaskNotExistError,
expectErr: false,
expectCalls: []string{"delete"},
expectedCNICalls: []string{"TearDownPod"},
@ -95,7 +92,7 @@ func TestStopPodSandbox(t *testing.T) {
expectedCNICalls: []string{"TearDownPod"},
},
"stop sandbox with Stat returns arbitrary error": {
sandboxContainers: []container.Container{testContainer},
sandboxTasks: []task.Task{testContainer},
injectSandbox: true,
expectErr: true,
injectStatErr: errors.New("arbitrary error"),
@ -103,7 +100,7 @@ func TestStopPodSandbox(t *testing.T) {
expectedCNICalls: []string{},
},
"stop sandbox with Stat returns not exist error": {
sandboxContainers: []container.Container{testContainer},
sandboxTasks: []task.Task{testContainer},
injectSandbox: true,
expectErr: false,
expectCalls: []string{"delete"},
@ -111,7 +108,7 @@ func TestStopPodSandbox(t *testing.T) {
expectedCNICalls: []string{},
},
"stop sandbox with TearDownPod fails": {
sandboxContainers: []container.Container{testContainer},
sandboxTasks: []task.Task{testContainer},
injectSandbox: true,
expectErr: true,
expectedCNICalls: []string{"TearDownPod"},
@ -121,10 +118,10 @@ func TestStopPodSandbox(t *testing.T) {
} {
t.Logf("TestCase %q", desc)
c := newTestCRIContainerdService()
fake := c.containerService.(*servertesting.FakeExecutionClient)
fake := c.taskService.(*servertesting.FakeExecutionClient)
fakeCNIPlugin := c.netPlugin.(*servertesting.FakeCNIPlugin)
fakeOS := c.os.(*ostesting.FakeOS)
fake.SetFakeContainers(test.sandboxContainers)
fake.SetFakeTasks(test.sandboxTasks)
if test.injectSandbox {
assert.NoError(t, c.sandboxStore.Create(testSandbox))

View File

@ -19,15 +19,20 @@ package server
import (
"fmt"
"github.com/containerd/containerd/api/services/containers"
contentapi "github.com/containerd/containerd/api/services/content"
diffapi "github.com/containerd/containerd/api/services/diff"
"github.com/containerd/containerd/api/services/execution"
imagesapi "github.com/containerd/containerd/api/services/images"
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
snapshotapi "github.com/containerd/containerd/api/services/snapshot"
versionapi "github.com/containerd/containerd/api/services/version"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
contentservice "github.com/containerd/containerd/services/content"
diffservice "github.com/containerd/containerd/services/diff"
imagesservice "github.com/containerd/containerd/services/images"
snapshotservice "github.com/containerd/containerd/services/snapshot"
"github.com/containerd/containerd/snapshot"
"github.com/docker/docker/pkg/truncindex"
"github.com/kubernetes-incubator/cri-o/pkg/ocicni"
"google.golang.org/grpc"
@ -73,12 +78,16 @@ type criContainerdService struct {
// containerNameIndex stores all container names and make sure each
// name is unique.
containerNameIndex *registrar.Registrar
// containerService is containerd container service client.
containerService execution.ContainerServiceClient
// containerService is containerd tasks client.
containerService containers.ContainersClient
// taskService is containerd tasks client.
taskService execution.TasksClient
// contentStoreService is the containerd content service client.
contentStoreService content.Store
// rootfsService is the containerd rootfs service client.
rootfsService rootfsapi.RootFSClient
// snapshotService is the containerd snapshot service client.
snapshotService snapshot.Snapshotter
// diffService is the containerd diff service client.
diffService diffservice.DiffService
// imageStoreService is the containerd service to store and track
// image metadata.
imageStoreService images.Store
@ -108,10 +117,12 @@ func NewCRIContainerdService(conn *grpc.ClientConn, rootDir, networkPluginBinDir
sandboxIDIndex: truncindex.NewTruncIndex(nil),
// TODO(random-liu): Add container id index.
containerNameIndex: registrar.NewRegistrar(),
containerService: execution.NewContainerServiceClient(conn),
containerService: containers.NewContainersClient(conn),
taskService: execution.NewTasksClient(conn),
imageStoreService: imagesservice.NewStoreFromClient(imagesapi.NewImagesClient(conn)),
contentStoreService: contentservice.NewStoreFromClient(contentapi.NewContentClient(conn)),
rootfsService: rootfsapi.NewRootFSClient(conn),
snapshotService: snapshotservice.NewSnapshotterFromClient(snapshotapi.NewSnapshotClient(conn)),
diffService: diffservice.NewDiffServiceFromClient(diffapi.NewDiffClient(conn)),
versionService: versionapi.NewVersionClient(conn),
healthService: healthapi.NewHealthClient(conn),
agentFactory: agents.NewAgentFactory(),

View File

@ -22,10 +22,13 @@ import (
"testing"
"github.com/containerd/containerd/api/services/execution"
snapshotservice "github.com/containerd/containerd/services/snapshot"
"github.com/docker/docker/pkg/truncindex"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
"github.com/kubernetes-incubator/cri-containerd/pkg/metadata"
"github.com/kubernetes-incubator/cri-containerd/pkg/metadata/store"
@ -33,10 +36,6 @@ import (
"github.com/kubernetes-incubator/cri-containerd/pkg/registrar"
agentstesting "github.com/kubernetes-incubator/cri-containerd/pkg/server/agents/testing"
servertesting "github.com/kubernetes-incubator/cri-containerd/pkg/server/testing"
imagedigest "github.com/opencontainers/go-digest"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
)
type nopReadWriteCloser struct{}
@ -66,20 +65,27 @@ func newTestCRIContainerdService() *criContainerdService {
sandboxIDIndex: truncindex.NewTruncIndex(nil),
containerStore: metadata.NewContainerStore(store.NewMetadataStore()),
containerNameIndex: registrar.NewRegistrar(),
containerService: servertesting.NewFakeExecutionClient(),
rootfsService: servertesting.NewFakeRootfsClient(),
taskService: servertesting.NewFakeExecutionClient(),
containerService: servertesting.NewFakeContainersClient(),
netPlugin: servertesting.NewFakeCNIPlugin(),
agentFactory: agentstesting.NewFakeAgentFactory(),
}
}
// WithFakeSnapshotClient add and return fake snapshot client.
func WithFakeSnapshotClient(c *criContainerdService) *servertesting.FakeSnapshotClient {
fake := servertesting.NewFakeSnapshotClient()
c.snapshotService = snapshotservice.NewSnapshotterFromClient(fake)
return fake
}
// Test all sandbox operations.
func TestSandboxOperations(t *testing.T) {
c := newTestCRIContainerdService()
fake := c.containerService.(*servertesting.FakeExecutionClient)
fakeRootfsClient := c.rootfsService.(*servertesting.FakeRootfsClient)
fake := c.taskService.(*servertesting.FakeExecutionClient)
fakeOS := c.os.(*ostesting.FakeOS)
fakeCNIPlugin := c.netPlugin.(*servertesting.FakeCNIPlugin)
WithFakeSnapshotClient(c)
fakeOS.OpenFifoFn = func(ctx context.Context, fn string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
return nopReadWriteCloser{}, nil
}
@ -89,8 +95,6 @@ func TestSandboxOperations(t *testing.T) {
ChainID: "test-chain-id",
Config: &imagespec.ImageConfig{Entrypoint: []string{"/pause"}},
}))
// Insert fake chainID
fakeRootfsClient.SetFakeChainIDs([]imagedigest.Digest{imagedigest.Digest("test-chain-id")})
config := &runtime.PodSandboxConfig{
Metadata: &runtime.PodSandboxMetadata{
@ -112,8 +116,8 @@ func TestSandboxOperations(t *testing.T) {
id := runRes.GetPodSandboxId()
t.Logf("should be able to get pod sandbox status")
info, err := fake.Info(context.Background(), &execution.InfoRequest{ID: id})
netns := getNetworkNamespace(info.Pid)
info, err := fake.Info(context.Background(), &execution.InfoRequest{ContainerID: id})
netns := getNetworkNamespace(info.Task.Pid)
assert.NoError(t, err)
expectSandboxStatus := &runtime.PodSandboxStatus{
Id: id,
@ -146,7 +150,7 @@ func TestSandboxOperations(t *testing.T) {
expectSandbox := &runtime.PodSandbox{
Id: id,
Metadata: config.GetMetadata(),
State: runtime.PodSandboxState_SANDBOX_READY,
State: runtime.PodSandboxState_SANDBOX_NOTREADY, // TODO(mikebrow) converting to client... should this be ready?
Labels: config.GetLabels(),
Annotations: config.GetAnnotations(),
}

View File

@ -0,0 +1,181 @@
/*
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 testing
import (
"fmt"
"sync"
"github.com/containerd/containerd/api/services/containers"
googleprotobuf "github.com/golang/protobuf/ptypes/empty"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
// ContainerNotExistError is the fake error returned when container does not exist.
var ContainerNotExistError = grpc.Errorf(codes.NotFound, "container does not exist")
// FakeContainersClient is a simple fake containers client, so that cri-containerd
// can be run for testing without requiring a real containerd setup.
type FakeContainersClient struct {
sync.Mutex
called []CalledDetail
errors map[string]error
ContainerList map[string]containers.Container
}
var _ containers.ContainersClient = &FakeContainersClient{}
// NewFakeContainersClient creates a FakeContainersClient
func NewFakeContainersClient() *FakeContainersClient {
return &FakeContainersClient{
errors: make(map[string]error),
ContainerList: make(map[string]containers.Container),
}
}
func (f *FakeContainersClient) getError(op string) error {
err, ok := f.errors[op]
if ok {
delete(f.errors, op)
return err
}
return nil
}
// InjectError inject error for call
func (f *FakeContainersClient) InjectError(fn string, err error) {
f.Lock()
defer f.Unlock()
f.errors[fn] = err
}
// InjectErrors inject errors for calls
func (f *FakeContainersClient) InjectErrors(errs map[string]error) {
f.Lock()
defer f.Unlock()
for fn, err := range errs {
f.errors[fn] = err
}
}
// ClearErrors clear errors for call
func (f *FakeContainersClient) ClearErrors() {
f.Lock()
defer f.Unlock()
f.errors = make(map[string]error)
}
func (f *FakeContainersClient) appendCalled(name string, argument interface{}) {
call := CalledDetail{Name: name, Argument: argument}
f.called = append(f.called, call)
}
// GetCalledNames get names of call
func (f *FakeContainersClient) GetCalledNames() []string {
f.Lock()
defer f.Unlock()
names := []string{}
for _, detail := range f.called {
names = append(names, detail.Name)
}
return names
}
// ClearCalls clear all call detail.
func (f *FakeContainersClient) ClearCalls() {
f.Lock()
defer f.Unlock()
f.called = []CalledDetail{}
}
// GetCalledDetails get detail of each call.
func (f *FakeContainersClient) GetCalledDetails() []CalledDetail {
f.Lock()
defer f.Unlock()
// Copy the list and return.
return append([]CalledDetail{}, f.called...)
}
// Create is a test implementation of containers.Create.
func (f *FakeContainersClient) Create(ctx context.Context, createOpts *containers.CreateContainerRequest, opts ...grpc.CallOption) (*containers.CreateContainerResponse, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("create", createOpts)
if err := f.getError("create"); err != nil {
return nil, err
}
_, ok := f.ContainerList[createOpts.Container.ID]
if ok {
return nil, fmt.Errorf("container already exists")
}
f.ContainerList[createOpts.Container.ID] = createOpts.Container
return &containers.CreateContainerResponse{Container: createOpts.Container}, nil
}
// Delete is a test implementation of containers.Delete
func (f *FakeContainersClient) Delete(ctx context.Context, deleteOpts *containers.DeleteContainerRequest, opts ...grpc.CallOption) (*googleprotobuf.Empty, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("delete", deleteOpts)
if err := f.getError("delete"); err != nil {
return nil, err
}
_, ok := f.ContainerList[deleteOpts.ID]
if !ok {
return nil, ContainerNotExistError
}
delete(f.ContainerList, deleteOpts.ID)
return &googleprotobuf.Empty{}, nil
}
// Get is a test implementation of containers.Get
func (f *FakeContainersClient) Get(ctx context.Context, getOpts *containers.GetContainerRequest, opts ...grpc.CallOption) (*containers.GetContainerResponse, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("get", getOpts)
if err := f.getError("get"); err != nil {
return nil, err
}
c, ok := f.ContainerList[getOpts.ID]
if !ok {
return nil, ContainerNotExistError
}
return &containers.GetContainerResponse{Container: c}, nil
}
// List is a test implementation of containers.List
func (f *FakeContainersClient) List(ctx context.Context, listOpts *containers.ListContainersRequest, opts ...grpc.CallOption) (*containers.ListContainersResponse, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("list", listOpts)
if err := f.getError("list"); err != nil {
return nil, err
}
var cs []containers.Container
for _, c := range f.ContainerList {
cs = append(cs, c)
}
return &containers.ListContainersResponse{Containers: cs}, nil
}
// Update is a test implementation of containers.Update
func (f *FakeContainersClient) Update(ctx context.Context, updateOpts *containers.UpdateContainerRequest, opts ...grpc.CallOption) (*containers.UpdateContainerResponse, error) {
// TODO: implement Update()
return nil, nil
}

View File

@ -22,17 +22,16 @@ import (
"sync"
"time"
"github.com/containerd/containerd"
"github.com/containerd/containerd/api/services/execution"
"github.com/containerd/containerd/api/types/container"
"github.com/containerd/containerd/api/types/task"
googleprotobuf "github.com/golang/protobuf/ptypes/empty"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
// ContainerNotExistError is the fake error returned when container does not exist.
var ContainerNotExistError = grpc.Errorf(codes.Unknown, containerd.ErrContainerNotExist.Error())
// TaskNotExistError is the fake error returned when task does not exist.
var TaskNotExistError = grpc.Errorf(codes.NotFound, "task does not exist")
// CalledDetail is the struct contains called function name and arguments.
type CalledDetail struct {
@ -42,16 +41,16 @@ type CalledDetail struct {
Argument interface{}
}
var _ execution.ContainerService_EventsClient = &EventClient{}
var _ execution.Tasks_EventsClient = &EventClient{}
// EventClient is a test implementation of execution.ContainerService_EventsClient
// EventClient is a test implementation of execution.Tasks_EventsClient
type EventClient struct {
Events chan *container.Event
Events chan *task.Event
grpc.ClientStream
}
// Recv is a test implementation of Recv
func (cli *EventClient) Recv() (*container.Event, error) {
func (cli *EventClient) Recv() (*task.Event, error) {
event, ok := <-cli.Events
if !ok {
return nil, fmt.Errorf("event channel closed")
@ -65,18 +64,18 @@ type FakeExecutionClient struct {
sync.Mutex
called []CalledDetail
errors map[string]error
ContainerList map[string]container.Container
eventsQueue chan *container.Event
TaskList map[string]task.Task
eventsQueue chan *task.Event
eventClients []*EventClient
}
var _ execution.ContainerServiceClient = &FakeExecutionClient{}
var _ execution.TasksClient = &FakeExecutionClient{}
// NewFakeExecutionClient creates a FakeExecutionClient
func NewFakeExecutionClient() *FakeExecutionClient {
return &FakeExecutionClient{
errors: make(map[string]error),
ContainerList: make(map[string]container.Container),
TaskList: make(map[string]task.Task),
}
}
@ -95,7 +94,7 @@ func (f *FakeExecutionClient) Stop() {
// WithEvents setup events publisher for FakeExecutionClient
func (f *FakeExecutionClient) WithEvents() *FakeExecutionClient {
f.eventsQueue = make(chan *container.Event, 1024)
f.eventsQueue = make(chan *task.Event, 1024)
go func() {
for e := range f.eventsQueue {
f.Lock()
@ -146,7 +145,7 @@ func generatePid() uint32 {
return randPid
}
func (f *FakeExecutionClient) sendEvent(event *container.Event) {
func (f *FakeExecutionClient) sendEvent(event *task.Event) {
if f.eventsQueue != nil {
f.eventsQueue <- event
}
@ -183,12 +182,12 @@ func (f *FakeExecutionClient) GetCalledDetails() []CalledDetail {
return append([]CalledDetail{}, f.called...)
}
// SetFakeContainers injects fake containers.
func (f *FakeExecutionClient) SetFakeContainers(containers []container.Container) {
// SetFakeTasks injects fake tasks.
func (f *FakeExecutionClient) SetFakeTasks(tasks []task.Task) {
f.Lock()
defer f.Unlock()
for _, c := range containers {
f.ContainerList[c.ID] = c
for _, t := range tasks {
f.TaskList[t.ID] = t
}
}
@ -200,23 +199,23 @@ func (f *FakeExecutionClient) Create(ctx context.Context, createOpts *execution.
if err := f.getError("create"); err != nil {
return nil, err
}
_, ok := f.ContainerList[createOpts.ID]
_, ok := f.TaskList[createOpts.ContainerID]
if ok {
return nil, containerd.ErrContainerExists
return nil, fmt.Errorf("task already exists")
}
pid := generatePid()
f.ContainerList[createOpts.ID] = container.Container{
ID: createOpts.ID,
f.TaskList[createOpts.ContainerID] = task.Task{
ContainerID: createOpts.ContainerID,
Pid: pid,
Status: container.Status_CREATED,
Status: task.StatusCreated,
}
f.sendEvent(&container.Event{
ID: createOpts.ID,
Type: container.Event_CREATE,
f.sendEvent(&task.Event{
ID: createOpts.ContainerID,
Type: task.Event_CREATE,
Pid: pid,
})
return &execution.CreateResponse{
ID: createOpts.ID,
ContainerID: createOpts.ContainerID,
Pid: pid,
}, nil
}
@ -229,23 +228,23 @@ func (f *FakeExecutionClient) Start(ctx context.Context, startOpts *execution.St
if err := f.getError("start"); err != nil {
return nil, err
}
c, ok := f.ContainerList[startOpts.ID]
c, ok := f.TaskList[startOpts.ContainerID]
if !ok {
return nil, ContainerNotExistError
return nil, TaskNotExistError
}
f.sendEvent(&container.Event{
f.sendEvent(&task.Event{
ID: c.ID,
Type: container.Event_START,
Type: task.Event_START,
Pid: c.Pid,
})
switch c.Status {
case container.Status_CREATED:
c.Status = container.Status_RUNNING
f.ContainerList[startOpts.ID] = c
case task.StatusCreated:
c.Status = task.StatusRunning
f.TaskList[startOpts.ContainerID] = c
return &googleprotobuf.Empty{}, nil
case container.Status_STOPPED:
case task.StatusStopped:
return &googleprotobuf.Empty{}, fmt.Errorf("cannot start a container that has stopped")
case container.Status_RUNNING:
case task.StatusRunning:
return &googleprotobuf.Empty{}, fmt.Errorf("cannot start an already running container")
default:
return &googleprotobuf.Empty{}, fmt.Errorf("cannot start a container in the %s state", c.Status)
@ -260,32 +259,32 @@ func (f *FakeExecutionClient) Delete(ctx context.Context, deleteOpts *execution.
if err := f.getError("delete"); err != nil {
return nil, err
}
c, ok := f.ContainerList[deleteOpts.ID]
c, ok := f.TaskList[deleteOpts.ContainerID]
if !ok {
return nil, ContainerNotExistError
return nil, TaskNotExistError
}
delete(f.ContainerList, deleteOpts.ID)
f.sendEvent(&container.Event{
delete(f.TaskList, deleteOpts.ContainerID)
f.sendEvent(&task.Event{
ID: c.ID,
Type: container.Event_EXIT,
Type: task.Event_EXIT,
Pid: c.Pid,
})
return nil, nil
}
// Info is a test implementation of execution.Info
func (f *FakeExecutionClient) Info(ctx context.Context, infoOpts *execution.InfoRequest, opts ...grpc.CallOption) (*container.Container, error) {
func (f *FakeExecutionClient) Info(ctx context.Context, infoOpts *execution.InfoRequest, opts ...grpc.CallOption) (*execution.InfoResponse, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("info", infoOpts)
if err := f.getError("info"); err != nil {
return nil, err
}
c, ok := f.ContainerList[infoOpts.ID]
c, ok := f.TaskList[infoOpts.ContainerID]
if !ok {
return nil, ContainerNotExistError
return nil, TaskNotExistError
}
return &c, nil
return &execution.InfoResponse{Task: &c}, nil
}
// List is a test implementation of execution.List
@ -297,8 +296,8 @@ func (f *FakeExecutionClient) List(ctx context.Context, listOpts *execution.List
return nil, err
}
resp := &execution.ListResponse{}
for _, c := range f.ContainerList {
resp.Containers = append(resp.Containers, &container.Container{
for _, c := range f.TaskList {
resp.Tasks = append(resp.Tasks, &task.Task{
ID: c.ID,
Pid: c.Pid,
Status: c.Status,
@ -315,22 +314,22 @@ func (f *FakeExecutionClient) Kill(ctx context.Context, killOpts *execution.Kill
if err := f.getError("kill"); err != nil {
return nil, err
}
c, ok := f.ContainerList[killOpts.ID]
c, ok := f.TaskList[killOpts.ContainerID]
if !ok {
return nil, ContainerNotExistError
return nil, TaskNotExistError
}
c.Status = container.Status_STOPPED
f.ContainerList[killOpts.ID] = c
f.sendEvent(&container.Event{
c.Status = task.StatusStopped
f.TaskList[killOpts.ContainerID] = c
f.sendEvent(&task.Event{
ID: c.ID,
Type: container.Event_EXIT,
Type: task.Event_EXIT,
Pid: c.Pid,
})
return &googleprotobuf.Empty{}, nil
}
// Events is a test implementation of execution.Events
func (f *FakeExecutionClient) Events(ctx context.Context, eventsOpts *execution.EventsRequest, opts ...grpc.CallOption) (execution.ContainerService_EventsClient, error) {
func (f *FakeExecutionClient) Events(ctx context.Context, eventsOpts *execution.EventsRequest, opts ...grpc.CallOption) (execution.Tasks_EventsClient, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("events", eventsOpts)
@ -338,7 +337,7 @@ func (f *FakeExecutionClient) Events(ctx context.Context, eventsOpts *execution.
return nil, err
}
var client = &EventClient{
Events: make(chan *container.Event, 100),
Events: make(chan *task.Event, 100),
}
f.eventClients = append(f.eventClients, client)
return client, nil
@ -373,3 +372,15 @@ func (f *FakeExecutionClient) Resume(ctx context.Context, in *execution.ResumeRe
// TODO: implement Resume()
return nil, nil
}
// Checkpoint is a test implementation of execution.Checkpoint
func (f *FakeExecutionClient) Checkpoint(ctx context.Context, in *execution.CheckpointRequest, opts ...grpc.CallOption) (*execution.CheckpointResponse, error) {
// TODO: implement Checkpoint()
return nil, nil
}
// Processes is a test implementation of execution.Processes
func (f *FakeExecutionClient) Processes(ctx context.Context, in *execution.ProcessesRequest, opts ...grpc.CallOption) (*execution.ProcessesResponse, error) {
// TODO: implement Processes()
return nil, nil
}

View File

@ -1,197 +0,0 @@
/*
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 testing
import (
"fmt"
"sync"
"github.com/containerd/containerd/api/services/rootfs"
"github.com/containerd/containerd/api/types/descriptor"
"github.com/containerd/containerd/api/types/mount"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/identity"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
// FakeRootfsClient is a simple fake rootfs client, so that cri-containerd
// can be run for testing without requiring a real containerd setup.
type FakeRootfsClient struct {
sync.Mutex
called []CalledDetail
errors map[string]error
ChainIDList map[digest.Digest]struct{}
MountList map[string][]*mount.Mount
}
var _ rootfs.RootFSClient = &FakeRootfsClient{}
// NewFakeRootfsClient creates a FakeRootfsClient
func NewFakeRootfsClient() *FakeRootfsClient {
return &FakeRootfsClient{
errors: make(map[string]error),
ChainIDList: make(map[digest.Digest]struct{}),
MountList: make(map[string][]*mount.Mount),
}
}
func (f *FakeRootfsClient) getError(op string) error {
err, ok := f.errors[op]
if ok {
delete(f.errors, op)
return err
}
return nil
}
// InjectError inject error for call
func (f *FakeRootfsClient) InjectError(fn string, err error) {
f.Lock()
defer f.Unlock()
f.errors[fn] = err
}
// InjectErrors inject errors for calls
func (f *FakeRootfsClient) InjectErrors(errs map[string]error) {
f.Lock()
defer f.Unlock()
for fn, err := range errs {
f.errors[fn] = err
}
}
// ClearErrors clear errors for call
func (f *FakeRootfsClient) ClearErrors() {
f.Lock()
defer f.Unlock()
f.errors = make(map[string]error)
}
//For uncompressed layers, diffID and digest will be the same. For compressed
//layers, we can look up the diffID from the digest if we've already unpacked it.
//In the FakeRootfsClient, We just use layer digest as diffID.
func generateChainID(layers []*descriptor.Descriptor) digest.Digest {
var digests []digest.Digest
for _, layer := range layers {
digests = append(digests, layer.Digest)
}
parent := identity.ChainID(digests)
return parent
}
func (f *FakeRootfsClient) appendCalled(name string, argument interface{}) {
call := CalledDetail{Name: name, Argument: argument}
f.called = append(f.called, call)
}
// GetCalledNames get names of call
func (f *FakeRootfsClient) GetCalledNames() []string {
f.Lock()
defer f.Unlock()
names := []string{}
for _, detail := range f.called {
names = append(names, detail.Name)
}
return names
}
// GetCalledDetails get detail of each call.
func (f *FakeRootfsClient) GetCalledDetails() []CalledDetail {
f.Lock()
defer f.Unlock()
// Copy the list and return.
return append([]CalledDetail{}, f.called...)
}
// SetFakeChainIDs injects fake chainIDs.
func (f *FakeRootfsClient) SetFakeChainIDs(chainIDs []digest.Digest) {
f.Lock()
defer f.Unlock()
for _, c := range chainIDs {
f.ChainIDList[c] = struct{}{}
}
}
// SetFakeMounts injects fake mounts.
func (f *FakeRootfsClient) SetFakeMounts(name string, mounts []*mount.Mount) {
f.Lock()
defer f.Unlock()
f.MountList[name] = mounts
}
// Unpack is a test implementation of rootfs.Unpack
func (f *FakeRootfsClient) Unpack(ctx context.Context, unpackOpts *rootfs.UnpackRequest, opts ...grpc.CallOption) (*rootfs.UnpackResponse, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("unpack", unpackOpts)
if err := f.getError("unpack"); err != nil {
return nil, err
}
chainID := generateChainID(unpackOpts.Layers)
_, ok := f.ChainIDList[chainID]
if ok {
return nil, fmt.Errorf("already unpacked")
}
f.ChainIDList[chainID] = struct{}{}
return &rootfs.UnpackResponse{
ChainID: chainID,
}, nil
}
// Prepare is a test implementation of rootfs.Prepare
func (f *FakeRootfsClient) Prepare(ctx context.Context, prepareOpts *rootfs.PrepareRequest, opts ...grpc.CallOption) (*rootfs.MountResponse, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("prepare", prepareOpts)
if err := f.getError("prepare"); err != nil {
return nil, err
}
_, ok := f.ChainIDList[prepareOpts.ChainID]
if !ok {
return nil, fmt.Errorf("have not been unpacked")
}
_, ok = f.MountList[prepareOpts.Name]
if ok {
return nil, fmt.Errorf("mounts already exist")
}
f.MountList[prepareOpts.Name] = []*mount.Mount{{
Type: "bind",
Source: prepareOpts.Name,
// TODO(random-liu): Fake options based on Readonly option.
}}
return &rootfs.MountResponse{
Mounts: f.MountList[prepareOpts.Name],
}, nil
}
// Mounts is a test implementation of rootfs.Mounts
func (f *FakeRootfsClient) Mounts(ctx context.Context, mountsOpts *rootfs.MountsRequest, opts ...grpc.CallOption) (*rootfs.MountResponse, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("mounts", mountsOpts)
if err := f.getError("mounts"); err != nil {
return nil, err
}
mounts, ok := f.MountList[mountsOpts.Name]
if !ok {
return nil, fmt.Errorf("mounts not exist")
}
return &rootfs.MountResponse{
Mounts: mounts,
}, nil
}

View File

@ -0,0 +1,221 @@
/*
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 testing
import (
"fmt"
"sync"
"github.com/containerd/containerd/api/services/snapshot"
"github.com/containerd/containerd/api/types/mount"
google_protobuf1 "github.com/golang/protobuf/ptypes/empty"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
// SnapshotNotExistError is the fake error returned when snapshot does not exist.
var SnapshotNotExistError = grpc.Errorf(codes.NotFound, "snapshot does not exist")
// FakeSnapshotClient is a simple fake snapshot client, so that cri-containerd
// can be run for testing without requiring a real containerd setup.
type FakeSnapshotClient struct {
sync.Mutex
called []CalledDetail
errors map[string]error
MountList map[string][]*mount.Mount
}
var _ snapshot.SnapshotClient = &FakeSnapshotClient{}
// NewFakeSnapshotClient creates a FakeSnapshotClient
func NewFakeSnapshotClient() *FakeSnapshotClient {
return &FakeSnapshotClient{
errors: make(map[string]error),
MountList: make(map[string][]*mount.Mount),
}
}
func (f *FakeSnapshotClient) getError(op string) error {
err, ok := f.errors[op]
if ok {
delete(f.errors, op)
return err
}
return nil
}
// InjectError inject error for call
func (f *FakeSnapshotClient) InjectError(fn string, err error) {
f.Lock()
defer f.Unlock()
f.errors[fn] = err
}
// InjectErrors inject errors for calls
func (f *FakeSnapshotClient) InjectErrors(errs map[string]error) {
f.Lock()
defer f.Unlock()
for fn, err := range errs {
f.errors[fn] = err
}
}
// ClearErrors clear errors for call
func (f *FakeSnapshotClient) ClearErrors() {
f.Lock()
defer f.Unlock()
f.errors = make(map[string]error)
}
func (f *FakeSnapshotClient) appendCalled(name string, argument interface{}) {
call := CalledDetail{Name: name, Argument: argument}
f.called = append(f.called, call)
}
// GetCalledNames get names of call
func (f *FakeSnapshotClient) GetCalledNames() []string {
f.Lock()
defer f.Unlock()
names := []string{}
for _, detail := range f.called {
names = append(names, detail.Name)
}
return names
}
// GetCalledDetails get detail of each call.
func (f *FakeSnapshotClient) GetCalledDetails() []CalledDetail {
f.Lock()
defer f.Unlock()
// Copy the list and return.
return append([]CalledDetail{}, f.called...)
}
// SetFakeMounts injects fake mounts.
func (f *FakeSnapshotClient) SetFakeMounts(name string, mounts []*mount.Mount) {
f.Lock()
defer f.Unlock()
f.MountList[name] = mounts
}
// ListMounts lists all the fake mounts.
func (f *FakeSnapshotClient) ListMounts() [][]*mount.Mount {
f.Lock()
defer f.Unlock()
var ms [][]*mount.Mount
for _, m := range f.MountList {
ms = append(ms, m)
}
return ms
}
// Prepare is a test implementation of snapshot.Prepare
func (f *FakeSnapshotClient) Prepare(ctx context.Context, prepareOpts *snapshot.PrepareRequest, opts ...grpc.CallOption) (*snapshot.MountsResponse, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("prepare", prepareOpts)
if err := f.getError("prepare"); err != nil {
return nil, err
}
_, ok := f.MountList[prepareOpts.Key]
if ok {
return nil, fmt.Errorf("mounts already exist")
}
f.MountList[prepareOpts.Key] = []*mount.Mount{{
Type: "bind",
Source: prepareOpts.Key,
// TODO(random-liu): Fake options based on Readonly option.
}}
return &snapshot.MountsResponse{
Mounts: f.MountList[prepareOpts.Key],
}, nil
}
// Mounts is a test implementation of snapshot.Mounts
func (f *FakeSnapshotClient) Mounts(ctx context.Context, mountsOpts *snapshot.MountsRequest, opts ...grpc.CallOption) (*snapshot.MountsResponse, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("mounts", mountsOpts)
if err := f.getError("mounts"); err != nil {
return nil, err
}
mounts, ok := f.MountList[mountsOpts.Key]
if !ok {
return nil, SnapshotNotExistError
}
return &snapshot.MountsResponse{
Mounts: mounts,
}, nil
}
// Commit is a test implementation of snapshot.Commit
func (f *FakeSnapshotClient) Commit(ctx context.Context, in *snapshot.CommitRequest, opts ...grpc.CallOption) (*google_protobuf1.Empty, error) {
return nil, nil
}
// View is a test implementation of snapshot.View
func (f *FakeSnapshotClient) View(ctx context.Context, viewOpts *snapshot.PrepareRequest, opts ...grpc.CallOption) (*snapshot.MountsResponse, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("view", viewOpts)
if err := f.getError("view"); err != nil {
return nil, err
}
_, ok := f.MountList[viewOpts.Key]
if ok {
return nil, fmt.Errorf("mounts already exist")
}
f.MountList[viewOpts.Key] = []*mount.Mount{{
Type: "bind",
Source: viewOpts.Key,
// TODO(random-liu): Fake options based on Readonly option.
}}
return &snapshot.MountsResponse{
Mounts: f.MountList[viewOpts.Key],
}, nil
}
// Remove is a test implementation of snapshot.Remove
func (f *FakeSnapshotClient) Remove(ctx context.Context, removeOpts *snapshot.RemoveRequest, opts ...grpc.CallOption) (*google_protobuf1.Empty, error) {
f.Lock()
defer f.Unlock()
f.appendCalled("remove", removeOpts)
if err := f.getError("remove"); err != nil {
return nil, err
}
if _, ok := f.MountList[removeOpts.Key]; !ok {
return nil, SnapshotNotExistError
}
delete(f.MountList, removeOpts.Key)
return &google_protobuf1.Empty{}, nil
}
// Stat is a test implementation of snapshot.Stat
func (f *FakeSnapshotClient) Stat(ctx context.Context, in *snapshot.StatRequest, opts ...grpc.CallOption) (*snapshot.StatResponse, error) {
return nil, nil
}
// List is a test implementation of snapshot.List
func (f *FakeSnapshotClient) List(ctx context.Context, in *snapshot.ListRequest, opts ...grpc.CallOption) (snapshot.Snapshot_ListClient, error) {
return nil, nil
}
// Usage is a test implementation of snapshot.Usage
func (f *FakeSnapshotClient) Usage(ctx context.Context, in *snapshot.UsageRequest, opts ...grpc.CallOption) (*snapshot.UsageResponse, error) {
return nil, nil
}

View File

@ -1,3 +0,0 @@
/bin/
**/coverage.txt
**/coverage.out

View File

@ -1,40 +0,0 @@
dist: trusty
sudo: required
language: go
go:
- 1.7.x
- 1.8.x
- tip
go_import_path: github.com/containerd/containerd
addons:
apt:
packages:
- btrfs-tools
env:
- TRAVIS_GOOS=windows TRAVIS_CGO_ENABLED=1
- TRAVIS_GOOS=linux TRAVIS_CGO_ENABLED=1
install:
- if [ "$TRAVIS_GOOS" = "windows" ] ; then sudo apt-get install -y gcc-multilib gcc-mingw-w64; export CC=x86_64-w64-mingw32-gcc ; export CXX=x86_64-w64-mingw32-g++ ; fi
- wget https://github.com/google/protobuf/releases/download/v3.1.0/protoc-3.1.0-linux-x86_64.zip -O /tmp/protoc-3.1.0-linux-x86_64.zip
- unzip -o -d /tmp/protobuf /tmp/protoc-3.1.0-linux-x86_64.zip
- export PATH=$PATH:/tmp/protobuf/bin/
- go get -u github.com/vbatts/git-validation
script:
- export GOOS=$TRAVIS_GOOS
- export CGO_ENABLED=$TRAVIS_CGO_ENABLED
- GIT_CHECK_EXCLUDE="./vendor" TRAVIS_COMMIT_RANGE="${TRAVIS_COMMIT_RANGE/.../..}" make dco
- make fmt
- make vet
- make binaries
- if [ "$GOOS" != "windows" ]; then make coverage ; fi
- if [ "$GOOS" != "windows" ]; then sudo PATH=$PATH GOPATH=$GOPATH make root-coverage ; fi
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@ -1,115 +0,0 @@
# Contributing
Contributions should be made via pull requests. Pull requests will be reviewed
by one or more maintainers and merged when acceptable.
This project is in an early state, making the impact of contributions much
greater than at other stages. In this respect, it is important to consider any
changes or additions for their future impact more so than their current impact.
## Successful Changes
We ask that before contributing, please make the effort to coordinate with the
maintainers of the project before submitting large or high impact PRs. This
will prevent you from doing extra work that may or may not be merged.
PRs that are just submitted without any prior communication will likely be
summarily closed.
While pull requests are the methodology for submitting changes to code, changes
are much more likely to be accepted if they are accompanied by additional
engineering work. While we don't define this explicitly, most of these goals
are accomplished through communication of the design goals and subsequent
solutions. Often times, it helps to first state the problem before presenting
solutions.
Typically, the best methods of accomplishing this are to submit an issue,
stating the problem. This issue can include a problem statement and a
checklist with requirements. If solutions are proposed, alternatives should be
listed and eliminated. Even if the criteria for elimination of a solution is
frivolous, say so.
Larger changes typically work best with design documents, similar to those found
in `design/`. These are focused on providing context to the design at the time
the feature was conceived and can inform future documentation contributions.
## Commit Messages
There are times for one line commit messages and this is not one of them.
Commit messages should follow best practices, including explaining the context
of the problem and how it was solved, including in caveats or follow up changes
required. They should tell the story of the change and provide readers
understanding of what led to it.
If you're lost about what this even means, please see [How to Write a Git
Commit Message](http://chris.beams.io/posts/git-commit/) for a start.
In practice, the best approach to maintaining a nice commit message is to
leverage a `git add -p` and `git commit --amend` to formulate a solid
changeset. This allows one to piece together a change, as information becomes
available.
If you squash a series of commits, don't just submit that. Re-write the commit
message, as if the series of commits was a single stroke of brilliance.
That said, there is no requirement to have a single commit for a PR, as long as
each commit tells the story. For example, if there is a feature that requires a
package, it might make sense to have the package in a separate commit then have
a subsequent commit that uses it.
Remember, you're telling part of the story with the commit message. Don't make
your chapter weird.
## Sign your work
The sign-off is a simple line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are pretty simple: if you can certify
the below (from [developercertificate.org](http://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.

View File

@ -1,300 +0,0 @@
# containerd project maintainers file
#
# This file describes who runs the containerd project and how.
# This is a living document - if you see something out of date or missing,
# speak up!
#
# It is structured to be consumable by both humans and programs.
# To extract its contents programmatically, use any TOML-compliant
# parser.
[Rules]
[Rules.maintainers]
title = "What is a maintainer?"
text = """
There are different types of maintainers, with different responsibilities, but
all maintainers have 3 things in common:
1) They share responsibility in the project's success.
2) They have made a long-term, recurring time investment to improve the project.
3) They spend that time doing whatever needs to be done, not necessarily what
is the most interesting or fun.
Maintainers are often under-appreciated, because their work is harder to appreciate.
It's easy to appreciate a really cool and technically advanced feature. It's harder
to appreciate the absence of bugs, the slow but steady improvement in stability,
or the reliability of a release process. But those things distinguish a good
project from a great one.
"""
[Rules.adding-maintainers]
title = "How are maintainers added?"
text = """
Maintainers are first and foremost contributors that have shown they are
committed to the long term success of a project. Contributors wanting to become
maintainers are expected to be deeply involved in contributing code, pull
request review, and triage of issues in the project for more than three months.
Just contributing does not make you a maintainer, it is about building trust
with the current maintainers of the project and being a person that they can
depend on and trust to make decisions in the best interest of the project.
Periodically, the existing maintainers curate a list of contributors that have
shown regular activity on the project over the prior months. From this list,
maintainer candidates are selected and proposed on the maintainers mailing list.
After a candidate has been announced on the maintainers mailing list, the
existing maintainers are given five business days to discuss the candidate,
raise objections and cast their vote. Candidates must be approved by the BDFL
and at least 66% of the current maintainers by adding their vote on the mailing
list. Only maintainers of the repository that the candidate is proposed for are
allowed to vote. The BDFL's vote is mandatory.
If a candidate is approved, a maintainer will contact the candidate to invite
the candidate to open a pull request that adds the contributor to the
MAINTAINERS file. The candidate becomes a maintainer once the pull request is
merged.
"""
[Rules.adding-sub-projects]
title = "How are sub projects added?"
text = """
Similar to adding maintainers, new sub projects can be added to containerd
GitHub organization as long as they adhere to the CNCF
[charter](https://www.cncf.io/about/charter/) and mission. After a project
proposal has been announced on a public forum (GitHub issue or mailing list),
the existing maintainers are given five business days to discuss the new
project, raise objections and cast their vote. Projects must be approved by at
least 66% of the current maintainers by adding their vote.
If a project is approved, a maintainer will add the project to the containerd
GitHub organization, and make an announcement on a public forum.
"""
[Rules.stepping-down-policy]
title = "Stepping down policy"
text = """
Life priorities, interests, and passions can change. If you're a maintainer but
feel you must remove yourself from the list, inform other maintainers that you
intend to step down, and if possible, help find someone to pick up your work.
At the very least, ensure your work can be continued where you left off.
After you've informed other maintainers, create a pull request to remove
yourself from the MAINTAINERS file.
"""
[Rules.inactive-maintainers]
title = "Removal of inactive maintainers"
text = """
Similar to the procedure for adding new maintainers, existing maintainers can
be removed from the list if they do not show significant activity on the
project. Periodically, the maintainers review the list of maintainers and their
activity over the last three months.
If a maintainer has shown insufficient activity over this period, a neutral
person will contact the maintainer to ask if they want to continue being
a maintainer. If the maintainer decides to step down as a maintainer, they
open a pull request to be removed from the MAINTAINERS file.
If the maintainer wants to remain a maintainer, but is unable to perform the
required duties they can be removed with a vote by the BDFL and at least 66% of
the current maintainers. The BDFL's vote is mandatory. An e-mail is sent to the
mailing list, inviting maintainers of the project to vote. The voting period is
five business days. Issues related to a maintainer's performance should be
discussed with them among the other maintainers so that they are not surprised
by a pull request removing them.
"""
[Rules.bdfl]
title = "The Benevolent dictator for life (BDFL)"
text = """
containerd follows the timeless, highly efficient and totally unfair system
known as [Benevolent dictator for
life](https://en.wikipedia.org/wiki/Benevolent_Dictator_for_Life), with yours
truly, Solomon Hykes, in the role of BDFL. This means that all decisions are
made, by default, by Solomon. Since making every decision himself would be
highly un-scalable, in practice decisions are spread across multiple
maintainers.
Ideally, the BDFL role is like the Queen of England: awesome crown, but not an
actual operational role day-to-day. The real job of a BDFL is to NEVER GO AWAY.
Every other rule can change, perhaps drastically so, but the BDFL will always be
there, preserving the philosophy and principles of the project, and keeping
ultimate authority over its fate. This gives us great flexibility in
experimenting with various governance models, knowing that we can always press
the "reset" button without fear of fragmentation or deadlock. See the US
congress for a counter-example.
BDFL daily routine:
* Is the project governance stuck in a deadlock or irreversibly fragmented?
* If yes: refactor the project governance
* Are there issues or conflicts escalated by maintainers?
* If yes: resolve them
* Go back to polishing that crown.
"""
[Rules.decisions]
title = "How are decisions made?"
text = """
Short answer: EVERYTHING IS A PULL REQUEST.
containerd is an open-source project with an open design philosophy. This means
that the repository is the source of truth for EVERY aspect of the project,
including its philosophy, design, road map, and APIs. *If it's part of the
project, it's in the repo. If it's in the repo, it's part of the project.*
As a result, all decisions can be expressed as changes to the repository. An
implementation change is a change to the source code. An API change is a change
to the API specification. A philosophy change is a change to the philosophy
manifesto, and so on.
All decisions affecting containerd, big and small, follow the same 3 steps:
* Step 1: Open a pull request. Anyone can do this.
* Step 2: Discuss the pull request. Anyone can do this.
* Step 3: Merge or refuse the pull request. Who does this depends on the nature
of the pull request and which areas of the project it affects.
"""
[Rules.DCO]
title = "Helping contributors with the DCO"
text = """
The [DCO or `Sign your work`](
https://github.com/containerd/containerd/blob/master/CONTRIBUTING.md#sign-your-work)
requirement is not intended as a roadblock or speed bump.
Some containerd contributors are not as familiar with `git`, or have used a web
based editor, and thus asking them to `git commit --amend -s` is not the best
way forward.
In this case, maintainers can update the commits based on clause (c) of the DCO.
The most trivial way for a contributor to allow the maintainer to do this, is to
add a DCO signature in a pull requests's comment, or a maintainer can simply
note that the change is sufficiently trivial that it does not substantially
change the existing contribution - i.e., a spelling change.
When you add someone's DCO, please also add your own to keep a log.
"""
[Rules."no direct push"]
title = "I'm a maintainer. Should I make pull requests too?"
text = """
Yes. Nobody should ever push to master directly. All changes should be
made through a pull request.
"""
[Rules.meta]
title = "How is this process changed?"
text = "Just like everything else: by making a pull request :)"
# Current project roles
[Roles]
[Roles.bdfl]
person = "shykes"
[Roles."Chief Architect"]
person = "crosbymichael"
text = """
The chief architect is responsible for the overall integrity of the technical
architecture across all subsystems, and the consistency of APIs and UI.
Changes to UI, public APIs and overall architecture must be approved by the
chief architect.
"""
[Roles."Chief Maintainer"]
person = "crosbymichael"
text = """
The chief maintainer is responsible for all aspects of quality for the project
including code reviews, usability, stability, security, performance, etc. The
most important function of the chief maintainer is to lead by example. On the
first day of a new maintainer, the best advice should be "follow the C.M.'s
example and you'll be fine".
"""
# Current project organization
[Org]
[Org.Maintainers]
people = [
"crosbymichael",
"dqminh",
"dmcgowan",
"estesp",
"hqhq",
"jhowardmsft",
"mlaventure",
"stevvooe",
]
[People]
[People.crosbymichael]
Name = "Michael Crosby"
Email = "crosbymichael@gmail.com"
GitHub = "crosbymichael"
[People.dqminh]
Name = "Daniel, Dao Quang Minh"
Email = "dqminh89@gmail.com"
GitHub = "dqminh"
[people.dmcgowan]
Name = "Derek McGowan"
Email = "derek@mcgstyle.net"
GitHub = "dmcgowan"
[People.estesp]
Name = "Phil Estes"
Email = "estesp@linux.vnet.ibm.com"
GitHub = "estesp"
[People.hqhq]
Name = "Qiang Huang"
Email = "h.huangqiang@huawei.com"
GitHub = "hqhq"
[People.jhowardmsft]
Name = "John Howard"
Email = "jhoward@microsoft.com"
GitHub = "jhowardmsft"
[People.mlaventure]
Name = "Kenfe-Mickaël Laventure"
Email = "mickael.laventure@docker.com"
GitHub = "mlaventure"
[People.stevvooe]
Name = "Stephen Day"
Email = "stephen.day@docker.com"
GitHub = "stevvooe"

View File

@ -1,190 +0,0 @@
# Root directory of the project (absolute path).
ROOTDIR=$(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# Base path used to install.
DESTDIR=/usr/local
# Used to populate variables in version package.
VERSION=$(shell git describe --match 'v[0-9]*' --dirty='.m' --always)
REVISION=$(shell git rev-parse HEAD)$(shell if ! git diff --no-ext-diff --quiet --exit-code; then echo .m; fi)
PKG=github.com/containerd/containerd
ifneq "$(strip $(shell command -v go 2>/dev/null))" ""
GOOS ?= $(shell go env GOOS)
else
GOOS ?= $$GOOS
endif
WHALE = "🐳"
ONI = "👹"
ifeq ("$(OS)", "Windows_NT")
WHALE="+"
ONI="-"
endif
# Project packages.
PACKAGES=$(shell go list ./... | grep -v /vendor/)
INTEGRATION_PACKAGE=${PKG}/integration
SNAPSHOT_PACKAGES=$(shell go list ./snapshot/...)
# Project binaries.
COMMANDS=ctr containerd protoc-gen-gogoctrd dist ctrd-protobuild
ifeq ("$(GOOS)", "linux")
COMMANDS += containerd-shim
endif
BINARIES=$(addprefix bin/,$(COMMANDS))
ifeq ("$(GOOS)", "windows")
BINARY_SUFFIX=".exe"
endif
GO_LDFLAGS=-ldflags "-X $(PKG)/version.Version=$(VERSION) -X $(PKG)/version.Revision=$(REVISION) -X $(PKG)/version.Package=$(PKG)"
# Flags passed to `go test`
TESTFLAGS ?=-parallel 8 -race
.PHONY: clean all AUTHORS fmt vet lint dco build binaries test integration setup generate protos checkprotos coverage ci check help install uninstall vendor
.DEFAULT: default
all: binaries
check: fmt vet lint ineffassign ## run fmt, vet, lint, ineffassign
ci: check binaries checkprotos coverage coverage-integration ## to be used by the CI
AUTHORS: .mailmap .git/HEAD
git log --format='%aN <%aE>' | sort -fu > $@
setup: ## install dependencies
@echo "$(WHALE) $@"
# TODO(stevvooe): Install these from the vendor directory
@go get -u github.com/golang/lint/golint
#@go get -u github.com/kisielk/errcheck
@go get -u github.com/gordonklaus/ineffassign
generate: protos
@echo "$(WHALE) $@"
@PATH=${ROOTDIR}/bin:${PATH} go generate -x ${PACKAGES}
protos: bin/protoc-gen-gogoctrd bin/ctrd-protobuild ## generate protobuf
@echo "$(WHALE) $@"
@PATH=${ROOTDIR}/bin:${PATH} ctrd-protobuild ${PACKAGES}
checkprotos: protos ## check if protobufs needs to be generated again
@echo "$(WHALE) $@"
@test -z "$$(git status --short | grep ".pb.go" | tee /dev/stderr)" || \
((git diff | cat) && \
(echo "$(ONI) please run 'make generate' when making changes to proto files" && false))
# Depends on binaries because vet will silently fail if it can't load compiled
# imports
vet: binaries ## run go vet
@echo "$(WHALE) $@"
@test -z "$$(go vet ${PACKAGES} 2>&1 | grep -v 'constant [0-9]* not a string in call to Errorf' | grep -v 'unrecognized printf verb 'r'' | egrep -v '(timestamp_test.go|duration_test.go|fetch.go|exit status 1)' | tee /dev/stderr)"
fmt: ## run go fmt
@echo "$(WHALE) $@"
@test -z "$$(gofmt -s -l . | grep -v vendor/ | grep -v ".pb.go$$" | tee /dev/stderr)" || \
(echo "$(ONI) please format Go code with 'gofmt -s -w'" && false)
@test -z "$$(find . -path ./vendor -prune -o ! -name timestamp.proto ! -name duration.proto -name '*.proto' -type f -exec grep -Hn -e "^ " {} \; | tee /dev/stderr)" || \
(echo "$(ONI) please indent proto files with tabs only" && false)
@test -z "$$(find . -path ./vendor -prune -o -name '*.proto' -type f -exec grep -EHn "[_ ]id = " {} \; | grep -v gogoproto.customname | tee /dev/stderr)" || \
(echo "$(ONI) id fields in proto files must have a gogoproto.customname set" && false)
@test -z "$$(find . -path ./vendor -prune -o -name '*.proto' -type f -exec grep -Hn "Meta meta = " {} \; | grep -v '(gogoproto.nullable) = false' | tee /dev/stderr)" || \
(echo "$(ONI) meta fields in proto files must have option (gogoproto.nullable) = false" && false)
lint: ## run go lint
@echo "$(WHALE) $@"
@test -z "$$(golint ./... | grep -v vendor/ | grep -v ".pb.go:" | tee /dev/stderr)"
dco: ## dco check
@which git-validation > /dev/null 2>/dev/null || (echo "ERROR: git-validation not found" && false)
ifdef TRAVIS_COMMIT_RANGE
git-validation -q -run DCO,short-subject,dangling-whitespace
else
git-validation -v -run DCO,short-subject,dangling-whitespace -range $(EPOCH_TEST_COMMIT)..HEAD
endif
ineffassign: ## run ineffassign
@echo "$(WHALE) $@"
@test -z "$$(ineffassign . | grep -v vendor/ | grep -v ".pb.go:" | tee /dev/stderr)"
#errcheck: ## run go errcheck
# @echo "$(WHALE) $@"
# @test -z "$$(errcheck ./... | grep -v vendor/ | grep -v ".pb.go:" | tee /dev/stderr)"
build: ## build the go packages
@echo "$(WHALE) $@"
@go build -i -v ${GO_LDFLAGS} ${GO_GCFLAGS} ${PACKAGES}
test: ## run tests, except integration tests and tests that require root
@echo "$(WHALE) $@"
@go test ${TESTFLAGS} $(filter-out ${INTEGRATION_PACKAGE},${PACKAGES})
root-test: ## run tests, except integration tests
@echo "$(WHALE) $@"
@go test ${TESTFLAGS} ${SNAPSHOT_PACKAGES} -test.root
integration: ## run integration tests
@echo "$(WHALE) $@"
@go test ${TESTFLAGS} ${INTEGRATION_PACKAGE}
FORCE:
# Build a binary from a cmd.
bin/%: cmd/% FORCE
@test $$(go list) = "${PKG}" || \
(echo "$(ONI) Please correctly set up your Go build environment. This project must be located at <GOPATH>/src/${PKG}" && false)
@echo "$(WHALE) $@${BINARY_SUFFIX}"
@go build -i -o $@${BINARY_SUFFIX} ${GO_LDFLAGS} ${GO_GCFLAGS} ./$<
binaries: $(BINARIES) ## build binaries
@echo "$(WHALE) $@"
clean: ## clean up binaries
@echo "$(WHALE) $@"
@rm -f $(BINARIES)
install: ## install binaries
@echo "$(WHALE) $@ $(BINARIES)"
@mkdir -p $(DESTDIR)/bin
@install $(BINARIES) $(DESTDIR)/bin
uninstall:
@echo "$(WHALE) $@"
@rm -f $(addprefix $(DESTDIR)/bin/,$(notdir $(BINARIES)))
coverage: ## generate coverprofiles from the unit tests, except tests that require root
@echo "$(WHALE) $@"
@rm -f coverage.txt
( for pkg in $(filter-out ${INTEGRATION_PACKAGE},${PACKAGES}); do \
go test -i ${TESTFLAGS} -test.short -coverprofile=coverage.out -covermode=atomic $$pkg || exit; \
if [ -f profile.out ]; then \
cat profile.out >> coverage.txt; \
rm profile.out; \
fi; \
go test ${TESTFLAGS} -test.short -coverprofile=coverage.out -covermode=atomic $$pkg || exit; \
if [ -f profile.out ]; then \
cat profile.out >> coverage.txt; \
rm profile.out; \
fi; \
done )
root-coverage: ## generae coverage profiles for the unit tests
@echo "$(WHALE) $@"
@( for pkg in ${SNAPSHOT_PACKAGES}; do \
go test -i ${TESTFLAGS} -test.short -coverprofile="../../../$$pkg/coverage.txt" -covermode=atomic $$pkg -test.root || exit; \
go test ${TESTFLAGS} -test.short -coverprofile="../../../$$pkg/coverage.txt" -covermode=atomic $$pkg -test.root || exit; \
done )
coverage-integration: ## generate coverprofiles from the integration tests
@echo "$(WHALE) $@"
go test ${TESTFLAGS} -test.short -coverprofile="../../../${INTEGRATION_PACKAGE}/coverage.txt" -covermode=atomic ${INTEGRATION_PACKAGE}
vendor:
@echo "$(WHALE) $@"
@vndr
help: ## this help
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort

View File

@ -1,137 +0,0 @@
![banner](/docs/images/containerd-dark.png?raw=true)
[![Build Status](https://travis-ci.org/containerd/containerd.svg?branch=master)](https://travis-ci.org/containerd/containerd)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fcontainerd%2Fcontainerd.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fcontainerd%2Fcontainerd?ref=badge_shield)
containerd is an industry-standard container runtime with an emphasis on simplicity, robustness and portability. It is available as a daemon for Linux and Windows, which can manage the complete container lifecycle of its host system: image transfer and storage, container execution and supervision, low-level storage and network attachments, etc..
containerd is designed to be embedded into a larger system, rather than being used directly by developers or end-users.
### State of the Project
containerd currently has two active branches.
There is a [v0.2.x](https://github.com/containerd/containerd/tree/v0.2.x) branch for the current release of containerd that is being consumed by Docker and others and the master branch is the development branch for the 1.0 roadmap and feature set.
Any PR or issue that is intended for the current v0.2.x release should be tagged with the same `v0.2.x` tag.
### Communication
For async communication and long running discussions please use issues and pull requests on the github repo.
This will be the best place to discuss design and implementation.
For sync communication we have a community slack with a #containerd channel that everyone is welcome to join and chat about development.
**Slack:** https://dockr.ly/community
### Developer Quick-Start
To build the daemon and `ctr` simple test client, the following build system dependencies are required:
* Go 1.8.x or above (requires 1.8 due to use of golang plugin(s))
* Protoc 3.x compiler and headers (download at the [Google protobuf releases page](https://github.com/google/protobuf/releases))
For proper results, install the `protoc` release into `/usr/local` on your build system. For example, the following commands will download and install the 3.1.0 release for a 64-bit Linux host:
```
$ wget -c https://github.com/google/protobuf/releases/download/v3.1.0/protoc-3.1.0-linux-x86_64.zip
$ sudo unzip protoc-3.1.0-linux-x86_64.zip -d /usr/local
```
With the required dependencies installed, the `Makefile` target named **binaries** will compile the `ctr` and `containerd` binaries and place them in the `bin/` directory. Using `sudo make install` will place the binaries in `/usr/local/bin`. When making any changes to the gRPC API, `make generate` will use the installed `protoc` compiler to regenerate the API generated code packages.
Vendoring of external imports uses the [`vndr` tool](https://github.com/LK4D4/vndr) which uses a simple config file, `vendor.conf`, to provide the URL and version or hash details for each vendored import. After modifying `vendor.conf` run the `vndr` tool to update the `vendor/` directory contents. Combining the `vendor.conf` update with the changeset in `vendor/` after running `vndr` should become a single commit for a PR which relies on vendored updates.
Containerd will by default use `runc` found via the $PATH as the OCI-compliant runtime. You can specify the runtime directly with the `runtime` flag when starting containerd. However you specify the runtime, the expectation is that during the pre-release development cycle for containerd, the supported version of `runc` will track the current master branch of [`opencontainers/runc`](https://github.com/opencontainers/runc).
## Features
* OCI Image Spec support
* OCI Runtime Spec support
* Image push and pull support
* Container runtime and lifecycle support
* Management of network namespaces containers to join existing namespaces
* Multi-tenant supported with CAS storage for global images
## Scope and Principles
Having a clearly defined scope of a project is important for ensuring consistency and focus.
These following criteria will be used when reviewing pull requests, features, and changes for the project before being accepted.
### Components
Components should not have tight dependencies on each other so that they are able to be used independently.
The APIs for images and containers should be designed in a way that when used together the components have a natural flow but still be useful independently.
An example for this design can be seen with the overlay filesystems and the container execution layer.
The execution layer and overlay filesystems can be used independently but if you were to use both, they share a common `Mount` struct that the filesystems produce and the execution layer consumes.
### Primitives
containerd should expose primitives to solve problems instead of building high level abstractions in the API.
A common example of this is how build would be implemented.
Instead of having a build API in containerd we should expose the lower level primitives that allow things required in build to work.
Breaking up the filesystem APIs to allow snapshots, copy functionality, and mounts allow people implementing build at the higher levels more flexibility.
### Extensibility and Defaults
For the various components in containerd there should be defined extension points where implementations can be swapped for alternatives.
The best example of this is that containerd will use `runc` from OCI as the default runtime in the execution layer but other runtimes conforming to the OCI Runtime specification they can be easily added to containerd.
containerd will come with a default implementation for the various components.
These defaults will be chosen by the maintainers of the project and should not change unless better tech for that component comes out.
Additional implementations will not be accepted into the core repository and should be developed in a separate repository not maintained by the containerd maintainers.
### Releases
containerd will be released with a 1.0 when feature complete and this version will be supported for 1 year with security and bug fixes applied and released.
The upgrade path for containerd is that the 0.0.x patch releases are always backward compatible with its major and minor version.
Minor (0.x.0) version will always be compatible with the previous minor release. i.e. 1.2.0 is backwards compatible with 1.1.0 and 1.1.0 is compatible with 1.0.0.
There is no compatibility guarantees with upgrades from two minor releases. i.e. 1.0.0 to 1.2.0.
There are not backwards compatibility guarantees with upgrades to major versions. i.e 1.0.0 to 2.0.0.
Each major version will be supported for 1 year with bug fixes and security patches.
### Scope
The following table specifies the various components of containerd and general features of container runtimes.
The table specifies whether or not the feature/component is in or out of scope.
| Name | Description | In/Out | Reason |
|------------------------------|--------------------------------------------------------------------------------------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| execution | Provide an extensible execution layer for executing a container | in | Create,start, stop pause, resume exec, signal, delete |
| cow filesystem | Built in functionality for overlay, aufs, and other copy on write filesystems for containers | in | |
| distribution | Having the ability to push and pull images as well as operations on images as a first class API object | in | containerd will fully support the management and retrieval of images |
| metrics | container-level metrics, cgroup stats, and OOM events | in |
| networking | creation and management of network interfaces | out | Networking will be handled and provided to containerd via higher level systems. |
| build | Building images as a first class API | out | Build is a higher level tooling feature and can be implemented in many different ways on top of containerd |
| volumes | Volume management for external data | out | The API supports mounts, binds, etc where all volumes type systems can be built on top of containerd. |
| logging | Persisting container logs | out | Logging can be build on top of containerd because the containers STDIO will be provided to the clients and they can persist any way they see fit. There is no io copying of container STDIO in containerd. |
containerd is scoped to a single host and makes assumptions based on that fact.
It can be used to build things like a node agent that launches containers but does not have any concepts of a distributed system.
containerd is designed to be embedded into a larger system, hence it only includes a barebone CLI (`ctr`) specifically for development and debugging purpose, with no mandate to be human-friendly, and no guarantee of interface stability over time.
Also things like service discovery are out of scope even though networking is in scope.
containerd should provide the primitives to create, add, remove, or manage network interfaces and network namespaces for a container but IP allocation, discovery, and DNS should be handled at higher layers.
### How is the scope changed?
The scope of this project is a whitelist.
If it's not mentioned as being in scope, it is out of scope.
For the scope of this project to change it requires a 100% vote from all maintainers of the project.
### Development reports.
Weekly summary on the progress and what is being worked on.
https://github.com/containerd/containerd/tree/master/reports
## Copyright and license
Copyright © 2016 Docker, Inc. All rights reserved, except as follows. Code
is released under the Apache 2.0 license. The README.md file, and files in the
"docs" folder are licensed under the Creative Commons Attribution 4.0
International License under the terms and conditions set forth in the file
"LICENSE.docs". You may obtain a duplicate copy of the same license, titled
CC-BY-SA-4.0, at http://creativecommons.org/licenses/by/4.0/.

View File

@ -1,78 +0,0 @@
# containerd roadmap
This is a high level roadmap for the project that outlines what is currently being worked on, what comes next, and where you can help.
For a more up to date look please review the milestones on [github](https://github.com/containerd/containerd/milestones).
The following are the different status the various phases of development can be in:
* Not Started - no work or thinking has been done towards the goal
* In Design - design work has started for the component and you can find design documents in the `design` folder
* In Progress - design has mostly finished and development has started
* Completed - the development work has been completed
* Stable - the apis for the phase are feature complete and considered stable
We would like to follow the roadmap and develop the components one by one to completion before starting the next phase. If PRs are opened for another phase before the previous phase has been completed they will be closed as we are not ready for them at that time.
## Phase 1
**Status:** In Progress
### GRPC API
**Documents:**
We are going from a top down design for filling out this missing pieces of containerd and design of the API.
### Design
**Documents:**
The high level design work is needed so that the architecture of containerd stays consistent throughout the development process.
### Build & Test Process
**Documents:**
We need to have a simple build and test process for new developers to bootstrap their environments.
Because containerd will be the base of many high level systems we need to have a simple build process that does
not require high level tooling.
## Phase 2
Phase 2 includes most of the design and development work for the execution and storage layers of containerd.
It will include porting over existing "graph drivers" from Docker Engine and finding a common model for representing snapshots for layered filesystems.
This will also include moving the existing execution code support OCI's Runtime Spec and the existing containerd execution code.
**Status:** In Design
### Runtime
The runtime layer is responsible for the creation of containers and their management, and supervision of the processes inside those containers.
### Storage
**Documents:** https://github.com/containerd/containerd/blob/master/design/snapshots.md
The current graph drivers were built when we only had overlay filesystems like aufs.
We forced the model to be designed around overlay filesystems and this introduced a lot of complexity for snapshotting graph drivers like btrfs and devicemapper thin-p.
Our current approach is to model our storage layer after snapshotting drivers instead of overlay drivers as we can get the same results and its cleaner and more robust to have an overlay filesytem model snapshots than it is to have a snapshot filesystem model overlay filesystems.
## Phase 3
This phase includes getting support for the OCI Image spec built into containerd.
**Status:** Not Started
### Distribution
## Phase 4
Phase 4 involves graduating to version 1.0, and shifting the focus from features to maintenance. Graduating to 1.0 implies:
- Completing all of the above phases.
- Covering the functionalities required by a majority of container-centric platforms.
- Offering feature parity, to the extent of technical possibilities, across Linux and Windows.
- Demonstrating that containerd fulfills the requirements of at least one higher-level platforms through its complete integration as an upstream.
**Status:** Not Started

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,116 @@
syntax = "proto3";
package containerd.v1;
import "gogoproto/gogo.proto";
import "google/protobuf/any.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "github.com/containerd/containerd/api/types/descriptor/descriptor.proto";
// Containers provides metadata storage for containers used in the execution
// service.
//
// The objects here provide an state-independent view of containers for use in
// management and resource pinning. From that perspective, contaienrs do not
// have a "state" but rather this is the set of resources that will be
// considered in use by the container.
//
// From the perspective of the execution service, these objects represent the
// base parameters for creating a container process.
//
// In general, when looking to add fields for this type, first ask yourself
// whether or not the function of the field has to do with runtime execution or
// is invariant of the runtime state of the container. If it has to do with
// runtime, or changes as the "container" is started and stops, it probably
// doesn't belong on this object.
service Containers {
rpc Get(GetContainerRequest) returns (GetContainerResponse);
rpc List(ListContainersRequest) returns (ListContainersResponse);
rpc Create(CreateContainerRequest) returns (CreateContainerResponse);
rpc Update(UpdateContainerRequest) returns (UpdateContainerResponse);
rpc Delete(DeleteContainerRequest) returns (google.protobuf.Empty);
}
message Container {
// ID is the user-specified identifier.
//
// This field may not be updated.
string id = 1;
// Labels provides an area to include arbitrary data on containers.
//
// Note that to add a new value to this field, read the existing set and
// include the entire result in the update call.
map<string, string> labels = 2;
// Image contains the reference of the image used to build the
// specification and snapshots for running this container.
//
// If this field is updated, the spec and rootfs needed to updated, as well.
string image = 3;
// Runtime specifies which runtime to use for executing this container.
string runtime = 4;
// Spec to be used when creating the container. This is runtime specific.
google.protobuf.Any spec = 6;
// RootFS specifies the snapshot key to use for the container's root
// filesystem. When starting a task from this container, a caller should
// look up the mounts from the snapshot service and include those on the
// task create request.
//
// Snapshots referenced in this field will not be garbage collected.
//
// This field may be updated.
string rootfs = 7 [(gogoproto.customname) = "RootFS"];
}
message GetContainerRequest {
string id = 1;
}
message GetContainerResponse {
Container container = 1 [(gogoproto.nullable) = false];
}
message ListContainersRequest {
string filter = 1; // TODO(stevvooe): Define a filtering syntax to make these queries.
}
message ListContainersResponse {
repeated Container containers = 1 [(gogoproto.nullable) = false];
}
message CreateContainerRequest {
Container container = 1 [(gogoproto.nullable) = false];
}
message CreateContainerResponse {
Container container = 1 [(gogoproto.nullable) = false];
}
// UpdateContainerRequest updates the metadata on one or more container.
//
// The operation should follow semantics described in
// https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/field-mask,
// unless otherwise qualified.
message UpdateContainerRequest {
// Container provides the target values, as declared by the mask, for the update.
//
// The ID field must be set.
Container container = 1 [(gogoproto.nullable) = false];
// UpdateMask specifies which fields to perform the update on. If empty,
// the operation applies to all fields.
google.protobuf.FieldMask update_mask = 2;
}
message UpdateContainerResponse {
Container container = 1 [(gogoproto.nullable) = false];
}
message DeleteContainerRequest {
string id = 1;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
syntax = "proto3";
package containerd.v1;
import "gogoproto/gogo.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "github.com/containerd/containerd/api/types/mount/mount.proto";
import "github.com/containerd/containerd/api/types/descriptor/descriptor.proto";
// Diff service creates and applies diffs
service Diff {
// Apply applies the content associated with the provided digests onto
// the provided mounts. Archive content will be extracted and
// decompressed if necessary.
rpc Apply(ApplyRequest) returns (ApplyResponse);
// Diff creates a diff between the given mounts and uploads the result
// to the content store.
rpc Diff(DiffRequest) returns (DiffResponse);
}
message ApplyRequest {
// Diff is the descriptor of the diff to be extracted
containerd.v1.types.Descriptor diff = 1;
repeated containerd.v1.types.Mount mounts = 2;
}
message ApplyResponse {
// Applied is the descriptor for the object which was applied.
// If the input was a compressed blob then the result will be
// the descriptor for the uncompressed blob.
containerd.v1.types.Descriptor applied = 1;
}
message DiffRequest {
// Left are the mounts which represent the older copy
// in which is the base of the computed changes.
repeated containerd.v1.types.Mount left = 1;
// Right are the mounts which represents the newer copy
// in which changes from the left were made into.
repeated containerd.v1.types.Mount right = 2;
// MediaType is the media type descriptor for the created diff
// object
string media_type = 3;
// Ref identifies the pre-commit content store object. This
// reference can be used to get the status from the content store.
string ref = 5;
}
message DiffResponse {
// Diff is the descriptor of the diff which can be applied
containerd.v1.types.Descriptor diff = 3;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,20 @@
syntax = "proto3";
package containerd.v1.services;
package containerd.v1.services.execution;
import "google/protobuf/empty.proto";
import "google/protobuf/any.proto";
import "gogoproto/gogo.proto";
import "github.com/containerd/containerd/api/types/mount/mount.proto";
import "github.com/containerd/containerd/api/types/container/container.proto";
import "github.com/containerd/containerd/api/types/descriptor/descriptor.proto";
import "github.com/containerd/containerd/api/types/task/task.proto";
import "google/protobuf/timestamp.proto";
service ContainerService {
service Tasks {
rpc Create(CreateRequest) returns (CreateResponse);
rpc Start(StartRequest) returns (google.protobuf.Empty);
rpc Delete(DeleteRequest) returns (DeleteResponse);
rpc Info(InfoRequest) returns (containerd.v1.types.Container);
rpc Info(InfoRequest) returns (InfoResponse);
rpc List(ListRequest) returns (ListResponse);
rpc Kill(KillRequest) returns (google.protobuf.Empty);
rpc Events(EventsRequest) returns (stream containerd.v1.types.Event);
@ -22,64 +23,102 @@ service ContainerService {
rpc CloseStdin(CloseStdinRequest) returns (google.protobuf.Empty);
rpc Pause(PauseRequest) returns (google.protobuf.Empty);
rpc Resume(ResumeRequest) returns (google.protobuf.Empty);
rpc Processes(ProcessesRequest) returns (ProcessesResponse);
rpc Checkpoint(CheckpointRequest) returns (CheckpointResponse);
}
message CreateRequest {
string id = 1 [(gogoproto.customname) = "ID"];
google.protobuf.Any spec = 2;
// ContainerID specifies the container to use for creating this task.
//
// The spec from the provided container id will be used to create the
// task associated with this container. Only one task can be run at a time
// per container.
//
// This should be created using the Containers service.
string container_id = 2;
// RootFS provides the pre-chroot mounts to perform in the shim before
// executing the container task.
//
// These are for mounts that cannot be performed in the user namespace.
// Typically, these mounts should be resolved from snapshots specified on
// the container object.
repeated containerd.v1.types.Mount rootfs = 3;
string runtime = 4;
string stdin = 5;
string stdout = 6;
string stderr = 7;
bool terminal = 8;
types.Descriptor checkpoint = 9;
}
message CreateResponse {
string id = 1 [(gogoproto.customname) = "ID"];
uint32 pid = 2;
// TODO(stevvooe): We no longer have an id for a task since they are bound
// to a single container. Although, we should represent each new task with
// an ID so one can differentiate between each instance of a container
// running.
//
// Hence, we are leaving this here and reserving the field number in case
// we need to move in this direction.
// string id = 1;
string container_id = 2;
uint32 pid = 3;
}
message StartRequest {
string id = 1 [(gogoproto.customname) = "ID"];
string container_id = 1;
}
message DeleteRequest {
string id = 1 [(gogoproto.customname) = "ID"];
string container_id = 1;
}
message DeleteResponse {
string id = 1 [(gogoproto.customname) = "ID"];
string container_id = 1;
uint32 exit_status = 2;
google.protobuf.Timestamp exited_at = 3 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false];
}
message InfoRequest {
string id = 1 [(gogoproto.customname) = "ID"];
string container_id = 1;
}
message InfoResponse {
types.Task task = 1;
}
message ListRequest {
}
message ListResponse {
repeated containerd.v1.types.Container containers = 1;
repeated containerd.v1.types.Task tasks = 1;
}
message KillRequest {
string id = 1 [(gogoproto.customname) = "ID"];
string container_id = 1;
uint32 signal = 2;
oneof pid_or_all {
bool all = 3;
uint32 pid = 4;
}
}
message EventsRequest {
}
message ExecRequest {
string id = 1 [(gogoproto.customname) = "ID"];
// ContainerID specifies the container in which to exec the process.
string container_id = 1;
bool terminal = 2;
string stdin = 3;
string stdout = 4;
string stderr = 5;
// Spec for starting a process in the target container.
//
// For runc, this is a process spec, for example.
google.protobuf.Any spec = 6;
}
@ -88,21 +127,44 @@ message ExecResponse {
}
message PtyRequest {
string id = 1 [(gogoproto.customname) = "ID"];
string container_id = 1;
uint32 pid = 2;
uint32 width = 3;
uint32 height = 4;
}
message CloseStdinRequest {
string id = 1 [(gogoproto.customname) = "ID"];
string container_id = 1;
uint32 pid = 2;
}
message PauseRequest {
string id = 1 [(gogoproto.customname) = "ID"];
string container_id = 1;
}
message ResumeRequest {
string id = 1 [(gogoproto.customname) = "ID"];
string container_id = 1;
}
message ProcessesRequest {
string container_id = 1;
}
message ProcessesResponse{
repeated containerd.v1.types.Process processes = 1;
}
message CheckpointRequest {
string container_id = 1;
bool allow_tcp = 2;
bool allow_unix_sockets = 3;
bool allow_terminal = 4;
bool file_locks = 5;
repeated string empty_namespaces = 6;
string parent_checkpoint = 7 [(gogoproto.customtype) = "github.com/opencontainers/go-digest.Digest", (gogoproto.nullable) = false];
bool exit = 8;
}
message CheckpointResponse {
repeated types.Descriptor descriptors = 1;
}

View File

@ -50,7 +50,8 @@ const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
type Image struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Target containerd_v1_types1.Descriptor `protobuf:"bytes,2,opt,name=target" json:"target"`
Labels string `protobuf:"bytes,2,opt,name=labels,proto3" json:"labels,omitempty"`
Target containerd_v1_types1.Descriptor `protobuf:"bytes,3,opt,name=target" json:"target"`
}
func (m *Image) Reset() { *m = Image{} }
@ -127,11 +128,11 @@ const _ = grpc.SupportPackageIsVersion4
type ImagesClient interface {
// Get returns an image by name.
Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error)
// List returns a list of all images known to containerd.
List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error)
// Put assigns the name to a given target image based on the provided
// image.
Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*google_protobuf1.Empty, error)
// List returns a list of all images known to containerd.
List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error)
// Delete deletes the image by name.
Delete(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*google_protobuf1.Empty, error)
}
@ -153,18 +154,18 @@ func (c *imagesClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.Cal
return out, nil
}
func (c *imagesClient) Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*google_protobuf1.Empty, error) {
out := new(google_protobuf1.Empty)
err := grpc.Invoke(ctx, "/containerd.v1.Images/Put", in, out, c.cc, opts...)
func (c *imagesClient) List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error) {
out := new(ListResponse)
err := grpc.Invoke(ctx, "/containerd.v1.Images/List", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *imagesClient) List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error) {
out := new(ListResponse)
err := grpc.Invoke(ctx, "/containerd.v1.Images/List", in, out, c.cc, opts...)
func (c *imagesClient) Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*google_protobuf1.Empty, error) {
out := new(google_protobuf1.Empty)
err := grpc.Invoke(ctx, "/containerd.v1.Images/Put", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
@ -185,11 +186,11 @@ func (c *imagesClient) Delete(ctx context.Context, in *DeleteRequest, opts ...gr
type ImagesServer interface {
// Get returns an image by name.
Get(context.Context, *GetRequest) (*GetResponse, error)
// List returns a list of all images known to containerd.
List(context.Context, *ListRequest) (*ListResponse, error)
// Put assigns the name to a given target image based on the provided
// image.
Put(context.Context, *PutRequest) (*google_protobuf1.Empty, error)
// List returns a list of all images known to containerd.
List(context.Context, *ListRequest) (*ListResponse, error)
// Delete deletes the image by name.
Delete(context.Context, *DeleteRequest) (*google_protobuf1.Empty, error)
}
@ -216,24 +217,6 @@ func _Images_Get_Handler(srv interface{}, ctx context.Context, dec func(interfac
return interceptor(ctx, in, info, handler)
}
func _Images_Put_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PutRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ImagesServer).Put(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.v1.Images/Put",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ImagesServer).Put(ctx, req.(*PutRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Images_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRequest)
if err := dec(in); err != nil {
@ -252,6 +235,24 @@ func _Images_List_Handler(srv interface{}, ctx context.Context, dec func(interfa
return interceptor(ctx, in, info, handler)
}
func _Images_Put_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PutRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ImagesServer).Put(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/containerd.v1.Images/Put",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ImagesServer).Put(ctx, req.(*PutRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Images_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteRequest)
if err := dec(in); err != nil {
@ -278,14 +279,14 @@ var _Images_serviceDesc = grpc.ServiceDesc{
MethodName: "Get",
Handler: _Images_Get_Handler,
},
{
MethodName: "Put",
Handler: _Images_Put_Handler,
},
{
MethodName: "List",
Handler: _Images_List_Handler,
},
{
MethodName: "Put",
Handler: _Images_Put_Handler,
},
{
MethodName: "Delete",
Handler: _Images_Delete_Handler,
@ -316,8 +317,14 @@ func (m *Image) MarshalTo(dAtA []byte) (int, error) {
i = encodeVarintImages(dAtA, i, uint64(len(m.Name)))
i += copy(dAtA[i:], m.Name)
}
if len(m.Labels) > 0 {
dAtA[i] = 0x12
i++
i = encodeVarintImages(dAtA, i, uint64(len(m.Labels)))
i += copy(dAtA[i:], m.Labels)
}
dAtA[i] = 0x1a
i++
i = encodeVarintImages(dAtA, i, uint64(m.Target.Size()))
n1, err := m.Target.MarshalTo(dAtA[i:])
if err != nil {
@ -511,6 +518,10 @@ func (m *Image) Size() (n int) {
if l > 0 {
n += 1 + l + sovImages(uint64(l))
}
l = len(m.Labels)
if l > 0 {
n += 1 + l + sovImages(uint64(l))
}
l = m.Target.Size()
n += 1 + l + sovImages(uint64(l))
return n
@ -591,6 +602,7 @@ func (this *Image) String() string {
}
s := strings.Join([]string{`&Image{`,
`Name:` + fmt.Sprintf("%v", this.Name) + `,`,
`Labels:` + fmt.Sprintf("%v", this.Labels) + `,`,
`Target:` + strings.Replace(strings.Replace(this.Target.String(), "Descriptor", "containerd_v1_types1.Descriptor", 1), `&`, ``, 1) + `,`,
`}`,
}, "")
@ -722,6 +734,35 @@ func (m *Image) Unmarshal(dAtA []byte) error {
m.Name = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Labels", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowImages
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthImages
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Labels = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 3:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Target", wireType)
}
@ -1334,32 +1375,32 @@ func init() {
}
var fileDescriptorImages = []byte{
// 419 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x52, 0x4d, 0x6f, 0xd3, 0x40,
0x10, 0xcd, 0x36, 0xa9, 0x25, 0xc6, 0xe4, 0xb2, 0xaa, 0x90, 0x71, 0x91, 0x6b, 0x99, 0x4b, 0xc5,
0x61, 0x0d, 0xe6, 0x02, 0x52, 0x29, 0x22, 0x2a, 0x54, 0x48, 0x1c, 0x2a, 0x1f, 0xb9, 0x39, 0xee,
0x60, 0x2c, 0xd5, 0x5e, 0xe3, 0x5d, 0x57, 0xca, 0x0d, 0xfe, 0x5d, 0x8e, 0x1c, 0x39, 0x21, 0xe2,
0x5f, 0x82, 0xbc, 0xbb, 0xf9, 0x32, 0x01, 0xd2, 0x4b, 0x32, 0xab, 0x79, 0xef, 0xcd, 0x7b, 0xe3,
0x81, 0x37, 0x59, 0x2e, 0x3f, 0x37, 0x53, 0x96, 0xf2, 0x22, 0x4c, 0x79, 0x29, 0x93, 0xbc, 0xc4,
0xfa, 0x7a, 0xb3, 0x4c, 0xaa, 0x3c, 0x14, 0x58, 0xdf, 0xe6, 0x29, 0x8a, 0x30, 0x2f, 0x92, 0x6c,
0xf5, 0xc7, 0xaa, 0x9a, 0x4b, 0x4e, 0xc7, 0x6b, 0x30, 0xbb, 0x7d, 0xe6, 0x1e, 0x65, 0x3c, 0xe3,
0xaa, 0x13, 0x76, 0x95, 0x06, 0xb9, 0xc7, 0x19, 0xe7, 0xd9, 0x0d, 0x86, 0xea, 0x35, 0x6d, 0x3e,
0x85, 0x58, 0x54, 0x72, 0x66, 0x9a, 0x67, 0x7b, 0x99, 0x90, 0xb3, 0x0a, 0x45, 0x58, 0xf0, 0xa6,
0x94, 0xfa, 0xd7, 0xb0, 0xdf, 0xdd, 0x81, 0x7d, 0x8d, 0x22, 0xad, 0xf3, 0x4a, 0xf2, 0x7a, 0xa3,
0xd4, 0x3a, 0xc1, 0x47, 0x38, 0x7c, 0xdf, 0xe5, 0xa2, 0x14, 0x46, 0x65, 0x52, 0xa0, 0x43, 0x7c,
0x72, 0x7a, 0x2f, 0x56, 0x35, 0x7d, 0x05, 0x96, 0x4c, 0xea, 0x0c, 0xa5, 0x73, 0xe0, 0x93, 0x53,
0x3b, 0x3a, 0x61, 0x5b, 0xa9, 0x99, 0x92, 0x67, 0x17, 0x2b, 0xcd, 0xc9, 0x68, 0xfe, 0xf3, 0x64,
0x10, 0x1b, 0x52, 0xe0, 0x03, 0x5c, 0xa2, 0x8c, 0xf1, 0x4b, 0x83, 0x42, 0xee, 0x1a, 0x10, 0xbc,
0x04, 0x5b, 0x21, 0x44, 0xc5, 0x4b, 0x81, 0xf4, 0x09, 0x1c, 0xaa, 0x25, 0x2b, 0x8c, 0x1d, 0x1d,
0xf5, 0xc6, 0x29, 0xa3, 0xb1, 0x86, 0x04, 0xe7, 0x00, 0x57, 0xcd, 0x4a, 0xfc, 0xe9, 0x1e, 0x4c,
0xe3, 0xce, 0xf0, 0xc7, 0x60, 0x7f, 0xc8, 0xc5, 0x52, 0x20, 0x98, 0xc0, 0x7d, 0xfd, 0x34, 0x56,
0x22, 0xb0, 0xf4, 0xf7, 0x76, 0x88, 0x3f, 0xfc, 0x8f, 0xa2, 0x41, 0x06, 0x8f, 0x61, 0x7c, 0x81,
0x37, 0x28, 0xf1, 0x1f, 0x91, 0xa3, 0x6f, 0x07, 0x60, 0x29, 0xb2, 0xa0, 0x67, 0x30, 0xbc, 0x44,
0x49, 0x1f, 0xf6, 0xa4, 0xd7, 0x3b, 0x73, 0xdd, 0x5d, 0x2d, 0xe3, 0xf0, 0x05, 0x0c, 0xaf, 0x9a,
0x3f, 0xd9, 0xeb, 0xa5, 0xb8, 0x0f, 0x98, 0xbe, 0x3f, 0xb6, 0xbc, 0x3f, 0xf6, 0xb6, 0xbb, 0x3f,
0xfa, 0x1a, 0x46, 0x5d, 0x56, 0xda, 0x57, 0xdf, 0xd8, 0x87, 0x7b, 0xbc, 0xb3, 0x67, 0x46, 0x9f,
0x83, 0xa5, 0x83, 0xd2, 0x47, 0x3d, 0xd8, 0x56, 0xfe, 0xbf, 0x19, 0x98, 0x38, 0xf3, 0x85, 0x37,
0xf8, 0xb1, 0xf0, 0x06, 0x5f, 0x5b, 0x8f, 0xcc, 0x5b, 0x8f, 0x7c, 0x6f, 0x3d, 0xf2, 0xab, 0xf5,
0xc8, 0xd4, 0x52, 0xc8, 0xe7, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0x45, 0x0e, 0x92, 0xb1, 0xa2,
0x03, 0x00, 0x00,
// 430 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x52, 0x41, 0x6f, 0x94, 0x40,
0x14, 0xde, 0xe9, 0x6e, 0x49, 0x7c, 0xb8, 0x97, 0x49, 0xd3, 0x20, 0x35, 0x94, 0xe0, 0xa5, 0xf1,
0x30, 0x28, 0x5e, 0x34, 0xa9, 0x35, 0x6e, 0xaa, 0x8d, 0x89, 0x87, 0x86, 0x7f, 0x00, 0xf4, 0x89,
0x24, 0xc0, 0x20, 0x33, 0x34, 0xe9, 0x4d, 0xff, 0xdd, 0x1e, 0x3d, 0x7a, 0x32, 0x96, 0x5f, 0x62,
0x98, 0x99, 0xee, 0x6e, 0x71, 0xd5, 0xf6, 0x02, 0xef, 0xcd, 0xfb, 0xbe, 0xef, 0xbd, 0xef, 0xe5,
0xc1, 0xdb, 0xbc, 0x90, 0x9f, 0xbb, 0x94, 0x65, 0xbc, 0x0a, 0x33, 0x5e, 0xcb, 0xa4, 0xa8, 0xb1,
0xbd, 0xd8, 0x0c, 0x93, 0xa6, 0x08, 0x05, 0xb6, 0x97, 0x45, 0x86, 0x22, 0x2c, 0xaa, 0x24, 0x5f,
0xfd, 0x58, 0xd3, 0x72, 0xc9, 0xe9, 0x7c, 0x0d, 0x66, 0x97, 0xcf, 0xdd, 0xbd, 0x9c, 0xe7, 0x5c,
0x55, 0xc2, 0x21, 0xd2, 0x20, 0xf7, 0x20, 0xe7, 0x3c, 0x2f, 0x31, 0x54, 0x59, 0xda, 0x7d, 0x0a,
0xb1, 0x6a, 0xe4, 0x95, 0x29, 0x1e, 0xdf, 0x69, 0x08, 0x79, 0xd5, 0xa0, 0x08, 0x2b, 0xde, 0xd5,
0x52, 0x7f, 0x0d, 0xfb, 0xfd, 0x3d, 0xd8, 0x17, 0x28, 0xb2, 0xb6, 0x68, 0x24, 0x6f, 0x37, 0x42,
0xad, 0x13, 0xb4, 0xb0, 0xfb, 0x61, 0xf0, 0x45, 0x29, 0xcc, 0xea, 0xa4, 0x42, 0x87, 0xf8, 0xe4,
0xe8, 0x41, 0xac, 0x62, 0xba, 0x0f, 0x56, 0x99, 0xa4, 0x58, 0x0a, 0x67, 0x47, 0xbd, 0x9a, 0x8c,
0xbe, 0x06, 0x4b, 0x26, 0x6d, 0x8e, 0xd2, 0x99, 0xfa, 0xe4, 0xc8, 0x8e, 0x0e, 0xd9, 0xad, 0x6d,
0x30, 0xd5, 0x96, 0x9d, 0xae, 0x7a, 0x2d, 0x66, 0xcb, 0x9f, 0x87, 0x93, 0xd8, 0x90, 0x02, 0x1f,
0xe0, 0x0c, 0x65, 0x8c, 0x5f, 0x3a, 0x14, 0x72, 0x5b, 0xe3, 0xe0, 0x15, 0xd8, 0x0a, 0x21, 0x1a,
0x5e, 0x0b, 0xa4, 0x4f, 0x61, 0x57, 0x2d, 0x5f, 0x61, 0xec, 0x68, 0x6f, 0xd4, 0x4e, 0x19, 0x88,
0x35, 0x24, 0x38, 0x01, 0x38, 0xef, 0x56, 0xe2, 0xcf, 0xee, 0xc0, 0x34, 0xd3, 0x19, 0xfe, 0x1c,
0xec, 0x8f, 0x85, 0xb8, 0x11, 0x08, 0x16, 0xf0, 0x50, 0xa7, 0x66, 0x94, 0x08, 0x2c, 0x7d, 0x07,
0x0e, 0xf1, 0xa7, 0xff, 0x51, 0x34, 0xc8, 0xe0, 0x09, 0xcc, 0x4f, 0xb1, 0x44, 0x89, 0xff, 0xb0,
0x1c, 0x7d, 0xdb, 0x01, 0x4b, 0x91, 0x05, 0x3d, 0x86, 0xe9, 0x19, 0x4a, 0xfa, 0x68, 0x24, 0xbd,
0xde, 0x99, 0xeb, 0x6e, 0x2b, 0x99, 0x09, 0xdf, 0xc0, 0x6c, 0x98, 0x98, 0x8e, 0x31, 0x1b, 0xae,
0xdc, 0x83, 0xad, 0x35, 0x23, 0xf0, 0x12, 0xa6, 0xe7, 0xdd, 0x9f, 0xed, 0xd7, 0x5b, 0x75, 0xf7,
0x99, 0x3e, 0x6c, 0x76, 0x73, 0xd8, 0xec, 0xdd, 0x70, 0xd8, 0xf4, 0x04, 0x2c, 0x6d, 0x94, 0x3e,
0x1e, 0x91, 0x6f, 0xf9, 0xff, 0x1b, 0x7f, 0xe1, 0x2c, 0xaf, 0xbd, 0xc9, 0x8f, 0x6b, 0x6f, 0xf2,
0xb5, 0xf7, 0xc8, 0xb2, 0xf7, 0xc8, 0xf7, 0xde, 0x23, 0xbf, 0x7a, 0x8f, 0xa4, 0x96, 0x42, 0xbe,
0xf8, 0x1d, 0x00, 0x00, 0xff, 0xff, 0x7b, 0xe2, 0xcf, 0xa0, 0xba, 0x03, 0x00, 0x00,
}

View File

@ -22,20 +22,21 @@ service Images {
// Get returns an image by name.
rpc Get(GetRequest) returns (GetResponse);
// List returns a list of all images known to containerd.
rpc List(ListRequest) returns (ListResponse);
// Put assigns the name to a given target image based on the provided
// image.
rpc Put(PutRequest) returns (google.protobuf.Empty);
// List returns a list of all images known to containerd.
rpc List(ListRequest) returns (ListResponse);
// Delete deletes the image by name.
rpc Delete(DeleteRequest) returns (google.protobuf.Empty);
}
message Image {
string name = 1;
types.Descriptor target = 2 [(gogoproto.nullable) = false];
string labels = 2;
types.Descriptor target = 3 [(gogoproto.nullable) = false];
}
message GetRequest {

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +0,0 @@
syntax = "proto3";
package containerd.v1;
import "gogoproto/gogo.proto";
import "github.com/containerd/containerd/api/types/mount/mount.proto";
import "github.com/containerd/containerd/api/types/descriptor/descriptor.proto";
service RootFS {
rpc Unpack(UnpackRequest) returns (UnpackResponse);
rpc Prepare(PrepareRequest) returns (MountResponse);
rpc Mounts(MountsRequest) returns (MountResponse);
}
message UnpackRequest {
repeated containerd.v1.types.Descriptor layers = 1;
}
message UnpackResponse {
string chainid = 1 [(gogoproto.customtype) = "github.com/opencontainers/go-digest.Digest", (gogoproto.nullable) = false, (gogoproto.customname) = "ChainID"];
}
message PrepareRequest {
string name = 1;
string chain_id = 2 [(gogoproto.customtype) = "github.com/opencontainers/go-digest.Digest", (gogoproto.nullable) = false, (gogoproto.customname) = "ChainID"];
bool readonly = 3;
}
message MountsRequest {
string name = 1;
}
message MountResponse {
repeated containerd.v1.types.Mount mounts = 1;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,81 @@
syntax = "proto3";
package containerd.v1.snapshot;
import "gogoproto/gogo.proto";
import "google/protobuf/empty.proto";
import "github.com/containerd/containerd/api/types/mount/mount.proto";
// Snapshot service manages snapshots
service Snapshot {
rpc Prepare(PrepareRequest) returns (MountsResponse);
rpc View(PrepareRequest) returns (MountsResponse);
rpc Mounts(MountsRequest) returns (MountsResponse);
rpc Commit(CommitRequest) returns (google.protobuf.Empty);
rpc Remove(RemoveRequest) returns (google.protobuf.Empty);
rpc Stat(StatRequest) returns (StatResponse);
rpc List(ListRequest) returns (stream ListResponse);
rpc Usage(UsageRequest) returns (UsageResponse);
// "Snapshot" prepares a new set of mounts from existing name
}
message PrepareRequest {
string key = 1;
string parent = 2;
}
message MountsRequest {
string key = 1;
}
message MountsResponse {
repeated containerd.v1.types.Mount mounts = 1;
}
message RemoveRequest {
string key = 1;
}
message CommitRequest {
string name = 1;
string key = 2;
}
message StatRequest {
string key = 1;
}
enum Kind {
option (gogoproto.goproto_enum_prefix) = false;
option (gogoproto.enum_customname) = "Kind";
ACTIVE = 0 [(gogoproto.enumvalue_customname) = "KindActive"];
COMMITTED = 1 [(gogoproto.enumvalue_customname) = "KindCommitted"];
}
message Info {
string name = 1;
string parent = 2;
Kind kind = 3;
bool readonly = 4;
}
message StatResponse {
Info info = 1 [(gogoproto.nullable) = false];
}
message ListRequest{}
message ListResponse {
repeated Info info = 1 [(gogoproto.nullable) = false];
}
message UsageRequest {
string key = 1;
}
message UsageResponse {
int64 inodes = 2;
int64 size = 1;
}

View File

@ -3,20 +3,26 @@ syntax = "proto3";
package containerd.v1.types;
import "gogoproto/gogo.proto";
import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";
enum Status {
UNKNOWN = 0;
CREATED = 1;
RUNNING = 2;
STOPPED = 3;
PAUSED = 4;
option (gogoproto.goproto_enum_prefix) = false;
option (gogoproto.enum_customname) = "Status";
UNKNOWN = 0 [(gogoproto.enumvalue_customname) = "StatusUnknown"];
CREATED = 1 [(gogoproto.enumvalue_customname) = "StatusCreated"];
RUNNING = 2 [(gogoproto.enumvalue_customname) = "StatusRunning"];
STOPPED = 3 [(gogoproto.enumvalue_customname) = "StatusStopped"];
PAUSED = 4 [(gogoproto.enumvalue_customname) = "StatusPaused"];
}
message Container {
string id = 1 [(gogoproto.customname) = "ID"];
uint32 pid = 2;
Status status = 3;
message Task {
string id = 1; // TODO(stevvooe): For now, this is just the container id.
string container_id = 2;
uint32 pid = 3;
Status status = 4;
google.protobuf.Any spec = 5;
}
message Process {
@ -28,6 +34,7 @@ message Process {
bool terminal = 6;
uint32 exit_status = 7;
Status status = 8;
google.protobuf.Any runtime_data = 9;
}
message User {
@ -37,7 +44,7 @@ message User {
}
message Event {
string id = 1 [(gogoproto.customname) = "ID"];
string id = 1;
enum EventType {
EXIT = 0;

View File

@ -40,13 +40,7 @@ func Diff(ctx context.Context, a, b string) io.ReadCloser {
r, w := io.Pipe()
go func() {
var err error
cw := newChangeWriter(w, b)
if err = fs.Changes(ctx, a, b, cw.HandleChange); err != nil {
err = errors.Wrap(err, "failed to create diff tar stream")
} else {
err = cw.Close()
}
err := WriteDiff(ctx, w, a, b)
if err = w.CloseWithError(err); err != nil {
log.G(ctx).WithError(err).Debugf("closing tar pipe failed")
}
@ -55,6 +49,22 @@ func Diff(ctx context.Context, a, b string) io.ReadCloser {
return r
}
// WriteDiff writes a tar stream of the computed difference between the
// provided directories.
//
// Produces a tar using OCI style file markers for deletions. Deleted
// files will be prepended with the prefix ".wh.". This style is
// based off AUFS whiteouts.
// See https://github.com/opencontainers/image-spec/blob/master/layer.md
func WriteDiff(ctx context.Context, w io.Writer, a, b string) error {
cw := newChangeWriter(w, b)
err := fs.Changes(ctx, a, b, cw.HandleChange)
if err != nil {
return errors.Wrap(err, "failed to create diff tar stream")
}
return cw.Close()
}
const (
// whiteoutPrefix prefix means file is a whiteout. If this is followed by a
// filename this means that file has been removed from the base layer.

View File

@ -122,7 +122,7 @@ func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
func getxattr(path, attr string) ([]byte, error) {
b, err := sysx.LGetxattr(path, attr)
if err == syscall.ENOTSUP || err == syscall.ENODATA {
if err == syscall.ENOTSUP || err == sysx.ENODATA {
return nil, nil
}
return b, err

View File

@ -1,3 +0,0 @@
## containerd Community Code of Conduct
containerd follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).

View File

@ -0,0 +1,24 @@
package containers
import "context"
// Container represents the set of data pinned by a container. Unless otherwise
// noted, the resources here are considered in use by the container.
//
// The resources specified in this object are used to create tasks from the container.
type Container struct {
ID string
Labels map[string]string
Image string
Runtime string
Spec []byte
RootFS string
}
type Store interface {
Get(ctx context.Context, id string) (Container, error)
List(ctx context.Context, filter string) ([]Container, error)
Create(ctx context.Context, container Container) (Container, error)
Update(ctx context.Context, container Container) (Container, error)
Delete(ctx context.Context, id string) error
}

View File

@ -41,6 +41,17 @@ func WriteBlob(ctx context.Context, cs Ingester, ref string, r io.Reader, size i
}
defer cw.Close()
return Copy(cw, r, size, expected)
}
// Copy copies data with the expected digest from the reader into the
// provided content store writer.
//
// This is useful when the digest and size are known beforehand. When
// the size or digest is unknown, these values may be empty.
//
// Copy is buffered, so no need to wrap reader in buffered io.
func Copy(cw Writer, r io.Reader, size int64, expected digest.Digest) error {
ws, err := cw.Status()
if err != nil {
return err
@ -50,7 +61,7 @@ func WriteBlob(ctx context.Context, cs Ingester, ref string, r io.Reader, size i
r, err = seekReader(r, ws.Offset, size)
if err != nil {
if !isUnseekable(err) {
return errors.Wrapf(err, "unabled to resume write to %v", ref)
return errors.Wrapf(err, "unabled to resume write to %v", ws.Ref)
}
// reader is unseekable, try to move the writer back to the start.
@ -69,7 +80,7 @@ func WriteBlob(ctx context.Context, cs Ingester, ref string, r io.Reader, size i
if err := cw.Commit(size, expected); err != nil {
if !IsExists(err) {
return errors.Wrapf(err, "failed commit on ref %q", ref)
return errors.Wrapf(err, "failed commit on ref %q", ws.Ref)
}
}

View File

@ -83,11 +83,11 @@ func compareSysStat(s1, s2 interface{}) (bool, error) {
func compareCapabilities(p1, p2 string) (bool, error) {
c1, err := sysx.LGetxattr(p1, "security.capability")
if err != nil && err != syscall.ENODATA {
if err != nil && err != sysx.ENODATA {
return false, errors.Wrapf(err, "failed to get xattr for %s", p1)
}
c2, err := sysx.LGetxattr(p2, "security.capability")
if err != nil && err != syscall.ENODATA {
if err != nil && err != sysx.ENODATA {
return false, errors.Wrapf(err, "failed to get xattr for %s", p2)
}
return bytes.Equal(c1, c2), nil

View File

@ -0,0 +1,87 @@
// +build linux
package fs
import (
"fmt"
"io/ioutil"
"os"
"syscall"
"unsafe"
)
func locateDummyIfEmpty(path string) (string, error) {
children, err := ioutil.ReadDir(path)
if err != nil {
return "", err
}
if len(children) != 0 {
return "", nil
}
dummyFile, err := ioutil.TempFile(path, "fsutils-dummy")
if err != nil {
return "", err
}
name := dummyFile.Name()
err = dummyFile.Close()
return name, err
}
// SupportsDType returns whether the filesystem mounted on path supports d_type
func SupportsDType(path string) (bool, error) {
// locate dummy so that we have at least one dirent
dummy, err := locateDummyIfEmpty(path)
if err != nil {
return false, err
}
if dummy != "" {
defer os.Remove(dummy)
}
visited := 0
supportsDType := true
fn := func(ent *syscall.Dirent) bool {
visited++
if ent.Type == syscall.DT_UNKNOWN {
supportsDType = false
// stop iteration
return true
}
// continue iteration
return false
}
if err = iterateReadDir(path, fn); err != nil {
return false, err
}
if visited == 0 {
return false, fmt.Errorf("did not hit any dirent during iteration %s", path)
}
return supportsDType, nil
}
func iterateReadDir(path string, fn func(*syscall.Dirent) bool) error {
d, err := os.Open(path)
if err != nil {
return err
}
defer d.Close()
fd := int(d.Fd())
buf := make([]byte, 4096)
for {
nbytes, err := syscall.ReadDirent(fd, buf)
if err != nil {
return err
}
if nbytes == 0 {
break
}
for off := 0; off < nbytes; {
ent := (*syscall.Dirent)(unsafe.Pointer(&buf[off]))
if stop := fn(ent); stop {
return nil
}
off += int(ent.Reclen)
}
}
return nil
}

View File

@ -27,7 +27,7 @@ func diskUsage(roots ...string) (Usage, error) {
}
stat := fi.Sys().(*syscall.Stat_t)
inodes[inode{dev: uint64(stat.Dev), ino: stat.Ino}] = struct{}{}
inodes[inode{dev: uint64(stat.Dev), ino: uint64(stat.Ino)}] = struct{}{}
size += fi.Size()
return nil
}); err != nil {

View File

@ -11,7 +11,17 @@ import (
"golang.org/x/sync/errgroup"
)
var SkipDesc = fmt.Errorf("skip descriptor")
var (
// SkipDesc is used to skip processing of a descriptor and
// its descendants.
SkipDesc = fmt.Errorf("skip descriptor")
// StopHandler is used to signify that the descriptor
// has been handled and should not be handled further.
// This applies only to a single descriptor in a handler
// chain and does not apply to descendant descriptors.
StopHandler = fmt.Errorf("stop handler")
)
type Handler interface {
Handle(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error)
@ -24,12 +34,17 @@ func (fn HandlerFunc) Handle(ctx context.Context, desc ocispec.Descriptor) (subd
}
// Handlers returns a handler that will run the handlers in sequence.
//
// A handler may return `StopHandler` to stop calling additional handlers
func Handlers(handlers ...Handler) HandlerFunc {
return func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) {
var children []ocispec.Descriptor
for _, handler := range handlers {
ch, err := handler.Handle(ctx, desc)
if err != nil {
if errors.Cause(err) == StopHandler {
break
}
return nil, err
}

View File

@ -7,7 +7,6 @@ import (
"io/ioutil"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/log"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
@ -18,6 +17,13 @@ type Image struct {
Target ocispec.Descriptor
}
type Store interface {
Put(ctx context.Context, name string, desc ocispec.Descriptor) error
Get(ctx context.Context, name string) (Image, error)
List(ctx context.Context) ([]Image, error)
Delete(ctx context.Context, name string) error
}
// TODO(stevvooe): Many of these functions make strong platform assumptions,
// which are untrue in a lot of cases. More refactoring must be done here to
// make this work in all cases.
@ -66,25 +72,7 @@ func (image *Image) RootFS(ctx context.Context, provider content.Provider) ([]di
if err != nil {
return nil, err
}
p, err := content.ReadBlob(ctx, provider, desc.Digest)
if err != nil {
log.G(ctx).Fatal(err)
}
var config ocispec.Image
if err := json.Unmarshal(p, &config); err != nil {
log.G(ctx).Fatal(err)
}
// TODO(stevvooe): Remove this bit when OCI structure uses correct type for
// rootfs.DiffIDs.
var diffIDs []digest.Digest
for _, diffID := range config.RootFS.DiffIDs {
diffIDs = append(diffIDs, digest.Digest(diffID))
}
return diffIDs, nil
return RootFS(ctx, provider, desc)
}
// Size returns the total size of an image's packed resources.
@ -123,3 +111,28 @@ func (image *Image) Size(ctx context.Context, provider content.Provider) (int64,
}), image.Target)
}
// RootFS returns the unpacked diffids that make up and images rootfs.
//
// These are used to verify that a set of layers unpacked to the expected
// values.
func RootFS(ctx context.Context, provider content.Provider, desc ocispec.Descriptor) ([]digest.Digest, error) {
p, err := content.ReadBlob(ctx, provider, desc.Digest)
if err != nil {
return nil, err
}
var config ocispec.Image
if err := json.Unmarshal(p, &config); err != nil {
return nil, err
}
// TODO(stevvooe): Remove this bit when OCI structure uses correct type for
// rootfs.DiffIDs.
var diffIDs []digest.Digest
for _, diffID := range config.RootFS.DiffIDs {
diffIDs = append(diffIDs, digest.Digest(diffID))
}
return diffIDs, nil
}

View File

@ -10,4 +10,10 @@ const (
MediaTypeDockerSchema2Config = "application/vnd.docker.container.image.v1+json"
MediaTypeDockerSchema2Manifest = "application/vnd.docker.distribution.manifest.v2+json"
MediaTypeDockerSchema2ManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
// Checkpoint/Restore Media Types
MediaTypeContainerd1Checkpoint = "application/vnd.containerd.container.criu.checkpoint.criu.tar"
MediaTypeContainerd1CheckpointPreDump = "application/vnd.containerd.container.criu.checkpoint.predump.tar"
MediaTypeContainerd1Resource = "application/vnd.containerd.container.resource.tar"
MediaTypeContainerd1RW = "application/vnd.containerd.container.rw.tar"
MediaTypeContainerd1CheckpointConfig = "application/vnd.containerd.container.checkpoint.config.v1+json"
)

View File

@ -1,215 +0,0 @@
package images
import (
"context"
"encoding/binary"
"fmt"
"github.com/boltdb/bolt"
"github.com/containerd/containerd/log"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
var (
ErrExists = errors.New("images: exists")
ErrNotFound = errors.New("images: not found")
)
type Store interface {
Put(ctx context.Context, name string, desc ocispec.Descriptor) error
Get(ctx context.Context, name string) (Image, error)
List(ctx context.Context) ([]Image, error)
Delete(ctx context.Context, name string) error
}
// IsNotFound returns true if the error is due to a missing image.
func IsNotFound(err error) bool {
return errors.Cause(err) == ErrNotFound
}
func IsExists(err error) bool {
return errors.Cause(err) == ErrExists
}
var (
bucketKeyStorageVersion = []byte("v1")
bucketKeyImages = []byte("images")
bucketKeyDigest = []byte("digest")
bucketKeyMediaType = []byte("mediatype")
bucketKeySize = []byte("size")
)
// TODO(stevvooe): This file comprises the data required to implement the
// "metadata" store. For now, it is bound tightly to the local machine and bolt
// but we can take this and use it to define a service interface.
// InitDB will initialize the database for use. The database must be opened for
// write and the caller must not be holding an open transaction.
func InitDB(db *bolt.DB) error {
log.L.Debug("init db")
return db.Update(func(tx *bolt.Tx) error {
_, err := createBucketIfNotExists(tx, bucketKeyStorageVersion, bucketKeyImages)
return err
})
}
func NewImageStore(tx *bolt.Tx) Store {
return &storage{tx: tx}
}
type storage struct {
tx *bolt.Tx
}
func (s *storage) Get(ctx context.Context, name string) (Image, error) {
var image Image
if err := withImageBucket(s.tx, name, func(bkt *bolt.Bucket) error {
image.Name = name
return readImage(&image, bkt)
}); err != nil {
return Image{}, err
}
return image, nil
}
func (s *storage) Put(ctx context.Context, name string, desc ocispec.Descriptor) error {
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
ibkt, err := bkt.CreateBucketIfNotExists([]byte(name))
if err != nil {
return err
}
var (
buf [binary.MaxVarintLen64]byte
sizeEncoded []byte = buf[:]
)
sizeEncoded = sizeEncoded[:binary.PutVarint(sizeEncoded, desc.Size)]
if len(sizeEncoded) == 0 {
return fmt.Errorf("failed encoding size = %v", desc.Size)
}
for _, v := range [][2][]byte{
{bucketKeyDigest, []byte(desc.Digest)},
{bucketKeyMediaType, []byte(desc.MediaType)},
{bucketKeySize, sizeEncoded},
} {
if err := ibkt.Put(v[0], v[1]); err != nil {
return err
}
}
return nil
})
}
func (s *storage) List(ctx context.Context) ([]Image, error) {
var images []Image
if err := withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
var (
image = Image{
Name: string(k),
}
kbkt = bkt.Bucket(k)
)
if err := readImage(&image, kbkt); err != nil {
return err
}
images = append(images, image)
return nil
})
}); err != nil {
return nil, err
}
return images, nil
}
func (s *storage) Delete(ctx context.Context, name string) error {
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
return bkt.DeleteBucket([]byte(name))
})
}
func readImage(image *Image, bkt *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
if v == nil {
return nil // skip it? a bkt maybe?
}
// TODO(stevvooe): This is why we need to use byte values for
// keys, rather than full arrays.
switch string(k) {
case string(bucketKeyDigest):
image.Target.Digest = digest.Digest(v)
case string(bucketKeyMediaType):
image.Target.MediaType = string(v)
case string(bucketKeySize):
image.Target.Size, _ = binary.Varint(v)
}
return nil
})
}
func createBucketIfNotExists(tx *bolt.Tx, keys ...[]byte) (*bolt.Bucket, error) {
bkt, err := tx.CreateBucketIfNotExists(keys[0])
if err != nil {
return nil, err
}
for _, key := range keys[1:] {
bkt, err = bkt.CreateBucketIfNotExists(key)
if err != nil {
return nil, err
}
}
return bkt, nil
}
func withImagesBucket(tx *bolt.Tx, fn func(bkt *bolt.Bucket) error) error {
bkt := getImagesBucket(tx)
if bkt == nil {
return ErrNotFound
}
return fn(bkt)
}
func withImageBucket(tx *bolt.Tx, name string, fn func(bkt *bolt.Bucket) error) error {
bkt := getImageBucket(tx, name)
if bkt == nil {
return ErrNotFound
}
return fn(bkt)
}
func getImagesBucket(tx *bolt.Tx) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyImages)
}
func getImageBucket(tx *bolt.Tx, name string) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyImages, []byte(name))
}
func getBucket(tx *bolt.Tx, keys ...[]byte) *bolt.Bucket {
bkt := tx.Bucket(keys[0])
for _, key := range keys[1:] {
if bkt == nil {
break
}
bkt = bkt.Bucket(key)
}
return bkt
}

View File

@ -0,0 +1,107 @@
package metadata
import (
"github.com/boltdb/bolt"
"github.com/containerd/containerd/log"
)
var (
bucketKeyStorageVersion = []byte("v1")
bucketKeyImages = []byte("images")
bucketKeyContainers = []byte("containers")
bucketKeyDigest = []byte("digest")
bucketKeyMediaType = []byte("mediatype")
bucketKeySize = []byte("size")
bucketKeyLabels = []byte("labels")
bucketKeyImage = []byte("image")
bucketKeyRuntime = []byte("runtime")
bucketKeySpec = []byte("spec")
bucketKeyRootFS = []byte("rootfs")
)
// InitDB will initialize the database for use. The database must be opened for
// write and the caller must not be holding an open transaction.
func InitDB(db *bolt.DB) error {
log.L.Debug("init db")
return db.Update(func(tx *bolt.Tx) error {
if _, err := createBucketIfNotExists(tx, bucketKeyStorageVersion, bucketKeyImages); err != nil {
return err
}
if _, err := createBucketIfNotExists(tx, bucketKeyStorageVersion, bucketKeyContainers); err != nil {
return err
}
return nil
})
}
func getBucket(tx *bolt.Tx, keys ...[]byte) *bolt.Bucket {
bkt := tx.Bucket(keys[0])
for _, key := range keys[1:] {
if bkt == nil {
break
}
bkt = bkt.Bucket(key)
}
return bkt
}
func createBucketIfNotExists(tx *bolt.Tx, keys ...[]byte) (*bolt.Bucket, error) {
bkt, err := tx.CreateBucketIfNotExists(keys[0])
if err != nil {
return nil, err
}
for _, key := range keys[1:] {
bkt, err = bkt.CreateBucketIfNotExists(key)
if err != nil {
return nil, err
}
}
return bkt, nil
}
func withImagesBucket(tx *bolt.Tx, fn func(bkt *bolt.Bucket) error) error {
bkt := getImagesBucket(tx)
if bkt == nil {
return ErrNotFound
}
return fn(bkt)
}
func withImageBucket(tx *bolt.Tx, name string, fn func(bkt *bolt.Bucket) error) error {
bkt := getImageBucket(tx, name)
if bkt == nil {
return ErrNotFound
}
return fn(bkt)
}
func getImagesBucket(tx *bolt.Tx) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyImages)
}
func getImageBucket(tx *bolt.Tx, name string) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyImages, []byte(name))
}
func createContainersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
bkt, err := tx.CreateBucketIfNotExists(bucketKeyStorageVersion)
if err != nil {
return nil, err
}
return bkt.CreateBucketIfNotExists(bucketKeyContainers)
}
func getContainersBucket(tx *bolt.Tx) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyContainers)
}
func getContainerBucket(tx *bolt.Tx, id string) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyContainers, []byte(id))
}

View File

@ -0,0 +1,171 @@
package metadata
import (
"context"
"github.com/boltdb/bolt"
"github.com/containerd/containerd/containers"
"github.com/pkg/errors"
)
type containerStore struct {
tx *bolt.Tx
}
func NewContainerStore(tx *bolt.Tx) containers.Store {
return &containerStore{
tx: tx,
}
}
func (s *containerStore) Get(ctx context.Context, id string) (containers.Container, error) {
bkt := getContainerBucket(s.tx, id)
if bkt == nil {
return containers.Container{}, errors.Wrap(ErrNotFound, "bucket does not exist")
}
container := containers.Container{ID: id}
if err := readContainer(&container, bkt); err != nil {
return containers.Container{}, errors.Wrap(err, "failed to read container")
}
return container, nil
}
func (s *containerStore) List(ctx context.Context, filter string) ([]containers.Container, error) {
var (
m = []containers.Container{}
bkt = getContainersBucket(s.tx)
)
if bkt == nil {
return m, nil
}
err := bkt.ForEach(func(k, v []byte) error {
cbkt := bkt.Bucket(k)
if cbkt == nil {
return nil
}
container := containers.Container{ID: string(k)}
if err := readContainer(&container, cbkt); err != nil {
return errors.Wrap(err, "failed to read container")
}
m = append(m, container)
return nil
})
if err != nil {
return nil, err
}
return m, nil
}
func (s *containerStore) Create(ctx context.Context, container containers.Container) (containers.Container, error) {
bkt, err := createContainersBucket(s.tx)
if err != nil {
return containers.Container{}, err
}
cbkt, err := bkt.CreateBucket([]byte(container.ID))
if err != nil {
if err == bolt.ErrBucketExists {
err = errors.Wrap(ErrExists, "content for id already exists")
}
return containers.Container{}, err
}
if err := writeContainer(&container, cbkt); err != nil {
return containers.Container{}, errors.Wrap(err, "failed to write container")
}
return container, nil
}
func (s *containerStore) Update(ctx context.Context, container containers.Container) (containers.Container, error) {
bkt := getContainersBucket(s.tx)
if bkt == nil {
return containers.Container{}, errors.Wrap(ErrNotFound, "no containers")
}
cbkt := bkt.Bucket([]byte(container.ID))
if cbkt == nil {
return containers.Container{}, errors.Wrap(ErrNotFound, "no content for id")
}
if err := writeContainer(&container, cbkt); err != nil {
return containers.Container{}, errors.Wrap(err, "failed to write container")
}
return container, nil
}
func (s *containerStore) Delete(ctx context.Context, id string) error {
bkt := getContainersBucket(s.tx)
if bkt == nil {
return errors.Wrap(ErrNotFound, "no containers")
}
err := bkt.DeleteBucket([]byte(id))
if err == bolt.ErrBucketNotFound {
return errors.Wrap(ErrNotFound, "no content for id")
}
return err
}
func readContainer(container *containers.Container, bkt *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
switch string(k) {
case string(bucketKeyImage):
container.Image = string(v)
case string(bucketKeyRuntime):
container.Runtime = string(v)
case string(bucketKeySpec):
container.Spec = v
case string(bucketKeyRootFS):
container.RootFS = string(v)
case string(bucketKeyLabels):
lbkt := bkt.Bucket(bucketKeyLabels)
if lbkt == nil {
return nil
}
container.Labels = map[string]string{}
if err := lbkt.ForEach(func(k, v []byte) error {
container.Labels[string(k)] = string(v)
return nil
}); err != nil {
return err
}
}
return nil
})
}
func writeContainer(container *containers.Container, bkt *bolt.Bucket) error {
for _, v := range [][2][]byte{
{bucketKeyImage, []byte(container.Image)},
{bucketKeyRuntime, []byte(container.Runtime)},
{bucketKeySpec, container.Spec},
{bucketKeyRootFS, []byte(container.RootFS)},
} {
if err := bkt.Put(v[0], v[1]); err != nil {
return err
}
}
// Remove existing labels to keep from merging
if lbkt := bkt.Bucket(bucketKeyLabels); lbkt != nil {
if err := bkt.DeleteBucket(bucketKeyLabels); err != nil {
return err
}
}
lbkt, err := bkt.CreateBucket(bucketKeyLabels)
if err != nil {
return err
}
for k, v := range container.Labels {
if err := lbkt.Put([]byte(k), []byte(v)); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,17 @@
package metadata
import "github.com/pkg/errors"
var (
ErrExists = errors.New("metadata: exists")
ErrNotFound = errors.New("metadata: not found")
)
// IsNotFound returns true if the error is due to a missing image.
func IsNotFound(err error) bool {
return errors.Cause(err) == ErrNotFound
}
func IsExists(err error) bool {
return errors.Cause(err) == ErrExists
}

View File

@ -0,0 +1,120 @@
package metadata
import (
"context"
"encoding/binary"
"fmt"
"github.com/boltdb/bolt"
"github.com/containerd/containerd/images"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
type imageStore struct {
tx *bolt.Tx
}
func NewImageStore(tx *bolt.Tx) images.Store {
return &imageStore{tx: tx}
}
func (s *imageStore) Get(ctx context.Context, name string) (images.Image, error) {
var image images.Image
if err := withImageBucket(s.tx, name, func(bkt *bolt.Bucket) error {
image.Name = name
return readImage(&image, bkt)
}); err != nil {
return images.Image{}, err
}
return image, nil
}
func (s *imageStore) Put(ctx context.Context, name string, desc ocispec.Descriptor) error {
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
ibkt, err := bkt.CreateBucketIfNotExists([]byte(name))
if err != nil {
return err
}
var (
buf [binary.MaxVarintLen64]byte
sizeEncoded []byte = buf[:]
)
sizeEncoded = sizeEncoded[:binary.PutVarint(sizeEncoded, desc.Size)]
if len(sizeEncoded) == 0 {
return fmt.Errorf("failed encoding size = %v", desc.Size)
}
for _, v := range [][2][]byte{
{bucketKeyDigest, []byte(desc.Digest)},
{bucketKeyMediaType, []byte(desc.MediaType)},
{bucketKeySize, sizeEncoded},
} {
if err := ibkt.Put(v[0], v[1]); err != nil {
return err
}
}
return nil
})
}
func (s *imageStore) List(ctx context.Context) ([]images.Image, error) {
var m []images.Image
if err := withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
var (
image = images.Image{
Name: string(k),
}
kbkt = bkt.Bucket(k)
)
if err := readImage(&image, kbkt); err != nil {
return err
}
m = append(m, image)
return nil
})
}); err != nil {
return nil, err
}
return m, nil
}
func (s *imageStore) Delete(ctx context.Context, name string) error {
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
err := bkt.DeleteBucket([]byte(name))
if err == bolt.ErrBucketNotFound {
return ErrNotFound
}
return err
})
}
func readImage(image *images.Image, bkt *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
if v == nil {
return nil // skip it? a bkt maybe?
}
// TODO(stevvooe): This is why we need to use byte values for
// keys, rather than full arrays.
switch string(k) {
case string(bucketKeyDigest):
image.Target.Digest = digest.Digest(v)
case string(bucketKeyMediaType):
image.Target.MediaType = string(v)
case string(bucketKeySize):
image.Target.Size, _ = binary.Varint(v)
}
return nil
})
}

View File

@ -1,4 +1,4 @@
package containerd
package mount
// Mount is the lingua franca of containerd. A mount represents a
// serialized mount syscall. Components either emit or consume mounts.

View File

@ -1,4 +1,4 @@
package containerd
package mount
import (
"strings"

View File

@ -1,6 +1,6 @@
// +build darwin freebsd
package containerd
package mount
import "github.com/pkg/errors"

View File

@ -1,4 +1,4 @@
package containerd
package mount
import "github.com/pkg/errors"

View File

@ -0,0 +1,40 @@
package mount
// Info reveals information about a particular mounted filesystem. This
// struct is populated from the content in the /proc/<pid>/mountinfo file.
type Info struct {
// ID is a unique identifier of the mount (may be reused after umount).
ID int
// Parent indicates the ID of the mount parent (or of self for the top of the
// mount tree).
Parent int
// Major indicates one half of the device ID which identifies the device class.
Major int
// Minor indicates one half of the device ID which identifies a specific
// instance of device.
Minor int
// Root of the mount within the filesystem.
Root string
// Mountpoint indicates the mount point relative to the process's root.
Mountpoint string
// Options represents mount-specific options.
Options string
// Optional represents optional fields.
Optional string
// FSType indicates the type of filesystem, such as EXT3.
FSType string
// Source indicates filesystem specific information or "none".
Source string
// VFSOptions represents per super block options.
VFSOptions string
}

View File

@ -0,0 +1,45 @@
package mount
/*
#include <sys/param.h>
#include <sys/ucred.h>
#include <sys/mount.h>
*/
import "C"
import (
"fmt"
"reflect"
"unsafe"
)
// Self retrieves a list of mounts for the current running process.
func Self() ([]Info, error) {
var rawEntries *C.struct_statfs
count := int(C.getmntinfo(&rawEntries, C.MNT_WAIT))
if count == 0 {
return nil, fmt.Errorf("Failed to call getmntinfo")
}
var entries []C.struct_statfs
header := (*reflect.SliceHeader)(unsafe.Pointer(&entries))
header.Cap = count
header.Len = count
header.Data = uintptr(unsafe.Pointer(rawEntries))
var out []Info
for _, entry := range entries {
var mountinfo Info
mountinfo.Mountpoint = C.GoString(&entry.f_mntonname[0])
mountinfo.Source = C.GoString(&entry.f_mntfromname[0])
mountinfo.FSType = C.GoString(&entry.f_fstypename[0])
out = append(out, mountinfo)
}
return out, nil
}
// PID collects the mounts for a specific process ID.
func PID(pid int) ([]Info, error) {
return nil, fmt.Errorf("mountinfo.PID is not implemented on freebsd")
}

View File

@ -0,0 +1,94 @@
// +build linux
package mount
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
const (
/* 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
(1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
(1) mount ID: unique identifier of the mount (may be reused after umount)
(2) parent ID: ID of parent (or of self for the top of the mount tree)
(3) major:minor: value of st_dev for files on filesystem
(4) root: root of the mount within the filesystem
(5) mount point: mount point relative to the process's root
(6) mount options: per mount options
(7) optional fields: zero or more fields of the form "tag[:value]"
(8) separator: marks the end of the optional fields
(9) filesystem type: name of filesystem of the form "type[.subtype]"
(10) mount source: filesystem specific information or "none"
(11) super options: per super block options*/
mountinfoFormat = "%d %d %d:%d %s %s %s %s"
)
// Self retrieves a list of mounts for the current running process.
func Self() ([]Info, error) {
f, err := os.Open("/proc/self/mountinfo")
if err != nil {
return nil, err
}
defer f.Close()
return parseInfoFile(f)
}
func parseInfoFile(r io.Reader) ([]Info, error) {
var (
s = bufio.NewScanner(r)
out = []Info{}
)
for s.Scan() {
if err := s.Err(); err != nil {
return nil, err
}
var (
p = Info{}
text = s.Text()
optionalFields string
)
if _, err := fmt.Sscanf(text, mountinfoFormat,
&p.ID, &p.Parent, &p.Major, &p.Minor,
&p.Root, &p.Mountpoint, &p.Options, &optionalFields); err != nil {
return nil, fmt.Errorf("Scanning '%s' failed: %s", text, err)
}
// Safe as mountinfo encodes mountpoints with spaces as \040.
index := strings.Index(text, " - ")
postSeparatorFields := strings.Fields(text[index+3:])
if len(postSeparatorFields) < 3 {
return nil, fmt.Errorf("Error found less than 3 fields post '-' in %q", text)
}
if optionalFields != "-" {
p.Optional = optionalFields
}
p.FSType = postSeparatorFields[0]
p.Source = postSeparatorFields[1]
p.VFSOptions = strings.Join(postSeparatorFields[2:], " ")
out = append(out, p)
}
return out, nil
}
// PID collects the mounts for a specific process ID. If the process
// ID is unknown, it is better to use `Self` which will inspect
// "/proc/self/mountinfo" instead.
func PID(pid int) ([]Info, error) {
f, err := os.Open(fmt.Sprintf("/proc/%d/mountinfo", pid))
if err != nil {
return nil, err
}
defer f.Close()
return parseInfoFile(f)
}

View File

@ -0,0 +1,43 @@
// +build solaris,cgo
package mount
/*
#include <stdio.h>
#include <sys/mnttab.h>
*/
import "C"
import (
"fmt"
)
// Self retrieves a list of mounts for the current running process.
func Self() ([]Info, error) {
mnttab := C.fopen(C.CString(C.MNTTAB), C.CString("r"))
if mnttab == nil {
return nil, fmt.Errorf("Failed to open %s", C.MNTTAB)
}
var out []Info
var mp C.struct_mnttab
ret := C.getmntent(mnttab, &mp)
for ret == 0 {
var mountinfo Info
mountinfo.Mountpoint = C.GoString(mp.mnt_mountp)
mountinfo.Source = C.GoString(mp.mnt_special)
mountinfo.FSType = C.GoString(mp.mnt_fstype)
mountinfo.Options = C.GoString(mp.mnt_mntopts)
out = append(out, mountinfo)
ret = C.getmntent(mnttab, &mp)
}
C.fclose(mnttab)
return out, nil
}
// PID collects the mounts for a specific process ID.
func PID(pid int) ([]Info, error) {
return nil, fmt.Errorf("mountinfo.PID is not implemented on solaris")
}

View File

@ -0,0 +1,18 @@
// +build !linux,!freebsd,!solaris freebsd,!cgo solaris,!cgo
package mount
import (
"fmt"
"runtime"
)
// Self retrieves a list of mounts for the current running process.
func Self() ([]Info, error) {
return nil, fmt.Errorf("mountinfo.Self is not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
// PID collects the mounts for a specific process ID.
func PID(pid int) ([]Info, error) {
return nil, fmt.Errorf("mountinfo.PID is not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}

View File

@ -1,15 +1,17 @@
package containerd
package plugin
import "golang.org/x/net/context"
import "context"
type ContainerInfo struct {
type TaskInfo struct {
ID string
ContainerID string
Runtime string
Spec []byte
}
type Container interface {
type Task interface {
// Information of the container
Info() ContainerInfo
Info() TaskInfo
// Start the container's user defined process
Start(context.Context) error
// State returns the container's state
@ -19,17 +21,27 @@ type Container interface {
// Resume unpauses the container process
Resume(context.Context) error
// Kill signals a container
Kill(context.Context, uint32, bool) error
Kill(context.Context, uint32, uint32, bool) error
// Exec adds a process into the container
Exec(context.Context, ExecOpts) (Process, error)
// Processes returns all pids for the container
Processes(context.Context) ([]uint32, error)
// Pty resizes the processes pty/console
Pty(context.Context, uint32, ConsoleSize) error
// CloseStdin closes the processes stdin
CloseStdin(context.Context, uint32) error
// Checkpoint checkpoints a container to an image with live system data
Checkpoint(context.Context, CheckpointOpts) error
}
type LinuxContainer interface {
Container
type CheckpointOpts struct {
Exit bool
AllowTCP bool
AllowUnixSockets bool
AllowTerminal bool
FileLocks bool
EmptyNamespaces []string
Path string
}
type ExecOpts struct {

View File

@ -1,4 +1,4 @@
package containerd
package plugin
import "errors"

View File

@ -1,4 +1,4 @@
package containerd
package plugin
import "time"

View File

@ -1,46 +1,44 @@
package plugin
import "github.com/containerd/containerd"
// ContainerMonitor provides an interface for monitoring of containers within containerd
type ContainerMonitor interface {
// TaskMonitor provides an interface for monitoring of containers within containerd
type TaskMonitor interface {
// Monitor adds the provided container to the monitor
Monitor(containerd.Container) error
Monitor(Task) error
// Stop stops and removes the provided container from the monitor
Stop(containerd.Container) error
Stop(Task) error
// Events emits events from the monitor
Events(chan<- *containerd.Event)
Events(chan<- *Event)
}
func NewMultiContainerMonitor(monitors ...ContainerMonitor) ContainerMonitor {
return &multiContainerMonitor{
func NewMultiTaskMonitor(monitors ...TaskMonitor) TaskMonitor {
return &multiTaskMonitor{
monitors: monitors,
}
}
func NewNoopMonitor() ContainerMonitor {
return &noopContainerMonitor{}
func NewNoopMonitor() TaskMonitor {
return &noopTaskMonitor{}
}
type noopContainerMonitor struct {
type noopTaskMonitor struct {
}
func (mm *noopContainerMonitor) Monitor(c containerd.Container) error {
func (mm *noopTaskMonitor) Monitor(c Task) error {
return nil
}
func (mm *noopContainerMonitor) Stop(c containerd.Container) error {
func (mm *noopTaskMonitor) Stop(c Task) error {
return nil
}
func (mm *noopContainerMonitor) Events(events chan<- *containerd.Event) {
func (mm *noopTaskMonitor) Events(events chan<- *Event) {
}
type multiContainerMonitor struct {
monitors []ContainerMonitor
type multiTaskMonitor struct {
monitors []TaskMonitor
}
func (mm *multiContainerMonitor) Monitor(c containerd.Container) error {
func (mm *multiTaskMonitor) Monitor(c Task) error {
for _, m := range mm.monitors {
if err := m.Monitor(c); err != nil {
return err
@ -49,7 +47,7 @@ func (mm *multiContainerMonitor) Monitor(c containerd.Container) error {
return nil
}
func (mm *multiContainerMonitor) Stop(c containerd.Container) error {
func (mm *multiTaskMonitor) Stop(c Task) error {
for _, m := range mm.monitors {
if err := m.Stop(c); err != nil {
return err
@ -58,7 +56,7 @@ func (mm *multiContainerMonitor) Stop(c containerd.Container) error {
return nil
}
func (mm *multiContainerMonitor) Events(events chan<- *containerd.Event) {
func (mm *multiTaskMonitor) Events(events chan<- *Event) {
for _, m := range mm.monitors {
m.Events(events)
}

View File

@ -5,7 +5,6 @@ import (
"sync"
"github.com/boltdb/bolt"
"github.com/containerd/containerd"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/snapshot"
@ -19,7 +18,7 @@ const (
RuntimePlugin PluginType = iota + 1
GRPCPlugin
SnapshotPlugin
ContainerMonitorPlugin
TaskMonitorPlugin
)
type Registration struct {
@ -28,17 +27,17 @@ type Registration struct {
Init func(*InitContext) (interface{}, error)
}
// TODO(@crosbymichael): how to we keep this struct from growing but support dependency injection for loaded plugins?
// TODO(@crosbymichael): how do we keep this struct from growing but support dependency injection for loaded plugins?
type InitContext struct {
Root string
State string
Runtimes map[string]containerd.Runtime
Runtimes map[string]Runtime
Content content.Store
Meta *bolt.DB
Snapshotter snapshot.Snapshotter
Config interface{}
Context context.Context
Monitor ContainerMonitor
Monitor TaskMonitor
}
type Service interface {

View File

@ -1,9 +1,10 @@
package containerd
package plugin
import (
"context"
"time"
"golang.org/x/net/context"
"github.com/containerd/containerd/mount"
)
type IO struct {
@ -17,9 +18,10 @@ type CreateOpts struct {
// Spec is the OCI runtime spec
Spec []byte
// Rootfs mounts to perform to gain access to the container's filesystem
Rootfs []Mount
Rootfs []mount.Mount
// IO for the container's main process
IO IO
Checkpoint string
}
type Exit struct {
@ -31,11 +33,11 @@ type Exit struct {
// arch, or custom usage.
type Runtime interface {
// Create creates a container with the provided id and options
Create(ctx context.Context, id string, opts CreateOpts) (Container, error)
Create(ctx context.Context, id string, opts CreateOpts) (Task, error)
// Containers returns all the current containers for the runtime
Containers(context.Context) ([]Container, error)
Tasks(context.Context) ([]Task, error)
// Delete removes the container in the runtime
Delete(context.Context, Container) (*Exit, error)
Delete(context.Context, Task) (*Exit, error)
// Events returns events for the runtime and all containers created by the runtime
Events(context.Context) <-chan *Event
}

View File

@ -79,20 +79,20 @@ func Parse(s string) (Spec, error) {
return Spec{}, ErrHostnameRequired
}
parts := splitRe.Split(u.Path, 2)
if len(parts) < 2 {
return Spec{}, ErrObjectRequired
}
var object string
if idx := splitRe.FindStringIndex(u.Path); idx != nil {
// This allows us to retain the @ to signify digests or shortend digests in
// the object.
object := u.Path[len(parts[0]):]
object = u.Path[idx[0]:]
if object[:1] == ":" {
object = object[1:]
}
u.Path = u.Path[:idx[0]]
}
return Spec{
Locator: path.Join(u.Host, parts[0]),
Locator: path.Join(u.Host, u.Path),
Object: object,
}, nil
}
@ -119,6 +119,9 @@ func (r Spec) Digest() digest.Digest {
// String returns the normalized string for the ref.
func (r Spec) String() string {
if r.Object == "" {
return r.Locator
}
if r.Object[:1] == "@" {
return fmt.Sprintf("%v%v", r.Locator, r.Object)
}

View File

@ -0,0 +1,78 @@
package docker
import (
"context"
"io"
"net/http"
"path"
"strings"
"github.com/Sirupsen/logrus"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
type dockerFetcher struct {
*dockerBase
}
func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(
logrus.Fields{
"base": r.base.String(),
"digest": desc.Digest,
},
))
paths, err := getV2URLPaths(desc)
if err != nil {
return nil, err
}
for _, path := range paths {
u := r.url(path)
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", strings.Join([]string{desc.MediaType, `*`}, ", "))
resp, err := r.doRequestWithRetries(ctx, req, nil)
if err != nil {
return nil, err
}
if resp.StatusCode > 299 {
if resp.StatusCode == http.StatusNotFound {
continue // try one of the other urls.
}
resp.Body.Close()
return nil, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
}
return resp.Body, nil
}
return nil, errors.New("not found")
}
// getV2URLPaths generates the candidate urls paths for the object based on the
// set of hints and the provided object id. URLs are returned in the order of
// most to least likely succeed.
func getV2URLPaths(desc ocispec.Descriptor) ([]string, error) {
var urls []string
switch desc.MediaType {
case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList,
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
urls = append(urls, path.Join("manifests", desc.Digest.String()))
}
// always fallback to attempting to get the object out of the blobs store.
urls = append(urls, path.Join("blobs", desc.Digest.String()))
return urls, nil
}

View File

@ -0,0 +1,144 @@
package docker
import (
"bytes"
"context"
"io"
"io/ioutil"
"net/http"
"path"
"strings"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
type dockerPusher struct {
*dockerBase
tag string
}
func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor, r io.Reader) error {
var (
isManifest bool
existCheck string
)
switch desc.MediaType {
case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList,
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
isManifest = true
existCheck = path.Join("manifests", desc.Digest.String())
default:
existCheck = path.Join("blobs", desc.Digest.String())
}
req, err := http.NewRequest(http.MethodHead, p.url(existCheck), nil)
if err != nil {
return err
}
req.Header.Set("Accept", strings.Join([]string{desc.MediaType, `*`}, ", "))
resp, err := p.doRequestWithRetries(ctx, req, nil)
if err != nil {
if errors.Cause(err) != ErrInvalidAuthorization {
return err
}
log.G(ctx).WithError(err).Debugf("Unable to check existence, continuing with push")
} else {
if resp.StatusCode == http.StatusOK {
return nil
}
if resp.StatusCode != http.StatusNotFound {
// TODO: log error
return errors.Errorf("unexpected response: %s", resp.Status)
}
}
// TODO: Lookup related objects for cross repository push
if isManifest {
// Read all to use bytes.Reader for using GetBody
b, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrap(err, "failed to read manifest")
}
var putPath string
if p.tag != "" {
putPath = path.Join("manifests", p.tag)
} else {
putPath = path.Join("manifests", desc.Digest.String())
}
req, err := http.NewRequest(http.MethodPut, p.url(putPath), nil)
if err != nil {
return err
}
req.ContentLength = int64(len(b))
req.Body = ioutil.NopCloser(bytes.NewReader(b))
req.GetBody = func() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(b)), nil
}
req.Header.Add("Content-Type", desc.MediaType)
resp, err := p.doRequestWithRetries(ctx, req, nil)
if err != nil {
return err
}
if resp.StatusCode != http.StatusCreated {
// TODO: log error
return errors.Errorf("unexpected response: %s", resp.Status)
}
} else {
// TODO: Do monolithic upload if size is small
// TODO: Turn multi-request blob uploader into ingester
// Start upload request
req, err := http.NewRequest(http.MethodPost, p.url("blobs", "uploads")+"/", nil)
if err != nil {
return err
}
resp, err := p.doRequestWithRetries(ctx, req, nil)
if err != nil {
return err
}
if resp.StatusCode != http.StatusAccepted {
// TODO: log error
return errors.Errorf("unexpected response: %s", resp.Status)
}
location := resp.Header.Get("Location")
// Support paths without host in location
if strings.HasPrefix(location, "/") {
u := p.base
u.Path = location
location = u.String()
}
// TODO: Support chunked upload
req, err = http.NewRequest(http.MethodPut, location, r)
if err != nil {
return err
}
q := req.URL.Query()
q.Add("digest", desc.Digest.String())
req.URL.RawQuery = q.Encode()
req.ContentLength = desc.Size
resp, err = p.doRequest(ctx, req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusCreated {
// TODO: log error
return errors.Errorf("unexpected response: %s", resp.Status)
}
}
return nil
}

View File

@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/textproto"
@ -66,13 +65,169 @@ func NewResolver(options ResolverOptions) remotes.Resolver {
var _ remotes.Resolver = &dockerResolver{}
func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocispec.Descriptor, remotes.Fetcher, error) {
func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocispec.Descriptor, error) {
refspec, err := reference.Parse(ref)
if err != nil {
return "", ocispec.Descriptor{}, nil, err
return "", ocispec.Descriptor{}, err
}
if refspec.Object == "" {
return "", ocispec.Descriptor{}, reference.ErrObjectRequired
}
base, err := r.base(refspec)
if err != nil {
return "", ocispec.Descriptor{}, err
}
fetcher := dockerFetcher{
dockerBase: base,
}
var (
urls []string
dgst = refspec.Digest()
)
if dgst != "" {
if err := dgst.Validate(); err != nil {
// need to fail here, since we can't actually resolve the invalid
// digest.
return "", ocispec.Descriptor{}, err
}
// turns out, we have a valid digest, make a url.
urls = append(urls, fetcher.url("manifests", dgst.String()))
} else {
urls = append(urls, fetcher.url("manifests", refspec.Object))
}
// fallback to blobs on not found.
urls = append(urls, fetcher.url("blobs", dgst.String()))
for _, u := range urls {
req, err := http.NewRequest(http.MethodHead, u, nil)
if err != nil {
return "", ocispec.Descriptor{}, err
}
// set headers for all the types we support for resolution.
req.Header.Set("Accept", strings.Join([]string{
images.MediaTypeDockerSchema2Manifest,
images.MediaTypeDockerSchema2ManifestList,
ocispec.MediaTypeImageManifest,
ocispec.MediaTypeImageIndex, "*"}, ", "))
log.G(ctx).Debug("resolving")
resp, err := fetcher.doRequestWithRetries(ctx, req, nil)
if err != nil {
return "", ocispec.Descriptor{}, err
}
resp.Body.Close() // don't care about body contents.
if resp.StatusCode > 299 {
if resp.StatusCode == http.StatusNotFound {
continue
}
return "", ocispec.Descriptor{}, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
}
// this is the only point at which we trust the registry. we use the
// content headers to assemble a descriptor for the name. when this becomes
// more robust, we mostly get this information from a secure trust store.
dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest"))
if dgstHeader != "" {
if err := dgstHeader.Validate(); err != nil {
return "", ocispec.Descriptor{}, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader)
}
dgst = dgstHeader
}
if dgst == "" {
return "", ocispec.Descriptor{}, errors.Errorf("could not resolve digest for %v", ref)
}
var (
size int64
sizeHeader = resp.Header.Get("Content-Length")
)
size, err = strconv.ParseInt(sizeHeader, 10, 64)
if err != nil {
return "", ocispec.Descriptor{}, errors.Wrapf(err, "invalid size header: %q", sizeHeader)
}
if size < 0 {
return "", ocispec.Descriptor{}, errors.Errorf("%q in header not a valid size", sizeHeader)
}
desc := ocispec.Descriptor{
Digest: dgst,
MediaType: resp.Header.Get("Content-Type"), // need to strip disposition?
Size: size,
}
log.G(ctx).WithField("desc.digest", desc.Digest).Debug("resolved")
return ref, desc, nil
}
return "", ocispec.Descriptor{}, errors.Errorf("%v not found", ref)
}
func (r *dockerResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
refspec, err := reference.Parse(ref)
if err != nil {
return nil, err
}
base, err := r.base(refspec)
if err != nil {
return nil, err
}
return dockerFetcher{
dockerBase: base,
}, nil
}
func (r *dockerResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) {
refspec, err := reference.Parse(ref)
if err != nil {
return nil, err
}
// Manifests can be pushed by digest like any other object, but the passed in
// reference cannot take a digest without the associated content. A tag is allowed
// and will be used to tag pushed manifests.
if refspec.Object != "" && strings.Contains(refspec.Object, "@") {
return nil, errors.New("cannot use digest reference for push locator")
}
base, err := r.base(refspec)
if err != nil {
return nil, err
}
return dockerPusher{
dockerBase: base,
tag: refspec.Object,
}, nil
}
type dockerBase struct {
base url.URL
token string
client *http.Client
useBasic bool
username string
secret string
}
func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
var (
err error
base url.URL
username, secret string
)
@ -93,184 +248,55 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
if r.credentials != nil {
username, secret, err = r.credentials(base.Host)
if err != nil {
return "", ocispec.Descriptor{}, nil, err
return nil, err
}
}
prefix := strings.TrimPrefix(refspec.Locator, host+"/")
base.Path = path.Join("/v2", prefix)
fetcher := &dockerFetcher{
return &dockerBase{
base: base,
client: r.client,
username: username,
secret: secret,
}, nil
}
var (
urls []string
dgst = refspec.Digest()
)
if dgst != "" {
if err := dgst.Validate(); err != nil {
// need to fail here, since we can't actually resolve the invalid
// digest.
return "", ocispec.Descriptor{}, nil, err
}
// turns out, we have a valid digest, make a url.
urls = append(urls, fetcher.url("manifests", dgst.String()))
} else {
urls = append(urls, fetcher.url("manifests", refspec.Object))
}
// fallback to blobs on not found.
urls = append(urls, fetcher.url("blobs", dgst.String()))
for _, u := range urls {
req, err := http.NewRequest(http.MethodHead, u, nil)
if err != nil {
return "", ocispec.Descriptor{}, nil, err
}
// set headers for all the types we support for resolution.
req.Header.Set("Accept", strings.Join([]string{
images.MediaTypeDockerSchema2Manifest,
images.MediaTypeDockerSchema2ManifestList,
ocispec.MediaTypeImageManifest,
ocispec.MediaTypeImageIndex, "*"}, ", "))
log.G(ctx).Debug("resolving")
resp, err := fetcher.doRequest(ctx, req)
if err != nil {
return "", ocispec.Descriptor{}, nil, err
}
resp.Body.Close() // don't care about body contents.
if resp.StatusCode > 299 {
if resp.StatusCode == http.StatusNotFound {
continue
}
return "", ocispec.Descriptor{}, nil, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
}
// this is the only point at which we trust the registry. we use the
// content headers to assemble a descriptor for the name. when this becomes
// more robust, we mostly get this information from a secure trust store.
dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest"))
if dgstHeader != "" {
if err := dgstHeader.Validate(); err != nil {
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader)
}
dgst = dgstHeader
}
if dgst == "" {
return "", ocispec.Descriptor{}, nil, errors.Errorf("could not resolve digest for %v", ref)
}
var (
size int64
sizeHeader = resp.Header.Get("Content-Length")
)
size, err = strconv.ParseInt(sizeHeader, 10, 64)
if err != nil {
return "", ocispec.Descriptor{}, nil, errors.Wrapf(err, "invalid size header: %q", sizeHeader)
}
if size < 0 {
return "", ocispec.Descriptor{}, nil, errors.Errorf("%q in header not a valid size", sizeHeader)
}
desc := ocispec.Descriptor{
Digest: dgst,
MediaType: resp.Header.Get("Content-Type"), // need to strip disposition?
Size: size,
}
log.G(ctx).WithField("desc.digest", desc.Digest).Debug("resolved")
return ref, desc, fetcher, nil
}
return "", ocispec.Descriptor{}, nil, errors.Errorf("%v not found", ref)
}
type dockerFetcher struct {
base url.URL
token string
client *http.Client
useBasic bool
username string
secret string
}
func (r *dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(
logrus.Fields{
"base": r.base.String(),
"digest": desc.Digest,
},
))
paths, err := getV2URLPaths(desc)
if err != nil {
return nil, err
}
for _, path := range paths {
u := r.url(path)
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", strings.Join([]string{desc.MediaType, `*`}, ", "))
resp, err := r.doRequest(ctx, req)
if err != nil {
return nil, err
}
if resp.StatusCode > 299 {
if resp.StatusCode == http.StatusNotFound {
continue // try one of the other urls.
}
resp.Body.Close()
return nil, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
}
return resp.Body, nil
}
return nil, errors.New("not found")
}
func (r *dockerFetcher) url(ps ...string) string {
func (r *dockerBase) url(ps ...string) string {
url := r.base
url.Path = path.Join(url.Path, path.Join(ps...))
return url.String()
}
func (r *dockerFetcher) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
return r.doRequestWithRetries(ctx, req, nil)
func (r *dockerBase) authorize(req *http.Request) {
if r.useBasic {
req.SetBasicAuth(r.username, r.secret)
} else if r.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.token))
}
}
func (r *dockerFetcher) doRequestWithRetries(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Response, error) {
func (r *dockerBase) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String()))
log.G(ctx).WithField("request.headers", req.Header).Debug("fetch content")
log.G(ctx).WithField("request.headers", req.Header).WithField("request.method", req.Method).Debug("Do request")
r.authorize(req)
resp, err := ctxhttp.Do(ctx, r.client, req)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "failed to do request")
}
log.G(ctx).WithFields(logrus.Fields{
"status": resp.Status,
"response.headers": resp.Header,
}).Debug("fetch response received")
return resp, nil
}
func (r *dockerBase) doRequestWithRetries(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Response, error) {
resp, err := r.doRequest(ctx, req)
if err != nil {
return nil, err
}
responses = append(responses, resp)
req, err = r.retryRequest(ctx, req, responses)
@ -283,15 +309,7 @@ func (r *dockerFetcher) doRequestWithRetries(ctx context.Context, req *http.Requ
return resp, err
}
func (r *dockerFetcher) authorize(req *http.Request) {
if r.useBasic {
req.SetBasicAuth(r.username, r.secret)
} else if r.token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.token))
}
}
func (r *dockerFetcher) retryRequest(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Request, error) {
func (r *dockerBase) retryRequest(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Request, error) {
if len(responses) > 5 {
return nil, nil
}
@ -300,19 +318,19 @@ func (r *dockerFetcher) retryRequest(ctx context.Context, req *http.Request, res
log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
for _, c := range parseAuthHeader(last.Header) {
if c.scheme == bearerAuth {
if errStr := c.parameters["error"]; errStr != "" {
// TODO: handle expired case
return nil, errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
if err := invalidAuthorization(c, responses); err != nil {
r.token = ""
return nil, err
}
if err := r.setTokenAuth(ctx, c.parameters); err != nil {
return nil, err
}
return req, nil
return copyRequest(req)
} else if c.scheme == basicAuth {
if r.username != "" && r.secret != "" {
r.useBasic = true
}
return req, nil
return copyRequest(req)
}
}
return nil, nil
@ -322,7 +340,7 @@ func (r *dockerFetcher) retryRequest(ctx context.Context, req *http.Request, res
if strings.Contains(req.URL.Path, "/manifests/") {
// TODO: copy request?
req.Method = http.MethodGet
return req, nil
return copyRequest(req)
}
}
@ -330,6 +348,42 @@ func (r *dockerFetcher) retryRequest(ctx context.Context, req *http.Request, res
return nil, nil
}
func invalidAuthorization(c challenge, responses []*http.Response) error {
errStr := c.parameters["error"]
if errStr == "" {
return nil
}
n := len(responses)
if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) {
return nil
}
return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
}
func sameRequest(r1, r2 *http.Request) bool {
if r1.Method != r2.Method {
return false
}
if *r1.URL != *r2.URL {
return false
}
return true
}
func copyRequest(req *http.Request) (*http.Request, error) {
ireq := *req
if ireq.GetBody != nil {
var err error
ireq.Body, err = ireq.GetBody()
if err != nil {
return nil, err
}
}
return &ireq, nil
}
func isManifestAccept(h http.Header) bool {
for _, ah := range h[textproto.CanonicalMIMEHeaderKey("Accept")] {
switch ah {
@ -346,7 +400,7 @@ func isManifestAccept(h http.Header) bool {
return false
}
func (r *dockerFetcher) setTokenAuth(ctx context.Context, params map[string]string) error {
func (r *dockerBase) setTokenAuth(ctx context.Context, params map[string]string) error {
realm, ok := params["realm"]
if !ok {
return errors.New("no realm specified for token auth challenge")
@ -387,24 +441,6 @@ func (r *dockerFetcher) setTokenAuth(ctx context.Context, params map[string]stri
return nil
}
// getV2URLPaths generates the candidate urls paths for the object based on the
// set of hints and the provided object id. URLs are returned in the order of
// most to least likely succeed.
func getV2URLPaths(desc ocispec.Descriptor) ([]string, error) {
var urls []string
switch desc.MediaType {
case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList,
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
urls = append(urls, path.Join("manifests", desc.Digest.String()))
}
// always fallback to attempting to get the object out of the blobs store.
urls = append(urls, path.Join("blobs", desc.Digest.String()))
return urls, nil
}
type tokenOptions struct {
realm string
service string
@ -419,7 +455,7 @@ type postTokenResponse struct {
Scope string `json:"scope"`
}
func (r *dockerFetcher) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
func (r *dockerBase) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
form := url.Values{}
form.Set("scope", strings.Join(to.scopes, " "))
form.Set("service", to.service)
@ -473,7 +509,7 @@ type getTokenResponse struct {
}
// getToken fetches a token using a GET request
func (r *dockerFetcher) getToken(ctx context.Context, to tokenOptions) (string, error) {
func (r *dockerBase) getToken(ctx context.Context, to tokenOptions) (string, error) {
req, err := http.NewRequest("GET", to.realm, nil)
if err != nil {
return "", err

View File

@ -56,11 +56,41 @@ func FetchHandler(ingester content.Ingester, fetcher Fetcher) images.HandlerFunc
func fetch(ctx context.Context, ingester content.Ingester, fetcher Fetcher, desc ocispec.Descriptor) error {
log.G(ctx).Debug("fetch")
ref := MakeRefKey(ctx, desc)
cw, err := ingester.Writer(ctx, ref, desc.Size, desc.Digest)
if err != nil {
if !content.IsExists(err) {
return err
}
return nil
}
defer cw.Close()
rc, err := fetcher.Fetch(ctx, desc)
if err != nil {
return err
}
defer rc.Close()
return content.WriteBlob(ctx, ingester, ref, rc, desc.Size, desc.Digest)
return content.Copy(cw, rc, desc.Size, desc.Digest)
}
func PushHandler(provider content.Provider, pusher Pusher) images.HandlerFunc {
return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(logrus.Fields{
"digest": desc.Digest,
"mediatype": desc.MediaType,
"size": desc.Size,
}))
log.G(ctx).Debug("push")
r, err := provider.Reader(ctx, desc.Digest)
if err != nil {
return nil, err
}
defer r.Close()
return nil, pusher.Push(ctx, desc, r)
}
}

View File

@ -7,7 +7,7 @@ import (
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// Resolver provides a remote based on a locator.
// Resolver provides remotes based on a locator.
type Resolver interface {
// Resolve attempts to resolve the reference into a name and descriptor.
//
@ -20,7 +20,15 @@ type Resolver interface {
// While the name may differ from ref, it should itself be a valid ref.
//
// If the resolution fails, an error will be returned.
Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, fetcher Fetcher, err error)
Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, err error)
// Fetcher returns a new fetcher for the provided reference.
// All content fetched from the returned fetcher will be
// from the namespace referred to by ref.
Fetcher(ctx context.Context, ref string) (Fetcher, error)
// Pusher returns a new pusher for the provided reference
Pusher(ctx context.Context, ref string) (Pusher, error)
}
type Fetcher interface {
@ -28,6 +36,12 @@ type Fetcher interface {
Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error)
}
type Pusher interface {
// Push pushes the resource identified by the descriptor using the
// passed in reader.
Push(ctx context.Context, d ocispec.Descriptor, r io.Reader) error
}
// FetcherFunc allows package users to implement a Fetcher with just a
// function.
type FetcherFunc func(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error)
@ -35,3 +49,11 @@ type FetcherFunc func(ctx context.Context, desc ocispec.Descriptor) (io.ReadClos
func (fn FetcherFunc) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
return fn(ctx, desc)
}
// PusherFunc allows package users to implement a Pusher with just a
// function.
type PusherFunc func(ctx context.Context, desc ocispec.Descriptor, r io.Reader) error
func (fn PusherFunc) Pusher(ctx context.Context, desc ocispec.Descriptor, r io.Reader) error {
return fn(ctx, desc, r)
}

View File

@ -2,14 +2,10 @@ package rootfs
import (
"context"
"io"
"io/ioutil"
"os"
"fmt"
"github.com/containerd/containerd"
"github.com/containerd/containerd/archive"
"github.com/containerd/containerd/archive/compression"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/snapshot"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/identity"
@ -17,134 +13,72 @@ import (
"github.com/pkg/errors"
)
type Unpacker interface {
Unpack(ctx context.Context, layers []ocispec.Descriptor) (digest.Digest, error)
type Applier interface {
Apply(context.Context, ocispec.Descriptor, []mount.Mount) (ocispec.Descriptor, error)
}
type Mounter interface {
Mount(target string, mounts ...containerd.Mount) error
Unmount(target string) error
type Layer struct {
Diff ocispec.Descriptor
Blob ocispec.Descriptor
}
// ApplyLayer applies the layer to the provided parent. The resulting snapshot
// will be stored under its ChainID.
//
// The parent *must* be the chainID of the parent layer.
//
// The returned digest is the diffID for the applied layer.
func ApplyLayer(snapshots snapshot.Snapshotter, mounter Mounter, rd io.Reader, parent digest.Digest) (digest.Digest, error) {
ctx := context.TODO()
// create a temporary directory to work from, needs to be on same
// filesystem. Probably better if this shared but we'll use a tempdir, for
// now.
dir, err := ioutil.TempDir("", "unpack-")
if err != nil {
return "", errors.Wrapf(err, "creating temporary directory failed")
}
defer os.RemoveAll(dir)
// TODO(stevvooe): Choose this key WAY more carefully. We should be able to
// create collisions for concurrent, conflicting unpack processes but we
// would need to have it be a function of the parent diffID and child
// layerID (since we don't know the diffID until we are done!).
key := dir
mounts, err := snapshots.Prepare(ctx, key, parent.String())
if err != nil {
func ApplyLayers(ctx context.Context, layers []Layer, sn snapshot.Snapshotter, a Applier) (digest.Digest, error) {
var chain []digest.Digest
for _, layer := range layers {
if err := applyLayer(ctx, layer, chain, sn, a); err != nil {
// TODO: possibly wait and retry if extraction of same chain id was in progress
return "", err
}
if err := mounter.Mount(dir, mounts...); err != nil {
if err := snapshots.Remove(ctx, key); err != nil {
log.L.WithError(err).Error("snapshot rollback failed")
chain = append(chain, layer.Diff.Digest)
}
return "", err
}
defer mounter.Unmount(dir)
rd, err = compression.DecompressStream(rd)
if err != nil {
return "", err
return identity.ChainID(chain), nil
}
digester := digest.Canonical.Digester() // used to calculate diffID.
rd = io.TeeReader(rd, digester.Hash())
if _, err := archive.Apply(context.Background(), dir, rd); err != nil {
return "", err
}
diffID := digester.Digest()
chainID := diffID
if parent != "" {
chainID = identity.ChainID([]digest.Digest{parent, chainID})
}
if _, err := snapshots.Stat(ctx, chainID.String()); err == nil {
return diffID, snapshots.Remove(ctx, key)
}
return diffID, snapshots.Commit(ctx, chainID.String(), key)
}
// Prepare the root filesystem from the set of layers. Snapshots are created
// for each layer if they don't exist, keyed by their chain id. If the snapshot
// already exists, it will be skipped.
//
// If successful, the chainID for the top-level layer is returned. That
// identifier can be used to check out a snapshot.
func Prepare(ctx context.Context, snapshots snapshot.Snapshotter, mounter Mounter, layers []ocispec.Descriptor,
// TODO(stevvooe): The following functions are candidate for internal
// object functions. We can use these to formulate the beginnings of a
// rootfs Controller.
//
// Just pass them in for now.
openBlob func(context.Context, digest.Digest) (io.ReadCloser, error),
resolveDiffID func(digest.Digest) digest.Digest,
registerDiffID func(diffID, dgst digest.Digest) error) (digest.Digest, error) {
func applyLayer(ctx context.Context, layer Layer, chain []digest.Digest, sn snapshot.Snapshotter, a Applier) error {
var (
parent digest.Digest
chain []digest.Digest
parent = identity.ChainID(chain)
chainID = identity.ChainID(append(chain, layer.Diff.Digest))
diff ocispec.Descriptor
)
for _, layer := range layers {
// This will convert a possibly compressed layer hash to the
// uncompressed hash, if we know about it. If we don't, we unpack and
// calculate it. If we do have it, we then calculate the chain id for
// the application and see if the snapshot is there.
diffID := resolveDiffID(layer.Digest)
if diffID != "" {
chainLocal := append(chain, diffID)
chainID := identity.ChainID(chainLocal)
if _, err := snapshots.Stat(ctx, chainID.String()); err == nil {
continue
}
_, err := sn.Stat(ctx, chainID.String())
if err == nil {
log.G(ctx).Debugf("Extraction not needed, layer snapshot exists")
return nil
} else if !snapshot.IsNotExist(err) {
return errors.Wrap(err, "failed to stat snapshot")
}
rc, err := openBlob(ctx, layer.Digest)
key := fmt.Sprintf("extract %s", chainID)
// Prepare snapshot with from parent
mounts, err := sn.Prepare(ctx, key, parent.String())
if err != nil {
return "", err
//TODO: If is snapshot exists error, retry
return errors.Wrap(err, "failed to prepare extraction layer")
}
defer rc.Close() // pretty lazy!
diffID, err = ApplyLayer(snapshots, mounter, rc, parent)
defer func() {
if err != nil {
return "", err
log.G(ctx).WithError(err).WithField("key", key).Infof("Apply failure, attempting cleanup")
if rerr := sn.Remove(ctx, key); rerr != nil {
log.G(ctx).WithError(rerr).Warnf("Extraction snapshot %q removal failed: %v", key)
}
}
}()
diff, err = a.Apply(ctx, layer.Blob, mounts)
if err != nil {
return errors.Wrapf(err, "failed to extract layer %s", layer.Diff.Digest)
}
if diff.Digest != layer.Diff.Digest {
err = errors.Errorf("wrong diff id calculated on extraction %q", diff.Digest)
return err
}
// Register the association between the diffID and the layer's digest.
// For uncompressed layers, this will be the same. For compressed
// layers, we can look up the diffID from the digest if we've already
// unpacked it.
if err := registerDiffID(diffID, layer.Digest); err != nil {
return "", err
if err = sn.Commit(ctx, chainID.String(), key); err != nil {
return errors.Wrapf(err, "failed to commit snapshot %s", parent)
}
chain = append(chain, diffID)
parent = identity.ChainID(chain)
}
return parent, nil
return nil
}

52
vendor/github.com/containerd/containerd/rootfs/diff.go generated vendored Normal file
View File

@ -0,0 +1,52 @@
package rootfs
import (
"context"
"fmt"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/snapshot"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
type MountDiffer interface {
DiffMounts(ctx context.Context, lower, upper []mount.Mount, media, ref string) (ocispec.Descriptor, error)
}
type DiffOptions struct {
MountDiffer
content.Store
snapshot.Snapshotter
}
func Diff(ctx context.Context, snapshotID, contentRef string, sn snapshot.Snapshotter, md MountDiffer) (ocispec.Descriptor, error) {
info, err := sn.Stat(ctx, snapshotID)
if err != nil {
return ocispec.Descriptor{}, err
}
lowerKey := fmt.Sprintf("%s-parent-view", info.Parent)
lower, err := sn.View(ctx, lowerKey, info.Parent)
if err != nil {
return ocispec.Descriptor{}, err
}
defer sn.Remove(ctx, lowerKey)
var upper []mount.Mount
if info.Kind == snapshot.KindActive {
upper, err = sn.Mounts(ctx, snapshotID)
if err != nil {
return ocispec.Descriptor{}, err
}
} else {
upperKey := fmt.Sprintf("%s-view", snapshotID)
upper, err = sn.View(ctx, upperKey, snapshotID)
if err != nil {
return ocispec.Descriptor{}, err
}
defer sn.Remove(ctx, lowerKey)
}
return md.DiffMounts(ctx, lower, upper, ocispec.MediaTypeImageLayer, contentRef)
}

View File

@ -6,8 +6,8 @@ import (
"io/ioutil"
"os"
"github.com/containerd/containerd"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/snapshot"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
@ -19,7 +19,12 @@ var (
type initializerFunc func(string) error
func InitRootFS(ctx context.Context, name string, parent digest.Digest, readonly bool, snapshotter snapshot.Snapshotter, mounter Mounter) ([]containerd.Mount, error) {
type Mounter interface {
Mount(target string, mounts ...mount.Mount) error
Unmount(target string) error
}
func InitRootFS(ctx context.Context, name string, parent digest.Digest, readonly bool, snapshotter snapshot.Snapshotter, mounter Mounter) ([]mount.Mount, error) {
_, err := snapshotter.Stat(ctx, name)
if err == nil {
return nil, errors.Errorf("rootfs already exists")

View File

@ -19,6 +19,7 @@ func (rr *remoteReader) Read(p []byte) (n int, err error) {
}
return
}
rr.extra = rr.extra[:0]
p = p[n:]
for len(p) > 0 {

View File

@ -0,0 +1,75 @@
package diff
import (
"context"
diffapi "github.com/containerd/containerd/api/services/diff"
"github.com/containerd/containerd/api/types/descriptor"
mounttypes "github.com/containerd/containerd/api/types/mount"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/rootfs"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
type DiffService interface {
rootfs.Applier
rootfs.MountDiffer
}
// NewApplierFromClient returns a new Applier which communicates
// over a GRPC connection.
func NewDiffServiceFromClient(client diffapi.DiffClient) DiffService {
return &remote{
client: client,
}
}
type remote struct {
client diffapi.DiffClient
}
func (r *remote) Apply(ctx context.Context, diff ocispec.Descriptor, mounts []mount.Mount) (ocispec.Descriptor, error) {
req := &diffapi.ApplyRequest{
Diff: fromDescriptor(diff),
Mounts: fromMounts(mounts),
}
resp, err := r.client.Apply(ctx, req)
if err != nil {
return ocispec.Descriptor{}, err
}
return toDescriptor(resp.Applied), nil
}
func (r *remote) DiffMounts(ctx context.Context, a, b []mount.Mount, media, ref string) (ocispec.Descriptor, error) {
req := &diffapi.DiffRequest{
Left: fromMounts(a),
Right: fromMounts(b),
MediaType: media,
Ref: ref,
}
resp, err := r.client.Diff(ctx, req)
if err != nil {
return ocispec.Descriptor{}, err
}
return toDescriptor(resp.Diff), nil
}
func fromDescriptor(d ocispec.Descriptor) *descriptor.Descriptor {
return &descriptor.Descriptor{
MediaType: d.MediaType,
Digest: d.Digest,
Size_: d.Size,
}
}
func fromMounts(mounts []mount.Mount) []*mounttypes.Mount {
apiMounts := make([]*mounttypes.Mount, len(mounts))
for i, m := range mounts {
apiMounts[i] = &mounttypes.Mount{
Type: m.Type,
Source: m.Source,
Options: m.Options,
}
}
return apiMounts
}

View File

@ -0,0 +1,203 @@
package diff
import (
"io"
"io/ioutil"
"os"
diffapi "github.com/containerd/containerd/api/services/diff"
"github.com/containerd/containerd/api/types/descriptor"
mounttypes "github.com/containerd/containerd/api/types/mount"
"github.com/containerd/containerd/archive"
"github.com/containerd/containerd/archive/compression"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/snapshot"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
func init() {
plugin.Register("diff-grpc", &plugin.Registration{
Type: plugin.GRPCPlugin,
Init: func(ic *plugin.InitContext) (interface{}, error) {
return newService(ic.Content, ic.Snapshotter)
},
})
}
type service struct {
store content.Store
snapshotter snapshot.Snapshotter
}
func newService(store content.Store, snapshotter snapshot.Snapshotter) (*service, error) {
return &service{
store: store,
snapshotter: snapshotter,
}, nil
}
func (s *service) Register(gs *grpc.Server) error {
diffapi.RegisterDiffServer(gs, s)
return nil
}
func (s *service) Apply(ctx context.Context, er *diffapi.ApplyRequest) (*diffapi.ApplyResponse, error) {
desc := toDescriptor(er.Diff)
// TODO: Check for supported media types
mounts := toMounts(er.Mounts)
dir, err := ioutil.TempDir("", "extract-")
if err != nil {
return nil, errors.Wrap(err, "failed to create temporary directory")
}
defer os.RemoveAll(dir)
if err := mount.MountAll(mounts, dir); err != nil {
return nil, errors.Wrap(err, "failed to mount")
}
defer mount.Unmount(dir, 0)
r, err := s.store.Reader(ctx, desc.Digest)
if err != nil {
return nil, errors.Wrap(err, "failed to get reader from content store")
}
defer r.Close()
// TODO: only decompress stream if media type is compressed
ds, err := compression.DecompressStream(r)
if err != nil {
return nil, err
}
defer ds.Close()
digester := digest.Canonical.Digester()
rc := &readCounter{
r: io.TeeReader(ds, digester.Hash()),
}
if _, err := archive.Apply(ctx, dir, rc); err != nil {
return nil, err
}
// Read any trailing data
if _, err := io.Copy(ioutil.Discard, rc); err != nil {
return nil, err
}
resp := &diffapi.ApplyResponse{
Applied: &descriptor.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digester.Digest(),
Size_: rc.c,
},
}
return resp, nil
}
func (s *service) Diff(ctx context.Context, dr *diffapi.DiffRequest) (*diffapi.DiffResponse, error) {
aMounts := toMounts(dr.Left)
bMounts := toMounts(dr.Right)
aDir, err := ioutil.TempDir("", "left-")
if err != nil {
return nil, errors.Wrap(err, "failed to create temporary directory")
}
defer os.RemoveAll(aDir)
bDir, err := ioutil.TempDir("", "right-")
if err != nil {
return nil, errors.Wrap(err, "failed to create temporary directory")
}
defer os.RemoveAll(bDir)
if err := mount.MountAll(aMounts, aDir); err != nil {
return nil, errors.Wrap(err, "failed to mount")
}
defer mount.Unmount(aDir, 0)
if err := mount.MountAll(bMounts, bDir); err != nil {
return nil, errors.Wrap(err, "failed to mount")
}
defer mount.Unmount(bDir, 0)
cw, err := s.store.Writer(ctx, dr.Ref, 0, "")
if err != nil {
return nil, errors.Wrap(err, "failed to open writer")
}
// TODO: Validate media type
// TODO: Support compressed media types (link compressed to uncompressed)
//dgstr := digest.SHA256.Digester()
//wc := &writeCounter{}
//compressed, err := compression.CompressStream(cw, compression.Gzip)
//if err != nil {
// return nil, errors.Wrap(err, "failed to get compressed stream")
//}
//err = archive.WriteDiff(ctx, io.MultiWriter(compressed, dgstr.Hash(), wc), lowerDir, upperDir)
//compressed.Close()
err = archive.WriteDiff(ctx, cw, aDir, bDir)
if err != nil {
return nil, errors.Wrap(err, "failed to write diff")
}
dgst := cw.Digest()
if err := cw.Commit(0, dgst); err != nil {
return nil, errors.Wrap(err, "failed to commit")
}
info, err := s.store.Info(ctx, dgst)
if err != nil {
return nil, errors.Wrap(err, "failed to get info from content store")
}
desc := ocispec.Descriptor{
MediaType: dr.MediaType,
Digest: info.Digest,
Size: info.Size,
}
return &diffapi.DiffResponse{
Diff: fromDescriptor(desc),
}, nil
}
type readCounter struct {
r io.Reader
c int64
}
func (rc *readCounter) Read(p []byte) (n int, err error) {
n, err = rc.r.Read(p)
rc.c += int64(n)
return
}
func toDescriptor(d *descriptor.Descriptor) ocispec.Descriptor {
return ocispec.Descriptor{
MediaType: d.MediaType,
Digest: d.Digest,
Size: d.Size_,
}
}
func toMounts(apim []*mounttypes.Mount) []mount.Mount {
mounts := make([]mount.Mount, len(apim))
for i, m := range apim {
mounts[i] = mount.Mount{
Type: m.Type,
Source: m.Source,
Options: m.Options,
}
}
return mounts
}

View File

@ -4,6 +4,7 @@ import (
imagesapi "github.com/containerd/containerd/api/services/images"
"github.com/containerd/containerd/api/types/descriptor"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/metadata"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"google.golang.org/grpc"
@ -67,9 +68,9 @@ func rewriteGRPCError(err error) error {
switch grpc.Code(errors.Cause(err)) {
case codes.AlreadyExists:
return images.ErrExists
return metadata.ErrExists
case codes.NotFound:
return images.ErrNotFound
return metadata.ErrNotFound
}
return err
@ -77,9 +78,9 @@ func rewriteGRPCError(err error) error {
func mapGRPCError(err error, id string) error {
switch {
case images.IsNotFound(err):
case metadata.IsNotFound(err):
return grpc.Errorf(codes.NotFound, "image %v not found", id)
case images.IsExists(err):
case metadata.IsExists(err):
return grpc.Errorf(codes.AlreadyExists, "image %v already exists", id)
}

View File

@ -4,6 +4,7 @@ import (
"github.com/boltdb/bolt"
imagesapi "github.com/containerd/containerd/api/services/images"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/metadata"
"github.com/containerd/containerd/plugin"
"github.com/golang/protobuf/ptypes/empty"
"golang.org/x/net/context"
@ -73,7 +74,7 @@ func (s *Service) Delete(ctx context.Context, req *imagesapi.DeleteRequest) (*em
}
func (s *Service) withStore(ctx context.Context, fn func(ctx context.Context, store images.Store) error) func(tx *bolt.Tx) error {
return func(tx *bolt.Tx) error { return fn(ctx, images.NewImageStore(tx)) }
return func(tx *bolt.Tx) error { return fn(ctx, metadata.NewImageStore(tx)) }
}
func (s *Service) withStoreView(ctx context.Context, fn func(ctx context.Context, store images.Store) error) error {

View File

@ -1,39 +0,0 @@
package rootfs
import (
"context"
rootfsapi "github.com/containerd/containerd/api/services/rootfs"
containerd_v1_types "github.com/containerd/containerd/api/types/descriptor"
"github.com/containerd/containerd/rootfs"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func NewUnpackerFromClient(client rootfsapi.RootFSClient) rootfs.Unpacker {
return remoteUnpacker{
client: client,
}
}
type remoteUnpacker struct {
client rootfsapi.RootFSClient
}
func (rp remoteUnpacker) Unpack(ctx context.Context, layers []ocispec.Descriptor) (digest.Digest, error) {
pr := rootfsapi.UnpackRequest{
Layers: make([]*containerd_v1_types.Descriptor, len(layers)),
}
for i, l := range layers {
pr.Layers[i] = &containerd_v1_types.Descriptor{
MediaType: l.MediaType,
Digest: l.Digest,
Size_: l.Size,
}
}
resp, err := rp.client.Unpack(ctx, &pr)
if err != nil {
return "", err
}
return resp.ChainID, nil
}

Some files were not shown because too many files have changed in this diff Show More